Guides

Android 연동 가이드

소스라이브 플레이어 Android WebView 연동 가이드

라이브 URL로 접근했을 때 보여지는 페이지이며, 라이브를 시청할 수 있습니다.

라이브 전에는 편성표로 이동하고, 라이브가 시작되었다면 바로 플레이어로 이동합니다.

안드로이드 WebView에 연동 방법

  • 로그인 페이지 URL과 소스라이브 페이지로 복귀하는 방식을 제공합니다.
  • WebView에 플레이어 URL을 추가합니다.

자사몰 로그인 페이지 URL

안드로이드 브릿지 통신 방법

프로젝트 설정

STEP 1. Android Studio를 사용하여 새로운 프로젝트를 생성합니다.

STEP 2. AndroidManifest.xml에 Bridge사용을 위한 권한을 추가합니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

WebView 설정

STEP 1. activity_webview.xmlWebView를 추가합니다.

 <WebView
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

STEP 2. WebViewActivity.java 또는 WebViewActivity.kt에서 브릿지를 설정합니다.

MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        webView = findViewById(R.id.webview)

        // JavaScript 활성화
        webView.settings.javaScriptEnabled = true

        // JavaScript 인터페이스 추가
        webView.addJavascriptInterface(WebAppInterface(this), "sauceflex")

        // Player Url 로드
        webView.loadUrl("https://player.sauceflex.com/broadcast/{방송주소}?accessToken={생성된accessToken}")
    }

    class WebAppInterface(private val mContext: Context) {

            // 라이브 입장 시 콜백 처리 - 페이지 진입 시 자동으로 호출
        @JavascriptInterface
        fun sauceflexBroadcastStatus() {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 라이브 상태 변경 시 콜백 처리 - 라이브 상태 변경 발생 시 자동으로 호출
        @JavascriptInterface
        fun sauceflexBroadcastStatus(message: String) {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 나가기 시 콜백 처리 
        @JavascriptInterface
        fun sauceflexMoveExit() {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 로그인 시 콜백 처리
        @JavascriptInterface
        fun sauceflexMoveLogin() {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 상품 클릭 시 콜백 처리
        @JavascriptInterface   
        fun sauceflexMoveProduct(message: String) {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 배너 클릭 시 콜백 처리
        @JavascriptInterface
        fun sauceflexMoveBanner(message: String) {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 쿠폰 클릭 시 콜백 처리
        @JavascriptInterface
        fun sauceflexMoveCoupon(message: String) {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

        // 리워드 완료 시 콜백 처리
        @JavascriptInterface
        fun sauceflexMoveReward(message: String) {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }    

        // 공유하기 클릭 시 콜백 처리
        @JavascriptInterface
        fun sauceflexOnShare(message: String) {
            handler.post {
                // 처리 코드를 여기에 추가
            }
        }

       // PIP 전환 버튼 클릭 시 콜백 처리 (PIP 전환 시에 플레이어 위 요소들은 자동으로 미표시됩니다.)
       @JavascriptInterface   
       fun sauceflexPictureInPicture () {
           handler.post {
               // 처리 코드를 여기에 추가
           }
        }

        // 웹뷰 리로딩
        @JavascriptInterface   
        fun sauceflexWebviewReloading() {
            handler.post {
                 // 처리 코드를 여기에 추가
            }
        }
    }
}

class WebViewActivity extends AppCompatActivity {

    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        webView = findViewById(R.id.webview);

        // JavaScript 활성화
        webView.getSettings().setJavaScriptEnabled(true);

        // JavaScript 인터페이스 추가
        webView.addJavascriptInterface(new WebAppInterface(this), "sauceflex");

        // Asset 폴더의 HTML 파일 로드
        webView.loadUrl("https://player.sauceflex.com/broadcast/{방송주소}?accessToken={생성된accessToken}&returnUrl={로그인페이지등}");
    }

    public class WebAppInterface {
        Context mContext;

        WebAppInterface(Context c) {
            mContext = c;
        }

            // 방송 입장 시 콜백 처리 - 페이지 진입 시 자동으로 호출
        @JavascriptInterface
        public void sauceflexEnter() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 방송 상태 변경 시 콜백 처리 - 방송 상태 변경 발생 시 자동으로 호출
        @JavascriptInterface
        public void sauceflexBroadcastStatus(String message) {
            final String finalMessage = message;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 나가기 시 콜백 처리 
        @JavascriptInterface
        public void sauceflexMoveExit() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 로그인 시 콜백 처리
        @JavascriptInterface
        public void sauceflexMoveLogin() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 상품 클릭 시 콜백 처리
        @JavascriptInterface
        public void sauceflexMoveProduct(String message) {
            final String finalMessage = message;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 배너 클릭 시 콜백 처리
        @JavascriptInterface
        public void sauceflexMoveBanner(String message) {
            final String finalMessage = message;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 쿠폰 클릭 시 콜백 처리
        @JavascriptInterface
        public void sauceflexMoveCoupon(String message) {
            final String finalMessage = message;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 리워드 완료 시 콜백 처리
        @JavascriptInterface
        public void sauceflexMoveReward(String message) {
            final String finalMessage = message;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }    

        // 공유하기 클릭 시 콜백 처리
        @JavascriptInterface
        public void sauceflexOnShare(String message) {
            final String finalMessage = message;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // PIP 전환 버튼 클릭 시 콜백 처리 (PIP 전환 시에 플레이어 위 요소들은 자동으로 미표시됩니다.)
        @JavascriptInterface
        public void sauceflexPictureInPicture() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }

        // 웹뷰 리로딩
        @JavascriptInterface
        public void sauceflexWebviewReloading() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    // 처리 코드를 여기에 추가
                }
            });
        }
    }
}


안드로이드 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 버튼을 노출하는 방법은 2가지가 있습니다.

컨피그(Config)로 설정

파트너사 설정 값에 모비두가 PIP 버튼을 노출하도록 설정하는 방법입니다.
파트너사별 Config 설정 (ex, Login URL 등록, Close 버튼 이동 URL 등록 등)을 할 때 PIP 버튼 노출을 요청합니다.

직접 설정

파트너사에서 직접 PIP 버튼을 노출 처리 할 수 있으며, 직접 관리하기 때문에 쉽게 설정할 수 있습니다.
(아래 설명 참조)

  • 앱과 Webview 간의 JavaScript 통신 방법 구현
    해당 Event 처리 시점은 sauceflexEnter 시점에 아래와 같이 PIP 버튼 노출을 처리합니다.
  • 웹뷰에 플레이어가 로되는 시간이 길어지는 경우 sauceflexEnter 시점에 javascript를 호출해도 적용이 안되는 경우가 발생할 수 있습니다. 해당 케이스의 경우 Javascript의 호출에 딜레이를 주어 적용을 확인합니다.
// PIP 사용 true 설정시 PIP 버튼 노출
myWebView?.evaluateJavascript( "(function() { window.dispatchEvent(sauceflexPictureInPictureUse(true)); })();") { }
// PIP 사용 false 설정시 PIP 버튼 미노출
myWebView?.evaluateJavascript( "(function() { window.dispatchEvent(sauceflexPictureInPictureUse(false)); })();") { }

PIP 모드 진입 방법

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

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

PIP 모드 설정

AndroidManifest.xml에 액티비티 컴포넌트에 속성 추가

<activity android:name="WebViewActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    ...

PIP 모드 활성화

  • 플레이어에서 PIP 모드 버튼으로 진입 - sauceflexPictureInPicture 브릿지에서 호출
@JavascriptInterface   // PIP 전환 버튼 클릭시, PIP 보기 전환시, 보여주고 있는 컴포넌트 GONE 처리를 자동으로 처리 합니다. (GONE 내용 ex: 좋아요 및 각정 플레이어 위에 띄워지는 버튼들)
   fun sauceflexPictureInPicture () {
       handler.post {
           if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
            val pipBuilder = PictureInPictureParams.Builder()

            pipBuilder.apply {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    setAutoEnterEnabled(true)
                    setSeamlessResizeEnabled(false) // 리사이징 시 깜빡이는 현상 개선
                }
                setAspectRatio(Rational(9,16)) // 9:16 비율로 PIP 뷰 생성
            }
            enterPictureInPictureMode(pipBuilder.build())
					}
       }
    }

  • 사용자의 홈키 동작 등 백그라운드로 이동 시 진입
override fun onUserLeaveHint() {
        super.onUserLeaveHint()

        if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
            val pipBuilder = PictureInPictureParams.Builder()

            pipBuilder.apply {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    setAutoEnterEnabled(true)
                    setSeamlessResizeEnabled(false) // 리사이징 시 깜빡이는 현상 개선
                }
                setAspectRatio(Rational(9,16)) // 9:16 비율로 PIP 뷰 생성
            }
            enterPictureInPictureMode(pipBuilder.build())
        }
    }

PIP 모드 플레이어 UI 숨김 처리

sauceflexPictureInPicture 브릿지를 통해 PIP 모드에 진입하는 경우에는 자동으로 UI가 숨김 처리 되지만, 이 외의 진입 시에는 UI 숨김 처리를 위한 Javascript 호출이 필요합니다.

override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean,
        newConfig: Configuration
    ) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)

        if(isInPictureInPictureMode) {
            binding.webView.evaluateJavascript("dispatchEvent(window.sauceflexPictureInPictureOn);") {
                // do something
            }
        } else {
            binding.webView.evaluateJavascript("dispatchEvent(window.sauceflexPictureInPictureOff);") {
                // do something
            }
        }
    }

❗️

PIP 모드 리사이징 이슈

PIP 모드는 안드로이드에서 제공하는 기본 시스템뷰이기 때문에 기본 동작들은 시스템에 의해 동작합니다.

두 번 터치 혹은 핀치 줌인/줌아웃 기능 동작 시 PIP 모드가 리사이징되면서 웹뷰의 깜빡이는 현상은 setSeamlessResizeEnabled(false) 호출로 개선 가능합니다.

해당 옵션은 안드로이드 버전 12 이상에서 지원하기 때문에, 이하 버전의 대응이 필요한 경우 PIP 모드를 시스템뷰가 아닌 커스텀 뷰를 윈도우에 띄우는 방식등으로 커스텀이 필요합니다.


공유하기 기능 구현 방법

안드로이드 공유하기 기본 시스템 뷰 구현

sauceflexOnShare 브릿지를 통해 공유하기 버튼 기능 구현

class WebAppInterface(private val context: Context) {
    private val handler = Handler()
    ...
  
    @JavascriptInterface   // 공유하기
    fun sauceflexOnShare(message: String) {
        handler.post {
            try {
                // message 형식 - Json String
              	val jsonObject = JSONObject(message)
                val shareURL = jsonObject.getString("linkUrl")
         		    // shortUrl 사용 시 아래 코드로 대체 가능
                // val shareURL = jsonObject.getString("shortUrl")
                val intent = Intent(Intent.ACTION_SEND).apply {
                    type = "text/plain"
                    putExtra(Intent.EXTRA_TEXT, shareURL)
                }
                context.startActivity(Intent.createChooser(intent, "Saucelive Player")) //Saucelive Player 대신 공유하기 제목 입력
            } catch (e: Exception) {
                e.printStackTrace()
                // 오류 처리
            }
        }
    }

   ...
}

public class WebAppInterface {
    private Context context;
    private Handler handler;

    // Constructor to initialize the context and handler
    public AndroidBridge(Context context) {
        this.context = context;
        this.handler = new Handler();
    }
  ...
  // 공유하기
    @JavascriptInterface
    public void sauceflexOnShare(String message) {
        final String finalMessage = message;
        handler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    // message 형식 - Json String
                    JSONObject jsonObject = new JSONObject(message);
                    String shareURL = jsonObject.getString("linkUrl");
                    // shortUrl 사용 시 아래 코드로 대체 가능
                    // String shareURL = jsonObject.getString("shortUrl");
                    Intent intent = new Intent(Intent.ACTION_SEND);
                    intent.setType("text/plain");
                    intent.putExtra(Intent.EXTRA_TEXT, shareURL);
                    context.startActivity(Intent.createChooser(intent, "Share via"));
                } catch (Exception e) {
                    e.printStackTrace();
                    // 오류 처리
                }
            }
        });
    }
  ...
}
linkUrl 사용

linkUrl 사용 ( 설정한 문구로 시스템 뷰에 노출 )

shortUrl 사용

shortUrl 사용 ( 라이브 방송의 제목으로 노출 )

📘

WebView에서 플레이어 연동 시 참고 사항

  • Android Webview에서 재생 아이콘 표시 없이 영상이 재생되는 것을 원하실 경우에는 아래 코드를 활용해주시기 바랍니다. (해당 가이드는 파트너사 AOS앱 개발팀에서 진행)
    override fun getDefaultVideoPoster(): Bitmap? { return Bitmap.createBitmap(10,10, Bitmap.Config.ARGB_8888) }