downloadMetaData method

Future<void> downloadMetaData(
  1. String classId,
  2. String token
)

根据本地 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);
  }
}