1229 lines
40 KiB
Dart
Raw Normal View History

2025-04-21 10:56:28 +08:00
import 'dart:async';
2025-04-21 15:11:23 +08:00
import 'dart:io';
2025-04-21 10:56:28 +08:00
import 'dart:typed_data';
2025-04-21 15:11:23 +08:00
import 'dart:math' as math;
2025-04-21 10:56:28 +08:00
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:video_decode_plugin/video_decode_plugin.dart';
2025-04-21 15:11:23 +08:00
// 测试图案绘制器
class TestPatternPainter extends CustomPainter {
2025-04-21 10:56:28 +08:00
@override
2025-04-21 15:11:23 +08:00
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);
}
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 绘制中心白色十字
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
canvas.drawLine(Offset(size.width / 2 - 50, size.height / 2),
Offset(size.width / 2 + 50, size.height / 2), paint);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
canvas.drawLine(Offset(size.width / 2, size.height / 2 - 50),
Offset(size.width / 2, size.height / 2 + 50), paint);
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 用于存储H264文件中解析出的帧
class H264Frame {
final Uint8List data;
final FrameType type;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
H264Frame(this.data, this.type);
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// H264 NAL 单元类型
class NalUnitType {
static const int UNSPECIFIED = 0;
static const int CODED_SLICE_NON_IDR = 1; // P帧
2025-04-23 10:37:53 +08:00
static const int PARTITION_A = 2;
static const int PARTITION_B = 3;
static const int PARTITION_C = 4;
2025-04-21 15:11:23 +08:00
static const int CODED_SLICE_IDR = 5; // I帧
2025-04-23 10:37:53 +08:00
static const int SEI = 6; // 补充增强信息
2025-04-21 15:11:23 +08:00
static const int SPS = 7; // 序列参数集
static const int PPS = 8; // 图像参数集
2025-04-23 10:37:53 +08:00
static const int AUD = 9; // 访问单元分隔符
static const int END_SEQUENCE = 10; // 序列结束
static const int END_STREAM = 11; // 码流结束
static const int FILLER_DATA = 12; // 填充数据
static const int SPS_EXT = 13; // SPS扩展
static const int PREFIX_NAL = 14; // 前缀NAL单元
static const int SUBSET_SPS = 15; // 子集SPS
static const int DEPTH_PARAM_SET = 16; // 深度参数集
static const int RESERVED_17 = 17; // 保留类型
static const int RESERVED_18 = 18; // 保留类型
static const int CODED_SLICE_AUX = 19; // 辅助切片
static const int CODED_SLICE_EXTENSION = 20; // 扩展切片
static const int CODED_SLICE_DEPTH = 21; // 深度扩展
static const int RESERVED_22 = 22; // 保留类型
static const int RESERVED_23 = 23; // 保留类型
static const int STAP_A = 24; // 单时间聚合包A
static const int STAP_B = 25; // 单时间聚合包B
static const int MTAP_16 = 26; // 多时间聚合包16
static const int MTAP_24 = 27; // 多时间聚合包24
static const int FU_A = 28; // 分片单元A
static const int FU_B = 29; // 分片单元B
static const int RESERVED_30 = 30; // 保留类型
static const int RESERVED_31 = 31; // 保留类型
// 帧类型别名,方便调用
static const int NON_IDR_PICTURE = CODED_SLICE_NON_IDR; // P帧
static const int IDR_PICTURE = CODED_SLICE_IDR; // I帧
2025-04-21 15:11:23 +08:00
// 获取类型名称
static String getName(int type) {
switch (type) {
case UNSPECIFIED:
return "未指定";
case CODED_SLICE_NON_IDR:
return "P帧";
2025-04-23 10:37:53 +08:00
case PARTITION_A:
return "分区A";
case PARTITION_B:
return "分区B";
case PARTITION_C:
return "分区C";
2025-04-21 15:11:23 +08:00
case CODED_SLICE_IDR:
return "I帧";
2025-04-23 10:37:53 +08:00
case SEI:
return "SEI";
2025-04-21 15:11:23 +08:00
case SPS:
return "SPS";
case PPS:
return "PPS";
2025-04-23 10:37:53 +08:00
case AUD:
return "AUD";
case END_SEQUENCE:
return "序列结束";
case END_STREAM:
return "码流结束";
case FILLER_DATA:
return "填充数据";
case SPS_EXT:
return "SPS扩展";
case PREFIX_NAL:
return "前缀NAL";
case SUBSET_SPS:
return "子集SPS";
case CODED_SLICE_AUX:
return "辅助切片";
case CODED_SLICE_EXTENSION:
return "扩展切片";
case FU_A:
return "分片单元A";
case FU_B:
return "分片单元B";
2025-04-21 15:11:23 +08:00
default:
return "未知($type)";
}
}
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
void main() {
runApp(const MyApp());
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '视频解码演示',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const VideoView(),
);
}
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
class VideoView extends StatefulWidget {
const VideoView({Key? key}) : super(key: key);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
@override
State<VideoView> createState() => _VideoViewState();
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
class _VideoViewState extends State<VideoView> {
// 解码器状态
int? _textureId;
bool _isInitialized = false;
bool _isPlaying = false;
String _statusText = "未初始化";
String _error = "";
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 帧统计
int _renderedFrameCount = 0;
DateTime? _lastFrameTime;
double _fps = 0;
2025-04-23 10:37:53 +08:00
double _decoderFps = 0; // 解码器内部计算的FPS
// 动态阈值参数
int _detectedGopSize = 0;
int _dynamicMaxPFrames = 0;
int _dynamicIFrameTimeoutMs = 0;
bool _enableDynamicThresholds = true;
// 用于刷新解码器统计信息的定时器
Timer? _statsTimer;
// 丢包模拟相关
bool _enablePacketLoss = false;
double _packetLossRate = 0.1; // 默认10%丢包率
bool _dropIFrames = false;
bool _dropPFrames = false;
bool _dropSPSPPS = false;
bool _burstPacketLossMode = false;
int _burstPacketLossCounter = 0;
int _droppedFramesCount = 0;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// H264文件解析
Uint8List? _h264FileData;
List<H264Frame> _h264Frames = [];
int _currentFrameIndex = 0;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 解码定时器
Timer? _frameTimer;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 日志
final List<String> _logs = [];
final ScrollController _logScrollController = ScrollController();
2025-04-21 10:56:28 +08:00
2025-04-23 10:37:53 +08:00
// 视频显示相关的属性
bool _showingErrorFrame = false;
Timer? _errorFrameResetTimer;
2025-04-21 10:56:28 +08:00
@override
void initState() {
super.initState();
_loadH264File();
2025-04-23 10:37:53 +08:00
// 启动定时器刷新解码器统计信息
_statsTimer = Timer.periodic(Duration(milliseconds: 1000), (timer) {
if (_isInitialized && _textureId != null) {
_updateDecoderStats();
}
});
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
@override
void dispose() {
_stopPlaying();
_releaseDecoder();
_frameTimer?.cancel();
2025-04-23 10:37:53 +08:00
_statsTimer?.cancel(); // 停止统计信息更新定时器
2025-04-21 15:11:23 +08:00
super.dispose();
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
// 加载H264文件
2025-04-21 10:56:28 +08:00
Future<void> _loadH264File() async {
try {
2025-04-21 15:11:23 +08:00
_log("正在加载 demo.h264 文件...");
2025-04-21 10:56:28 +08:00
final ByteData data = await rootBundle.load('assets/demo.h264');
setState(() {
2025-04-21 15:11:23 +08:00
_h264FileData = data.buffer.asUint8List();
2025-04-21 10:56:28 +08:00
});
2025-04-21 15:11:23 +08:00
_log("H264文件加载完成: ${_h264FileData!.length} 字节");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 解析H264文件
2025-04-21 10:56:28 +08:00
_parseH264File();
} catch (e) {
2025-04-21 15:11:23 +08:00
_log("加载H264文件失败: $e");
2025-04-21 10:56:28 +08:00
setState(() {
2025-04-21 15:11:23 +08:00
_error = "加载H264文件失败: $e";
2025-04-21 10:56:28 +08:00
});
}
}
2025-04-21 15:11:23 +08:00
// 解析H264文件提取NAL单元
2025-04-21 10:56:28 +08:00
void _parseH264File() {
2025-04-21 15:11:23 +08:00
if (_h264FileData == null) return;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("开始解析H264文件...");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
List<H264Frame> frames = [];
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 查找起始码 0x00000001 或 0x000001
int startIndex = 0;
bool hasSps = false;
bool hasPps = false;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
while (startIndex < _h264FileData!.length - 4) {
// 查找下一个起始码
int nextStartIndex = _findStartCode(_h264FileData!, startIndex + 3);
if (nextStartIndex == -1) {
nextStartIndex = _h264FileData!.length;
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 提取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];
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 根据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;
2025-04-21 10:56:28 +08:00
}
}
2025-04-21 15:11:23 +08:00
startIndex = nextStartIndex;
2025-04-21 10:56:28 +08:00
}
setState(() {
2025-04-21 15:11:23 +08:00
_h264Frames = frames;
2025-04-21 10:56:28 +08:00
});
2025-04-21 15:11:23 +08:00
_log("H264文件解析完成找到 ${frames.length} 个帧包含SPS=${hasSps}, PPS=${hasPps}");
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
// 查找起始码的辅助方法
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;
}
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
return -1;
2025-04-21 10:56:28 +08:00
}
2025-04-23 10:37:53 +08:00
// 获取NAL类型
// 获取NAL类型
2025-04-21 15:11:23 +08:00
int _getNalType(Uint8List data) {
2025-04-23 10:37:53 +08:00
// 跳过起始码后再获取NAL单元类型
if (data.length > 4) {
// 检查是否有0x00000001的起始码
if (data[0] == 0x00 &&
data[1] == 0x00 &&
data[2] == 0x00 &&
data[3] == 0x01) {
// 起始码后的第一个字节的低5位是NAL类型
if (data.length > 4) {
return data[4] & 0x1F;
}
}
// 检查是否有0x000001的起始码
else if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01) {
// 起始码后的第一个字节的低5位是NAL类型
if (data.length > 3) {
return data[3] & 0x1F;
2025-04-21 10:56:28 +08:00
}
}
2025-04-21 15:11:23 +08:00
}
2025-04-23 10:37:53 +08:00
// 如果找不到起始码或数据不足,则尝试直接获取
if (data.length > 0) {
return data[0] & 0x1F;
2025-04-21 15:11:23 +08:00
}
2025-04-23 10:37:53 +08:00
return 0;
2025-04-21 15:11:23 +08:00
}
// 当新帧可用时调用
void _onFrameAvailable(int textureId) {
if (!mounted) return;
_log("收到帧回调 - 渲染帧 ${_renderedFrameCount + 1}");
// 更新帧时间用于FPS计算
_lastFrameTime = DateTime.now();
// 立即更新UI以显示新帧
setState(() {
_renderedFrameCount++;
2025-04-21 10:56:28 +08:00
});
}
2025-04-21 15:11:23 +08:00
Future<void> _initializeDecoder() async {
if (_isInitialized) {
await _releaseDecoder();
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
_log("正在初始化解码器");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
try {
2025-04-21 10:56:28 +08:00
final config = VideoDecoderConfig(
2025-04-21 15:11:23 +08:00
width: 640,
height: 480,
2025-04-21 10:56:28 +08:00
codecType: CodecType.h264,
2025-04-21 15:11:23 +08:00
frameRate: 30,
bufferSize: 30,
isDebug: true, // 打开调试日志
2025-04-23 10:37:53 +08:00
enableDynamicThresholds: _enableDynamicThresholds, // 使用动态阈值
initialMaxPFrames: 60, // 初始最大连续P帧数
initialIFrameTimeoutMs: 5000, // 初始I帧超时时间
minMaxPFrames: 5, // 最小最大连续P帧数
maxMaxPFrames: 60, // 最大最大连续P帧数
2025-04-21 10:56:28 +08:00
);
final textureId = await VideoDecodePlugin.initDecoder(config);
2025-04-21 15:11:23 +08:00
if (textureId != null) {
_textureId = textureId;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 设置帧回调
VideoDecodePlugin.setFrameCallbackForTexture(
textureId, _onFrameAvailable);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
setState(() {
_isInitialized = true;
_error = "";
_statusText = "就绪";
_renderedFrameCount = 0; // 重置帧计数
});
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("解码器初始化成功纹理ID: $_textureId");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 自动发送测试帧以触发渲染
await _sendTestIFrame();
} else {
setState(() {
_error = "获取纹理ID失败";
_statusText = "初始化失败";
});
_log("解码器初始化失败 - 返回空纹理ID");
}
2025-04-21 10:56:28 +08:00
} catch (e) {
setState(() {
2025-04-21 15:11:23 +08:00
_error = e.toString();
_statusText = "初始化错误";
2025-04-21 10:56:28 +08:00
});
2025-04-21 15:11:23 +08:00
_log("解码器初始化错误: $e");
2025-04-21 10:56:28 +08:00
}
}
2025-04-21 15:11:23 +08:00
// 添加一个测试I帧来触发渲染
Future<void> _sendTestIFrame() async {
if (_textureId == null || !_isInitialized) {
_log("解码器未准备好,无法发送测试帧");
return;
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("生成并发送测试I帧");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 创建一个简单的NAL单元 (IDR帧)
// 5字节的起始码 + NAL类型5(I帧) + 一些简单的数据
List<int> testFrameData = [
0x00, 0x00, 0x00, 0x01, 0x65, // 起始码 + NAL类型 (0x65 = 101|0101 -> 类型5)
0x88, 0x84, 0x21, 0x43, 0x14, 0x56, 0x32, 0x80 // 一些随机数据
];
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
Uint8List testFrame = Uint8List.fromList(testFrameData);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
try {
_log("发送测试I帧: ${testFrame.length} 字节");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
bool success = await VideoDecodePlugin.decodeFrameForTexture(
_textureId!, testFrame, FrameType.iFrame);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("测试I帧发送结果: ${success ? '成功' : '失败'}");
2025-04-21 10:56:28 +08:00
} catch (e) {
2025-04-21 15:11:23 +08:00
_log("发送测试帧错误: $e");
2025-04-21 10:56:28 +08:00
}
}
2025-04-21 15:11:23 +08:00
Future<void> _releaseDecoder() async {
2025-04-21 10:56:28 +08:00
if (_textureId != null) {
2025-04-21 15:11:23 +08:00
_log("正在释放解码器资源");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
try {
await VideoDecodePlugin.releaseDecoderForTexture(_textureId!);
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
setState(() {
_textureId = null;
_isInitialized = false;
_statusText = "已释放";
});
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("解码器资源释放成功");
} catch (e) {
_log("释放解码器错误: $e");
}
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
Future<void> _startPlaying() async {
if (!_isInitialized || _isPlaying || _h264Frames.isEmpty) {
_log(
"播放条件未满足: 初始化=${_isInitialized}, 播放中=${_isPlaying}, 帧数量=${_h264Frames.length}");
2025-04-21 10:56:28 +08:00
return;
}
2025-04-21 15:11:23 +08:00
_log("开始播放H264视频");
2025-04-21 10:56:28 +08:00
try {
2025-04-21 15:11:23 +08:00
// 重置帧率跟踪
_renderedFrameCount = 0;
_lastFrameTime = null;
_fps = 0;
_currentFrameIndex = 0;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 确保首先发送SPS和PPS
await _sendSpsAndPps();
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 开始解码帧
_startDecodingFrames();
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
setState(() {
_isPlaying = true;
_statusText = "播放中";
});
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("播放已开始");
} catch (e) {
_log("播放开始错误: $e");
}
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 发送SPS和PPS
Future<void> _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));
}
2025-04-21 10:56:28 +08:00
}
}
2025-04-21 15:11:23 +08:00
}
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
void _stopPlaying() {
if (!_isPlaying) return;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("停止播放");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_frameTimer?.cancel();
_frameTimer = null;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
setState(() {
_isPlaying = false;
_statusText = "已停止";
});
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_log("播放已停止");
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
void _startDecodingFrames() {
_log("开始解码视频帧");
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 使用更低的帧率更稳定
const int frameIntervalMs = 50; // 20 fps
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
_frameTimer =
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) async {
if (_currentFrameIndex >= _h264Frames.length) {
_log("所有帧已解码,重新开始");
_currentFrameIndex = 0;
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 重新发送SPS和PPS
await _sendSpsAndPps();
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
final frame = _h264Frames[_currentFrameIndex];
2025-04-23 10:37:53 +08:00
bool decodeSuccess = await _decodeNextFrame(frame);
// 只有在成功解码的情况下才显示日志信息
if (!decodeSuccess && _enablePacketLoss) {
_log("跳过索引 $_currentFrameIndex 的帧(丢帧模拟)");
}
// 无论解码是否成功,都移动到下一帧
2025-04-21 15:11:23 +08:00
_currentFrameIndex++;
});
}
2025-04-21 10:56:28 +08:00
2025-04-23 10:37:53 +08:00
Future<bool> _decodeNextFrame(H264Frame frame) async {
if (_textureId == null || !_isInitialized || !_isPlaying) {
return false;
}
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
try {
2025-04-23 10:37:53 +08:00
// 获取NAL类型
2025-04-21 15:11:23 +08:00
int nalType = _getNalType(frame.data);
2025-04-23 10:37:53 +08:00
// 模拟丢包
if (_enablePacketLoss) {
bool shouldDrop = false;
// 爆发式丢包模式
if (_burstPacketLossMode && _burstPacketLossCounter > 0) {
shouldDrop = true;
_burstPacketLossCounter--;
}
// 随机丢包
else if (math.Random().nextDouble() < _packetLossRate) {
shouldDrop = true;
// 触发爆发式丢包
if (_burstPacketLossMode) {
_burstPacketLossCounter = math.Random().nextInt(5) + 1; // 随机爆发1-5个包
}
}
// 特定类型NAL的丢包策略
if (nalType == NalUnitType.CODED_SLICE_IDR && _dropIFrames) {
shouldDrop = true;
} else if ((nalType == NalUnitType.CODED_SLICE_NON_IDR ||
nalType == NalUnitType.CODED_SLICE_EXTENSION) &&
_dropPFrames) {
shouldDrop = true;
} else if ((nalType == NalUnitType.SPS || nalType == NalUnitType.PPS) &&
_dropSPSPPS) {
shouldDrop = true;
}
if (shouldDrop) {
_droppedFramesCount++;
String nalTypeName = NalUnitType.getName(nalType);
_log("丢弃帧NAL类型 = $nalTypeName");
// 显示丢帧效果
setState(() {
_showingErrorFrame = true;
});
// 1秒后重置丢帧效果指示器
_errorFrameResetTimer?.cancel();
_errorFrameResetTimer = Timer(Duration(milliseconds: 1000), () {
if (mounted) {
setState(() {
_showingErrorFrame = false;
});
}
});
return false; // 直接返回false不进行解码
}
}
// 解码帧
2025-04-21 15:11:23 +08:00
final success = await VideoDecodePlugin.decodeFrameForTexture(
_textureId!,
frame.data,
frame.type,
);
if (!success) {
2025-04-23 10:37:53 +08:00
_log("解码帧失败,索引 $_currentFrameIndex (${frame.type})");
2025-04-21 15:11:23 +08:00
} else {
2025-04-23 10:37:53 +08:00
String nalTypeName = NalUnitType.getName(nalType);
2025-04-21 15:11:23 +08:00
_log(
2025-04-23 10:37:53 +08:00
"解码帧成功,索引 $_currentFrameIndex (${frame.type}), NAL类型: $nalTypeName");
2025-04-21 10:56:28 +08:00
}
2025-04-23 10:37:53 +08:00
return success;
2025-04-21 10:56:28 +08:00
} catch (e) {
2025-04-21 15:11:23 +08:00
_log("解码帧错误: $e");
2025-04-23 10:37:53 +08:00
return false;
2025-04-21 10:56:28 +08:00
}
}
2025-04-21 15:11:23 +08:00
void _log(String message) {
final timestamp = DateTime.now().toString().split('.').first;
final logMessage = "[$timestamp] $message";
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
setState(() {
_logs.add(logMessage);
if (_logs.length > 100) {
_logs.removeAt(0);
}
});
2025-04-21 10:56:28 +08:00
2025-04-21 15:11:23 +08:00
// 滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_logScrollController.hasClients) {
_logScrollController.animateTo(
_logScrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 200),
curve: Curves.easeOut,
);
}
});
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
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),
),
),
),
),
);
2025-04-21 10:56:28 +08:00
}
2025-04-21 15:11:23 +08:00
return Stack(
fit: StackFit.expand,
2025-04-21 10:56:28 +08:00
children: [
2025-04-21 15:11:23 +08:00
// 背景色
Container(color: Colors.black),
// 测试图案 - 如果没有渲染任何帧则显示
if (_renderedFrameCount == 0)
CustomPaint(painter: TestPatternPainter()),
// 视频纹理 - 使用RepaintBoundary和ValueKey确保正确更新
RepaintBoundary(
child: Texture(
textureId: _textureId!,
filterQuality: FilterQuality.medium,
key: ValueKey('texture_${_renderedFrameCount}'),
),
),
2025-04-21 10:56:28 +08:00
2025-04-23 10:37:53 +08:00
// 丢包效果指示器 - 当有帧被丢弃时,上方显示红色条带
if (_enablePacketLoss && _droppedFramesCount > 0)
Positioned(
top: 0,
left: 0,
right: 0,
height: 5,
child: Container(
color: Colors.red.withOpacity(0.8),
),
),
2025-04-21 15:11:23 +08:00
// 显示帧计数 - 调试用
Positioned(
right: 10,
top: 10,
child: Container(
padding: EdgeInsets.all(5),
color: Colors.black.withOpacity(0.5),
child: Text(
2025-04-23 10:37:53 +08:00
'帧: $_renderedFrameCount${_enablePacketLoss ? ' (丢帧: $_droppedFramesCount)' : ''}',
2025-04-21 15:11:23 +08:00
style: TextStyle(color: Colors.white, fontSize: 12),
2025-04-21 10:56:28 +08:00
),
2025-04-21 15:11:23 +08:00
),
),
],
2025-04-21 10:56:28 +08:00
);
}
@override
Widget build(BuildContext context) {
2025-04-21 15:11:23 +08:00
// 更新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,
2025-04-21 10:56:28 +08:00
),
2025-04-21 15:11:23 +08:00
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,
2025-04-21 10:56:28 +08:00
children: [
2025-04-21 15:11:23 +08:00
// 状态信息区 - 使用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)),
2025-04-23 10:37:53 +08:00
// Text('计算FPS: ${_fps.toStringAsFixed(1)}'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'解码器FPS: ${_decoderFps.toStringAsFixed(1)}',style: TextStyle(
color: Colors.green
),),
Text('已渲染帧数: $_renderedFrameCount'),
2025-04-21 15:11:23 +08:00
],
),
if (_error.isNotEmpty)
Text('错误: $_error',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
2025-04-23 10:37:53 +08:00
Text('检测到的GOP: $_detectedGopSize'),
2025-04-21 15:11:23 +08:00
Text('解析的帧数: ${_h264Frames.length}'),
],
),
Text(
'H264文件大小: ${(_h264FileData?.length ?? 0) / 1024} KB'),
2025-04-23 10:37:53 +08:00
// 动态阈值参数显示
if (_enableDynamicThresholds)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('动态阈值参数:',
style: TextStyle(
fontWeight: FontWeight.bold)),
Text('最大连续P帧: $_dynamicMaxPFrames'),
Text(
'I帧超时: ${_dynamicIFrameTimeoutMs}ms'),
],
),
),
2025-04-21 15:11:23 +08:00
],
),
2025-04-21 10:56:28 +08:00
),
),
2025-04-21 15:11:23 +08:00
// 控制按钮区
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('刷新'),
2025-04-21 10:56:28 +08:00
),
2025-04-21 15:11:23 +08:00
],
2025-04-21 10:56:28 +08:00
),
),
2025-04-21 15:11:23 +08:00
),
2025-04-23 10:37:53 +08:00
// 丢包模拟控制面板
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text('丢包模拟',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16)),
Switch(
value: _enablePacketLoss,
onChanged: (value) {
setState(() {
_enablePacketLoss = value;
if (value) {
_droppedFramesCount = 0; // 重置丢帧计数
}
});
},
),
],
),
// 动态阈值开关
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text('动态阈值',
style: TextStyle(
fontWeight: FontWeight.bold)),
Switch(
value: _enableDynamicThresholds,
onChanged: (value) {
setState(() {
_enableDynamicThresholds = value;
// 需要重新初始化解码器以应用新设置
if (_isInitialized) {
_log("更改动态阈值设置,需要重新初始化解码器");
// 如果正在播放,先停止
if (_isPlaying) {
_stopPlaying();
}
// 延迟一下再重新初始化
Future.delayed(
Duration(milliseconds: 100), () {
_initializeDecoder();
});
}
});
},
),
],
),
Divider(),
// 丢包率控制
Row(
children: [
Text(
'丢包率: ${(_packetLossRate * 100).toStringAsFixed(0)}%'),
Expanded(
child: Slider(
value: _packetLossRate,
min: 0.0,
max: 1.0,
divisions: 20,
onChanged: _enablePacketLoss
? (value) {
setState(() {
_packetLossRate = value;
});
}
: null,
),
),
],
),
// 丢包模式选择
Row(
children: [
Expanded(
child: CheckboxListTile(
dense: true,
title: Text('爆发式丢包'),
value: _burstPacketLossMode,
onChanged: _enablePacketLoss
? (value) {
setState(() {
_burstPacketLossMode = value!;
});
}
: null,
),
),
Expanded(
child: CheckboxListTile(
dense: true,
title: Text('丢弃I帧'),
value: _dropIFrames,
onChanged: _enablePacketLoss
? (value) {
setState(() {
_dropIFrames = value!;
});
}
: null,
),
),
],
),
Row(
children: [
Expanded(
child: CheckboxListTile(
dense: true,
title: Text('丢弃P帧'),
value: _dropPFrames,
onChanged: _enablePacketLoss
? (value) {
setState(() {
_dropPFrames = value!;
});
}
: null,
),
),
Expanded(
child: CheckboxListTile(
dense: true,
title: Text('丢弃SPS/PPS'),
value: _dropSPSPPS,
onChanged: _enablePacketLoss
? (value) {
setState(() {
_dropSPSPPS = value!;
});
}
: null,
),
),
],
),
// 丢包统计信息
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
'已丢弃帧数: $_droppedFramesCount',
style: TextStyle(
color: _droppedFramesCount > 0
? Colors.red
: Colors.black),
),
),
],
),
),
),
2025-04-21 15:11:23 +08:00
// 日志区域
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',
),
);
},
),
),
2025-04-21 10:56:28 +08:00
],
),
),
),
2025-04-21 15:11:23 +08:00
),
],
2025-04-21 10:56:28 +08:00
),
),
);
}
2025-04-23 10:37:53 +08:00
// 更新解码器统计信息
Future<void> _updateDecoderStats() async {
if (_textureId == null || !_isInitialized) return;
try {
// 获取FPS
final fps = await VideoDecodePlugin.getCurrentFps(_textureId);
// 获取动态阈值参数
final thresholdParams =
await VideoDecodePlugin.getDynamicThresholdParams(_textureId);
if (mounted) {
setState(() {
_decoderFps = fps;
_detectedGopSize = thresholdParams['detectedGopSize'] ?? 0;
_dynamicMaxPFrames =
thresholdParams['dynamicMaxConsecutivePFrames'] ?? 0;
_dynamicIFrameTimeoutMs =
thresholdParams['dynamicIFrameTimeoutMs'] ?? 0;
_enableDynamicThresholds =
thresholdParams['enableDynamicThresholds'] ?? true;
});
}
} catch (e) {
_log("获取解码器统计信息失败: $e");
}
}
}
// 添加错误帧绘制器
class ErrorFramePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final random = math.Random();
final paint = Paint()
..color = Colors.red.withOpacity(0.2)
..style = PaintingStyle.fill;
// 绘制随机条纹效果
for (int i = 0; i < 20; i++) {
double y = random.nextDouble() * size.height;
canvas.drawRect(
Rect.fromLTWH(0, y, size.width, 3 + random.nextDouble() * 5),
paint,
);
}
// 绘制马赛克块
for (int i = 0; i < 50; i++) {
double x = random.nextDouble() * size.width;
double y = random.nextDouble() * size.height;
double blockSize = 10 + random.nextDouble() * 30;
canvas.drawRect(
Rect.fromLTWH(x, y, blockSize, blockSize),
Paint()
..color = Color.fromRGBO(random.nextInt(255), random.nextInt(255),
random.nextInt(255), 0.3));
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; // 每次都重新绘制以产生动态效果
}
2025-04-21 10:56:28 +08:00
}