import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_decode_plugin/video_decode_plugin.dart'; // 测试图案绘制器 class TestPatternPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final colors = [ Colors.red, Colors.green, Colors.blue, Colors.yellow, Colors.purple, ]; const int gridSize = 4; final double cellWidth = size.width / gridSize; final double cellHeight = size.height / gridSize; for (int x = 0; x < gridSize; x++) { for (int y = 0; y < gridSize; y++) { final paint = Paint() ..color = colors[(x + y) % colors.length] ..style = PaintingStyle.fill; final rect = Rect.fromLTWH(x * cellWidth, y * cellHeight, cellWidth, cellHeight); canvas.drawRect(rect, paint); } } // 绘制中心白色十字 final paint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 5.0; canvas.drawLine(Offset(size.width / 2 - 50, size.height / 2), Offset(size.width / 2 + 50, size.height / 2), paint); canvas.drawLine(Offset(size.width / 2, size.height / 2 - 50), Offset(size.width / 2, size.height / 2 + 50), paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return false; } } // 用于存储H264文件中解析出的帧 class H264Frame { final Uint8List data; final FrameType type; H264Frame(this.data, this.type); } // H264 NAL 单元类型 class NalUnitType { static const int UNSPECIFIED = 0; static const int CODED_SLICE_NON_IDR = 1; // P帧 static const int CODED_SLICE_IDR = 5; // I帧 static const int SPS = 7; // 序列参数集 static const int PPS = 8; // 图像参数集 // 获取类型名称 static String getName(int type) { switch (type) { case UNSPECIFIED: return "未指定"; case CODED_SLICE_NON_IDR: return "P帧"; case CODED_SLICE_IDR: return "I帧"; case SPS: return "SPS"; case PPS: return "PPS"; default: return "未知($type)"; } } } void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: '视频解码演示', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), home: const VideoView(), ); } } class VideoView extends StatefulWidget { const VideoView({Key? key}) : super(key: key); @override State createState() => _VideoViewState(); } class _VideoViewState extends State { // 解码器状态 int? _textureId; bool _isInitialized = false; bool _isPlaying = false; String _statusText = "未初始化"; String _error = ""; // 帧统计 int _renderedFrameCount = 0; DateTime? _lastFrameTime; double _fps = 0; // H264文件解析 Uint8List? _h264FileData; List _h264Frames = []; int _currentFrameIndex = 0; // 解码定时器 Timer? _frameTimer; // 日志 final List _logs = []; final ScrollController _logScrollController = ScrollController(); @override void initState() { super.initState(); _loadH264File(); } @override void dispose() { _stopPlaying(); _releaseDecoder(); _frameTimer?.cancel(); super.dispose(); } // 加载H264文件 Future _loadH264File() async { try { _log("正在加载 demo.h264 文件..."); final ByteData data = await rootBundle.load('assets/demo.h264'); setState(() { _h264FileData = data.buffer.asUint8List(); }); _log("H264文件加载完成: ${_h264FileData!.length} 字节"); // 解析H264文件 _parseH264File(); } catch (e) { _log("加载H264文件失败: $e"); setState(() { _error = "加载H264文件失败: $e"; }); } } // 解析H264文件,提取NAL单元 void _parseH264File() { if (_h264FileData == null) return; _log("开始解析H264文件..."); List frames = []; // 查找起始码 0x00000001 或 0x000001 int startIndex = 0; bool hasSps = false; bool hasPps = false; while (startIndex < _h264FileData!.length - 4) { // 查找下一个起始码 int nextStartIndex = _findStartCode(_h264FileData!, startIndex + 3); if (nextStartIndex == -1) { nextStartIndex = _h264FileData!.length; } // 提取NAL单元,跳过起始码(3或4字节) int skipBytes = (_h264FileData![startIndex] == 0x00 && _h264FileData![startIndex + 1] == 0x00 && _h264FileData![startIndex + 2] == 0x00 && _h264FileData![startIndex + 3] == 0x01) ? 4 : 3; if (nextStartIndex > startIndex + skipBytes) { // 获取NAL类型 int nalType = _h264FileData![startIndex + skipBytes] & 0x1F; // 创建NAL单元数据 var nalData = Uint8List(nextStartIndex - startIndex); for (int i = 0; i < nalData.length; i++) { nalData[i] = _h264FileData![startIndex + i]; } // 根据NAL类型分类 switch (nalType) { case NalUnitType.SPS: _log("找到SPS: 位置=${startIndex}, 长度=${nalData.length}"); hasSps = true; frames.add(H264Frame(nalData, FrameType.iFrame)); break; case NalUnitType.PPS: _log("找到PPS: 位置=${startIndex}, 长度=${nalData.length}"); hasPps = true; frames.add(H264Frame(nalData, FrameType.iFrame)); break; case NalUnitType.CODED_SLICE_IDR: _log("找到I帧: 位置=${startIndex}, 长度=${nalData.length}"); frames.add(H264Frame(nalData, FrameType.iFrame)); break; case NalUnitType.CODED_SLICE_NON_IDR: frames.add(H264Frame(nalData, FrameType.pFrame)); break; default: // 其他类型的NAL单元也添加进去 frames.add(H264Frame(nalData, FrameType.pFrame)); break; } } startIndex = nextStartIndex; } setState(() { _h264Frames = frames; }); _log("H264文件解析完成,找到 ${frames.length} 个帧,包含SPS=${hasSps}, PPS=${hasPps}"); } // 查找起始码的辅助方法 int _findStartCode(Uint8List data, int offset) { for (int i = offset; i < data.length - 3; i++) { // 检查是否为0x000001 if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01) { return i; } // 检查是否为0x00000001 if (i < data.length - 4 && data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) { return i; } } return -1; } // 查找NAL类型的辅助方法(用于调试) int _getNalType(Uint8List data) { // 打印头几个字节 String headerBytes = ''; for (int i = 0; i < math.min(16, data.length); i++) { headerBytes += '${data[i].toRadixString(16).padLeft(2, '0')} '; } _log("帧数据头: $headerBytes"); // 尝试找到起始码位置 int nalOffset = -1; // 检查标准起始码 if (data.length > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01) { nalOffset = 4; _log("找到4字节起始码 (0x00000001) 位置: 0"); } else if (data.length > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01) { nalOffset = 3; _log("找到3字节起始码 (0x000001) 位置: 0"); } else { // 尝试搜索起始码 for (int i = 0; i < data.length - 4; i++) { if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) { nalOffset = i + 4; _log("在偏移量 $i 处找到4字节起始码"); break; } else if (i < data.length - 3 && data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01) { nalOffset = i + 3; _log("在偏移量 $i 处找到3字节起始码"); break; } } } // 如果找到了起始码 if (nalOffset >= 0 && nalOffset < data.length) { int nalType = data[nalOffset] & 0x1F; _log("解析NAL类型: ${NalUnitType.getName(nalType)} ($nalType)"); return nalType; } _log("无法解析NAL类型"); return -1; } // 当新帧可用时调用 void _onFrameAvailable(int textureId) { if (!mounted) return; _log("收到帧回调 - 渲染帧 ${_renderedFrameCount + 1}"); // 更新帧时间用于FPS计算 _lastFrameTime = DateTime.now(); // 立即更新UI以显示新帧 setState(() { _renderedFrameCount++; }); } Future _initializeDecoder() async { if (_isInitialized) { await _releaseDecoder(); } _log("正在初始化解码器"); try { final config = VideoDecoderConfig( width: 640, height: 480, codecType: CodecType.h264, frameRate: 30, bufferSize: 30, isDebug: true, // 打开调试日志 ); final textureId = await VideoDecodePlugin.initDecoder(config); if (textureId != null) { _textureId = textureId; // 设置帧回调 VideoDecodePlugin.setFrameCallbackForTexture( textureId, _onFrameAvailable); setState(() { _isInitialized = true; _error = ""; _statusText = "就绪"; _renderedFrameCount = 0; // 重置帧计数 }); _log("解码器初始化成功,纹理ID: $_textureId"); // 自动发送测试帧以触发渲染 await _sendTestIFrame(); } else { setState(() { _error = "获取纹理ID失败"; _statusText = "初始化失败"; }); _log("解码器初始化失败 - 返回空纹理ID"); } } catch (e) { setState(() { _error = e.toString(); _statusText = "初始化错误"; }); _log("解码器初始化错误: $e"); } } // 添加一个测试I帧来触发渲染 Future _sendTestIFrame() async { if (_textureId == null || !_isInitialized) { _log("解码器未准备好,无法发送测试帧"); return; } _log("生成并发送测试I帧"); // 创建一个简单的NAL单元 (IDR帧) // 5字节的起始码 + NAL类型5(I帧) + 一些简单的数据 List testFrameData = [ 0x00, 0x00, 0x00, 0x01, 0x65, // 起始码 + NAL类型 (0x65 = 101|0101 -> 类型5) 0x88, 0x84, 0x21, 0x43, 0x14, 0x56, 0x32, 0x80 // 一些随机数据 ]; Uint8List testFrame = Uint8List.fromList(testFrameData); try { _log("发送测试I帧: ${testFrame.length} 字节"); bool success = await VideoDecodePlugin.decodeFrameForTexture( _textureId!, testFrame, FrameType.iFrame); _log("测试I帧发送结果: ${success ? '成功' : '失败'}"); } catch (e) { _log("发送测试帧错误: $e"); } } Future _releaseDecoder() async { if (_textureId != null) { _log("正在释放解码器资源"); try { await VideoDecodePlugin.releaseDecoderForTexture(_textureId!); setState(() { _textureId = null; _isInitialized = false; _statusText = "已释放"; }); _log("解码器资源释放成功"); } catch (e) { _log("释放解码器错误: $e"); } } } Future _startPlaying() async { if (!_isInitialized || _isPlaying || _h264Frames.isEmpty) { _log( "播放条件未满足: 初始化=${_isInitialized}, 播放中=${_isPlaying}, 帧数量=${_h264Frames.length}"); return; } _log("开始播放H264视频"); try { // 重置帧率跟踪 _renderedFrameCount = 0; _lastFrameTime = null; _fps = 0; _currentFrameIndex = 0; // 确保首先发送SPS和PPS await _sendSpsAndPps(); // 开始解码帧 _startDecodingFrames(); setState(() { _isPlaying = true; _statusText = "播放中"; }); _log("播放已开始"); } catch (e) { _log("播放开始错误: $e"); } } // 发送SPS和PPS Future _sendSpsAndPps() async { for (int i = 0; i < math.min(10, _h264Frames.length); i++) { H264Frame frame = _h264Frames[i]; // 检查是否是SPS或PPS (通过检查NAL类型) if (frame.data.length > 4) { int skipBytes = (frame.data[0] == 0x00 && frame.data[1] == 0x00 && frame.data[2] == 0x00 && frame.data[3] == 0x01) ? 4 : 3; if (skipBytes < frame.data.length) { int nalType = frame.data[skipBytes] & 0x1F; if (nalType == NalUnitType.SPS || nalType == NalUnitType.PPS) { _log("发送${nalType == NalUnitType.SPS ? 'SPS' : 'PPS'}数据"); await _decodeNextFrame(frame); // 发送后等待一小段时间,确保解码器处理 await Future.delayed(Duration(milliseconds: 30)); } } } } } void _stopPlaying() { if (!_isPlaying) return; _log("停止播放"); _frameTimer?.cancel(); _frameTimer = null; setState(() { _isPlaying = false; _statusText = "已停止"; }); _log("播放已停止"); } void _startDecodingFrames() { _log("开始解码视频帧"); // 使用更低的帧率更稳定 const int frameIntervalMs = 50; // 20 fps _frameTimer = Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) async { if (_currentFrameIndex >= _h264Frames.length) { _log("所有帧已解码,重新开始"); _currentFrameIndex = 0; // 重新发送SPS和PPS await _sendSpsAndPps(); } final frame = _h264Frames[_currentFrameIndex]; await _decodeNextFrame(frame); _currentFrameIndex++; }); } Future _decodeNextFrame(H264Frame frame) async { if (_textureId == null || !_isInitialized) return; try { // 检查帧的NAL类型(仅用于调试) int nalType = _getNalType(frame.data); final success = await VideoDecodePlugin.decodeFrameForTexture( _textureId!, frame.data, frame.type, ); if (!success) { _log( "解码帧失败,索引 $_currentFrameIndex (${frame.type}), NAL类型: ${NalUnitType.getName(nalType)}"); } else { _log( "解码帧成功,索引 $_currentFrameIndex (${frame.type}), NAL类型: ${NalUnitType.getName(nalType)}"); } } catch (e) { _log("解码帧错误: $e"); } } void _log(String message) { final timestamp = DateTime.now().toString().split('.').first; final logMessage = "[$timestamp] $message"; setState(() { _logs.add(logMessage); if (_logs.length > 100) { _logs.removeAt(0); } }); // 滚动到底部 WidgetsBinding.instance.addPostFrameCallback((_) { if (_logScrollController.hasClients) { _logScrollController.animateTo( _logScrollController.position.maxScrollExtent, duration: Duration(milliseconds: 200), curve: Curves.easeOut, ); } }); } Widget _buildVideoDisplay() { if (_textureId == null) { return Center( child: Container( width: 640, height: 480, color: Colors.black, child: CustomPaint( painter: TestPatternPainter(), child: Center( child: Text( '无可用纹理', style: TextStyle(color: Colors.white), ), ), ), ), ); } return Stack( fit: StackFit.expand, children: [ // 背景色 Container(color: Colors.black), // 测试图案 - 如果没有渲染任何帧则显示 if (_renderedFrameCount == 0) CustomPaint(painter: TestPatternPainter()), // 视频纹理 - 使用RepaintBoundary和ValueKey确保正确更新 RepaintBoundary( child: Texture( textureId: _textureId!, filterQuality: FilterQuality.medium, key: ValueKey('texture_${_renderedFrameCount}'), ), ), // 显示帧计数 - 调试用 Positioned( right: 10, top: 10, child: Container( padding: EdgeInsets.all(5), color: Colors.black.withOpacity(0.5), child: Text( '帧: $_renderedFrameCount', style: TextStyle(color: Colors.white, fontSize: 12), ), ), ), ], ); } @override Widget build(BuildContext context) { // 更新FPS计算 if (_lastFrameTime != null && _renderedFrameCount > 0) { final now = DateTime.now(); final elapsed = now.difference(_lastFrameTime!).inMilliseconds; if (elapsed > 0) { _fps = 1000 / elapsed; } } return Scaffold( appBar: AppBar( title: Text('视频解码插件演示'), backgroundColor: Theme.of(context).colorScheme.primaryContainer, ), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 视频显示区域 - 使用AspectRatio控制大小 AspectRatio( aspectRatio: 16 / 9, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), child: _buildVideoDisplay(), ), ), // 控制面板和状态信息 - 使用Expanded和SingleChildScrollView Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 状态信息区 - 使用Card使其更紧凑 Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('状态: $_statusText', style: TextStyle( fontWeight: FontWeight.bold)), Text('FPS: ${_fps.toStringAsFixed(1)}'), ], ), if (_error.isNotEmpty) Text('错误: $_error', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold)), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('已渲染帧数: $_renderedFrameCount'), Text('解析的帧数: ${_h264Frames.length}'), ], ), Text( 'H264文件大小: ${(_h264FileData?.length ?? 0) / 1024} KB'), ], ), ), ), // 控制按钮区 Card( child: Padding( padding: const EdgeInsets.all(8.0), child: Wrap( spacing: 8, runSpacing: 8, children: [ ElevatedButton( onPressed: _isInitialized ? null : _initializeDecoder, child: Text('初始化'), ), ElevatedButton( onPressed: (!_isInitialized || _isPlaying || _h264Frames.isEmpty) ? null : _startPlaying, child: Text('播放'), ), ElevatedButton( onPressed: (!_isPlaying) ? null : _stopPlaying, child: Text('停止'), ), ElevatedButton( onPressed: (_textureId == null) ? null : _releaseDecoder, child: Text('释放'), ), ElevatedButton( onPressed: () { setState(() { // 强制重绘 _renderedFrameCount++; }); }, child: Text('刷新'), ), ], ), ), ), // 日志区域 SizedBox(height: 8), Text('日志:', style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 4), Container( height: 280, decoration: BoxDecoration( color: Colors.black, border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(4.0), ), padding: EdgeInsets.all(4), child: ListView.builder( controller: _logScrollController, itemCount: _logs.length, itemBuilder: (context, index) { return Text( _logs[index], style: TextStyle( color: _logs[index].contains('错误') ? Colors.red : Colors.green, fontSize: 11, fontFamily: 'monospace', ), ); }, ), ), ], ), ), ), ), ], ), ), ); } }