fix:完成720P 20帧渲染需求
This commit is contained in:
parent
419912c590
commit
e806987fa0
@ -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);
|
||||||
|
|||||||
@ -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帧,先分割NALU,找到SPS/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();
|
||||||
|
// 兼容部分ROM,优先用Download
|
||||||
|
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帧包,只写入IDR(type 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -106,4 +106,19 @@ class TalkViewNativeDecodeState {
|
|||||||
|
|
||||||
// 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq
|
// 帧跟踪Map,记录每个提交的帧,key为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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user