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

1359 lines
45 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';
// 用于存储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 = "";
// 解码器状态信息
DecoderState _decoderState = DecoderState.initializing;
String _decoderStateText = "初始化中";
bool _isActuallyRendering = false; // 区分预通知和实际渲染状态
// 帧统计
int _renderedFrameCount = 0;
DateTime? _lastFrameTime;
double _fps = 0;
double _decoderFps = 0; // 解码器内部计算的FPS
// 用于刷新解码器统计信息的定时器
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;
// 额外的解码器属性
int _totalFrames = 0;
int _droppedFrames = 0;
bool _hasSentIDR = false;
bool _hasSentSPS = false;
bool _hasSentPPS = false;
@override
void initState() {
super.initState();
_loadH264File();
// 仅在需要时使用定时器更新一些UI元素
_statsTimer = Timer.periodic(Duration(milliseconds: 1000), (timer) {
if (mounted) {
setState(() {
// 更新UI元素例如帧率计算等
// 解码器统计信息现在通过回调获取,不需要在这里请求
});
}
});
}
@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: 24, // 设置为接近原视频的24fps (23.976)
isDebug: true, // 打开调试日志
);
final textureId = await VideoDecodePlugin.initDecoder(config);
if (textureId != null) {
_textureId = textureId;
// 设置帧回调
VideoDecodePlugin.setFrameCallbackForTexture(
textureId, _onFrameAvailable);
// 设置状态回调
VideoDecodePlugin.setStateCallbackForTexture(
textureId, _onDecoderStateChanged);
setState(() {
_isInitialized = true;
_error = "";
_statusText = "就绪";
_renderedFrameCount = 0; // 重置帧计数
_decoderState = DecoderState.initializing;
_decoderStateText = "初始化中";
_isActuallyRendering = false;
});
_log("解码器初始化成功纹理ID: $_textureId");
// 自动发送测试帧以触发渲染
await _sendTestIFrame();
} else {
setState(() {
_error = "获取纹理ID失败";
_statusText = "初始化失败";
});
_log("解码器初始化失败 - 返回空纹理ID");
}
} catch (e) {
setState(() {
_error = e.toString();
_statusText = "初始化错误";
});
_log("解码器初始化错误: $e");
}
}
// 解码器状态变化回调
void _onDecoderStateChanged(
int textureId, DecoderState state, Map<String, dynamic> stats) {
if (!mounted) return;
String stateText;
switch (state) {
case DecoderState.initializing:
stateText = "初始化中";
break;
case DecoderState.ready:
stateText = "准备就绪";
break;
case DecoderState.rendering:
stateText = "渲染中";
// 标记实际渲染状态
_isActuallyRendering = true;
break;
case DecoderState.error:
stateText = "出错";
// 获取错误信息
final errorMessage = stats['errorMessage'] as String?;
if (errorMessage != null) {
_log("解码器错误: $errorMessage");
}
break;
case DecoderState.released:
stateText = "已释放";
break;
default:
stateText = "未知状态";
}
// 更新解码器状态UI
setState(() {
_decoderState = state;
_decoderStateText = stateText;
// 更新统计信息
if (stats.isNotEmpty) {
_decoderFps = (stats['fps'] as num?)?.toDouble() ?? 0.0;
_renderedFrameCount = (stats['renderedFrames'] as int?) ?? 0;
// 更新更多统计信息
_totalFrames = (stats['totalFrames'] as int?) ?? 0;
_droppedFrames = (stats['droppedFrames'] as int?) ?? 0;
_hasSentIDR = (stats['hasSentIDR'] as bool?) ?? false;
_hasSentSPS = (stats['hasSentSPS'] as bool?) ?? false;
_hasSentPPS = (stats['hasSentPPS'] as bool?) ?? false;
// 更新状态文本
if (state == DecoderState.rendering) {
_statusText = _isPlaying
? "播放中 (解码总帧: $_totalFrames, 丢弃: $_droppedFrames)"
: "已停止";
}
}
});
String decoderInfo = "解码器状态更新: $_decoderStateText, " +
"帧数据: 渲染=$_renderedFrameCount, 总计=$_totalFrames, 丢弃=$_droppedFrames, " +
"FPS=${_decoderFps.toStringAsFixed(1)}, " +
"参数集: SPS=${_hasSentSPS}, PPS=${_hasSentPPS}, IDR=${_hasSentIDR}";
_log(decoderInfo);
}
// 添加一个测试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 {
_statsTimer?.cancel(); // 取消统计信息定时器
if (_textureId != null) {
_log("正在释放解码器资源");
try {
await VideoDecodePlugin.releaseDecoderForTexture(_textureId!);
setState(() {
_textureId = null;
_isInitialized = false;
_statusText = "已释放";
_isActuallyRendering = false;
_decoderState = DecoderState.released;
_decoderStateText = "已释放";
});
_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 = 42; // 约23.8 fps (接近原视频23.9fps)
_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 Container(
width: 640,
height: 480,
color: Colors.black54,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.videocam_off, size: 48, color: Colors.white70),
SizedBox(height: 16),
Text(
'无可用纹理',
style: TextStyle(color: Colors.white, fontSize: 16),
),
],
),
),
);
}
return Stack(
fit: StackFit.expand,
children: [
// 背景色
Container(color: Colors.black),
// 无帧时显示加载指示
if (_renderedFrameCount == 0 || !_isActuallyRendering)
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white70),
),
SizedBox(height: 16),
Text(
_decoderState == DecoderState.initializing
? '初始化中...'
: _decoderState == DecoderState.ready
? '准备就绪,等待首帧...'
: '加载中...',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
],
),
),
// 视频纹理 - 使用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),
),
),
// 解码器状态指示
if (_decoderState == DecoderState.error)
Container(
color: Colors.red.withOpacity(0.3),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.white),
SizedBox(height: 16),
Text(
'解码器错误',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold),
),
],
),
),
),
// 显示帧计数 - 调试用
Positioned(
right: 10,
top: 10,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(4),
),
constraints: BoxConstraints(
maxWidth: 150, // 限制最大宽度
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, // 确保column只占用所需空间
children: [
Text(
'帧: $_renderedFrameCount',
style: TextStyle(color: Colors.white, fontSize: 12),
),
if (_enablePacketLoss)
Text(
'丢帧: $_droppedFramesCount',
style: TextStyle(
color: _droppedFramesCount > 0
? Colors.orange
: Colors.white70,
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: [
// 状态行
Text('状态: $_statusText',
style:
TextStyle(fontWeight: FontWeight.bold)),
// 解码器状态行
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'解码器状态: $_decoderStateText',
style: TextStyle(
color: _getStateColor(),
fontWeight: FontWeight.bold),
),
),
// 实际渲染状态
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'实际渲染: ${_isActuallyRendering ? "" : ""}',
style: TextStyle(
color: _isActuallyRendering
? Colors.green
: Colors.orange,
),
),
),
// FPS和帧数信息
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'解码器FPS: ${_decoderFps.toStringAsFixed(1)}',
style: TextStyle(color: Colors.green),
),
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text('已渲染帧数: $_renderedFrameCount'),
),
// 丢弃帧信息
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'已丢弃帧数: $_droppedFramesCount',
style: TextStyle(
color: _droppedFramesCount > 0
? Colors.orange
: Colors.black),
),
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text('当前帧索引: $_currentFrameIndex'),
),
// 参数集状态
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('参数集状态:'),
Padding(
padding: const EdgeInsets.only(
left: 8.0, top: 2.0),
child: Text(
'SPS: ${_hasSentSPS ? "已发送" : "未发送"}'),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, top: 2.0),
child: Text(
'PPS: ${_hasSentPPS ? "已发送" : "未发送"}'),
),
Padding(
padding: const EdgeInsets.only(
left: 8.0, top: 2.0),
child: Text(
'IDR: ${_hasSentIDR ? "已发送" : "未发送"}'),
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text('总帧数: $_totalFrames'),
),
// 错误信息
if (_error.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'错误: $_error',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold),
),
),
// H264文件信息
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text('解析的帧数: ${_h264Frames.length}'),
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
'H264文件大小: ${((_h264FileData?.length ?? 0) ~/ 1024)} KB'),
),
],
),
),
),
// 控制按钮区
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(
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 {
// 获取所有解码器统计信息
final stats = await VideoDecodePlugin.getDecoderStats(_textureId!);
if (mounted) {
_log("手动更新解码器统计信息: $stats");
}
} catch (e) {
_log("获取解码器统计信息失败: $e");
}
}
Color _getStateColor() {
switch (_decoderState) {
case DecoderState.initializing:
return Colors.orange;
case DecoderState.ready:
return Colors.green;
case DecoderState.rendering:
return Colors.blue;
case DecoderState.error:
return Colors.red;
case DecoderState.released:
return Colors.grey;
default:
return Colors.black;
}
}
}
// 添加错误帧绘制器
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; // 每次都重新绘制以产生动态效果
}
}