app-starlock/lib/talk/starChart/webView/h264_web_logic.dart

447 lines
15 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:collection';
import 'dart:io';
import 'dart:ui' as ui;
import 'dart:math'; // Import the math package to use sqrt
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:get/get.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:star_lock/app_settings/app_settings.dart';
import 'package:star_lock/login/login/entity/LoginEntity.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart';
import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart';
import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart';
import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart';
import 'package:star_lock/network/api_repository.dart';
import 'package:star_lock/talk/call/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/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/talk/starChart/webView/h264_web_view_state.dart';
import 'package:star_lock/tools/G711Tool.dart';
import 'package:star_lock/tools/bugly/bugly_tool.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../../tools/baseGetXController.dart';
class H264WebViewLogic extends BaseGetXController {
final H264WebViewState state = H264WebViewState();
final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state;
// 添加模拟数据相关变量
static const int CHUNK_SIZE = 4096;
Timer? _mockDataTimer;
// 定义音频帧缓冲和发送函数
final List<int> _bufferedAudioFrames = <int>[];
final Queue<List<int>> _frameBuffer = Queue<List<int>>();
static const int FRAME_BUFFER_SIZE = 25;
@override
void onInit() {
super.onInit();
// 初始化 WebView 控制器
state.webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..enableZoom(false)
..addJavaScriptChannel(
'Flutter',
onMessageReceived: (message) {
print("来自 HTML 的消息: ${message.message}");
},
);
state.isShowLoading.value = true;
// 加载本地 HTML
_loadLocalHtml();
// 创建流数据监听
_createFramesStreamListen();
// playLocalTestVideo();
_startListenTalkStatus();
state.talkStatus.value = state.startChartTalkStatus.status;
// 初始化音频播放器
_initFlutterPcmSound();
// 初始化录音控制器
_initAudioRecorder();
}
/// 初始化音频录制器
void _initAudioRecorder() {
state.voiceProcessor = VoiceProcessor.instance;
}
/// 初始化音频播放器
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 _createFramesStreamListen() async {
state.talkDataRepository.talkDataStream.listen((TalkData event) async {
// 添加新帧到缓冲区
_frameBuffer.add(event.content);
// 当缓冲区超过最大容量时,发送最早的帧并移除
while (_frameBuffer.length > FRAME_BUFFER_SIZE) {
if (_frameBuffer.isNotEmpty) {
final frame = _frameBuffer.removeFirst();
await _sendBufferedData(frame);
}
}
});
}
/// 播放本地测试视频文件
// Future<void> playLocalTestVideo() async {
// try {
// ByteData data = await rootBundle.load('assets/html/demo.h264');
// List<int> bytes = data.buffer.asUint8List();
//
// int offset = 0;
// _mockDataTimer = Timer.periodic(Duration(milliseconds: 40), (timer) {
// if (offset >= bytes.length) {
// timer.cancel();
// return;
// }
//
// int end = min(offset + CHUNK_SIZE, bytes.length);
// List<int> chunk = bytes.sublist(offset, end);
// _sendBufferedData(chunk);
//
// offset += CHUNK_SIZE;
// });
// } catch (e) {
// AppLog.log('加载测试视频文件失败: $e');
// }
// }
/// 加载html文件
Future<void> _loadLocalHtml() async {
// 加载 HTML 文件内容
final String fileHtmlContent =
await rootBundle.loadString('assets/html/h264.html');
// 加载 JS 文件内容
final String jsContent =
await rootBundle.loadString('assets/html/jmuxer.min.js');
// 将 JS 文件内容嵌入到 HTML 中
final String htmlWithJs = fileHtmlContent.replaceAll(
'<script src="jmuxer.min.js"></script>', // 替换掉引用外部 JS 的标签
'<script>$jsContent</script>' // 使用内联方式嵌入 JS 内容
);
// 加载最终的 HTML 字符串到 WebView 中
if (state.webViewController != null) {
state.webViewController.loadHtmlString(htmlWithJs); // 设置 baseUrl 避免资源加载问题
}
}
// 修改后的发送方法
_sendBufferedData(List<int> buffer) async {
// 原始发送逻辑
String jsCode = "feedDataFromFlutter($buffer);";
await state.webViewController.runJavaScript(jsCode);
if (state.isShowLoading.isTrue) {
await Future.delayed(Duration(seconds: 1));
state.isShowLoading.value = false;
}
}
/// 监听对讲状态
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.isShowLoading.isFalse) {
state.oneMinuteTime.value++;
if (state.oneMinuteTime.value >= 60) {
t.cancel(); // 取消定时器
state.oneMinuteTime.value = 0;
}
}
});
break;
default:
// 其他状态的处理
break;
}
});
}
/// 更新发送预期数据
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);
}
/// 处理无效通话状态
void _handleInvalidTalkStatus() {}
/// 截图并保存到相册
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');
}
}
// 发起接听命令
void initiateAnswerCommand() {
StartChartManage().startTalkAcceptTimer();
}
//开始录音
Future<void> startProcessingAudio() async {
try {
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.startRecordingAudioTime.value = DateTime.now();
// 增加录音帧监听器和错误监听器
state.voiceProcessor
?.addFrameListeners(<VoiceProcessorFrameListener>[_onFrame]);
state.voiceProcessor?.addErrorListener(_onError);
} else {
// state.errorMessage.value = 'Recording permission not granted';
}
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
/// 停止录音
Future<void> stopProcessingAudio() async {
try {
await state.voiceProcessor?.stop();
state.voiceProcessor?.removeFrameListener(_onFrame);
state.udpSendDataFrameNumber = 0;
// 记录结束时间
state.endRecordingAudioTime.value = DateTime.now();
// 计算录音的持续时间
final Duration duration = state.endRecordingAudioTime.value
.difference(state.startRecordingAudioTime.value);
state.recordingAudioTime.value = duration.inSeconds;
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to stop recorder: $ex';
} finally {
final bool? isRecording = await state.voiceProcessor?.isRecording();
state.isRecordingAudio.value = isRecording!;
state.isOpenVoice.value = true;
}
}
// 音频帧处理
Future<void> _onFrame(List<int> frame) async {
// 添加最大缓冲限制
if (_bufferedAudioFrames.length > state.frameLength * 3) {
_bufferedAudioFrames.clear(); // 清空过多积累的数据
return;
}
// 首先应用固定增益提升基础音量
List<int> amplifiedFrame = _applyGain(frame, 1.6);
// 编码为G711数据
List<int> encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law
_bufferedAudioFrames.addAll(encodedData);
// 使用相对时间戳
final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
int getFrameLength = state.frameLength;
if (Platform.isIOS) {
getFrameLength = state.frameLength * 2;
}
// 添加发送间隔控制
if (_bufferedAudioFrames.length >= state.frameLength) {
try {
await StartChartManage().sendTalkDataMessage(
talkData: TalkData(
content: _bufferedAudioFrames,
contentType: TalkData_ContentTypeE.G711,
durationMs: ms,
),
);
} finally {
_bufferedAudioFrames.clear(); // 确保清理缓冲区
}
} else {
_bufferedAudioFrames.addAll(encodedData);
}
}
// 错误监听
void _onError(VoiceProcessorException error) {
AppLog.log(error.message!);
}
// 添加音频增益处理方法
List<int> _applyGain(List<int> pcmData, double gainFactor) {
List<int> result = List<int>.filled(pcmData.length, 0);
for (int i = 0; i < pcmData.length; i++) {
// PCM数据通常是有符号的16位整数
int sample = pcmData[i];
// 应用增益
double amplified = sample * gainFactor;
// 限制在有效范围内,防止溢出
if (amplified > 32767) {
amplified = 32767;
} else if (amplified < -32768) {
amplified = -32768;
}
result[i] = amplified.toInt();
}
return result;
}
/// 挂断
void udpHangUpAction() async {
if (state.talkStatus.value == TalkStatus.answeredSuccessfully) {
// 如果是通话中就挂断
StartChartManage().startTalkHangupMessageTimer();
} else {
// 拒绝
StartChartManage().startTalkRejectMessageTimer();
}
// _mockDataTimer?.cancel();
// _mockDataTimer = null;
PacketLossStatistics().reset();
Get.back();
}
// 远程开锁
Future<void> remoteOpenLock() async {
final lockPeerId = StartChartManage().lockPeerId;
final lockListPeerId = StartChartManage().lockListPeerId;
int lockId = lockDetailState.keyInfos.value.lockId ?? 0;
// 如果锁列表获取到peerId代表有多个锁使用锁列表的peerId
// 从列表中遍历出对应的peerId
lockListPeerId.forEach((element) {
if (element.network?.peerId == lockPeerId) {
lockId = element.lockId ?? 0;
}
});
final LockSetInfoEntity lockSetInfoEntity =
await ApiRepository.to.getLockSettingInfoData(
lockId: lockId.toString(),
);
if (lockSetInfoEntity.errorCode!.codeIsSuccessful) {
if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 &&
lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) {
final LoginEntity entity = await ApiRepository.to
.remoteOpenLock(lockId: lockId.toString(), timeOut: 60);
if (entity.errorCode!.codeIsSuccessful) {
showToast('已开锁'.tr);
StartChartManage().lockListPeerId = [];
}
} else {
showToast('该锁的远程开锁功能未启用'.tr);
}
}
}
@override
void dispose() {
// _mockDataTimer?.cancel();
// _mockDataTimer = null;
super.dispose();
StartChartManage().startTalkHangupMessageTimer();
state.animationController.dispose();
state.webViewController.clearCache();
state.webViewController.reload();
state.oneMinuteTimeTimer?.cancel();
state.oneMinuteTimeTimer = null;
stopProcessingAudio();
StartChartManage().reSetDefaultTalkExpect();
_frameBuffer.clear();
}
}