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

814 lines
24 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 CODED_SLICE_IDR = 5; // I帧
static const int SPS = 7; // 序列参数集
static const int PPS = 8; // 图像参数集
// 获取类型名称
static String getName(int type) {
switch (type) {
case UNSPECIFIED:
return "未指定";
case CODED_SLICE_NON_IDR:
return "P帧";
case CODED_SLICE_IDR:
return "I帧";
case SPS:
return "SPS";
case PPS:
return "PPS";
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;
// H264文件解析
Uint8List? _h264FileData;
List<H264Frame> _h264Frames = [];
int _currentFrameIndex = 0;
// 解码定时器
Timer? _frameTimer;
// 日志
final List<String> _logs = [];
final ScrollController _logScrollController = ScrollController();
@override
void initState() {
super.initState();
_loadH264File();
}
@override
void dispose() {
_stopPlaying();
_releaseDecoder();
_frameTimer?.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类型的辅助方法用于调试
int _getNalType(Uint8List data) {
// 打印头几个字节
String headerBytes = '';
for (int i = 0; i < math.min(16, data.length); i++) {
headerBytes += '${data[i].toRadixString(16).padLeft(2, '0')} ';
}
_log("帧数据头: $headerBytes");
// 尝试找到起始码位置
int nalOffset = -1;
// 检查标准起始码
if (data.length > 4 &&
data[0] == 0x00 &&
data[1] == 0x00 &&
data[2] == 0x00 &&
data[3] == 0x01) {
nalOffset = 4;
_log("找到4字节起始码 (0x00000001) 位置: 0");
} else if (data.length > 3 &&
data[0] == 0x00 &&
data[1] == 0x00 &&
data[2] == 0x01) {
nalOffset = 3;
_log("找到3字节起始码 (0x000001) 位置: 0");
} else {
// 尝试搜索起始码
for (int i = 0; i < data.length - 4; i++) {
if (data[i] == 0x00 &&
data[i + 1] == 0x00 &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
nalOffset = i + 4;
_log("在偏移量 $i 处找到4字节起始码");
break;
} else if (i < data.length - 3 &&
data[i] == 0x00 &&
data[i + 1] == 0x00 &&
data[i + 2] == 0x01) {
nalOffset = i + 3;
_log("在偏移量 $i 处找到3字节起始码");
break;
}
}
}
// 如果找到了起始码
if (nalOffset >= 0 && nalOffset < data.length) {
int nalType = data[nalOffset] & 0x1F;
_log("解析NAL类型: ${NalUnitType.getName(nalType)} ($nalType)");
return nalType;
}
_log("无法解析NAL类型");
return -1;
}
// 当新帧可用时调用
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, // 打开调试日志
);
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];
await _decodeNextFrame(frame);
_currentFrameIndex++;
});
}
Future<void> _decodeNextFrame(H264Frame frame) async {
if (_textureId == null || !_isInitialized) return;
try {
// 检查帧的NAL类型仅用于调试
int nalType = _getNalType(frame.data);
final success = await VideoDecodePlugin.decodeFrameForTexture(
_textureId!,
frame.data,
frame.type,
);
if (!success) {
_log(
"解码帧失败,索引 $_currentFrameIndex (${frame.type}), NAL类型: ${NalUnitType.getName(nalType)}");
} else {
_log(
"解码帧成功,索引 $_currentFrameIndex (${frame.type}), NAL类型: ${NalUnitType.getName(nalType)}");
}
} catch (e) {
_log("解码帧错误: $e");
}
}
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}'),
),
),
// 显示帧计数 - 调试用
Positioned(
right: 10,
top: 10,
child: Container(
padding: EdgeInsets.all(5),
color: Colors.black.withOpacity(0.5),
child: Text(
'帧: $_renderedFrameCount',
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)}'),
],
),
if (_error.isNotEmpty)
Text('错误: $_error',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text('已渲染帧数: $_renderedFrameCount'),
Text('解析的帧数: ${_h264Frames.length}'),
],
),
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('刷新'),
),
],
),
),
),
// 日志区域
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',
),
);
},
),
),
],
),
),
),
),
],
),
),
);
}
}