diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c287c9f7..e68a2798 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,10 +16,10 @@ variables: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "develop_sky" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ - - if: $CI_COMMIT_BRANCH == "canary_release" + - if: $CI_COMMIT_BRANCH == "canary_release_sky" - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ .notify_rule: @@ -27,8 +27,8 @@ variables: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "develop_sky" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ .generate_tag_rule: @@ -36,16 +36,16 @@ variables: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "master" + - if: $CI_COMMIT_BRANCH == "master_sky" .generate_next_version_rule: tags: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "release" - - if: $CI_COMMIT_BRANCH == "canary_release" + - if: $CI_COMMIT_BRANCH == "develop_sky" + - if: $CI_COMMIT_BRANCH == "release_sky" + - if: $CI_COMMIT_BRANCH == "canary_release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ .print_env: diff --git a/README.md b/README.md index 45dfc905..d6ee8c02 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP - +测试ci 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 diff --git a/android/build.sh b/android/build.sh index 4b598959..40a15413 100755 --- a/android/build.sh +++ b/android/build.sh @@ -8,8 +8,9 @@ export ENV_BUILD_WORKSPACE=${CI_PROJECT_DIR} echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/android echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' -if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then +# 只支持 v1.2.3_sky 这种tag格式 +regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$' +if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} bundle exec fastlane release_apk flavor:xhj --verbose @@ -20,11 +21,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then bundle exec fastlane release_apk flavor:sky --verbose bundle exec fastlane release_bundle flavor:xhj_bundle --verbose bundle exec fastlane release_bundle flavor:sky --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then +elif [[ "${ENV_BUILD_BRANCH}" == "develop_sky" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:dev --verbose bundle exec fastlane beta flavor:sky env:dev --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then +elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" || "${ENV_BUILD_BRANCH}" == "feat_devops_sky" ]] ; then echo "===build pre===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:pre --verbose bundle exec fastlane beta flavor:sky env:pre --verbose diff --git a/assets/html/h264.html b/assets/html/h264.html index 0995b612..60ec7039 100644 --- a/assets/html/h264.html +++ b/assets/html/h264.html @@ -29,8 +29,8 @@ #player { object-fit: cover; - height: 56vh; - transform: rotate(-90deg); + width: 100vw; + height: 100vh; } diff --git a/ios/build.sh b/ios/build.sh index a8a977bf..2c46b1f1 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -9,8 +9,9 @@ echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/ios #bundle exec pod install echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' -if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then +# 只支持 v1.2.3_sky 这种tag格式 +regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$' +if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} bundle exec fastlane release_ipa flavor:xhj --verbose @@ -19,11 +20,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" bundle exec fastlane release_ipa flavor:xhj --verbose bundle exec fastlane release_ipa flavor:sky --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then +elif [[ "${ENV_BUILD_BRANCH}" == "develop_sky" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:Dev --verbose bundle exec fastlane beta flavor:sky env:Dev --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then +elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" || "${ENV_BUILD_BRANCH}" == "feat_devops_sky" ]] ; then echo "===build pre===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:Pre --verbose bundle exec fastlane beta flavor:sky env:Pre --verbose diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 4f8650e0..895ba92e 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -143,7 +143,8 @@ platform :ios do sh("flutter","pub","get") end Dir.chdir ".." do - sh("bundle", "exec" ,"pod", "install") + #sh("bundle", "exec" ,"pod", "install") + sh("pod", "install") end Dir.chdir "../.." do sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") diff --git a/lib/appRouters.dart b/lib/appRouters.dart index ab6c17a9..0a06445a 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -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'; @@ -202,6 +203,7 @@ import 'mine/valueAddedServices/valueAddedServicesRealName/value_added_services_ import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesAddSMSTemplate/newSMSTemplate_page.dart'; import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesListSMSTemplate/customSMSTemplateList_page.dart'; import 'starLockApplication/starLockApplication.dart'; +import 'talk/starChart/views/imageTransmission/image_transmission_page.dart'; import 'tools/seletKeyCyclicDate/seletKeyCyclicDate_page.dart'; abstract class Routers { @@ -514,6 +516,8 @@ abstract class Routers { static const String starChartPage = '/starChartPage'; //星图 static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面 static const String h264WebView = '/h264WebView'; //星图对讲页面 + static const String imageTransmissionView = + '/imageTransmissionView'; //星图对讲页面(图传) } abstract class AppRouters { @@ -1184,6 +1188,13 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), - GetPage(name: Routers.h264WebView, page: () => H264WebView()), + GetPage( + name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), + // 插件播放页面 + GetPage( + name: Routers.imageTransmissionView, + page: () => ImageTransmissionPage()), + // 插件播放页面 + // GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 ]; } diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 6f258b2c..a955171b 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -5,6 +5,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/commonDataManage.dart'; @@ -186,10 +187,12 @@ class BlueManage { continue; } - final isMatch = _isMatch(scanResult - .advertisementData.serviceUuids - .map((e) => e.uuid) - .toList()); + final isMatch = _isMatch( + scanResult.advertisementData.serviceUuids + .map((e) => e.uuid) + .toList(), + isSingle: true, + ); if (isMatch && (scanResult.rssi >= -100)) { // 查询id相同的元素 @@ -272,6 +275,7 @@ class BlueManage { .map((e) => e.uuid) .toList(), deviceType: deviceType, + isSingle: false, ); // 判断名字为空的直接剔除 if (isMatch && (scanResult.rssi >= -100)) { @@ -315,25 +319,48 @@ class BlueManage { } /// 判断是否包含指定的uuid - bool _isMatch(List serviceUuids, {DeviceType deviceType = DeviceType.blue}) { - final List prefixes = getDeviceType(deviceType).map((e) => e.toLowerCase()).toList(); + bool _isMatch(List serviceUuids, + {DeviceType deviceType = DeviceType.blue, required bool isSingle}) { + final List prefixes = + getDeviceType(deviceType).map((e) => e.toLowerCase()).toList(); for (String uuid in serviceUuids) { - final String cleanUuid = uuid.replaceAll('-', '').toLowerCase(); + final String cleanUuid = uuid.toLowerCase(); if (cleanUuid.length == 8) { - // 8位,判断前两位 + // 8位,判断第4、5位 + String pairStatus = cleanUuid.substring(4, 6); // 第4、5位(索引3和4) for (final prefix in prefixes) { if (cleanUuid.startsWith(prefix)) { - return true; + if (isSingle) { + return true; // isSingle为true,前缀匹配即返回true + } else { + // 00=未配对,01=已配对 + if (pairStatus == '00') { + return true; // 未配对才返回true + } + // 已配对(01)不返回true,继续判断下一个uuid + } } } - } else if (cleanUuid.length == 32) { + } else { // 128位,判断前8位的第3、第4位 - final String first8 = cleanUuid.substring(0, 8); - if (first8.length >= 4) { - final String thirdAndFourth = first8.substring(2, 4); // 索引2和3 + if (cleanUuid.length >= 32) { + final String thirdAndFourth = cleanUuid.substring(2, 4); // 索引2和3 for (final prefix in prefixes) { if (thirdAndFourth == prefix) { - return true; + if (isSingle) { + return true; // isSingle为true,前缀匹配即返回true + } else { + // 判断配对状态(带横杠UUID的第31、32位,从1开始计数) + if (cleanUuid.length >= 32) { + String pairStatus = + cleanUuid.substring(30, 32); // 第31、32位(从1开始计数) + // 00=未配对,01=已配对 + if (pairStatus == '00') { + return true; // 未配对才返回true + } + // 已配对(01)不返回true,继续判断下一个uuid + } + } } } } @@ -561,7 +588,10 @@ class BlueManage { }); } else { connectStateCallBack(BluetoothConnectionState.disconnected); - EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + if (!F.isSKY) { + EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + } + scanDevices.clear(); BuglyTool.uploadException( @@ -592,7 +622,9 @@ class BlueManage { }); } else { connectStateCallBack(BluetoothConnectionState.disconnected); - EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + if (!F.isSKY) { + EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + } scanDevices.clear(); BuglyTool.uploadException( @@ -785,7 +817,7 @@ class BlueManage { } } - // 写入 + /// 写入蓝牙特征值,并等待响应 Future writeCharacteristicWithResponse(List value) async { final List services = await bluetoothConnectDevice!.discoverServices(); @@ -795,30 +827,70 @@ class BlueManage { in service.characteristics) { if (characteristic.characteristicUuid == _characteristicIdWrite) { try { + // 添加重试机制 + int retryCount = 0; + const int maxRetries = 3; + const int retryDelayMs = 500; + final List valueList = value; final List subData = splitList(valueList, _mtuSize!); - // AppLog.log('writeCharacteristicWithResponse _mtuSize:$_mtuSize 得到的分割数据:$subData'); + for (int i = 0; i < subData.length; i++) { - if (characteristic.properties.writeWithoutResponse) { - // 使用WRITE_NO_RESPONSE属性写入值 - await characteristic.write(subData[i], withoutResponse: true); - } else if (characteristic.properties.write) { - // 使用WRITE属性写入值 - await characteristic.write(subData[i]); - } else { - // 特性不支持写入 + // 对每个数据包都应用重试逻辑 + bool packetSent = false; + retryCount = 0; + + while (!packetSent && retryCount < maxRetries) { + try { + if (characteristic.properties.writeWithoutResponse) { + await characteristic.write(subData[i], + withoutResponse: true); + } else if (characteristic.properties.write) { + await characteristic.write(subData[i]); + } else { + // 特性不支持写入 + throw Exception( + 'This characteristic does not support writing.'); + } + + // 如果到这里没有异常,则包发送成功 + packetSent = true; + } catch (e) { + if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && + retryCount < maxRetries - 1) { + // GATT错误133,尝试重试 + retryCount++; + AppLog.log( + '蓝牙写入失败(GATT 133),数据包 ${i + 1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); + await Future.delayed( + Duration(milliseconds: retryDelayMs)); + continue; + } else { + // 其他错误或已达到最大重试次数,抛出异常 + AppLog.log('APP写入失败: $e'); + throw e; + } + } + } + + if (!packetSent) { throw Exception( - 'This characteristic does not support writing.'); + '蓝牙写入失败,数据包 ${i + 1}/${subData.length} 已达到最大重试次数'); } } + + return; // 所有数据包都发送成功 } on Exception catch (e, s) { - AppLog.log('APP写入失败: $e $s'); + AppLog.log('APP写入失败: $e $s'); rethrow; } } } } } + + // 如果找不到合适的特性用于写入 + throw Exception('未找到适合写入的蓝牙特性'); } // 停止扫描蓝牙设备 diff --git a/lib/blue/reciver_data.dart b/lib/blue/reciver_data.dart index 2fc7fe54..0b0f4c61 100755 --- a/lib/blue/reciver_data.dart +++ b/lib/blue/reciver_data.dart @@ -61,16 +61,16 @@ class CommandReciverManager { final int dataSize = data.length; // 验证CRC校验 - if (dataSize >= 2) { - final int calculatedCrc = - _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2); - final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1]; - - if (calculatedCrc != receivedCrc) { - AppLog.log('CRC校验失败'); - return; - } - } + // if (dataSize >= 2) { + // final int calculatedCrc = + // _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2); + // final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1]; + // + // if (calculatedCrc != receivedCrc) { + // throw Exception('CRC校验失败'); + // return; + // } + // } // 当小于包头加起来13个字节 if (dataSize < 13) { return; diff --git a/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart b/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart index bc2fd663..8d8140e6 100755 --- a/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart +++ b/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/card/cardDetail/cardDetail_state.dart'; import '../../../../appRouters.dart'; @@ -179,13 +180,22 @@ class _CardDetailPageState extends State with RouteAware { isHaveRightWidget: true, rightWidget: SizedBox( width: 60.w, height: 50.h, child: _isStressFingerprint()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/card/cardList/cardList_logic.dart b/lib/main/lockDetail/card/cardList/cardList_logic.dart index 2fc2e376..c007eca1 100755 --- a/lib/main/lockDetail/card/cardList/cardList_logic.dart +++ b/lib/main/lockDetail/card/cardList/cardList_logic.dart @@ -249,8 +249,9 @@ class CardListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getICCardListData(isRefresh: true); } - await getICCardListData(isRefresh: true); + } @override diff --git a/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart b/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart index 766e81ab..e4af7a36 100755 --- a/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart +++ b/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_logic.dart'; import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_state.dart'; @@ -174,13 +175,22 @@ class _FaceDetailPageState extends State with RouteAware { // isHaveRightWidget: true, // isHaveLine: true, // rightWidget: SizedBox(width: 60.w, height: 50.h, child: _isStressFace()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/face/faceList/faceList_logic.dart b/lib/main/lockDetail/face/faceList/faceList_logic.dart index a1499556..2f6e8a18 100755 --- a/lib/main/lockDetail/face/faceList/faceList_logic.dart +++ b/lib/main/lockDetail/face/faceList/faceList_logic.dart @@ -443,8 +443,9 @@ class FaceListLogic extends BaseGetXController { // senderCheckingCardStatus(); // senderCheckingUserInfoCount(); + await getFaceListData(isRefresh: true); } - getFaceListData(isRefresh: true); + } @override diff --git a/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart b/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart index c5ff8d1c..ebf94158 100755 --- a/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart +++ b/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; import 'package:star_lock/apm/apm_helper.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/entity/LoginEntity.dart'; import 'package:star_lock/main/lockDetail/fingerprint/addFingerprint/addFingerprint_entity.dart'; import 'package:star_lock/tools/dateTool.dart'; @@ -141,7 +142,7 @@ class AddFingerprintLogic extends BaseGetXController { break; case 0xFE: case 12: - // 管理员已满 + // 管理员已满 state.ifAddState.value = false; showToast('管理员已满'.tr, something: () { Get.back(); @@ -250,6 +251,12 @@ class AddFingerprintLogic extends BaseGetXController { final List? token = await Storage.getStringList(saveBlueToken); final List getTokenList = changeStringListToIntList(token!); + String startTime = DateTool().dateToHNString(state.effectiveDateTime.value); + String endTime = DateTool().dateToHNString(state.failureDateTime.value); + if (F.isSKY) { + startTime = '255:00'; + endTime = '255:00'; + } final String command = SenderAddFingerprintWithTimeCycleCoercionCommand( keyID: '1', @@ -267,8 +274,8 @@ class AddFingerprintLogic extends BaseGetXController { // 周循环 startDate: int.parse(state.startDate.value) ~/ 1000, endDate: int.parse(state.endDate.value) ~/ 1000, - startTime: DateTool().dateToHNString(state.effectiveDateTime.value), - endTime: DateTool().dateToHNString(state.failureDateTime.value), + startTime: startTime, + endTime: endTime, needAuthor: 1, signKey: signKeyDataList, privateKey: getPrivateKeyList, @@ -324,8 +331,8 @@ class AddFingerprintLogic extends BaseGetXController { // 周循环 startDate: int.parse(state.startDate.value) ~/ 1000, endDate: int.parse(state.endDate.value) ~/ 1000, - startTime: DateTool().dateToHNString(state.effectiveDateTime.value), - endTime: DateTool().dateToHNString(state.failureDateTime.value), + startTime: startTime, + endTime: endTime, needAuthor: 1, signKey: signKeyDataList, privateKey: getPrivateKeyList, diff --git a/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart b/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart index b3f66632..fc018b40 100755 --- a/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart +++ b/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_state.dart'; import '../../../../appRouters.dart'; @@ -178,13 +179,22 @@ class _FingerprintDetailPageState extends State isHaveLine: true, rightWidget: SizedBox( width: 60.w, height: 50.h, child: _isStressFingerprint()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index f3668a2c..cf9eda27 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -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 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}'); @@ -741,42 +743,40 @@ class LockDetailLogic extends BaseGetXController { eventBus.fire(RefreshLockDetailInfoDataEvent()); } - /// 请求设备网络信息并设置 - void requestDeviceNetworkInfo() async { - final DeviceNetwork deviceNetworkInfo = - await ApiRepository.to.getDeviceNetwork( - deviceType: 2, - deviceMac: state.keyInfos.value.mac!, - ); - if (deviceNetworkInfo.data?.wifiName == null || - deviceNetworkInfo.data?.wifiName == '') { - return; - } else { - final peerId = deviceNetworkInfo?.data?.peerId; - if (peerId == null || peerId.isEmpty || peerId == '') { - throw Exception('设备peerId为空'); - } - // 设置锁的peerID - StartChartManage().lockNetworkInfo = - deviceNetworkInfo.data ?? DeviceNetworkInfo(); - StartChartManage().lockPeerId = peerId; - } - } + // /// 请求设备网络信息并设置 + // void requestDeviceNetworkInfo() async { + // final DeviceNetwork deviceNetworkInfo = + // await ApiRepository.to.getDeviceNetwork( + // deviceType: 2, + // deviceMac: state.keyInfos.value.mac!, + // ); + // if (deviceNetworkInfo.data?.peerId == null || + // deviceNetworkInfo.data?.peerId == '') { + // return; + // } + // final peerId = deviceNetworkInfo!.data!.peerId; + // // 设置锁的peerID + // StartChartManage().lockNetworkInfo = + // deviceNetworkInfo.data ?? DeviceNetworkInfo(); + // StartChartManage().lockPeerId = peerId!; + // } /// 发送监控消息 void sendMonitorMessage() { final catEyeConfig = state.keyInfos.value.lockSetting?.catEyeConfig ?? []; + final network = state.keyInfos.value.network; if (catEyeConfig.isNotEmpty && catEyeConfig.length > 0 && catEyeConfig[0].catEyeMode != 0) { - if (StartChartManage().lockNetworkInfo.wifiName == null || - StartChartManage().lockNetworkInfo.wifiName == '') { + if (network == null || network?.peerId == null || network?.peerId == '') { showToast('设备未配网'.tr); return; } + // 重置丢包率监控 + // PacketLossStatistics().reset(); // 发送监控id - StartChartManage().startCallRequestMessageTimer( - ToPeerId: StartChartManage().lockPeerId ?? ''); + StartChartManage() + .startCallRequestMessageTimer(ToPeerId: network!.peerId ?? ''); } else { showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr); } @@ -788,12 +788,13 @@ class LockDetailLogic extends BaseGetXController { getServerDatetime(); await PermissionDialog.request(Permission.location); await PermissionDialog.requestBluetooth(); - requestDeviceNetworkInfo(); + // requestDeviceNetworkInfo(); } @override void onInit() { super.onInit(); + state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus .on() .listen((LockSetChangeSetRefreshLockDetailWithType event) { @@ -840,11 +841,11 @@ class LockDetailLogic extends BaseGetXController { } }); - state.SuccessfulDistributionNetworkEvent = eventBus - .on() - .listen((SuccessfulDistributionNetwork event) { - // 配网成功获取一下配网信息 - requestDeviceNetworkInfo(); - }); + // state.SuccessfulDistributionNetworkEvent = eventBus + // .on() + // .listen((SuccessfulDistributionNetwork event) { + // // 配网成功获取一下配网信息 + // requestDeviceNetworkInfo(); + // }); } } diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index 1040b5ba..f7605c48 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -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 /// 路由订阅 AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!); state.isOpenLockNeedOnline.refresh(); - } StreamSubscription? _lockRefreshLockDetailInfoDataEvent; @@ -1103,13 +1103,15 @@ class _LockDetailPageState extends State })); // 密码 - showWidgetArr.add(bottomItem('images/main/icon_main_password.png', '密码'.tr, - state.bottomBtnisEable.value, () { - Get.toNamed(Routers.passwordKeyListPage, - arguments: { - 'keyInfo': state.keyInfos.value - }); - })); + if (state.keyInfos.value.lockFeature!.password == 1) { + showWidgetArr.add(bottomItem('images/main/icon_main_password.png', + '密码'.tr, state.bottomBtnisEable.value, () { + Get.toNamed(Routers.passwordKeyListPage, + arguments: { + 'keyInfo': state.keyInfos.value + }); + })); + } // ic卡 if (state.keyInfos.value.lockFeature!.icCard == 1) { @@ -1179,7 +1181,7 @@ class _LockDetailPageState extends State } //可视对讲门锁新增->监控 - if (state.keyInfos.value.lockFeature!.videoIntercom == 1) { + if (state.keyInfos.value.lockFeature!.isSupportCatEye == 1) { showWidgetArr.add( bottomItem('images/main/icon_catEyes.png', '监控'.tr, state.bottomBtnisEable.value, () async { @@ -1467,7 +1469,7 @@ class _LockDetailPageState extends State state.iSOpenLock.value = true; state.openLockBtnState.value = 1; state.animationController!.forward(); - // AppLog.log('点击开锁'); + AppLog.log('点击开锁'); if (isOpenLockNeedOnline) { // 不需要联网 state.openDoorModel = 0; diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index 01178f52..44efb77d 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -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 keyInfos = LockListInfoItemEntity().obs; final Rx lockSetInfoData = LockSetInfoData().obs; late StreamSubscription 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; diff --git a/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart b/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart index 58c5a9bb..20c36487 100755 --- a/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart +++ b/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart @@ -158,23 +158,23 @@ class _BasicInformationPageState extends State { allHeight: 70.h, isHaveLine: true), )), - Obx(() => CommonItem( - leftTitel: '位置信息'.tr, - // rightTitle: state.lockBasicInfo.value.address ?? "-", - allHeight: 80.h, - isHaveLine: false, - isHaveRightWidget: true, - rightWidget: SizedBox( - width: 300.w, - child: Text(state.lockBasicInfo.value.address ?? '无'.tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - style: TextStyle( - fontSize: 22.sp, - color: AppColors.darkGrayTextColor)), - ), - )), + // Obx(() => CommonItem( + // leftTitel: '位置信息'.tr, + // // rightTitle: state.lockBasicInfo.value.address ?? "-", + // allHeight: 80.h, + // isHaveLine: false, + // isHaveRightWidget: true, + // rightWidget: SizedBox( + // width: 300.w, + // child: Text(state.lockBasicInfo.value.address ?? '无'.tr, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.end, + // style: TextStyle( + // fontSize: 22.sp, + // color: AppColors.darkGrayTextColor)), + // ), + // )), /* 2024-01-12 会议确定去掉“微信二维码” by DaisyWu CommonItem( leftTitel: diff --git a/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart b/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart index 74e2c236..95830a3b 100755 --- a/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart +++ b/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -17,31 +16,36 @@ import '../../../../tools/eventBusEventManage.dart'; import '../../../../tools/storage.dart'; import 'burglarAlarm_state.dart'; -class BurglarAlarmLogic extends BaseGetXController{ +class BurglarAlarmLogic extends BaseGetXController { BurglarAlarmState state = BurglarAlarmState(); // 配置锁的常开模式设置 -> 防撬报警 - Future _setLockSetGeneralSetting() async{ + Future _setLockSetGeneralSetting() async { final LoginEntity entity = await ApiRepository.to.setBurglarAlarmData( lockId: state.lockSetInfoData.value.lockId!, - antiPrySwitch:state.burglarAlarmEnable.value == 1 ? 0 : 1, // 1-开启、2-关闭; + antiPrySwitch: state.burglarAlarmEnable.value == 1 ? 0 : 1, // 1-开启、2-关闭; ); - if(entity.errorCode!.codeIsSuccessful){ - // eventBus.fire(RefreshLockListInfoDataEvent()); + if (entity.errorCode!.codeIsSuccessful) { + eventBus.fire(RefreshLockListInfoDataEvent()); - state.burglarAlarmEnable.value = state.burglarAlarmEnable.value == 1 ? 0 : 1; - state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch = state.burglarAlarmEnable.value; - showToast('操作成功'.tr, something: (){ - eventBus.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); + state.burglarAlarmEnable.value = + state.burglarAlarmEnable.value == 1 ? 0 : 1; + state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch = + state.burglarAlarmEnable.value; + showToast('操作成功'.tr, something: () { + eventBus + .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); }); } } // 获取解析后的数据 late StreamSubscription _replySubscription; + void _initReplySubscription() { - _replySubscription = EventBusManager().eventBus!.on().listen((Reply reply) { - if(reply is SetSupportFunctionsNoParametersReply) { + _replySubscription = + EventBusManager().eventBus!.on().listen((Reply reply) { + if (reply is SetSupportFunctionsNoParametersReply) { _replySetSupportFunctionsWithParameters(reply); } @@ -71,7 +75,7 @@ class BurglarAlarmLogic extends BaseGetXController{ // 设置自动落锁数据解析 Future _replySetSupportFunctionsWithParameters(Reply reply) async { final int status = reply.data[2]; - switch(status){ + switch (status) { case 0x00: //成功 state.sureBtnState.value = 0; @@ -91,41 +95,47 @@ class BurglarAlarmLogic extends BaseGetXController{ // 设置支持功能(带参数) Future sendBurglarAlarm() async { - if(state.sureBtnState.value == 1){ + if (state.sureBtnState.value == 1) { return; } state.sureBtnState.value = 1; EasyLoading.show(); - showBlueConnetctToastTimer(action: (){ + showBlueConnetctToastTimer(action: () { dismissEasyLoading(); state.sureBtnState.value = 0; }); - BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { + BlueManage().blueSendData(BlueManage().connectDeviceName, + (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - final List? privateKey = await Storage.getStringList(saveBluePrivateKey); - final List getPrivateKeyList = changeStringListToIntList(privateKey!); + final List? privateKey = + await Storage.getStringList(saveBluePrivateKey); + final List getPrivateKeyList = + changeStringListToIntList(privateKey!); final List? token = await Storage.getStringList(saveBlueToken); final List getTokenList = changeStringListToIntList(token!); - final List? publicKey = await Storage.getStringList(saveBluePublicKey); - final List getPublicKeyList = changeStringListToIntList(publicKey!); + final List? publicKey = + await Storage.getStringList(saveBluePublicKey); + final List getPublicKeyList = + changeStringListToIntList(publicKey!); IoSenderManage.setSupportFunctionsNoParametersCommand( - keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(), - userID: await Storage.getUid(), - featureBit: 30, - featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1, - token: getTokenList, - needAuthor: 1, - publicKey: getPublicKeyList, - privateKey: getPrivateKeyList); + keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(), + userID: await Storage.getUid(), + featureBit: 30, + featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1, + token: getTokenList, + needAuthor: 1, + publicKey: getPublicKeyList, + privateKey: getPrivateKeyList, + ); } else if (connectionState == BluetoothConnectionState.disconnected) { dismissEasyLoading(); cancelBlueConnetctToastTimer(); state.sureBtnState.value = 0; - if(state.ifCurrentScreen.value == true){ + if (state.ifCurrentScreen.value == true) { showBlueConnetctToast(); } } @@ -152,5 +162,4 @@ class BurglarAlarmLogic extends BaseGetXController{ _replySubscription.cancel(); } - } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 25c8e43d..6f0a4570 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -37,40 +37,46 @@ import 'configuringWifi_state.dart'; class ConfiguringWifiLogic extends BaseGetXController { final ConfiguringWifiState state = ConfiguringWifiState(); + final int _configurationTimeout = 60; // 配网超时时间(秒) + /// 获取WiFi锁服务IP和端口 Future getWifiLockServiceIpAndPort() async { - final ConfiguringWifiEntity entity = - await ApiRepository.to.getWifiLockServiceIpAndPort(); - if (entity.errorCode! == 0) { - state.configuringWifiEntity.value = entity; + try { + final ConfiguringWifiEntity entity = + await ApiRepository.to.getWifiLockServiceIpAndPort(); + if (entity.errorCode! == 0) { + state.configuringWifiEntity.value = entity; + } else { + AppLog.log('获取WiFi锁服务IP和端口失败:${entity.errorCode}'); + } + } catch (e) { + AppLog.log('获取WiFi锁服务IP和端口异常:$e'); } } - void updateNetworkInfo({ + /// 更新网络信息到服务器 + Future updateNetworkInfo({ required String peerId, required String wifiName, required String secretKey, required String deviceMac, required String networkMac, }) async { - final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork( - deviceType: 2, - deviceMac: deviceMac, - wifiName: wifiName, - networkMac: networkMac, - secretKey: secretKey, - peerId: peerId, - ); - if (entity.errorCode!.codeIsSuccessful) { - // 设置锁的peerID - StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + try { + final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork( + deviceType: 2, + deviceMac: deviceMac, wifiName: wifiName, networkMac: networkMac, secretKey: secretKey, peerId: peerId, ); - - await _getUploadLockSet(); + return entity; + } catch (e) { + dismissEasyLoading(); + state.sureBtnState.value = 0; + AppLog.log('网络配置异常:$e'); + return LoginEntity(); } } @@ -84,81 +90,165 @@ class ConfiguringWifiLogic extends BaseGetXController { if (reply is GatewayConfiguringWifiResultReply) { _replySenderConfiguringWifiResult(reply); } + // wifi配网命令应答结果 if (reply is GatewayConfiguringWifiReply) { - _replySenderConfiguringWifiResult(reply); + _replySenderConfiguringWifi(reply); } if (reply is GatewayGetStatusReply) { _replyGatewayGetStatusReply(reply); } - // if (reply is GatewayGetStatusReply) { - // _replyStatusInfo(reply); - // } // 上传数据获取锁设置 if (reply is UpdataLockSetReply) { _replyUpdataLockSetReply(reply); } - AppLog.log('蓝牙回调处理完毕${EasyLoading.isShow}'); }); } - // WIFI配网结果 - Future _replySenderConfiguringWifiResult(Reply reply) async { + // WIFI配网操作结果处理 + Future _replySenderConfiguringWifi(Reply reply) async { final int status = reply.data[2]; - // state.sureBtnState.value = 0; - - // 取消loading超时定时器 - state.loadingTimer?.cancel(); - state.loadingTimer = null; switch (status) { case 0x00: - await Storage.removeLockNetWorkInfoCache(); - final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3]; - - final List secretKeyList = - reply.data.sublist(5, 5 + secretKeyJsonLength); - String result = utf8String(secretKeyList); - // 解析 JSON 字符串为 Map - Map jsonMap = json.decode(result); - - // 提取 peerId - String? peerId = jsonMap['peerId']; - String? wifiName = jsonMap['wifiName']; - String? secretKey = jsonMap['secretKey']; - String? deviceMac = jsonMap['deviceMac']; - String? networkMac = jsonMap['networkMac']; - - /// 配网成功后,赋值锁的peerId - StartChartManage().lockPeerId = peerId ?? ''; - - state.isLoading.value = false; - // 保存到缓存 - await Storage.saveLockNetWorkInfo(jsonMap); - // 上报服务器 - updateNetworkInfo( - peerId: peerId ?? '', - wifiName: wifiName ?? '', - secretKey: secretKey ?? '', - deviceMac: deviceMac ?? '', - networkMac: networkMac ?? ''); - + AppLog.log('wifi配网命令回复结果:成功'); break; default: //失败 dismissEasyLoading(); // 关闭loading - cancelBlueConnetctToastTimer(); - if (state.loadingTimer != null) { - state.loadingTimer!.cancel(); - state.loadingTimer = null; - } - showToast('配网失败'.tr); state.isLoading.value = false; break; } } -// 辅助函数:美化 JSON 输出 + // WIFI配网结果处理 + Future _replySenderConfiguringWifiResult(Reply reply) async { + final int status = reply.data[2]; + + // 收到响应后,取消蓝牙超时计时器 + cancelBlueConnetctToastTimer(); + + switch (status) { + case 0x00: + // 配网成功 - 不关闭loading,保持状态直到全部完成 + await Storage.removeLockNetWorkInfoCache(); + + try { + final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3]; + final List secretKeyList = + reply.data.sublist(5, 5 + secretKeyJsonLength); + String result = utf8String(secretKeyList); + + AppLog.log('解析配网信息: $result'); + + // 解析 JSON 字符串为 Map + Map jsonMap = json.decode(result); + + // 提取网络信息 + String? peerId = jsonMap['peerId']; + String? wifiName = jsonMap['wifiName']; + String? secretKey = jsonMap['secretKey']; + String? deviceMac = jsonMap['deviceMac']; + String? networkMac = jsonMap['networkMac']; + + // 验证关键字段 + if (peerId == null || + peerId.isEmpty || + secretKey == null || + secretKey.isEmpty) { + throw Exception('Missing required network information'); + } + + // 上报服务器 - 注意: sureBtnState 状态将在 updateNetworkInfo 方法中或其回调中完成重置 + final info = await updateNetworkInfo( + peerId: peerId, + wifiName: wifiName ?? '', + secretKey: secretKey, + deviceMac: deviceMac ?? '', + networkMac: networkMac ?? ''); + if (info.errorCode!.codeIsSuccessful) { + // 设置锁的peerID + StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + wifiName: wifiName, + networkMac: networkMac, + secretKey: secretKey, + peerId: peerId, + ); + + /// 配网成功后,赋值锁的peerId + StartChartManage().lockPeerId = peerId; + + // 保存到缓存 + await Storage.saveLockNetWorkInfo(jsonMap); + + showToast('配网成功'.tr, something: () { + state.sureBtnState.value = 0; // 确保重置状态 + if (state.pageName.value == 'lockSet') { + Get.close(2); + } else { + Get.offAllNamed(Routers.starLockMain); + } + eventBus.fire(SuccessfulDistributionNetwork()); + eventBus.fire(RefreshLockListInfoDataEvent(clearScanDevices: true,isUnShowLoading: true)); + }); + + // 获取锁设置 + _getUploadLockSet(); + } else { + dismissEasyLoading(); + // showToast('网络配置失败,请重试'.tr); + state.sureBtnState.value = 0; + } + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + // showToast('解析配网信息失败,请重试'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + AppLog.log('解析配网信息失败: $e'); + return; // 添加return阻止后续流程 + } + break; + + case 0x01: + // WiFi密码错误 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + // showToast('WiFi密码错误,请重新输入'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + case 0x02: + // 找不到WiFi + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + // showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + case 0x03: + // 网络连接超时 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + // showToast('连接WiFi超时,请确保网络信号良好'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + default: + // 其他错误 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + // showToast('配网失败 (错误码: $status),请重试'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + } + } + + // 辅助函数:美化 JSON 输出 String prettyPrintJson(String jsonString) { var jsonObject = json.decode(jsonString); return JsonEncoder.withIndent(' ').convert(jsonObject); @@ -168,80 +258,111 @@ class ConfiguringWifiLogic extends BaseGetXController { Future senderConfiguringWifiAction() async { AppLog.log('开始配网${EasyLoading.isShow}'); - if (state.isLoading.isTrue) { + if (state.sureBtnState.value == 1) { AppLog.log('正在配网中请勿重复点击'); return; } - if (state.wifiNameController.text.isEmpty) { - showToast('请输入wifi名称'.tr); + + // 获取网关配置信息 + try { + final GetGatewayConfigurationEntity entity = await ApiRepository.to + .getGatewayConfigurationNotLoading(timeout: _configurationTimeout); + if (entity.errorCode!.codeIsSuccessful) { + state.getGatewayConfigurationStr = entity.data ?? ''; + } else { + // showToast('获取网关配置失败,请重试'.tr); + AppLog.log('获取网关配置失败,请重试'); + return; + } + + // 判断是否登录账户 + final loginData = await Storage.getLoginData(); + if (loginData == null) { + AppLog.log('未检测到登录信息,请重新登录'.tr); + return; + } + + // 获取app用户的peerId + String appPeerId = loginData.starchart?.starchartId ?? ''; + if (appPeerId.isEmpty) { + AppLog.log('用户ID获取失败,请重新登录'.tr); + return; + } + + // 处理配置字符串 + if (state.getGatewayConfigurationStr.isNotEmpty) { + // 解析 JSON 字符串为 Map + Map jsonMap = + json.decode(state.getGatewayConfigurationStr); + + // 移除指定的键 + jsonMap.remove("starCloudUrl"); + jsonMap.remove("starLockPeerId"); + + // 追加新的键值对 + jsonMap['userPeerld'] = appPeerId; + + // 将 Map 转换回 JSON 字符串 + state.getGatewayConfigurationStr = + json.encode(jsonMap).replaceAll(',', ',\n'); + + // 确保格式化输出 + state.getGatewayConfigurationStr = + prettyPrintJson(state.getGatewayConfigurationStr); + } else { + // 如果为空,则直接赋值 + state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}"; + } + } catch (e) { + AppLog.log('网关配置准备失败:${e.toString()}'.tr); return; } - if (state.wifiPWDController.text.isEmpty) { - showToast('请输入WiFi密码'.tr); - return; - } - // if (state.sureBtnState.value == 1) { - // return; - // } - // state.sureBtnState.value = 1; + // 先设置sureBtnState状态,以禁用按钮 + state.sureBtnState.value = 1; - final GetGatewayConfigurationEntity entity = - await ApiRepository.to.getGatewayConfigurationNotLoading(timeout: 60); - if (entity.errorCode!.codeIsSuccessful) { - state.getGatewayConfigurationStr = entity.data ?? ''; + // 显示loading,如果已经显示则不再重复显示 + if (!EasyLoading.isShow) { + showEasyLoading(); } - // 判断是否登录账户 - final loginData = await Storage.getLoginData(); - - // 获取app用户的peerId - String appPeerId = loginData?.starchart?.starchartId ?? ''; - // 如果已有值,则追加 - if (state.getGatewayConfigurationStr.isNotEmpty) { - // 解析 JSON 字符串为 Map - Map jsonMap = - json.decode(state.getGatewayConfigurationStr); - - // 移除指定的键 - jsonMap.remove("starCloudUrl"); - jsonMap.remove("starLockPeerId"); - - // 追加新的键值对 - jsonMap['userPeerld'] = appPeerId; - - // 将 Map 转换回 JSON 字符串 - state.getGatewayConfigurationStr = - json.encode(jsonMap).replaceAll(',', ',\n'); - - // 确保格式化输出 - state.getGatewayConfigurationStr = - prettyPrintJson(state.getGatewayConfigurationStr); - } else { - // 如果为空,则直接赋值 - state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}"; - } - showEasyLoading(); - showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); - state.isLoading.value = false; - }); + // 设置蓝牙操作超时处理 + showBlueConnetctToastTimer( + action: () { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; // 连接超时时重置状态 + }, + outTimer: 30, + ); // 发送配网指令 BlueManage().blueSendData( BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - IoSenderManage.gatewayConfiguringWifiCommand( - ssid: state.wifiNameController.text, - password: state.wifiPWDController.text, - gatewayConfigurationStr: state.getGatewayConfigurationStr, - ); + try { + IoSenderManage.gatewayConfiguringWifiCommand( + ssid: state.wifiNameController.text, + password: state.wifiPWDController.text, + gatewayConfigurationStr: state.getGatewayConfigurationStr, + ); + // 注意:此处不要重置sureBtnState状态,等待配网结果回调 + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + state.sureBtnState.value = 0; // 发送命令失败时重置状态 + // showToast('发送配网指令失败:${e.toString()}'.tr); + } } else if (connectionState == BluetoothConnectionState.disconnected) { - dismissEasyLoading(); + if (EasyLoading.isShow) { + dismissEasyLoading(); + } cancelBlueConnetctToastTimer(); - state.isLoading.value = false; - // state.sureBtnState.value = 0; + state.sureBtnState.value = 0; // 蓝牙断开时重置状态 if (state.ifCurrentScreen.value == true) { showBlueConnetctToast(); } @@ -249,7 +370,6 @@ class ConfiguringWifiLogic extends BaseGetXController { }, isAddEquipment: false, ); - state.isLoading.value = true; } // 获取设备状态 @@ -278,11 +398,15 @@ class ConfiguringWifiLogic extends BaseGetXController { final NetworkInfo _networkInfo = NetworkInfo(); Future getWifiName() async { - String ssid = ''; - ssid = (await _networkInfo.getWifiName())!; - ssid = ssid ?? ''; - ssid = ssid.replaceAll(r'"', ''); - return ssid ?? ''; + try { + String? ssid = await _networkInfo.getWifiName(); + ssid = ssid ?? ''; + ssid = ssid.replaceAll(r'"', ''); + return ssid; + } catch (e) { + AppLog.log('获取WiFi名称失败: $e'); + return ''; + } } ///定位权限 @@ -308,17 +432,12 @@ class ConfiguringWifiLogic extends BaseGetXController { getWifiLockServiceIpAndPort(); _initReplySubscription(); - // getDevicesStatusAction(); - } - - @override - void onInit() { - super.onInit(); } @override void onClose() { _replySubscription.cancel(); + cancelBlueConnetctToastTimer(); // 确保取消蓝牙超时计时器 super.onClose(); } @@ -330,8 +449,6 @@ class ConfiguringWifiLogic extends BaseGetXController { switch (status) { case 0x00: //成功 - // state.sureBtnState.value = 0; - final GetGatewayInfoModel gatewayModel = GetGatewayInfoModel(); // 网关MAC地址 int index = 3; @@ -372,25 +489,16 @@ class ConfiguringWifiLogic extends BaseGetXController { default: //失败 dismissEasyLoading(); - showToast('配网失败'.tr); - if (state.loadingTimer != null) { - state.loadingTimer!.cancel(); - state.loadingTimer = null; - } + // showToast('获取设备状态失败'.tr); break; } } // 上传数据获取设置 Future _getUploadLockSet() async { - showEasyLoading(); - showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); - }); - final List? token = await Storage.getStringList(saveBlueToken); final List getTokenList = changeStringListToIntList(token!); - + // 蓝牙获取锁设置 await _uploadLockSet(getTokenList); } @@ -398,44 +506,74 @@ class ConfiguringWifiLogic extends BaseGetXController { Future _uploadLockSet(List token) async { final List? privateKey = await Storage.getStringList(saveBluePrivateKey); - final List getPrivateKeyList = changeStringListToIntList(privateKey!); + if (privateKey == null || privateKey.isEmpty) { + throw Exception('Private key is empty'); + } + final List getPrivateKeyList = changeStringListToIntList(privateKey); final List? signKey = await Storage.getStringList(saveBlueSignKey); - final List signKeyDataList = changeStringListToIntList(signKey!); + if (signKey == null || signKey.isEmpty) { + throw Exception('Sign key is empty'); + } + final List signKeyDataList = changeStringListToIntList(signKey); - IoSenderManage.updataLockSetCommand( - lockID: BlueManage().connectDeviceName, - userID: await Storage.getUid(), - token: token, - needAuthor: 1, - signKey: signKeyDataList, - privateKey: getPrivateKeyList); + BlueManage().blueSendData(BlueManage().connectDeviceName, + (BluetoothConnectionState connectionState) async { + if (connectionState == BluetoothConnectionState.connected) { + IoSenderManage.updataLockSetCommand( + lockID: BlueManage().connectDeviceName, + userID: await Storage.getUid(), + token: token, + needAuthor: 1, + signKey: signKeyDataList, + privateKey: getPrivateKeyList, + ); + } else if (connectionState == BluetoothConnectionState.disconnected) { + dismissEasyLoading(); + cancelBlueConnetctToastTimer(); + if (state.ifCurrentScreen.value == true) { + showBlueConnetctToast(); + } + } + }, isAddEquipment: true); } // 上传数据获取锁设置解析 Future _replyUpdataLockSetReply(Reply reply) async { final int status = reply.data[2]; - dismissEasyLoading(); // 关闭loading + // 保持loading状态直到整个过程完成 cancelBlueConnetctToastTimer(); + switch (status) { case 0x00: await _lockDataUpload( uploadType: 1, recordType: 0, records: reply.data.sublist(7, reply.data.length)); - break; + case 0x06: - //无权限 - final List token = reply.data.sublist(3, 7); - final List saveStrList = changeIntListToStringList(token); - Storage.setStringList(saveBlueToken, saveStrList); - - _uploadLockSet(token); + //无权限,尝试重新获取token + try { + final List token = reply.data.sublist(3, 7); + final List saveStrList = changeIntListToStringList(token); + await Storage.setStringList(saveBlueToken, saveStrList); + _uploadLockSet(token); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); // 错误时关闭loading + } + // showToast('获取设置权限失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + } break; + default: - dismissEasyLoading(); - cancelBlueConnetctToastTimer(); + if (EasyLoading.isShow) { + dismissEasyLoading(); // 错误时关闭loading + } + // showToast('获取锁设置失败 (错误码: $status)'.tr); + state.sureBtnState.value = 0; // 确保重置状态 break; } } @@ -451,19 +589,10 @@ class ConfiguringWifiLogic extends BaseGetXController { recordType: recordType, records: records, isUnShowLoading: true); - if (entity.errorCode!.codeIsSuccessful) { - showToast('配网成功'.tr, something: () { - state.isLoading.value = false; - if (state.pageName.value == 'lockSet') { - Get.close(2); - } else { - Get.offAllNamed(Routers.starLockMain); - } - eventBus - .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(SuccessfulDistributionNetwork()); - }); + if (entity.errorCode!.codeIsSuccessful) { + eventBus + .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); } } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart index ed653308..33a38066 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart @@ -25,6 +25,9 @@ class _ConfiguringWifiPageState extends State final ConfiguringWifiLogic logic = Get.put(ConfiguringWifiLogic()); final ConfiguringWifiState state = Get.find().state; + // 添加密码可见性控制 + final RxBool _obscureText = true.obs; + @override Widget build(BuildContext context) { return Scaffold( @@ -39,19 +42,36 @@ class _ConfiguringWifiPageState extends State 'WiFi名称'.tr, '请输入WiFi名字'.tr, state.wifiNameController), Container( width: 1.sw, height: 1.h, color: AppColors.mainBackgroundColor), - configuringWifiTFWidget( + configuringWifiPasswordTFWidget( 'WiFi密码'.tr, '请输入WiFi密码'.tr, state.wifiPWDController), SizedBox( height: 50.h, ), Obx( () => SubmitBtn( - btnName: '确定'.tr, - isDisabled: state.isLoading.isFalse, - onClick: state.isLoading.isTrue + btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr, + // 当sureBtnState为1时按钮不可用 + isDisabled: state.sureBtnState.value == 0, + onClick: state.sureBtnState.value == 1 ? null : () { FocusScope.of(context).requestFocus(FocusNode()); + // 验证输入 + if (state.wifiNameController.text.isEmpty) { + logic.showToast('请输入WiFi名称'.tr); + return; + } + if (state.wifiPWDController.text.isEmpty) { + logic.showToast('请输入WiFi密码'.tr); + return; + } + // 检查WiFi名称是否包含5G关键字 + if (state.wifiNameController.text + .toLowerCase() + .contains('5g')) { + logic.showToast('请确保使用2.4GHz WiFi网络'.tr); + return; + } logic.senderConfiguringWifiAction(); }, ), @@ -86,7 +106,22 @@ class _ConfiguringWifiPageState extends State ); } - // 接受者信息输入框 + Widget configuringWifiPasswordTFWidget( + String titleStr, String rightTitle, TextEditingController controller) { + return Column( + children: [ + Container(height: 10.h), + CommonItem( + leftTitel: titleStr, + rightTitle: '', + isHaveRightWidget: true, + rightWidget: getPasswordTFWidget(rightTitle, controller)), + Container(height: 10.h), + ], + ); + } + + // 普通输入框 Widget getTFWidget(String tfStr, TextEditingController controller) { return Container( height: 65.h, @@ -95,18 +130,14 @@ class _ConfiguringWifiPageState extends State children: [ Expanded( child: TextField( - //输入框一行 maxLines: 1, inputFormatters: [ FilteringTextInputFormatter.deny('\n'), - // LengthLimitingTextInputFormatter(30), ], controller: controller, autofocus: false, textAlign: TextAlign.end, decoration: InputDecoration( - //输入里面输入文字内边距设置 - // contentPadding: const EdgeInsets.only(top: 12.0, bottom: 8.0), hintText: tfStr, hintStyle: TextStyle(fontSize: 22.sp), focusedBorder: const OutlineInputBorder( @@ -135,6 +166,61 @@ class _ConfiguringWifiPageState extends State ); } + // 密码输入框 + Widget getPasswordTFWidget(String tfStr, TextEditingController controller) { + return Container( + height: 65.h, + width: 300.w, + child: Row( + children: [ + Expanded( + child: Obx( + () => TextField( + maxLines: 1, + obscureText: _obscureText.value, + inputFormatters: [ + FilteringTextInputFormatter.deny('\n'), + ], + controller: controller, + autofocus: false, + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: tfStr, + hintStyle: TextStyle(fontSize: 22.sp), + focusedBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + disabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + enabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + border: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + style: TextStyle( + fontSize: 22.sp, textBaseline: TextBaseline.alphabetic), + ), + ), + ), + IconButton( + icon: Icon( + _obscureText.value ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + size: 24.sp, + ), + onPressed: () { + _obscureText.value = !_obscureText.value; + }, + ), + ], + ), + ); + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart index 513fede2..350a2863 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:network_info_plus/network_info_plus.dart'; @@ -33,5 +31,4 @@ class ConfiguringWifiState { String getGatewayConfigurationStr = ''; RxBool isLoading = false.obs; - Timer? loadingTimer; } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart index 8f95204e..93532323 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/blue/io_gateway/io_gateway_getWifiList.dart'; import 'package:star_lock/blue/io_protocol/io_getWifiList.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; @@ -20,7 +21,9 @@ class WifiListLogic extends BaseGetXController { // 获取解析后的数据 late StreamSubscription _replySubscription; + Timer? _connectionTimer; + /// 初始化订阅,监听设备响应 void _initReplySubscription() { _replySubscription = EventBusManager().eventBus!.on().listen((Reply reply) { @@ -31,78 +34,130 @@ class WifiListLogic extends BaseGetXController { if (reply is GatewayGetWifiListReply) { _replyGetWifiListParameters(reply); } + }, onError: (error) { + // 处理CRC校验失败等错误 + AppLog.log('WiFi列表获取过程中发生错误: $error'); + + // 取消loading状态,显示错误提示 + dismissEasyLoading(); + cancelBlueConnetctToastTimer(); + + // 重置按钮状态,允许重新扫描 + state.sureBtnState.value = 0; + + // 如果是CRC校验失败,显示特定提示 + if (error.toString().contains('CRC')) { + showToast('数据校验失败,请重新扫描'.tr); + } else { + showToast('扫描WiFi失败,请重试'.tr); + } }); } - // 发送获取wifi列表数据解析 + /// 发送获取wifi列表数据解析 Future _replySendGetWifiParameters(Reply reply) async { final int status = reply.data[2]; switch (status) { case 0x00: - //成功 - showEasyLoading(); + //成功 - 不显示loading框,UI中已经有进度指示器 cancelBlueConnetctToastTimer(); - Future.delayed(5.seconds, dismissEasyLoading); break; case 0x06: // 需要鉴权 + dismissEasyLoading(); + AppLog.log('需要设备鉴权,请重试'.tr); + state.sureBtnState.value = 0; break; default: + // 处理其他错误状态 + dismissEasyLoading(); + AppLog.log('获取WiFi列表失败,错误码:$status'.tr); + state.sureBtnState.value = 0; break; } } - // 获取WiFi数据解析 + /// 获取WiFi数据解析 Future _replyGetWifiListParameters(Reply reply) async { final int status = reply.data[2]; switch (status) { case 0x00: //成功 - // showEasyLoading(); dismissEasyLoading(); state.sureBtnState.value = 0; if (reply.data[3] > 0) { reply.data.removeRange(0, 4); - // 把得到的数据按33位分割成数组 然后塞进一个新的数组里面 + // 把得到的数据按33位分割成数组然后处理 final List> getList = splitList(reply.data, 33); final List> uploadList = >[]; + for (int i = 0; i < getList.length; i++) { final List indexList = getList[i]; final Map indexMap = {}; final List wifiName = indexList.sublist(0, 32); - indexMap['wifiName'] = utf8String(wifiName); + final String wifiNameStr = utf8String(wifiName).trim(); + + // 过滤掉空的WiFi名称 + if (wifiNameStr.isEmpty) { + continue; + } + + indexMap['wifiName'] = wifiNameStr; indexMap['rssi'] = (indexList.last - 255).toString(); uploadList.add(indexMap); - state.wifiNameDataList.value = uploadList; } + + // 按信号强度排序WiFi列表 (从强到弱) + uploadList.sort( + (a, b) => int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); + + state.wifiNameDataList.value = uploadList; + + if (uploadList.isEmpty) { + showToast('未检测到可用的WiFi网络'.tr); + } + } else { + // 处理WiFi列表为空的情况 + state.wifiNameDataList.clear(); + showToast('未检测到可用的WiFi网络'.tr); } break; default: + // 处理其他错误状态 + dismissEasyLoading(); + showToast('解析WiFi列表失败,错误码:$status'.tr); + state.sureBtnState.value = 0; break; } } - // 获取wifi列表 + /// 获取WiFi列表 Future senderGetWifiListWifiAction() async { if (state.sureBtnState.value == 1) { return; } state.sureBtnState.value = 1; + state.wifiNameDataList.clear(); // 清空之前的列表 - showEasyLoading(); + // 不显示loading框,UI中已经有进度指示器 showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); state.sureBtnState.value = 0; }); + BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - IoSenderManage.gatewayGetWifiCommand( - userID: await Storage.getUid(), - ); + try { + IoSenderManage.gatewayGetWifiCommand( + userID: await Storage.getUid(), + ); + } catch (e) { + state.sureBtnState.value = 0; + cancelBlueConnetctToastTimer(); + showToast('发送获取WiFi列表请求失败:${e.toString()}'.tr); + } } else if (connectionState == BluetoothConnectionState.disconnected) { - dismissEasyLoading(); state.sureBtnState.value = 0; cancelBlueConnetctToastTimer(); if (state.ifCurrentScreen.value == true) { @@ -113,22 +168,27 @@ class WifiListLogic extends BaseGetXController { } @override - void onReady() { + void onReady() async { super.onReady(); - _initReplySubscription(); + await senderGetWifiListWifiAction(); + dismissEasyLoading(); } @override void onInit() { super.onInit(); - - senderGetWifiListWifiAction(); + // 页面进入时标记为当前页面 + state.ifCurrentScreen.value = true; } @override void onClose() { - super.onClose(); + // 取消所有计时器和订阅,防止内存泄漏 _replySubscription.cancel(); + _connectionTimer?.cancel(); + cancelBlueConnetctToastTimer(); + state.ifCurrentScreen.value = false; + super.onClose(); } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart index 2508cc27..fd4c6765 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart @@ -23,6 +23,32 @@ class _WifiListPageState extends State { final WifiListLogic logic = Get.put(WifiListLogic()); final WifiListState state = Get.find().state; + /// 计算WiFi信号强度图标 + IconData _getWifiSignalIcon(String rssi) { + final int rssiValue = int.parse(rssi); + if (rssiValue >= -50) { + return Icons.signal_wifi_4_bar; + } else if (rssiValue >= -70) { + return Icons.network_wifi; + } else if (rssiValue >= -80) { + return Icons.network_wifi_2_bar_rounded; + } else { + return Icons.signal_wifi_0_bar; + } + } + + @override + void initState() { + super.initState(); + state.ifCurrentScreen.value = true; + } + + @override + void dispose() { + state.ifCurrentScreen.value = false; + super.dispose(); + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -38,13 +64,15 @@ class _WifiListPageState extends State { barTitle: 'WIFI列表'.tr, haveBack: state.pageName.value == 'lockSet', actionsList: [ - TextButton( - child: Text( - '刷新'.tr, - style: TextStyle(color: Colors.white, fontSize: 24.sp), - ), - onPressed: logic.senderGetWifiListWifiAction, - ), + Obx(() => TextButton( + child: Text( + '刷新'.tr, + style: TextStyle(color: Colors.white, fontSize: 24.sp), + ), + onPressed: state.sureBtnState.value == 0 + ? logic.senderGetWifiListWifiAction + : null, + )), ], backgroundColor: AppColors.mainColor, ), @@ -52,26 +80,56 @@ class _WifiListPageState extends State { children: [ Expanded( child: Obx(() => state.wifiNameDataList.value.isNotEmpty - ? ListView.builder( - itemCount: state.wifiNameDataList.value.length, - itemBuilder: (BuildContext c, int index) { - Map wifiNameStr = state.wifiNameDataList.value[index]; - return _messageListItem( - wifiNameStr['wifiName'], wifiNameStr['rssi'], () { - Get.toNamed(Routers.configuringWifiPage, - arguments: { - 'lockSetInfoData': - state.lockSetInfoData.value, - 'wifiName': wifiNameStr['wifiName'], - 'pageName': state.pageName.value, - }); - }); - }) - : NoData( - noDataHeight: 1.sh - - ScreenUtil().statusBarHeight - - ScreenUtil().bottomBarHeight - - 64.h)), + ? RefreshIndicator( + onRefresh: () async { + if (state.sureBtnState.value == 0) { + await logic.senderGetWifiListWifiAction(); + } + }, + child: ListView.builder( + itemCount: state.wifiNameDataList.value.length, + itemBuilder: (BuildContext c, int index) { + Map wifiNameStr = + state.wifiNameDataList.value[index]; + return _messageListItem( + wifiNameStr['wifiName'], wifiNameStr['rssi'], + () { + Get.toNamed(Routers.configuringWifiPage, + arguments: { + 'lockSetInfoData': + state.lockSetInfoData.value, + 'wifiName': wifiNameStr['wifiName'], + 'pageName': state.pageName.value, + }); + }); + }), + ) + : Obx(() => state.sureBtnState.value == 1 + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 50.w, + height: 50.w, + child: CircularProgressIndicator( + strokeWidth: 4.w, + valueColor: AlwaysStoppedAnimation( + AppColors.mainColor, + ), + backgroundColor: Colors.grey[200], + ), + ), + SizedBox(height: 20.h), + Text('正在扫描WiFi网络...\n请确保设备处于正常状态'.tr, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24.sp, + color: AppColors.blackColor)) + ], + ), + ) + : NoData())), ), state.pageName.value == 'saveLock' ? SubmitBtn( @@ -140,24 +198,37 @@ class _WifiListPageState extends State { height: 79.h, width: 1.sw - 20.w * 2, child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( + flex: 4, child: Text( - '$wifiName(${rssi}db)', + wifiName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 22.sp, color: AppColors.blackColor), + fontSize: 24.sp, color: AppColors.blackColor), ), ), - // Text( - // rssi, - // maxLines: 1, - // overflow: TextOverflow.ellipsis, - // style: TextStyle( - // fontSize: 22.sp, color: AppColors.blackColor), - // ) + Flexible( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + _getWifiSignalIcon(rssi), + color: AppColors.mainColor, + size: 24.sp, + ), + SizedBox(width: 8.w), + Text( + '$rssi dB', + style: + TextStyle(fontSize: 18.sp, color: Colors.black), + ), + ], + ), + ) ], ), ), diff --git a/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart b/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart index ed696552..e203a907 100755 --- a/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart +++ b/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart @@ -118,6 +118,15 @@ class _NormallyOpenModePageState extends State with RouteA : SubmitBtn( btnName: '保存'.tr, onClick: () { + if (state.weekDays.value.isEmpty) { + logic.showToast('请选择常开日期'.tr); + return; + } + + if (state.endTimeMinute.value < state.beginTimeMinute.value) { + logic.showToast('结束时间不能小于开始时间哦'.tr); + return; + } logic.sendAutoLock(); }), )), diff --git a/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart b/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart index d5cc7d9d..3d62a5c1 100755 --- a/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart +++ b/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart @@ -21,9 +21,10 @@ class RemoteUnlockingLogic extends BaseGetXController { RemoteUnlockingState state = RemoteUnlockingState(); void remoteUnlockingOpenOrClose() async { - final LoginEntity entity = await ApiRepository.to.remoteUnlockingOpenOrClose( - lockId: state.lockSetInfoData.value.lockId!, - remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1); + final LoginEntity entity = await ApiRepository.to + .remoteUnlockingOpenOrClose( + lockId: state.lockSetInfoData.value.lockId!, + remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1); if (entity.errorCode!.codeIsSuccessful) { showToast('操作成功'.tr, something: () { eventBus.fire(RefreshLockListInfoDataEvent()); @@ -32,7 +33,6 @@ class RemoteUnlockingLogic extends BaseGetXController { state.remoteEnable.value; eventBus .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(RefreshLockListInfoDataEvent()); eventBus.fire(LockSetChangeSetRefreshLockDetailWithType( 5, state.lockSetInfoData.value.lockSettingInfo!.remoteUnlock! @@ -44,6 +44,7 @@ class RemoteUnlockingLogic extends BaseGetXController { // 获取解析后的数据 late StreamSubscription _replySubscription; + void _initReplySubscription() { _replySubscription = EventBusManager().eventBus!.on().listen((reply) { diff --git a/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart b/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart index e646ffb2..ff350d42 100755 --- a/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart +++ b/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart @@ -29,9 +29,9 @@ class CoerceOpenDoorLogic extends BaseGetXController { case 2: return '密码'.tr; case 3: - return '指纹'.tr; + return 'IC卡'.tr; case 4: - return '卡'.tr; + return '指纹'.tr; case 5: return '人脸'.tr; default: diff --git a/lib/main/lockDetail/palm/palmList/palmList_logic.dart b/lib/main/lockDetail/palm/palmList/palmList_logic.dart index 8d15fa33..c4b94410 100755 --- a/lib/main/lockDetail/palm/palmList/palmList_logic.dart +++ b/lib/main/lockDetail/palm/palmList/palmList_logic.dart @@ -234,6 +234,7 @@ class PalmListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getPalmListData(isRefresh: true); } } diff --git a/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart b/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart index cc02af47..b9a39d6c 100755 --- a/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart +++ b/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_logic.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_state.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyList/passwordKeyListEntity.dart'; @@ -133,7 +134,7 @@ class _PasswordKeyDetailPageState extends State action: () {}), Container(height: 10.h), Obx(() => Visibility( - visible: state.itemData.value.isCustom! == 1, + visible: state.itemData.value.isCustom! == 1 && !F.isSKY, child: Column( children: [ CommonItem( diff --git a/lib/main/lockMian/entity/lockListInfo_entity.dart b/lib/main/lockMian/entity/lockListInfo_entity.dart index 754891c1..28c48457 100755 --- a/lib/main/lockMian/entity/lockListInfo_entity.dart +++ b/lib/main/lockMian/entity/lockListInfo_entity.dart @@ -361,6 +361,7 @@ class Bluetooth { class LockFeature { LockFeature({ this.password, + this.passwordIssue, this.icCard, this.fingerprint, this.fingerVein, @@ -374,10 +375,14 @@ class LockFeature { this.isNoSupportedBlueBroadcast, this.wifiLockType, this.wifi, + this.isH264, + this.isH265, + this.isMJpeg, }); LockFeature.fromJson(Map json) { password = json['password']; + passwordIssue = json['passwordIssue']; icCard = json['icCard']; fingerprint = json['fingerprint']; fingerVein = json['fingerVein']; @@ -391,9 +396,13 @@ class LockFeature { isNoSupportedBlueBroadcast = json['isNoSupportedBlueBroadcast']; wifiLockType = json['wifiLockType']; wifi = json['wifi']; + isH264 = json['isH264']; + isH265 = json['isH265']; + isMJpeg = json['isMJpeg']; } int? password; + int? passwordIssue; int? icCard; int? fingerprint; int? fingerVein; @@ -407,10 +416,14 @@ class LockFeature { int? isNoSupportedBlueBroadcast; int? wifiLockType; int? wifi; + int? isH264; + int? isH265; + int? isMJpeg; Map toJson() { final Map data = {}; data['password'] = password; + data['passwordIssue'] = passwordIssue; data['icCard'] = icCard; data['fingerprint'] = fingerprint; data['fingerVein'] = fingerVein; @@ -424,6 +437,9 @@ class LockFeature { data['isNoSupportedBlueBroadcast'] = isNoSupportedBlueBroadcast; data['wifiLockType'] = wifiLockType; data['wifi'] = wifi; + data['isH264'] = isH264; + data['isH265'] = isH265; + data['isMJpeg'] = isMJpeg; return data; } } diff --git a/lib/main/lockMian/lockList/lockList_logic.dart b/lib/main/lockMian/lockList/lockList_logic.dart index dab00451..948546d3 100755 --- a/lib/main/lockMian/lockList/lockList_logic.dart +++ b/lib/main/lockMian/lockList/lockList_logic.dart @@ -27,13 +27,13 @@ class LockListLogic extends BaseGetXController { LockListLogic(this.entity) {} LockListState state = LockListState(); - List _groupDataList = []; + final RxList groupDataList = [].obs; LockListInfoGroupEntity? entity; final ShowTipView showTipView = ShowTipView(); - List get groupDataList { + List get groupDataListFiltered { final List list = - _groupDataList.map((GroupList e) => e.copy()).toList(); + groupDataList.map((GroupList e) => e.copy()).toList(); if (state.searchStr.value != '' && state.showSearch.value) { list.forEach((GroupList element) { element.lockList?.removeWhere((LockListInfoItemEntity element) => @@ -60,15 +60,12 @@ class LockListLogic extends BaseGetXController { //设置数据 void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) { this.entity = entity; - if (entity.pageNo == 1) { - _groupDataList = []; - } - _groupDataList.addAll(entity.groupList!); - update(); + groupDataList.value = entity.groupList!; } // 监听蓝牙协议返回结果 late StreamSubscription _replySubscription; + late StreamSubscription _setLockListInfoGroupEntity; void _initReplySubscription() { _replySubscription = @@ -336,17 +333,30 @@ class LockListLogic extends BaseGetXController { void onReady() { super.onReady(); _initReplySubscription(); + _initEventHandler(); } @override void onInit() { super.onInit(); - // AppLog.log('onInit调用了 setLockListInfoGroupEntity'); - setLockListInfoGroupEntity(entity!); + AppLog.log('[onInit] entity: \\${entity?.toString()}'); + if (entity != null) { + setLockListInfoGroupEntity(entity!); + } + _initEventHandler(); } @override void onClose() { _replySubscription.cancel(); + _setLockListInfoGroupEntity.cancel(); + } + + void _initEventHandler() { + _setLockListInfoGroupEntity = eventBus + .on() + .listen((SetLockListInfoGroupEntity event) async { + setLockListInfoGroupEntity(event.lockListInfoGroupEntity); + }); } } diff --git a/lib/main/lockMian/lockList/lockList_page.dart b/lib/main/lockMian/lockList/lockList_page.dart index 1e53cdb9..55cb149a 100755 --- a/lib/main/lockMian/lockList/lockList_page.dart +++ b/lib/main/lockMian/lockList/lockList_page.dart @@ -37,32 +37,31 @@ class _LockListPageState extends State with RouteAware { @override Widget build(BuildContext context) { - return GetBuilder(builder: (LockListLogic logic) { - return Scaffold( - body: ListView.separated( - itemCount: logic.groupDataList.length, - itemBuilder: (BuildContext context, int index) { - final GroupList itemData = logic.groupDataList[index]; - return _buildLockExpandedList(context, index, itemData); - }, - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - separatorBuilder: (BuildContext context, int index) { - return const Divider( - height: 1, - color: AppColors.greyLineColor, - ); - }), - ); - }); + return Obx(() => Scaffold( + body: ListView.separated( + itemCount: logic.groupDataListFiltered.length, + itemBuilder: (BuildContext context, int index) { + final GroupList itemData = logic.groupDataListFiltered[index]; + return _buildLockExpandedList(context, index, itemData, key: ValueKey(itemData.groupId)); + }, + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 1, + color: AppColors.greyLineColor, + ); + }), + )); } //设备多层级列表 Widget _buildLockExpandedList(BuildContext context, int index, - GroupList itemData) { + GroupList itemData, {Key? key}) { final List lockItemList = itemData.lockList ?? []; return LockListGroupView( + key: key, onTap: () { //是否选中组 if (itemData.isChecked) {} else {} diff --git a/lib/main/lockMian/lockMain/lockMain_logic.dart b/lib/main/lockMian/lockMain/lockMain_logic.dart index 802e9bbe..3ac67134 100755 --- a/lib/main/lockMian/lockMain/lockMain_logic.dart +++ b/lib/main/lockMian/lockMain/lockMain_logic.dart @@ -132,20 +132,21 @@ class LockMainLogic extends BaseGetXController { state.lockListInfoGroupEntity.refresh(); // AppLog.log('entity:$entity state.lockListInfoGroupEntity.value.groupList!.length:${state.lockListInfoGroupEntity.value.groupList![0].lockList!.length}'); //检测控制器是否存在 - if (Get.isRegistered()) { - //设置控制器数据并刷新 - // AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity'); - Get.find().setLockListInfoGroupEntity(entity); - } else { - //延迟加载 - Future.delayed(200.milliseconds, () { - if (Get.isRegistered()) { - //设置控制器数据并刷新 - // AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity'); - Get.find().setLockListInfoGroupEntity(entity); - } - }); - } + eventBus.fire(SetLockListInfoGroupEntity(lockListInfoGroupEntity: entity)); + // if (Get.isRegistered()) { + // //设置控制器数据并刷新 + // // AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity'); + // Get.find().setLockListInfoGroupEntity(entity); + // } else { + // //延迟加载 + // Future.delayed(500.milliseconds, () { + // if (Get.isRegistered()) { + // //设置控制器数据并刷新 + // // AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity'); + // Get.find().setLockListInfoGroupEntity(entity); + // } + // }); + // } if (state.dataLength.value == 1) { if (Get.isRegistered()) { diff --git a/lib/network/api_provider.dart b/lib/network/api_provider.dart index 4cad97cc..01aabb37 100755 --- a/lib/network/api_provider.dart +++ b/lib/network/api_provider.dart @@ -353,7 +353,7 @@ class ApiProvider extends BaseProvider { ); // 获取手机联网token - Future getLockNetToken(String lockId) => post( + Future getLockNetToken(int lockId) => post( getLockNetTokenURL.toUrl, jsonEncode({ 'lockId': lockId, diff --git a/lib/network/api_repository.dart b/lib/network/api_repository.dart index b5e09c33..86d3c604 100755 --- a/lib/network/api_repository.dart +++ b/lib/network/api_repository.dart @@ -325,7 +325,7 @@ class ApiRepository { } // 获取手机联网token - Future getLockNetToken({required String lockId}) async { + Future getLockNetToken({required int lockId}) async { final res = await apiProvider.getLockNetToken(lockId); return LockNetTokenEntity.fromJson(res.body); } diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index 60d57a0d..ff8a6727 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -16,4 +16,8 @@ class TalkConstant { videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); + static TalkExpectReq H264_720P_Expect = TalkExpectReq( + videoType: [VideoTypeE.H264_720P], + audioType: [AudioTypeE.G711], + ); } diff --git a/lib/talk/starChart/entity/scp_message.dart b/lib/talk/starChart/entity/scp_message.dart index fc8a3141..ca5fcef3 100644 --- a/lib/talk/starChart/entity/scp_message.dart +++ b/lib/talk/starChart/entity/scp_message.dart @@ -68,6 +68,18 @@ class ScpMessage { return 'ScpMessage{ProtocolFlag: $ProtocolFlag, MessageType: $MessageType, MessageId: $MessageId, SpTotal: $SpTotal, SpIndex: $SpIndex, FromPeerId: $FromPeerId, ToPeerId: $ToPeerId, PayloadType: $PayloadType, PayloadCRC: $PayloadCRC, PayloadLength: $PayloadLength, Payload: $Payload}'; } + // 辅助函数:定长字符串编码 + List encodeFixedLengthString(String? str, int length) { + final bytes = utf8.encode(str ?? ''); + if (bytes.length > length) { + return bytes.sublist(0, length); + } else if (bytes.length < length) { + return bytes + List.filled(length - bytes.length, 0); + } else { + return bytes; + } + } + String serialize() { final bytes = []; @@ -98,16 +110,19 @@ class ScpMessage { if (SpIndex != null) { bytes.add(SpIndex!); } - - // FromPeerId (字符串,记录长度) + // FromPeerId (字符串,长度固定为44字节) if (FromPeerId != null) { bytes.addAll(utf8.encode(FromPeerId!)); } + // FromPeerId (44字节定长) + // bytes.addAll(encodeFixedLengthString(FromPeerId, 44)); - // ToPeerId (字符串,假设长度固定为32字节) + // ToPeerId (字符串,长度固定为44字节) if (ToPeerId != null) { bytes.addAll(utf8.encode(ToPeerId!)); } + // ToPeerId (44字节定长) + // bytes.addAll(encodeFixedLengthString(ToPeerId, 44)); // PayloadType (2 bytes) if (PayloadType != null) { diff --git a/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart b/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart index beebeb75..cff8154f 100644 --- a/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.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/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -22,7 +23,7 @@ class UdpEchoTestHandler extends ScpMessageBaseHandle EasyLoading.showToast(scpMessage.Payload, duration: 2000.milliseconds); } else { talkDataRepository.addTalkData( - TalkData(content: payload, contentType: TalkData_ContentTypeE.Image)); + TalkDataModel(talkData: TalkData(content: payload, contentType: TalkData_ContentTypeE.Image))); } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart index 5f71aafa..35389a70 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; import 'package:get/get.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; @@ -13,6 +14,8 @@ import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; +import 'package:star_lock/tools/storage.dart'; import '../../star_chart_manage.dart'; @@ -32,7 +35,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle // 停止同意接听的重发 startChartManage.stopTalkAcceptTimer(); // 接听之后增加期望音频的接收 - _handleSendExpect(); + _handleSendExpect(lockPeerID: scpMessage.FromPeerId!); // 停止播放铃声 stopRingtone(); // 设置状态为接听成功 @@ -76,8 +79,48 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle } } - void _handleSendExpect() { - // 修改预期数据并启动发送预期数据定时器,在收到回复时停止 - startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + /// 收到同意接听回复之后增加音频的期望数据 + void _handleSendExpect({ + required String lockPeerID, + }) async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + 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) { + isH264 = lockInfo.lockFeature?.isH264 == 1; + isMJpeg = lockInfo.lockFeature?.isMJpeg == 1; + } + } + } + } + }); + } + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart index 0a9e7ab3..028ffb73 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart @@ -5,6 +5,7 @@ import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.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/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -61,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) { @@ -118,7 +116,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle void _handleVideoH264(TalkData talkData) { final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame(); talkDataH264Frame.mergeFromBuffer(talkData.content); - frameHandler.handleFrame(talkDataH264Frame); + frameHandler.handleFrame(talkDataH264Frame, talkData); } /// 处理图片数据 @@ -127,7 +125,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle await _processCompletePayload(Uint8List.fromList(talkData.content)); processCompletePayload.forEach((element) { talkData.content = element; - talkDataRepository.addTalkData(talkData); + talkDataRepository.addTalkData( + TalkDataModel( + talkData: talkData, + ), + ); }); } @@ -138,7 +140,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle // // 转pcm数据 // List pcmBytes = G711().convertList(g711Data); // talkData.content = pcmBytes; - talkDataRepository.addTalkData(talkData); + talkDataRepository.addTalkData( + TalkDataModel( + talkData: talkData, + ), + ); } catch (e) { print('Error decoding G.711 to PCM: $e'); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index 20dc1d2f..792e7f2c 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; @@ -20,7 +21,7 @@ import '../../star_chart_manage.dart'; class UdpTalkExpectHandler extends ScpMessageBaseHandle implements ScpMessageHandler { - final TalkViewState talkViewState = Get.put(TalkViewLogic()).state; + // final TalkViewState talkViewState = Get.put(TalkViewLogic()).state; @override void handleReq(ScpMessage scpMessage) { @@ -40,7 +41,11 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle startChartManage.stopTalkExpectMessageTimer(); // 停止发送对讲请求 startChartManage.stopCallRequestMessageTimer(); - talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; + // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; + startChartManage.rotateAngle = talkExpectResp.rotate; + startChartManage.videoWidth = talkExpectResp.width; + startChartManage.videoHeight = talkExpectResp.height; + AppLog.log('视频画面需要旋转:${talkExpectResp.rotate},画面宽高:${talkExpectResp.width}-${talkExpectResp.height}'); // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index be2f4565..195c2ab6 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -1,21 +1,19 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/services.dart'; - -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:star_lock/appRouters.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.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/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; -import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:star_lock/translations/current_locale_tool.dart'; @@ -25,27 +23,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle RxString currentLanguage = CurrentLocaleTool.getCurrentLocaleString().obs; // 当前选择语言 - // 添加上次处理请求的时间戳 - int _lastRequestTime = 0; - @override void handleReq(ScpMessage scpMessage) async { - - final currentTime = DateTime.now().millisecondsSinceEpoch; - // 确保与上次请求间隔至少1秒 - if (currentTime - _lastRequestTime < 1000) { - // 如果间隔小于1秒,直接拒绝请求 - replyErrorMessage(scpMessage); - AppLog.log('对讲请求过于频繁,已拒绝'); - return; - } - - // 更新最后处理时间 - _lastRequestTime = currentTime; - // 判断是否登录账户 final loginData = await Storage.getLoginData(); - // 如果登录账户不为空,且不是被动接听状态,且不是接听成功状态 if (loginData != null && (talkStatus.status != TalkStatus.passiveCallWaitingAnswer || @@ -56,7 +37,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle startChartManage.ToPeerId = scpMessage.FromPeerId!; startChartManage.lockPeerId = scpMessage.FromPeerId!; // 处理收到接听请求后的事件 - _talkRequestEvent(talkObjectName: talkReq.callerName); + _talkRequestEvent( + talkObjectName: talkReq.callerName, + lockPeerID: scpMessage.FromPeerId!, + ); // 回复成功 replySuccessMessage(scpMessage); @@ -75,6 +59,11 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 收到对讲请求的应答 startChartManage.FromPeerId = scpMessage.ToPeerId!; startChartManage.ToPeerId = scpMessage.FromPeerId!; + startChartManage.lockPeerId = scpMessage.FromPeerId!; + // 处理预期数据格式 + _handleResponseSendExpect( + lockPeerID: scpMessage.FromPeerId!, + ); // 发送预期数据 startChartManage.startTalkExpectTimer(); // 停止发送对讲请求 @@ -95,28 +84,56 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle void handleRealTimeData(ScpMessage scpMessage) {} // 来电事件的处理 - void _talkRequestEvent({required String talkObjectName}) { + void _talkRequestEvent({ + required String talkObjectName, + required String lockPeerID, + }) async { // 发送预期数据、通知锁板需要获取视频数据 - _handleSendExpect(); + _handleRequestSendExpect(lockPeerID: lockPeerID); // 播放铃声 //test:使用自定义铃声 playRingtone(); // 显示状态栏弹窗 - _showTalkRequestNotification(talkObjectName: talkObjectName); + // _showTalkRequestNotification(talkObjectName: talkObjectName); // 设置为等待接听状态 talkStatus.setPassiveCallWaitingAnswer(); - // 收到呼叫请求,跳转到接听页面 + + // 获取锁支持项 + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + var isWifiLockType = currentKeyInfo.lockFeature?.wifiLockType == 1; + + 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) { + isWifiLockType = lockInfo.lockFeature?.wifiLockType == 1; + } + } + } + } + }); + } + if (isWifiLockType) { + Get.toNamed(Routers.imageTransmissionView); + return; + } if (startChartManage - .getDefaultTalkExpect() - .videoType - .indexOf(VideoTypeE.H264) == - -1) { + .getDefaultTalkExpect() + .videoType + .contains(VideoTypeE.H264)) { Get.toNamed( - Routers.starChartTalkView, + Routers.h264WebView, ); } else { Get.toNamed( - Routers.h264WebView, + Routers.starChartTalkView, ); } } @@ -188,8 +205,97 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } } - void _handleSendExpect() { - // 修改预期数据并启动发送预期数据定时器,在收到回复时停止 - startChartManage.sendOnlyImageVideoTalkExpectData(); + /// app收到的对讲请求后,发送的预期数据 + void _handleRequestSendExpect({ + required String lockPeerID, + }) async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + 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) { + isH264 = lockInfo.lockFeature?.isH264 == 1; + isMJpeg = lockInfo.lockFeature?.isMJpeg == 1; + } + } + } + } + }); + } + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendOnlyH264VideoTalkExpectData(); + print( + 'app收到的对讲请求后,发送的预期数据=========锁支持H264,发送H264视频格式期望数据,peerID=${lockPeerID}'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendOnlyImageVideoTalkExpectData(); + print( + 'app收到的对讲请求后,发送的预期数据=========锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); + } else { + // 默认使用图像视频 + startChartManage.sendOnlyImageVideoTalkExpectData(); + print( + 'app收到的对讲请求后,发送的预期数据=========锁不支持H264和MJPEG,默认发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); + } + } + + /// app主动发请求,收到回复后发送的预期数据 + void _handleResponseSendExpect({ + required String lockPeerID, + }) async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + 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) { + isH264 = lockInfo.lockFeature?.isH264 == 1; + isMJpeg = lockInfo.lockFeature?.isMJpeg == 1; + } + } + } + } + }); + } + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + AppLog.log( + 'app主动发对讲请求,收到回复后发送的预期数据=======锁支持H264,发送H264视频格式期望数据,peerID=${lockPeerID}'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + AppLog.log( + 'app主动发对讲请求,收到回复后发送的预期数据=======锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + AppLog.log( + 'app主动发对讲请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); + } } } diff --git a/lib/talk/starChart/handle/other/h264_frame_handler.dart b/lib/talk/starChart/handle/other/h264_frame_handler.dart index cd2f8bc2..face6b68 100644 --- a/lib/talk/starChart/handle/other/h264_frame_handler.dart +++ b/lib/talk/starChart/handle/other/h264_frame_handler.dart @@ -3,15 +3,17 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:star_lock/app_settings/app_settings.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 '../../proto/talk_data_h264_frame.pb.dart'; class H264FrameHandler { - - final void Function(List frameData) onCompleteFrame; + final void Function(TalkDataModel frameData) onCompleteFrame; H264FrameHandler({required this.onCompleteFrame}); - void handleFrame(TalkDataH264Frame frame) { - onCompleteFrame(frame.frameData); + void handleFrame(TalkDataH264Frame frame, TalkData talkData) { + onCompleteFrame( + TalkDataModel(talkData: talkData, talkDataH264Frame: frame)); } } diff --git a/lib/talk/starChart/handle/other/packet_loss_statistics.dart b/lib/talk/starChart/handle/other/packet_loss_statistics.dart index 34dd2282..8aabf6fd 100644 --- a/lib/talk/starChart/handle/other/packet_loss_statistics.dart +++ b/lib/talk/starChart/handle/other/packet_loss_statistics.dart @@ -10,6 +10,10 @@ class PacketLossStatistics { // key: messageId, value: {totalPackets, receivedPackets} final Map _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 = []; + + _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 receivedPackets = HashSet(); + int timestamp; // 添加时间戳字段,记录最后更新时间 - PacketInfo(this.totalPackets); + PacketInfo(this.totalPackets) + : timestamp = DateTime.now().millisecondsSinceEpoch; } // 丢包统计信息类 diff --git a/lib/talk/starChart/handle/other/talk_data_model.dart b/lib/talk/starChart/handle/other/talk_data_model.dart new file mode 100644 index 00000000..a84f1d3d --- /dev/null +++ b/lib/talk/starChart/handle/other/talk_data_model.dart @@ -0,0 +1,9 @@ +import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; + +class TalkDataModel { + TalkData? talkData; + TalkDataH264Frame? talkDataH264Frame; + + TalkDataModel({required this.talkData, this.talkDataH264Frame}); +} diff --git a/lib/talk/starChart/handle/other/talk_data_repository.dart b/lib/talk/starChart/handle/other/talk_data_repository.dart index 062835cb..587a481f 100644 --- a/lib/talk/starChart/handle/other/talk_data_repository.dart +++ b/lib/talk/starChart/handle/other/talk_data_repository.dart @@ -1,9 +1,10 @@ import 'dart:async'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; class TalkDataRepository { TalkDataRepository._() { - _talkDataStreamController = StreamController.broadcast( + _talkDataStreamController = StreamController.broadcast( onListen: () { _isListening = true; }, @@ -18,13 +19,13 @@ class TalkDataRepository { static TalkDataRepository get instance => _instance; - late final StreamController _talkDataStreamController; + late final StreamController _talkDataStreamController; bool _isListening = false; // 直接返回原始流,不做转换 - Stream get talkDataStream => _talkDataStreamController.stream; + Stream get talkDataStream => _talkDataStreamController.stream; - void addTalkData(TalkData talkData) { + void addTalkData(TalkDataModel talkData) { if (_isListening) { _talkDataStreamController.add(talkData); } diff --git a/lib/talk/starChart/handle/scp_message_base_handle.dart b/lib/talk/starChart/handle/scp_message_base_handle.dart index 11328f46..495db676 100644 --- a/lib/talk/starChart/handle/scp_message_base_handle.dart +++ b/lib/talk/starChart/handle/scp_message_base_handle.dart @@ -15,6 +15,7 @@ import 'package:star_lock/talk/starChart/constant/payload_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/udp_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/handle/other/talke_data_over_time_timer_manager.dart'; @@ -55,10 +56,10 @@ class ScpMessageBaseHandle { // 处理出完整帧数据后的回调 final H264FrameHandler frameHandler = - H264FrameHandler(onCompleteFrame: (frameData) { + H264FrameHandler(onCompleteFrame: (TalkDataModel talkDataModel) { // 处理完整的帧数据 TalkDataRepository.instance.addTalkData( - TalkData(contentType: TalkData_ContentTypeE.H264, content: frameData), + talkDataModel, ); }); @@ -71,6 +72,7 @@ class ScpMessageBaseHandle { messageId: scpMessage.MessageId!, ); } + // 回复失败消息 void replyErrorMessage(ScpMessage scpMessage) { startChartManage.sendGenericRespErrorMessage( diff --git a/lib/talk/starChart/proto/talk_expect.pbenum.dart b/lib/talk/starChart/proto/talk_expect.pbenum.dart index d6b34250..10306109 100644 --- a/lib/talk/starChart/proto/talk_expect.pbenum.dart +++ b/lib/talk/starChart/proto/talk_expect.pbenum.dart @@ -19,12 +19,14 @@ class VideoTypeE extends $pb.ProtobufEnum { static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264'); static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE'); static const VideoTypeE VP8 = VideoTypeE._(3, _omitEnumNames ? '' : 'VP8'); + static const VideoTypeE H264_720P = VideoTypeE._(4, _omitEnumNames ? '' : 'H264_720P'); static const $core.List values = [ NONE_V, H264, IMAGE, VP8, + H264_720P, ]; static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 7ffed8e5..2d8b4529 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -45,6 +45,7 @@ import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pbserver.dart'; import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; import 'package:star_lock/tools/baseGetXController.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/deviceInfo_utils.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:uuid/uuid.dart'; @@ -112,8 +113,12 @@ class StartChartManage { RbcuConfirm? rbcuConfirm; final int _maxPayloadSize = 8 * 1024; // 分包大小 + int rotateAngle = 0; // 视频旋转角度 + int videoWidth = 0; // 视频宽度 + int videoHeight = 0; // 视频高度 + // 默认通话的期望数据格式 - TalkExpectReq _defaultTalkExpect = TalkConstant.ImageExpect; + TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect; String relayPeerId = ''; // 中继peerId @@ -227,20 +232,6 @@ class StartChartManage { /// 设置数据接收回调 _onReceiveData(_udpSocket!, Get.context!); - - // //ToDo: 增加对讲调试、正式可删除 - // // 每秒重置数据速率 - // Timer.periodic(Duration(seconds: 1), (Timer t) { - // UdpTalkDataHandler().resetDataRates(); - // // 更新调试信息 - // Provider.of(Get.context!, listen: false) - // .updateDebugInfo( - // UdpTalkDataHandler().getLastRecvDataRate() ~/ 1024, // 转换为KB - // UdpTalkDataHandler().getLastRecvPacketCount(), - // UdpTalkDataHandler().getLastSendDataRate() ~/ 1024, // 转换为KB - // UdpTalkDataHandler().getLastSendPacketCount(), - // ); - // }); }).catchError((error) { _log(text: 'Failed to bind UDP socket: $error'); }); @@ -419,17 +410,36 @@ class StartChartManage { /// 启动持续发送对讲请求 void startCallRequestMessageTimer({required String ToPeerId}) async { // 如果已经处于等待接听状态就不发送 - if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { - // 如果是h264则跳转至webview - if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { - Get.toNamed( - Routers.h264WebView, - ); - } else { - Get.toNamed( - Routers.starChartTalkView, - ); - } + // if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { + // // 如果是h264则跳转至webview + // if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { + // Get.toNamed( + // Routers.h264WebView, + // ); + // } else { + // Get.toNamed( + // Routers.starChartTalkView, + // ); + // } + // } + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + Get.toNamed( + Routers.h264WebView, + ); + } else if (isMJpeg) { + Get.toNamed( + Routers.starChartTalkView, + ); + } else { + Get.toNamed( + Routers.starChartTalkView, + ); } // 启动定时器持续发送对讲请求 talkRequestTimer ??= Timer.periodic( @@ -596,7 +606,7 @@ class StartChartManage { void startTalkRejectMessageTimer() async { try { int count = 0; - final int maxCount = 10; // 最大执行次数为10秒 + final int maxCount = 3; // 最大执行次数为10秒 talkRejectTimer ??= Timer.periodic( Duration(seconds: _defaultIntervalTime), @@ -622,6 +632,8 @@ class StartChartManage { stopCallRequestMessageTimer(); stopSendingRbcuInfoMessages(); stopSendingRbcuProBeMessages(); + stopTalkAcceptTimer(); + stopCallRequestMessageTimer(); // 取消定时器 talkePingOverTimeTimerManager.cancel(); @@ -720,6 +732,8 @@ class StartChartManage { stopCallRequestMessageTimer(); stopSendingRbcuInfoMessages(); stopSendingRbcuProBeMessages(); + stopTalkAcceptTimer(); + stopCallRequestMessageTimer(); // 取消定时器 talkePingOverTimeTimerManager.cancel(); talkDataOverTimeTimerManager.cancel(); @@ -1145,7 +1159,7 @@ class StartChartManage { } void reSetDefaultTalkExpect() { - _defaultTalkExpect = TalkConstant.ImageExpect; + _defaultTalkExpect = TalkConstant.H264Expect; } TalkExpectReq getDefaultTalkExpect() { @@ -1163,12 +1177,27 @@ class StartChartManage { } /// 修改预期接收到的数据 - void sendImageVideoAndG711AudioTalkExpectData() { - final talkExpectReq = TalkConstant.ImageExpect; + void sendOnlyH264VideoTalkExpectData() { + final talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.H264], + audioType: [], + ); changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( talkExpect: talkExpectReq); } + /// 修改预期接收到的数据 + void sendImageVideoAndG711AudioTalkExpectData() { + changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: TalkConstant.ImageExpect); + } + + /// 修改预期接收到的数据 + void sendH264VideoAndG711AudioTalkExpectData() { + changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: TalkConstant.H264Expect); + } + /// 发送远程开锁 void sendRemoteUnLockMessage({ required String bluetoothDeviceName, @@ -1198,6 +1227,16 @@ class StartChartManage { /// 销毁资源 void destruction() async { + // 先挂断 + final status = talkStatus.status; + if (status == TalkStatus.passiveCallWaitingAnswer || + status == TalkStatus.proactivelyCallWaitingAnswer || + status == TalkStatus.answeredSuccessfully || + status == TalkStatus.uninitialized) { + startTalkRejectMessageTimer(); + startTalkHangupMessageTimer(); + await Future.delayed(Duration(seconds: 1)); + } isOnlineStarChartServer = false; // 停止发送心跳消息 stopHeartbeat(); @@ -1225,7 +1264,6 @@ class StartChartManage { await Storage.removerStarChartRegisterNodeInfo(); // 关闭udp服务 closeUdpSocket(); - PacketLossStatistics().reset(); } /// 重置数据 diff --git a/lib/talk/starChart/status/appLifecycle_observer.dart b/lib/talk/starChart/status/appLifecycle_observer.dart index 37b70152..03a356d1 100644 --- a/lib/talk/starChart/status/appLifecycle_observer.dart +++ b/lib/talk/starChart/status/appLifecycle_observer.dart @@ -30,14 +30,13 @@ class AppLifecycleObserver extends WidgetsBindingObserver { // 处理应用程序进入后台的逻辑 final status = StartChartManage().talkStatus.status; - if (status == TalkStatus.passiveCallWaitingAnswer || status == TalkStatus.proactivelyCallWaitingAnswer || status == TalkStatus.answeredSuccessfully || status == TalkStatus.uninitialized) { - StartChartManage().destruction(); Get.back(); } + StartChartManage().destruction(); } void onAppResumed() async { diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart new file mode 100644 index 00000000..c9c42baf --- /dev/null +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart @@ -0,0 +1,675 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math'; // Import the math package to use sqrt +import 'dart:ui' show decodeImageFromList; + +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/g711.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.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_expect.pb.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_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:star_lock/tools/commonDataManage.dart'; +import 'package:star_lock/tools/storage.dart'; + +import '../../../../tools/baseGetXController.dart'; + +class ImageTransmissionLogic extends BaseGetXController { + ImageTransmissionState state = ImageTransmissionState(); + + final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; + + int bufferSize = 8; // 增大缓冲区,满时才渲染 + + int audioBufferSize = 2; // 音频默认缓冲2帧 + bool _isFirstAudioFrame = true; // 是否是第一帧 + + int _startAudioTime = 0; // 开始播放时间戳 + + // 定义音频帧缓冲和发送函数 + final List _bufferedAudioFrames = []; + + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + + Timer? videoRenderTimer; // 视频渲染定时器 + + int _renderedFrameCount = 0; + int _lastFpsPrintTime = 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(); + } + 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 contentType = talkData!.contentType; + final currentTime = DateTime.now().millisecondsSinceEpoch; + + // 判断数据类型,进行分发处理 + 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); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + break; + case TalkData_ContentTypeE.Image: + // 固定长度缓冲区,最多保留bufferSize帧 + state.videoBuffer.add(talkData); + if (state.videoBuffer.length > bufferSize) { + state.videoBuffer.removeAt(0); // 移除最旧帧 + } + break; + } + }); + } + + // 新增:音频帧播放逻辑 + 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.listData.value.length > 0) { + state.oneMinuteTime.value++; + // if (state.oneMinuteTime.value >= 60) { + // t.cancel(); // 取消定时器 + // state.oneMinuteTime.value = 0; + // // 倒计时结束挂断 + // // udpHangUpAction(); + // } + } + }); + 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(); + } + + /// 开门 + // udpOpenDoorAction() async { + // final List? privateKey = + // await Storage.getStringList(saveBluePrivateKey); + // final List getPrivateKeyList = changeStringListToIntList(privateKey!); + // + // final List? signKey = await Storage.getStringList(saveBlueSignKey); + // final List signKeyDataList = changeStringListToIntList(signKey!); + // + // final List? token = await Storage.getStringList(saveBlueToken); + // final List getTokenList = changeStringListToIntList(token!); + // + // await _getLockNetToken(); + // + // final OpenLockCommand openLockCommand = OpenLockCommand( + // lockID: BlueManage().connectDeviceName, + // userID: await Storage.getUid(), + // openMode: lockDetailState.openDoorModel, + // openTime: _getUTCNetTime(), + // onlineToken: lockDetailState.lockNetToken, + // token: getTokenList, + // needAuthor: 1, + // signKey: signKeyDataList, + // privateKey: getPrivateKeyList, + // ); + // final messageDetail = openLockCommand.packageData(); + // // 将 List 转换为十六进制字符串 + // String hexString = messageDetail + // .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + // .join(' '); + // + // AppLog.log('open lock hexString: $hexString'); + // // 发送远程开门消息 + // StartChartManage().sendRemoteUnLockMessage( + // bluetoothDeviceName: BlueManage().connectDeviceName, + // openLockCommand: messageDetail, + // ); + // showToast('正在开锁中...'.tr); + // } + + int _getUTCNetTime() { + if (lockDetailState.isHaveNetwork) { + return DateTime.now().millisecondsSinceEpoch ~/ 1000 + + lockDetailState.differentialTime; + } else { + return 0; + } + } + + /// 获取权限状态 + Future 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 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 startRecording() async { + // requestPermissions(); + // if (state.isRecordingScreen.value) { + // showToast('录屏已开始,请勿重复点击'); + // } + // bool start = await FlutterScreenRecording.startRecordScreen( + // "Screen Recording", // 视频文件名 + // titleNotification: "Recording in progress", // 通知栏标题 + // messageNotification: "Tap to stop recording", // 通知栏内容 + // ); + // + // if (start) { + // state.isRecordingScreen.value = true; + // } + } + + Future stopRecording() async { + // String path = await FlutterScreenRecording.stopRecordScreen; + // print("Recording saved to: $path"); + // + // // 将视频保存到系统相册 + // bool? success = await GallerySaver.saveVideo(path); + // if (success == true) { + // print("Video saved to gallery"); + // } else { + // print("Failed to save video to gallery"); + // } + // + // showToast('录屏结束,已保存到系统相册'); + // state.isRecordingScreen.value = false; + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onInit() { + super.onInit(); + + // 启动监听音视频数据流 + _startListenTalkData(); + // 启动监听对讲状态 + _startListenTalkStatus(); + // 在没有监听成功之前赋值一遍状态 + // *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 *** + state.talkStatus.value = state.startChartTalkStatus.status; + + // 初始化音频播放器 + _initFlutterPcmSound(); + + // 启动播放定时器 + // _startPlayback(); + + // 初始化录音控制器 + _initAudioRecorder(); + + requestPermissions(); + + // 启动视频渲染定时器(10fps) + videoRenderTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + final int now = DateTime.now().millisecondsSinceEpoch; + if (state.videoBuffer.isNotEmpty) { + final TalkData oldestFrame = state.videoBuffer.removeAt(0); + if (oldestFrame.content.isNotEmpty) { + state.listData.value = + Uint8List.fromList(oldestFrame.content); // 备份原始数据 + final int decodeStart = DateTime.now().millisecondsSinceEpoch; + decodeImageFromList(Uint8List.fromList(oldestFrame.content)) + .then((ui.Image img) { + final int decodeEnd = DateTime.now().millisecondsSinceEpoch; + state.currentImage.value = img; + _renderedFrameCount++; + // 每秒统计一次fps + if (now - _lastFpsPrintTime >= 1000) { + // print('实际渲染fps: $_renderedFrameCount'); + _renderedFrameCount = 0; + _lastFpsPrintTime = now; + } + }).catchError((e) { + print('图片解码失败: $e'); + }); + } + } + // 如果缓冲区为空,不做任何操作,保持上一次内容 + }); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + state.listData.value = Uint8List(0); // 清空视频数据 + state.audioBuffer.clear(); // 清空音频缓冲区 + state.videoBuffer.clear(); // 清空视频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + stopProcessingAudio(); + // 清理图片缓存 + // _imageCache.clear(); + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; + + super.onClose(); + } + + @override + void dispose() { + stopProcessingAudio(); + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; + super.dispose(); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() { + state.listData.value = Uint8List(0); + // 停止播放音频 + _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 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 remoteOpenLock() async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var lockId = currentKeyInfo.lockId ?? 0; + var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0; + + final lockPeerId = StartChartManage().lockPeerId; + 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; + } + } + } + } + }); + } + 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); + } + } + + /// 初始化音频录制器 + void _initAudioRecorder() { + state.voiceProcessor = VoiceProcessor.instance; + } + + //开始录音 + Future 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([_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 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 _onFrame(List frame) async { + // 添加最大缓冲限制 + if (_bufferedAudioFrames.length > state.frameLength * 3) { + _bufferedAudioFrames.clear(); // 清空过多积累的数据 + return; + } + + // 首先应用固定增益提升基础音量 + List amplifiedFrame = _applyGain(frame, 1.6); + // 编码为G711数据 + List 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 _applyGain(List pcmData, double gainFactor) { + List result = List.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; + } +} diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart new file mode 100644 index 00000000..60b023bd --- /dev/null +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_colors.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/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_logic.dart'; +import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_state.dart'; +import 'package:star_lock/tools/titleAppBar.dart'; +import 'package:slide_to_act/slide_to_act.dart'; + +// 可选:引入第三方滑动解锁库 +// import 'package:flutter_slider_button/flutter_slider_button.dart'; + +class ImageTransmissionPage extends StatefulWidget { + const ImageTransmissionPage(); + + @override + State createState() => _ImageTransmissionPageState(); +} + +class _ImageTransmissionPageState extends State + with TickerProviderStateMixin { + final ImageTransmissionLogic logic = Get.put(ImageTransmissionLogic()); + final ImageTransmissionState state = Get.find().state; + final startChartManage = StartChartManage(); + + @override + void initState() { + super.initState(); + state.animationController = AnimationController( + vsync: this, // 确保使用的TickerProvider是当前Widget + duration: const Duration(seconds: 1), + ); + state.animationController.repeat(); + 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 + void dispose() { + state.animationController.dispose(); + CallTalk().finishAVData(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.mainBackgroundColor, + resizeToAvoidBottomInset: false, + appBar: TitleAppBar( + barTitle: '图传'.tr, + haveBack: true, + backgroundColor: AppColors.mainColor, + backAction: () { + logic.udpHangUpAction(); + }, + ), + body: Obx(() => Column( + children: [ + SizedBox(height: 24.h), + SizedBox( + height: 0.6.sh, + child: state.listData.value.isEmpty + ? _buildWaitingView() + : _buildVideoView(), + ), + SizedBox(height: 30.h), + _buildBottomToolBar(), + SizedBox(height: 30.h), + ], + )), + ); + } + + Widget _buildWaitingView() { + double barWidth = MediaQuery.of(context).size.width - 60.w; + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(30.h), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: barWidth, + height: double.infinity, + child: Image.asset( + 'images/main/monitorBg.png', + fit: BoxFit.cover, + ), + ), + RotationTransition( + turns: state.animationController, + child: Image.asset( + 'images/main/realTime_connecting.png', + width: 300.w, + height: 300.w, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ); + } + + Widget _buildVideoView() { + double barWidth = MediaQuery.of(context).size.width - 60.w; + return PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(30.h), + child: Container( + width: barWidth, + height: double.infinity, + child: RotatedBox( + quarterTurns: startChartManage.rotateAngle ~/ 90, + child: RawImage( + image: state.currentImage.value, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildBottomToolBar() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 30.w), + padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30.h), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _circleButton( + icon: Icons.call, + color: Colors.green, + onTap: () { + if (state.talkStatus.value == + TalkStatus.passiveCallWaitingAnswer) { + // 接听 + logic.initiateAnswerCommand(); + } + }, + ), + _circleButton( + icon: Icons.call_end, + color: Colors.red, + onTap: () { + logic.udpHangUpAction(); + }, + ), + _circleButton( + icon: Icons.camera_alt, + color: Colors.blue, + onTap: () async { + await logic.captureAndSavePng(); + }, + ), + ], + ), + SizedBox(height: 36.h), + SlideAction( + height: 64.h, + borderRadius: 24.h, + elevation: 0, + innerColor: Colors.amber, + outerColor: Colors.amber.withOpacity(0.15), + sliderButtonIcon: Icon(Icons.lock, color: Colors.white, size: 40.w), + text: '滑动解锁', + textStyle: TextStyle( + fontSize: 26.sp, + color: Colors.black54, + fontWeight: FontWeight.bold), + onSubmit: () { + // TODO: 实现滑动解锁逻辑 + logic.remoteOpenLock(); + }, + ), + ], + ), + ); + } + + Widget _circleButton( + {required IconData icon, + required Color color, + required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 90.w, + height: 90.w, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.3), + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Icon(icon, color: Colors.white, size: 48.w), + ), + ); + } +} diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_state.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_state.dart new file mode 100644 index 00000000..a7cd0efc --- /dev/null +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_state.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +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/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 '../../../../tools/storage.dart'; + +enum NetworkStatus { + normal, // 0 + lagging, // 1 + delayed, // 2 + packetLoss // 3 +} + +class ImageTransmissionState{ + int udpSendDataFrameNumber = 0; // 帧序号 + // var isSenderAudioData = false.obs;// 是否要发送音频数据 + + Future userMobileIP = NetworkInfo().getWifiIP(); + Future userUid = Storage.getUid(); + + RxInt udpStatus = + 0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话 + TextEditingController passwordTF = TextEditingController(); + + Rx listData = Uint8List(0).obs; //得到的视频流字节数据 + RxList listAudioData = [].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; + + late Timer autoBackTimer = + Timer(const Duration(seconds: 1), () {}); //发送30秒监视后自动返回 + late Timer realTimePicTimer = + Timer(const Duration(seconds: 1), () {}); //监视命令定时器 + RxInt elapsedSeconds = 0.obs; + + // 星图对讲相关状态 + List audioBuffer = [].obs; + List activeAudioBuffer = [].obs; + List activeVideoBuffer = [].obs; + + List videoBuffer = [].obs; + List videoBuffer2 = [].obs; + RxBool isPlaying = false.obs; // 是否开始播放 + Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 + // 获取 startChartTalkStatus 的唯一实例 + final StartChartTalkStatus startChartTalkStatus = + StartChartTalkStatus.instance; + + // 通话数据流的单例流数据处理类 + final TalkDataRepository talkDataRepository = TalkDataRepository.instance; + RxInt lastFrameTimestamp = 0.obs; // 上一帧的时间戳,用来判断网络环境 + Rx networkStatus = + NetworkStatus.normal.obs; // 网络状态:0-正常 1-网络卡顿 2-网络延迟 3-网络丢包 + RxInt alertCount = 0.obs; // 网络状态提示计数器 + RxInt maxAlertNumber = 3.obs; // 网络状态提示最大提示次数 + RxBool isOpenVoice = true.obs; // 是否打开声音 + RxBool isRecordingScreen = false.obs; // 是否录屏中 + RxBool isRecordingAudio = false.obs; // 是否录音中 + Rx startRecordingAudioTime = DateTime.now().obs; // 开始录音时间 + Rx endRecordingAudioTime = DateTime.now().obs; // 结束录音时间 + RxInt recordingAudioTime = 0.obs; // 录音时间持续时间 + RxInt fps = 0.obs; // 添加 FPS 计数 + late VoiceProcessor? voiceProcessor; // 音频处理器、录音 + final int frameLength = 320; //录音视频帧长度为640 + final int sampleRate = 8000; //录音频采样率为8000 + List recordingAudioAllFrames = []; // 录制音频的所有帧 + List lockRecordingAudioAllFrames = []; // 录制音频的所有帧 + RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) + RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) + RxBool hasAudioData = false.obs; // 是否有音频数据 + RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 + Rx currentImage = Rx(null); +} \ No newline at end of file diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart new file mode 100644 index 00000000..9ddf4a57 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -0,0 +1,1322 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math'; // Import the math package to use sqrt + +import 'package:flutter/cupertino.dart'; +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: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'; + +class TalkViewNativeDecodeLogic extends BaseGetXController { + final TalkViewNativeDecodeState state = TalkViewNativeDecodeState(); + + final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; + + int bufferSize = 25; // 初始化为默认大小 + + int audioBufferSize = 2; // 音频默认缓冲2帧 + + // 定义音频帧缓冲和发送函数 + final List _bufferedAudioFrames = []; + + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + + Timer? _batchProcessTimer; + + // 添加一个集合来跟踪已成功解码的I帧序号 + final Set _decodedIFrames = {}; + + // 写入前的缓存队列(I帧前) + final List> _preIFrameCache = []; + bool _hasWrittenFirstIFrame = false; + + // 新增:SPS/PPS状态追踪变量 + bool hasSps = false; + bool hasPps = false; + + // 新增:SPS/PPS缓存 + List? spsCache; + List? ppsCache; + + // 新增:记录上一个已接收的frameSeq + int? _lastFrameSeq; + + // 新增:frameSeq回绕检测标志 + bool _pendingStreamReset = false; + + // 新增:记录切换时的宽高参数 + int _pendingResetWidth = 864; + int _pendingResetHeight = 480; + + // 新增:等待新I帧状态 + bool _waitingForIFrame = false; + + // 初始化视频解码器 + Future _initVideoDecoder() async { + try { + state.isLoading.value = true; + // 创建解码器配置 + final config = VideoDecoderConfig( + width: 864, + // 实际视频宽度 + height: 480, + codecType: 'h264', + ); + // 初始化解码器并获取textureId + final textureId = await VideoDecodePlugin.initDecoder(config); + if (textureId != null) { + Future.microtask(() => state.textureId.value = textureId); + AppLog.log('视频解码器初始化成功:textureId=$textureId'); + VideoDecodePlugin.setOnFrameRenderedListener((textureId) { + AppLog.log('已经开始渲染======='); + // 只有真正渲染出首帧时才关闭loading + Future.microtask(() => state.isLoading.value = false); + }); + } else { + AppLog.log('视频解码器初始化失败'); + } + // 启动定时器发送帧数据 + _startFrameProcessTimer(); + } catch (e) { + AppLog.log('初始化视频解码器错误: $e'); + // 如果初始化失败,延迟后重试 + await Future.delayed(const Duration(seconds: 2)); + if (!Get.isRegistered()) { + return; // 如果控制器已经被销毁,不再重试 + } + _initVideoDecoder(); // 重试初始化 + } + } + + /// 初始化音频播放器 + 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(); + } + VideoDecodePlugin.releaseDecoder(); + Get.back(); + } + + /// 添加H264帧到缓冲区 + void _addFrameToBuffer( + List frameData, + TalkDataH264Frame_FrameTypeE frameType, + int pts, + int frameSeq, + int frameSeqI, + ) { + // 检测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; + } + // 创建包含帧数据和类型的Map + final Map frameMap = { + 'frameData': frameData, + 'frameType': frameType, + 'frameSeq': frameSeq, + 'frameSeqI': frameSeqI, + 'pts': pts, + }; + + // 如果缓冲区超出最大大小,优先丢弃P/B帧 + while (state.h264FrameBuffer.length >= state.maxFrameBufferSize) { + int pbIndex = state.h264FrameBuffer + .indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P); + if (pbIndex != -1) { + state.h264FrameBuffer.removeAt(pbIndex); + } else { + state.h264FrameBuffer.removeAt(0); + } + } + + // 将帧添加到缓冲区 + state.h264FrameBuffer.add(frameMap); + } + + /// 启动帧处理定时器 + void _startFrameProcessTimer() { + // 取消已有定时器 + state.frameProcessTimer?.cancel(); + + // 计算定时器间隔,确保以目标帧率处理帧 + final int intervalMs = (1000 / state.targetFps).round(); + + // 创建新定时器 + state.frameProcessTimer = + Timer.periodic(Duration(milliseconds: intervalMs), (timer) { + _processNextFrameFromBuffer(); + }); + AppLog.log('启动帧处理定时器,目标帧率: ${state.targetFps}fps,间隔: ${intervalMs}ms'); + } + + /// 从缓冲区处理下一帧 + void _processNextFrameFromBuffer() async { + // 避免重复处理 + if (state.isProcessingFrame) { + return; + } + + // 如果缓冲区为空,跳过 + if (state.h264FrameBuffer.isEmpty) { + return; + } + + // 设置正在处理标志 + state.isProcessingFrame = true; + + try { + // 取出最早的帧 + final Map? frameMap = state.h264FrameBuffer.isNotEmpty + ? state.h264FrameBuffer.removeAt(0) + : null; + if (frameMap == null) { + state.isProcessingFrame = false; + return; + } + final List? 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; + } + await VideoDecodePlugin.sendFrame( + frameData: frameData, + frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, + frameSeq: frameSeq, + timestamp: pts, + splitNalFromIFrame: true, + refIFrameSeq: frameSeqI, + ); + } catch (e) { + AppLog.log('处理缓冲帧失败: $e'); + } finally { + // 重置处理标志 + state.isProcessingFrame = false; + } + } + + /// 停止帧处理定时器 + void _stopFrameProcessTimer() { + state.frameProcessTimer?.cancel(); + state.frameProcessTimer = null; + state.h264FrameBuffer.clear(); + state.isProcessingFrame = false; + } + + // 发起接听命令 + 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: + // 处理H264帧 + if (state.textureId.value != null) { + if (talkDataH264Frame != null) { + _addFrameToBuffer( + talkData.content, + talkDataH264Frame.frameType, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + } + } else { + AppLog.log('无法处理H264帧:textureId为空'); + } + break; + } + }); + } + + // 新增:音频帧播放逻辑 + 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 && state.isLoading.isFalse) { + 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 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 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 startRecording() async {} + + Future stopRecording() async {} + + @override + void onReady() { + super.onReady(); + } + + @override + void onInit() { + super.onInit(); + + // 启动监听音视频数据流 + _startListenTalkData(); + // 启动监听对讲状态 + _startListenTalkStatus(); + // 在没有监听成功之前赋值一遍状态 + // *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 *** + state.talkStatus.value = state.startChartTalkStatus.status; + + // 初始化音频播放器 + _initFlutterPcmSound(); + + // 初始化录音控制器 + _initAudioRecorder(); + + requestPermissions(); + + // 初始化视频解码器 + _initVideoDecoder(); + + _initHdOptions(); + // 初始化H264帧缓冲区 + state.h264FrameBuffer.clear(); + state.isProcessingFrame = false; + } + + @override + void onClose() { + // _closeH264File(); + // 停止帧处理定时器 + _stopFrameProcessTimer(); + + _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(); + Future.microtask(() => state.textureId.value = null); + } + + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + VideoDecodePlugin.releaseDecoder(); + + // 取消批处理定时器 + _batchProcessTimer?.cancel(); + _batchProcessTimer = null; + + // 清空已解码I帧集合 + _decodedIFrames.clear(); + + super.onClose(); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() { + // 停止播放音频 + _stopPlayG711Data(); + stopProcessingAudio(); + } + + /// 更新发送预期数据 + void updateTalkExpect() { + // 清晰度与VideoTypeE的映射 + final Map qualityToVideoType = { + '标清': VideoTypeE.H264, + '高清': VideoTypeE.H264_720P, + // 可扩展更多清晰度 + }; + TalkExpectReq talkExpectReq = TalkExpectReq(); + state.isOpenVoice.value = !state.isOpenVoice.value; + // 根据当前清晰度动态设置videoType + VideoTypeE currentVideoType = qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264; + if (!state.isOpenVoice.value) { + talkExpectReq = TalkExpectReq( + videoType: [currentVideoType], + audioType: [], + ); + showToast('已静音'.tr); + } else { + talkExpectReq = TalkExpectReq( + videoType: [currentVideoType], + audioType: [AudioTypeE.G711], + ); + } + + /// 修改发送预期数据 + StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: talkExpectReq); + } + + /// 截图并保存到相册 + Future 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 remoteOpenLock() async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var lockId = currentKeyInfo.lockId ?? 0; + var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0; + + final lockPeerId = StartChartManage().lockPeerId; + 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; + } + } + } + } + }); + } + 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); + } + } + + /// 初始化音频录制器 + void _initAudioRecorder() { + state.voiceProcessor = VoiceProcessor.instance; + } + + //开始录音 + Future 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([_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 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 _onFrame(List frame) async { + // 添加最大缓冲限制 + if (_bufferedAudioFrames.length > state.frameLength * 3) { + _bufferedAudioFrames.clear(); // 清空过多积累的数据 + return; + } + + // 首先应用固定增益提升基础音量 + List amplifiedFrame = _applyGain(frame, 1.6); + // 编码为G711数据 + List 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 _applyGain(List pcmData, double gainFactor) { + List result = List.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; + } + + /// 追加写入一帧到h264文件(需传入帧数据和帧类型frameType) + Future _appendH264FrameToFile( + List frameData, TalkDataH264Frame_FrameTypeE frameType) async { + try { + if (state.h264File == null) { + await _initH264File(); + } + // NALU分割函数,返回每个NALU的完整字节数组 + List> splitNalus(List data) { + List> nalus = []; + int i = 0; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + return nalus; + } + + // 优化:I帧前只缓存SPS/PPS/IDR,首次写入严格顺序 + if (!_hasWrittenFirstIFrame) { + final nalus = splitNalus(frameData); + List> spsList = []; + List> ppsList = []; + List> idrList = []; + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 7) { + spsList.add(nalu); + // AppLog.log('SPS内容: ' + + // nalu + // .map((b) => b.toRadixString(16).padLeft(2, '0')) + // .join(' ')); + } else if (naluType == 8) { + ppsList.add(nalu); + // AppLog.log('PPS内容: ' + + // nalu + // .map((b) => b.toRadixString(16).padLeft(2, '0')) + // .join(' ')); + } else if (naluType == 5) { + idrList.add(nalu); + } + // 其他类型不缓存也不写入头部 + } + } + // 只在首次I帧写入前缓存,严格顺序写入 + if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) { + for (final sps in spsList) { + await _writeSingleFrameToFile(_ensureStartCode(sps)); + // AppLog.log('写入顺序: SPS'); + } + for (final pps in ppsList) { + await _writeSingleFrameToFile(_ensureStartCode(pps)); + // AppLog.log('写入顺序: PPS'); + } + for (final idr in idrList) { + await _writeSingleFrameToFile(_ensureStartCode(idr)); + // AppLog.log('写入顺序: IDR'); + } + _hasWrittenFirstIFrame = true; + } else { + // 未收齐SPS/PPS/IDR则继续缓存,等待下次I帧 + if (spsList.isNotEmpty) _preIFrameCache.addAll(spsList); + if (ppsList.isNotEmpty) _preIFrameCache.addAll(ppsList); + if (idrList.isNotEmpty) _preIFrameCache.addAll(idrList); + } + } else { + // 首帧后只写入IDR和P帧 + final nalus = splitNalus(frameData); + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 5) { + await _writeSingleFrameToFile(_ensureStartCode(nalu)); + // AppLog.log('写入顺序: IDR'); + } else if (naluType == 1) { + await _writeSingleFrameToFile(_ensureStartCode(nalu)); + // AppLog.log('写入顺序: P帧'); + } else if (naluType == 7) { + // AppLog.log('遇到新SPS,已忽略'); + } else if (naluType == 8) { + // AppLog.log('遇到新PPS,已忽略'); + } + // 其他类型不写入 + } + } + } + } catch (e) { + AppLog.log('写入H264帧到文件失败: $e'); + } + } + + // 统一NALU起始码为0x00000001 + List _ensureStartCode(List nalu) { + if (nalu.length >= 4 && + nalu[0] == 0x00 && + nalu[1] == 0x00 && + nalu[2] == 0x00 && + nalu[3] == 0x01) { + return nalu; + } else if (nalu.length >= 3 && + nalu[0] == 0x00 && + nalu[1] == 0x00 && + nalu[2] == 0x01) { + return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3); + } else { + return [0x00, 0x00, 0x00, 0x01] + nalu; + } + } + + /// 实际写入单帧到文件(带NALU头判断) + Future _writeSingleFrameToFile(List frameData) async { + bool hasNaluHeader = false; + if (frameData.length >= 4 && + frameData[0] == 0x00 && + frameData[1] == 0x00 && + ((frameData[2] == 0x01) || + (frameData[2] == 0x00 && frameData[3] == 0x01))) { + hasNaluHeader = true; + } + if (hasNaluHeader) { + await state.h264File!.writeAsBytes(frameData, mode: FileMode.append); + } else { + final List naluHeader = [0x00, 0x00, 0x01]; + await state.h264File! + .writeAsBytes(naluHeader + frameData, mode: FileMode.append); + } + } + + /// 初始化h264文件 + Future _initH264File() async { + try { + if (state.h264File != null) return; + // 获取Download目录 + Directory? downloadsDir; + if (Platform.isAndroid) { + // Android 10+ 推荐用getExternalStorageDirectory() + downloadsDir = await getExternalStorageDirectory(); + // 兼容部分ROM,优先用Download + final downloadPath = '/storage/emulated/0/Download'; + if (Directory(downloadPath).existsSync()) { + downloadsDir = Directory(downloadPath); + } + } else { + downloadsDir = await getApplicationDocumentsDirectory(); + } + final filePath = + '${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264'; + state.h264FilePath = filePath; + state.h264File = File(filePath); + if (!await state.h264File!.exists()) { + await state.h264File!.create(recursive: true); + } + AppLog.log('H264文件初始化: $filePath'); + } catch (e) { + AppLog.log('H264文件初始化失败: $e'); + } + } + + /// 关闭h264文件 + Future _closeH264File() async { + try { + if (state.h264File != null) { + AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}'); + } + state.h264File = null; + state.h264FilePath = null; + _preIFrameCache.clear(); + _hasWrittenFirstIFrame = false; + } catch (e) { + AppLog.log('关闭H264文件时出错: $e'); + } + } + + /// 从I帧数据中分割NALU并将SPS/PPS优先放入缓冲区(用于缓冲区发送) + void _extractAndBufferSpsPpsForBuffer( + List frameData, int durationMs, int frameSeq, int frameSeqI) { + List> splitNalus(List data) { + List> nalus = []; + int i = 0; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + return nalus; + } + + final nalus = splitNalus(frameData); + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 7) { + // SPS + hasSps = true; + // 只在首次或内容变化时更新缓存 + if (spsCache == null || !_listEquals(spsCache!, nalu)) { + spsCache = List.from(nalu); + } + } else if (naluType == 8) { + // PPS + hasPps = true; + if (ppsCache == null || !_listEquals(ppsCache!, nalu)) { + ppsCache = List.from(nalu); + } + } + } + } + } + + // 新增:List比较工具 + bool _listEquals(List a, List b) { + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + + // 新增:I帧处理方法 + void _handleIFrameWithSpsPpsAndIdr( + List frameData, int durationMs, int frameSeq, int frameSeqI) { + // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) + state.h264FrameBuffer.clear(); + _extractAndBufferSpsPpsForBuffer( + frameData, durationMs, frameSeq, frameSeqI); + // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR) + if (spsCache == null || ppsCache == null) { + // 没有SPS/PPS缓存,丢弃本次I帧 + return; + } + // 先写入SPS/PPS + _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); + _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); + // 分割I帧包,只写入IDR(type 5) + List> nalus = []; + int i = 0; + List data = frameData; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 5) { + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); + } + } + } + } + + // 新增:P帧处理方法 + void _handlePFrame( + List frameData, int durationMs, int frameSeq, int frameSeqI) { + // 只写入P帧(type 1) + List> nalus = []; + int i = 0; + List data = frameData; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 1) { + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, + frameSeq, frameSeqI); + } + } + } + } + + // 切换清晰度的方法,后续补充具体实现 + 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 _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'); + } + } +} diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart new file mode 100644 index 00000000..f4a7eb13 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -0,0 +1,534 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:io'; + +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/star_chart_manage.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 createState() => + _TalkViewNativeDecodePageState(); +} + +class _TalkViewNativeDecodePageState extends State + with TickerProviderStateMixin { + final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic()); + final TalkViewNativeDecodeState state = + Get.find().state; + final startChartManage = StartChartManage(); + + @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: [ + // 悬浮帧率统计信息条 + 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); // 选择较大的缩放比例 + // 防御性处理:只要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, + ), + ), + ), + ), + ), + ); + } + }, + ), + + Obx(() => state.isLoading.isTrue + ? Positioned( + bottom: 310.h, + child: Text( + '正在创建安全连接...'.tr, + style: TextStyle(color: Colors.black, fontSize: 26.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: [ + 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: [ + 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: [ + 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: [ + // 打开关闭声音 + 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'), + ), + ), + ), + 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 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), + ), + ), + ]); + } + + Widget bottomBottomBtnWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 接听 + 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: [ + 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(); + } +} diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart new file mode 100644 index 00000000..8d176500 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -0,0 +1,123 @@ +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 userMobileIP = NetworkInfo().getWifiIP(); + Future userUid = Storage.getUid(); + + RxInt udpStatus = + 0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话 + TextEditingController passwordTF = TextEditingController(); + + RxList listAudioData = [].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 audioBuffer = [].obs; + + RxBool isLoading = true.obs; // 是否在加载 + RxBool isPlaying = false.obs; // 是否开始播放 + Rx 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 startRecordingAudioTime = DateTime.now().obs; // 开始录音时间 + Rx 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 textureId = Rx(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> frameTracker = {}; + + // H264帧缓冲区相关 + final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 + final int maxFrameBufferSize = 15; // 最大缓冲区大小 + final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 + Timer? frameProcessTimer; // 帧处理定时器 + bool isProcessingFrame = false; // 是否正在处理帧 + int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 + // H264文件保存相关 + String? h264FilePath; + File? h264File; + + // 当前清晰度选项,初始为'高清' + RxString currentQuality = '高清'.obs; // 可选:高清、标清、流畅 +} diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index 779e44e9..cfe0d6f2 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'dart:math'; // Import the math package to use sqrt +import 'dart:ui' show decodeImageFromList; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -23,12 +24,15 @@ import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/network/api_repository.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/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.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/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 '../../../../tools/baseGetXController.dart'; @@ -37,32 +41,24 @@ class TalkViewLogic extends BaseGetXController { final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; - final int minBufferSize = 2; // 最小缓冲2帧,约166ms - final int maxBufferSize = 20; // 最大缓冲8帧,约666ms - int bufferSize = 8; // 初始化为默认大小 - // 修改音频相关的成员变量 - final int minAudioBufferSize = 1; // 音频最小缓冲1帧 - final int maxAudioBufferSize = 3; // 音频最大缓冲3帧 - int audioBufferSize = 2; // 音频默认缓冲2帧 + int bufferSize = 8; // 增大缓冲区,满时才渲染 - // 添加开始时间记录 - int _startTime = 0; // 开始播放时间戳 - int _startAudioTime = 0; // 开始播放时间戳 - bool _isFirstFrame = true; // 是否是第一帧 + int audioBufferSize = 2; // 音频默认缓冲2帧 bool _isFirstAudioFrame = true; // 是否是第一帧 + int _startAudioTime = 0; // 开始播放时间戳 + // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; - final Map _imageCache = {}; + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; - // 添加一个变量用于记录上一帧的时间戳 - int _lastFrameTimestamp = 0; // 初始值为 0 + Timer? videoRenderTimer; // 视频渲染定时器 - // 添加帧率计算相关变量 - int _frameCount = 0; - int _lastFpsUpdateTime = 0; - Timer? _fpsTimer; + int _renderedFrameCount = 0; + int _lastFpsPrintTime = DateTime.now().millisecondsSinceEpoch; /// 初始化音频播放器 void _initFlutterPcmSound() { @@ -96,18 +92,28 @@ class TalkViewLogic extends BaseGetXController { // 监听音视频数据流 void _startListenTalkData() { - state.talkDataRepository.talkDataStream.listen((TalkData talkData) async { - final contentType = talkData.contentType; + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; final currentTime = DateTime.now().millisecondsSinceEpoch; // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: // // 第一帧到达时记录开始时间 - // if (_isFirstAudioFrame) { - // _startAudioTime = currentTime; - // _isFirstAudioFrame = false; - // } + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } // 计算音频延迟 final expectedTime = _startAudioTime + talkData.durationMs; @@ -129,80 +135,16 @@ class TalkViewLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.Image: - // 第一帧到达时记录开始时间 - if (_isFirstFrame) { - _startTime = currentTime; - _isFirstFrame = false; - AppLog.log('第一帧帧的时间戳:${talkData.durationMs}'); - } - // AppLog.log('其他帧的时间戳:${talkData.durationMs}'); - // 计算帧间间隔 - if (_lastFrameTimestamp != 0) { - final int frameInterval = talkData.durationMs - _lastFrameTimestamp; - _adjustBufferSize(frameInterval); // 根据帧间间隔调整缓冲区 - } - _lastFrameTimestamp = talkData.durationMs; // 更新上一帧时间戳 - - // 然后添加到播放缓冲区 - if (state.videoBuffer.length >= bufferSize) { - state.videoBuffer.removeAt(0); - } + // 固定长度缓冲区,最多保留bufferSize帧 state.videoBuffer.add(talkData); - // 先进行解码和缓存 - await _decodeAndCacheFrame(talkData); - // 最后尝试播放 - _playVideoFrames(); + if (state.videoBuffer.length > bufferSize) { + state.videoBuffer.removeAt(0); // 移除最旧帧 + } break; } }); } - // 修改:视频帧播放逻辑 - void _playVideoFrames() { - // 如果缓冲区为空或未达到目标大小,不进行播放 - if (state.videoBuffer.isEmpty || state.videoBuffer.length < bufferSize) { - // AppLog.log('📊 缓冲中 - 当前缓冲区大小: ${state.videoBuffer.length}/${bufferSize}'); - return; - } - // 找出时间戳最小的帧(最旧的帧) - TalkData? oldestFrame; - int oldestIndex = -1; - for (int i = 0; i < state.videoBuffer.length; i++) { - if (oldestFrame == null || - state.videoBuffer[i].durationMs < oldestFrame.durationMs) { - oldestFrame = state.videoBuffer[i]; - oldestIndex = i; - } - } - // 确保找到了有效帧 - if (oldestFrame != null && oldestIndex != -1) { - final cacheKey = oldestFrame.content.hashCode.toString(); - - // 使用缓存的解码图片更新显示 - if (_imageCache.containsKey(cacheKey)) { - state.currentImage.value = _imageCache[cacheKey]; - state.listData.value = Uint8List.fromList(oldestFrame.content); - state.videoBuffer.removeAt(oldestIndex); // 移除已播放的帧 - - // // 更新帧率计算 - // _frameCount++; - // final currentTime = DateTime.now().millisecondsSinceEpoch; - // final elapsed = currentTime - _lastFpsUpdateTime; - // - // if (elapsed >= 1000) { - // // 每秒更新一次 - // state.fps.value = (_frameCount * 1000 / elapsed).round(); - // _frameCount = 0; - // _lastFpsUpdateTime = currentTime; - // } - - } else { - // AppLog.log('⚠️ 帧未找到缓存 - Key: $cacheKey'); - state.videoBuffer.removeAt(oldestIndex); // 移除无法播放的帧 - } - } - } - // 新增:音频帧播放逻辑 void _playAudioFrames() { // 如果缓冲区为空或未达到目标大小,不进行播放 @@ -233,50 +175,6 @@ class TalkViewLogic extends BaseGetXController { } } - // 新增:解码和缓存帧的方法 - Future _decodeAndCacheFrame(TalkData talkData) async { - try { - String cacheKey = talkData.content.hashCode.toString(); - - // 如果该帧还没有被缓存,则进行解码和缓存 - if (!_imageCache.containsKey(cacheKey)) { - final Uint8List uint8Data = Uint8List.fromList(talkData.content); - final ui.Image image = await decodeImageFromList(uint8Data); - - // 管理缓存大小 - if (_imageCache.length >= bufferSize) { - _imageCache.remove(_imageCache.keys.first); - } - - // 添加到缓存 - _imageCache[cacheKey] = image; - - // AppLog.log('📥 缓存新帧 - 缓存数: ${_imageCache.length}, Key: $cacheKey'); - } - } catch (e) { - AppLog.log('❌ 帧解码错误: $e'); - } - } - - // 新增:动态调整缓冲区大小的方法 - void _adjustBufferSize(int frameInterval) { - const int frameDuration = 83; // 假设每帧的时间间隔为 83ms(12fps) - const int delayThresholdHigh = frameDuration * 2; // 高延迟阈值(2帧时间) - const int delayThresholdLow = frameDuration; // 低延迟阈值(1帧时间) - const int adjustInterval = 1; // 每次调整1帧 - - if (frameInterval > delayThresholdHigh && bufferSize < maxBufferSize) { - // 帧间间隔较大,增加缓冲区 - bufferSize = min(bufferSize + adjustInterval, maxBufferSize); - AppLog.log('📈 增加缓冲区 - 当前大小: $bufferSize, 帧间间隔: ${frameInterval}ms'); - } else if (frameInterval < delayThresholdLow && - bufferSize > minBufferSize) { - // 帧间间隔较小,减少缓冲区 - bufferSize = max(bufferSize - adjustInterval, minBufferSize); - AppLog.log('📉 减少缓冲区 - 当前大小: $bufferSize, 帧间间隔: ${frameInterval}ms'); - } - } - /// 监听对讲状态 void _startListenTalkStatus() { state.startChartTalkStatus.statusStream.listen((talkStatus) { @@ -382,20 +280,6 @@ class TalkViewLogic extends BaseGetXController { } } - // 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口 - Future _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 getPermissionStatus() async { final Permission permission = Permission.microphone; @@ -498,6 +382,31 @@ class TalkViewLogic extends BaseGetXController { requestPermissions(); + // 启动视频渲染定时器(10fps) + videoRenderTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + final int now = DateTime.now().millisecondsSinceEpoch; + if (state.videoBuffer.isNotEmpty) { + final TalkData oldestFrame = state.videoBuffer.removeAt(0); + if (oldestFrame.content.isNotEmpty) { + state.listData.value = Uint8List.fromList(oldestFrame.content); // 备份原始数据 + final int decodeStart = DateTime.now().millisecondsSinceEpoch; + decodeImageFromList(Uint8List.fromList(oldestFrame.content)).then((ui.Image img) { + final int decodeEnd = DateTime.now().millisecondsSinceEpoch; + state.currentImage.value = img; + _renderedFrameCount++; + // 每秒统计一次fps + if (now - _lastFpsPrintTime >= 1000) { + // print('实际渲染fps: $_renderedFrameCount'); + _renderedFrameCount = 0; + _lastFpsPrintTime = now; + } + }).catchError((e) { + print('图片解码失败: $e'); + }); + } + } + // 如果缓冲区为空,不做任何操作,保持上一次内容 + }); } @override @@ -512,10 +421,17 @@ class TalkViewLogic extends BaseGetXController { stopProcessingAudio(); // 清理图片缓存 - _imageCache.clear(); + // _imageCache.clear(); state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 state.oneMinuteTimeTimer = null; // 取消旧定时器 state.oneMinuteTime.value = 0; + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; super.onClose(); } @@ -525,6 +441,9 @@ class TalkViewLogic extends BaseGetXController { stopProcessingAudio(); // 重置期望数据 StartChartManage().reSetDefaultTalkExpect(); + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; super.dispose(); } @@ -597,34 +516,40 @@ class TalkViewLogic extends BaseGetXController { // 远程开锁 Future 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); } } diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index 38b22343..7abb28b1 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -12,6 +12,7 @@ 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/star_chart_manage.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'; @@ -30,6 +31,7 @@ class _TalkViewPageState extends State final TalkViewLogic logic = Get.put(TalkViewLogic()); final TalkViewState state = Get.find().state; late Stream _latencyStream; + final startChartManage = StartChartManage(); @override void initState() { @@ -96,60 +98,55 @@ class _TalkViewPageState extends State child: Stack( alignment: Alignment.center, children: [ - 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.listData.value.isEmpty - ? 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( - quarterTurns: -1, - child: Obx( - () => state.currentImage.value != null - ? RawImage( - image: state.currentImage.value, - width: ScreenUtil().scaleWidth, - height: ScreenUtil().scaleHeight, - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - ) - : Container(color: Colors.transparent), - ), - ), - ), - ), - ); - }, - ), + // 全屏背景图片或渐变色背景 + Obx(() { + if (state.listData.value.isEmpty) { + return SizedBox.expand( + child: Image.asset( + 'images/main/monitorBg.png', + fit: BoxFit.cover, + ), + ); + } + final int videoW = startChartManage.videoWidth; + final int videoH = startChartManage.videoHeight; + if (videoW == 320 && videoH == 240) { + return SizedBox.expand( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF232526), + Color(0xFF414345), + ], + ), + ), + ), + ); + } + return const SizedBox.shrink(); + }), + // 视频窗口,分辨率判断 + Obx(() { + if (state.listData.value.isEmpty) { + return const SizedBox.shrink(); + } + final int videoW = startChartManage.videoWidth; + final int videoH = startChartManage.videoHeight; + if (videoW == 320 && videoH == 240) { + return Positioned( + top: 150.h, + left: 0, + right: 0, + child: _buildVideoWidget(), + ); + } else { + // 直接全屏显示 + return _buildVideoWidget(); + } + }), Obx(() => state.listData.value.isEmpty ? Positioned( bottom: 310.h, @@ -158,35 +155,35 @@ class _TalkViewPageState extends State style: TextStyle(color: Colors.black, fontSize: 26.sp), )) : Container()), - Obx( - () => state.listData.value.isNotEmpty && - 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: [ - Text( - '$min:$sec', - style: TextStyle( - fontSize: 26.sp, color: Colors.white), - ), - ], - ); - }, - ), - ) - : Container(), - ), + Obx(() => + state.listData.value.isNotEmpty && 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: [ + Text( + '$min:$sec', + style: TextStyle( + fontSize: 26.sp, color: Colors.white), + ), + ], + ); + }, + ), + ) + : Container()), + + /// 工具栏 Positioned( bottom: 10.w, child: Container( @@ -456,7 +453,6 @@ class _TalkViewPageState extends State // if (state.talkStatus.value == TalkStatus.answeredSuccessfully && // state.listData.value.length > 0) { // logic.udpOpenDoorAction(); - logic.remoteOpenLock(); // } // if (UDPManage().remoteUnlock == 1) { // logic.udpOpenDoorAction(); @@ -464,6 +460,7 @@ class _TalkViewPageState extends State // } else { // logic.showToast('请在锁设置中开启远程开锁'.tr); // } + logic.remoteOpenLock(); }, ) ]); @@ -618,4 +615,68 @@ class _TalkViewPageState extends State // UdpTalkDataHandler().resetDataRates(); super.dispose(); } + + Widget _buildVideoWidget() { + // 工具栏宽度 + double barWidth = 1.sw - 30.w * 2; + int videoW = startChartManage.videoWidth; + int videoH = startChartManage.videoHeight; + int quarterTurns = startChartManage.rotateAngle ~/ 90; + bool isRotated = quarterTurns % 2 == 1; + // 旋转后宽高互换 + double videoAspect = isRotated ? videoW / videoH : videoH / videoW; + double containerHeight = + barWidth * (isRotated ? videoW / videoH : videoH / videoW); + + if (videoW == 320 && videoH == 240) { + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(20.h), + child: Container( + width: barWidth, + height: containerHeight, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF232526), + Color(0xFF414345), + ], + ), + ), + child: RotatedBox( + quarterTurns: quarterTurns, + child: RawImage( + image: state.currentImage.value, + fit: BoxFit.contain, + filterQuality: FilterQuality.high, + width: barWidth, + height: containerHeight, + ), + ), + ), + ), + ); + } else { + return PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + quarterTurns: startChartManage.rotateAngle ~/ 90, + child: RawImage( + image: state.currentImage.value, + width: ScreenUtil().scaleWidth, + height: ScreenUtil().scaleHeight, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + ), + ), + ), + ), + ); + } + } } diff --git a/lib/talk/starChart/views/talkView/talk_view_state.dart b/lib/talk/starChart/views/talkView/talk_view_state.dart index aafc8605..02f83efb 100644 --- a/lib/talk/starChart/views/talkView/talk_view_state.dart +++ b/lib/talk/starChart/views/talkView/talk_view_state.dart @@ -90,6 +90,5 @@ class TalkViewState { RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) RxBool hasAudioData = false.obs; // 是否有音频数据 RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 - // 添加图片状态变量 - final Rx currentImage = Rx(null); + Rx currentImage = Rx(null); } diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index fdbaad3f..b7307383 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -25,7 +25,9 @@ import 'package:star_lock/network/api_repository.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/talkView/talk_view_state.dart'; @@ -44,14 +46,20 @@ class H264WebViewLogic extends BaseGetXController { // 添加模拟数据相关变量 static const int CHUNK_SIZE = 4096; Timer? _mockDataTimer; + int _startAudioTime = 0; // 开始播放时间戳 + int audioBufferSize = 2; // 音频默认缓冲2帧 + bool _isFirstAudioFrame = true; // 是否是第一帧 // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; final Queue> _frameBuffer = Queue>(); static const int FRAME_BUFFER_SIZE = 25; + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + @override void onInit() { - super.onInit(); // 初始化 WebView 控制器 state.webViewController = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -63,18 +71,40 @@ class H264WebViewLogic extends BaseGetXController { }, ); - state.isShowLoading.value = true; - // 加载本地 HTML - _loadLocalHtml(); + super.onInit(); // 创建流数据监听 _createFramesStreamListen(); - // playLocalTestVideo(); _startListenTalkStatus(); state.talkStatus.value = state.startChartTalkStatus.status; // 初始化音频播放器 _initFlutterPcmSound(); + // 初始化录音控制器 _initAudioRecorder(); + + // 加载本地 HTML + _loadLocalHtml(); + + // playLocalTestVideo(); + + requestPermissions(); + } + + Future 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(); // 跳转到应用设置页面 + } + } } /// 初始化音频录制器 @@ -96,16 +126,66 @@ class H264WebViewLogic extends BaseGetXController { } void _createFramesStreamListen() async { - state.talkDataRepository.talkDataStream.listen((TalkData event) async { - // 添加新帧到缓冲区 - _frameBuffer.add(event.content); + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } - // 当缓冲区超过最大容量时,发送最早的帧并移除 - while (_frameBuffer.length > FRAME_BUFFER_SIZE) { - if (_frameBuffer.isNotEmpty) { - final frame = _frameBuffer.removeFirst(); - await _sendBufferedData(frame); - } + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; + final currentTime = DateTime.now().millisecondsSinceEpoch; + + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + if (state.isShowLoading.isFalse) { + // // 第一帧到达时记录开始时间 + 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); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + } + + break; + case TalkData_ContentTypeE.H264: + // // 添加新帧到缓冲区 + _frameBuffer.add(talkData.content); + + // 当缓冲区超过最大容量时,发送最早的帧并移除 + while (_frameBuffer.length > FRAME_BUFFER_SIZE) { + if (_frameBuffer.isNotEmpty) { + final frame = _frameBuffer.removeFirst(); + await _sendBufferedData(frame); + } + if (state.isShowLoading.isTrue) { + state.isShowLoading.value = false; + } + } + break; } }); } @@ -134,6 +214,51 @@ class H264WebViewLogic extends BaseGetXController { // } // } + // 新增:音频帧播放逻辑 + 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 _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; + } + } + } + /// 加载html文件 Future _loadLocalHtml() async { // 加载 HTML 文件内容 @@ -186,10 +311,10 @@ class H264WebViewLogic extends BaseGetXController { Timer.periodic(const Duration(seconds: 1), (Timer t) { if (state.isShowLoading.isFalse) { state.oneMinuteTime.value++; - if (state.oneMinuteTime.value >= 60) { - t.cancel(); // 取消定时器 - state.oneMinuteTime.value = 0; - } + // if (state.oneMinuteTime.value >= 60) { + // t.cancel(); // 取消定时器 + // state.oneMinuteTime.value = 0; + // } } }); break; @@ -321,7 +446,7 @@ class H264WebViewLogic extends BaseGetXController { } // 首先应用固定增益提升基础音量 - List amplifiedFrame = _applyGain(frame, 1.6); + List amplifiedFrame = _applyGain(frame, 1.8); // 编码为G711数据 List encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law _bufferedAudioFrames.addAll(encodedData); @@ -409,7 +534,7 @@ class H264WebViewLogic extends BaseGetXController { }); final LockSetInfoEntity lockSetInfoEntity = - await ApiRepository.to.getLockSettingInfoData( + await ApiRepository.to.getLockSettingInfoData( lockId: lockId.toString(), ); if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { @@ -427,6 +552,38 @@ class H264WebViewLogic extends BaseGetXController { } } + /// 停止播放音频 + void _stopPlayG711Data() async { + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + + state.audioBuffer.clear(); // 清空音频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + // 停止播放音频 + stopProcessingAudio(); + + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + + super.onClose(); + } @override void dispose() { diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index 51aa30d8..f0f11885 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/app_settings/app_settings.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/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; @@ -25,6 +26,7 @@ class _H264WebViewState extends State with TickerProviderStateMixin { final H264WebViewLogic logic = Get.put(H264WebViewLogic()); final H264WebViewState state = Get.find().state; + final startChartManage = StartChartManage(); @override void initState() { @@ -71,8 +73,13 @@ class _H264WebViewState extends State height: screenHeight, fit: BoxFit.cover, ) - : WebViewWidget( - controller: state.webViewController, + : SizedBox.expand( + child: RotatedBox( + quarterTurns: startChartManage.rotateAngle ~/ 90, + child: WebViewWidget( + controller: state.webViewController, + ), + ), ); }), Obx( @@ -411,10 +418,13 @@ class _H264WebViewState extends State ), ); } + @override void dispose() { - state.animationController.dispose(); // 确保释放控制器 - super.dispose(); + state.animationController.dispose(); + CallTalk().finishAVData(); + // UdpTalkDataHandler().resetDataRates(); + super.dispose(); } } diff --git a/lib/talk/starChart/webView/h264_web_view_state.dart b/lib/talk/starChart/webView/h264_web_view_state.dart index 2ae11041..671e805c 100644 --- a/lib/talk/starChart/webView/h264_web_view_state.dart +++ b/lib/talk/starChart/webView/h264_web_view_state.dart @@ -5,6 +5,7 @@ import 'package:flutter_voice_processor/flutter_voice_processor.dart'; import 'package:get/get.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.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:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -49,4 +50,6 @@ class H264WebViewState { RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) RxBool hasAudioData = false.obs; // 是否有音频数据 RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 + List audioBuffer = [].obs; + RxBool isPlaying = false.obs; // 是否开始播放 } diff --git a/lib/tools/eventBusEventManage.dart b/lib/tools/eventBusEventManage.dart index 810ad0a6..4c4fbb3d 100755 --- a/lib/tools/eventBusEventManage.dart +++ b/lib/tools/eventBusEventManage.dart @@ -1,4 +1,5 @@ import 'package:event_bus/event_bus.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import '../main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; @@ -195,6 +196,7 @@ class RogerThatLockInfoDataEvent { class GetGatewayListRefreshUI { GetGatewayListRefreshUI(); } + /// 同意隐私协议 class AgreePrivacyAgreement { AgreePrivacyAgreement(); @@ -204,3 +206,10 @@ class AgreePrivacyAgreement { class SuccessfulDistributionNetwork { SuccessfulDistributionNetwork(); } + +/// 设置锁列表数据 +class SetLockListInfoGroupEntity { + SetLockListInfoGroupEntity({required this.lockListInfoGroupEntity}); + + LockListInfoGroupEntity lockListInfoGroupEntity; +} diff --git a/notify.sh b/notify.sh index b881f57f..0a5ecf90 100755 --- a/notify.sh +++ b/notify.sh @@ -2,13 +2,13 @@ set -e APP_PRODUCT_NAME=$APP_PRODUCT_NAME BUILD_STATUS=$1 -if [[ "${CI_COMMIT_BRANCH}" == "release" ]] ; then +if [[ "${CI_COMMIT_BRANCH}" == "release_sky" ]] ; then WECAHT_WEBHOOK_URL=$PRE_QYWECAHT_WEBHOOK_URL SKY_IOS_DOWNLOAD_URL=$PRE_SKY_IOS_DOWNLOAD_URL SKY_ANDROID_DOWNLOAD_URL=$PRE_SKY_ANDROID_DOWNLOAD_URL XHJ_IOS_DOWNLOAD_URL=$PRE_XHJ_IOS_DOWNLOAD_URL XHJ_ANDROID_DOWNLOAD_URL=$PRE_XHJ_ANDROID_DOWNLOAD_URL -elif [[ "${CI_COMMIT_BRANCH}" == "develop" ]] || [[ "${CI_COMMIT_BRANCH}" == "feat_devops" ]]; then +elif [[ "${CI_COMMIT_BRANCH}" == "develop_sky" ]] || [[ "${CI_COMMIT_BRANCH}" == "feat_devops_sky" ]]; then WECAHT_WEBHOOK_URL=$DEV_QYWECAHT_WEBHOOK_URL SKY_IOS_DOWNLOAD_URL=$DEV_SKY_IOS_DOWNLOAD_URL SKY_ANDROID_DOWNLOAD_URL=$DEV_SKY_ANDROID_DOWNLOAD_URL diff --git a/pubspec.yaml b/pubspec.yaml index 7819d6e8..36b08c64 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -127,7 +127,10 @@ dependencies: sdk: flutter aliyun_face_plugin: path: aliyun_face_plugin - + video_decode_plugin: + git: + url: git@code.star-lock.cn:liyi/video_decode_plugin.git + ref: 68bb4b7fb637ef5a78856908e1bc464f50fe967a flutter_localizations: sdk: flutter @@ -277,6 +280,8 @@ dependencies: video_thumbnail: ^0.5.3 # 角标管理 flutter_app_badger: ^1.3.0 + # 滑块支持 + slide_to_act: ^2.0.2 diff --git a/tag_generator.sh b/tag_generator.sh index 27e39182..5fd79429 100755 --- a/tag_generator.sh +++ b/tag_generator.sh @@ -1,63 +1,106 @@ #!/bin/bash -# 读取环境变量 -URL=$CI_API_V4_URL -TOKEN=$GITLAB_ACCESS_TOKEN -PROJECT_ID=$CI_PROJECT_ID -next_tag="" -newest_tag="" +# ======================== +# tag_generator.sh +# 用于自动识别并递增以 _sky 结尾的 tag(如 v1.5.556_sky),并生成下一个 tag。 +# 递增规则:feat: 提交递增 minor,fix: 或其他提交递增 patch。 +# ======================== + +# 读取环境变量(由 CI/CD 系统注入) +URL=$CI_API_V4_URL # GitLab API v4 地址 +TOKEN=$GITLAB_ACCESS_TOKEN # GitLab 访问 Token +PROJECT_ID=$CI_PROJECT_ID # 当前项目 ID +next_tag="" # 新的 tag 变量 +newest_tag="" # 最新 tag 变量 + echo "PRIVATE-TOKEN: $TOKEN $URL/projects/$PROJECT_ID/repository/tags" +# 获取项目所有 tag 的 json 列表 +# 需要 jq 工具解析 json +# tags_json 是所有 tag 的原始 json 数据 tags_json=$(curl -H "Content-Type: application/json" -H "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/tags") -#echo "tags_json:$tags_json\n" +# 提取所有 tag 名称,按时间倒序排列(最新的在前) tags=$(echo "$tags_json" | jq -r '.[].name') -tags_length=$(echo "$tags_json" | jq -r 'length') -if [ "$tags_length" -lt 1 ]; then - next_tag="v1.0.0" + +# 只保留以 _sky 结尾的 tag(即只处理 v1.2.3_sky 这种格式的 tag) +sky_tags=$(echo "$tags" | grep '_sky$') +# 取最新的 _sky tag(即第一个) +newest_sky_tag=$(echo "$sky_tags" | head -n 1) + +# 如果没有 _sky 结尾的 tag,则从最新的 tag 提取版本号,生成 vX.Y.Z_sky +if [ -z "$newest_sky_tag" ]; then + # 取最新的 tag(不管是否带_sky) + latest_tag=$(echo "$tags" | head -n 1) + if [ -n "$latest_tag" ]; then + # 提取版本号部分(去掉前缀v和后缀_sky等) + version_part=${latest_tag#v} # 去掉v + version_part=${version_part%_sky} # 去掉_sky(如果有) + IFS='.' read -r major minor patch <<< "$version_part" + next_tag="v$major.$minor.$patch_sky" + else + next_tag="v1.0.0_sky" + fi else - newest_tag=$(echo "$tags" | head -n 1) - IFS='.' read -r major minor patch <<< "$newest_tag" - major="${major#v}" - compare_json="" + # 解析版本号部分(去掉 _sky 后缀和 v 前缀) + # 例如 v1.5.556_sky -> 1.5.556 + version_part=${newest_sky_tag%_sky} # 去掉 _sky 后缀 + version_part=${version_part#v} # 去掉 v 前缀 + IFS='.' read -r major minor patch <<< "$version_part" # 拆分出主、次、修订号 + compare_json="" # 用于存储 commit 对比结果 + # 判断命令参数,决定对比范围 if [[ "$1" == "generate_tag" ]];then - echo "generate_tag:$newest_tag-to-master\n" - compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master") + # 生成 tag 时,对比最新 _sky tag 和 master 之间的提交 + echo "generate_tag:$newest_sky_tag-to-master\n" + compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_sky_tag&to=master") elif [[ "$1" == "generate_version" ]]; then - echo "generate_version:master-to-$CI_COMMIT_BRANCH\n" + # 生成版本号时,对比 master 和当前分支之间的提交 + echo "generate_version:master-to-$CI_COMMIT_BRANCH\n" compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master&to=$CI_COMMIT_BRANCH") fi echo "compare_json:$compare_json\n" - new_patch=$patch - new_minor=$minor + new_patch=$patch # 新的 patch 号 + new_minor=$minor # 新的 minor 号 + # 遍历所有 commit,根据提交信息递增版本号 while IFS= read -r commit_json; do - # 使用 jq 解析每一行的 JSON 对象 + # 解析每个 commit 的 id 和 message commit_id=$(echo "$commit_json" | jq -r '.id') commit_message=$(echo "$commit_json" | jq -r '.message') echo "----$commit_message" + # 如果有 feat: 类型提交,minor 递增(只递增一次) if [[ "$commit_message" =~ ("feat:"*) ]] && [[ $new_minor == $minor ]]; then ((new_minor++)) -# new_patch=0 -# break + # 如果有 fix: 类型提交,patch 递增 elif [[ "$commit_message" =~ ("fix:"*) ]]; then ((new_patch++)) + # 其他类型提交(非 Merge/Revert),patch 递增 elif [[ ! "$commit_message" =~ ("Merge"* | "Revert"*) ]]; then ((new_patch++)) fi done < <(echo "$compare_json" | jq -c '.commits[] | {id: .id, message: .message}') - next_tag="v$major.$new_minor.$new_patch" + # 组装新的 tag,格式为 v.._sky + next_tag="v$major.$new_minor.$new_patch_sky" fi -echo "New Tag:$newest_tag;New version: $next_tag;command: $1" + +echo "New Tag:$newest_sky_tag;New version: $next_tag;command: $1" + +# 如果是 generate_tag 命令,且新 tag 和最新 tag 一致,则跳过生成 if [[ "$1" == "generate_tag" ]];then - if [ "$next_tag" == "$newest_tag" ]; then + if [ "$next_tag" == "$newest_sky_tag" ]; then echo "no change from master,skip to generate tag" exit 0 fi + # 配置 git 用户名(可根据需要补充邮箱等) git config user.name + # 创建并推送新 tag git tag $next_tag git push -u origin $next_tag echo "generate tag: $next_tag" elif [[ "$1" == "generate_version" ]]; then + # 如果是 generate_version 命令,仅导出新版本号到环境变量 export NEXT_VERSION="$next_tag" echo "generate version: $NEXT_VERSION" fi +# 无论哪种情况,都把新 tag 写入 app_new.version 文件 +# 供后续流程使用 + echo "$next_tag" > app_new.version