app-starlock/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart

666 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<int> _bufferedAudioFrames = <int>[];
// 添加监听状态和订阅引用
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 (!state.isOpenVoice.value && state.isRecordingAudio.value) {
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 &&
state.isRecordingAudio.value == false) {
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<String>? privateKey =
// await Storage.getStringList(saveBluePrivateKey);
// final List<int> getPrivateKeyList = changeStringListToIntList(privateKey!);
//
// final List<String>? signKey = await Storage.getStringList(saveBlueSignKey);
// final List<int> signKeyDataList = changeStringListToIntList(signKey!);
//
// final List<String>? token = await Storage.getStringList(saveBlueToken);
// final List<int> 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<int> 转换为十六进制字符串
// 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<bool> getPermissionStatus() async {
final Permission permission = Permission.microphone;
//granted 通过denied 被拒绝permanentlyDenied 拒绝且不在提示
final PermissionStatus status = await permission.status;
if (status.isGranted) {
return true;
} else if (status.isDenied) {
requestPermission(permission);
} else if (status.isPermanentlyDenied) {
openAppSettings();
} else if (status.isRestricted) {
requestPermission(permission);
} else {}
return false;
}
///申请权限
void requestPermission(Permission permission) async {
final PermissionStatus status = await permission.request();
if (status.isPermanentlyDenied) {
openAppSettings();
}
}
Future<void> requestPermissions() async {
// 申请存储权限
var storageStatus = await Permission.storage.request();
// 申请录音权限
var microphoneStatus = await Permission.microphone.request();
if (storageStatus.isGranted && microphoneStatus.isGranted) {
print("Permissions granted");
} else {
print("Permissions denied");
// 如果权限被拒绝,可以提示用户或跳转到设置页面
if (await Permission.storage.isPermanentlyDenied) {
openAppSettings(); // 跳转到应用设置页面
}
}
}
Future<void> startRecording() async {
// 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<void> 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<void> captureAndSavePng() async {
try {
if (state.globalKey.currentContext == null) {
AppLog.log('截图失败: 未找到当前上下文');
return;
}
final RenderRepaintBoundary boundary = state.globalKey.currentContext!
.findRenderObject()! as RenderRepaintBoundary;
final ui.Image image = await boundary.toImage();
final ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
AppLog.log('截图失败: 图像数据为空');
return;
}
final Uint8List pngBytes = byteData.buffer.asUint8List();
// 获取应用程序的文档目录
final Directory directory = await getApplicationDocumentsDirectory();
final String imagePath = '${directory.path}/screenshot.png';
// 将截图保存为文件
final File imgFile = File(imagePath);
await imgFile.writeAsBytes(pngBytes);
// 将截图保存到相册
await ImageGallerySaver.saveFile(imagePath);
AppLog.log('截图保存路径: $imagePath');
showToast('截图已保存到相册'.tr);
} catch (e) {
AppLog.log('截图失败: $e');
}
}
// 远程开锁
Future<void> remoteOpenLock() async {
final 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;
}
Timer? _startProcessingAudioTimer;
//开始录音
Future<void> startProcessingAudio() async {
try {
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.startRecordingAudioTime.value = DateTime.now();
// 增加录音帧监听器和错误监听器
state.voiceProcessor
?.addFrameListeners(<VoiceProcessorFrameListener>[_onFrame]);
state.voiceProcessor?.addErrorListener(_onError);
} else {
// state.errorMessage.value = 'Recording permission not granted';
}
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
}
/// 停止录音
Future<void> stopProcessingAudio() async {
try {
await state.voiceProcessor?.stop();
state.voiceProcessor?.removeFrameListener(_onFrame);
state.udpSendDataFrameNumber = 0;
// 记录结束时间
state.endRecordingAudioTime.value = DateTime.now();
// 计算录音的持续时间
final Duration duration = state.endRecordingAudioTime.value
.difference(state.startRecordingAudioTime.value);
state.recordingAudioTime.value = duration.inSeconds;
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally {
// 延迟关闭定时器,确保剩余数据能发送出去
if (_startProcessingAudioTimer != null) {
// 插入5个320长度的全0数据包
for (int i = 0; i < 5; i++) {
_bufferedAudioFrames.addAll(List.filled(chunkSize, 0));
}
Future.delayed(const Duration(milliseconds: 300), () {
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
});
} else {
_bufferedAudioFrames.clear();
}
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
}
}
static const int chunkSize = 320; // 每次发送320字节10ms G.711
static const int intervalMs = 40; // 每40ms发送一次4个chunk
void _sendAudioChunk(Timer timer) async {
if (_bufferedAudioFrames.length < chunkSize) {
// 数据不足,等待下一周期
return;
}
// 截取前 chunkSize 个字节
final chunk = _bufferedAudioFrames.sublist(0, chunkSize);
// 更新缓冲区:移除已发送部分
_bufferedAudioFrames.removeRange(0, chunkSize);
// 获取时间戳(相对时间)
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000;
print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: chunk,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
}
// 音频帧处理
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// 编码为G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0表示A-law
_bufferedAudioFrames.addAll(encodedData);
// 启动定时发送器(仅启动一次)
if (_startProcessingAudioTimer == null &&
_bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer =
Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
}
// 错误监听
void _onError(VoiceProcessorException error) {
AppLog.log(error.message!);
}
List<int> _applyGain(List<int> pcmData, double gainFactor) {
return pcmData.map((sample) {
// 增益并裁剪
int amplified = (sample * gainFactor).round();
return amplified.clamp(-32768, 32767);
}).toList();
}
}