fix:增加原生插件解码的页面、增加h264、mjpeg切换的debug按钮
This commit is contained in:
parent
07aa71c679
commit
1784f75c47
@ -60,6 +60,7 @@ import 'package:star_lock/mine/mineSet/transferSmartLock/transferSmartLockList/t
|
||||
import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFeaturesWeb_page.dart';
|
||||
import 'package:star_lock/mine/valueAddedServices/advancedFunctionRecord/advancedFunctionRecord_page.dart';
|
||||
import 'package:star_lock/mine/valueAddedServices/valueAddedServicesRecord/value_added_services_record_page.dart';
|
||||
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_page.dart';
|
||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_page.dart';
|
||||
import 'package:star_lock/talk/starChart/webView/h264_web_view.dart';
|
||||
|
||||
@ -1184,6 +1185,7 @@ abstract class AppRouters {
|
||||
page: () => const DoubleLockLinkPage()),
|
||||
GetPage<dynamic>(
|
||||
name: Routers.starChartTalkView, page: () => const TalkViewPage()),
|
||||
// GetPage<dynamic>(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()),
|
||||
GetPage<dynamic>(name: Routers.h264WebView, page: () => H264WebView()),
|
||||
];
|
||||
}
|
||||
|
||||
@ -15,6 +15,8 @@ import 'package:star_lock/main/lockDetail/lockDetail/device_network_info.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockSet/lockTime/getServerDatetime_entity.dart';
|
||||
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||
import 'package:star_lock/tools/throttler.dart';
|
||||
@ -564,7 +566,7 @@ class LockDetailLogic extends BaseGetXController {
|
||||
// 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口
|
||||
Future<void> getLockNetToken() async {
|
||||
final LockNetTokenEntity entity = await ApiRepository.to
|
||||
.getLockNetToken(lockId: state.keyInfos.value.lockId.toString());
|
||||
.getLockNetToken(lockId: state.keyInfos.value.lockId!);
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
state.lockNetToken = entity.data!.token!.toString();
|
||||
// AppLog.log('从服务器获取联网token:${state.lockNetToken}');
|
||||
@ -769,12 +771,12 @@ class LockDetailLogic extends BaseGetXController {
|
||||
if (catEyeConfig.isNotEmpty &&
|
||||
catEyeConfig.length > 0 &&
|
||||
catEyeConfig[0].catEyeMode != 0) {
|
||||
if (StartChartManage().lockNetworkInfo.wifiName == null ||
|
||||
StartChartManage().lockNetworkInfo.wifiName == '') {
|
||||
if ((StartChartManage().lockNetworkInfo.wifiName == null ||
|
||||
StartChartManage().lockNetworkInfo.wifiName == '') ) {
|
||||
showToast('设备未配网'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
PacketLossStatistics().reset();
|
||||
// 发送监控id
|
||||
StartChartManage().startCallRequestMessageTimer(
|
||||
ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? '');
|
||||
@ -795,6 +797,15 @@ class LockDetailLogic extends BaseGetXController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// 初始化开关状态为当前对讲视频模式
|
||||
final currentTalkExpect = StartChartManage().getDefaultTalkExpect();
|
||||
if (currentTalkExpect.videoType.contains(VideoTypeE.H264)) {
|
||||
state.useH264Mode.value = true;
|
||||
} else if (currentTalkExpect.videoType.contains(VideoTypeE.IMAGE)) {
|
||||
state.useH264Mode.value = false;
|
||||
}
|
||||
|
||||
state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus
|
||||
.on<LockSetChangeSetRefreshLockDetailWithType>()
|
||||
.listen((LockSetChangeSetRefreshLockDetailWithType event) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
@ -88,7 +89,6 @@ class _LockDetailPageState extends State<LockDetailPage>
|
||||
/// 路由订阅
|
||||
AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
state.isOpenLockNeedOnline.refresh();
|
||||
|
||||
}
|
||||
|
||||
StreamSubscription? _lockRefreshLockDetailInfoDataEvent;
|
||||
@ -507,6 +507,60 @@ class _LockDetailPageState extends State<LockDetailPage>
|
||||
Widget skWidget() {
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
// Container(
|
||||
// padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20),
|
||||
// margin: EdgeInsets.only(top: 10, bottom: 10),
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.white,
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black.withOpacity(0.05),
|
||||
// blurRadius: 5,
|
||||
// offset: Offset(0, 2),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text('对讲视频模式'.tr,
|
||||
// style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
// Row(
|
||||
// children: [
|
||||
// Text('mjpeg',
|
||||
// style: TextStyle(
|
||||
// fontSize: 14,
|
||||
// color: !state.useH264Mode.value
|
||||
// ? AppColors.mainColor
|
||||
// : Colors.grey)),
|
||||
// Obx(() => Switch(
|
||||
// value: state.useH264Mode.value,
|
||||
// activeColor: AppColors.mainColor,
|
||||
// onChanged: (value) {
|
||||
// state.useH264Mode.value = value;
|
||||
// if (value) {
|
||||
// // 使用H264模式
|
||||
// StartChartManage()
|
||||
// .sendH264VideoAndG711AudioTalkExpectData();
|
||||
// } else {
|
||||
// // 使用Image模式
|
||||
// StartChartManage()
|
||||
// .sendImageVideoAndG711AudioTalkExpectData();
|
||||
// }
|
||||
// },
|
||||
// )),
|
||||
// Text('H264'.tr,
|
||||
// style: TextStyle(
|
||||
// fontSize: 14,
|
||||
// color: state.useH264Mode.value
|
||||
// ? AppColors.mainColor
|
||||
// : Colors.grey)),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
Visibility(
|
||||
visible:
|
||||
(state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime ||
|
||||
@ -1467,7 +1521,7 @@ class _LockDetailPageState extends State<LockDetailPage>
|
||||
state.iSOpenLock.value = true;
|
||||
state.openLockBtnState.value = 1;
|
||||
state.animationController!.forward();
|
||||
// AppLog.log('点击开锁');
|
||||
AppLog.log('点击开锁');
|
||||
if (isOpenLockNeedOnline) {
|
||||
// 不需要联网
|
||||
state.openDoorModel = 0;
|
||||
|
||||
@ -7,18 +7,18 @@ import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dar
|
||||
import '../../../blue/io_reply.dart';
|
||||
import '../../lockMian/entity/lockListInfo_entity.dart';
|
||||
|
||||
|
||||
class LockDetailState {
|
||||
Rx<LockListInfoItemEntity> keyInfos = LockListInfoItemEntity().obs;
|
||||
final Rx<LockSetInfoData> lockSetInfoData = LockSetInfoData().obs;
|
||||
late StreamSubscription<Reply> replySubscription;
|
||||
StreamSubscription? lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent;
|
||||
StreamSubscription?
|
||||
lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent;
|
||||
StreamSubscription? LockSetChangeSetRefreshLockDetailWithTypeSubscription;
|
||||
StreamSubscription? DetailLockInfo;
|
||||
StreamSubscription? SuccessfulDistributionNetworkEvent;
|
||||
|
||||
String lockNetToken = '0';
|
||||
int differentialTime = 0;// 服务器时间与本地时间差值
|
||||
int differentialTime = 0; // 服务器时间与本地时间差值
|
||||
bool isHaveNetwork = true;
|
||||
int lockUserNo = 0;
|
||||
int senderUserId = 0;
|
||||
@ -41,7 +41,7 @@ class LockDetailState {
|
||||
RxBool bottomBtnisEable = true.obs; // 是否不可用 用于限制底部按钮是否可用
|
||||
RxBool openDoorBtnisUneable = true.obs; // 当钥匙状态不能使用的情况下开锁按钮禁止使用,默认可用
|
||||
|
||||
int openDoorModel = 0;// 离线开门0, 在线开门2 离线关门32 在线关门34
|
||||
int openDoorModel = 0; // 离线开门0, 在线开门2 离线关门32 在线关门34
|
||||
|
||||
//过渡动画控制器
|
||||
AnimationController? animationController;
|
||||
@ -58,6 +58,9 @@ class LockDetailState {
|
||||
int logCountPage = 10; // 蓝牙记录一页多少个
|
||||
RxInt nextAuthTime = 0.obs; // 下次认证时间
|
||||
|
||||
// 视频编码模式选择开关状态
|
||||
RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式
|
||||
|
||||
// LockDetailState() {
|
||||
// Map map = Get.arguments;
|
||||
// lockCount = map["lockCount"];
|
||||
|
||||
@ -353,7 +353,7 @@ class ApiProvider extends BaseProvider {
|
||||
);
|
||||
|
||||
// 获取手机联网token
|
||||
Future<Response> getLockNetToken(String lockId) => post(
|
||||
Future<Response> getLockNetToken(int lockId) => post(
|
||||
getLockNetTokenURL.toUrl,
|
||||
jsonEncode({
|
||||
'lockId': lockId,
|
||||
|
||||
@ -325,7 +325,7 @@ class ApiRepository {
|
||||
}
|
||||
|
||||
// 获取手机联网token
|
||||
Future<LockNetTokenEntity> getLockNetToken({required String lockId}) async {
|
||||
Future<LockNetTokenEntity> getLockNetToken({required int lockId}) async {
|
||||
final res = await apiProvider.getLockNetToken(lockId);
|
||||
return LockNetTokenEntity.fromJson(res.body);
|
||||
}
|
||||
|
||||
@ -62,9 +62,6 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
||||
int? spTotal,
|
||||
int? spIndex,
|
||||
int? messageId}) {
|
||||
// 获取统计信息
|
||||
final stats = PacketLossStatistics().getStatistics();
|
||||
// _asyncLog('丢包统计: $stats');
|
||||
// _asyncLog(
|
||||
// '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength');
|
||||
if (messageType == MessageTypeConstant.RealTimeData) {
|
||||
|
||||
@ -10,9 +10,6 @@ import '../../proto/talk_data_h264_frame.pb.dart';
|
||||
class H264FrameHandler {
|
||||
final void Function(TalkDataModel frameData) onCompleteFrame;
|
||||
|
||||
// 只记录最近一个I帧的序号
|
||||
int _lastProcessedIFrameSeq = -1;
|
||||
|
||||
H264FrameHandler({required this.onCompleteFrame});
|
||||
|
||||
void handleFrame(TalkDataH264Frame frame, TalkData talkData) {
|
||||
|
||||
@ -10,6 +10,10 @@ class PacketLossStatistics {
|
||||
// key: messageId, value: {totalPackets, receivedPackets}
|
||||
final Map<int, PacketInfo> _packetsMap = HashMap();
|
||||
|
||||
// 配置参数
|
||||
int _maxCapacity = 300; // 最大容量为300条记录
|
||||
int _timeoutMs = 30000; // 默认超时时间为30秒
|
||||
|
||||
// 统计信息
|
||||
int _totalMessages = 0; // 总消息数
|
||||
int _lostMessages = 0; // 丢包的消息数
|
||||
@ -18,10 +22,19 @@ class PacketLossStatistics {
|
||||
|
||||
// 记录分包数据
|
||||
void recordPacket(int messageId, int currentIndex, int totalPackets) {
|
||||
// 定期清理超时记录
|
||||
_cleanupExpiredPackets();
|
||||
|
||||
// 检查容量限制
|
||||
_checkCapacityLimit();
|
||||
|
||||
if (!_packetsMap.containsKey(messageId)) {
|
||||
_packetsMap[messageId] = PacketInfo(totalPackets);
|
||||
_totalMessages++;
|
||||
_totalPackets += totalPackets;
|
||||
} else {
|
||||
// 更新时间戳
|
||||
_packetsMap[messageId]!.timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
_packetsMap[messageId]!.receivedPackets.add(currentIndex);
|
||||
@ -32,6 +45,51 @@ class PacketLossStatistics {
|
||||
}
|
||||
}
|
||||
|
||||
// 清理超时的记录
|
||||
void _cleanupExpiredPackets() {
|
||||
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
||||
final expiredMessageIds = <int>[];
|
||||
|
||||
_packetsMap.forEach((messageId, info) {
|
||||
// 如果记录超时,添加到待清理列表
|
||||
if (currentTime - info.timestamp > _timeoutMs) {
|
||||
expiredMessageIds.add(messageId);
|
||||
|
||||
// 统计丢包
|
||||
_lostMessages++;
|
||||
_lostPackets += (info.totalPackets - info.receivedPackets.length);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除超时记录
|
||||
for (var messageId in expiredMessageIds) {
|
||||
_packetsMap.remove(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并确保不超过最大容量
|
||||
void _checkCapacityLimit() {
|
||||
if (_packetsMap.length <= _maxCapacity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果超过容量限制,按时间戳排序并删除最旧的记录
|
||||
var entries = _packetsMap.entries.toList()
|
||||
..sort((a, b) => a.value.timestamp.compareTo(b.value.timestamp));
|
||||
|
||||
// 计算需要移除的数量(移除25%的旧记录,至少保证有一定空间)
|
||||
int removeCount = (_packetsMap.length * 0.25).ceil();
|
||||
|
||||
// 移除并统计丢包
|
||||
for (int i = 0; i < removeCount && i < entries.length; i++) {
|
||||
var entry = entries[i];
|
||||
_lostMessages++;
|
||||
_lostPackets +=
|
||||
(entry.value.totalPackets - entry.value.receivedPackets.length);
|
||||
_packetsMap.remove(entry.key);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查丢包情况
|
||||
void _checkPacketLoss(int messageId) {
|
||||
final info = _packetsMap[messageId]!;
|
||||
@ -62,6 +120,28 @@ class PacketLossStatistics {
|
||||
return PacketLossInfo(messageLossRate, packetLossRate);
|
||||
}
|
||||
|
||||
// Getter和Setter,允许外部调整参数
|
||||
int get maxCapacity => _maxCapacity;
|
||||
set maxCapacity(int value) {
|
||||
if (value > 0) {
|
||||
_maxCapacity = value;
|
||||
// 设置新容量后立即检查
|
||||
_checkCapacityLimit();
|
||||
}
|
||||
}
|
||||
|
||||
int get timeoutMs => _timeoutMs;
|
||||
set timeoutMs(int value) {
|
||||
if (value > 0) {
|
||||
_timeoutMs = value;
|
||||
// 设置新超时后立即清理
|
||||
_cleanupExpiredPackets();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前未完成记录数
|
||||
int get pendingRecordsCount => _packetsMap.length;
|
||||
|
||||
// 重置统计数据
|
||||
void reset() {
|
||||
_packetsMap.clear();
|
||||
@ -76,8 +156,10 @@ class PacketLossStatistics {
|
||||
class PacketInfo {
|
||||
final int totalPackets;
|
||||
final Set<int> receivedPackets = HashSet<int>();
|
||||
int timestamp; // 添加时间戳字段,记录最后更新时间
|
||||
|
||||
PacketInfo(this.totalPackets);
|
||||
PacketInfo(this.totalPackets)
|
||||
: timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
// 丢包统计信息类
|
||||
|
||||
@ -1226,7 +1226,7 @@ class StartChartManage {
|
||||
await Storage.removerStarChartRegisterNodeInfo();
|
||||
// 关闭udp服务
|
||||
closeUdpSocket();
|
||||
PacketLossStatistics().reset();
|
||||
|
||||
}
|
||||
|
||||
/// 重置数据
|
||||
|
||||
@ -0,0 +1,798 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math'; // Import the math package to use sqrt
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
||||
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||
import 'package:gallery_saver/gallery_saver.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:star_lock/app_settings/app_settings.dart';
|
||||
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart';
|
||||
import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
|
||||
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
|
||||
import 'package:star_lock/network/api_repository.dart';
|
||||
import 'package:star_lock/talk/call/callTalk.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/handle/other/packet_loss_statistics.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_h264_frame.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/star_chart_manage.dart';
|
||||
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart';
|
||||
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:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
import '../../../../tools/baseGetXController.dart';
|
||||
|
||||
class TalkViewNativeDecodeLogic extends BaseGetXController {
|
||||
final TalkViewNativeDecodeState state = TalkViewNativeDecodeState();
|
||||
|
||||
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
|
||||
|
||||
int bufferSize = 25; // 初始化为默认大小
|
||||
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
|
||||
// 定义音频帧缓冲和发送函数
|
||||
final List<int> _bufferedAudioFrames = <int>[];
|
||||
|
||||
// 添加监听状态和订阅引用
|
||||
bool _isListening = false;
|
||||
StreamSubscription? _streamSubscription;
|
||||
|
||||
Timer? _batchProcessTimer;
|
||||
|
||||
// 添加一个集合来跟踪已成功解码的I帧序号
|
||||
final Set<int> _decodedIFrames = <int>{};
|
||||
|
||||
// 初始化视频解码器
|
||||
Future<void> _initVideoDecoder() async {
|
||||
try {
|
||||
// 创建解码器配置
|
||||
final config = VideoDecoderConfig(
|
||||
width: 864,
|
||||
// 实际视频宽度
|
||||
height: 480,
|
||||
frameRate: 25,
|
||||
// 明确设置帧率
|
||||
// 增大缓冲区大小
|
||||
codecType: CodecType.h264,
|
||||
// 编解码类型
|
||||
isDebug: true,
|
||||
);
|
||||
|
||||
// 初始化解码器并获取textureId
|
||||
final textureId = await VideoDecodePlugin.initDecoder(config);
|
||||
|
||||
if (textureId != null) {
|
||||
state.textureId.value = textureId;
|
||||
AppLog.log('视频解码器初始化成功:textureId=$textureId');
|
||||
|
||||
// 设置帧回调
|
||||
VideoDecodePlugin.setFrameCallback(_onFrameAvailable);
|
||||
|
||||
// 设置状态回调
|
||||
VideoDecodePlugin.setStateCallbackForTexture(
|
||||
textureId, _onDecoderStateChanged);
|
||||
|
||||
// 启动FPS监测
|
||||
startFpsMonitoring();
|
||||
} else {
|
||||
AppLog.log('视频解码器初始化失败');
|
||||
}
|
||||
} catch (e) {
|
||||
AppLog.log('初始化视频解码器错误: $e');
|
||||
// 如果初始化失败,延迟后重试
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!Get.isRegistered<TalkViewNativeDecodeLogic>()) {
|
||||
return; // 如果控制器已经被销毁,不再重试
|
||||
}
|
||||
_initVideoDecoder(); // 重试初始化
|
||||
}
|
||||
}
|
||||
|
||||
// 添加帧可用回调
|
||||
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() {
|
||||
const int sampleRate = 8000;
|
||||
FlutterPcmSound.setLogLevel(LogLevel.none);
|
||||
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
||||
// 设置 feed 阈值
|
||||
if (Platform.isAndroid) {
|
||||
FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理
|
||||
} else {
|
||||
FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理
|
||||
}
|
||||
}
|
||||
|
||||
/// 挂断
|
||||
void udpHangUpAction() async {
|
||||
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||
// 如果是通话中就挂断
|
||||
StartChartManage().startTalkHangupMessageTimer();
|
||||
} else {
|
||||
// 拒绝
|
||||
StartChartManage().startTalkRejectMessageTimer();
|
||||
}
|
||||
if (state.textureId.value != null) {
|
||||
VideoDecodePlugin.releaseDecoderForTexture(state.textureId.value!);
|
||||
}
|
||||
VideoDecodePlugin.releaseAllDecoders();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// 发起接听命令
|
||||
void initiateAnswerCommand() {
|
||||
StartChartManage().startTalkAcceptTimer();
|
||||
}
|
||||
|
||||
// 监听音视频数据流
|
||||
void _startListenTalkData() {
|
||||
// 防止重复监听
|
||||
if (_isListening) {
|
||||
AppLog.log("已经存在数据流监听,避免重复监听");
|
||||
return;
|
||||
}
|
||||
|
||||
AppLog.log("==== 启动新的数据流监听 ====");
|
||||
_isListening = true;
|
||||
|
||||
_streamSubscription = state.talkDataRepository.talkDataStream
|
||||
.listen((TalkDataModel talkDataModel) async {
|
||||
final talkData = talkDataModel.talkData;
|
||||
final talkDataH264Frame = talkDataModel.talkDataH264Frame;
|
||||
final contentType = talkData!.contentType;
|
||||
|
||||
// 判断数据类型,进行分发处理
|
||||
switch (contentType) {
|
||||
case TalkData_ContentTypeE.G711:
|
||||
if (state.audioBuffer.length >= audioBufferSize) {
|
||||
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||
}
|
||||
state.audioBuffer.add(talkData); // 添加新数据
|
||||
// 添加音频播放逻辑,与视频类似
|
||||
_playAudioFrames();
|
||||
break;
|
||||
case TalkData_ContentTypeE.H264:
|
||||
// 添加到视频帧缓冲区,而不是直接处理
|
||||
// _processH264Frame(talkData, talkDataH264Frame!);
|
||||
// 直接处理H264视频帧
|
||||
_processH264Frame(talkData, talkDataH264Frame!);
|
||||
|
||||
// 记录关键调试信息
|
||||
if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) {
|
||||
AppLog.log(
|
||||
'帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}');
|
||||
}
|
||||
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() {
|
||||
// 如果缓冲区为空或未达到目标大小,不进行播放
|
||||
// 音频缓冲区要求更小,以减少延迟
|
||||
if (state.audioBuffer.isEmpty ||
|
||||
state.audioBuffer.length < audioBufferSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找出时间戳最小的音频帧
|
||||
TalkData? oldestFrame;
|
||||
int oldestIndex = -1;
|
||||
for (int i = 0; i < state.audioBuffer.length; i++) {
|
||||
if (oldestFrame == null ||
|
||||
state.audioBuffer[i].durationMs < oldestFrame.durationMs) {
|
||||
oldestFrame = state.audioBuffer[i];
|
||||
oldestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保找到了有效帧
|
||||
if (oldestFrame != null && oldestIndex != -1) {
|
||||
if (state.isOpenVoice.value) {
|
||||
// 播放音频
|
||||
_playAudioData(oldestFrame);
|
||||
}
|
||||
state.audioBuffer.removeAt(oldestIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// 监听对讲状态
|
||||
void _startListenTalkStatus() {
|
||||
state.startChartTalkStatus.statusStream.listen((talkStatus) {
|
||||
state.talkStatus.value = talkStatus;
|
||||
switch (talkStatus) {
|
||||
case TalkStatus.rejected:
|
||||
case TalkStatus.hangingUpDuring:
|
||||
case TalkStatus.notTalkData:
|
||||
case TalkStatus.notTalkPing:
|
||||
case TalkStatus.end:
|
||||
_handleInvalidTalkStatus();
|
||||
break;
|
||||
case TalkStatus.answeredSuccessfully:
|
||||
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||
state.oneMinuteTimeTimer ??=
|
||||
Timer.periodic(const Duration(seconds: 1), (Timer t) {
|
||||
if (state.isLoading.isFalse) {
|
||||
state.oneMinuteTime.value++;
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// 其他状态的处理
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 播放音频数据
|
||||
void _playAudioData(TalkData talkData) async {
|
||||
if (state.isOpenVoice.value) {
|
||||
final list =
|
||||
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
|
||||
// // 将 PCM 数据转换为 PcmArrayInt16
|
||||
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
||||
FlutterPcmSound.feed(fromList);
|
||||
if (!state.isPlaying.value) {
|
||||
FlutterPcmSound.play();
|
||||
state.isPlaying.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止播放音频
|
||||
void _stopPlayG711Data() async {
|
||||
await FlutterPcmSound.pause();
|
||||
await FlutterPcmSound.stop();
|
||||
await FlutterPcmSound.clear();
|
||||
}
|
||||
|
||||
/// 获取权限状态
|
||||
Future<bool> getPermissionStatus() async {
|
||||
final Permission permission = Permission.microphone;
|
||||
//granted 通过,denied 被拒绝,permanentlyDenied 拒绝且不在提示
|
||||
final PermissionStatus status = await permission.status;
|
||||
if (status.isGranted) {
|
||||
return true;
|
||||
} else if (status.isDenied) {
|
||||
requestPermission(permission);
|
||||
} else if (status.isPermanentlyDenied) {
|
||||
openAppSettings();
|
||||
} else if (status.isRestricted) {
|
||||
requestPermission(permission);
|
||||
} else {}
|
||||
return false;
|
||||
}
|
||||
|
||||
///申请权限
|
||||
void requestPermission(Permission permission) async {
|
||||
final PermissionStatus status = await permission.request();
|
||||
if (status.isPermanentlyDenied) {
|
||||
openAppSettings();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> requestPermissions() async {
|
||||
// 申请存储权限
|
||||
var storageStatus = await Permission.storage.request();
|
||||
// 申请录音权限
|
||||
var microphoneStatus = await Permission.microphone.request();
|
||||
|
||||
if (storageStatus.isGranted && microphoneStatus.isGranted) {
|
||||
print("Permissions granted");
|
||||
} else {
|
||||
print("Permissions denied");
|
||||
// 如果权限被拒绝,可以提示用户或跳转到设置页面
|
||||
if (await Permission.storage.isPermanentlyDenied) {
|
||||
openAppSettings(); // 跳转到应用设置页面
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startRecording() async {}
|
||||
|
||||
Future<void> stopRecording() async {}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// 启动监听音视频数据流
|
||||
_startListenTalkData();
|
||||
// 启动监听对讲状态
|
||||
_startListenTalkStatus();
|
||||
// 在没有监听成功之前赋值一遍状态
|
||||
// *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 ***
|
||||
state.talkStatus.value = state.startChartTalkStatus.status;
|
||||
|
||||
// 初始化音频播放器
|
||||
_initFlutterPcmSound();
|
||||
|
||||
// 启动播放定时器
|
||||
// _startPlayback();
|
||||
|
||||
// 初始化录音控制器
|
||||
_initAudioRecorder();
|
||||
|
||||
requestPermissions();
|
||||
|
||||
// 初始化视频解码器
|
||||
_initVideoDecoder();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_stopPlayG711Data(); // 停止播放音频
|
||||
|
||||
state.audioBuffer.clear(); // 清空音频缓冲区
|
||||
|
||||
state.oneMinuteTimeTimer?.cancel();
|
||||
state.oneMinuteTimeTimer = null;
|
||||
|
||||
// 停止播放音频
|
||||
stopProcessingAudio();
|
||||
|
||||
state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器
|
||||
state.oneMinuteTimeTimer = null; // 取消旧定时器
|
||||
state.oneMinuteTime.value = 0;
|
||||
|
||||
// 释放视频解码器资源
|
||||
if (state.textureId.value != null) {
|
||||
VideoDecodePlugin.releaseDecoder();
|
||||
state.textureId.value = null;
|
||||
}
|
||||
|
||||
// 取消数据流监听
|
||||
_streamSubscription?.cancel();
|
||||
_isListening = false;
|
||||
|
||||
// 停止FPS监测
|
||||
stopFpsMonitoring();
|
||||
// 重置期望数据
|
||||
StartChartManage().reSetDefaultTalkExpect();
|
||||
VideoDecodePlugin.releaseAllDecoders();
|
||||
|
||||
// 取消批处理定时器
|
||||
_batchProcessTimer?.cancel();
|
||||
_batchProcessTimer = null;
|
||||
|
||||
// 清空已解码I帧集合
|
||||
_decodedIFrames.clear();
|
||||
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
/// 处理无效通话状态
|
||||
void _handleInvalidTalkStatus() {
|
||||
// 停止播放音频
|
||||
_stopPlayG711Data();
|
||||
stopProcessingAudio();
|
||||
}
|
||||
|
||||
/// 更新发送预期数据
|
||||
void updateTalkExpect() {
|
||||
TalkExpectReq talkExpectReq = TalkExpectReq();
|
||||
state.isOpenVoice.value = !state.isOpenVoice.value;
|
||||
if (!state.isOpenVoice.value) {
|
||||
talkExpectReq = TalkExpectReq(
|
||||
videoType: [VideoTypeE.IMAGE],
|
||||
audioType: [],
|
||||
);
|
||||
showToast('已静音'.tr);
|
||||
} else {
|
||||
talkExpectReq = TalkExpectReq(
|
||||
videoType: [VideoTypeE.IMAGE],
|
||||
audioType: [AudioTypeE.G711],
|
||||
);
|
||||
}
|
||||
|
||||
/// 修改发送预期数据
|
||||
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
||||
talkExpect: talkExpectReq);
|
||||
}
|
||||
|
||||
/// 截图并保存到相册
|
||||
Future<void> captureAndSavePng() async {
|
||||
try {
|
||||
if (state.globalKey.currentContext == null) {
|
||||
AppLog.log('截图失败: 未找到当前上下文');
|
||||
return;
|
||||
}
|
||||
final RenderRepaintBoundary boundary = state.globalKey.currentContext!
|
||||
.findRenderObject()! as RenderRepaintBoundary;
|
||||
final ui.Image image = await boundary.toImage();
|
||||
final ByteData? byteData =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
|
||||
if (byteData == null) {
|
||||
AppLog.log('截图失败: 图像数据为空');
|
||||
return;
|
||||
}
|
||||
final Uint8List pngBytes = byteData.buffer.asUint8List();
|
||||
|
||||
// 获取应用程序的文档目录
|
||||
final Directory directory = await getApplicationDocumentsDirectory();
|
||||
final String imagePath = '${directory.path}/screenshot.png';
|
||||
|
||||
// 将截图保存为文件
|
||||
final File imgFile = File(imagePath);
|
||||
await imgFile.writeAsBytes(pngBytes);
|
||||
|
||||
// 将截图保存到相册
|
||||
await ImageGallerySaver.saveFile(imagePath);
|
||||
|
||||
AppLog.log('截图保存路径: $imagePath');
|
||||
showToast('截图已保存到相册'.tr);
|
||||
} catch (e) {
|
||||
AppLog.log('截图失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 远程开锁
|
||||
Future<void> remoteOpenLock() async {
|
||||
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 = [];
|
||||
}
|
||||
} else {
|
||||
showToast('该锁的远程开锁功能未启用'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化音频录制器
|
||||
void _initAudioRecorder() {
|
||||
state.voiceProcessor = VoiceProcessor.instance;
|
||||
}
|
||||
|
||||
//开始录音
|
||||
Future<void> startProcessingAudio() async {
|
||||
try {
|
||||
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
||||
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
||||
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||
state.isRecordingAudio.value = isRecording!;
|
||||
state.startRecordingAudioTime.value = DateTime.now();
|
||||
|
||||
// 增加录音帧监听器和错误监听器
|
||||
state.voiceProcessor
|
||||
?.addFrameListeners(<VoiceProcessorFrameListener>[_onFrame]);
|
||||
state.voiceProcessor?.addErrorListener(_onError);
|
||||
} else {
|
||||
// state.errorMessage.value = 'Recording permission not granted';
|
||||
}
|
||||
} on PlatformException catch (ex) {
|
||||
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
||||
}
|
||||
state.isOpenVoice.value = false;
|
||||
}
|
||||
|
||||
/// 停止录音
|
||||
Future<void> stopProcessingAudio() async {
|
||||
try {
|
||||
await state.voiceProcessor?.stop();
|
||||
state.voiceProcessor?.removeFrameListener(_onFrame);
|
||||
state.udpSendDataFrameNumber = 0;
|
||||
// 记录结束时间
|
||||
state.endRecordingAudioTime.value = DateTime.now();
|
||||
|
||||
// 计算录音的持续时间
|
||||
final Duration duration = state.endRecordingAudioTime.value
|
||||
.difference(state.startRecordingAudioTime.value);
|
||||
|
||||
state.recordingAudioTime.value = duration.inSeconds;
|
||||
} on PlatformException catch (ex) {
|
||||
// state.errorMessage.value = 'Failed to stop recorder: $ex';
|
||||
} finally {
|
||||
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||
state.isRecordingAudio.value = isRecording!;
|
||||
state.isOpenVoice.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 音频帧处理
|
||||
Future<void> _onFrame(List<int> frame) async {
|
||||
// 添加最大缓冲限制
|
||||
if (_bufferedAudioFrames.length > state.frameLength * 3) {
|
||||
_bufferedAudioFrames.clear(); // 清空过多积累的数据
|
||||
return;
|
||||
}
|
||||
|
||||
// 首先应用固定增益提升基础音量
|
||||
List<int> amplifiedFrame = _applyGain(frame, 1.6);
|
||||
// 编码为G711数据
|
||||
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
// 使用相对时间戳
|
||||
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
|
||||
int getFrameLength = state.frameLength;
|
||||
if (Platform.isIOS) {
|
||||
getFrameLength = state.frameLength * 2;
|
||||
}
|
||||
|
||||
// 添加发送间隔控制
|
||||
if (_bufferedAudioFrames.length >= state.frameLength) {
|
||||
try {
|
||||
await StartChartManage().sendTalkDataMessage(
|
||||
talkData: TalkData(
|
||||
content: _bufferedAudioFrames,
|
||||
contentType: TalkData_ContentTypeE.G711,
|
||||
durationMs: ms,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_bufferedAudioFrames.clear(); // 确保清理缓冲区
|
||||
}
|
||||
} else {
|
||||
_bufferedAudioFrames.addAll(encodedData);
|
||||
}
|
||||
}
|
||||
|
||||
// 错误监听
|
||||
void _onError(VoiceProcessorException error) {
|
||||
AppLog.log(error.message!);
|
||||
}
|
||||
|
||||
// 添加音频增益处理方法
|
||||
List<int> _applyGain(List<int> pcmData, double gainFactor) {
|
||||
List<int> result = List<int>.filled(pcmData.length, 0);
|
||||
|
||||
for (int i = 0; i < pcmData.length; i++) {
|
||||
// PCM数据通常是有符号的16位整数
|
||||
int sample = pcmData[i];
|
||||
|
||||
// 应用增益
|
||||
double amplified = sample * gainFactor;
|
||||
|
||||
// 限制在有效范围内,防止溢出
|
||||
if (amplified > 32767) {
|
||||
amplified = 32767;
|
||||
} else if (amplified < -32768) {
|
||||
amplified = -32768;
|
||||
}
|
||||
|
||||
result[i] = amplified.toInt();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 启动网络状态监测
|
||||
void startFpsMonitoring() {
|
||||
// 确保只有一个计时器在运行
|
||||
stopFpsMonitoring();
|
||||
|
||||
// 初始化时间记录
|
||||
state.lastFpsUpdateTime.value = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// 创建一个计时器,每秒更新一次丢包率和性能数据
|
||||
state.fpsTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
// 更新丢包率数据
|
||||
updatePacketLossStats();
|
||||
|
||||
// 分析性能数据
|
||||
_analyzePerformance();
|
||||
});
|
||||
}
|
||||
|
||||
// 停止网络状态监测
|
||||
void stopFpsMonitoring() {
|
||||
state.fpsTimer?.cancel();
|
||||
state.fpsTimer = null;
|
||||
}
|
||||
|
||||
// 日志记录方法
|
||||
void logMessage(String message) {
|
||||
AppLog.log(message);
|
||||
}
|
||||
|
||||
// 更新丢包率统计数据
|
||||
void updatePacketLossStats() async {
|
||||
try {} catch (e) {
|
||||
logMessage('获取丢包率数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 添加性能分析方法
|
||||
void _analyzePerformance() {
|
||||
final int now = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// 如果是首次调用,初始化数据
|
||||
if (state.lastPerformanceCheck == 0) {
|
||||
state.lastPerformanceCheck = now;
|
||||
state.lastFrameCount = state.renderedFrameCount.value;
|
||||
return;
|
||||
}
|
||||
|
||||
// 每秒分析一次性能
|
||||
if (now - state.lastPerformanceCheck >= 1000) {
|
||||
// 计算过去一秒的实际帧率
|
||||
final int frameRendered =
|
||||
state.renderedFrameCount.value - state.lastFrameCount;
|
||||
final double actualFPS =
|
||||
frameRendered * 1000 / (now - state.lastPerformanceCheck);
|
||||
|
||||
// 计算丢帧率
|
||||
final double dropRate = state.droppedFrames.value /
|
||||
(state.totalFrames.value > 0 ? state.totalFrames.value : 1) *
|
||||
100;
|
||||
|
||||
// 计算当前解码器积压帧数
|
||||
final int pendingFrames =
|
||||
state.totalFrames.value - state.renderedFrameCount.value;
|
||||
|
||||
// 计算跟踪Map中的帧数(正在处理中的帧)
|
||||
final int processingFrames = state.frameTracker.length;
|
||||
|
||||
// 分析渲染瓶颈
|
||||
String performanceStatus = "正常";
|
||||
if (actualFPS < 15 && dropRate > 10) {
|
||||
performanceStatus = "严重渲染瓶颈";
|
||||
} else if (actualFPS < 20 && dropRate > 5) {
|
||||
performanceStatus = "轻微渲染瓶颈";
|
||||
}
|
||||
|
||||
// 输出综合性能分析
|
||||
AppLog.log("性能分析: 实际帧率=${actualFPS.toStringAsFixed(1)}fps, " +
|
||||
"丢帧率=${dropRate.toStringAsFixed(1)}%, " +
|
||||
"待处理帧数=$pendingFrames, " +
|
||||
"处理中帧数=$processingFrames, " +
|
||||
"状态=$performanceStatus");
|
||||
|
||||
// 重置统计数据
|
||||
state.lastPerformanceCheck = now;
|
||||
state.lastFrameCount = state.renderedFrameCount.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,557 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:star_lock/flavors.dart';
|
||||
import 'package:star_lock/talk/call/callTalk.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart';
|
||||
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_logic.dart';
|
||||
import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart';
|
||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart';
|
||||
import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart';
|
||||
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
import '../../../../app_settings/app_colors.dart';
|
||||
import '../../../../tools/showTFView.dart';
|
||||
|
||||
class TalkViewNativeDecodePage extends StatefulWidget {
|
||||
const TalkViewNativeDecodePage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TalkViewNativeDecodePage> createState() =>
|
||||
_TalkViewNativeDecodePageState();
|
||||
}
|
||||
|
||||
class _TalkViewNativeDecodePageState extends State<TalkViewNativeDecodePage>
|
||||
with TickerProviderStateMixin {
|
||||
final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic());
|
||||
final TalkViewNativeDecodeState state =
|
||||
Get.find<TalkViewNativeDecodeLogic>().state;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
state.animationController = AnimationController(
|
||||
vsync: this, // 确保使用的TickerProvider是当前Widget
|
||||
duration: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
state.animationController.repeat();
|
||||
//动画开始、结束、向前移动或向后移动时会调用StatusListener
|
||||
state.animationController.addStatusListener((AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
state.animationController.reset();
|
||||
state.animationController.forward();
|
||||
} else if (status == AnimationStatus.dismissed) {
|
||||
state.animationController.reset();
|
||||
state.animationController.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
// 返回 false 表示禁止退出
|
||||
return false;
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 1.sw,
|
||||
height: 1.sh,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
Obx(
|
||||
() {
|
||||
final double screenWidth = MediaQuery.of(context).size.width;
|
||||
final double screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
final double logicalWidth = MediaQuery.of(context).size.width;
|
||||
final double logicalHeight = MediaQuery.of(context).size.height;
|
||||
final double devicePixelRatio =
|
||||
MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
// 计算物理像素值
|
||||
final double physicalWidth = logicalWidth * devicePixelRatio;
|
||||
final double physicalHeight = logicalHeight * devicePixelRatio;
|
||||
|
||||
// 旋转后的图片尺寸
|
||||
const int rotatedImageWidth = 480; // 原始高度
|
||||
const int rotatedImageHeight = 864; // 原始宽度
|
||||
|
||||
// 计算缩放比例
|
||||
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,
|
||||
)
|
||||
: PopScope(
|
||||
canPop: false,
|
||||
child: RepaintBoundary(
|
||||
key: state.globalKey,
|
||||
child: SizedBox.expand(
|
||||
child: RotatedBox(
|
||||
// 解码器不支持硬件旋转,使用RotatedBox
|
||||
quarterTurns: -1,
|
||||
child: Texture(
|
||||
textureId: state.textureId.value!,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Obx(() => state.isLoading.isTrue
|
||||
? Positioned(
|
||||
bottom: 310.h,
|
||||
child: Text(
|
||||
'正在创建安全连接...'.tr,
|
||||
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
||||
))
|
||||
: 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
|
||||
? Positioned(
|
||||
top: ScreenUtil().statusBarHeight + 75.h,
|
||||
width: 1.sw,
|
||||
child: Obx(
|
||||
() {
|
||||
final String sec = (state.oneMinuteTime.value % 60)
|
||||
.toString()
|
||||
.padLeft(2, '0');
|
||||
final String min = (state.oneMinuteTime.value ~/ 60)
|
||||
.toString()
|
||||
.padLeft(2, '0');
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'$min:$sec',
|
||||
style: TextStyle(
|
||||
fontSize: 26.sp, color: Colors.white),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
: Container()),
|
||||
Positioned(
|
||||
bottom: 10.w,
|
||||
child: Container(
|
||||
width: 1.sw - 30.w * 2,
|
||||
// height: 300.h,
|
||||
margin: EdgeInsets.all(30.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20.h)),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(height: 20.h),
|
||||
bottomTopBtnWidget(),
|
||||
SizedBox(height: 20.h),
|
||||
bottomBottomBtnWidget(),
|
||||
SizedBox(height: 20.h),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(() => state.isLoading.isTrue
|
||||
? buildRotationTransition()
|
||||
: Container()),
|
||||
Obx(() => state.isLongPressing.value
|
||||
? Positioned(
|
||||
top: 80.h,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.mic, color: Colors.white, size: 24.w),
|
||||
SizedBox(width: 10.w),
|
||||
Text(
|
||||
'正在说话...'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 20.sp, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget bottomTopBtnWidget() {
|
||||
return Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||
// 打开关闭声音
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||
// 打开关闭声音
|
||||
logic.updateTalkExpect();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
padding: EdgeInsets.all(5.w),
|
||||
child: Obx(() => Image(
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
image: state.isOpenVoice.value
|
||||
? const AssetImage(
|
||||
'images/main/icon_lockDetail_monitoringOpenVoice.png')
|
||||
: const AssetImage(
|
||||
'images/main/icon_lockDetail_monitoringCloseVoice.png'))),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 50.w),
|
||||
// 截图
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||
await logic.captureAndSavePng();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
padding: EdgeInsets.all(5.w),
|
||||
child: Image(
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
image: const AssetImage(
|
||||
'images/main/icon_lockDetail_monitoringScreenshot.png')),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 50.w),
|
||||
// 录制
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
logic.showToast('功能暂未开放'.tr);
|
||||
// if (
|
||||
// state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||
// if (state.isRecordingScreen.value) {
|
||||
// await logic.stopRecording();
|
||||
// } else {
|
||||
// await logic.startRecording();
|
||||
// }
|
||||
// }
|
||||
},
|
||||
child: Container(
|
||||
width: 50.w,
|
||||
height: 50.w,
|
||||
padding: EdgeInsets.all(5.w),
|
||||
child: Image(
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
fit: BoxFit.fill,
|
||||
image: const AssetImage(
|
||||
'images/main/icon_lockDetail_monitoringScreenRecording.png'),
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget bottomBottomBtnWidget() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
// 接听
|
||||
Obx(
|
||||
() => bottomBtnItemWidget(
|
||||
getAnswerBtnImg(),
|
||||
getAnswerBtnName(),
|
||||
Colors.white,
|
||||
longPress: () async {
|
||||
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
|
||||
// 启动录音
|
||||
logic.startProcessingAudio();
|
||||
state.isLongPressing.value = true;
|
||||
}
|
||||
},
|
||||
longPressUp: () async {
|
||||
// 停止录音
|
||||
logic.stopProcessingAudio();
|
||||
state.isLongPressing.value = false;
|
||||
},
|
||||
onClick: () async {
|
||||
if (state.talkStatus.value ==
|
||||
TalkStatus.passiveCallWaitingAnswer) {
|
||||
// 接听
|
||||
logic.initiateAnswerCommand();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomBtnItemWidget(
|
||||
'images/main/icon_lockDetail_hangUp.png', '挂断'.tr, Colors.red,
|
||||
onClick: () {
|
||||
// 挂断
|
||||
logic.udpHangUpAction();
|
||||
}),
|
||||
bottomBtnItemWidget(
|
||||
'images/main/icon_lockDetail_monitoringUnlock.png',
|
||||
'开锁'.tr,
|
||||
AppColors.mainColor,
|
||||
onClick: () {
|
||||
// if (state.talkStatus.value == TalkStatus.answeredSuccessfully &&
|
||||
// state.listData.value.length > 0) {
|
||||
// logic.udpOpenDoorAction();
|
||||
// }
|
||||
// if (UDPManage().remoteUnlock == 1) {
|
||||
// logic.udpOpenDoorAction();
|
||||
// showDeletPasswordAlertDialog(context);
|
||||
// } else {
|
||||
// logic.showToast('请在锁设置中开启远程开锁'.tr);
|
||||
// }
|
||||
logic.remoteOpenLock();
|
||||
},
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
String getAnswerBtnImg() {
|
||||
switch (state.talkStatus.value) {
|
||||
case TalkStatus.passiveCallWaitingAnswer:
|
||||
return 'images/main/icon_lockDetail_monitoringAnswerCalls.png';
|
||||
case TalkStatus.answeredSuccessfully:
|
||||
case TalkStatus.proactivelyCallWaitingAnswer:
|
||||
return 'images/main/icon_lockDetail_monitoringUnTalkback.png';
|
||||
default:
|
||||
return 'images/main/icon_lockDetail_monitoringAnswerCalls.png';
|
||||
}
|
||||
}
|
||||
|
||||
String getAnswerBtnName() {
|
||||
switch (state.talkStatus.value) {
|
||||
case TalkStatus.passiveCallWaitingAnswer:
|
||||
return '接听'.tr;
|
||||
case TalkStatus.proactivelyCallWaitingAnswer:
|
||||
case TalkStatus.answeredSuccessfully:
|
||||
return '长按说话'.tr;
|
||||
default:
|
||||
return '接听'.tr;
|
||||
}
|
||||
}
|
||||
|
||||
Widget bottomBtnItemWidget(
|
||||
String iconUrl,
|
||||
String name,
|
||||
Color backgroundColor, {
|
||||
required Function() onClick,
|
||||
Function()? longPress,
|
||||
Function()? longPressUp,
|
||||
}) {
|
||||
double wh = 80.w;
|
||||
return GestureDetector(
|
||||
onTap: onClick,
|
||||
onLongPress: longPress,
|
||||
onLongPressUp: longPressUp,
|
||||
child: SizedBox(
|
||||
height: 160.w,
|
||||
width: 140.w,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: wh,
|
||||
height: wh,
|
||||
constraints: BoxConstraints(
|
||||
minWidth: wh,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2),
|
||||
),
|
||||
padding: EdgeInsets.all(20.w),
|
||||
child: Image.asset(iconUrl, fit: BoxFit.fitWidth),
|
||||
),
|
||||
SizedBox(height: 20.w),
|
||||
Text(
|
||||
name,
|
||||
style: TextStyle(fontSize: 20.sp, color: Colors.white),
|
||||
textAlign: TextAlign.center, // 当文本超出指定行数时,使用省略号表示
|
||||
maxLines: 2, // 设置最大行数为1
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 根据丢包率返回对应的颜色
|
||||
Color _getPacketLossColor(double lossRate) {
|
||||
if (lossRate < 1.0) {
|
||||
return Colors.green; // 丢包率低于1%显示绿色
|
||||
} else if (lossRate < 5.0) {
|
||||
return Colors.yellow; // 丢包率1%-5%显示黄色
|
||||
} else if (lossRate < 10.0) {
|
||||
return Colors.orange; // 丢包率5%-10%显示橙色
|
||||
} else {
|
||||
return Colors.red; // 丢包率高于10%显示红色
|
||||
}
|
||||
}
|
||||
|
||||
//旋转动画
|
||||
Widget buildRotationTransition() {
|
||||
return Positioned(
|
||||
left: ScreenUtil().screenWidth / 2 - 220.w / 2,
|
||||
top: ScreenUtil().screenHeight / 2 - 220.w / 2 - 150.h,
|
||||
child: GestureDetector(
|
||||
child: RotationTransition(
|
||||
//设置动画的旋转中心
|
||||
alignment: Alignment.center,
|
||||
//动画控制器
|
||||
turns: state.animationController,
|
||||
//将要执行动画的子view
|
||||
child: AnimatedOpacity(
|
||||
opacity: 0.5,
|
||||
duration: const Duration(seconds: 2),
|
||||
child: Image.asset(
|
||||
'images/main/realTime_connecting.png',
|
||||
width: 220.w,
|
||||
height: 220.w,
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
state.animationController.forward();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
state.animationController.dispose();
|
||||
CallTalk().finishAVData();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_rx/get_rx.dart';
|
||||
import 'package:get/get_rx/src/rx_types/rx_types.dart';
|
||||
import 'package:get/state_manager.dart';
|
||||
import 'package:network_info_plus/network_info_plus.dart';
|
||||
import 'package:star_lock/talk/starChart/constant/talk_status.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart';
|
||||
import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart';
|
||||
import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart';
|
||||
import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart';
|
||||
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
import '../../../../tools/storage.dart';
|
||||
|
||||
enum NetworkStatus {
|
||||
normal, // 0
|
||||
lagging, // 1
|
||||
delayed, // 2
|
||||
packetLoss // 3
|
||||
}
|
||||
|
||||
class TalkViewNativeDecodeState {
|
||||
// 视频源最大帧率限制
|
||||
static const int maxSourceFps = 25; // 视频源最高支持25fps
|
||||
|
||||
int udpSendDataFrameNumber = 0; // 帧序号
|
||||
// var isSenderAudioData = false.obs;// 是否要发送音频数据
|
||||
|
||||
Future<String?> userMobileIP = NetworkInfo().getWifiIP();
|
||||
Future<String?> userUid = Storage.getUid();
|
||||
|
||||
RxInt udpStatus =
|
||||
0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话
|
||||
TextEditingController passwordTF = TextEditingController();
|
||||
|
||||
RxList<int> listAudioData = <int>[].obs; //得到的音频流字节数据
|
||||
GlobalKey globalKey = GlobalKey();
|
||||
|
||||
Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面
|
||||
RxInt oneMinuteTime = 0.obs; // 定时器秒数
|
||||
|
||||
// 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次
|
||||
late Timer answerTimer;
|
||||
late Timer hangUpTimer;
|
||||
late Timer openDoorTimer;
|
||||
Timer? fpsTimer;
|
||||
late AnimationController animationController;
|
||||
|
||||
RxInt elapsedSeconds = 0.obs;
|
||||
|
||||
// 星图对讲相关状态
|
||||
List<TalkData> audioBuffer = <TalkData>[].obs;
|
||||
|
||||
RxBool isLoading = true.obs; // 是否在加载
|
||||
RxBool isPlaying = false.obs; // 是否开始播放
|
||||
Rx<TalkStatus> talkStatus = TalkStatus.none.obs; //星图对讲状态
|
||||
// 获取 startChartTalkStatus 的唯一实例
|
||||
final StartChartTalkStatus startChartTalkStatus =
|
||||
StartChartTalkStatus.instance;
|
||||
|
||||
// 通话数据流的单例流数据处理类
|
||||
final TalkDataRepository talkDataRepository = TalkDataRepository.instance;
|
||||
|
||||
RxBool isOpenVoice = true.obs; // 是否打开声音
|
||||
RxBool isRecordingScreen = false.obs; // 是否录屏中
|
||||
RxBool isRecordingAudio = false.obs; // 是否录音中
|
||||
Rx<DateTime> startRecordingAudioTime = DateTime.now().obs; // 开始录音时间
|
||||
Rx<DateTime> endRecordingAudioTime = DateTime.now().obs; // 结束录音时间
|
||||
RxInt recordingAudioTime = 0.obs; // 录音时间持续时间
|
||||
late VoiceProcessor? voiceProcessor; // 音频处理器、录音
|
||||
final int frameLength = 320; //录音视频帧长度为640
|
||||
final int sampleRate = 8000; //录音频采样率为8000
|
||||
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
||||
// 视频解码器纹理ID
|
||||
Rx<int?> textureId = Rx<int?>(null);
|
||||
// FPS监测相关变量
|
||||
|
||||
RxInt lastFpsUpdateTime = 0.obs; // 上次FPS更新时间
|
||||
RxBool showFps = true.obs; // 是否显示FPS
|
||||
// 丢包率统计相关变量
|
||||
RxDouble decoderFps = 0.0.obs; // 消息丢失率
|
||||
RxDouble messageLossRate = 0.0.obs; // 消息丢失率
|
||||
RxDouble packetLossRate = 0.0.obs; // 分包丢失率
|
||||
RxInt lastPacketStatsUpdateTime = 0.obs; // 上次更新丢包统计的时间
|
||||
|
||||
// 解码器详细统计信息
|
||||
RxInt renderedFrameCount = 0.obs; // 已渲染帧数
|
||||
RxInt totalFrames = 0.obs; // 总帧数
|
||||
RxInt droppedFrames = 0.obs; // 丢弃帧数
|
||||
RxBool hasSentIDR = false.obs; // 是否已发送IDR帧
|
||||
RxBool hasSentSPS = false.obs; // 是否已发送SPS
|
||||
RxBool hasSentPPS = false.obs; // 是否已发送PPS
|
||||
RxInt keyFrameInterval = 0.obs; // 关键帧间隔时间(ms)
|
||||
RxInt decodingJitterMs = 0.obs; // 解码抖动时间(ms)
|
||||
|
||||
// 性能分析变量
|
||||
int lastPerformanceCheck = 0;
|
||||
int lastFrameCount = 0;
|
||||
|
||||
// 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq
|
||||
Map<String, Map<String, dynamic>> frameTracker = {};
|
||||
}
|
||||
@ -45,12 +45,12 @@ class TalkViewLogic extends BaseGetXController {
|
||||
final int minAudioBufferSize = 1; // 音频最小缓冲1帧
|
||||
final int maxAudioBufferSize = 3; // 音频最大缓冲3帧
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
|
||||
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||
// 添加开始时间记录
|
||||
int _startTime = 0; // 开始播放时间戳
|
||||
int _startAudioTime = 0; // 开始播放时间戳
|
||||
bool _isFirstFrame = true; // 是否是第一帧
|
||||
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||
|
||||
|
||||
// 定义音频帧缓冲和发送函数
|
||||
final List<int> _bufferedAudioFrames = <int>[];
|
||||
@ -106,6 +106,24 @@ class TalkViewLogic extends BaseGetXController {
|
||||
// 判断数据类型,进行分发处理
|
||||
switch (contentType) {
|
||||
case TalkData_ContentTypeE.G711:
|
||||
// // 第一帧到达时记录开始时间
|
||||
if (_isFirstAudioFrame) {
|
||||
_startAudioTime = currentTime;
|
||||
_isFirstAudioFrame = false;
|
||||
}
|
||||
|
||||
// 计算音频延迟
|
||||
final expectedTime = _startAudioTime + talkData.durationMs;
|
||||
final audioDelay = currentTime - expectedTime;
|
||||
|
||||
// 如果延迟太大,清空缓冲区并直接播放
|
||||
if (audioDelay > 500) {
|
||||
state.audioBuffer.clear();
|
||||
if (state.isOpenVoice.value) {
|
||||
_playAudioFrames();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (state.audioBuffer.length >= audioBufferSize) {
|
||||
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||
}
|
||||
@ -118,7 +136,7 @@ class TalkViewLogic extends BaseGetXController {
|
||||
if (_isFirstFrame) {
|
||||
_startTime = currentTime;
|
||||
_isFirstFrame = false;
|
||||
AppLog.log('第一帧帧的时间戳:${talkData.durationMs}');
|
||||
// AppLog.log('第一帧帧的时间戳:${talkData.durationMs}');
|
||||
}
|
||||
// AppLog.log('其他帧的时间戳:${talkData.durationMs}');
|
||||
// 计算帧间间隔
|
||||
@ -366,19 +384,6 @@ class TalkViewLogic extends BaseGetXController {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口
|
||||
Future<void> _getLockNetToken() async {
|
||||
final LockNetTokenEntity entity = await ApiRepository.to.getLockNetToken(
|
||||
lockId: lockDetailState.keyInfos.value.lockId.toString());
|
||||
if (entity.errorCode!.codeIsSuccessful) {
|
||||
lockDetailState.lockNetToken = entity.data!.token!.toString();
|
||||
AppLog.log('从服务器获取联网token:${lockDetailState.lockNetToken}');
|
||||
} else {
|
||||
BuglyTool.uploadException(
|
||||
message: '点击了需要联网开锁', detail: '点击了需要联网开锁 获取连网token失败', upload: true);
|
||||
showToast('网络访问失败,请检查网络是否正常'.tr, something: () {});
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取权限状态
|
||||
Future<bool> getPermissionStatus() async {
|
||||
|
||||
@ -48,7 +48,7 @@ class H264WebViewLogic extends BaseGetXController {
|
||||
Timer? _mockDataTimer;
|
||||
int _startAudioTime = 0; // 开始播放时间戳
|
||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
||||
|
||||
bool _isFirstAudioFrame = true; // 是否是第一帧
|
||||
// 定义音频帧缓冲和发送函数
|
||||
final List<int> _bufferedAudioFrames = <int>[];
|
||||
final Queue<List<int>> _frameBuffer = Queue<List<int>>();
|
||||
@ -131,6 +131,24 @@ class H264WebViewLogic extends BaseGetXController {
|
||||
// 判断数据类型,进行分发处理
|
||||
switch (contentType) {
|
||||
case TalkData_ContentTypeE.G711:
|
||||
// // 第一帧到达时记录开始时间
|
||||
if (_isFirstAudioFrame) {
|
||||
_startAudioTime = currentTime;
|
||||
_isFirstAudioFrame = false;
|
||||
}
|
||||
|
||||
// 计算音频延迟
|
||||
final expectedTime = _startAudioTime + talkData.durationMs;
|
||||
final audioDelay = currentTime - expectedTime;
|
||||
|
||||
// 如果延迟太大,清空缓冲区并直接播放
|
||||
if (audioDelay > 500) {
|
||||
state.audioBuffer.clear();
|
||||
if (state.isOpenVoice.value) {
|
||||
_playAudioFrames();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (state.audioBuffer.length >= audioBufferSize) {
|
||||
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||
}
|
||||
|
||||
@ -127,7 +127,8 @@ dependencies:
|
||||
sdk: flutter
|
||||
aliyun_face_plugin:
|
||||
path: aliyun_face_plugin
|
||||
|
||||
video_decode_plugin:
|
||||
path: ../video_decode_plugin
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user