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/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); // 0表示A-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); // 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!);
|
||||
_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; // 每次发送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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 统一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); // 0表示A-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();
|
||||
// 兼容部分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 {
|
||||
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); // 丢弃最旧的数据
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user