Browse Source

fix(上传): 改进文件上传的稳定性和WAV文件头校验,解决本地录音后即刻转写不完整的问题,录音文件完全稳定后才上传cos

添加文件大小稳定检测机制,防止上传不完整文件
增加WAV文件头校验,确保文件完整性
优化文件移动操作,优先使用rename提高效率
newdev_shunjiawei
lxm 9 months ago
parent
commit
ddcc283c50
  1. 105
      lib/data/services/meeting/meeting_upload_service.dart
  2. 22
      lib/modules/meeting/controllers/meeting_home_controller.dart

105
lib/data/services/meeting/meeting_upload_service.dart

@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:async';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
@ -21,6 +22,9 @@ class MeetingUploadService extends GetxService {
bool _isUpload = false;
int _handled = 0; //队列处理指针,表示当前循环语句处理的任务数量
static const Duration _fileStableTimeout = Duration(seconds: 20);
static const Duration _fileStablePoll = Duration(milliseconds: 350);
static const int _fileStableNeededCount = 3;
@override
void onInit() {
@ -80,8 +84,26 @@ class MeetingUploadService extends GetxService {
meetingId.value = element['id'];
try {
int totalProgress = 0;
final filePath = (element['filepath'] ?? '').toString();
if (filePath.isEmpty) {
throw Exception('Empty upload file path');
}
final fileSizeBytes = await _waitForStableFileSize(
filePath,
timeout: _fileStableTimeout,
pollInterval: _fileStablePoll,
stableNeededCount: _fileStableNeededCount,
);
final isWav = filePath.toLowerCase().endsWith('.wav');
if (isWav) {
final ok = await _isWavHeaderConsistent(filePath, fileSizeBytes);
if (!ok) {
throw Exception('WAV header is inconsistent with file size');
}
}
element['fileSizeBytes'] = fileSizeBytes;
final audiourl = await UploadOss.upload(
filepath: element['filepath'],
filepath: filePath,
rootDir: element['rootdir'],
onSendProgress: (int count, int total) {
// uploadProgress.value = count / total;
@ -139,6 +161,87 @@ class MeetingUploadService extends GetxService {
_isUpload = false;
}
Future<int> _waitForStableFileSize(
String filePath, {
Duration timeout = _fileStableTimeout,
Duration pollInterval = _fileStablePoll,
int stableNeededCount = _fileStableNeededCount,
}) async {
final file = File(filePath);
final deadline = DateTime.now().add(timeout);
int lastSize = -1;
int stableCount = 0;
while (DateTime.now().isBefore(deadline)) {
final exists = await file.exists();
if (exists) {
int size = 0;
try {
size = await file.length();
} catch (_) {
size = 0;
}
if (size > 0) {
if (size == lastSize) {
stableCount += 1;
} else {
stableCount = 0;
lastSize = size;
}
if (stableCount >= stableNeededCount) {
return size;
}
}
}
await Future.delayed(pollInterval);
}
throw TimeoutException('File not stable: $filePath', timeout);
}
Future<bool> _isWavHeaderConsistent(
String filePath, int fileSizeBytes) async {
RandomAccessFile? raf;
try {
raf = await File(filePath).open(mode: FileMode.read);
final header = await raf.read(12);
if (header.length < 12) return false;
final riff = String.fromCharCodes(header.sublist(0, 4));
final wave = String.fromCharCodes(header.sublist(8, 12));
if (riff != 'RIFF' || wave != 'WAVE') return true;
int offset = 12;
const int maxScanBytes = 64 * 1024;
while (offset + 8 <= fileSizeBytes && offset <= maxScanBytes) {
await raf.setPosition(offset);
final chunkHeader = await raf.read(8);
if (chunkHeader.length < 8) return false;
final chunkId = String.fromCharCodes(chunkHeader.sublist(0, 4));
final chunkSize = _readUint32Le(chunkHeader, 4);
final chunkDataStart = offset + 8;
final expectedEnd = chunkDataStart + chunkSize;
if (chunkId == 'data') {
return fileSizeBytes >= expectedEnd;
}
final paddedSize = chunkSize + (chunkSize % 2);
offset = chunkDataStart + paddedSize;
}
return true;
} catch (_) {
return true;
} finally {
try {
await raf?.close();
} catch (_) {}
}
}
int _readUint32Le(List<int> bytes, int offset) {
if (offset + 4 > bytes.length) return 0;
return (bytes[offset]) |
(bytes[offset + 1] << 8) |
(bytes[offset + 2] << 16) |
(bytes[offset + 3] << 24);
}
void removeUpload(int id, {bool isDeleteTask = false}) {
Map uploadData = uploadList.firstWhere(
(element) => element['id'] == id,

22
lib/modules/meeting/controllers/meeting_home_controller.dart

@ -260,8 +260,15 @@ class MeetingHomeController extends GetxController {
final creationTime = fileStat.changed.millisecondsSinceEpoch;
// 准备新路径
final fileName = filePath.split('/').last; //获取文件名(包含扩展名)
final newPath = "${destDir.path}/$fileName"; //构建新路径,保持原文件名
String fileName = filePath.split('/').last; //获取文件名(包含扩展名)
String newPath = "${destDir.path}/$fileName"; //构建新路径,保持原文件名
if (await File(newPath).exists()) {
final ext = path.extension(fileName);
final base = path.basenameWithoutExtension(fileName);
final ts = DateTime.now().millisecondsSinceEpoch;
fileName = '${base}_$ts$ext';
newPath = "${destDir.path}/$fileName";
}
// 构建要存入数据库的音频文件抽象对象
Map<String, Object?> data = {
@ -286,6 +293,13 @@ class MeetingHomeController extends GetxController {
...data,
});
if (meetingResult != null) {
// 移动文件:优先 rename,失败再 copy+delete,确保上传读取的是完整目标文件
try {
await file.rename(newPath);
} catch (_) {
await file.copy(newPath);
await file.delete();
}
// 安排后台上传任务
meetingUploadService.addUpload(meetingId, newPath, 'LocalAudio');
@ -306,10 +320,6 @@ class MeetingHomeController extends GetxController {
});
}
// 移动文件
await file.copy(newPath); // 复制到新路径
await file.delete(); // 删除原文件
addUp++;
}
} catch (e) {

Loading…
Cancel
Save