androidandroid-webviewprogressive-web-appslive-streamingexoplayer2.x

How can I get livestreams to play in an android WebView on a FireTV 4k Max


For multiple days now, I've tried to get livestreams running in a WebView on my FireTV stick. I am completely stuck and would appreciate any help I can get.

The website shown works perfectly in the standard browser on my test device. I've already managed to get past all the DRM problems and am able to hear the encrypted sound. But visually, there is nothing. I was unable to find any relevant resources about this. The guides always stop at the point where the media plays. And I'm probably missing some important keyword when googling because I can't actually find a similar case.

I tried to work around this issue and started running my livestreams in an exoplayer instance that sits behind my WebView. That works perfectly fine. But then I noticed that the video elements in the WebView start playing with visuals after the .prepare() function on the exoplayer instance has been called. So I checked out the newest version of the exoplayer. Sadly, I wasn't able to identify any specific action that the exoplayer performs to enable proper playback in the WebView. Do you have any idea what might be missing?

    <RelativeLayout>
        <FrameLayout/>
        <WebView/>
    </RelativeLayout>
    class MyWebChromeClient : WebChromeClient() {
        override fun onPermissionRequest(request: PermissionRequest?) {
            request?.let {
                it.grant(it.resources)
            }
        }
    }
    
    class MyWebViewClient() : WebViewClient() {
        override fun onReceiveSslError(view: WebView?, handler: SslErrorHandler, error: SslError?) {
            handler.proceed()
        }
        override fun shouldOverrideUrlLoading(view: WebView, url: String?): Boolean {
            view.loadUrl(url!!)
            return true
        }
    }
    
    class MainActivity : ComponentActivity() {
        private lateinit var myWebView: WebView
        private lateinit var exoPlayer: ExoPlayer
        private lateinit var belowFrame: FrameLayout
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            belowFrame = findViewById(R.id.exo_below) as FrameLayout
            myWebView = findViewById(R.id.webview) as WebView
    
            exoPlayer = ExoPlayer.Builder(this).build()
            val belowView = PlayerView(this)
            belowView.player = exoPlayer
    
            belowFrame.addView(belowView)
    
            myWebView.setWebViewClient(MyWebViewClient())
            myWebView.setWebChromeClient(MyWebChromeClient())
    
            myWebView.settings.javaScriptEnabled = true
            myWebView.settings.javaScriptCanOpenWindowsAutomatically = true
            myWebView.settings.allowContentAccess = true
            myWebView.settings.domStorageEnabled = true
            myWebView.settings.mediaPlaybackRequiresUserGesture = false
            myWebView.settings.allowFileAccess = true
            myWebView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
            myWebView.setInitialScale(100)
    
            myWebView.loadUrl("localhost:3000")
    
            myWebView.requestFocus()
        }
        override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                myWebView.evaluateJavascript("document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 8 }))", null)
                return true
            }
        }
    
        // this is called via javascript interface
        fun onVideoStart(videoUrl: String) {
            runBlocking(Dispatchers.Main.immedate)
            {
                val
                mediaSource = buildMediaSource("http://mcdn.daserste.de/daserste/dash/manifest.mpd")
                exoPlayer.setMediaSource(mediaSource)
                exoPlayer.prepare() // <= this step makes WebView videos play as expected
            }
        }
        fun buildMediaSource(videoUrl: String): MediaSource {
            val dataSourceFactory = DefaultHttpDataSource.Factory()
            return DashMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(videoUrl))
        }
    }

I've added the folling permissions to the manifest: android.permission.INTERNET android.permission.ACCESS_NETWORK_STATE android.permission.RESOURCE_PROTECTED_MEDIA_ID`

minSdk = 25
targetSdk = 34
compileSdk = 34

Solution

  • After a lot more reading up on this topic and looking into lots of logs I figured it out.

    As it turns out you need a SurfaceView below your WebView. As far as I can tell, it doesn't really matter where. All VideoPlayers seem to create or inherit such a view themselves. But the native WebView skips this for some reason.

    As soon as you add a Surfaceview, the videos in your WebView will be visible.