Flutter使用youtube_player_iframe遇updateAcquireFence错误无法播放视频
Flutter集成youtube_player_iframe播放异常问题排查与修复
问题现象
使用youtube_player_iframe: ^4.0.1包集成YouTube视频播放时出现以下问题:
- 控制台输出错误提示
updateAcquireFence: Did not find frame. Flutter - 视频无法正常播放,等待数分钟后仅显示缩略图,点击播放按钮后持续处于加载状态
代码问题分析
视频ID提取逻辑错误
- API返回的
link字段存在空格(如https: //youtu.be/Sm3pR9FYZcY),硬编码使用substring(17)会提取到错误的视频ID - 固定截取位置的方式不兼容所有YouTube链接格式(如带参数的长链接)
- API返回的
控制器管理不当
- 每个列表项都新建
YoutubePlayerController,未进行生命周期管理,易引发资源冲突和内存泄漏 - 外层
YoutubePlayerScaffold与列表内YoutubePlayer的嵌套方式不符合插件设计规范
- 每个列表项都新建
网络请求重复触发
- 将
getAllLinks()直接传入FutureBuilder的future参数,组件每次重建都会重新发起网络请求
- 将
无效链接未处理
- API返回的第4条数据中
link为testUrl,属于无效链接,会导致播放器初始化失败
- API返回的第4条数据中
修复方案
1. 正确提取视频ID
使用正则表达式兼容多种YouTube链接格式,同时处理链接中的空格:
String extractVideoId(String url) { // 移除链接中的空格 url = url.replaceAll(' ', ''); final regex = RegExp(r'(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})'); final match = regex.firstMatch(url); return match?.group(1) ?? ''; }
2. 优化控制器生命周期管理
- 移除外层
YoutubePlayerScaffold,直接在列表项中使用YoutubePlayer - 在页面销毁时统一销毁所有控制器,避免内存泄漏
3. 避免重复网络请求
在initState中初始化网络请求,将Future对象赋值给变量后传入FutureBuilder:
late Future<LinkModel> _linksFuture; @override void initState() { super.initState(); _linksFuture = getAllLinks(); }
4. 处理无效链接
在构建列表项时判断视频ID是否有效,无效则显示错误提示:
if (videoId.isEmpty) { return Card( child: Padding( padding: EdgeInsets.all(16), child: Text('无效的视频链接'), ), ); }
5. 配置平台权限
- Android:在
AndroidManifest.xml中添加网络权限<uses-permission android:name="android.permission.INTERNET"/> - iOS:确保
Info.plist中允许HTTPS请求(YouTube链接为HTTPS,默认已支持)
完整修复代码
import 'package:flutter/material.dart'; import 'package:youtube_player_iframe/youtube_player_iframe.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; class LinkModel { bool? status; String? result; List<LinkData>? data; int? totalPosts; int? totalPages; int? perPageLimit; int? currentPage; int? hasNextPage; LinkModel({ this.status, this.result, this.data, this.totalPosts, this.totalPages, this.perPageLimit, this.currentPage, this.hasNextPage, }); LinkModel.fromJson(Map<String, dynamic> json) { status = json['status']; result = json['result']; if (json['data'] != null) { data = <LinkData>[]; json['data'].forEach((v) { data!.add(LinkData.fromJson(v)); }); } totalPosts = json['totalPosts']; totalPages = json['totalPages']; perPageLimit = json['perPageLimit']; currentPage = json['currentPage']; hasNextPage = json['hasNextPage']; } } class LinkData { int? refId; String? userId; String? username; String? link; String? postDateTime; String? city; String? channelName; LinkData({ this.refId, this.userId, this.username, this.link, this.postDateTime, this.city, this.channelName, }); LinkData.fromJson(Map<String, dynamic> json) { refId = json['ref_id']; userId = json['userid'].toString(); username = json['username']; link = json['link']; postDateTime = json['postDateTime']; city = json['city']; channelName = json['channelName']; } } class LiveProgramScreen extends StatefulWidget { const LiveProgramScreen({super.key}); @override State<LiveProgramScreen> createState() => _LiveProgramScreenState(); } class _LiveProgramScreenState extends State<LiveProgramScreen> { late Future<LinkModel> _linksFuture; final List<YoutubePlayerController> _controllers = []; @override void initState() { super.initState(); _linksFuture = getAllLinks(); } @override void dispose() { // 统一销毁所有控制器 for (var controller in _controllers) { controller.close(); } super.dispose(); } Future<LinkModel> getAllLinks() async { var url = "https://url.com/MajlisLinks.php"; var response = await http.get(Uri.parse(url)); var jsondata = jsonDecode(response.body.toString()); print("****" + jsondata.toString()); return LinkModel.fromJson(jsondata); } String extractVideoId(String url) { url = url.replaceAll(' ', ''); final regex = RegExp(r'(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})'); final match = regex.firstMatch(url); return match?.group(1) ?? ''; } @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( appBar: AppBar( title: const Text( "Live Program", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, ), ), centerTitle: true, backgroundColor: const Color(0xFF006064), ), body: FutureBuilder<LinkModel>( future: _linksFuture, builder: (context, snapshot) { if (snapshot.hasData) { return ListView.builder( itemCount: snapshot.data!.data!.length, itemBuilder: (context, index) { var allItem = snapshot.data!.data![index]; final videoId = extractVideoId(allItem.link!); if (videoId.isEmpty) { return Card( color: const Color(0xFF03A9F4), elevation: 6, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), child: Padding( padding: const EdgeInsets.all(16.0), child: Text( '无效视频链接: ${allItem.link}', style: const TextStyle(color: Colors.white), ), ), ); } final controller = YoutubePlayerController.fromVideoId( videoId: videoId, autoPlay: false, params: const YoutubePlayerParams( showFullscreenButton: true, showControls: true, ), ); _controllers.add(controller); return Card( color: const Color(0xFF03A9F4), elevation: 6, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), child: Padding( padding: const EdgeInsets.all(8.0), child: YoutubePlayer( controller: controller, aspectRatio: 16 / 9, ), ), ); }, ); } else if (snapshot.hasError) { return Center(child: Text('加载失败: ${snapshot.error}')); } else { return const Center(child: CircularProgressIndicator()); } }, ), ), ); } } // 十六进制转Color辅助函数 int hexColors(String hex) { return int.parse('0xFF$hex'); }
内容的提问来源于stack exchange,提问作者Taki Rajani
相关产品推荐
相关产品推荐

