fix:调整ios的录音发送逻辑

This commit is contained in:
liyi 2025-08-15 13:52:13 +08:00
parent 88db0e850b
commit 47ddb9b72a

View File

@ -8,6 +8,8 @@ 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_sound/flutter_sound.dart';
import 'package:flutter_sound/public/flutter_sound_recorder.dart';
import 'package:flutter_voice_processor/flutter_voice_processor.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:get/get.dart';
@ -51,7 +53,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
int bufferSize = 25; //
int audioBufferSize = 2; // 2
int audioBufferSize = 20; // 2
// frameSeq较小时阈值也小
int _getFrameSeqRolloverThreshold(int lastSeq) {
@ -144,11 +146,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
FlutterPcmSound.setLogLevel(LogLevel.none);
FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1);
// feed
if (Platform.isAndroid) {
FlutterPcmSound.setFeedThreshold(1024); // Android
} else {
FlutterPcmSound.setFeedThreshold(2000); // Android
}
// if (Platform.isAndroid) {
// FlutterPcmSound.setFeedThreshold(1024); // Android
// } else {
// FlutterPcmSound.setFeedThreshold(4096); // Android
// }
}
///
@ -499,15 +501,18 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
///
void _playAudioData(TalkData talkData) async {
if (state.isOpenVoice.value && state.isLoading.isFalse) {
final list =
G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150);
// // PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list);
List<int> encodedData = G711Tool.decode(talkData.content, 0); // 0A-law
// PCM PcmArrayInt16
final PcmArrayInt16 fromList = PcmArrayInt16.fromList(encodedData);
FlutterPcmSound.feed(fromList);
if (!state.isPlaying.value) {
AppLog.log('play');
FlutterPcmSound.play();
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() {
super.onInit();
//
_startListenTalkData();
//
_startListenTalkStatus();
//
@ -596,6 +599,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// H264帧缓冲区
state.h264FrameBuffer.clear();
state.isProcessingFrame = false;
//
_startListenTalkData();
}
@override
@ -639,7 +645,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
// I帧集合
_decodedIFrames.clear();
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
super.onClose();
}
@ -652,33 +660,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
///
void updateTalkExpect() {
// VideoTypeE的映射
final Map<String, VideoTypeE> qualityToVideoType = {
'标清': VideoTypeE.H264,
'高清': VideoTypeE.H264_720P,
//
};
TalkExpectReq talkExpectReq = TalkExpectReq();
state.isOpenVoice.value = !state.isOpenVoice.value;
// videoType
VideoTypeE currentVideoType =
qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264;
if (!state.isOpenVoice.value) {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [],
);
showToast('已静音'.tr);
if (state.isOpenVoice.isTrue) {
FlutterPcmSound.play();
} else {
talkExpectReq = TalkExpectReq(
videoType: [currentVideoType],
audioType: [AudioTypeE.G711],
);
FlutterPcmSound.pause();
}
///
StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer(
talkExpect: talkExpectReq);
}
///
@ -762,8 +749,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
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);
@ -781,7 +771,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} on PlatformException catch (ex) {
// state.errorMessage.value = 'Failed to start recorder: $ex';
}
state.isOpenVoice.value = false;
}
///
@ -803,51 +792,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
} 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); // 0A-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!);
_startProcessingAudioTimer?.cancel();
_startProcessingAudioTimer = null;
_bufferedAudioFrames.clear();
}
//
@ -873,453 +821,98 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
return result;
}
/// h264文件frameType
Future<void> _appendH264FrameToFile(
List<int> frameData, TalkDataH264Frame_FrameTypeE frameType) async {
try {
if (state.h264File == null) {
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');
static const int chunkSize = 320; // 32010ms G.711
static const int intervalMs = 40; // 40ms发送一次4chunk
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,
),
);
}
// NALU起始码为0x00000001
List<int> _ensureStartCode(List<int> nalu) {
if (nalu.length >= 4 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x00 &&
nalu[3] == 0x01) {
return nalu;
} else if (nalu.length >= 3 &&
nalu[0] == 0x00 &&
nalu[1] == 0x00 &&
nalu[2] == 0x01) {
return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3);
} else {
return [0x00, 0x00, 0x00, 0x01] + nalu;
//
Future<void> _onFrame(List<int> frame) async {
final applyGain = _applyGain(frame, 1.6);
// G711数据
List<int> encodedData = G711Tool.encode(applyGain, 0); // 0A-law
_bufferedAudioFrames.addAll(encodedData);
//
if (_startProcessingAudioTimer == null && _bufferedAudioFrames.length > chunkSize) {
_startProcessingAudioTimer = Timer.periodic(Duration(milliseconds: intervalMs), _sendAudioChunk);
}
// 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 {
bool hasNaluHeader = false;
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);
}
//
void _onError(VoiceProcessorException error) {
AppLog.log(error.message!);
}
/// h264文件
Future<void> _initH264File() async {
try {
if (state.h264File != null) return;
// Download目录
Directory? downloadsDir;
if (Platform.isAndroid) {
// Android 10+ getExternalStorageDirectory()
downloadsDir = await getExternalStorageDirectory();
// ROMDownload
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帧包IDRtype 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 {
state.currentQuality.value = quality;
@ -1432,6 +1025,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController {
//
switch (contentType) {
case TalkData_ContentTypeE.G711:
if (!state.isOpenVoice.value) {
return;
}
if (state.audioBuffer.length >= audioBufferSize) {
state.audioBuffer.removeAt(0); //
}