Flutter应用WebView嵌入YouTube触发机器人检测问题
Flutter应用中YouTube视频播放问题
问题背景
- 最初使用
youtube_player_flutter包,持续返回150错误码(该错误通常表示视频无法在嵌入式播放器中播放); - 因此改用
flutter_inappwebview,通过WebView嵌入iframe的方式播放YouTube; - 问题表现:在巴基斯坦地区使用无异常,但美国地区的客户端播放数小时后,YouTube会触发机器人检测;切换网络连接可临时恢复正常播放;
疑问
- 使用WebView+iframe播放YouTube是否会触发机器人检测?
- Flutter应用中嵌入YouTube视频的官方推荐方式是什么?
- 是否有开发者遇到过类似的WebView播放问题?
相关代码
class YoutubeShortsPlayerView extends StatefulWidget { final String videoUrl; final String? videoID; final String? title; final Duration? startPosition; final bool islive; final bool hasOpacityLow; const YoutubeShortsPlayerView({ super.key, required this.videoUrl, this.videoID, this.startPosition, this.title, this.islive = false, this.hasOpacityLow = false, }); @override State<YoutubeShortsPlayerView> createState() => _YoutubeShortsPlayerViewState(); } class _YoutubeShortsPlayerViewState extends State<YoutubeShortsPlayerView> with WidgetsBindingObserver { InAppWebViewController? _webViewController; bool _isLoading = true; String get _videoId => widget.videoID ?? GlobalVariables.extractYoutubeId(widget.videoUrl); bool get _isShort => widget.videoUrl.contains('/shorts/'); InAppWebViewSettings get _settings => InAppWebViewSettings( transparentBackground: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserGesture: false, allowsAirPlayForMediaPlayback: false, allowsPictureInPictureMediaPlayback: false, allowsBackForwardNavigationGestures: false, javaScriptEnabled: true, javaScriptCanOpenWindowsAutomatically: false, disableContextMenu: true, supportZoom: false, iframeAllowFullscreen: false, domStorageEnabled: true, ); String _buildHtml(String videoId, int startSeconds) { return ''' <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="referrer" content="strict-origin-when-cross-origin"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; background: #000; overflow: hidden; } #yt-player { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } </style> </head> <body> <div id="yt-player"></div> <script> // ✅ Load official YouTube IFrame API var tag = document.createElement('script'); tag.src = 'https://www.youtube.com/iframe_api'; var firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); var player; function onYouTubeIframeAPIReady() { player = new YT.Player('yt-player', { host: 'https://www.youtube-nocookie.com', videoId: '$videoId', playerVars: { autoplay: 1, playsinline: 1, controls: 1, rel: 0, start: $startSeconds, fs: 0, iv_load_policy: 3, }, events: { onReady: function(event) { event.target.playVideo(); }, onError: function(event) { console.log('Player error: ' + event.data); }, onAutoplayBlocked: function(event) { // Show play button if autoplay blocked console.log('Autoplay blocked'); } } }); player.setSize(window.innerWidth, window.innerHeight); } window.onresize = function() { if (player) player.setSize(window.innerWidth, window.innerHeight); }; // Block deep links window.open = function(url) { return null; }; document.addEventListener('click', function(e) { var el = e.target; while (el && el.tagName !== 'A') el = el.parentElement; if (el && el.href) { var href = el.href; if ( href.startsWith('youtube://') || href.startsWith('vnd.youtube://') || href.startsWith('intent://') ) { e.preventDefault(); e.stopPropagation(); } } }, true); </script> </body> </html> '''; } void _loadHtml(InAppWebViewController controller) { final startSeconds = widget.startPosition?.inSeconds ?? 0; controller.loadData( data: _buildHtml(_videoId, startSeconds), mimeType: 'text/html', encoding: 'utf-8', baseUrl: WebUri('https://www.youtube-nocookie.com'), ); } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) async { if (state == AppLifecycleState.paused) { _webViewController?.evaluateJavascript( source: ''' if (player && player.pauseVideo) player.pauseVideo(); ''', ); } else if (state == AppLifecycleState.resumed) { _webViewController?.evaluateJavascript( source: 'if (player && player.playVideo) player.playVideo();', ); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: widget.hasOpacityLow ? Colors.black.withOpacity(0.45) : Colors.black, body: OrientationBuilder( // Add this to listen for orientation changes builder: (context, orientation) { return Stack( children: [ Center( child: _isShort ? AspectRatio(aspectRatio: 9 / 16, child: _buildWebView()) : LayoutBuilder( builder: (context, constraints) { // Re-calculate based on current orientation constraints const ratio = 16 / 9; double w = constraints.maxWidth; double h = w / ratio; // If calculated height exceeds available height (common in landscape), // scale based on height instead. if (h > constraints.maxHeight) { h = constraints.maxHeight; w = h * ratio; } return SafeArea( child: Directionality( textDirection: TextDirection.ltr, child: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center, children: [ if (orientation == Orientation.portrait && !_isShort) IconButton( icon: const Icon( Icons.cancel, color: Colors.white, size: 30, ), onPressed: () => Navigator.pop(context), ), Center( child: SizedBox( width: w, height: h - 30, child: _buildWebView(), ), ), ], ), ), ); }, ), ), if (_isLoading) const Center( child: CircularProgressIndicator( color: Color(0xFFFF0000), strokeWidth: 2.5, ), ), if (_isShort || (orientation == Orientation.landscape && !_isShort)) Positioned( top: orientation == Orientation.portrait ? 35 : 10, // Adjust for landscape right: 24, child: IconButton( icon: const Icon( Icons.cancel, color: Colors.white, size: 30, ), onPressed: () => Navigator.pop(context), ), ), ], ); }, ), ); } // Helper to keep the build method clean Widget _buildWebView() { return InAppWebView( initialSettings: _settings, onWebViewCreated: (controller) async { _webViewController = controller; // ✅ Clear before very first load too // await InAppWebViewController.clearAllCache(); // await CookieManager.instance().deleteAllCookies(); _loadHtml(controller); }, onLoadStop: (controller, url) { if (mounted) setState(() => _isLoading = false); }, shouldOverrideUrlLoading: (controller, navigationAction) async { final url = navigationAction.request.url?.toString() ?? ''; if (url.startsWith("https://com.nayatel.jarayid")) return NavigationActionPolicy.ALLOW; if (url.startsWith('youtube://')) return NavigationActionPolicy.CANCEL; if (url.startsWith('vnd.youtube://')) return NavigationActionPolicy.CANCEL; if (url.startsWith('intent://')) return NavigationActionPolicy.CANCEL; if (url.startsWith('market://')) return NavigationActionPolicy.CANCEL; if (url == 'about:blank') return NavigationActionPolicy.ALLOW; if (url.contains('youtube-nocookie.com/embed') || url.contains('youtube.com/embed')) return NavigationActionPolicy.ALLOW; if (url.contains('youtube.com/watch')) return NavigationActionPolicy.CANCEL; if (url.contains('youtube.com/shorts')) return NavigationActionPolicy.CANCEL; if (url.contains('m.youtube.com')) return NavigationActionPolicy.CANCEL; if (url.contains('youtu.be')) return NavigationActionPolicy.CANCEL; if (url == 'https://www.youtube-nocookie.com/') return NavigationActionPolicy.ALLOW; if (url == 'https://www.youtube.com/') return NavigationActionPolicy.ALLOW; if (url.contains('ytimg.com')) return NavigationActionPolicy.ALLOW; if (url.contains('googlevideo.com')) return NavigationActionPolicy.ALLOW; if (url.contains('ggpht.com')) return NavigationActionPolicy.ALLOW; if (url.contains('googleapis.com')) return NavigationActionPolicy.ALLOW; if (url.contains('google.com')) return NavigationActionPolicy.ALLOW; return NavigationActionPolicy.CANCEL; }, onCreateWindow: (controller, createWindowAction) async { return false; }, ); } }
内容的提问来源于stack exchange,提问作者Zetro
相关产品推荐
相关产品推荐

