Merge branch 'develop_sky_liyi' into 'develop_sky'
Develop sky liyi See merge request StarlockTeam/app-starlock!174
This commit is contained in:
commit
6385a2c7b9
@ -33,7 +33,10 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
if (scpMessage.Payload != null) {
|
if (scpMessage.Payload != null) {
|
||||||
final TalkData talkData = scpMessage.Payload;
|
final TalkData talkData = scpMessage.Payload;
|
||||||
// 处理音视频数据
|
// 处理音视频数据
|
||||||
_handleTalkData(talkData: talkData);
|
_handleTalkData(
|
||||||
|
talkData: talkData,
|
||||||
|
scpMessage: scpMessage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,12 +96,15 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
return hexList.join('');
|
return hexList.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleTalkData({required TalkData talkData}) {
|
void _handleTalkData({
|
||||||
|
required TalkData talkData,
|
||||||
|
required ScpMessage scpMessage,
|
||||||
|
}) {
|
||||||
if (talkData == null) return;
|
if (talkData == null) return;
|
||||||
final contentType = talkData.contentType;
|
final contentType = talkData.contentType;
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case TalkData_ContentTypeE.H264:
|
case TalkData_ContentTypeE.H264:
|
||||||
_handleVideoH264(talkData);
|
_handleVideoH264(talkData, scpMessage);
|
||||||
break;
|
break;
|
||||||
case TalkData_ContentTypeE.Image:
|
case TalkData_ContentTypeE.Image:
|
||||||
_handleVideoImage(talkData);
|
_handleVideoImage(talkData);
|
||||||
@ -113,10 +119,10 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 处理h264协议的数据
|
/// 处理h264协议的数据
|
||||||
void _handleVideoH264(TalkData talkData) {
|
void _handleVideoH264(TalkData talkData, ScpMessage scpMessage) {
|
||||||
final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame();
|
final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame();
|
||||||
talkDataH264Frame.mergeFromBuffer(talkData.content);
|
talkDataH264Frame.mergeFromBuffer(talkData.content);
|
||||||
frameHandler.handleFrame(talkDataH264Frame, talkData);
|
frameHandler.handleFrame(talkDataH264Frame, talkData, scpMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 处理图片数据
|
/// 处理图片数据
|
||||||
|
|||||||
@ -93,20 +93,21 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle
|
|||||||
}
|
}
|
||||||
if (isWifiLockType ||
|
if (isWifiLockType ||
|
||||||
(talkExpectResp.rotate == 0 &&
|
(talkExpectResp.rotate == 0 &&
|
||||||
talkExpectResp.width == 640 &&
|
talkExpectResp.width == 640 &&
|
||||||
talkExpectResp.height == 480)) {
|
talkExpectResp.height == 480) &&
|
||||||
|
talkStatus.status != TalkStatus.answeredSuccessfully) {
|
||||||
Get.toNamed(Routers.imageTransmissionView);
|
Get.toNamed(Routers.imageTransmissionView);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (startChartManage
|
if (startChartManage
|
||||||
.getDefaultTalkExpect()
|
.getDefaultTalkExpect()
|
||||||
.videoType
|
.videoType
|
||||||
.contains(VideoTypeE.H264)) {
|
.contains(VideoTypeE.H264) &&
|
||||||
|
talkStatus.status != TalkStatus.answeredSuccessfully) {
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routers.h264View,
|
Routers.h264View,
|
||||||
);
|
);
|
||||||
} else {
|
} else if (talkStatus.status != TalkStatus.answeredSuccessfully) {
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routers.starChartTalkView,
|
Routers.starChartTalkView,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:star_lock/app_settings/app_settings.dart';
|
import 'package:star_lock/app_settings/app_settings.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
import '../../proto/talk_data_h264_frame.pb.dart';
|
import '../../proto/talk_data_h264_frame.pb.dart';
|
||||||
@ -12,8 +13,12 @@ class H264FrameHandler {
|
|||||||
|
|
||||||
H264FrameHandler({required this.onCompleteFrame});
|
H264FrameHandler({required this.onCompleteFrame});
|
||||||
|
|
||||||
void handleFrame(TalkDataH264Frame frame, TalkData talkData) {
|
void handleFrame(
|
||||||
onCompleteFrame(
|
TalkDataH264Frame frame, TalkData talkData, ScpMessage scpMessage) {
|
||||||
TalkDataModel(talkData: talkData, talkDataH264Frame: frame));
|
onCompleteFrame(TalkDataModel(
|
||||||
|
talkData: talkData,
|
||||||
|
talkDataH264Frame: frame,
|
||||||
|
scpMessage: scpMessage,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart';
|
||||||
|
|
||||||
class TalkDataModel {
|
class TalkDataModel {
|
||||||
TalkData? talkData;
|
TalkData? talkData;
|
||||||
TalkDataH264Frame? talkDataH264Frame;
|
TalkDataH264Frame? talkDataH264Frame;
|
||||||
|
ScpMessage? scpMessage;
|
||||||
|
|
||||||
TalkDataModel({required this.talkData, this.talkDataH264Frame});
|
TalkDataModel(
|
||||||
|
{required this.talkData, this.talkDataH264Frame, this.scpMessage});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,10 @@ import 'package:star_lock/tools/deviceInfo_utils.dart';
|
|||||||
import 'package:star_lock/tools/storage.dart';
|
import 'package:star_lock/tools/storage.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
// Socket选项常量
|
||||||
|
const int SO_RCVBUF = 8; // 接收缓冲区
|
||||||
|
const int SO_SNDBUF = 7; // 发送缓冲区
|
||||||
|
|
||||||
class StartChartManage {
|
class StartChartManage {
|
||||||
// 私有构造函数,防止外部直接new对象
|
// 私有构造函数,防止外部直接new对象
|
||||||
StartChartManage._internal();
|
StartChartManage._internal();
|
||||||
@ -125,6 +129,17 @@ class StartChartManage {
|
|||||||
// 获取 StartChartTalkStatus 的唯一实例
|
// 获取 StartChartTalkStatus 的唯一实例
|
||||||
StartChartTalkStatus talkStatus = StartChartTalkStatus.instance;
|
StartChartTalkStatus talkStatus = StartChartTalkStatus.instance;
|
||||||
|
|
||||||
|
// 音视频帧级丢包统计变量
|
||||||
|
final Map<int, Set<int>> _avFrameParts = {};
|
||||||
|
int _avFrameTotal = 0;
|
||||||
|
int _avFrameLost = 0;
|
||||||
|
|
||||||
|
// 查询音视频帧丢包率
|
||||||
|
double getAvFrameLossRate() {
|
||||||
|
if (_avFrameTotal == 0) return 0.0;
|
||||||
|
return _avFrameLost / _avFrameTotal;
|
||||||
|
}
|
||||||
|
|
||||||
// 星图服务初始化
|
// 星图服务初始化
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (F.isXHJ) {
|
if (F.isXHJ) {
|
||||||
@ -225,6 +240,25 @@ class StartChartManage {
|
|||||||
var addressIListenFrom = InternetAddress.anyIPv4;
|
var addressIListenFrom = InternetAddress.anyIPv4;
|
||||||
RawDatagramSocket.bind(addressIListenFrom, localPort)
|
RawDatagramSocket.bind(addressIListenFrom, localPort)
|
||||||
.then((RawDatagramSocket socket) {
|
.then((RawDatagramSocket socket) {
|
||||||
|
|
||||||
|
// 设置接收缓冲区大小 (SO_RCVBUF = 8)
|
||||||
|
socket.setRawOption(
|
||||||
|
RawSocketOption.fromInt(
|
||||||
|
RawSocketOption.levelSocket,
|
||||||
|
8, // SO_RCVBUF for Android/iOS
|
||||||
|
2 * 1024 * 1024, // 2MB receive buffer
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 设置发送缓冲区大小 (SO_SNDBUF = 7)
|
||||||
|
socket.setRawOption(
|
||||||
|
RawSocketOption.fromInt(
|
||||||
|
RawSocketOption.levelSocket,
|
||||||
|
7, // SO_SNDBUF for Android/iOS
|
||||||
|
2 * 1024 * 1024, // 2MB send buffer
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
_udpSocket = socket;
|
_udpSocket = socket;
|
||||||
|
|
||||||
/// 广播功能
|
/// 广播功能
|
||||||
@ -1017,35 +1051,54 @@ class StartChartManage {
|
|||||||
void _onReceiveData(RawDatagramSocket socket, BuildContext context) {
|
void _onReceiveData(RawDatagramSocket socket, BuildContext context) {
|
||||||
socket.listen((RawSocketEvent event) {
|
socket.listen((RawSocketEvent event) {
|
||||||
if (event == RawSocketEvent.read) {
|
if (event == RawSocketEvent.read) {
|
||||||
Datagram? dg = socket.receive();
|
Datagram? dg;
|
||||||
try {
|
while ((dg = socket.receive()) != null) {
|
||||||
if (dg?.data != null) {
|
try {
|
||||||
final deserialize = ScpMessage.deserialize(dg!.data);
|
if (dg?.data != null) {
|
||||||
|
final deserialize = ScpMessage.deserialize(dg!.data);
|
||||||
|
|
||||||
// //ToDo: 增加对讲调试、正式可删除
|
// 音视频帧丢包统计:只统计PayloadType==talkData的数据包,结合分包信息
|
||||||
// UdpTalkDataHandler().updateRecvDataRate(dg.data.length);
|
if (deserialize != null &&
|
||||||
|
deserialize.PayloadType == PayloadTypeConstant.talkData) {
|
||||||
// // 更新调试信息
|
int? msgId = deserialize.MessageId;
|
||||||
// Provider.of<DebugInfoModel>(context, listen: false).updateDebugInfo(
|
int spTotal = deserialize.SpTotal ?? 1;
|
||||||
// UdpTalkDataHandler().getLastRecvDataRate() ~/ 1024, // 转换为KB
|
int spIndex = deserialize.SpIndex ?? 1;
|
||||||
// UdpTalkDataHandler().getLastRecvPacketCount(),
|
if (msgId != null) {
|
||||||
// UdpTalkDataHandler().getLastSendDataRate() ~/ 1024, // 转换为KB
|
// 记录收到的分包
|
||||||
// UdpTalkDataHandler().getLastSendPacketCount(),
|
_avFrameParts.putIfAbsent(msgId, () => <int>{});
|
||||||
// );
|
_avFrameParts[msgId]!.add(spIndex);
|
||||||
|
// 如果收到最后一个分包,判断该帧是否完整
|
||||||
if (deserialize != null) {
|
if (spIndex == spTotal) {
|
||||||
// 处理返回数据
|
_avFrameTotal++;
|
||||||
_handleUdpResultData(deserialize);
|
if (_avFrameParts[msgId]!.length < spTotal) {
|
||||||
}
|
_avFrameLost++;
|
||||||
if (deserialize.PayloadType != PayloadTypeConstant.heartbeat) {
|
// _log(text: '音视频丢包,丢失的messageId: $msgId');
|
||||||
if (deserialize.Payload != null) {
|
}
|
||||||
// _log(text: 'Udp收到结构体数据---》$deserialize');
|
_avFrameParts.remove(msgId);
|
||||||
|
// 可选:每100帧打印一次丢包率
|
||||||
|
if (_avFrameTotal % 100 == 0) {
|
||||||
|
_log(
|
||||||
|
text:
|
||||||
|
'音视频帧丢包率: ${(getAvFrameLossRate() * 100).toStringAsFixed(2)}%');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deserialize != null) {
|
||||||
|
// 处理返回数据
|
||||||
|
_handleUdpResultData(deserialize);
|
||||||
|
}
|
||||||
|
// if (deserialize.PayloadType != PayloadTypeConstant.heartbeat) {
|
||||||
|
// if (deserialize.Payload != null) {
|
||||||
|
// _log(text: 'Udp收到结构体数据---》$deserialize');
|
||||||
|
// }
|
||||||
// _log(text: 'text---》${utf8.decode(deserialize.Payload)}');
|
// _log(text: 'text---》${utf8.decode(deserialize.Payload)}');
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
throw StartChartMessageException('$e\n,$stackTrace');
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
|
||||||
throw StartChartMessageException('$e\n,$stackTrace');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import 'package:star_lock/network/api_repository.dart';
|
|||||||
import 'package:star_lock/talk/call/callTalk.dart';
|
import 'package:star_lock/talk/call/callTalk.dart';
|
||||||
import 'package:star_lock/talk/call/g711.dart';
|
import 'package:star_lock/talk/call/g711.dart';
|
||||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||||
|
import 'package:star_lock/talk/starChart/entity/scp_message.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart';
|
||||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||||
@ -87,15 +88,17 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
// 新增:等待新I帧状态
|
// 新增:等待新I帧状态
|
||||||
bool _waitingForIFrame = false;
|
bool _waitingForIFrame = false;
|
||||||
|
|
||||||
|
int? lastDecodedIFrameSeq;
|
||||||
|
|
||||||
// 初始化视频解码器
|
// 初始化视频解码器
|
||||||
Future<void> _initVideoDecoder() async {
|
Future<void> _initVideoDecoder() async {
|
||||||
try {
|
try {
|
||||||
state.isLoading.value = true;
|
state.isLoading.value = true;
|
||||||
// 创建解码器配置
|
// 创建解码器配置
|
||||||
final config = VideoDecoderConfig(
|
final config = VideoDecoderConfig(
|
||||||
width: 864,
|
width: StartChartManage().videoWidth,
|
||||||
// 实际视频宽度
|
// 实际视频宽度
|
||||||
height: 480,
|
height: StartChartManage().videoHeight,
|
||||||
codecType: 'h264',
|
codecType: 'h264',
|
||||||
);
|
);
|
||||||
// 初始化解码器并获取textureId
|
// 初始化解码器并获取textureId
|
||||||
@ -157,6 +160,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
int pts,
|
int pts,
|
||||||
int frameSeq,
|
int frameSeq,
|
||||||
int frameSeqI,
|
int frameSeqI,
|
||||||
|
ScpMessage scpMessage,
|
||||||
) {
|
) {
|
||||||
// 检测frameSeq回绕,且为I帧
|
// 检测frameSeq回绕,且为I帧
|
||||||
if (!_pendingStreamReset &&
|
if (!_pendingStreamReset &&
|
||||||
@ -212,6 +216,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
'frameSeq': frameSeq,
|
'frameSeq': frameSeq,
|
||||||
'frameSeqI': frameSeqI,
|
'frameSeqI': frameSeqI,
|
||||||
'pts': pts,
|
'pts': pts,
|
||||||
|
'scpMessage': scpMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果缓冲区超出最大大小,优先丢弃P/B帧
|
// 如果缓冲区超出最大大小,优先丢弃P/B帧
|
||||||
@ -257,14 +262,25 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置正在处理标志
|
// 优先查找I帧,按frameSeq最小的I帧消费
|
||||||
state.isProcessingFrame = true;
|
final iFrames = state.h264FrameBuffer
|
||||||
|
.where((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.I)
|
||||||
|
.toList();
|
||||||
|
iFrames
|
||||||
|
.sort((a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int));
|
||||||
|
|
||||||
try {
|
if (iFrames.isNotEmpty) {
|
||||||
// 取出最早的帧
|
// 有I帧,消费最小的I帧,并记录其frameSeq
|
||||||
final Map<String, dynamic>? frameMap = state.h264FrameBuffer.isNotEmpty
|
final minIFrame = iFrames.first;
|
||||||
? state.h264FrameBuffer.removeAt(0)
|
final minIFrameSeq = minIFrame['frameSeq'];
|
||||||
: null;
|
final targetIndex = state.h264FrameBuffer.indexWhere(
|
||||||
|
(f) =>
|
||||||
|
f['frameType'] == TalkDataH264Frame_FrameTypeE.I &&
|
||||||
|
f['frameSeq'] == minIFrameSeq,
|
||||||
|
);
|
||||||
|
state.isProcessingFrame = true;
|
||||||
|
final Map<String, dynamic>? frameMap =
|
||||||
|
state.h264FrameBuffer.removeAt(targetIndex);
|
||||||
if (frameMap == null) {
|
if (frameMap == null) {
|
||||||
state.isProcessingFrame = false;
|
state.isProcessingFrame = false;
|
||||||
return;
|
return;
|
||||||
@ -274,6 +290,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
final int? frameSeq = frameMap['frameSeq'];
|
final int? frameSeq = frameMap['frameSeq'];
|
||||||
final int? frameSeqI = frameMap['frameSeqI'];
|
final int? frameSeqI = frameMap['frameSeqI'];
|
||||||
final int? pts = frameMap['pts'];
|
final int? pts = frameMap['pts'];
|
||||||
|
final ScpMessage? scpMessage = frameMap['scpMessage'];
|
||||||
if (frameData == null ||
|
if (frameData == null ||
|
||||||
frameType == null ||
|
frameType == null ||
|
||||||
frameSeq == null ||
|
frameSeq == null ||
|
||||||
@ -282,25 +299,82 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
state.isProcessingFrame = false;
|
state.isProcessingFrame = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 解码器未初始化或textureId为null时跳过
|
|
||||||
if (state.textureId.value == null) {
|
if (state.textureId.value == null) {
|
||||||
state.isProcessingFrame = false;
|
state.isProcessingFrame = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
lastDecodedIFrameSeq = minIFrameSeq;
|
||||||
|
// AppLog.log('送入解码器的P帧数据frameSeq:${frameSeq},frameSeqI:${frameSeqI},'
|
||||||
|
// 'frameType:${frameType},messageId:${scpMessage!.MessageId}');
|
||||||
await VideoDecodePlugin.sendFrame(
|
await VideoDecodePlugin.sendFrame(
|
||||||
frameData: frameData,
|
frameData: frameData,
|
||||||
frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1,
|
frameType: 0,
|
||||||
frameSeq: frameSeq,
|
frameSeq: frameSeq,
|
||||||
timestamp: pts,
|
timestamp: pts,
|
||||||
splitNalFromIFrame: true,
|
splitNalFromIFrame: true,
|
||||||
refIFrameSeq: frameSeqI,
|
refIFrameSeq: frameSeqI,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
|
||||||
AppLog.log('处理缓冲帧失败: $e');
|
|
||||||
} finally {
|
|
||||||
// 重置处理标志
|
|
||||||
state.isProcessingFrame = false;
|
state.isProcessingFrame = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 没有I帧时,只消费refIFrameSeq等于lastDecodedIFrameSeq的P帧
|
||||||
|
if (lastDecodedIFrameSeq != null) {
|
||||||
|
final validPFrames = state.h264FrameBuffer
|
||||||
|
.where((f) =>
|
||||||
|
f['frameType'] == TalkDataH264Frame_FrameTypeE.P &&
|
||||||
|
f['frameSeqI'] == lastDecodedIFrameSeq)
|
||||||
|
.toList();
|
||||||
|
if (validPFrames.isNotEmpty) {
|
||||||
|
validPFrames.sort(
|
||||||
|
(a, b) => (a['frameSeq'] as int).compareTo(b['frameSeq'] as int));
|
||||||
|
final minPFrame = validPFrames.first;
|
||||||
|
final targetIndex = state.h264FrameBuffer.indexWhere(
|
||||||
|
(f) =>
|
||||||
|
f['frameType'] == TalkDataH264Frame_FrameTypeE.P &&
|
||||||
|
f['frameSeq'] == minPFrame['frameSeq'] &&
|
||||||
|
f['frameSeqI'] == lastDecodedIFrameSeq,
|
||||||
|
);
|
||||||
|
state.isProcessingFrame = true;
|
||||||
|
final Map<String, dynamic>? frameMap =
|
||||||
|
state.h264FrameBuffer.removeAt(targetIndex);
|
||||||
|
if (frameMap == null) {
|
||||||
|
state.isProcessingFrame = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final List<int>? frameData = frameMap['frameData'];
|
||||||
|
final TalkDataH264Frame_FrameTypeE? frameType = frameMap['frameType'];
|
||||||
|
final int? frameSeq = frameMap['frameSeq'];
|
||||||
|
final int? frameSeqI = frameMap['frameSeqI'];
|
||||||
|
final int? pts = frameMap['pts'];
|
||||||
|
final ScpMessage? scpMessage = frameMap['scpMessage'];
|
||||||
|
if (frameData == null ||
|
||||||
|
frameType == null ||
|
||||||
|
frameSeq == null ||
|
||||||
|
frameSeqI == null ||
|
||||||
|
pts == null) {
|
||||||
|
state.isProcessingFrame = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.textureId.value == null) {
|
||||||
|
state.isProcessingFrame = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// AppLog.log('送入解码器的P帧数据frameSeq:${frameSeq},frameSeqI:${frameSeqI},'
|
||||||
|
// 'frameType:${frameType},messageId:${scpMessage!.MessageId}');
|
||||||
|
await VideoDecodePlugin.sendFrame(
|
||||||
|
frameData: frameData,
|
||||||
|
frameType: 1,
|
||||||
|
frameSeq: frameSeq,
|
||||||
|
timestamp: pts,
|
||||||
|
splitNalFromIFrame: true,
|
||||||
|
refIFrameSeq: frameSeqI,
|
||||||
|
);
|
||||||
|
state.isProcessingFrame = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 其他情况不消费,等待I帧到来
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止帧处理定时器
|
/// 停止帧处理定时器
|
||||||
@ -331,6 +405,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
.listen((TalkDataModel talkDataModel) async {
|
.listen((TalkDataModel talkDataModel) async {
|
||||||
final talkData = talkDataModel.talkData;
|
final talkData = talkDataModel.talkData;
|
||||||
final talkDataH264Frame = talkDataModel.talkDataH264Frame;
|
final talkDataH264Frame = talkDataModel.talkDataH264Frame;
|
||||||
|
final scpMessage = talkDataModel.scpMessage;
|
||||||
final contentType = talkData!.contentType;
|
final contentType = talkData!.contentType;
|
||||||
|
|
||||||
// 判断数据类型,进行分发处理
|
// 判断数据类型,进行分发处理
|
||||||
@ -345,7 +420,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
break;
|
break;
|
||||||
case TalkData_ContentTypeE.H264:
|
case TalkData_ContentTypeE.H264:
|
||||||
// 处理H264帧
|
// 处理H264帧
|
||||||
if (state.textureId.value != null) {
|
if (state.textureId.value != null || true) {
|
||||||
if (talkDataH264Frame != null) {
|
if (talkDataH264Frame != null) {
|
||||||
_addFrameToBuffer(
|
_addFrameToBuffer(
|
||||||
talkData.content,
|
talkData.content,
|
||||||
@ -353,6 +428,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
talkData.durationMs,
|
talkData.durationMs,
|
||||||
talkDataH264Frame.frameSeq,
|
talkDataH264Frame.frameSeq,
|
||||||
talkDataH264Frame.frameSeqI,
|
talkDataH264Frame.frameSeqI,
|
||||||
|
scpMessage!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -585,7 +661,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
TalkExpectReq talkExpectReq = TalkExpectReq();
|
TalkExpectReq talkExpectReq = TalkExpectReq();
|
||||||
state.isOpenVoice.value = !state.isOpenVoice.value;
|
state.isOpenVoice.value = !state.isOpenVoice.value;
|
||||||
// 根据当前清晰度动态设置videoType
|
// 根据当前清晰度动态设置videoType
|
||||||
VideoTypeE currentVideoType = qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
|
VideoTypeE currentVideoType =
|
||||||
|
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
|
||||||
if (!state.isOpenVoice.value) {
|
if (!state.isOpenVoice.value) {
|
||||||
talkExpectReq = TalkExpectReq(
|
talkExpectReq = TalkExpectReq(
|
||||||
videoType: [currentVideoType],
|
videoType: [currentVideoType],
|
||||||
@ -1104,144 +1181,144 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 新增:I帧处理方法
|
// 新增:I帧处理方法
|
||||||
void _handleIFrameWithSpsPpsAndIdr(
|
// void _handleIFrameWithSpsPpsAndIdr(
|
||||||
List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
||||||
// 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧)
|
// // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧)
|
||||||
state.h264FrameBuffer.clear();
|
// state.h264FrameBuffer.clear();
|
||||||
_extractAndBufferSpsPpsForBuffer(
|
// _extractAndBufferSpsPpsForBuffer(
|
||||||
frameData, durationMs, frameSeq, frameSeqI);
|
// frameData, durationMs, frameSeq, frameSeqI);
|
||||||
// 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR)
|
// // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR)
|
||||||
if (spsCache == null || ppsCache == null) {
|
// if (spsCache == null || ppsCache == null) {
|
||||||
// 没有SPS/PPS缓存,丢弃本次I帧
|
// // 没有SPS/PPS缓存,丢弃本次I帧
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 先写入SPS/PPS
|
// // 先写入SPS/PPS
|
||||||
_addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
// _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
||||||
frameSeq, frameSeqI);
|
// frameSeq, frameSeqI);
|
||||||
_addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
// _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
||||||
frameSeq, frameSeqI);
|
// frameSeq, frameSeqI);
|
||||||
// 分割I帧包,只写入IDR(type 5)
|
// // 分割I帧包,只写入IDR(type 5)
|
||||||
List<List<int>> nalus = [];
|
// List<List<int>> nalus = [];
|
||||||
int i = 0;
|
// int i = 0;
|
||||||
List<int> data = frameData;
|
// List<int> data = frameData;
|
||||||
while (i < data.length - 3) {
|
// while (i < data.length - 3) {
|
||||||
int start = -1;
|
// int start = -1;
|
||||||
int next = -1;
|
// int next = -1;
|
||||||
if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
||||||
if (data[i + 2] == 0x01) {
|
// if (data[i + 2] == 0x01) {
|
||||||
start = i;
|
// start = i;
|
||||||
i += 3;
|
// i += 3;
|
||||||
} else if (i + 3 < data.length &&
|
// } else if (i + 3 < data.length &&
|
||||||
data[i + 2] == 0x00 &&
|
// data[i + 2] == 0x00 &&
|
||||||
data[i + 3] == 0x01) {
|
// data[i + 3] == 0x01) {
|
||||||
start = i;
|
// start = i;
|
||||||
i += 4;
|
// i += 4;
|
||||||
} else {
|
// } else {
|
||||||
i++;
|
// i++;
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
next = i;
|
// next = i;
|
||||||
while (next < data.length - 3) {
|
// while (next < data.length - 3) {
|
||||||
if (data[next] == 0x00 &&
|
// if (data[next] == 0x00 &&
|
||||||
data[next + 1] == 0x00 &&
|
// data[next + 1] == 0x00 &&
|
||||||
((data[next + 2] == 0x01) ||
|
// ((data[next + 2] == 0x01) ||
|
||||||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
next++;
|
// next++;
|
||||||
}
|
// }
|
||||||
nalus.add(data.sublist(start, next));
|
// nalus.add(data.sublist(start, next));
|
||||||
i = next;
|
// i = next;
|
||||||
} else {
|
// } else {
|
||||||
i++;
|
// i++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
int nalusTotalLen =
|
// int nalusTotalLen =
|
||||||
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
||||||
if (nalus.isEmpty && data.isNotEmpty) {
|
// if (nalus.isEmpty && data.isNotEmpty) {
|
||||||
nalus.add(data);
|
// nalus.add(data);
|
||||||
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
||||||
nalus.add(data.sublist(nalusTotalLen));
|
// nalus.add(data.sublist(nalusTotalLen));
|
||||||
}
|
// }
|
||||||
for (final nalu in nalus) {
|
// for (final nalu in nalus) {
|
||||||
int offset = 0;
|
// int offset = 0;
|
||||||
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||||
if (nalu[2] == 0x01)
|
// if (nalu[2] == 0x01)
|
||||||
offset = 3;
|
// offset = 3;
|
||||||
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||||
}
|
// }
|
||||||
if (nalu.length > offset) {
|
// if (nalu.length > offset) {
|
||||||
int naluType = nalu[offset] & 0x1F;
|
// int naluType = nalu[offset] & 0x1F;
|
||||||
if (naluType == 5) {
|
// if (naluType == 5) {
|
||||||
_addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs,
|
||||||
frameSeq, frameSeqI);
|
// frameSeq, frameSeqI);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 新增:P帧处理方法
|
// 新增:P帧处理方法
|
||||||
void _handlePFrame(
|
// void _handlePFrame(
|
||||||
List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
// List<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
||||||
// 只写入P帧(type 1)
|
// // 只写入P帧(type 1)
|
||||||
List<List<int>> nalus = [];
|
// List<List<int>> nalus = [];
|
||||||
int i = 0;
|
// int i = 0;
|
||||||
List<int> data = frameData;
|
// List<int> data = frameData;
|
||||||
while (i < data.length - 3) {
|
// while (i < data.length - 3) {
|
||||||
int start = -1;
|
// int start = -1;
|
||||||
int next = -1;
|
// int next = -1;
|
||||||
if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
// if (data[i] == 0x00 && data[i + 1] == 0x00) {
|
||||||
if (data[i + 2] == 0x01) {
|
// if (data[i + 2] == 0x01) {
|
||||||
start = i;
|
// start = i;
|
||||||
i += 3;
|
// i += 3;
|
||||||
} else if (i + 3 < data.length &&
|
// } else if (i + 3 < data.length &&
|
||||||
data[i + 2] == 0x00 &&
|
// data[i + 2] == 0x00 &&
|
||||||
data[i + 3] == 0x01) {
|
// data[i + 3] == 0x01) {
|
||||||
start = i;
|
// start = i;
|
||||||
i += 4;
|
// i += 4;
|
||||||
} else {
|
// } else {
|
||||||
i++;
|
// i++;
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
next = i;
|
// next = i;
|
||||||
while (next < data.length - 3) {
|
// while (next < data.length - 3) {
|
||||||
if (data[next] == 0x00 &&
|
// if (data[next] == 0x00 &&
|
||||||
data[next + 1] == 0x00 &&
|
// data[next + 1] == 0x00 &&
|
||||||
((data[next + 2] == 0x01) ||
|
// ((data[next + 2] == 0x01) ||
|
||||||
(data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
// (data[next + 2] == 0x00 && data[next + 3] == 0x01))) {
|
||||||
break;
|
// break;
|
||||||
}
|
// }
|
||||||
next++;
|
// next++;
|
||||||
}
|
// }
|
||||||
nalus.add(data.sublist(start, next));
|
// nalus.add(data.sublist(start, next));
|
||||||
i = next;
|
// i = next;
|
||||||
} else {
|
// } else {
|
||||||
i++;
|
// i++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
int nalusTotalLen =
|
// int nalusTotalLen =
|
||||||
nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
// nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0;
|
||||||
if (nalus.isEmpty && data.isNotEmpty) {
|
// if (nalus.isEmpty && data.isNotEmpty) {
|
||||||
nalus.add(data);
|
// nalus.add(data);
|
||||||
} else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
// } else if (nalus.isNotEmpty && nalusTotalLen < data.length) {
|
||||||
nalus.add(data.sublist(nalusTotalLen));
|
// nalus.add(data.sublist(nalusTotalLen));
|
||||||
}
|
// }
|
||||||
for (final nalu in nalus) {
|
// for (final nalu in nalus) {
|
||||||
int offset = 0;
|
// int offset = 0;
|
||||||
if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
// if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) {
|
||||||
if (nalu[2] == 0x01)
|
// if (nalu[2] == 0x01)
|
||||||
offset = 3;
|
// offset = 3;
|
||||||
else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
// else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4;
|
||||||
}
|
// }
|
||||||
if (nalu.length > offset) {
|
// if (nalu.length > offset) {
|
||||||
int naluType = nalu[offset] & 0x1F;
|
// int naluType = nalu[offset] & 0x1F;
|
||||||
if (naluType == 1) {
|
// if (naluType == 1) {
|
||||||
_addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs,
|
// _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs,
|
||||||
frameSeq, frameSeqI);
|
// frameSeq, frameSeqI);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 切换清晰度的方法,后续补充具体实现
|
// 切换清晰度的方法,后续补充具体实现
|
||||||
void onQualityChanged(String quality) async {
|
void onQualityChanged(String quality) async {
|
||||||
@ -1293,30 +1370,56 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
// 新增:重置解码器方法
|
// 新增:重置解码器方法
|
||||||
Future<void> _resetDecoderForNewStream(int width, int height) async {
|
Future<void> _resetDecoderForNewStream(int width, int height) async {
|
||||||
try {
|
try {
|
||||||
|
// 先停止帧处理定时器
|
||||||
|
_stopFrameProcessTimer();
|
||||||
|
|
||||||
|
// 释放旧解码器
|
||||||
if (state.textureId.value != null) {
|
if (state.textureId.value != null) {
|
||||||
await VideoDecodePlugin.releaseDecoder();
|
await VideoDecodePlugin.releaseDecoder();
|
||||||
Future.microtask(() => state.textureId.value = null);
|
state.textureId.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 等待一小段时间确保资源释放完成
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
// 创建新的解码器配置
|
||||||
final config = VideoDecoderConfig(
|
final config = VideoDecoderConfig(
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
codecType: 'h264',
|
codecType: 'h264',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 初始化新解码器
|
||||||
final textureId = await VideoDecodePlugin.initDecoder(config);
|
final textureId = await VideoDecodePlugin.initDecoder(config);
|
||||||
if (textureId != null) {
|
if (textureId != null) {
|
||||||
Future.microtask(() => state.textureId.value = textureId);
|
state.textureId.value = textureId;
|
||||||
AppLog.log('frameSeq回绕后解码器初始化成功:textureId=$textureId');
|
AppLog.log('frameSeq回绕后解码器初始化成功:textureId=$textureId');
|
||||||
|
|
||||||
|
// 设置帧渲染监听
|
||||||
VideoDecodePlugin.setOnFrameRenderedListener((textureId) {
|
VideoDecodePlugin.setOnFrameRenderedListener((textureId) {
|
||||||
AppLog.log('已经开始渲染=======');
|
AppLog.log('已经开始渲染=======');
|
||||||
// 只有真正渲染出首帧时才关闭loading
|
// 只有真正渲染出首帧时才关闭loading
|
||||||
Future.microtask(() => state.isLoading.value = false);
|
state.isLoading.value = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 重新启动帧处理定时器
|
||||||
|
_startFrameProcessTimer();
|
||||||
|
|
||||||
|
// 重置相关状态
|
||||||
|
_decodedIFrames.clear();
|
||||||
|
state.h264FrameBuffer.clear();
|
||||||
|
state.isProcessingFrame = false;
|
||||||
|
hasSps = false;
|
||||||
|
hasPps = false;
|
||||||
|
spsCache = null;
|
||||||
|
ppsCache = null;
|
||||||
} else {
|
} else {
|
||||||
AppLog.log('frameSeq回绕后解码器初始化失败');
|
AppLog.log('frameSeq回绕后解码器初始化失败');
|
||||||
|
state.isLoading.value = false;
|
||||||
}
|
}
|
||||||
_startFrameProcessTimer();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppLog.log('frameSeq回绕时解码器初始化错误: $e');
|
AppLog.log('frameSeq回绕时解码器初始化错误: $e');
|
||||||
|
state.isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -110,8 +110,8 @@ class TalkViewNativeDecodeState {
|
|||||||
|
|
||||||
// H264帧缓冲区相关
|
// H264帧缓冲区相关
|
||||||
final List<Map<String, dynamic>> h264FrameBuffer = <Map<String, dynamic>>[]; // H264帧缓冲区,存储帧数据和类型
|
final List<Map<String, dynamic>> h264FrameBuffer = <Map<String, dynamic>>[]; // H264帧缓冲区,存储帧数据和类型
|
||||||
final int maxFrameBufferSize = 15; // 最大缓冲区大小
|
final int maxFrameBufferSize = 150; // 最大缓冲区大小
|
||||||
final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区
|
final int targetFps = 60; // 目标解码帧率,只是为了快速填充native的缓冲区
|
||||||
Timer? frameProcessTimer; // 帧处理定时器
|
Timer? frameProcessTimer; // 帧处理定时器
|
||||||
bool isProcessingFrame = false; // 是否正在处理帧
|
bool isProcessingFrame = false; // 是否正在处理帧
|
||||||
int lastProcessedTimestamp = 0; // 上次处理帧的时间戳
|
int lastProcessedTimestamp = 0; // 上次处理帧的时间戳
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user