835 lines
25 KiB
Dart
Raw Normal View History

2025-04-21 10:56:28 +08:00
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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
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<String, dynamic> _decoderStats = {};
// 需要强制更新Texture
bool _needsTextureUpdate = false;
// 诊断日志
List<String> _logs = [];
ScrollController _logScrollController = ScrollController();
// 帧分隔符 (NAL 单元起始码)
final List<int> _startCode = [0, 0, 0, 1];
// 当前解析位置
int _parsePosition = 0;
// 帧索引位置列表 (每个帧的起始位置)
List<int> _framePositions = [];
// 帧类型列表
List<FrameType> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<int>(
_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('强制刷新'),
),
),
],
),
],
),
),
),
);
}
}