Merge branch 'develop_liyi' into canary_release

This commit is contained in:
Liuyf 2025-03-14 10:21:03 +08:00
commit 52dc86018d
13 changed files with 353 additions and 366 deletions

Binary file not shown.

View File

@ -1145,5 +1145,6 @@
"2.在APP里开启锁的远程开锁功能这个功能默认是关闭的。如果没有这个选项则锁不支持Google Home": "2. Enable the remote unlocking function of the lock in the APP (this function is turned off by default). If this option is not available, the lock will not support Google Home",
"3.安装Google Home APP点击左上角的加号按钮": "3. Install the Google Home app and click the plus button in the upper left corner",
"网关通电后长按重置按钮5秒蓝色指示灯闪烁时点击下一步": "After the gateway is powered on, press and hold the reset button for 5 seconds. Click Next when the blue indicator light flashes",
"暂无最新记录": "There are currently no latest records available",
"网关添加成功": "Gateway added successfully"
}

View File

@ -1,10 +1,10 @@
{
"星锁": "星鎖",
"锁通通": "鎖定直通",
"点击开锁,长按闭锁": "掂解鎖,按住鎖定",
"星锁": "星鎖",
"锁通通": "鎖通",
"点击开锁,长按闭锁": "點擊開鎖,長按閉鎖",
"考勤": "出席",
"考勤设置": "考勤設置",
"电子钥匙": "eKey",
"电子钥匙": "電子鑰匙",
"添加卡": "添加卡",
"卡号": "卡號",
"添加指纹": "添加指紋",
@ -185,7 +185,7 @@
"退出": "註銷",
"删除账号": "刪除帳戶",
"个人信息": "賬戶信息",
"头像": "化身",
"头像": "頭像",
"昵称": "暱稱",
"请输入昵称": "請輸入您的暱稱",
"修改昵称": "重命名",
@ -373,7 +373,7 @@
"未打卡": "暫無記錄",
"钥匙将在": "此ekey將在",
"天后失效": "天",
"电量更新时间:": "電更新時間:",
"电量更新时间:": "電更新時間:",
"新增配件": "加",
"钥匙不可用": "密鑰不可用",
"正在开锁中...": "解鎖。。。",
@ -505,7 +505,7 @@
"您的钥匙已过期": "您的密鑰已過期",
"常开模式开启": "鎖處於Passage Mode",
"超级管理员": "超級管理員",
"授权管理员": "設為admin",
"授权管理员": "授權管理員",
"普通用户": "普通用戶",
"余": "平衡",
"天": "日",
@ -718,7 +718,7 @@
"钥匙无效": "密鑰無效",
"操作失败,请确认锁是否在附近,或重启手机蓝牙后再试。": "無法連接到鎖。請重新啟動手機嘅Blutooth並重試。",
"如果是全自动锁,请使屏幕变亮": "如果係全自動鎖,請讓屏幕更光",
"正在尝试闭锁……": "嘗試鎖定。 請稍候。。。",
"正在尝试闭锁……": "正在嘗試閉鎖……",
"清空记录": "清除記錄",
"是否要删除操作记录?": "繼續刪除記錄?",
"被删除的记录不能恢复": "刪除後無法恢復記錄。",
@ -815,7 +815,7 @@
"配置网络": "配置網絡",
"你好": "你好",
"成功": "成功的",
"类型选择": "鍵入select",
"类型选择": "類型選擇",
"请选择要使用哪种类型": "請選擇要使用的類型",
"系统邮件(推荐)": "系統電子郵件(推薦)",
"系统短信(推荐)": "系統短信(推薦)",
@ -1100,8 +1100,8 @@
"英语": "英文",
"Google Home操作流程的值": "1.使用智能鎖APP添加鎖和網關\n\n2.喺APP中開啟鎖嘅遠程解鎖功能此功能默認關閉。 如果冇此選項鎖唔撐Google Home\n\n3.安裝Google Home APP點擊左上角嘅“+”按鈕\n\n4.在“設置”頁面上選擇“與Google合作”\n\n5.搜索“ScienerSmart”使用智能鎖APP賬號和密碼進行授權",
"密码需至少包含数字/字母/字符中的2种组合": "密碼必須至少包含以下2個數字、字母同特殊字符",
"已开锁": "鎖",
"已闭锁": "鎖",
"已开锁": "已開鎖",
"已闭锁": "已閉鎖",
"两次密码不一致哦": "密碼不一緻",
"中功率": "中等功率",
"常规使用": "經常使用",

View File

@ -1148,5 +1148,6 @@
"4.在设置页面选择与Google协同工作": "4.在设置页面选择与Google协同工作",
"5.搜索": "5.搜索",
"并用智能锁APP的账号和密码进行授权": "并用智能锁APP的账号和密码进行授权",
"暂无最新记录": "暂无最新记录",
"网关添加成功": "网关添加成功"
}

View File

@ -1,6 +1,6 @@
{
"星锁": "星鎖",
"锁通通": "鎖定通過",
"星锁": "星鎖",
"锁通通": "鎖通通",
"点击开锁,长按闭锁": "觸摸以解鎖,按住以鎖定",
"考勤": "出席情況",
"考勤设置": "考勤設置",
@ -267,9 +267,9 @@
"您可通过邮件将密码、电子钥匙信息发给接收人。": "電子郵件可用於向收件人發送密碼和ekey信息。",
"购买实名认证提示": "啟用該功能後,您需要使用指紋,臉部或帳戶密碼才能打開該應用程序。 3分鐘不用再驗證",
"请选择你希望的实名认证频次": "請選擇您想要的實名認證頻率",
"仅首次": "第一次",
"仅首次": "僅首次",
"每日一次": "每天一次",
"每周一次": "每一次",
"每周一次": "每一次",
"每月一次": "每月一次",
"当前状态": "當前狀態",
"试用中": "在審判中",
@ -1081,8 +1081,8 @@
"重置后,该锁的掌静脉都将被删除哦,确认要重置吗?": "重置後,鎖的掌靜脈將被刪除。 是否確實要重置?",
"在线": "在線",
"离线": "離線",
"购买记录": "購記錄",
"使用记录": "記錄",
"购买记录": "記錄",
"使用记录": "使用記錄",
"失效时间要大于当前时间": "過期時間必須長於當前時間",
"修改名字": "編輯名稱",
"时": "小時",

View File

@ -1148,5 +1148,6 @@
"4.在设置页面选择与Google协同工作": "4.在设置页面选择与Google协同工作",
"5.搜索": "5.搜索",
"并用智能锁APP的账号和密码进行授权": "并用智能锁APP的账号和密码进行授权",
"暂无最新记录": "暂无最新记录",
"网关添加成功": "网关添加成功"
}

View File

@ -1,4 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -87,7 +87,8 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
onTap: () {
state.isIphoneType.value = true;
},
child: Obx(() => Container(
child: Obx(
() => Container(
width: 170.w,
height: 60.h,
decoration: state.isIphoneType.value
@ -99,39 +100,52 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
width: 1.0, color: AppColors.greyLineColor))
: null,
child: Center(
child: Text(
'手机'.tr,
style: TextStyle(
color: state.isIphoneType.value
? Colors.white
: Colors.black),
)))),
child: Text(
'手机'.tr,
style: TextStyle(
color: state.isIphoneType.value
? Colors.white
: Colors.black),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () {
state.isIphoneType.value = false;
},
child: Obx(() => Container(
child: Obx(
() => Container(
height: 60.h,
// color: Colors.red,
decoration: state.isIphoneType.value
? null
: BoxDecoration(
color: AppColors.mainColor,
borderRadius:
BorderRadius.all(Radius.circular(30.h)),
borderRadius: BorderRadius.all(
Radius.circular(
30.h,
),
),
border: Border.all(
width: 1.0,
color: AppColors.greyLineColor)),
width: 1.0,
color: AppColors.greyLineColor,
),
),
child: Center(
child: Text(
'邮箱'.tr,
style: TextStyle(
child: Text(
'邮箱'.tr,
style: TextStyle(
color: state.isIphoneType.value
? Colors.black
: Colors.white),
)))),
: Colors.white,
),
),
),
),
),
),
),
],
@ -157,8 +171,7 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
children: <Widget>[
SizedBox(width: 5.w),
Expanded(
child: Text(
'你所在的国家/地区'.tr,
child: Text('你所在的国家/地区'.tr,
style: TextStyle(
fontSize: 26.sp, color: AppColors.blackColor))),
SizedBox(width: 20.w),
@ -213,8 +226,7 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
height: 30.w,
),
),
hintText:
state.isIphoneType.value ? '请输入手机号'.tr : '请输入邮箱'.tr,
hintText: state.isIphoneType.value ? '请输入手机号'.tr : '请输入邮箱'.tr,
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
// FilteringTextInputFormatter.allow(RegExp('[0-9]')),
@ -236,7 +248,7 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
height: 30.w,
),
),
hintText:'请输入密码'.tr,
hintText: '请输入密码'.tr,
inputFormatters: <TextInputFormatter>[
LengthLimitingTextInputFormatter(20),
]),
@ -282,8 +294,7 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
height: 30.w,
),
),
hintText:
'请输入验证码'.tr,
hintText: '请输入验证码'.tr,
inputFormatters: <TextInputFormatter>[
LengthLimitingTextInputFormatter(20),
]),
@ -292,29 +303,29 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
width: 20.w,
),
Obx(() => GestureDetector(
onTap:
(state.canSendCode.value && state.canResend.value)
? () async {
// Navigator.pushNamed(context, Routers.safetyVerificationPage, arguments: {"countryCode":"+86", "account":state.phoneOrEmailStr.value});
final Object? result = await Navigator.pushNamed(
context, Routers.safetyVerificationPage,
arguments: <String, Object>{
'countryCode': state.countryCode,
'account': state.phoneOrEmailStr.value
});
state.xWidth.value =
(result! as Map<String, dynamic>)['xWidth'];
logic.sendValidationCode();
}
: null,
onTap: (state.canSendCode.value && state.canResend.value)
? () async {
// Navigator.pushNamed(context, Routers.safetyVerificationPage, arguments: {"countryCode":"+86", "account":state.phoneOrEmailStr.value});
final Object? result = await Navigator.pushNamed(
context, Routers.safetyVerificationPage,
arguments: <String, Object>{
'countryCode': state.countryCode,
'account': state.phoneOrEmailStr.value
});
state.xWidth.value =
(result! as Map<String, dynamic>)['xWidth'];
logic.sendValidationCode();
}
: null,
child: Container(
width: 180.w,
// height: 60.h,
padding: EdgeInsets.all(10.h),
decoration: BoxDecoration(
color: (state.canSendCode.value && state.canResend.value)
? AppColors.mainColor
: Colors.grey,
color:
(state.canSendCode.value && state.canResend.value)
? AppColors.mainColor
: Colors.grey,
borderRadius: BorderRadius.circular(5)),
child: Center(
child: Text(state.btnText.value,
@ -361,29 +372,29 @@ class _StarLockRegisterPageState extends State<StarLockRegisterPage> {
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
child: Text(
'${'用户协议'.tr}',
child: Text('${'用户协议'.tr}',
style: TextStyle(
color: AppColors.mainColor, fontSize: 20.sp)),
onTap: () {
Get.toNamed(Routers.webviewShowPage, arguments: <String, String>{
'url': XSConstantMacro.userAgreementURL,
'title': '用户协议'.tr
});
Get.toNamed(Routers.webviewShowPage,
arguments: <String, String>{
'url': XSConstantMacro.userAgreementURL,
'title': '用户协议'.tr
});
},
)),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: GestureDetector(
child: Text(
'${'隐私政策'.tr}',
child: Text('${'隐私政策'.tr}',
style: TextStyle(
color: AppColors.mainColor, fontSize: 20.sp)),
onTap: () {
Get.toNamed(Routers.webviewShowPage, arguments: <String, String>{
'url': XSConstantMacro.privacyPolicyURL,
'title': '隐私政策'.tr
});
Get.toNamed(Routers.webviewShowPage,
arguments: <String, String>{
'url': XSConstantMacro.privacyPolicyURL,
'title': '隐私政策'.tr
});
},
)),
],

View File

@ -531,7 +531,8 @@ class _SendElectronicKeyViewState extends State<SendElectronicKeyView>
SizedBox(
height: 10.h,
),
if (logic.emailOrPhone != null)
if (logic.emailOrPhone != null &&
logic.state.currentLanguage.value == 'zh_CN')
OutLineBtn(
btnName: '微信通知'.tr,
onClick: () {

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_native_contact_picker/flutter_native_contact_picker.dart';
import 'package:get/get.dart';
import 'package:star_lock/tools/dateTool.dart';
import 'package:star_lock/translations/current_locale_tool.dart';
class SendElectronicKeyViewState {
//
@ -42,4 +43,7 @@ class SendElectronicKeyViewState {
final String permanentTips = '接收者可以使用此App开关锁'.tr; //
final String onceLimitTips = '单次钥匙有效期为1小时只能使用一次'.tr; //
final String cycleLimitTips = '接收者可以在有效期内的固定时间段里,不限次数使用'.tr;
RxString currentLanguage =
CurrentLocaleTool.getCurrentLocaleString().obs; //
}

View File

@ -71,6 +71,7 @@ class _ValueAddedServicesRecordPageState
//
class _PurchaseRecords extends StatelessWidget {
const _PurchaseRecords({required this.buyRecordList, required this.logic});
final List<UseItemData> buyRecordList;
final ValueAddedServicesRecordLogic logic;
@ -134,6 +135,7 @@ class _PurchaseRecords extends StatelessWidget {
// 使
class _UseRecordsTable extends StatelessWidget {
const _UseRecordsTable({required this.useRecordList, required this.logic});
final List<UseItemData> useRecordList;
final ValueAddedServicesRecordLogic logic;
@ -172,9 +174,12 @@ class _UseRecordsTable extends StatelessWidget {
Text(
logic.state.useCountStr.value,
style: TextStyle(
fontSize: 24.sp,
color: AppColors.blackColor,
fontWeight: FontWeight.bold),
fontSize: 24.sp,
color: AppColors.blackColor,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis, //
maxLines: 1, //
),
Expanded(child: Container()),
if (itemData.authStatus == 0)

View File

@ -43,7 +43,7 @@ class ScpMessageBaseHandle {
// messageId
static Map<String, List<List<int>>> _packetBuffer = {};
final Map<String, Timer> _packetTimers = {};
final Duration _timeoutDuration = Duration(seconds: 10); //
final Duration _timeoutDuration = Duration(seconds: 3); //
//
final TalkDataRepository talkDataRepository = TalkDataRepository.instance;
@ -106,40 +106,104 @@ class ScpMessageBaseHandle {
required int payloadType,
}) {
//
String key = '$messageId-$payloadType';
if (!_packetBuffer.containsKey(key)) {
_packetBuffer[key] = List.filled(spTotal, []);
_startTimer(key);
}
// 使key生成方式
final key = '${messageId}_$payloadType';
//
// AppLog.log(
// '📦 收到分包 - MessageId: $messageId, 总包数: $spTotal, 当前包序号: $spIndex');
//
if (spIndex < 1 || spIndex > spTotal) {
// print(
// 'Invalid spTotal: $spTotal spIndex: $spIndex for messageId: $messageId');
AppLog.log(
'❌ 分包序号异常 - MessageId: $messageId, 总包数: $spTotal, 无效包序号: $spIndex');
return null;
}
// ()
if (spIndex < 1 || spIndex > spTotal) return null;
// (使)
var packets = _packetBuffer[key];
if (packets == null) {
// ,
packets = List<List<int>>.filled(spTotal, const [], growable: false);
_packetBuffer[key] = packets;
_startTimer(key);
// AppLog.log('📝 新建分包缓存 - MessageId: $messageId, 预期总包数: $spTotal');
}
//
_packetBuffer[key]![spIndex - 1] = byte;
packets[spIndex - 1] = byte;
//
if (_packetBuffer[key]!.every((packet) => packet.isNotEmpty)) {
//
Uint8List completePayload = Uint8List.fromList(
_packetBuffer[key]!.expand((packet) => packet).toList());
//
// ,使every
var isComplete = true;
var totalLength = 0;
for (var i = 0; i < packets.length; i++) {
if (packets[i].isEmpty) {
isComplete = false;
} else {
totalLength += packets[i].length;
}
}
if (isComplete) {
// buffer,
final buffer = Uint8List(totalLength);
var offset = 0;
// ,使expand
for (var packet in packets) {
buffer.setRange(offset, offset + packet.length, packet);
offset += packet.length;
}
//
_clearPacketData(key);
// 使TalkData
// TalkData
if (payloadType == PayloadTypeConstant.talkData) {
final talkData = TalkData();
talkData.mergeFromBuffer(completePayload);
talkData.mergeFromBuffer(buffer);
return talkData;
}
} else {
// null
return null;
}
return null;
// if (!_packetBuffer.containsKey(key)) {
// _packetBuffer[key] = List.filled(spTotal, []);
// _startTimer(key);
// }
//
// //
// if (spIndex < 1 || spIndex > spTotal) {
// // print(
// // 'Invalid spTotal: $spTotal spIndex: $spIndex for messageId: $messageId');
// return null;
// }
//
// //
// _packetBuffer[key]![spIndex - 1] = byte;
//
// //
// if (_packetBuffer[key]!.every((packet) => packet.isNotEmpty)) {
// //
// Uint8List completePayload = Uint8List.fromList(
// _packetBuffer[key]!.expand((packet) => packet).toList());
// //
// _clearPacketData(key);
//
// // 使TalkData
// if (payloadType == PayloadTypeConstant.talkData) {
// final talkData = TalkData();
// talkData.mergeFromBuffer(completePayload);
// return talkData;
// }
// } else {
// // null
// return null;
// }
}
//

View File

@ -36,25 +36,24 @@ class TalkViewLogic extends BaseGetXController {
final TalkViewState state = TalkViewState();
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
Timer? _syncTimer; //
Timer? _audioTimer; //
Timer? _networkQualityTimer; //
int _startTime = 0; //
int bufferSize = 40; //
int audioBufferSize = 500; //
//
final List<double> _lastFewFps = <double>[]; //
final int minBufferSize = 2; // 2166ms
final int maxBufferSize = 8; // 8666ms
int bufferSize = 3; //
//
final int minAudioBufferSize = 1; // 1
final int maxAudioBufferSize = 3; // 3
int audioBufferSize = 2; // 2
//
int _startTime = 0; //
int _startAudioTime = 0; //
bool _isFirstFrame = true; //
bool _isFirstAudioFrame = true; //
int frameIntervalMs = 83; // 8312FPS
int audioFrameIntervalMs = 20; // 4522FPS
int minFrameIntervalMs = 83; // 12 FPS
int maxFrameIntervalMs = 166; // 6 FPS
//
final List<int> _bufferedAudioFrames = <int>[];
//
final int maxImageCacheCount = 40; //
final Map<String, ui.Image> _imageCache = {};
///
@ -91,24 +90,173 @@ class TalkViewLogic extends BaseGetXController {
void _startListenTalkData() {
state.talkDataRepository.talkDataStream.listen((TalkData talkData) async {
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:
if (state.videoBuffer.length >= bufferSize) {
state.videoBuffer.removeAt(0); //
//
if (_isFirstFrame) {
_startTime = currentTime;
_isFirstFrame = false;
AppLog.log('记录第一帧的时间戳${currentTime},${talkData.durationMs}');
}
state.videoBuffer.add(talkData); //
// -
final expectedTime = _startTime + talkData.durationMs;
final videoDelay = currentTime - expectedTime; //
//
_adjustBufferSize(videoDelay);
//
if (state.videoBuffer.length >= bufferSize) {
state.videoBuffer.removeAt(0);
}
state.videoBuffer.add(talkData);
//
await _decodeAndCacheFrame(talkData);
//
_playVideoFrames();
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); //
// AppLog.log('🎬 播放帧 - 缓冲区剩余: ${state.videoBuffer.length}/${bufferSize}, '
// '播放延迟: ${currentTime - oldestFrame.durationMs}ms, '
// '帧时间戳: ${oldestFrame.durationMs}');
} else {
// AppLog.log('⚠️ 帧未找到缓存 - Key: $cacheKey');
state.videoBuffer.removeAt(oldestIndex); //
}
}
}
//
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);
}
}
//
Future<void> _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 delay) {
const int delayThresholdHigh = 250; // 3
const int delayThresholdLow = 166; // 2
const int adjustInterval = 1; // 1
if (delay > delayThresholdHigh && bufferSize < maxBufferSize) {
//
bufferSize = min(bufferSize + adjustInterval, maxBufferSize);
AppLog.log('📈 增加缓冲区 - 当前大小: $bufferSize, 延迟: ${delay}ms');
} else if (delay < delayThresholdLow && bufferSize > minBufferSize) {
//
bufferSize = max(bufferSize - adjustInterval, minBufferSize);
AppLog.log('📉 减少缓冲区 - 当前大小: $bufferSize, 延迟: ${delay}ms');
}
}
///
void _startListenTalkStatus() {
state.startChartTalkStatus.statusStream.listen((talkStatus) {
@ -156,231 +304,6 @@ class TalkViewLogic extends BaseGetXController {
}
}
///
void _playVideoData(TalkData talkData) async {
try {
// key
String cacheKey = talkData.content.hashCode.toString();
//
if (_imageCache.containsKey(cacheKey)) {
// 使
state.currentImage.value = _imageCache[cacheKey];
} else {
// List<int> Uint8List
final Uint8List uint8Data = Uint8List.fromList(talkData.content);
// 线
ui.Image? image = await decodeImageFromList(uint8Data);
//
if (_imageCache.length >= maxImageCacheCount) {
_imageCache.remove(_imageCache.keys.first);
}
//
_imageCache[cacheKey] = image;
state.currentImage.value = image;
}
//
state.listData.value = Uint8List.fromList(talkData.content);
} catch (e) {
print('视频帧解码错误: $e');
}
// state.listData.value = Uint8List.fromList(talkData.content);
}
///
void _startPlayback() {
Future.delayed(Duration(milliseconds: 800), () {
//
_networkQualityTimer ??=
Timer.periodic(const Duration(seconds: 5), _checkNetworkQuality);
_startTime = DateTime.now().millisecondsSinceEpoch;
_syncTimer ??=
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
//
_adjustFrameInterval();
//
_monitorFrameStability();
});
});
}
///
void _adjustFrameInterval() {
//
int targetInterval = _calculateTargetInterval();
//
if (frameIntervalMs != targetInterval) {
// 2ms使
frameIntervalMs += (targetInterval > frameIntervalMs) ? 2 : -2;
//
frameIntervalMs =
frameIntervalMs.clamp(minFrameIntervalMs, maxFrameIntervalMs);
//
if ((frameIntervalMs - targetInterval).abs() >= 5) {
_rebuildTimers();
}
}
// int newFrameIntervalMs = frameIntervalMs;
// if (state.videoBuffer.length < 10 && frameIntervalMs < maxFrameIntervalMs) {
// //
// frameIntervalMs += 5;
// } else if (state.videoBuffer.length > 20 &&
// frameIntervalMs > minFrameIntervalMs) {
// //
// frameIntervalMs -= 5;
// }
// //
// if (newFrameIntervalMs != frameIntervalMs) {
// frameIntervalMs = newFrameIntervalMs;
// //
// _syncTimer?.cancel();
// _syncTimer =
// Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
// //
// _playVideoFrames();
// });
//
// _audioTimer?.cancel();
// _audioTimer =
// Timer.periodic(Duration(milliseconds: audioFrameIntervalMs), (timer) {
// final currentTime = DateTime.now().millisecondsSinceEpoch;
// final elapsedTime = currentTime - _startTime;
//
// //
// if (state.audioBuffer.isNotEmpty &&
// state.audioBuffer.first.durationMs <= elapsedTime) {
// //
// if (state.isOpenVoice.value) {
// _playAudioData(state.audioBuffer.removeAt(0));
// } else {
// //
// //
// //
// state.audioBuffer.removeAt(0);
// }
// }
// });
// }
}
///
void _monitorFrameStability() {
const stabilityThreshold = 5; //
final currentFps = 1000 / frameIntervalMs;
if (_lastFewFps.length >= 10) {
_lastFewFps.removeAt(0);
}
_lastFewFps.add(currentFps);
//
if (_lastFewFps.length >= 5) {
double mean = _lastFewFps.reduce((a, b) => a + b) / _lastFewFps.length;
double variance =
_lastFewFps.map((fps) => pow(fps - mean, 2)).reduce((a, b) => a + b) /
_lastFewFps.length;
double stdDev = sqrt(variance);
//
if (stdDev > stabilityThreshold) {
_smoothFrameRate(mean);
}
}
}
///
void _checkNetworkQuality(Timer timer) {
final bufferHealth = state.videoBuffer.length / bufferSize;
if (bufferHealth < 0.3) {
// 30%
//
frameIntervalMs = min(frameIntervalMs + 10, maxFrameIntervalMs);
_rebuildTimers();
} else if (bufferHealth > 0.7) {
// 70%
//
frameIntervalMs = max(frameIntervalMs - 5, minFrameIntervalMs);
_rebuildTimers();
}
}
///
int _calculateTargetInterval() {
const int optimalBufferSize = 15; //
const int bufferTolerance = 5; //
if (state.videoBuffer.length < optimalBufferSize - bufferTolerance) {
//
return (frameIntervalMs * 1.2).round();
} else if (state.videoBuffer.length > optimalBufferSize + bufferTolerance) {
//
return (frameIntervalMs * 0.8).round();
}
return frameIntervalMs;
}
///
void _rebuildTimers() {
//
_syncTimer?.cancel();
_audioTimer?.cancel();
//
_syncTimer =
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
_playVideoFrames();
});
// 使
_audioTimer =
Timer.periodic(Duration(milliseconds: audioFrameIntervalMs), (timer) {
_processAudioFrame();
});
}
///
void _processAudioFrame() {
final currentTime = DateTime.now().millisecondsSinceEpoch;
final elapsedTime = currentTime - _startTime;
while (state.audioBuffer.isNotEmpty &&
state.audioBuffer.first.durationMs <= elapsedTime) {
if (state.isOpenVoice.value) {
_playAudioData(state.audioBuffer.removeAt(0));
} else {
state.audioBuffer.removeAt(0);
}
}
}
void _playVideoFrames() {
final currentTime = DateTime.now().millisecondsSinceEpoch;
final elapsedTime = currentTime - _startTime;
//
//
int maxFramesToProcess = 5; // 5
int processedFrames = 0;
while (state.videoBuffer.isNotEmpty &&
state.videoBuffer.first.durationMs <= elapsedTime &&
processedFrames < maxFramesToProcess) {
if (state.videoBuffer.length > 1) {
state.videoBuffer.removeAt(0);
} else {
_playVideoData(state.videoBuffer.removeAt(0));
}
processedFrames++;
}
}
///
void _stopPlayG711Data() async {
await FlutterPcmSound.pause();
@ -546,7 +469,7 @@ class TalkViewLogic extends BaseGetXController {
_initFlutterPcmSound();
//
_startPlayback();
// _startPlayback();
//
_initAudioRecorder();
@ -554,39 +477,16 @@ class TalkViewLogic extends BaseGetXController {
requestPermissions();
}
///
void _smoothFrameRate(double targetFps) {
//
int targetInterval = (1000 / targetFps).round();
// 使
double weight = 0.3; //
frameIntervalMs =
(frameIntervalMs * (1 - weight) + targetInterval * weight).round();
//
frameIntervalMs =
frameIntervalMs.clamp(minFrameIntervalMs, maxFrameIntervalMs);
//
_rebuildTimers();
}
@override
void onClose() {
_stopPlayG711Data(); //
state.listData.value = Uint8List(0); //
state.audioBuffer.clear(); //
state.videoBuffer.clear(); //
_syncTimer?.cancel(); //
_syncTimer = null; //
_audioTimer?.cancel();
_audioTimer = null; //
state.oneMinuteTimeTimer?.cancel();
state.oneMinuteTimeTimer = null;
//
_networkQualityTimer?.cancel();
_lastFewFps.clear();
stopProcessingAudio();
//
_imageCache.clear();

View File

@ -55,7 +55,6 @@ class TalkViewState {
//
List<TalkData> audioBuffer = <TalkData>[].obs;
List<TalkData> audioBuffer2 = <TalkData>[].obs;
List<TalkData> activeAudioBuffer = <TalkData>[].obs;
List<TalkData> activeVideoBuffer = <TalkData>[].obs;