在Android Kotlin应用中嵌入的YouTube播放失败,错误码为152-4,尽管onReady已触发
我正在用Kotlin构建一个Android教育应用,课程按课程、年级和章节组织,视频来自YouTube。
为了排查问题,我创建了一个全新的极简Android测试应用。没有后端、没有播放列表逻辑,我使用单个硬编码的视频ID进行测试。
设置
-
Android Studio
- Kotlin
com.pierfrancescosoffritti.androidyoutubeplayer:core- 硬编码的视频ID
- 不涉及后端
- 不涉及播放列表
发生了什么
播放器成功初始化,onReady() 触发,但 cueVideo(...) 和 loadVideo(...) 都失败。
屏幕上的YouTube错误:152-4
库回调:播放器错误:UNKNOWN
相关日志
A. onCreate entered
1. Activity started. Target Video ID: [rDo-GlvNsUc] (Raw: [rDo-GlvNsUc])
Internal WebView found and settings applied.
B. initialize called
C. initialize returned
Activity onStart
Activity onResume
D. onReady fired
Player width: 2020, height: 1080
E. calling cueVideo with [rDo-GlvNsUc]
F. onError fired: [UNKNOWN]
onVideoId: rDo-GlvNsUc
F. onError fired: [UNKNOWN]
最小代码
youtubePlayerView.addYouTubePlayerListener(object : AbstractYouTubePlayerListener() {
override fun onReady(youTubePlayer: YouTubePlayer) {
Log.d("YT", "onReady fired")
youTubePlayer.cueVideo("rDo-GlvNsUc", 0f)
}
override fun onError(youTubePlayer: YouTubePlayer, error: PlayerConstants.PlayerError) {
Log.d("YT", "onError fired: [$error]")
}
})
我已经排除了哪些
- 后端或API问题
- 无效的视频ID解析
- 视图大小问题
- 在
onReady()之前调用播放器方法 - 播放列表解析问题
- 服务器或存储问题
- 同样的故障也出现在从头创建的一个极简新应用中
问题
在这个Android嵌入式播放场景中,YouTube错误 152-4 表示什么意思?
通常是不是因为以下原因导致的:
- 该视频的嵌入被禁用
- YouTube在 Android应用或WebView嵌入中对播放的限制更严格
- 这个库不再与当前的YouTube嵌入行为可靠地工作
我在努力判断这是一个视频特定的限制、YouTube端的变更,还是Android WebView/库的问题。
如果你愿意,我还可以给你一个更贴合Stack Overflow的版本,听起来更专业一些,不太容易被挑剔。
解决方案
我在使用Jetpack Compose的应用中也遇到了同样的问题。
实际上这篇帖子的答案对我帮助很大 YouTube视频在WebView中在Android上给出错误代码153
我尝试实现那个库的一个包装器,处理这些头部设置,现在152-4已经解决。
@Composable
fun YoutubePlayerView(
videoId: String,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val packageName = "com.example.myapp" //replace with yours packageName
AndroidView(
modifier = modifier.clip(RoundedCornerShape(12.dp)),
factory = { context ->
YouTubePlayerView(context).apply {
enableAutomaticInitialization = false
lifecycleOwner.lifecycle.addObserver(this)
val webView = this.getChildAt(0) as? android.webkit.WebView
webView?.settings?.apply {
javaScriptEnabled = true
domStorageEnabled = true
// just a sample user agent
userAgentString = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36"
}
//the game changers :)
val extraHeaders = mapOf(
"Referer" to "https://$packageName",
"Referrer-Policy" to "strict-origin-when-cross-origin"
)
val options = IFramePlayerOptions.Builder()
.controls(1)
.origin("https://$packageName")
.build()
initialize(object : AbstractYouTubePlayerListener() {
override fun onReady(youTubePlayer: YouTubePlayer) {
youTubePlayer.loadVideo(videoId, 0f)
}
}, options)
}
},
onRelease = { it.release() }
)
}
备选方案
在我的应用中也遇到同样的问题,使用的是相同的依赖。问题不是依赖本身,而是YouTube针对嵌入的新策略。YouTube最近更新了安全策略,要求非常具体的 Referer策略 和有效的 Origin。解决办法?
- Meta标签: 在HTML头部添加一个
<meta>标签以强制引用策略。 - IFrame属性: 在iframe中显式添加
referrerpolicy="strict-origin-when-cross-origin"。 - 真实的基础URL: 不要把
youtube.com作为基础地址,用你应用的实际网站或一个看起来像真实站点的占位域名。
``` import android.view.ViewGroup import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView
@Composable fun YouTubeIFramePlayer( videoId: String, modifier: Modifier = Modifier ) {
val html = """
<!DOCTYPE html>
<html>
<head>
<meta name="referrer" content="strict-origin-when-cross-origin">
<style>
body { margin: 0; padding: 0; background-color: black; }
.video-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; }
.video-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; }
</style>
</head>
<body>
<div class="video-container">
<iframe
id="player"
src="https://www.youtube.com/embed/$videoId?autoplay=1&modestbranding=1&rel=0&enablejsapi=1"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen
/* FIX 2: Explicit Policy on the Iframe itself */
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
</div>
</body>
</html>
""".trimIndent()
AndroidView(
modifier = modifier
.fillMaxWidth()
.aspectRatio(16 / 9f),
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
useWideViewPort = true
loadWithOverviewMode = true
// Spoofing a very specific modern Chrome agent
userAgentString = "Mozilla/5.0 (Linux; Android 13; Samsung-SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36"
}
webViewClient = WebViewClient()
webChromeClient = WebChromeClient()
val appBaseUrl = "https://anywebsite.com"
loadDataWithBaseURL(appBaseUrl, html, "text/html", "UTF-8", null)
}
},
onRelease = { webView ->
webView.destroy()
}
)
} ```