fix:调整发送g711音频数据、增加回声消除、增大缓冲区
This commit is contained in:
parent
1fb9a5a188
commit
b876f608e4
@ -1599,33 +1599,8 @@ class _LockDetailPageState extends State<LockDetailPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleLockMonitor() async {
|
Future<void> _handleLockMonitor() async {
|
||||||
final lockId = state.keyInfos.value.lockId;
|
// 发送监控id
|
||||||
final LockSetInfoEntity entity =
|
StartChartManage().startCallRequestMessageTimer(
|
||||||
await ApiRepository.to.getLockSettingInfoData(
|
ToPeerId: StartChartManage().lockPeerId ?? '');
|
||||||
lockId: lockId.toString(),
|
|
||||||
);
|
|
||||||
if (entity.errorCode!.codeIsSuccessful) {
|
|
||||||
final LockSetInfoData data = entity.data!;
|
|
||||||
final mac = data.lockBasicInfo?.mac;
|
|
||||||
if (mac != null && mac.isNotEmpty) {
|
|
||||||
final DeviceNetwork deviceNetworkInfo = await ApiRepository.to
|
|
||||||
.getDeviceNetwork(deviceType: 2, deviceMac: mac);
|
|
||||||
if (deviceNetworkInfo.data?.wifiName == null) {
|
|
||||||
// 未找到配网信息
|
|
||||||
logic.showToast('请先进行配网');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
final peerId = deviceNetworkInfo?.data?.peerId;
|
|
||||||
if (peerId == null || peerId.isEmpty) {
|
|
||||||
throw Exception('设备peerId为空');
|
|
||||||
}
|
|
||||||
// 设置锁的peerID
|
|
||||||
StartChartManage().lockPeerId = peerId;
|
|
||||||
// 发送监控id
|
|
||||||
StartChartManage().startCallRequestMessageTimer(
|
|
||||||
ToPeerId: StartChartManage().lockPeerId ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,7 +148,6 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
}
|
}
|
||||||
state.sureBtnState.value = 1;
|
state.sureBtnState.value = 1;
|
||||||
|
|
||||||
showEasyLoading();
|
|
||||||
showBlueConnetctToastTimer(action: () {
|
showBlueConnetctToastTimer(action: () {
|
||||||
dismissEasyLoading();
|
dismissEasyLoading();
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
@ -198,8 +197,6 @@ class ConfiguringWifiLogic extends BaseGetXController {
|
|||||||
gatewayConfigurationStr: state.getGatewayConfigurationStr,
|
gatewayConfigurationStr: state.getGatewayConfigurationStr,
|
||||||
);
|
);
|
||||||
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
} else if (connectionState == BluetoothConnectionState.disconnected) {
|
||||||
dismissEasyLoading();
|
|
||||||
cancelBlueConnetctToastTimer();
|
|
||||||
state.sureBtnState.value = 0;
|
state.sureBtnState.value = 0;
|
||||||
if (state.ifCurrentScreen.value == true) {
|
if (state.ifCurrentScreen.value == true) {
|
||||||
showBlueConnetctToast();
|
showBlueConnetctToast();
|
||||||
|
|||||||
@ -3,54 +3,12 @@ import 'dart:math';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class G711 {
|
class G711 {
|
||||||
|
|
||||||
List<int> _aLawTable = [
|
|
||||||
1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
|
|
||||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
|
||||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
|
||||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
|
|
||||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|
||||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|
||||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
|
|
||||||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
|
|
||||||
];
|
|
||||||
|
|
||||||
Future<List<int>> readAssetFile(String assetPath) async {
|
Future<List<int>> readAssetFile(String assetPath) async {
|
||||||
final ByteData data = await rootBundle.load(assetPath);
|
final ByteData data = await rootBundle.load(assetPath);
|
||||||
final List<int> bytes = data.buffer.asUint8List();
|
final List<int> bytes = data.buffer.asUint8List();
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<int> encodeALaw(List<int> pcmSamples) {
|
|
||||||
final List<int> aLawSamples = [];
|
|
||||||
|
|
||||||
for (final sample in pcmSamples) {
|
|
||||||
// 将 16 位 PCM 样本归一化为 13 位有符号整数
|
|
||||||
int normalizedSample = sample >> 3;
|
|
||||||
|
|
||||||
// 获取样本的符号位
|
|
||||||
int sign = (normalizedSample & 0x8000) != 0 ? 0x80 : 0x00;
|
|
||||||
|
|
||||||
// 取绝对值
|
|
||||||
normalizedSample = normalizedSample.abs();
|
|
||||||
|
|
||||||
// 查找编码表中的段
|
|
||||||
int segment = _aLawTable[normalizedSample >> 8];
|
|
||||||
|
|
||||||
// 计算量化值
|
|
||||||
int quantizedValue = (normalizedSample >> (segment + 3)) & 0x0F;
|
|
||||||
|
|
||||||
// 生成 A-law 编码
|
|
||||||
int aLawSample = sign | (segment << 4) | quantizedValue;
|
|
||||||
|
|
||||||
// 添加到结果列表
|
|
||||||
aLawSamples.add(aLawSample);
|
|
||||||
}
|
|
||||||
|
|
||||||
return aLawSamples;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
int ALawToLinear(int aVal) {
|
int ALawToLinear(int aVal) {
|
||||||
// 取反
|
// 取反
|
||||||
aVal = ~aVal;
|
aVal = ~aVal;
|
||||||
@ -107,6 +65,18 @@ class G711 {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<int> removeEcho(List<int> audioData, int delaySamples, double decay) {
|
||||||
|
final List<int> processedData = [];
|
||||||
|
for (int i = 0; i < audioData.length; i++) {
|
||||||
|
int sample = audioData[i];
|
||||||
|
if (i >= delaySamples) {
|
||||||
|
sample -= (audioData[i - delaySamples] * decay).round();
|
||||||
|
}
|
||||||
|
processedData.add(sample);
|
||||||
|
}
|
||||||
|
return processedData;
|
||||||
|
}
|
||||||
|
|
||||||
/// 解码并降噪
|
/// 解码并降噪
|
||||||
List<int> decodeAndDenoise(List<int> encodedData, bool isALaw, int sampleRate,
|
List<int> decodeAndDenoise(List<int> encodedData, bool isALaw, int sampleRate,
|
||||||
double cutoffFreq, int threshold) {
|
double cutoffFreq, int threshold) {
|
||||||
@ -121,7 +91,10 @@ class G711 {
|
|||||||
// 噪声门
|
// 噪声门
|
||||||
List<int> denoisedData = noiseGate(filteredData, threshold);
|
List<int> denoisedData = noiseGate(filteredData, threshold);
|
||||||
|
|
||||||
return denoisedData;
|
// 回声消除处理
|
||||||
|
final List<int> processedData = removeEcho(denoisedData, 160, 0.5);
|
||||||
|
|
||||||
|
return processedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
//711解码为pcm数据
|
//711解码为pcm数据
|
||||||
|
|||||||
@ -390,7 +390,7 @@ class StartChartManage {
|
|||||||
// 如果已经处于等待接听状态就不发送
|
// 如果已经处于等待接听状态就不发送
|
||||||
if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) {
|
if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) {
|
||||||
// 停止播放铃声
|
// 停止播放铃声
|
||||||
AudioPlayerManager().playRingtone();
|
// AudioPlayerManager().playRingtone();
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routers.starChartTalkView,
|
Routers.starChartTalkView,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -31,8 +31,7 @@ import 'package:star_lock/talk/startChart/proto/talk_expect.pb.dart';
|
|||||||
import 'package:star_lock/talk/startChart/start_chart_manage.dart';
|
import 'package:star_lock/talk/startChart/start_chart_manage.dart';
|
||||||
|
|
||||||
import 'package:star_lock/talk/startChart/views/talkView/talk_view_state.dart';
|
import 'package:star_lock/talk/startChart/views/talkView/talk_view_state.dart';
|
||||||
import 'package:star_lock/talk/udp/udp_manage.dart';
|
|
||||||
import 'package:star_lock/talk/udp/udp_senderManage.dart';
|
|
||||||
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
import 'package:star_lock/tools/bugly/bugly_tool.dart';
|
||||||
import 'package:star_lock/tools/storage.dart';
|
import 'package:star_lock/tools/storage.dart';
|
||||||
|
|
||||||
@ -44,8 +43,9 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
Timer? _syncTimer; // 音视频播放刷新率定时器
|
Timer? _syncTimer; // 音视频播放刷新率定时器
|
||||||
int _startTime = 0; // 开始播放时间戳,用于判断帧数据中的时间戳位置
|
int _startTime = 0; // 开始播放时间戳,用于判断帧数据中的时间戳位置
|
||||||
int bufferSize = 20; // 缓冲区大小(以帧为单位)
|
int bufferSize = 20; // 缓冲区大小(以帧为单位)
|
||||||
|
int audioBufferSize = 640; // 缓冲区大小(以帧为单位)
|
||||||
final List<int> frameTimestamps = []; // 帧时间戳用于计算 FPS
|
final List<int> frameTimestamps = []; // 帧时间戳用于计算 FPS
|
||||||
int frameIntervalMs = 45; // 初始帧间隔设置为45毫秒(约22FPS)
|
int frameIntervalMs = 35; // 初始帧间隔设置为45毫秒(约22FPS)
|
||||||
int minFrameIntervalMs = 30; // 最小帧间隔(约33 FPS)
|
int minFrameIntervalMs = 30; // 最小帧间隔(约33 FPS)
|
||||||
int maxFrameIntervalMs = 100; // 最大帧间隔(约1 FPS)
|
int maxFrameIntervalMs = 100; // 最大帧间隔(约1 FPS)
|
||||||
// int maxFrameIntervalMs = 100; // 最大帧间隔(约10 FPS)
|
// int maxFrameIntervalMs = 100; // 最大帧间隔(约10 FPS)
|
||||||
@ -53,11 +53,11 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
/// 初始化音频播放器
|
/// 初始化音频播放器
|
||||||
void _initFlutterPcmSound() {
|
void _initFlutterPcmSound() {
|
||||||
const int sampleRate = 8000;
|
const int sampleRate = 8000;
|
||||||
FlutterPcmSound.setLogLevel(LogLevel.none);
|
FlutterPcmSound.setLogLevel(LogLevel.verbose);
|
||||||
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
||||||
// 设置 feed 阈值
|
// 设置 feed 阈值
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
FlutterPcmSound.setFeedThreshold(-1); // Android 平台的特殊处理
|
FlutterPcmSound.setFeedThreshold(sampleRate ~/ 2); // Android 平台的特殊处理
|
||||||
} else {
|
} else {
|
||||||
FlutterPcmSound.setFeedThreshold(sampleRate ~/ 32); // 非 Android 平台的处理
|
FlutterPcmSound.setFeedThreshold(sampleRate ~/ 32); // 非 Android 平台的处理
|
||||||
}
|
}
|
||||||
@ -88,10 +88,14 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
// 判断数据类型,进行分发处理
|
// 判断数据类型,进行分发处理
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case TalkData_ContentTypeE.G711:
|
case TalkData_ContentTypeE.G711:
|
||||||
if (state.audioBuffer.length >= bufferSize) {
|
if (state.audioBuffer.length >= audioBufferSize) {
|
||||||
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||||
|
// readAudioBufferSize.removeAt(0); // 丢弃最旧的数据
|
||||||
}
|
}
|
||||||
state.audioBuffer.add(talkData); // 添加新数据
|
state.audioBuffer.add(talkData); // 添加新数据
|
||||||
|
|
||||||
|
// readAudioBufferSize.add(talkData.content);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case TalkData_ContentTypeE.Image:
|
case TalkData_ContentTypeE.Image:
|
||||||
if (state.videoBuffer.length >= bufferSize) {
|
if (state.videoBuffer.length >= bufferSize) {
|
||||||
@ -128,7 +132,6 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
void _playAudioData(TalkData talkData) async {
|
void _playAudioData(TalkData talkData) async {
|
||||||
// final list = G711().convertList(talkData.content);
|
// final list = G711().convertList(talkData.content);
|
||||||
final list = G711().decodeAndDenoise(talkData.content, true, 8000, 300, 50);
|
final list = G711().decodeAndDenoise(talkData.content, true, 8000, 300, 50);
|
||||||
//
|
|
||||||
// // 将 PCM 数据转换为 PcmArrayInt16
|
// // 将 PCM 数据转换为 PcmArrayInt16
|
||||||
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
||||||
FlutterPcmSound.feed(fromList);
|
FlutterPcmSound.feed(fromList);
|
||||||
@ -145,44 +148,14 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
|
|
||||||
/// 启动播放
|
/// 启动播放
|
||||||
void _startPlayback() {
|
void _startPlayback() {
|
||||||
int frameIntervalMs = 45; // 初始帧间隔设置为45毫秒(约22FPS)
|
|
||||||
|
|
||||||
Future.delayed(Duration(milliseconds: 800), () {
|
Future.delayed(Duration(milliseconds: 800), () {
|
||||||
_startTime = DateTime.now().millisecondsSinceEpoch;
|
_startTime = DateTime.now().millisecondsSinceEpoch;
|
||||||
_syncTimer ??=
|
_syncTimer ??=
|
||||||
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
|
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
|
||||||
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
final elapsedTime = currentTime - _startTime;
|
|
||||||
|
|
||||||
// 动态调整帧间隔
|
// 动态调整帧间隔
|
||||||
_adjustFrameInterval();
|
_adjustFrameInterval();
|
||||||
|
|
||||||
// 播放合适的音频帧
|
_playFrames();
|
||||||
if (state.audioBuffer.isNotEmpty &&
|
|
||||||
state.audioBuffer.first.durationMs <= elapsedTime) {
|
|
||||||
// 判断音频开关是否打开
|
|
||||||
if (state.isOpenVoice.value) {
|
|
||||||
AppLog.log('播放音频:${state.audioBuffer[0]}');
|
|
||||||
_playAudioData(state.audioBuffer.removeAt(0));
|
|
||||||
} else {
|
|
||||||
// 如果不播放音频,只从缓冲区中读取数据,但不移除
|
|
||||||
// 你可以根据需要调整此处逻辑,例如保留缓冲区的最大长度,防止无限增长
|
|
||||||
// 仅移除缓冲区数据但不播放音频,确保音频也是实时更新的
|
|
||||||
state.audioBuffer.removeAt(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 播放合适的视频帧
|
|
||||||
// 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧
|
|
||||||
while (state.videoBuffer.isNotEmpty &&
|
|
||||||
state.videoBuffer.first.durationMs <= elapsedTime) {
|
|
||||||
// 如果有多个帧,移除旧的帧,保持最新的帧
|
|
||||||
if (state.videoBuffer.length > 1) {
|
|
||||||
state.videoBuffer.removeAt(0);
|
|
||||||
} else {
|
|
||||||
_playVideoData(state.videoBuffer.removeAt(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -211,42 +184,46 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
_syncTimer?.cancel();
|
_syncTimer?.cancel();
|
||||||
_syncTimer =
|
_syncTimer =
|
||||||
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
|
Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) {
|
||||||
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
_playFrames();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 播放合适的视频帧
|
|
||||||
// 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧
|
|
||||||
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 _playFrames() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放合适的视频帧
|
||||||
|
// 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧
|
||||||
|
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 updateNetworkStatus(int currentTimestamp) {
|
void updateNetworkStatus(int currentTimestamp) {
|
||||||
if (state.lastFrameTimestamp.value != 0) {
|
if (state.lastFrameTimestamp.value != 0) {
|
||||||
@ -474,8 +451,6 @@ class TalkViewLogic extends BaseGetXController {
|
|||||||
// 停止播放音频
|
// 停止播放音频
|
||||||
_stopPlayG711Data();
|
_stopPlayG711Data();
|
||||||
stopProcessingAudio();
|
stopProcessingAudio();
|
||||||
// 状态错误,返回页面
|
|
||||||
Get.back();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 更新发送预期数据
|
/// 更新发送预期数据
|
||||||
|
|||||||
@ -62,143 +62,150 @@ class _TalkViewPageState extends State<TalkViewPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return WillPopScope(
|
||||||
width: 1.sw,
|
onWillPop: () async {
|
||||||
height: 1.sh,
|
// 返回 false 表示禁止退出
|
||||||
child: Stack(
|
return false;
|
||||||
alignment: Alignment.center,
|
},
|
||||||
children: <Widget>[
|
child: SizedBox(
|
||||||
Obx(
|
width: 1.sw,
|
||||||
() {
|
height: 1.sh,
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
child: Stack(
|
||||||
final screenHeight = MediaQuery.of(context).size.height;
|
alignment: Alignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Obx(
|
||||||
|
() {
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
|
|
||||||
final logicalWidth = MediaQuery.of(context).size.width;
|
final logicalWidth = MediaQuery.of(context).size.width;
|
||||||
final logicalHeight = MediaQuery.of(context).size.height;
|
final logicalHeight = MediaQuery.of(context).size.height;
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio =
|
||||||
|
MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
// 计算物理像素值
|
// 计算物理像素值
|
||||||
final physicalWidth = logicalWidth * devicePixelRatio;
|
final physicalWidth = logicalWidth * devicePixelRatio;
|
||||||
final physicalHeight = logicalHeight * devicePixelRatio;
|
final physicalHeight = logicalHeight * devicePixelRatio;
|
||||||
|
|
||||||
// 旋转后的图片尺寸
|
// 旋转后的图片尺寸
|
||||||
final rotatedImageWidth = 480; // 原始高度
|
final rotatedImageWidth = 480; // 原始高度
|
||||||
final rotatedImageHeight = 864; // 原始宽度
|
final rotatedImageHeight = 864; // 原始宽度
|
||||||
|
|
||||||
// 计算缩放比例
|
// 计算缩放比例
|
||||||
final scaleWidth = physicalWidth / rotatedImageWidth;
|
final scaleWidth = physicalWidth / rotatedImageWidth;
|
||||||
final scaleHeight = physicalHeight / rotatedImageHeight;
|
final scaleHeight = physicalHeight / rotatedImageHeight;
|
||||||
final scale = max(scaleWidth, scaleHeight); // 选择较大的缩放比例
|
final scale = max(scaleWidth, scaleHeight); // 选择较大的缩放比例
|
||||||
|
|
||||||
return state.listData.value.isEmpty
|
return state.listData.value.isEmpty
|
||||||
? Image.asset(
|
? Image.asset(
|
||||||
'images/main/monitorBg.png',
|
'images/main/monitorBg.png',
|
||||||
width: screenWidth,
|
width: screenWidth,
|
||||||
height: screenHeight,
|
height: screenHeight,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
: PopScope(
|
: PopScope(
|
||||||
canPop: false,
|
canPop: false,
|
||||||
child: RepaintBoundary(
|
child: RepaintBoundary(
|
||||||
key: state.globalKey,
|
key: state.globalKey,
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle:
|
angle:
|
||||||
state.rotateAngle.value * (pi / 180), // 旋转 90 度
|
state.rotateAngle.value * (pi / 180), // 旋转 90 度
|
||||||
child: Transform.scale(
|
child: Transform.scale(
|
||||||
scale: scale, // 动态计算的缩放比例
|
scale: scale, // 动态计算的缩放比例
|
||||||
child: Image.memory(
|
child: Image.memory(
|
||||||
state.listData.value,
|
state.listData.value,
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
errorBuilder: (
|
errorBuilder: (
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Object error,
|
Object error,
|
||||||
StackTrace? stackTrace,
|
StackTrace? stackTrace,
|
||||||
) {
|
) {
|
||||||
return Container(color: Colors.transparent);
|
return Container(color: Colors.transparent);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
Obx(() => state.listData.value.isEmpty
|
||||||
Obx(() => state.listData.value.isEmpty
|
? Positioned(
|
||||||
? Positioned(
|
bottom: 300.h,
|
||||||
bottom: 300.h,
|
child: Text(
|
||||||
child: Text(
|
'正在创建安全连接...'.tr,
|
||||||
'正在创建安全连接...'.tr,
|
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
||||||
style: TextStyle(color: Colors.black, fontSize: 26.sp),
|
))
|
||||||
))
|
: Container()),
|
||||||
: Container()),
|
Positioned(
|
||||||
Positioned(
|
bottom: 10.w,
|
||||||
bottom: 10.w,
|
child: Container(
|
||||||
child: Container(
|
width: 1.sw - 30.w * 2,
|
||||||
width: 1.sw - 30.w * 2,
|
// height: 300.h,
|
||||||
// height: 300.h,
|
margin: EdgeInsets.all(30.w),
|
||||||
margin: EdgeInsets.all(30.w),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: Colors.black.withOpacity(0.2),
|
||||||
color: Colors.black.withOpacity(0.2),
|
borderRadius: BorderRadius.circular(20.h)),
|
||||||
borderRadius: BorderRadius.circular(20.h)),
|
child: Column(
|
||||||
child: Column(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
SizedBox(height: 20.h),
|
||||||
SizedBox(height: 20.h),
|
bottomTopBtnWidget(),
|
||||||
bottomTopBtnWidget(),
|
SizedBox(height: 20.h),
|
||||||
SizedBox(height: 20.h),
|
bottomBottomBtnWidget(),
|
||||||
bottomBottomBtnWidget(),
|
SizedBox(height: 20.h),
|
||||||
SizedBox(height: 20.h),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Positioned(
|
||||||
// Positioned(
|
// top: 100.h,
|
||||||
// top: 100.h,
|
// left: 10.w,
|
||||||
// left: 10.w,
|
// child: Obx(
|
||||||
// child: Obx(
|
// () => Text(
|
||||||
// () => Text(
|
// 'FPS:${state.fps.value}',
|
||||||
// 'FPS:${state.fps.value}',
|
// style: TextStyle(
|
||||||
// style: TextStyle(
|
// fontSize: 30.sp,
|
||||||
// fontSize: 30.sp,
|
// color: Colors.orange,
|
||||||
// color: Colors.orange,
|
// fontWeight: FontWeight.bold),
|
||||||
// fontWeight: FontWeight.bold),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
// ),
|
Obx(() => state.listData.value.isEmpty
|
||||||
Obx(() => state.listData.value.isEmpty
|
? buildRotationTransition()
|
||||||
? buildRotationTransition()
|
: Container()),
|
||||||
: Container()),
|
|
||||||
|
|
||||||
Obx(() => state.isLongPressing.value
|
Obx(() => state.isLongPressing.value
|
||||||
? Positioned(
|
? Positioned(
|
||||||
top: 80.h,
|
top: 80.h,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(10.w),
|
padding: EdgeInsets.all(10.w),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.7),
|
color: Colors.black.withOpacity(0.7),
|
||||||
borderRadius: BorderRadius.circular(10.w),
|
borderRadius: BorderRadius.circular(10.w),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.mic, color: Colors.white, size: 24.w),
|
Icon(Icons.mic, color: Colors.white, size: 24.w),
|
||||||
SizedBox(width: 10.w),
|
SizedBox(width: 10.w),
|
||||||
Text(
|
Text(
|
||||||
'正在说话...',
|
'正在说话...',
|
||||||
style:
|
style: TextStyle(
|
||||||
TextStyle(fontSize: 20.sp, color: Colors.white),
|
fontSize: 20.sp, color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
: Container()),
|
||||||
: Container()),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,4 +87,6 @@ class TalkViewState {
|
|||||||
List<List<int>> recordingAudioAllFrames = <List<int>>[]; // 录制音频的所有帧
|
List<List<int>> recordingAudioAllFrames = <List<int>>[]; // 录制音频的所有帧
|
||||||
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
||||||
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
||||||
|
RxBool hasAudioData = false.obs; // 是否有音频数据
|
||||||
|
RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user