import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_decode_plugin/video_decode_plugin.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { int? _textureId; bool _isInitialized = false; bool _isPlaying = false; String _statusMessage = '等待初始化...'; // 视频分辨率和帧率 final int _width = 640; final int _height = 360; final int _frameRate = 25; // 解码定时器 Timer? _decodeTimer; // 状态监控定时器 Timer? _statsTimer; // 添加一个计时器记录帧率 Stopwatch? _frameRateWatch; // H.264文件数据 Uint8List? _h264Data; // 已解码的帧数 int _frameCount = 0; // 接收到的渲染帧数 int _renderedFrameCount = 0; // 状态信息 Map _decoderStats = {}; // 需要强制更新Texture bool _needsTextureUpdate = false; // 诊断日志 List _logs = []; ScrollController _logScrollController = ScrollController(); // 帧分隔符 (NAL 单元起始码) final List _startCode = [0, 0, 0, 1]; // 当前解析位置 int _parsePosition = 0; // 帧索引位置列表 (每个帧的起始位置) List _framePositions = []; // 帧类型列表 List _frameTypes = []; // 解码器行为配置 final int _bufferSize = 30; // 缓冲区大小(帧数) // 是否使用原始数据解析 bool _useRawParsing = true; @override void initState() { super.initState(); _loadH264File(); } // 添加日志 void _addLog(String message) { print(message); // 同时打印到控制台 setState(() { _logs.add("[${DateTime.now().toString().split('.').first}] $message"); // 延迟滚动到底部 Future.delayed(Duration(milliseconds: 100), () { if (_logScrollController.hasClients) { _logScrollController.animateTo( _logScrollController.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.easeOut, ); } }); }); } // 加载H.264文件 Future _loadH264File() async { try { setState(() { _statusMessage = '加载H.264文件中...'; _logs = []; }); _addLog('开始加载H.264文件'); // 从assets加载示例H.264文件 final ByteData data = await rootBundle.load('assets/demo.h264'); _h264Data = data.buffer.asUint8List(); _addLog( 'H.264文件加载完成,大小: ${(_h264Data!.length / 1024).toStringAsFixed(2)} KB'); setState(() { _statusMessage = 'H.264文件加载完成,大小: ${(_h264Data!.length / 1024).toStringAsFixed(2)} KB'; }); // 预解析H.264文件 _parseH264File(); } catch (e) { _addLog('H.264文件加载失败: $e'); setState(() { _statusMessage = 'H.264文件加载失败: $e'; }); } } // 预解析H.264文件,找出所有帧的位置和类型 void _parseH264File() { if (_h264Data == null || _h264Data!.isEmpty) return; setState(() { _statusMessage = '正在解析H.264文件结构...'; }); _addLog('开始解析H.264文件结构...'); _framePositions.clear(); _frameTypes.clear(); // 查找所有起始码位置 int iFrameCount = 0; int pFrameCount = 0; for (int i = 0; i < _h264Data!.length - 4; i++) { if (_isStartCode(i)) { // 检查NAL类型 if (i + 4 < _h264Data!.length) { int nalType = _h264Data![i + 4] & 0x1F; _addLog('在位置 $i 找到NAL单元, 类型: $nalType'); _framePositions.add(i); // 根据NAL类型确定帧类型 // 5 = IDR帧 (I帧), 7 = SPS, 8 = PPS if (nalType == 5 || nalType == 7 || nalType == 8) { _frameTypes.add(FrameType.iFrame); iFrameCount++; } else { _frameTypes.add(FrameType.pFrame); pFrameCount++; } } } } // 尝试直接添加SPS/PPS帧,可能在文件开始处 _useRawParsing = (_framePositions.isEmpty || iFrameCount == 0); _addLog( '解析完成: 找到 ${_framePositions.length} 个NAL单元, I帧: $iFrameCount, P帧: $pFrameCount'); if (_useRawParsing) { _addLog('警告: 未检测到有效I帧,将使用原始数据直接解码'); } setState(() { _statusMessage = 'H.264解析完成,共找到 ${_framePositions.length} 个NAL单元'; }); } // 检查是否为起始码 bool _isStartCode(int position) { if (position + 3 >= _h264Data!.length) return false; return _h264Data![position] == 0 && _h264Data![position + 1] == 0 && _h264Data![position + 2] == 0 && _h264Data![position + 3] == 1; } // 帧回调函数 void _onFrameAvailable(int textureId) { if (mounted) { _addLog('收到帧可用回调: textureId=$textureId'); // 必须调用setState刷新UI setState(() { _renderedFrameCount++; }); } } // 开始统计监控 void _startStatsMonitoring() { _statsTimer?.cancel(); _statsTimer = Timer.periodic(const Duration(milliseconds: 1000), (_) async { if (_textureId != null && mounted) { try { final stats = await VideoDecodePlugin.getDecoderStats(_textureId!); if (mounted) { setState(() { _decoderStats = stats; }); // 记录关键的统计变化 if (stats['droppedFrames'] > 0 || stats['isBuffering'] == true) { _addLog('解码器状态: ${stats['isBuffering'] ? "缓冲中" : "播放中"}, ' + '输入队列: ${stats['inputQueueSize']}, ' + '输出队列: ${stats['outputQueueSize']}, ' + '丢弃帧: ${stats['droppedFrames']}'); } } } catch (e) { _addLog('获取统计信息失败: $e'); } } }); } // 初始化解码器 Future _initDecoder() async { if (_h264Data == null) { setState(() { _statusMessage = 'H.264文件未加载'; }); _addLog('错误: H.264文件未加载'); return; } try { // 检查平台支持 if (!VideoDecodePlugin.isPlatformSupported) { setState(() { _statusMessage = '当前平台不支持视频解码'; }); _addLog('错误: 当前平台不支持视频解码'); return; } setState(() { _statusMessage = '正在初始化解码器...'; }); _addLog('开始初始化解码器...'); // 配置解码器 final config = VideoDecoderConfig( width: _width, height: _height, frameRate: _frameRate, codecType: CodecType.h264, bufferSize: _bufferSize, threadCount: 2, isDebug: true, enableHardwareDecoder: true, ); _addLog( '解码器配置: 分辨率 ${_width}x${_height}, 帧率 $_frameRate, 缓冲区 $_bufferSize'); // 先释放之前的解码器 if (_textureId != null) { _addLog('释放旧解码器: $_textureId'); await VideoDecodePlugin.releaseDecoder(); } // 初始化解码器并获取纹理ID final textureId = await VideoDecodePlugin.initDecoder(config); if (textureId == null) { setState(() { _statusMessage = '解码器初始化失败'; }); _addLog('错误: 解码器初始化失败,返回的textureId为null'); return; } _addLog('解码器初始化成功,textureId: $textureId'); // 设置帧可用回调 VideoDecodePlugin.setFrameCallbackForTexture( textureId, _onFrameAvailable); _addLog('已设置帧可用回调'); // 开始监控统计信息 _startStatsMonitoring(); _addLog('已启动统计信息监控'); setState(() { _textureId = textureId; _isInitialized = true; _frameCount = 0; _renderedFrameCount = 0; _parsePosition = 0; _needsTextureUpdate = false; _statusMessage = '解码器初始化成功,纹理ID: $_textureId'; }); // 尝试立即解码第一帧I帧 await _injectFirstIFrame(); } catch (e) { _addLog('初始化解码器错误: $e'); setState(() { _statusMessage = '初始化错误: $e'; }); } } // 尝试立即注入第一个I帧,帮助启动解码 Future _injectFirstIFrame() async { if (_h264Data == null || !_isInitialized) return; try { _addLog('尝试注入首个I帧进行测试...'); // 如果找不到有效的I帧位置,直接使用文件开头部分作为I帧 if (_useRawParsing || _framePositions.isEmpty) { // 直接使用前1024字节作为I帧 int len = _h264Data!.length > 1024 ? 1024 : _h264Data!.length; Uint8List firstFrame = Uint8List(len); firstFrame.setRange(0, len, _h264Data!, 0); _addLog('使用原始数据作为I帧进行测试,大小: $len'); bool success = await VideoDecodePlugin.decodeFrame(firstFrame, FrameType.iFrame); _addLog('注入测试I帧 ${success ? "成功" : "失败"}'); return; } // 找到第一个I帧的位置 int iFramePos = -1; for (int i = 0; i < _frameTypes.length; i++) { if (_frameTypes[i] == FrameType.iFrame) { iFramePos = i; break; } } if (iFramePos == -1) { _addLog('错误: 未找到I帧'); return; } // 获取I帧数据 int startPos = _framePositions[iFramePos]; int endPos = (iFramePos + 1 < _framePositions.length) ? _framePositions[iFramePos + 1] : _h264Data!.length; int frameSize = endPos - startPos; _addLog('找到I帧: 位置 $startPos, 大小 $frameSize'); // 提取I帧数据 Uint8List iFrameData = Uint8List(frameSize); iFrameData.setRange(0, frameSize, _h264Data!, startPos); // 解码I帧 bool success = await VideoDecodePlugin.decodeFrame(iFrameData, FrameType.iFrame); _addLog('注入I帧 ${success ? "成功" : "失败"}'); } catch (e) { _addLog('注入I帧失败: $e'); } } // 开始播放 void _startPlaying() { if (!_isInitialized || _isPlaying || _h264Data == null) { return; } setState(() { _isPlaying = true; _statusMessage = '开始播放...'; }); _addLog('开始播放, 解码位置: $_parsePosition'); // 添加强制刷新的逻辑 _addDummyFrame(); // 尝试强制发送第一个I帧 _injectFirstIFrame().then((_) { // 重置帧率计时器 _frameRateWatch = Stopwatch()..start(); // 创建定时器以固定帧率解码 _decodeTimer = Timer.periodic(Duration(milliseconds: 1000 ~/ _frameRate), (_) { _decodeNextFrame(); }); }); } // 添加一个虚拟帧进行测试 void _addDummyFrame() { _addLog('添加测试图形'); // 创建一个虚拟帧进行测试 if (_textureId != null) { // 强制组件立即重绘 setState(() { _renderedFrameCount++; }); // 延迟后再次强制刷新 Future.delayed(Duration(milliseconds: 500), () { if (mounted) { setState(() { _renderedFrameCount++; }); } }); } } // 停止播放 void _stopPlaying() { _decodeTimer?.cancel(); _decodeTimer = null; _frameRateWatch?.stop(); setState(() { _isPlaying = false; _statusMessage = '播放已停止'; }); _addLog('播放已停止'); } // 解码下一帧 Future _decodeNextFrame() async { if (!_isInitialized || _h264Data == null) { _stopPlaying(); _addLog('解码器未初始化或H264数据为空,停止播放'); return; } // 如果解析失败,尝试使用原始数据 if (_useRawParsing) { await _decodeRawData(); return; } // 正常解析模式 if (_framePositions.isEmpty) { _stopPlaying(); _addLog('没有找到有效帧,停止播放'); return; } try { // 检查是否播放完毕 if (_parsePosition >= _framePositions.length) { // 循环播放,重新开始 _parsePosition = 0; setState(() { _statusMessage = '播放完成,重新开始'; }); _addLog('播放完成,循环回到开始位置'); } // 获取当前帧位置 int currentPos = _framePositions[_parsePosition]; // 计算帧大小 (到下一帧开始或文件结束) int nextPos = _parsePosition + 1 < _framePositions.length ? _framePositions[_parsePosition + 1] : _h264Data!.length; int frameSize = nextPos - currentPos; // 提取帧数据 Uint8List frameData = Uint8List(frameSize); frameData.setRange(0, frameSize, _h264Data!, currentPos); // 获取帧类型 FrameType frameType = _frameTypes[_parsePosition]; // 如果是第一帧或每隔一定数量的帧,记录一下详细信息 if (_frameCount % 10 == 0 || _frameCount < 5) { String hexPrefix = ''; if (frameData.length >= 8) { hexPrefix = '0x' + frameData .sublist(0, 8) .map((e) => e.toRadixString(16).padLeft(2, '0')) .join(''); } _addLog( '解码帧 #$_frameCount, 类型: ${frameType == FrameType.iFrame ? "I" : "P"}帧, ' + '大小: ${(frameSize / 1024).toStringAsFixed(2)} KB, 前缀: $hexPrefix'); } // 解码帧 final success = await VideoDecodePlugin.decodeFrame(frameData, frameType); // 如果前几帧解码失败,记录详细错误 if (!success && _frameCount < 5) { _addLog( '解码失败: 帧 #$_frameCount, 类型: ${frameType == FrameType.iFrame ? "I" : "P"}帧'); } // 更新状态 _frameCount++; _parsePosition++; if (mounted) { // 计算实际帧率 String frameRateInfo = ''; if (_frameRateWatch != null && _frameRateWatch!.elapsedMilliseconds > 0) { double actualFps = _frameCount / (_frameRateWatch!.elapsedMilliseconds / 1000); frameRateInfo = ', 实际帧率: ${actualFps.toStringAsFixed(1)} fps'; } setState(() { _statusMessage = '正在播放: 第${_parsePosition}/${_framePositions.length}帧, ' + '类型: ${frameType == FrameType.iFrame ? "I" : "P"}帧, ' + '大小: ${(frameSize / 1024).toStringAsFixed(2)} KB, ' + '${success ? "成功" : "失败"}$frameRateInfo'; }); } // 检查是否播放完毕 if (_parsePosition >= _framePositions.length) { _stopPlaying(); setState(() { _statusMessage = '播放完成'; _parsePosition = 0; }); _addLog('播放完成,已解码 $_frameCount 帧'); } } catch (e) { _addLog('解码错误: $e'); setState(() { _statusMessage = '解码错误: $e'; }); _stopPlaying(); } } // 使用原始数据直接解码,每次取一小块 Future _decodeRawData() async { try { // 计算当前位置 int currentPos = _parsePosition * 1024; // 每次取1KB数据 // 检查是否到达文件末尾 if (currentPos >= _h264Data!.length) { _parsePosition = 0; currentPos = 0; _addLog('原始解码模式:已到达文件末尾,重新开始'); } // 计算块大小 int blockSize = 1024; if (currentPos + blockSize > _h264Data!.length) { blockSize = _h264Data!.length - currentPos; } // 提取数据块 Uint8List blockData = Uint8List(blockSize); blockData.setRange(0, blockSize, _h264Data!, currentPos); // 每10帧记录一下进度 if (_frameCount % 10 == 0) { _addLog('原始解码模式:解码块 #$_frameCount, 位置: $currentPos, 大小: $blockSize'); } // 解码数据块,强制当作I帧 bool success = await VideoDecodePlugin.decodeFrame(blockData, FrameType.iFrame); // 更新计数 _frameCount++; _parsePosition++; // 更新状态 if (mounted) { setState(() { _statusMessage = '原始模式播放: 位置 $currentPos/${_h264Data!.length}, ' + '大小: ${(blockSize / 1024).toStringAsFixed(2)} KB, ' + '${success ? "成功" : "失败"}'; }); } } catch (e) { _addLog('原始模式解码错误: $e'); _stopPlaying(); } } // 释放解码器资源 Future _releaseDecoder() async { _stopPlaying(); _statsTimer?.cancel(); _statsTimer = null; if (!_isInitialized) { return; } try { _addLog('开始释放解码器'); final bool success = await VideoDecodePlugin.releaseDecoder(); setState(() { _isInitialized = !success; _textureId = null; _statusMessage = success ? '解码器已释放' : '解码器释放失败'; }); _addLog('解码器释放 ${success ? "成功" : "失败"}'); } catch (e) { _addLog('释放解码器错误: $e'); setState(() { _statusMessage = '释放解码器错误: $e'; }); } } @override void dispose() { _stopPlaying(); _statsTimer?.cancel(); _releaseDecoder(); super.dispose(); } // 构建统计信息UI Widget _buildStatsDisplay() { if (_decoderStats.isEmpty) { return const Text('无统计信息'); } // 从统计信息中提取有用的字段 final bool isBuffering = _decoderStats['isBuffering'] ?? false; final int totalFrames = _decoderStats['totalFrames'] ?? 0; final int renderedFrames = _decoderStats['renderedFrames'] ?? 0; final int droppedFrames = _decoderStats['droppedFrames'] ?? 0; final int inputQueueSize = _decoderStats['inputQueueSize'] ?? 0; final int outputQueueSize = _decoderStats['outputQueueSize'] ?? 0; final int bufferFillPercentage = _decoderStats['bufferFillPercentage'] ?? 0; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('解码状态: ${isBuffering ? "缓冲中" : "播放中"}', style: TextStyle( fontWeight: FontWeight.bold, color: isBuffering ? Colors.orange : Colors.green)), Text('已解码帧: $totalFrames, 渲染帧: $renderedFrames, 丢弃帧: $droppedFrames'), Text( '输入队列: $inputQueueSize, 输出队列: $outputQueueSize, 缓冲填充率: $bufferFillPercentage%'), Text('Flutter接收到的帧数: $_renderedFrameCount, 已解析帧位置: $_parsePosition'), ], ); } // 构建日志显示区域 Widget _buildLogDisplay() { return Container( height: 150, decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(8), ), margin: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.all(8), child: ListView.builder( controller: _logScrollController, itemCount: _logs.length, itemBuilder: (context, index) { return Text( _logs[index], style: TextStyle( color: _logs[index].contains('错误') ? Colors.red : _logs[index].contains('警告') ? Colors.yellow : Colors.green, fontSize: 12, fontFamily: 'monospace', ), ); }, ), ); } @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold( appBar: AppBar( title: const Text('视频解码插件示例'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 显示解码的视频,使用Flutter的Texture组件 if (_textureId != null) Container( width: _width.toDouble(), height: _height.toDouble(), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), clipBehavior: Clip.antiAlias, child: Stack( children: [ RepaintBoundary( child: Texture( textureId: _textureId!, filterQuality: FilterQuality.medium, ), ), // 添加一个覆盖层用于触发刷新 if (_renderedFrameCount > 0) Positioned.fill( child: IgnorePointer( child: Opacity( opacity: 0.0, child: Container( color: Colors.transparent, key: ValueKey( _renderedFrameCount), // 通过key强制刷新 ), ), ), ), ], ), ) else Container( width: _width.toDouble(), height: _height.toDouble(), decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), color: Colors.black, ), child: Center( child: Text( '未初始化', style: TextStyle(color: Colors.white), ), ), ), const SizedBox(height: 20), Text( _statusMessage, textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 10), // 显示解码统计信息 Container( width: double.infinity, padding: const EdgeInsets.all(8), margin: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), color: Colors.grey.shade50, ), child: _buildStatsDisplay(), ), const SizedBox(height: 10), // 显示日志 _buildLogDisplay(), const SizedBox(height: 20), // 控制按钮 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (!_isInitialized) ElevatedButton( onPressed: _initDecoder, child: const Text('初始化解码器'), ) else if (!_isPlaying) ElevatedButton( onPressed: _startPlaying, child: const Text('播放'), ) else ElevatedButton( onPressed: _stopPlaying, child: const Text('停止'), ), const SizedBox(width: 20), ElevatedButton( onPressed: _isInitialized ? _releaseDecoder : null, child: const Text('释放解码器'), ), if (_isInitialized) Padding( padding: const EdgeInsets.only(left: 20.0), child: ElevatedButton( onPressed: () { setState(() {}); // 强制重绘 _addLog('触发强制刷新'); }, child: const Text('强制刷新'), ), ), ], ), ], ), ), ), ); } }