You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
261 lines
7.9 KiB
261 lines
7.9 KiB
import 'dart:io';
|
|
import 'dart:async';
|
|
import 'package:get/get.dart';
|
|
import 'package:get_storage/get_storage.dart';
|
|
|
|
import '../../../core/utils/logger.dart';
|
|
import '../../../core/utils/upload_oss.dart';
|
|
import '../network/api.dart';
|
|
import '../usage_stats_service.dart';
|
|
import '../db/sqflite_api.dart';
|
|
import 'meeting_task_service.dart';
|
|
|
|
/// 管理会议音频文件上传服务
|
|
class MeetingUploadService extends GetxService {
|
|
final _taskService = Get.find<MeetingTaskService>();
|
|
|
|
final GetStorage _storage = GetStorage();
|
|
|
|
RxInt meetingId = 0.obs;
|
|
RxDouble uploadProgress = 0.0.obs;
|
|
RxList uploadList = [].obs;
|
|
|
|
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() {
|
|
super.onInit();
|
|
_getUploadList();
|
|
}
|
|
|
|
void cancelUpload() {
|
|
_isUpload = false;
|
|
_handled = 0;
|
|
uploadList.clear();
|
|
_storage.remove("meeting_upload_list");
|
|
}
|
|
|
|
void _getUploadList() {
|
|
uploadList.value = _storage.read("meeting_upload_list") ?? [];
|
|
executeUpload();
|
|
}
|
|
|
|
/// 添加待上传的音频文件
|
|
void addUpload(
|
|
int id,
|
|
String filePath,
|
|
String rootDir, {
|
|
bool isUpload = true,
|
|
}) async {
|
|
int fileSize = 0;
|
|
try {
|
|
fileSize = await File(filePath).length();
|
|
} catch (_) {}
|
|
uploadList.add({
|
|
'id': id,
|
|
'filepath': filePath,
|
|
'rootdir': rootDir,
|
|
'audiourl': '',
|
|
'isTask': false,
|
|
'formlanguage': '',
|
|
'tolanguage': '',
|
|
'isdistinguishspeaker': true,
|
|
'templateid': 0,
|
|
'fileSizeBytes': fileSize,
|
|
});
|
|
_storage.write("meeting_upload_list", uploadList);
|
|
if (isUpload) {
|
|
executeUpload();
|
|
}
|
|
}
|
|
|
|
void executeUpload() async {
|
|
if (_isUpload) return;
|
|
_isUpload = true;
|
|
_handled = 0;
|
|
while (_handled < uploadList.length) {
|
|
Map element = uploadList[_handled];
|
|
_handled++;
|
|
if (element['audiourl'].isEmpty) {
|
|
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: filePath,
|
|
rootDir: element['rootdir'],
|
|
onSendProgress: (int count, int total) {
|
|
// uploadProgress.value = count / total;
|
|
totalProgress = total;
|
|
uploadProgress.value = total > 0 ? count / total : 0.0;
|
|
},
|
|
);
|
|
await Api.upRecordEchomeet({
|
|
'id': element['id'],
|
|
'audiourl': audiourl,
|
|
});
|
|
await SqfliteApi.editMeetingTitle(
|
|
element['id'],
|
|
{'audiourl': audiourl},
|
|
);
|
|
try {
|
|
// 记录对象存储用量
|
|
final svc = Get.isRegistered<UsageStatsService>()
|
|
? Get.find<UsageStatsService>()
|
|
: Get.put(UsageStatsService(), permanent: true);
|
|
int fileSizeBytes = 0;
|
|
try {
|
|
fileSizeBytes = element['fileSizeBytes'] ?? 0;
|
|
} catch (_) {}
|
|
final transferred =
|
|
totalProgress > 0 ? totalProgress : fileSizeBytes;
|
|
svc.recordMeetingObjectStorage(
|
|
meetingId: element['id'],
|
|
storageUrl: audiourl,
|
|
storageBytes: fileSizeBytes,
|
|
transferBytes: transferred,
|
|
uploadTimestamp: DateTime.now().millisecondsSinceEpoch,
|
|
);
|
|
} catch (e) {
|
|
Logger.w('MeetingOSrecord', '对象存储用量记录失败: $e');
|
|
}
|
|
if (element['isTask']) {
|
|
await _taskService.submitTask(
|
|
element['id'],
|
|
element['formlanguage'],
|
|
element['tolanguage'],
|
|
element['isdistinguishspeaker'],
|
|
element['templateid'],
|
|
element['tid'] ?? '',
|
|
);
|
|
}
|
|
element['audiourl'] = audiourl;
|
|
uploadProgress.value = 0.0;
|
|
_storage.write("meeting_upload_list", uploadList);
|
|
} catch (e) {
|
|
Logger.error('Upload failed: $e');
|
|
}
|
|
}
|
|
}
|
|
meetingId.value = 0;
|
|
_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,
|
|
orElse: () => {},
|
|
);
|
|
if (uploadData.isNotEmpty) {
|
|
if (isDeleteTask || uploadData['audiourl'].isNotEmpty) {
|
|
uploadList.remove(uploadData);
|
|
_storage.write("meeting_upload_list", uploadList);
|
|
if (uploadList.isEmpty) {
|
|
_handled = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|