fix:调整ios的录音发送逻辑
This commit is contained in:
parent
88db0e850b
commit
47ddb9b72a
@ -8,6 +8,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
|
||||||
|
import 'package:flutter_sound/flutter_sound.dart';
|
||||||
|
import 'package:flutter_sound/public/flutter_sound_recorder.dart';
|
||||||
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
|
||||||
import 'package:gallery_saver/gallery_saver.dart';
|
import 'package:gallery_saver/gallery_saver.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -51,7 +53,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
|
|
||||||
int bufferSize = 25; // 初始化为默认大小
|
int bufferSize = 25; // 初始化为默认大小
|
||||||
|
|
||||||
int audioBufferSize = 2; // 音频默认缓冲2帧
|
int audioBufferSize = 20; // 音频默认缓冲2帧
|
||||||
|
|
||||||
// 回绕阈值,动态调整,frameSeq较小时阈值也小
|
// 回绕阈值,动态调整,frameSeq较小时阈值也小
|
||||||
int _getFrameSeqRolloverThreshold(int lastSeq) {
|
int _getFrameSeqRolloverThreshold(int lastSeq) {
|
||||||
@ -144,11 +146,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
FlutterPcmSound.setLogLevel(LogLevel.none);
|
FlutterPcmSound.setLogLevel(LogLevel.none);
|
||||||
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
|
||||||
// 设置 feed 阈值
|
// 设置 feed 阈值
|
||||||
if (Platform.isAndroid) {
|
// if (Platform.isAndroid) {
|
||||||
FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理
|
// FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理
|
||||||
} else {
|
// } else {
|
||||||
FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理
|
// FlutterPcmSound.setFeedThreshold(4096); // 非 Android 平台的处理
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 挂断
|
/// 挂断
|
||||||
@ -499,15 +501,18 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
/// 播放音频数据
|
/// 播放音频数据
|
||||||
void _playAudioData(TalkData talkData) async {
|
void _playAudioData(TalkData talkData) async {
|
||||||
if (state.isOpenVoice.value && state.isLoading.isFalse) {
|
if (state.isOpenVoice.value && state.isLoading.isFalse) {
|
||||||
final list =
|
List<int> encodedData = G711Tool.decode(talkData.content, 0); // 0表示A-law
|
||||||
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
|
// 将 PCM 数据转换为 PcmArrayInt16
|
||||||
// // 将 PCM 数据转换为 PcmArrayInt16
|
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(encodedData);
|
||||||
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
|
|
||||||
FlutterPcmSound.feed(fromList);
|
FlutterPcmSound.feed(fromList);
|
||||||
if (!state.isPlaying.value) {
|
if (!state.isPlaying.value) {
|
||||||
|
AppLog.log('play');
|
||||||
FlutterPcmSound.play();
|
FlutterPcmSound.play();
|
||||||
state.isPlaying.value = true;
|
state.isPlaying.value = true;
|
||||||
}
|
}
|
||||||
|
} else if (state.isOpenVoice.isFalse) {
|
||||||
|
FlutterPcmSound.pause();
|
||||||
|
state.isPlaying.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,8 +578,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
// 启动监听音视频数据流
|
|
||||||
_startListenTalkData();
|
|
||||||
// 启动监听对讲状态
|
// 启动监听对讲状态
|
||||||
_startListenTalkStatus();
|
_startListenTalkStatus();
|
||||||
// 在没有监听成功之前赋值一遍状态
|
// 在没有监听成功之前赋值一遍状态
|
||||||
@ -596,6 +599,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
// 初始化H264帧缓冲区
|
// 初始化H264帧缓冲区
|
||||||
state.h264FrameBuffer.clear();
|
state.h264FrameBuffer.clear();
|
||||||
state.isProcessingFrame = false;
|
state.isProcessingFrame = false;
|
||||||
|
|
||||||
|
// 启动监听音视频数据流
|
||||||
|
_startListenTalkData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -639,7 +645,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
|
|
||||||
// 清空已解码I帧集合
|
// 清空已解码I帧集合
|
||||||
_decodedIFrames.clear();
|
_decodedIFrames.clear();
|
||||||
|
_startProcessingAudioTimer?.cancel();
|
||||||
|
_startProcessingAudioTimer = null;
|
||||||
|
_bufferedAudioFrames.clear();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -652,33 +660,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
|
|
||||||
/// 更新发送预期数据
|
/// 更新发送预期数据
|
||||||
void updateTalkExpect() {
|
void updateTalkExpect() {
|
||||||
// 清晰度与VideoTypeE的映射
|
|
||||||
final Map<String, VideoTypeE> qualityToVideoType = {
|
|
||||||
'标清': VideoTypeE.H264,
|
|
||||||
'高清': VideoTypeE.H264_720P,
|
|
||||||
// 可扩展更多清晰度
|
|
||||||
};
|
|
||||||
TalkExpectReq talkExpectReq = TalkExpectReq();
|
|
||||||
state.isOpenVoice.value = !state.isOpenVoice.value;
|
state.isOpenVoice.value = !state.isOpenVoice.value;
|
||||||
// 根据当前清晰度动态设置videoType
|
if (state.isOpenVoice.isTrue) {
|
||||||
VideoTypeE currentVideoType =
|
FlutterPcmSound.play();
|
||||||
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
|
|
||||||
if (!state.isOpenVoice.value) {
|
|
||||||
talkExpectReq = TalkExpectReq(
|
|
||||||
videoType: [currentVideoType],
|
|
||||||
audioType: [],
|
|
||||||
);
|
|
||||||
showToast('已静音'.tr);
|
|
||||||
} else {
|
} else {
|
||||||
talkExpectReq = TalkExpectReq(
|
FlutterPcmSound.pause();
|
||||||
videoType: [currentVideoType],
|
|
||||||
audioType: [AudioTypeE.G711],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 修改发送预期数据
|
|
||||||
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
|
|
||||||
talkExpect: talkExpectReq);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 截图并保存到相册
|
/// 截图并保存到相册
|
||||||
@ -762,8 +749,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
state.voiceProcessor = VoiceProcessor.instance;
|
state.voiceProcessor = VoiceProcessor.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer? _startProcessingAudioTimer;
|
||||||
|
|
||||||
//开始录音
|
//开始录音
|
||||||
Future<void> startProcessingAudio() async {
|
Future<void> startProcessingAudio() async {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) {
|
||||||
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
await state.voiceProcessor?.start(state.frameLength, state.sampleRate);
|
||||||
@ -781,7 +771,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
} on PlatformException catch (ex) {
|
} on PlatformException catch (ex) {
|
||||||
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
// state.errorMessage.value = 'Failed to start recorder: $ex';
|
||||||
}
|
}
|
||||||
state.isOpenVoice.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止录音
|
/// 停止录音
|
||||||
@ -803,51 +792,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
} finally {
|
} finally {
|
||||||
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
final bool? isRecording = await state.voiceProcessor?.isRecording();
|
||||||
state.isRecordingAudio.value = isRecording!;
|
state.isRecordingAudio.value = isRecording!;
|
||||||
state.isOpenVoice.value = true;
|
|
||||||
}
|
}
|
||||||
}
|
_startProcessingAudioTimer?.cancel();
|
||||||
|
_startProcessingAudioTimer = null;
|
||||||
// 音频帧处理
|
_bufferedAudioFrames.clear();
|
||||||
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!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加音频增益处理方法
|
// 添加音频增益处理方法
|
||||||
@ -873,453 +821,98 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
static const int chunkSize = 320; // 每次发送320字节(10ms G.711)
|
||||||
/// 追加写入一帧到h264文件(需传入帧数据和帧类型frameType)
|
static const int intervalMs = 40; // 每40ms发送一次(4个chunk)
|
||||||
Future<void> _appendH264FrameToFile(
|
void _sendAudioChunk(Timer timer) async {
|
||||||
List<int> frameData, TalkDataH264Frame_FrameTypeE frameType) async {
|
if (_bufferedAudioFrames.length < chunkSize) {
|
||||||
try {
|
// 数据不足,等待下一周期
|
||||||
if (state.h264File == null) {
|
return;
|
||||||
await _initH264File();
|
|
||||||
}
|
|
||||||
// NALU分割函数,返回每个NALU的完整字节数组
|
|
||||||
List<List<int>> splitNalus(List<int> data) {
|
|
||||||
List<List<int>> 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<List<int>> spsList = [];
|
|
||||||
List<List<int>> ppsList = [];
|
|
||||||
List<List<int>> 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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 截取前 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统一NALU起始码为0x00000001
|
// 音频帧处理
|
||||||
List<int> _ensureStartCode(List<int> nalu) {
|
Future<void> _onFrame(List<int> frame) async {
|
||||||
if (nalu.length >= 4 &&
|
final applyGain = _applyGain(frame, 1.6);
|
||||||
nalu[0] == 0x00 &&
|
|
||||||
nalu[1] == 0x00 &&
|
// 编码为G711数据
|
||||||
nalu[2] == 0x00 &&
|
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0表示A-law
|
||||||
nalu[3] == 0x01) {
|
_bufferedAudioFrames.addAll(encodedData);
|
||||||
return nalu;
|
|
||||||
} else if (nalu.length >= 3 &&
|
|
||||||
nalu[0] == 0x00 &&
|
// 启动定时发送器(仅启动一次)
|
||||||
nalu[1] == 0x00 &&
|
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
|
||||||
nalu[2] == 0x01) {
|
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
|
||||||
return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3);
|
|
||||||
} else {
|
|
||||||
return [0x00, 0x00, 0x00, 0x01] + nalu;
|
|
||||||
}
|
}
|
||||||
|
// if (_startProcessingAudioTimer == null &&
|
||||||
|
// _bufferedAudioFrames.length > 320) {
|
||||||
|
// // 每 10ms 发送一次 320 长度的数据
|
||||||
|
// const int intervalMs = 40;
|
||||||
|
// const int chunkSize = 320;
|
||||||
|
// _startProcessingAudioTimer =
|
||||||
|
// Timer.periodic(Duration(milliseconds: intervalMs), (timer) async {
|
||||||
|
// // 从 _bufferedAudioFrames 中截取 320 个数据(循环发送)
|
||||||
|
// int startIndex = (timer.tick - 1) * chunkSize; // tick 从 1 开始
|
||||||
|
// int endIndex = startIndex + chunkSize;
|
||||||
|
// // 使用相对时间戳
|
||||||
|
// final int ms =
|
||||||
|
// DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳
|
||||||
|
//
|
||||||
|
// // 循环使用数据(防止越界)
|
||||||
|
// List<int> chunk;
|
||||||
|
// if (endIndex <= _bufferedAudioFrames.length) {
|
||||||
|
// chunk = _bufferedAudioFrames.sublist(startIndex, endIndex);
|
||||||
|
// } else {
|
||||||
|
// // 超出范围时循环
|
||||||
|
// chunk = <int>[];
|
||||||
|
// while (chunk.length < chunkSize) {
|
||||||
|
// int remaining = chunkSize - chunk.length;
|
||||||
|
// int take = endIndex > _bufferedAudioFrames.length
|
||||||
|
// ? _bufferedAudioFrames.length -
|
||||||
|
// (startIndex % _bufferedAudioFrames.length)
|
||||||
|
// : remaining;
|
||||||
|
// take = take.clamp(0, remaining);
|
||||||
|
// int start = startIndex % _bufferedAudioFrames.length;
|
||||||
|
// chunk.addAll(_bufferedAudioFrames.sublist(start,
|
||||||
|
// (start + take).clamp(start, _bufferedAudioFrames.length)));
|
||||||
|
// startIndex += take;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // 示例:打印前10个数据
|
||||||
|
// print('Send chunk ${timer.tick}: ${chunk.take(10).toList()}...');
|
||||||
|
// await StartChartManage().sendTalkDataMessage(
|
||||||
|
// talkData: TalkData(
|
||||||
|
// content: chunk,
|
||||||
|
// contentType: TalkData_ContentTypeE.G711,
|
||||||
|
// durationMs: ms,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 实际写入单帧到文件(带NALU头判断)
|
// 错误监听
|
||||||
Future<void> _writeSingleFrameToFile(List<int> frameData) async {
|
void _onError(VoiceProcessorException error) {
|
||||||
bool hasNaluHeader = false;
|
AppLog.log(error.message!);
|
||||||
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<int> naluHeader = [0x00, 0x00, 0x01];
|
|
||||||
await state.h264File!
|
|
||||||
.writeAsBytes(naluHeader + frameData, mode: FileMode.append);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 初始化h264文件
|
|
||||||
Future<void> _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<void> _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<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
|
||||||
List<List<int>> splitNalus(List<int> data) {
|
|
||||||
List<List<int>> 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<int>.from(nalu);
|
|
||||||
}
|
|
||||||
} else if (naluType == 8) {
|
|
||||||
// PPS
|
|
||||||
hasPps = true;
|
|
||||||
if (ppsCache == null || !_listEquals(ppsCache!, nalu)) {
|
|
||||||
ppsCache = List<int>.from(nalu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增:List比较工具
|
|
||||||
bool _listEquals(List<int> a, List<int> 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<int> 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<List<int>> nalus = [];
|
|
||||||
// int i = 0;
|
|
||||||
// List<int> 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<int> frameData, int durationMs, int frameSeq, int frameSeqI) {
|
|
||||||
// // 只写入P帧(type 1)
|
|
||||||
// List<List<int>> nalus = [];
|
|
||||||
// int i = 0;
|
|
||||||
// List<int> 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 {
|
void onQualityChanged(String quality) async {
|
||||||
state.currentQuality.value = quality;
|
state.currentQuality.value = quality;
|
||||||
@ -1432,6 +1025,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
|
|||||||
// 判断数据类型,进行分发处理
|
// 判断数据类型,进行分发处理
|
||||||
switch (contentType) {
|
switch (contentType) {
|
||||||
case TalkData_ContentTypeE.G711:
|
case TalkData_ContentTypeE.G711:
|
||||||
|
if (!state.isOpenVoice.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (state.audioBuffer.length >= audioBufferSize) {
|
if (state.audioBuffer.length >= audioBufferSize) {
|
||||||
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
state.audioBuffer.removeAt(0); // 丢弃最旧的数据
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user