diff --git a/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt b/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt index ecd7ff0..eec1910 100644 --- a/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt +++ b/android/app/src/main/java/com/remotedisplay/player/player/MediaPlayerManager.kt @@ -55,13 +55,12 @@ class MediaPlayerManager( exoPlayer?.stop() youtubeWebView?.apply { - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - settings.mediaPlaybackRequiresUserGesture = false - webViewClient = WebViewClient() - webChromeClient = WebChromeClient() + com.remotedisplay.player.util.WebViewSupport.configure(this, "YouTube") setBackgroundColor(android.graphics.Color.BLACK) - loadUrl(embedUrl) + // Load via an embed wrapper with a valid youtube.com origin (Error 153 fix). + val html = com.remotedisplay.player.util.WebViewSupport.youtubeEmbedHtml(embedUrl) + if (html != null) loadDataWithBaseURL(com.remotedisplay.player.util.WebViewSupport.YT_BASE, html, "text/html", "UTF-8", null) + else loadUrl(embedUrl) } } @@ -78,12 +77,7 @@ class MediaPlayerManager( exoPlayer?.stop() youtubeWebView?.apply { - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - settings.mediaPlaybackRequiresUserGesture = false - webViewClient = WebViewClient() - webChromeClient = WebChromeClient() - setBackgroundColor(android.graphics.Color.TRANSPARENT) + com.remotedisplay.player.util.WebViewSupport.configure(this, "Widget") loadUrl(url) } } diff --git a/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt b/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt index 070197c..68c435b 100644 --- a/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt +++ b/android/app/src/main/java/com/remotedisplay/player/player/ZoneManager.kt @@ -155,10 +155,13 @@ class ZoneManager( container.addView(webView); zoneViews[zone.id] = webView if (multi) scheduleZoneAdvance(zone.id, durationMs, advance) } - // YouTube - render in WebView + // YouTube - render via an embed wrapper with a valid origin (Error 153 fix) mimeType == "video/youtube" && !remoteUrl.isNullOrEmpty() -> { val webView = createWebView() - webView.loadUrl(remoteUrl); webView.layoutParams = params + val html = com.remotedisplay.player.util.WebViewSupport.youtubeEmbedHtml(remoteUrl) + if (html != null) webView.loadDataWithBaseURL(com.remotedisplay.player.util.WebViewSupport.YT_BASE, html, "text/html", "UTF-8", null) + else webView.loadUrl(remoteUrl) + webView.layoutParams = params container.addView(webView); zoneViews[zone.id] = webView if (multi) scheduleZoneAdvance(zone.id, durationMs, advance) } @@ -245,11 +248,7 @@ class ZoneManager( private fun createWebView(): WebView { return WebView(context).apply { - settings.javaScriptEnabled = true - settings.domStorageEnabled = true - settings.mediaPlaybackRequiresUserGesture = false - setBackgroundColor(android.graphics.Color.TRANSPARENT) - webViewClient = WebViewClient() + com.remotedisplay.player.util.WebViewSupport.configure(this, "Zone") } } diff --git a/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt b/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt new file mode 100644 index 0000000..d9cbcd8 --- /dev/null +++ b/android/app/src/main/java/com/remotedisplay/player/util/WebViewSupport.kt @@ -0,0 +1,79 @@ +package com.remotedisplay.player.util + +import android.webkit.ConsoleMessage +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient + +/** + * Shared setup + helpers for the player's WebViews (zone widgets, fullscreen + * widgets, YouTube). Centralizes: + * - JS / DOM storage / autoplay-without-gesture, + * - mixed-content ALLOW (self-hosted servers are often http on the LAN; without + * this an https page embedding http - or vice versa - is silently blocked into + * a black broken-frame), + * - error/console logging piped to DebugLog so a failing web frame shows the + * real reason in the live debug panel instead of just a black broken-page view, + * - a YouTube embed that loads with a valid youtube.com origin (fixes Error 153). + */ +object WebViewSupport { + + const val YT_BASE = "https://www.youtube.com" + + fun configure(webView: WebView, tag: String) { + webView.settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + mediaPlaybackRequiresUserGesture = false + mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + } + webView.setBackgroundColor(android.graphics.Color.TRANSPARENT) + webView.webViewClient = object : WebViewClient() { + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + if (request?.isForMainFrame == true) { + DebugLog.e(tag, "WebView load error ${error?.errorCode} ${error?.description} url=${request.url}") + } + } + override fun onReceivedHttpError(view: WebView?, request: WebResourceRequest?, errorResponse: WebResourceResponse?) { + if (request?.isForMainFrame == true) { + DebugLog.e(tag, "WebView HTTP ${errorResponse?.statusCode} url=${request.url}") + } + } + } + webView.webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(msg: ConsoleMessage?): Boolean { + if (msg?.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { + DebugLog.w(tag, "JS error: ${msg.message()} @${msg.sourceId()}:${msg.lineNumber()}") + } + return super.onConsoleMessage(msg) + } + } + } + + fun extractYoutubeId(url: String): String? { + val patterns = listOf( + Regex("""embed/([A-Za-z0-9_-]{6,})"""), + Regex("""[?&]v=([A-Za-z0-9_-]{6,})"""), + Regex("""youtu\.be/([A-Za-z0-9_-]{6,})""") + ) + for (p in patterns) p.find(url)?.let { return it.groupValues[1] } + return null + } + + /** + * HTML wrapper for a YouTube embed. Loaded via loadDataWithBaseURL(YT_BASE, ...) + * so the iframe has a valid youtube.com origin/referer (a bare loadUrl of the + * embed gives Error 153 "player misconfigured"). Returns null if no video id. + */ + fun youtubeEmbedHtml(url: String): String? { + val id = extractYoutubeId(url) ?: return null + val src = "$YT_BASE/embed/$id?autoplay=1&mute=1&controls=0&rel=0&modestbranding=1&loop=1&playlist=$id&playsinline=1" + return "" + + "" + + "" + } +}