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

1229 lines
40 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: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 PARTITION_A = 2;
static const int PARTITION_B = 3;
static const int PARTITION_C = 4;
static const int CODED_SLICE_IDR = 5; // I帧
static const int SEI = 6; // 补充增强信息
static const int SPS = 7; // 序列参数集
static const int PPS = 8; // 图像参数集
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帧
// 获取类型名称
static String getName(int type) {
switch (type) {
case UNSPECIFIED:
return "未指定";
case CODED_SLICE_NON_IDR:
return "P帧";
case PARTITION_A:
return "分区A";
case PARTITION_B:
return "分区B";
case PARTITION_C:
return "分区C";
case CODED_SLICE_IDR:
return "I帧";
case SEI:
return "SEI";
case SPS:
return "SPS";
case PPS:
return "PPS";
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";
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<VideoView> createState() => _VideoViewState();
}
class _VideoViewState extends State<VideoView> {
// 解码器状态
int? _textureId;
bool _isInitialized = false;
bool _isPlaying = false;
String _statusText = "未初始化";
String _error = "";
// 帧统计
int _renderedFrameCount = 0;
DateTime? _lastFrameTime;
double _fps = 0;
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;
// H264文件解析
Uint8List? _h264FileData;
List<H264Frame> _h264Frames = [];
int _currentFrameIndex = 0;
// 解码定时器
Timer? _frameTimer;
// 日志
final List<String> _logs = [];
final ScrollController _logScrollController = ScrollController();
// 视频显示相关的属性
bool _showingErrorFrame = false;
Timer? _errorFrameResetTimer;
@override
void initState() {
super.initState();
_loadH264File();
// 启动定时器刷新解码器统计信息
_statsTimer = Timer.periodic(Duration(milliseconds: 1000), (timer) {
if (_isInitialized && _textureId != null) {
_updateDecoderStats();
}
});
}
@override
void dispose() {
_stopPlaying();
_releaseDecoder();
_frameTimer?.cancel();
_statsTimer?.cancel(); // 停止统计信息更新定时器
super.dispose();
}
// 加载H264文件
Future<void> _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<H264Frame> 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类型
// 获取NAL类型
int _getNalType(Uint8List data) {
// 跳过起始码后再获取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;
}
}
}
// 如果找不到起始码或数据不足,则尝试直接获取
if (data.length > 0) {
return data[0] & 0x1F;
}
return 0;
}
// 当新帧可用时调用
void _onFrameAvailable(int textureId) {
if (!mounted) return;
_log("收到帧回调 - 渲染帧 ${_renderedFrameCount + 1}");
// 更新帧时间用于FPS计算
_lastFrameTime = DateTime.now();
// 立即更新UI以显示新帧
setState(() {
_renderedFrameCount++;
});
}
Future<void> _initializeDecoder() async {
if (_isInitialized) {
await _releaseDecoder();
}
_log("正在初始化解码器");
try {
final config = VideoDecoderConfig(
width: 640,
height: 480,
codecType: CodecType.h264,
frameRate: 30,
bufferSize: 30,
isDebug: true, // 打开调试日志
enableDynamicThresholds: _enableDynamicThresholds, // 使用动态阈值
initialMaxPFrames: 60, // 初始最大连续P帧数
initialIFrameTimeoutMs: 5000, // 初始I帧超时时间
minMaxPFrames: 5, // 最小最大连续P帧数
maxMaxPFrames: 60, // 最大最大连续P帧数
);
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<void> _sendTestIFrame() async {
if (_textureId == null || !_isInitialized) {
_log("解码器未准备好,无法发送测试帧");
return;
}
_log("生成并发送测试I帧");
// 创建一个简单的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 // 一些随机数据
];
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<void> _releaseDecoder() async {
if (_textureId != null) {
_log("正在释放解码器资源");
try {
await VideoDecodePlugin.releaseDecoderForTexture(_textureId!);
setState(() {
_textureId = null;
_isInitialized = false;
_statusText = "已释放";
});
_log("解码器资源释放成功");
} catch (e) {
_log("释放解码器错误: $e");
}
}
}
Future<void> _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<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));
}
}
}
}
}
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];
bool decodeSuccess = await _decodeNextFrame(frame);
// 只有在成功解码的情况下才显示日志信息
if (!decodeSuccess && _enablePacketLoss) {
_log("跳过索引 $_currentFrameIndex 的帧(丢帧模拟)");
}
// 无论解码是否成功,都移动到下一帧
_currentFrameIndex++;
});
}
Future<bool> _decodeNextFrame(H264Frame frame) async {
if (_textureId == null || !_isInitialized || !_isPlaying) {
return false;
}
try {
// 获取NAL类型
int nalType = _getNalType(frame.data);
// 模拟丢包
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不进行解码
}
}
// 解码帧
final success = await VideoDecodePlugin.decodeFrameForTexture(
_textureId!,
frame.data,
frame.type,
);
if (!success) {
_log("解码帧失败,索引 $_currentFrameIndex (${frame.type})");
} else {
String nalTypeName = NalUnitType.getName(nalType);
_log(
"解码帧成功,索引 $_currentFrameIndex (${frame.type}), NAL类型: $nalTypeName");
}
return success;
} catch (e) {
_log("解码帧错误: $e");
return false;
}
}
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}'),
),
),
// 丢包效果指示器 - 当有帧被丢弃时,上方显示红色条带
if (_enablePacketLoss && _droppedFramesCount > 0)
Positioned(
top: 0,
left: 0,
right: 0,
height: 5,
child: Container(
color: Colors.red.withOpacity(0.8),
),
),
// 显示帧计数 - 调试用
Positioned(
right: 10,
top: 10,
child: Container(
padding: EdgeInsets.all(5),
color: Colors.black.withOpacity(0.5),
child: Text(
'帧: $_renderedFrameCount${_enablePacketLoss ? ' (丢帧: $_droppedFramesCount)' : ''}',
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)}'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'解码器FPS: ${_decoderFps.toStringAsFixed(1)}',style: TextStyle(
color: Colors.green
),),
Text('已渲染帧数: $_renderedFrameCount'),
],
),
if (_error.isNotEmpty)
Text('错误: $_error',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text('检测到的GOP: $_detectedGopSize'),
Text('解析的帧数: ${_h264Frames.length}'),
],
),
Text(
'H264文件大小: ${(_h264FileData?.length ?? 0) / 1024} KB'),
// 动态阈值参数显示
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'),
],
),
),
],
),
),
),
// 控制按钮区
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('刷新'),
),
],
),
),
),
// 丢包模拟控制面板
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),
),
),
],
),
),
),
// 日志区域
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',
),
);
},
),
),
],
),
),
),
),
],
),
),
);
}
// 更新解码器统计信息
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; // 每次都重新绘制以产生动态效果
}
}