Mobile 플로팅 구현

1. iOS Player WebView

샘플 코드

iOS PIP 샘플 소스 코드

플로팅뷰 개발과 Bridge를 이용한 앱과 웹 간의 통신하기

애플의 PIP 기능과 무관하며, 커스텀 된 플로팅 뷰 개발이 선행되어야 합니다.

이를 기반으로 앱과 웹 간의 특정한 값을 주고 받아서 값을 해결해야 하는 경우가 발생합니다.

이때 필요한 것 Bridge 호출하는 방식입니다. 현재 소스라이브 플레이어는 Bridge 개발이 완료되어 있습니다.

이를 활용하여 앱에서는 WebView 간의 연동 구현을 하게 됩니다.

플로팅 커스텀뷰 구현

  • 앱 내 플레이어 실행 시 애플에서 제공해주는 PIP 기능과 유사하게 커스텀 된 플로팅뷰 기반으로 작동합니다.
  • 모비두 서비스에는 PIPKit(https://github.com/Kofktu/PIPKit) 이라는 오픈소스를 활용해 플로팅 기능을 구현했습니다. 위 오픈소스가 표준은 아니며, 플로팅 기능은 자유롭게 구현이 가능합니다.
  • PIPKit에 대해 아래 이미지를 참고 바랍니다.

Bridge & Message


extension ViewController: WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController,
                                    didReceive message: WKScriptMessage) {
        switch (message.name) {
            case "sauceflexEnter":   // 처음 플레이어 진입시
                print("sauceflexEnter")
                break
 
            case "sauceflexMoveExit":   // 닫기 버튼 눌러 팝업에서 나가기시
                print("sauceflexMoveExit")
                break
 
            case "sauceflexMoveLogin":   // 로그인 팝업에서 확이시
                print("sauceflexMoveLogin")
                break
 
            case "sauceflexMoveProduct":   // 상품 클릭시
                print("sauceflexMoveProduct")
                print(message.body)
                break
 
            case "sauceflexOnShare":   // 공유하기
                print("sauceflexOnShare")
                print(message.body)
                break
 
            case "sauceflexPictureInPicture":   // PIP 전환 버튼 클릭시, PIP 보기 전환시 보여주고 있는 컴포넌트 GONE 처리를 자동으로 처리합니다. (좋아요 버튼 등 플레이어 위에 보여지는 버튼들)
                print("sauceflexPictureInPicture")
                print(message.body)
                break
            
              // case "sauceflexPictureInPictureNoEvent":   // PIP 전환 버튼 클릭시, PIP 보기 전환시 보여주고 있는 플레이어 컴포넌트에 대해서는 직접적으로 GONE 처리 event 스크립트를 호출해야 합니다.
                //print("sauceflexPictureInPictureNoEvent")
                //print(message.body)
                //break
 
            // * PIP 버튼 브릿지 연동시에는 sauceflexPictureInPicture 또는 sauceflexPictureInPictureNoEvent 중 하나만 선언하여 사용해야 합니다.
 
            case "sauceflexMoveBanner":   // 배너 클릭시 bannerId (배너 고유 아이디), linkUrl (배너에 등록된 URL), broadcastIdx (방송 번호)
                print("sauceflexMoveBanner")
                print(message.body)
                break
 
            default:
                print("message.name \\(message.name) not handled.")
        }
    }
}

모드 전환 시 Event 넘겨주기

다음 과정으로 PIP 모드 전환 시, 또는 PIP 모드 해제 시 각각 WebView에 정보를 전달해야 WebView Player 는 화면에 그려지는 컴포넌트를 GONE/VISIBLE 처리가 가능하게 됩니다.

이를 위해 아래와 같이 모드 전환 시 event를 넘겨줘야 합니다.

  • 플레이어 전체 보기에서 PIP 보기 전환 시
if #available(iOS 14.2, *) {
    self.playerView.player?.pictureInPictureController?.canStartPictureInPictureAutomaticallyFromInline = true
//    해당 포인트가 PIP 모드 전환 시나리오 이면,자바스크립트 호출, WKWebView 의 evaluateJavaScript 함수를 이용한다.
        // native call -> js (이건 언제든지 가능)
    myWebView?.evaluateJavascript ("(function() { window.dispatchEvent(sauceflexPictureInPictureOn); })();" ) { }
}
  • 플레이어 PIP 보기에서 전체 보기 전환 시
if isActive == true {
    self.playerView.player?.pictureInPictureController?.stopPictureInPicture()
//    해당 포인트가 PIP 모드에서 Full 보기 모드 전환 시나리오 이면,자바스크립트 호출, WKWebView 의 evaluateJavaScript 함수를 이용한다.
//    주석 처리된 방식으로 구현해도 됩니다.
//    let myJsFuncName = "mergeAppUserInfo('\(myUserid)','\(myVersion!)','ios','\(iosVersion)')"
        // native call -> js (이건 언제든지 가능)
//        mainWebView.evaluateJavaScript(myJsFuncName) { (result, error) in
//            if let result = result{
//                print(result)
//            }
//        }
    myWebView?.evaluateJavascript( "(function() { window.dispatchEvent(sauceflexPictureInPictureOff); })();" ) { }
}

2. Android Player WebView

샘플 코드

Android PIP 샘플 소스 코드

PIP 모드 소개

Android 8.0(API 수준 26)에서는 활동을 PIP 모드로 실행할 수 있습니다. PIP는 주로 동영상 재생에 사용되는 특수한 유형의 멀티 윈도우 모드입니다. 사용자가 기본 화면에서 앱 간에 이동하거나 콘텐츠를 탐색할 때 화면 모서리에 고정된 작은 창에서 동영상을 볼 수 있습니다.
PIP는 Android 7.0에서 사용할 수 있는 멀티 윈도우 API를 활용하여 고정 동영상 오버레이 창을 제공합니다. PIP를 앱에 추가하려면 PIP를 지원하는 활동을 등록하고 필요한 경우 활동을 PIP 모드로 전환하며 활동이 PIP 모드일 때 UI 요소를 숨기고 동영상 재생이 지속되게 해야 합니다.
PIP 창이 화면 맨 윗 부분에서 시스템이 선택한 모서리에 나타납니다. PIP 창을 다른 위치로 드래그 할 수 있습니다. 창을 탭하면 두 개의 특수 제어가 표시됩니다. 전체 화면 전환(창 가운데)과 닫기 버튼(오른쪽 상단 모서리의 'X')입니다.

앱에서 현재 활동이 PIP 모드로 전환되는 시점을 제어합니다. 다음은 몇 가지 예입니다.

  • 사용자가 홈 또는 최근 버튼을 탭하여 다른 앱을 선택하면 활동이 PIP 모드로 전환될 수 있습니다. 이것은 사용자가 다른 활동을 동시에 실행하는 동안에도 Google 지도에서 계속 경로를 표시하는 방법과 같은 방법입니다.
  • 사용자가 동영상을 보다가 다른 콘텐츠를 탐색할 때 동영상을 PIP 모드로 전환할 수 있습니다.
  • 사용자가 콘텐츠 에피소드의 끝을 시청하는 동안 동영상을 PIP 모드로 전환할 수 있습니다. 기본 화면에는 시리즈의 다음 에피소드에 관한 홍보 또는 요약 정보가 표시됩니다.
  • 사용자가 동영상을 보는 동안 앱이 추가 콘텐츠를 대기열에 올리는 방법을 제공할 수 있습니다. 동영상이 PIP 모드로 계속 재생되는 동안 기본 화면에 콘텐츠 선택 활동이 표시됩니다.

PIP 버튼 노출 설정 방법

플레이어 내에서 PIP 버튼을 노출하는 방법은 2가지가 있습니다.

  1. 파트너사 설정 값에 모비두가 PIP 버튼을 노출하도록 설정하는 방법입니다. 파트너사별 Config 설정 (ex, Login URL 등록, Close 버튼 이동 URL 등록 등)을 할 때 PIP 버튼 노출을 등록합니다.
    이 경우에는 파트너사의 앱 업데이트와 동기화 하는 문제가 발생될 수 있습니다. 그 이유는, PIP 버튼 노출의 경우 배포와 동시에 노출하는 방식이다 보니, 파트너사의 앱 업데이트와 함께 배포되어야 하는 경우에는 문제가 될 수 있습니다.
  2. 파트너사에서 직접 PIP 버튼 노출 처리를 할 수 있습니다. 이 경우에는 직접 노출 관리를 하기 때문에 좀 더 쉽게 직접 관리가 가능 합니다. (아래 설명 참조)
  • 앱과 Webview 간의 JavaScript 통신 방법 구현
    해당 Event 처리 시점은 sauceflexEnter 시점에 아래와 같이 PIP 버튼 노출을 처리하시면 됩니다.
// PIP 사용 true 설정시 PIP 버튼 노출
myWebView?.evaluateJavascript(
                    "(function() { window.dispatchEvent(sauceflexPictureInPictureUse(true)); })();"
                ) { }
// PIP 사용 false 설정시 PIP 버튼 미노출
myWebView?.evaluateJavascript(
                    "(function() { window.dispatchEvent(sauceflexPictureInPictureUse(false)); })();"
                ) { }

3. Native App내 구현

Native Player 구현

본격적으로 시스템에서는 앱의 PIP를 자동으로 지원하지 않습니다. 앱에서 PIP를 지원하려면 android:supportsPictureInPicturetrue로 설정하여 매니페스트에 동영상 활동을 등록해야 합니다.

또한 PIP 모드 전환 중에 레이아웃이 변경될 때 활동이 다시 시작되지 않도록 활동이 레이아웃 구성 변경을 처리하도록 지정합니다.

PIP 모드 설정

  • 매니페스트의 액티비티 컴포넌트에 속성 추가
<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    ...
  • PIP 모드 활성화
var pipWidth = 900
var pipHeight = 1600
 
var pipBuilder = PictureInPictureParams.Builder()
pipBuilder.setAspectRatio(Rational(pipWidth, pipHeight))
(activity as BaseVideoActivity).enterPictureInPictureMode(pipBuilder.build())
  • PIP 모드 상태 변화 리스너
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
   super.onPictureInPictureModeChanged(isInPictureInPictureMode)
   if (isInPictureInPictureMode) {
      normalToPip()
   } else {
      pipToNormal()
   }
}

PIP 모드에서 단일 재생 활동 사용 설정 (VOD/방송 액티비티 중복 호출 방지)

앱에서 동영상 재생 활동이 PIP 모드인 동안 사용자가 기본 화면에서 콘텐츠를 탐색할 때 새 동영상을 선택하는 경우가 있습니다. 사용자를 혼란스럽게 할 수 있는 새로운 활동을 실행하는 대신 기존 재생 활동의 새 동영상을 전체 화면 모드로 재생합니다.
동영상 재생 요청에 단일 활동을 사용하고 필요에 따라 PIP 모드로 전환해 들어가거나 나오려면 매니페스트에서 활동의 android:launchModesingleTask로 설정합니다.

  • 매니페스트의 액티비티 컴포넌트에 다음 속성을 추가
android:launchMode= "singleTask"

백스택에 남아있는 App 지우기 설정

홈키를 길게 누를 때 나오는 Recent List 에 실행시키는 Activity가 나오지 않게 하기 위한 설정입니다.

  • 매니페스트의 액티비티 컴포넌트에 다음 속성을 추가
android:excludeFromRecents= "true"

PIP 모드 생명 주기

  • 일반 화면에서 PIP화면으로 : Running → onPause
  • PIP화면 지우기 : onPause → onStop → onDestroy
  • PIP화면에서 일반 화면으로 : onPause → onResume → Running

WebView Player 구현 (Bridge 이용한 App과 Web의 통신하기)

앱과 웹 사이의 특정한 값을 주고 받아서 값을 해결해야 하는 경우가 있습니다. 이때 필요 것이 Bridge 호출하는 방식입니다.

현재 소스라이브 플레이어는 Bridge 개발이 완료되어 있습니다. 이를 활용하여 앱에서는 WebView 간의 연동 구현을 하게 됩니다.

먼저, JavascriptInterface를 추가해야 합니다. interface name 은 "sauceflex" 로 선언해 주세요.

Bridge & Message


class AndroidBridge(private val activity: MainActivity) {
    private val handler = Handler()
  
    @JavascriptInterface   // 처음 플레이어 진입시
    fun sauceflexEnter() {
        handler.post {
            Toast.makeText(activity, "sauceflexEnter", Toast.LENGTH_SHORT).show()
        }
    }
  
    @JavascriptInterface   // 닫기 버튼 눌러 팝업에서 나가기시
    fun sauceflexMoveExit() {
        handler.post {
            Toast.makeText(activity, "sauceflexMoveExit", Toast.LENGTH_SHORT).show()
        }
    }
 
    @JavascriptInterface   // 로그인 팝업에서 확인시
    fun sauceflexMoveLogin() {
        handler.post {
            Toast.makeText(activity, "sauceflexMoveLogin", Toast.LENGTH_SHORT).show()
        }
    }
  
    @JavascriptInterface   // 상품 클릭시
    fun sauceflexMoveProduct(message: String) {
        handler.post {
            Toast.makeText(activity, "sauceflexMoveProduct \\n $message", Toast.LENGTH_SHORT).show()
        }
    }
 
    @JavascriptInterface   // 공유하기
    fun sauceflexOnShare(message: String) {
        handler.post {
            Toast.makeText(activity, "sauceflexOnShare \\n $message", Toast.LENGTH_SHORT).show()
        }
    }
  
   @JavascriptInterface   // PIP 전환 버튼 클릭시, PIP 보기 전환시, 보여주고 있는 컴포넌트 GONE 처리를 자동으로 처리 합니다. (GONE 내용 ex: 좋아요 및 각정 플레이어 위에 띄워지는 버튼들)
   fun sauceflexPictureInPicture () {
       handler.post {
           Toast.makeText(activity, "sauceflexPictureInPicture", Toast.LENNGTH_SHORT).show()
       }
    }
  
    //@JavascriptInterface   // PIP 전환 버튼 클릭시, PIP 보기 전환시, PIP 보기 전환시 보여주고 있는 플레이어 컴포넌트에 대해서는 직접 GONE 처리 event 스크립트를 호출해야 합니다.
   //fun sauceflexPictureInPictureNoEvent () {
       //handler.post {
           //Toast.makeText(activity, "sauceflexPictureInPictureNoEvent", Toast.LENNGTH_SHORT).show()
       //}
    //}
   //* PIP 버튼 브릿지 연동시에는 sauceflexPictureInPicture () 또는 sauceflexPictureInPictureNoEvent () 중 하나만 선언하여 사용해야 합니다.
 
    @JavascriptInterface   // 배너 클릭시 bannerId (배너 고유 아이디), linkUrl (배너에 등록된 URL), broadcastIdx (방송 번호)
    fun sauceflexMoveBanner(message: String) {
        handler.post {
            Toast.makeText(activity, "sauceflexMoveBanner \\n $message", Toast.LENGTH_SHORT).show()
        }
    }
}
  

모드 전환시 Event 넘겨주기

다음 단계로 PIP 모드 전환 시, 혹은 PIP 모드 해제 시 각각 WebView에 정보를 전달해야 WebView Player 는 화면에 그려지는 컴포넌트를 GONE/VISIBLE 처리가 가능하게 됩니다. 이를 위해 아래와 같이 모드 전환시 event 를 넘겨줘야 합니다.

  • 플레이어 전체 보기에서 PIP 보기 전환시 (sauceflexPictureInPictureOn 호출은 "sauceflexPictureInPictureNoEvent" Bridge를 통해 PIP 전환 구현시에만 호출해 주면 됩니다.)
    sauceflexPictureInPicture 호출을 통해 구현되면 sauceflexPictureInPictureOn 은 event를 넘기지 않아도 컴포넌트가 자동으로 GONE/VISIBLE 처리가 가능합니다.
fun minimize() {
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      myWebView?.evaluateJavascript ("(function() { window.dispatchEvent(sauceflexPictureInPictureOn); })();" ) { }
   //…
   var pipBuilder = dPictureInPictureParams.Builder()
       .setAspectRatio(Rational(900, 1600))
       .build()
   enterfPictureInPictureMode (pipBuilder)
   }
}
  • 플레이어 PIP 보기에서 전체 보기 전환 시
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {   // PIP 보기에서 FULL 보기 전환시
   super.onPictureInPictureModeChanged(isInPictureInPictureMode)
       if (isInPictureInPictureMode) {
           normalToPip()
       } else {
          myWebView?.evaluateJavascript( "(function() { window.dispatchEvent(sauceflexPictureInPictureOff); })();" ) { }
          pipToNormal()
       }
}

iFrame Player 구현 (Bridge 이용한 App과 iFrame의 통신하기)

내용을 토대로 자바스크립트 통신(dispatchEvent)시도 시 에러가 발생되는 경우 아래의 방법으로 앱과 iFrame 간의 통신 연결을 해주세요.

  • case : 파트너사 플레이어의 iframe으로 개발되면서 window.dispatchEvent의 window 가 파트너사 웹의 window 에서 문제 발생 시
const iframe = document.querySelector('#id') // ==> node ID
iframe.contentWindow.postMessage('sauceflexPictureInPictureOn','*') // pip 모드 on
iframe.contentWindow.postMessage('sauceflexPictureInPictureOff','*') // pip 모드 off
 
// * querySelector 관련 자료 : https://developer.mozilla.org/ko/docs/Web/API/Document/querySelector

PIP 기본 시나리오

PIP 동작에 대한 영상을 업로드 하였습니다. 개발 시 참고하세요.

  1. 방송/VOD 재생 도중 홈버튼을 누르면 PIP모드 활성화
  2. 방송/VOD 재생 도중 뒤로가기 버튼을 누르면 PIP모드 활성화
  3. 방송/VOD 재생 도중 판매 상품을 누르면 PIP모드 활성화