2025-04-28 09:11:51 +08:00

835 lines
25 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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('强制刷新'),
),
),
],
),
],
),
),
),
);
}
}