You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 10:37:33