compress method

  1. @override
Future<VideoCompressResult> compress(
  1. String id,
  2. String path,
  3. VideoCompressConfig config,
  4. String? outputDir,
  5. String? outputName,
)
override

Compress path using config. id identifies the job (for progress correlation and cancellation). When outputPath is non-null the encoded file is written there; otherwise it lands in the plugin cache.

Implementation

@override
Future<VideoCompressResult> compress(
  String id,
  String path,
  VideoCompressConfig config,
  String? outputDir,
  String? outputName,
) async {
  // On the web there is no filesystem: the output is a blob: URL and the
  // download name is chosen at saveToDownloads time, so outputDir/outputName
  // don't apply here. Video is always mp4 (mp4-muxer).
  await _ensureLoaded();
  _busy = true;
  try {
    final m = await _rawInfo(path);
    final srcW = (m['width'] as num).toInt();
    final srcH = (m['height'] as num).toInt();
    // Use the same duration `estimate` does, so the two agree. (Web doesn't
    // apply the trim window yet, but the bitrate budget must still match.)
    final durationMs = _clampDuration(
      (m['durationMs'] as num).toInt(),
      config,
    );
    final srcKbps = (m['bitrateKbps'] as num).toInt();
    final srcBytes = (m['sizeBytes'] as num).toInt();
    final (tw, th) = SizeMath.targetDimensions(srcW, srcH, config);
    final videoBps = SizeMath.videoBitrateBps(
      config: config,
      durationMs: durationMs,
      sourceBitrateKbps: srcKbps,
      targetHeight: th,
      // Web v1 drops the audio track, so the whole budget goes to video.
      reserveAudio: false,
    );

    final cfg =
        <String, Object?>{
              'id': id,
              'targetWidth': tw,
              'targetHeight': th,
              'videoBitrateBps': videoBps,
              'frameRate': config.frameRate ?? 30,
              // Requested codec; the JS engine encodes HEVC when the browser supports
              // it, otherwise falls back to H.264 (and reports which was used).
              'codec': config.codec.name,
              // Hit the target closely when a size is requested; stay efficient (VBR)
              // for quality/bitrate modes.
              'bitrateMode': config.targetSizeMB != null
                  ? 'constant'
                  : 'variable',
              'keepOriginalIfLarger': config.keepOriginalIfLarger,
              'minSavingsPercent': config.minSavingsPercent,
              'originalSizeBytes': srcBytes,
            }.jsify()!
            as JSObject;

    final onProgress = (double fraction, double outBytes) {
      _progressCtrl.add(
        CompressionProgress(
          id: id,
          progress: fraction,
          currentOutputBytes: outBytes > 0 ? outBytes.toInt() : null,
        ),
      );
    }.toJS;

    final resJs = await _jsCompress(path, cfg, onProgress).toDart;
    final r = (resJs.dartify() as Map).cast<dynamic, dynamic>();
    return VideoCompressResult(
      id: id,
      outputPath: r['outputUrl'] as String,
      originalSizeBytes: srcBytes,
      compressedSizeBytes: (r['sizeBytes'] as num).toInt(),
      width: (r['width'] as num).toInt(),
      height: (r['height'] as num).toInt(),
      durationMs: (r['durationMs'] as num).toInt(),
      codec: r['codec'] as String,
      skipped: r['skipped'] == true,
      frameRate: (r['frameRate'] as num?)?.toDouble(),
      hasAudio: r['hasAudio'] as bool?,
    );
  } catch (e) {
    final msg = e.toString();
    if (msg.contains('cancelled')) {
      throw VideoCompressCancelledException();
    }
    throw VideoCompressException('compress_failed', msg);
  } finally {
    _busy = false;
  }
}