835 lines
25 KiB
Dart
835 lines
25 KiB
Dart
|
|
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('强制刷新'),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|