downloadMetaData method
根据本地 classId.json 下载视频和文档图片,将在线 URL 替换为本地路径
成功 → classId-success.json;失败 → classId-failed.json
Implementation
Future<void> downloadMetaData(String classId, String token) async {
final cacheDir = await _ensureCacheDir();
final safeId = _safeId(classId);
final metaFile = File(p.join(cacheDir.path, '$safeId.json'));
if (!await metaFile.exists()) {
_downloadingClassIds.remove(classId);
cacheMetadataListeners[classId]?.onError(classId, 1002, '本地缓存文件不存在,请重新缓存');
return;
}
try {
final data =
jsonDecode(await metaFile.readAsString()) as Map<String, dynamic>;
final videoUrl = data['video_url'] as String;
final whiteBoardVideoUrl = data['whiteboard_url'] as String;
final documentPages =
List<Map<String, dynamic>>.from(data['document_pages']);
// 每个课程使用独立子目录,避免多课程缓存互相覆盖
final classCacheDir = Directory(p.join(cacheDir.path, safeId));
if (!await classCacheDir.exists()) {
await classCacheDir.create(recursive: true);
}
final classCachePath = classCacheDir.path;
var completedBytes = 0;
int? lastReportTimeMs;
const reportIntervalMs = 1000;
void reportProgress(int received, int total) {
final now = DateTime.now().millisecondsSinceEpoch;
if (lastReportTimeMs != null && now - lastReportTimeMs! < reportIntervalMs) {
return;
}
lastReportTimeMs = now;
final currentBytes = completedBytes + received;
final totalBytes = completedBytes + (total > 0 ? total : received);
final currentSizeKb = currentBytes / 1024;
final totalSizeKb = totalBytes / 1024;
cacheMetadataListeners[classId]?.onCacheProgress(
classId, currentSizeKb, totalSizeKb);
}
// 老师视频固定命名为 teacher_video.mp4
final localVideoPath = await downloadByUrl(
videoUrl,
savePath: p.join(classCachePath, 'teacher_video.mp4'),
onReceiveProgress: (received, total) => reportProgress(received, total),
);
completedBytes += await File(localVideoPath).length();
// 白板视频固定命名为 whiteboard_video.mp4
final localWhiteBoardPath = await downloadByUrl(
whiteBoardVideoUrl,
savePath: p.join(classCachePath, 'whiteboard_video.mp4'),
onReceiveProgress: (received, total) => reportProgress(received, total),
);
completedBytes += await File(localWhiteBoardPath).length();
// 课件图片以数据中的 offset 命名:${offset}.${imageType}
for (var i = 0; i < documentPages.length; i++) {
final doc = documentPages[i];
final pageIndexList =
List<Map<String, dynamic>>.from(doc['page_index']);
for (var j = 0; j < pageIndexList.length; j++) {
final pageIndex = pageIndexList[j];
final imageUrl = pageIndex['url'] as String;
final offset = pageIndex['offset'] as int? ?? 0;
final imageType =
_extractExt(imageUrl, '.png').replaceFirst('.', '');
final localImagePath = await downloadByUrl(
imageUrl,
savePath: p.join(classCachePath, '$offset.$imageType'),
onReceiveProgress: (received, total) =>
reportProgress(received, total),
);
pageIndexList[j] = {...pageIndex, 'url': localImagePath};
completedBytes += await File(localImagePath).length();
}
documentPages[i] = {...doc, 'page_index': pageIndexList};
}
final successFile = File(p.join(cacheDir.path, '$safeId-success.json'));
await successFile.writeAsString(jsonEncode({
'document_pages': documentPages,
'im_messages': data['im_messages'],
'video_url': localVideoPath,
'whiteboard_url': localWhiteBoardPath,
'class_name': data['class_name'],
}));
final totalSizeKb = completedBytes / 1024;
cacheMetadataListeners[classId]?.onCacheProgress(
classId, totalSizeKb, totalSizeKb);
cacheMetadataListeners[classId]?.onCacheSuccess(classId);
await metaFile.delete();
} catch (e) {
cacheMetadataListeners[classId]?.onError(classId, 1003, '下载元数据失败: $e');
final failedFile = File(p.join(cacheDir.path, '$safeId-failed.json'));
await failedFile.writeAsString(jsonEncode({'error': e.toString()}));
try {
await metaFile.delete();
} catch (_) {}
} finally {
_downloadingClassIds.remove(classId);
}
}