fix:完成720P 20帧渲染需求

This commit is contained in:
liyi 2025-04-30 10:25:19 +08:00
parent 419912c590
commit e806987fa0
4 changed files with 774 additions and 358 deletions

View File

@ -19,12 +19,14 @@ class VideoTypeE extends $pb.ProtobufEnum {
static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264'); static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264');
static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE'); static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE');
static const VideoTypeE VP8 = VideoTypeE._(3, _omitEnumNames ? '' : 'VP8'); static const VideoTypeE VP8 = VideoTypeE._(3, _omitEnumNames ? '' : 'VP8');
static const VideoTypeE H264_720P = VideoTypeE._(4, _omitEnumNames ? '' : 'H264_720P');
static const $core.List<VideoTypeE> values = <VideoTypeE> [ static const $core.List<VideoTypeE> values = <VideoTypeE> [
NONE_V, NONE_V,
H264, H264,
IMAGE, IMAGE,
VP8, VP8,
H264_720P,
]; ];
static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values);

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:math'; // Import the math package to use sqrt import 'dart:math'; // Import the math package to use sqrt
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -62,41 +63,76 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// //
int? _previousFrameTimestamp; int? _previousFrameTimestamp;
int _flutterToNativeFrameCount = 0;
int _lastFlutterToNativePrintTime = 0;
int _networkFrameCount = 0;
int _lastNetworkPrintTime = 0;
Timer? _frameRefreshTimer;
bool _isFrameAvailable = true;
int _renderedFrameCount = 0;
int _lastRenderedFrameTime = 0;
// I帧前
final List<List<int>> _preIFrameCache = [];
bool _hasWrittenFirstIFrame = false;
bool _isStartNative = false;
// SPS/PPS状态追踪变量
bool hasSps = false;
bool hasPps = false;
// SPS/PPS缓存
List<int>? spsCache;
List<int>? ppsCache;
void _setupFrameRefresh() {
// 16ms对应约60fps
_frameRefreshTimer =
Timer.periodic(const Duration(milliseconds: 16), (timer) {
if (_isFrameAvailable) {
_isFrameAvailable = false;
_renderedFrameCount++;
//
int now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastRenderedFrameTime > 1000) {
print('[Flutter] 每秒渲染帧数: $_renderedFrameCount');
_renderedFrameCount = 0;
_lastRenderedFrameTime = now;
}
// Flutter重建widget
WidgetsBinding.instance.scheduleFrame();
}
});
}
void onFrameAvailable() {
_isFrameAvailable = true;
}
// //
Future<void> _initVideoDecoder() async { Future<void> _initVideoDecoder() async {
try { try {
// //
final config = VideoDecoderConfig( final config = VideoDecoderConfig(
width: 864, width: 1280,
// //
height: 480, height: 720,
frameRate: 25, codecType: 'h264',
//
//
codecType: CodecType.h264,
//
isDebug: true,
); );
// textureId // textureId
final textureId = await VideoDecodePlugin.initDecoder(config); final textureId = await VideoDecodePlugin.initDecoder(config);
if (textureId != null) { if (textureId != null) {
state.textureId.value = textureId; state.textureId.value = textureId;
AppLog.log('视频解码器初始化成功textureId=$textureId'); AppLog.log('视频解码器初始化成功textureId=$textureId');
//
//
VideoDecodePlugin.setFrameCallback(_onFrameAvailable);
//
VideoDecodePlugin.setStateCallbackForTexture(
textureId, _onDecoderStateChanged);
// FPS监测
startFpsMonitoring();
} else { } else {
AppLog.log('视频解码器初始化失败'); AppLog.log('视频解码器初始化失败');
} }
_startFrameProcessTimer();
} catch (e) { } catch (e) {
AppLog.log('初始化视频解码器错误: $e'); AppLog.log('初始化视频解码器错误: $e');
// //
@ -108,66 +144,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} }
} }
//
void _onFrameAvailable(int textureId) {}
//
void _onDecoderStateChanged(
int textureId, DecoderState decoderState, Map<String, dynamic> stats) {
String stateText;
switch (decoderState) {
case DecoderState.initializing:
state.isLoading.value = true;
stateText = "初始化中";
break;
case DecoderState.ready:
stateText = "准备就绪";
break;
case DecoderState.rendering:
stateText = "渲染中";
state.isLoading.value = false;
break;
case DecoderState.error:
stateText = "出错";
//
final errorMessage = stats['errorMessage'] as String?;
if (errorMessage != null) {
AppLog.log("解码器错误: $errorMessage");
}
break;
case DecoderState.released:
stateText = "已释放";
break;
default:
stateText = "未知状态";
}
//
if (stats.isNotEmpty) {
//
final PacketLossInfo packetLossInfo =
PacketLossStatistics().getStatistics();
// FPS
// state.decoderFps.value = (stats['fps'] as num?)?.toDouble() ?? 0.0;
//
state.renderedFrameCount.value = (stats['renderedFrames'] as int?) ?? 0;
state.totalFrames.value = (stats['totalFrames'] as int?) ?? 0;
state.droppedFrames.value = (stats['droppedFrames'] as int?) ?? 0;
state.hasSentIDR.value = (stats['hasSentIDR'] as bool?) ?? false;
state.hasSentSPS.value = (stats['hasSentSPS'] as bool?) ?? false;
state.hasSentPPS.value = (stats['hasSentPPS'] as bool?) ?? false;
state.keyFrameInterval.value = (stats['keyFrameInterval'] as int?) ?? 0;
state.decodingJitterMs.value = (stats['decodingJitterMs'] as int?) ?? 0;
//
state.messageLossRate.value = packetLossInfo.messageLossRate;
state.packetLossRate.value = packetLossInfo.packetLossRate;
state.lastPacketStatsUpdateTime.value =
DateTime.now().millisecondsSinceEpoch;
}
}
/// ///
void _initFlutterPcmSound() { void _initFlutterPcmSound() {
const int sampleRate = 8000; const int sampleRate = 8000;
@ -190,13 +166,149 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// //
StartChartManage().startTalkRejectMessageTimer(); StartChartManage().startTalkRejectMessageTimer();
} }
if (state.textureId.value != null) { VideoDecodePlugin.releaseDecoder();
VideoDecodePlugin.releaseDecoderForTexture(state.textureId.value!);
}
VideoDecodePlugin.releaseAllDecoders();
Get.back(); Get.back();
} }
/// H264帧到缓冲区
void _addFrameToBuffer(
List<int> frameData,
TalkDataH264Frame_FrameTypeE frameType,
int pts,
int frameSeq,
int frameSeqI,
) {
_networkFrameCount++;
int now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastNetworkPrintTime > 1000) {
AppLog.log('[Flutter] 每秒收到网络H264帧数: ' + _networkFrameCount.toString());
state.networkH264Fps.value = _networkFrameCount;
_networkFrameCount = 0;
_lastNetworkPrintTime = now;
}
// Map
final Map<String, dynamic> frameMap = {
'frameData': frameData,
'frameType': frameType,
'frameSeq': frameSeq,
'frameSeqI': frameSeqI,
'pts': pts,
};
//
state.h264FrameBuffer.add(frameMap);
//
while (state.h264FrameBuffer.length > state.maxFrameBufferSize) {
state.h264FrameBuffer.removeAt(0);
}
_flutterToNativeFrameCount++;
if (now - _lastFlutterToNativePrintTime > 1000) {
AppLog.log(
'[Flutter] 每秒送入Native帧数: ' + _flutterToNativeFrameCount.toString());
state.nativeSendFps.value = _flutterToNativeFrameCount;
_flutterToNativeFrameCount = 0;
_lastFlutterToNativePrintTime = now;
}
}
///
void _startFrameProcessTimer() {
//
state.frameProcessTimer?.cancel();
//
final int intervalMs = (1000 / state.targetFps).round();
//
state.frameProcessTimer =
Timer.periodic(Duration(milliseconds: intervalMs), (timer) {
if (state.isLoading.isTrue) {
state.isLoading.value = false;
}
_processNextFrameFromBuffer();
});
AppLog.log('启动帧处理定时器,目标帧率: ${state.targetFps}fps间隔: ${intervalMs}ms');
}
///
void _processNextFrameFromBuffer() async {
//
if (state.isProcessingFrame) {
return;
}
//
if (state.h264FrameBuffer.isEmpty) {
return;
}
//
state.isProcessingFrame = true;
try {
//
final Map<String, dynamic> frameMap = state.h264FrameBuffer.removeAt(0);
final List<int> frameData = frameMap['frameData'];
final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType'];
final int frameSeq = frameMap['frameSeq'];
final int frameSeqI = frameMap['frameSeqI'];
int pts = DateTime.now().millisecondsSinceEpoch;
if (frameType == TalkDataH264Frame_FrameTypeE.P) {
// frameSeqI为I帧序号标识
if (!(_decodedIFrames.contains(frameSeqI))) {
AppLog.log('丢弃P帧未收到对应I帧frameSeqI=${frameSeqI}');
return;
}
} else if (frameType == TalkDataH264Frame_FrameTypeE.I) {
// I帧序号
_decodedIFrames.add(frameSeq);
}
// h264文件
_appendH264FrameToFile(frameData, frameType);
final timestamp = DateTime.now().microsecondsSinceEpoch;
VideoDecodePlugin.decodeFrame(
frameData: Uint8List.fromList(frameData),
frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 1 : 0,
frameSeq: frameSeq,
timestamp: timestamp,
refIFrameSeq: frameSeqI,
);
// P帧对应I帧是否已解码P帧
if (frameType == TalkDataH264Frame_FrameTypeE.P) {
// frameSeqI为I帧序号标识
if (!(_decodedIFrames.contains(frameSeqI))) {
AppLog.log('丢弃P帧未收到对应I帧frameSeqI=${frameSeqI}');
}
} else if (frameType == TalkDataH264Frame_FrameTypeE.I) {
// I帧序号
_decodedIFrames.add(frameSeq);
}
// h264文件
_appendH264FrameToFile(frameData, frameType);
} catch (e) {
AppLog.log('处理缓冲帧失败: $e');
} finally {
//
state.isProcessingFrame = false;
}
}
///
void _stopFrameProcessTimer() {
state.frameProcessTimer?.cancel();
state.frameProcessTimer = null;
state.h264FrameBuffer.clear();
state.isProcessingFrame = false;
// AppLog.log('停止帧处理定时器');
}
// //
void initiateAnswerCommand() { void initiateAnswerCommand() {
StartChartManage().startTalkAcceptTimer(); StartChartManage().startTalkAcceptTimer();
@ -230,82 +342,63 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
_playAudioFrames(); _playAudioFrames();
break; break;
case TalkData_ContentTypeE.H264: case TalkData_ContentTypeE.H264:
// // if (_isStartNative) {
final currentTimestamp = DateTime.now().millisecondsSinceEpoch; // if (talkDataH264Frame != null) {
// // I帧NALUSPS/PPS并优先放入缓冲区
// // if (talkDataH264Frame.frameType ==
if (_previousFrameTimestamp != null) { // TalkDataH264Frame_FrameTypeE.I) {
final timeDifference = currentTimestamp - _previousFrameTimestamp!; // // I帧前所有未处理帧SPS/PPS/I帧
AppLog.log('当前帧与上一帧的时间间隔: $timeDifference 毫秒'); // state.h264FrameBuffer.clear();
} // _extractAndBufferSpsPpsForBuffer(
// // talkData.content,
_previousFrameTimestamp = currentTimestamp; // talkData.durationMs,
// talkDataH264Frame.frameSeq,
// // talkDataH264Frame.frameSeqI);
// _processH264Frame(talkData, talkDataH264Frame!);
// H264视频帧
// _processH264Frame(talkData, talkDataH264Frame!);
//
// //
// if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) {
// AppLog.log(
// '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}');
// } // }
// _addFrameToBuffer(
// talkData.content,
// talkDataH264Frame.frameType,
// talkData.durationMs,
// talkDataH264Frame.frameSeq,
// talkDataH264Frame.frameSeqI);
// }
// } else {
// await VideoDecodePlugin.startNativePlayer(
// VideoDecoderConfig(width: 1280, height: 720, codecType: 'h264'),
// );
// _isStartNative = true;
// }
// H264帧
if (state.textureId.value != null) {
if (talkDataH264Frame != null) {
if (talkDataH264Frame.frameType ==
TalkDataH264Frame_FrameTypeE.I) {
_handleIFrameWithSpsPpsAndIdr(
talkData.content,
talkData.durationMs,
talkDataH264Frame.frameSeq,
talkDataH264Frame.frameSeqI,
);
return;
} else if (talkDataH264Frame.frameType ==
TalkDataH264Frame_FrameTypeE.P) {
_handlePFrame(
talkData.content,
talkData.durationMs,
talkDataH264Frame.frameSeq,
talkDataH264Frame.frameSeqI,
);
return;
}
}
} else {
AppLog.log('无法处理H264帧textureId为空');
}
break; break;
} }
}); });
} }
// H264视频帧
Future<void> _processH264Frame(
TalkData talkData, TalkDataH264Frame frameInfo) async {
//
if (state.textureId.value == null) {
//
AppLog.log('解码器尚未初始化,尝试重新初始化...');
await _initVideoDecoder();
//
if (state.textureId.value == null) {
return;
}
}
// P帧对应的I帧序号
final frameSeqI = frameInfo.frameSeqI;
// P帧检查I帧未解码成功
if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.P &&
!_decodedIFrames.contains(frameSeqI)) {
AppLog.log('丢弃P帧: 依赖的I帧(${frameSeqI})尚未解码, P帧序号: ${frameInfo.frameSeq}');
return;
}
// talkData中提取H264帧数据
final Uint8List frameData = Uint8List.fromList(talkData.content);
//
final FrameType frameType =
frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I
? FrameType.iFrame
: FrameType.pFrame;
//
try {
final bool result =
await VideoDecodePlugin.decodeFrame(frameData, frameType);
// I帧且成功解码I帧集合
if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I && result) {
_decodedIFrames.add(frameInfo.frameSeq);
//
if (_decodedIFrames.length > 30) {
_decodedIFrames.remove(_decodedIFrames.first);
}
}
} catch (e) {
AppLog.log('解码帧错误: $e, 帧序号: ${frameInfo.frameSeq}');
}
}
// //
void _playAudioFrames() { void _playAudioFrames() {
// //
@ -462,10 +555,20 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// //
_initVideoDecoder(); _initVideoDecoder();
// H264帧缓冲区
state.h264FrameBuffer.clear();
state.isProcessingFrame = false;
_setupFrameRefresh();
} }
@override @override
void onClose() { void onClose() {
_closeH264File();
//
_stopFrameProcessTimer();
_stopPlayG711Data(); // _stopPlayG711Data(); //
state.audioBuffer.clear(); // state.audioBuffer.clear(); //
@ -490,11 +593,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
_streamSubscription?.cancel(); _streamSubscription?.cancel();
_isListening = false; _isListening = false;
// FPS监测
stopFpsMonitoring();
// //
StartChartManage().reSetDefaultTalkExpect(); StartChartManage().reSetDefaultTalkExpect();
VideoDecodePlugin.releaseAllDecoders(); VideoDecodePlugin.releaseDecoder();
// //
_batchProcessTimer?.cancel(); _batchProcessTimer?.cancel();
@ -502,7 +603,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// I帧集合 // I帧集合
_decodedIFrames.clear(); _decodedIFrames.clear();
_frameRefreshTimer?.cancel();
_frameRefreshTimer = null;
super.onClose(); super.onClose();
} }
@ -722,91 +824,430 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
return result; return result;
} }
// /// h264文件frameType
void startFpsMonitoring() { Future<void> _appendH264FrameToFile(
// List<int> frameData, TalkDataH264Frame_FrameTypeE frameType) async {
stopFpsMonitoring(); try {
if (state.h264File == null) {
// await _initH264File();
state.lastFpsUpdateTime.value = DateTime.now().millisecondsSinceEpoch; }
// NALU分割函数NALU的完整字节数组
// List<List<int>> splitNalus(List<int> data) {
state.fpsTimer = Timer.periodic(const Duration(seconds: 1), (timer) { List<List<int>> nalus = [];
// int i = 0;
updatePacketLossStats(); while (i < data.length - 3) {
int start = -1;
// int next = -1;
_analyzePerformance(); if (data[i] == 0x00 && data[i + 1] == 0x00) {
}); if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 &&
data[next + 1] == 0x00 &&
((data[next + 2] == 0x01) ||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen =
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
return nalus;
} }
// // I帧前只缓存SPS/PPS/IDR
void stopFpsMonitoring() { if (!_hasWrittenFirstIFrame) {
state.fpsTimer?.cancel(); final nalus = splitNalus(frameData);
state.fpsTimer = null; List<List<int>> spsList = [];
List<List<int>> ppsList = [];
List<List<int>> idrList = [];
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
} }
if (nalu.length > offset) {
// int naluType = nalu[offset] & 0x1F;
void logMessage(String message) { if (naluType == 7) {
AppLog.log(message); spsList.add(nalu);
AppLog.log('SPS内容: ' +
nalu
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(' '));
} else if (naluType == 8) {
ppsList.add(nalu);
AppLog.log('PPS内容: ' +
nalu
.map((b) => b.toRadixString(16).padLeft(2, '0'))
.join(' '));
} else if (naluType == 5) {
idrList.add(nalu);
} }
//
// }
void updatePacketLossStats() async { }
try {} catch (e) { // I帧写入前缓存
logMessage('获取丢包率数据失败: $e'); if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) {
for (final sps in spsList) {
await _writeSingleFrameToFile(_ensureStartCode(sps));
AppLog.log('写入顺序: SPS');
}
for (final pps in ppsList) {
await _writeSingleFrameToFile(_ensureStartCode(pps));
AppLog.log('写入顺序: PPS');
}
for (final idr in idrList) {
await _writeSingleFrameToFile(_ensureStartCode(idr));
AppLog.log('写入顺序: IDR');
}
_hasWrittenFirstIFrame = true;
} else {
// SPS/PPS/IDR则继续缓存I帧
if (spsList.isNotEmpty) _preIFrameCache.addAll(spsList);
if (ppsList.isNotEmpty) _preIFrameCache.addAll(ppsList);
if (idrList.isNotEmpty) _preIFrameCache.addAll(idrList);
}
} else {
// IDR和P帧
final nalus = splitNalus(frameData);
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 5) {
await _writeSingleFrameToFile(_ensureStartCode(nalu));
// AppLog.log('写入顺序: IDR');
} else if (naluType == 1) {
await _writeSingleFrameToFile(_ensureStartCode(nalu));
// AppLog.log('写入顺序: P帧');
} else if (naluType == 7) {
// AppLog.log('遇到新SPS已忽略');
} else if (naluType == 8) {
// AppLog.log('遇到新PPS已忽略');
}
//
}
}
}
} catch (e) {
AppLog.log('写入H264帧到文件失败: $e');
} }
} }
// // NALU起始码为0x00000001
void _analyzePerformance() { List<int> _ensureStartCode(List<int> nalu) {
final int now = DateTime.now().millisecondsSinceEpoch; if (nalu.length >= 4 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x00 &&
nalu[3] == 0x01) {
return nalu;
} else if (nalu.length >= 3 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x01) {
return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3);
} else {
return [0x00, 0x00, 0x00, 0x01] + nalu;
}
}
// /// NALU头判断
if (state.lastPerformanceCheck == 0) { Future<void> _writeSingleFrameToFile(List<int> frameData) async {
state.lastPerformanceCheck = now; bool hasNaluHeader = false;
state.lastFrameCount = state.renderedFrameCount.value; if (frameData.length >= 4 &&
frameData[0] == 0x00 &&
frameData[1] == 0x00 &&
((frameData[2] == 0x01) ||
(frameData[2] == 0x00 && frameData[3] == 0x01))) {
hasNaluHeader = true;
}
if (hasNaluHeader) {
await state.h264File!.writeAsBytes(frameData, mode: FileMode.append);
} else {
final List<int> naluHeader = [0x00, 0x00, 0x01];
await state.h264File!
.writeAsBytes(naluHeader + frameData, mode: FileMode.append);
}
}
/// h264文件
Future<void> _initH264File() async {
try {
if (state.h264File != null) return;
// Download目录
Directory? downloadsDir;
if (Platform.isAndroid) {
// Android 10+ getExternalStorageDirectory()
downloadsDir = await getExternalStorageDirectory();
// ROMDownload
final downloadPath = '/storage/emulated/0/Download';
if (Directory(downloadPath).existsSync()) {
downloadsDir = Directory(downloadPath);
}
} else {
downloadsDir = await getApplicationDocumentsDirectory();
}
final filePath =
'${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264';
state.h264FilePath = filePath;
state.h264File = File(filePath);
if (!await state.h264File!.exists()) {
await state.h264File!.create(recursive: true);
}
AppLog.log('H264文件初始化: $filePath');
} catch (e) {
AppLog.log('H264文件初始化失败: $e');
}
}
/// h264文件
Future<void> _closeH264File() async {
try {
if (state.h264File != null) {
AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}');
}
state.h264File = null;
state.h264FilePath = null;
_preIFrameCache.clear();
_hasWrittenFirstIFrame = false;
} catch (e) {
AppLog.log('关闭H264文件时出错: $e');
}
}
/// I帧数据中分割NALU并将SPS/PPS优先放入缓冲区
void _extractAndBufferSpsPpsForBuffer(
List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
List<List<int>> splitNalus(List<int> data) {
List<List<int>> nalus = [];
int i = 0;
while (i < data.length - 3) {
int start = -1;
int next = -1;
if (data[i] == 0x00 && data[i + 1] == 0x00) {
if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length &&
data[i + 2] == 0x00 &&
data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 &&
data[next + 1] == 0x00 &&
((data[next + 2] == 0x01) ||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen =
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
return nalus;
}
final nalus = splitNalus(frameData);
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 7) {
// SPS
hasSps = true;
//
if (spsCache == null || !_listEquals(spsCache!, nalu)) {
spsCache = List<int>.from(nalu);
}
} else if (naluType == 8) {
// PPS
hasPps = true;
if (ppsCache == null || !_listEquals(ppsCache!, nalu)) {
ppsCache = List<int>.from(nalu);
}
}
}
}
}
// List比较工具
bool _listEquals(List<int> a, List<int> b) {
if (a.length != b.length) return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) return false;
}
return true;
}
// I帧处理方法
void _handleIFrameWithSpsPpsAndIdr(List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
// I帧前所有未处理帧SPS/PPS/I帧
state.h264FrameBuffer.clear();
_extractAndBufferSpsPpsForBuffer(frameData, durationMs, frameSeq, frameSeqI);
// SPS/PPS就先写入I帧本体IDR
if (spsCache == null || ppsCache == null) {
// SPS/PPS缓存I帧
return; return;
} }
// SPS/PPS
// _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI);
if (now - state.lastPerformanceCheck >= 1000) { _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI);
// // I帧包IDRtype 5
final int frameRendered = List<List<int>> nalus = [];
state.renderedFrameCount.value - state.lastFrameCount; int i = 0;
final double actualFPS = List<int> data = frameData;
frameRendered * 1000 / (now - state.lastPerformanceCheck); while (i < data.length - 3) {
int start = -1;
// int next = -1;
final double dropRate = state.droppedFrames.value / if (data[i] == 0x00 && data[i + 1] == 0x00) {
(state.totalFrames.value > 0 ? state.totalFrames.value : 1) * if (data[i + 2] == 0x01) {
100; start = i;
i += 3;
// } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
final int pendingFrames = start = i;
state.totalFrames.value - state.renderedFrameCount.value; i += 4;
} else {
// Map中的帧数 i++;
final int processingFrames = state.frameTracker.length; continue;
}
// next = i;
String performanceStatus = "正常"; while (next < data.length - 3) {
if (actualFPS < 15 && dropRate > 10) { if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
performanceStatus = "严重渲染瓶颈"; break;
} else if (actualFPS < 20 && dropRate > 5) { }
performanceStatus = "轻微渲染瓶颈"; next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 5) {
_addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI);
}
}
}
} }
// // P帧处理方法
AppLog.log("性能分析: 实际帧率=${actualFPS.toStringAsFixed(1)}fps, " + void _handlePFrame(List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
"丢帧率=${dropRate.toStringAsFixed(1)}%, " + // P帧type 1
"待处理帧数=$pendingFrames, " + List<List<int>> nalus = [];
"处理中帧数=$processingFrames, " + int i = 0;
"状态=$performanceStatus"); List<int> data = frameData;
while (i < data.length - 3) {
// int start = -1;
state.lastPerformanceCheck = now; int next = -1;
state.lastFrameCount = state.renderedFrameCount.value; if (data[i] == 0x00 && data[i + 1] == 0x00) {
if (data[i + 2] == 0x01) {
start = i;
i += 3;
} else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
start = i;
i += 4;
} else {
i++;
continue;
}
next = i;
while (next < data.length - 3) {
if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
break;
}
next++;
}
nalus.add(data.sublist(start, next));
i = next;
} else {
i++;
}
}
int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
if (nalus.isEmpty && data.isNotEmpty) {
nalus.add(data);
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
nalus.add(data.sublist(nalusTotalLen));
}
for (final nalu in nalus) {
int offset = 0;
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
if (nalu[2] == 0x01)
offset = 3;
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
}
if (nalu.length > offset) {
int naluType = nalu[offset] & 0x1F;
if (naluType == 1) {
_addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, frameSeq, frameSeqI);
}
}
} }
} }
} }

View File

@ -56,6 +56,7 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
state.animationController.forward(); state.animationController.forward();
} }
}); });
} }
@override @override
@ -71,6 +72,7 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: <Widget>[ children: <Widget>[
//
Obx( Obx(
() { () {
final double screenWidth = MediaQuery.of(context).size.width; final double screenWidth = MediaQuery.of(context).size.width;
@ -93,8 +95,10 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
final double scaleWidth = physicalWidth / rotatedImageWidth; final double scaleWidth = physicalWidth / rotatedImageWidth;
final double scaleHeight = physicalHeight / rotatedImageHeight; final double scaleHeight = physicalHeight / rotatedImageHeight;
max(scaleWidth, scaleHeight); // max(scaleWidth, scaleHeight); //
return Column(
return state.isLoading.isTrue children: [
Expanded(
child: state.isLoading.isTrue
? Image.asset( ? Image.asset(
'images/main/monitorBg.png', 'images/main/monitorBg.png',
width: screenWidth, width: screenWidth,
@ -116,9 +120,59 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
), ),
), ),
), ),
),
),
],
); );
}, },
), ),
Positioned(
top: 300.h,
right: 20.w,
child: Obx(() => Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.network_check, color: Colors.redAccent, size: 18),
SizedBox(width: 6),
Text(
'接受服务端H264帧率/秒: ',
style: TextStyle(color: Colors.white, fontSize: 15),
),
Text(
'${state.networkH264Fps.value}',
style: TextStyle(color: Colors.redAccent, fontSize: 16, fontWeight: FontWeight.bold),
),
Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)),
],
),
SizedBox(height: 4),
Row(
children: [
Icon(Icons.send, color: Colors.blueAccent, size: 18),
SizedBox(width: 6),
Text(
'送入Native帧率/秒: ',
style: TextStyle(color: Colors.white, fontSize: 15),
),
Text(
'${state.nativeSendFps.value}',
style: TextStyle(color: Colors.blueAccent, fontSize: 16, fontWeight: FontWeight.bold),
),
Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)),
],
),
],
),
)),
),
Obx(() => state.isLoading.isTrue Obx(() => state.isLoading.isTrue
? Positioned( ? Positioned(
bottom: 310.h, bottom: 310.h,
@ -127,102 +181,6 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
style: TextStyle(color: Colors.black, fontSize: 26.sp), style: TextStyle(color: Colors.black, fontSize: 26.sp),
)) ))
: Container()), : Container()),
Obx(() => state.textureId.value != null && state.showFps.value
? Positioned(
top: ScreenUtil().statusBarHeight + 10.h,
right: 20.w,
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(5.h),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
// Text(
// 'FPS: ${state.decoderFps.value.toStringAsFixed(1)}',
// style: TextStyle(
// color: _getPacketLossColor(
// state.packetLossRate.value),
// fontSize: 20.sp,
// ),
// ),
Text(
'丢包率: ${state.packetLossRate.value.toStringAsFixed(1)}%',
style: TextStyle(
color: _getPacketLossColor(
state.packetLossRate.value),
fontSize: 20.sp,
),
),
Text(
'消息丢失: ${state.messageLossRate.value.toStringAsFixed(1)}%',
style: TextStyle(
color: _getPacketLossColor(
state.messageLossRate.value),
fontSize: 20.sp,
),
),
Divider(
color: Colors.white30,
height: 10.h,
thickness: 1),
Text(
'已渲染帧: ${state.renderedFrameCount.value}',
style:
TextStyle(color: Colors.white, fontSize: 18.sp),
),
Text(
'总帧数: ${state.totalFrames.value}',
style:
TextStyle(color: Colors.white, fontSize: 18.sp),
),
Text(
'丢弃帧: ${state.droppedFrames.value}',
style:
TextStyle(color: Colors.white, fontSize: 18.sp),
),
Text(
'IDR帧: ${state.hasSentIDR.value ? "已发送" : "未发送"}',
style: TextStyle(
color: state.hasSentIDR.value
? Colors.green
: Colors.red,
fontSize: 18.sp),
),
Text(
'SPS: ${state.hasSentSPS.value ? "已发送" : "未发送"}',
style: TextStyle(
color: state.hasSentSPS.value
? Colors.green
: Colors.red,
fontSize: 18.sp),
),
Text(
'PPS: ${state.hasSentPPS.value ? "已发送" : "未发送"}',
style: TextStyle(
color: state.hasSentPPS.value
? Colors.green
: Colors.red,
fontSize: 18.sp),
),
Text(
'keyFrameInterval: ${state.keyFrameInterval.value}',
style:
TextStyle(color: Colors.green, fontSize: 18.sp),
),
Text(
'decodingJitterMs: ${state.decodingJitterMs.value}',
style:
TextStyle(color: Colors.green, fontSize: 18.sp),
),
],
),
),
)
: Container()),
Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0 Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0
? Positioned( ? Positioned(
top: ScreenUtil().statusBarHeight + 75.h, top: ScreenUtil().statusBarHeight + 75.h,

View File

@ -106,4 +106,19 @@ class TalkViewNativeDecodeState {
// Mapkey为textureId_frameSeq // Mapkey为textureId_frameSeq
Map<String, Map<String, dynamic>> frameTracker = {}; Map<String, Map<String, dynamic>> frameTracker = {};
// H264帧缓冲区相关
final List<Map<String, dynamic>> h264FrameBuffer = <Map<String, dynamic>>[]; // H264帧缓冲区
final int maxFrameBufferSize = 25; //
final int targetFps = 120; //
Timer? frameProcessTimer; //
bool isProcessingFrame = false; //
int lastProcessedTimestamp = 0; //
// H264文件保存相关
String? h264FilePath;
File? h264File;
//
RxInt networkH264Fps = 0.obs;
RxInt nativeSendFps = 0.obs;
} }