fix:完成切换清晰度逻辑
This commit is contained in:
parent
069ef1b592
commit
90f94e1a9a
@ -35,6 +35,8 @@ import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_st
|
||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||
import 'package:star_lock/tools/G711Tool.dart';
|
||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||
import 'package:star_lock/tools/commonDataManage.dart';
|
||||
import 'package:star_lock/tools/storage.dart';
|
||||
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
import '../../../../tools/baseGetXController.dart';
|
||||
@ -75,6 +77,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
// 新增:记录上一个已接收的frameSeq
|
||||
int? _lastFrameSeq;
|
||||
|
||||
// 新增:frameSeq回绕检测标志
|
||||
bool _pendingStreamReset = false;
|
||||
|
||||
// 新增:记录切换时的宽高参数
|
||||
int _pendingResetWidth = 864;
|
||||
int _pendingResetHeight = 480;
|
||||
|
||||
// 新增:等待新I帧状态
|
||||
bool _waitingForIFrame = false;
|
||||
|
||||
// 初始化视频解码器
|
||||
Future<void> _initVideoDecoder() async {
|
||||
try {
|
||||
@ -89,12 +101,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
// 初始化解码器并获取textureId
|
||||
final textureId = await VideoDecodePlugin.initDecoder(config);
|
||||
if (textureId != null) {
|
||||
state.textureId.value = textureId;
|
||||
Future.microtask(() => state.textureId.value = textureId);
|
||||
AppLog.log('视频解码器初始化成功:textureId=$textureId');
|
||||
|
||||
VideoDecodePlugin.setOnFrameRenderedListener((textureId) {
|
||||
state.isLoading.value = false;
|
||||
AppLog.log('已经开始渲染=======');
|
||||
// 只有真正渲染出首帧时才关闭loading
|
||||
Future.microtask(() => state.isLoading.value = false);
|
||||
});
|
||||
} else {
|
||||
AppLog.log('视频解码器初始化失败');
|
||||
@ -146,12 +158,53 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
int frameSeq,
|
||||
int frameSeqI,
|
||||
) {
|
||||
// 只允许frameSeq严格递增,乱序或重复帧直接丢弃
|
||||
if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) {
|
||||
AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq');
|
||||
return;
|
||||
// 检测frameSeq回绕,且为I帧
|
||||
if (!_pendingStreamReset &&
|
||||
_lastFrameSeq != null &&
|
||||
frameType == TalkDataH264Frame_FrameTypeE.I &&
|
||||
frameSeq < _lastFrameSeq!) {
|
||||
// 检测到新流I帧,进入loading并重置所有本地状态
|
||||
AppLog.log(
|
||||
'检测到新流I帧,frameSeq回绕,进入loading并重置: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq');
|
||||
Future.microtask(() => state.isLoading.value = true);
|
||||
_pendingStreamReset = true;
|
||||
// 先暂停帧处理定时器,防止竞态
|
||||
_stopFrameProcessTimer();
|
||||
// 先释放并重新初始化解码器
|
||||
_resetDecoderForNewStream(_pendingResetWidth, _pendingResetHeight);
|
||||
// 重置所有本地状态
|
||||
_lastFrameSeq = null;
|
||||
_decodedIFrames.clear();
|
||||
state.h264FrameBuffer.clear();
|
||||
// 再恢复帧处理定时器
|
||||
_startFrameProcessTimer();
|
||||
// 不return,直接用该I帧初始化解码器并解码
|
||||
// 继续往下执行
|
||||
}
|
||||
// 如果处于pendingStreamReset,等待新I帧
|
||||
if (_pendingStreamReset) {
|
||||
if (frameType == TalkDataH264Frame_FrameTypeE.I) {
|
||||
// 收到新流I帧,关闭loading,恢复正常解码
|
||||
AppLog.log('收到新流I帧,关闭loading: frameSeq=$frameSeq');
|
||||
//Future.microtask(() => state.isLoading.value = false);
|
||||
_pendingStreamReset = false;
|
||||
_lastFrameSeq = frameSeq;
|
||||
_decodedIFrames.clear();
|
||||
_decodedIFrames.add(frameSeq);
|
||||
// 继续往下执行,直接用该I帧解码
|
||||
} else {
|
||||
// 等待新流I帧期间,丢弃所有非I帧
|
||||
AppLog.log('等待新流I帧,丢弃非I帧: frameSeq=$frameSeq, frameType=$frameType');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 正常流程
|
||||
if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) {
|
||||
AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq');
|
||||
return;
|
||||
}
|
||||
_lastFrameSeq = frameSeq;
|
||||
}
|
||||
_lastFrameSeq = frameSeq;
|
||||
// 创建包含帧数据和类型的Map
|
||||
final Map<String, dynamic> frameMap = {
|
||||
'frameData': frameData,
|
||||
@ -163,8 +216,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
|
||||
// 如果缓冲区超出最大大小,优先丢弃P/B帧
|
||||
while (state.h264FrameBuffer.length >= state.maxFrameBufferSize) {
|
||||
int pbIndex = state.h264FrameBuffer.indexWhere((f) =>
|
||||
f['frameType'] == TalkDataH264Frame_FrameTypeE.P);
|
||||
int pbIndex = state.h264FrameBuffer
|
||||
.indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P);
|
||||
if (pbIndex != -1) {
|
||||
state.h264FrameBuffer.removeAt(pbIndex);
|
||||
} else {
|
||||
@ -209,29 +262,31 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
|
||||
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 = frameMap['pts'];
|
||||
// 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);
|
||||
final Map<String, dynamic>? frameMap = state.h264FrameBuffer.isNotEmpty
|
||||
? state.h264FrameBuffer.removeAt(0)
|
||||
: null;
|
||||
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'];
|
||||
if (frameData == null ||
|
||||
frameType == null ||
|
||||
frameSeq == null ||
|
||||
frameSeqI == null ||
|
||||
pts == null) {
|
||||
state.isProcessingFrame = false;
|
||||
return;
|
||||
}
|
||||
// 解码器未初始化或textureId为null时跳过
|
||||
if (state.textureId.value == null) {
|
||||
state.isProcessingFrame = false;
|
||||
return;
|
||||
}
|
||||
// 实时写入h264文件
|
||||
// _appendH264FrameToFile(frameData, frameType);
|
||||
|
||||
// final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
// final timestamp64 = timestamp is int ? timestamp : timestamp.toInt();
|
||||
await VideoDecodePlugin.sendFrame(
|
||||
frameData: frameData,
|
||||
frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1,
|
||||
@ -462,6 +517,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
// 初始化视频解码器
|
||||
_initVideoDecoder();
|
||||
|
||||
_initHdOptions();
|
||||
// 初始化H264帧缓冲区
|
||||
state.h264FrameBuffer.clear();
|
||||
state.isProcessingFrame = false;
|
||||
@ -490,7 +546,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
// 释放视频解码器资源
|
||||
if (state.textureId.value != null) {
|
||||
VideoDecodePlugin.releaseDecoder();
|
||||
state.textureId.value = null;
|
||||
Future.microtask(() => state.textureId.value = null);
|
||||
}
|
||||
|
||||
// 取消数据流监听
|
||||
@ -577,36 +633,42 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
}
|
||||
}
|
||||
|
||||
// 远程开锁
|
||||
// 远程开锁
|
||||
Future<void> remoteOpenLock() async {
|
||||
final LockListInfoItemEntity currentKeyInfo =
|
||||
CommonDataManage().currentKeyInfo;
|
||||
|
||||
var lockId = currentKeyInfo.lockId ?? 0;
|
||||
var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0;
|
||||
|
||||
final lockPeerId = StartChartManage().lockPeerId;
|
||||
final lockListPeerId = StartChartManage().lockListPeerId;
|
||||
int lockId = lockDetailState.keyInfos.value.lockId ?? 0;
|
||||
|
||||
// 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId
|
||||
// 从列表中遍历出对应的peerId
|
||||
lockListPeerId.forEach((element) {
|
||||
if (element.network?.peerId == lockPeerId) {
|
||||
lockId = element.lockId ?? 0;
|
||||
}
|
||||
});
|
||||
|
||||
final LockSetInfoEntity lockSetInfoEntity =
|
||||
await ApiRepository.to.getLockSettingInfoData(
|
||||
lockId: lockId.toString(),
|
||||
);
|
||||
if (lockSetInfoEntity.errorCode!.codeIsSuccessful) {
|
||||
if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 &&
|
||||
lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) {
|
||||
final LoginEntity entity = await ApiRepository.to
|
||||
.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
showToast('已开锁'.tr);
|
||||
StartChartManage().lockListPeerId = [];
|
||||
final LockListInfoGroupEntity? lockListInfoGroupEntity =
|
||||
await Storage.getLockMainListData();
|
||||
if (lockListInfoGroupEntity != null) {
|
||||
lockListInfoGroupEntity!.groupList?.forEach((element) {
|
||||
final lockList = element.lockList;
|
||||
if (lockList != null && lockList.length != 0) {
|
||||
for (var lockInfo in lockList) {
|
||||
final peerId = lockInfo.network?.peerId;
|
||||
if (peerId != null && peerId != '') {
|
||||
if (peerId == lockPeerId) {
|
||||
lockId = lockInfo.lockId ?? 0;
|
||||
remoteUnlock = lockInfo.lockSetting?.remoteUnlock ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast('该锁的远程开锁功能未启用'.tr);
|
||||
});
|
||||
}
|
||||
if (remoteUnlock == 1) {
|
||||
final LoginEntity entity = await ApiRepository.to
|
||||
.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
showToast('已开锁'.tr);
|
||||
StartChartManage().lockListPeerId = [];
|
||||
}
|
||||
} else {
|
||||
showToast('该锁的远程开锁功能未启用'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1172,4 +1234,81 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换清晰度的方法,后续补充具体实现
|
||||
void onQualityChanged(String quality) async {
|
||||
state.currentQuality.value = quality;
|
||||
TalkExpectReq talkExpectReq = StartChartManage().getDefaultTalkExpect();
|
||||
final audioType = talkExpectReq.audioType;
|
||||
int width = 864;
|
||||
int height = 480;
|
||||
switch (quality) {
|
||||
case '高清':
|
||||
talkExpectReq = TalkExpectReq(
|
||||
videoType: [VideoTypeE.H264_720P],
|
||||
audioType: audioType,
|
||||
);
|
||||
width = 1280;
|
||||
height = 720;
|
||||
break;
|
||||
case '标清':
|
||||
talkExpectReq = TalkExpectReq(
|
||||
videoType: [VideoTypeE.H264],
|
||||
audioType: audioType,
|
||||
);
|
||||
width = 864;
|
||||
height = 480;
|
||||
break;
|
||||
}
|
||||
|
||||
/// 修改发送预期数据
|
||||
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||
talkExpect: talkExpectReq);
|
||||
|
||||
// 不立即loading,继续解码旧流帧,等待frameSeq回绕检测
|
||||
// 仅重置frameSeq回绕检测标志
|
||||
_pendingStreamReset = false;
|
||||
_pendingResetWidth = width;
|
||||
_pendingResetHeight = height;
|
||||
}
|
||||
|
||||
void _initHdOptions() {
|
||||
TalkExpectReq talkExpectReq = StartChartManage().getDefaultTalkExpect();
|
||||
final videoType = talkExpectReq.videoType;
|
||||
if (videoType.contains(VideoTypeE.H264)) {
|
||||
state.currentQuality.value = '标清';
|
||||
} else if (videoType.contains(VideoTypeE.H264_720P)) {
|
||||
state.currentQuality.value = '高清';
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:重置解码器方法
|
||||
Future<void> _resetDecoderForNewStream(int width, int height) async {
|
||||
try {
|
||||
if (state.textureId.value != null) {
|
||||
await VideoDecodePlugin.releaseDecoder();
|
||||
Future.microtask(() => state.textureId.value = null);
|
||||
}
|
||||
final config = VideoDecoderConfig(
|
||||
width: width,
|
||||
height: height,
|
||||
codecType: 'h264',
|
||||
);
|
||||
final textureId = await VideoDecodePlugin.initDecoder(config);
|
||||
if (textureId != null) {
|
||||
Future.microtask(() => state.textureId.value = textureId);
|
||||
AppLog.log('frameSeq回绕后解码器初始化成功:textureId=$textureId');
|
||||
VideoDecodePlugin.setOnFrameRenderedListener((textureId) {
|
||||
AppLog.log('已经开始渲染=======');
|
||||
// 只有真正渲染出首帧时才关闭loading
|
||||
Future.microtask(() => state.isLoading.value = false);
|
||||
});
|
||||
} else {
|
||||
AppLog.log('frameSeq回绕后解码器初始化失败');
|
||||
}
|
||||
_startFrameProcessTimer();
|
||||
} catch (e) {
|
||||
AppLog.log('frameSeq回绕时解码器初始化错误: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,40 +97,42 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
|
||||
final double scaleWidth = physicalWidth / rotatedImageWidth;
|
||||
final double scaleHeight = physicalHeight / rotatedImageHeight;
|
||||
max(scaleWidth, scaleHeight); // 选择较大的缩放比例
|
||||
return state.isLoading.isTrue
|
||||
? Image.asset(
|
||||
'images/main/monitorBg.png',
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Positioned.fill(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
child: RepaintBoundary(
|
||||
key: state.globalKey,
|
||||
child: SizedBox.expand(
|
||||
child: RotatedBox(
|
||||
// 解码器不支持硬件旋转,使用RotatedBox
|
||||
quarterTurns:
|
||||
startChartManage.rotateAngle ~/ 90,
|
||||
child: Platform.isIOS
|
||||
? Transform.scale(
|
||||
scale: 1.008, // 轻微放大,消除iOS白边
|
||||
child: Texture(
|
||||
textureId: state.textureId.value!,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
)
|
||||
: Texture(
|
||||
textureId: state.textureId.value!,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 防御性处理:只要loading中或textureId为null,优先渲染loading/占位
|
||||
if (state.isLoading.isTrue || state.textureId.value == null) {
|
||||
return Image.asset(
|
||||
'images/main/monitorBg.png',
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
} else {
|
||||
return Positioned.fill(
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
child: RepaintBoundary(
|
||||
key: state.globalKey,
|
||||
child: SizedBox.expand(
|
||||
child: RotatedBox(
|
||||
// 解码器不支持硬件旋转,使用RotatedBox
|
||||
quarterTurns: startChartManage.rotateAngle ~/ 90,
|
||||
child: Platform.isIOS
|
||||
? Transform.scale(
|
||||
scale: 1.008, // 轻微放大,消除iOS白边
|
||||
child: Texture(
|
||||
textureId: state.textureId.value!,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
)
|
||||
: Texture(
|
||||
textureId: state.textureId.value!,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@ -295,6 +297,62 @@ class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 50.w),
|
||||
// 清晰度切换按钮
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
// 弹出底部弹出层,选择清晰度
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
final List<String> qualities = ['高清', '标清'];
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: qualities.map((q) {
|
||||
return Obx(() => InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
logic.onQualityChanged(q);
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 18.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
q,
|
||||
style: TextStyle(
|
||||
color: state.currentQuality.value == q
|
||||
? AppColors.mainColor
|
||||
: Colors.black,
|
||||
fontWeight: state.currentQuality.value == q
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
fontSize: 28.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
child: Icon(Icons.high_quality_outlined, color: Colors.white, size: 38.w),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -117,4 +117,7 @@ class TalkViewNativeDecodeState {
|
||||
// H264文件保存相关
|
||||
String? h264FilePath;
|
||||
File? h264File;
|
||||
|
||||
// 当前清晰度选项,初始为'高清'
|
||||
RxString currentQuality = '高清'.obs; // 可选:高清、标清、流畅
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user