capacitor-native-video-compressor 1.0.5 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile, toBlobURL } from '@ffmpeg/util';\n\nimport type {\n NativeVideoCompressorPlugin,\n CompressOptions,\n CompressResult,\n CancelOptions,\n CancelResult,\n} from './definitions';\nimport { CANCELLED } from './definitions';\n\n/**\n * Rejection shape for a cancelled compress. `code` matches what `call.reject`\n * puts on the error object on iOS/Android, so one `error.code === 'CANCELLED'`\n * check works on every platform.\n */\nclass CancelledError extends Error {\n code = CANCELLED;\n constructor() {\n super('Đã hủy nén');\n this.name = 'CancelledError';\n }\n}\n\nconst CORE_BASE_URL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n\n/**\n * Cancelling means terminating the worker, which drops the loaded core with\n * it — so without this the next compress re-downloads ~30 MB of wasm and\n * \"cancel\" quietly costs more than it saves. Module scope, not instance: the\n * bytes are identical for every instance and every job.\n */\nlet corePromise: Promise<{ coreURL: string; wasmURL: string }> | null = null;\n\nconst loadCoreURLs = () => {\n if (!corePromise) {\n corePromise = Promise.all([\n toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.js`, 'text/javascript'),\n toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.wasm`, 'application/wasm'),\n ])\n .then(([coreURL, wasmURL]) => ({ coreURL, wasmURL }))\n .catch((e) => {\n // Don't cache a failure — a flaky network shouldn't make every later\n // compress in this session fail too.\n corePromise = null;\n throw e;\n });\n }\n return corePromise;\n};\n\nexport class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {\n private ffmpeg: FFmpeg | null = null;\n private isLoaded = false;\n /** The job `compressVideo` is running right now, for `cancel({ jobId })`. */\n private currentJobId: string | null = null;\n private cancelRequested = false;\n\n private async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg) return;\n\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n\n // Built on a local, published to the field only once it is ready and still\n // wanted. Assigning `this.ffmpeg` up front and setting `isLoaded` after the\n // await left a window where a cancel could null the instance and the load,\n // finishing a moment later, would set `isLoaded = true` on nothing at all —\n // after which every compressVideo threw 'FFmpeg failed to load' for the\n // rest of the page's life.\n const instance = new FFmpeg();\n\n // Listen to FFmpeg progress and map it to capacitor plugin events\n instance.on('progress', ({ progress }) => {\n // progress is a fraction between 0 and 1\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n\n instance.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n\n try {\n // Use standard single-threaded version for best compatibility across browsers\n // without requiring complex SharedArrayBuffer headers configuration\n console.log('Starting to load FFmpeg core from:', CORE_BASE_URL);\n await instance.load(await loadCoreURLs());\n } catch (e: any) {\n instance.terminate();\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n\n // Cancelled while this was loading. Terminate what we just built rather\n // than publish it — the caller asked for none of it.\n if (this.cancelRequested) {\n instance.terminate();\n throw new CancelledError();\n }\n\n this.ffmpeg = instance;\n this.isLoaded = true;\n console.log('FFmpeg core loaded successfully');\n }\n\n /**\n * Kill the worker outright — there is no gentler option.\n *\n * `ffmpeg.exec()` runs *synchronously* inside the worker (see\n * @ffmpeg/ffmpeg's worker.js: `ffmpeg.exec(...args)` is not awaited), so the\n * worker never reaches a message poll while encoding. The AbortSignal that\n * `exec()` accepts only rejects the main-thread promise; the encode keeps\n * burning CPU behind it. `worker.terminate()` is what actually stops it.\n *\n * The cost is the loaded core, which is why the core URLs are cached above.\n */\n async cancel(options?: CancelOptions): Promise<CancelResult> {\n if (!this.currentJobId) return { cancelled: false };\n if (options?.jobId && options.jobId !== this.currentJobId) return { cancelled: false };\n\n // Set even when there is no instance yet: the job may still be inside\n // loadFFmpeg, which reads this flag before publishing what it built.\n this.cancelRequested = true;\n this.ffmpeg?.terminate();\n this.ffmpeg = null;\n this.isLoaded = false;\n return { cancelled: true };\n }\n\n async initialize(): Promise<{ success: boolean; message?: string }> {\n try {\n if (this.isLoaded) {\n return { success: true, message: 'Already initialized' };\n }\n await this.loadFFmpeg();\n return { success: true, message: 'Initialization successful' };\n } catch (e: any) {\n return { success: false, message: e.message || 'Initialization failed' };\n }\n }\n\n async compressVideo(options: CompressOptions): Promise<CompressResult> {\n this.currentJobId = options.jobId ?? 'default';\n this.cancelRequested = false;\n\n // Checked at every stage boundary, not just around exec: terminate() only\n // reaches work that has already got to the worker, so a cancel during the\n // core download or the source fetch has nothing to interrupt and would\n // otherwise let the compress carry on as if nothing had happened.\n const throwIfCancelled = () => {\n if (this.cancelRequested) throw new CancelledError();\n };\n\n try {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n throwIfCancelled();\n\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n // Bound once. `this.ffmpeg` is nulled by cancel(), so reading it after\n // any of the four awaits below can hand back null on a reference the\n // compiler narrowed at this line — surfacing a cancel as a TypeError\n // instead of a CancelledError.\n const ffmpeg = this.ffmpeg;\n\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n\n const { sourcePath, quality = 'MEDIUM' } = options;\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n\n // Map quality to CRF, Scale, and Audio Bitrate\n let crf = '28';\n let scale = ''; // default keep resolution\n let audioBitrate = '128k';\n\n switch (quality) {\n case 'VERY_HIGH': // 1080p\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH': // 720p\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM': // 540p\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW': // 480p\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P': // 360p\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW': // 240p\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n\n console.log('[FFmpeg] Fetching source file...', sourcePath);\n let inputData: Uint8Array;\n try {\n inputData = await fetchFile(sourcePath);\n } catch (err) {\n console.error('[FFmpeg] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n throwIfCancelled();\n\n console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');\n try {\n await ffmpeg.writeFile(inputFileName, inputData);\n } catch (err) {\n if (this.cancelRequested) throw new CancelledError();\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n throwIfCancelled();\n\n // Construct FFmpeg command array\n const args: string[] = ['-i', inputFileName];\n\n // Video codec and compression parameters (superfast provides much better size than ultrafast)\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n\n // Resize if needed (split by space)\n if (scale) {\n args.push(...scale.split(' '));\n }\n\n // Audio re-encoding to save space\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n\n // Output\n args.push(outputFileName);\n\n console.log('[FFmpeg] Executing command:', args.join(' '));\n try {\n const retCode = await ffmpeg.exec(args);\n console.log('[FFmpeg] Exec returned code:', retCode);\n } catch (err) {\n // terminate() rejects every in-flight call, so a cancel arrives here\n // looking like a generic failure unless we name it.\n if (this.cancelRequested) throw new CancelledError();\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n throwIfCancelled();\n\n console.log('[FFmpeg] Reading output file...');\n let data: any;\n try {\n data = await ffmpeg.readFile(outputFileName);\n } catch (err) {\n if (this.cancelRequested) throw new CancelledError();\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n\n // Free memory\n await ffmpeg.deleteFile(inputFileName);\n await ffmpeg.deleteFile(outputFileName);\n\n console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');\n // Create a blob URL for the resulting file\n const blob = new Blob([data as any], { type: 'video/mp4' });\n const destPath = URL.createObjectURL(blob);\n\n console.log('[FFmpeg] Compression complete, returning path:', destPath);\n return {\n success: true,\n destPath: destPath,\n };\n } catch (error: any) {\n if (error?.code === CANCELLED) {\n console.log('[FFmpeg] Compression cancelled');\n } else {\n console.error('[FFmpeg] Compression process failed:', error);\n }\n throw error;\n } finally {\n this.currentJobId = null;\n this.cancelRequested = false;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { toBlobURL } from '@ffmpeg/util';\nimport {\n ALL_FORMATS,\n BlobSource,\n BufferTarget,\n Conversion,\n ConversionCanceledError,\n Input,\n Mp4OutputFormat,\n Output,\n Quality,\n canEncodeVideo,\n} from 'mediabunny';\n\nimport type {\n NativeVideoCompressorPlugin,\n CompressOptions,\n CompressResult,\n CancelOptions,\n CancelResult,\n} from './definitions';\nimport { CANCELLED } from './definitions';\n\ntype VideoQuality = NonNullable<CompressOptions['quality']>;\n\n/**\n * Rejection shape for a cancelled compress. `code` matches what `call.reject`\n * puts on the error object on iOS/Android, so one `error.code === 'CANCELLED'`\n * check works on every platform — and on both web engines below.\n */\nclass CancelledError extends Error {\n code = CANCELLED;\n constructor() {\n super('Đã hủy nén');\n this.name = 'CancelledError';\n }\n}\n\n/**\n * WebCodecs target per quality tier: cap height + a bitrate ceiling. The\n * quantizer (constant 26, CRF-like) drives quality where the encoder honours\n * it; H.264 on most browsers is bitrate-driven, so `bitrate` is the real knob.\n * fps is capped to 30 at call time (never upsampled). Audio is left to\n * mediabunny, which re-encodes to AAC for the MP4 container.\n */\nconst WEBCODECS_LADDER: Record<VideoQuality, { height: number; bitrate: number }> = {\n VERY_HIGH: { height: 1080, bitrate: 4000000 },\n HIGH: { height: 720, bitrate: 2000000 },\n MEDIUM: { height: 540, bitrate: 1200000 },\n LOW: { height: 480, bitrate: 1000000 },\n '360P': { height: 360, bitrate: 700000 },\n VERY_LOW: { height: 240, bitrate: 350000 },\n};\n\nconst WEBCODECS_QUANTIZER = 26;\nconst FPS_CAP = 30;\n\nconst CORE_BASE_URL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n\n/**\n * Cancelling means terminating the worker, which drops the loaded core with\n * it — so without this the next compress re-downloads ~30 MB of wasm and\n * \"cancel\" quietly costs more than it saves. Module scope, not instance: the\n * bytes are identical for every instance and every job. Only the ffmpeg\n * fallback ever pays this download; the WebCodecs primary path needs no core.\n */\nlet corePromise: Promise<{ coreURL: string; wasmURL: string }> | null = null;\n\nconst loadCoreURLs = () => {\n if (!corePromise) {\n corePromise = Promise.all([\n toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.js`, 'text/javascript'),\n toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.wasm`, 'application/wasm'),\n ])\n .then(([coreURL, wasmURL]) => ({ coreURL, wasmURL }))\n .catch((e) => {\n // Don't cache a failure — a flaky network shouldn't make every later\n // compress in this session fail too.\n corePromise = null;\n throw e;\n });\n }\n return corePromise;\n};\n\nexport class NativeVideoCompressorWeb extends WebPlugin implements NativeVideoCompressorPlugin {\n private ffmpeg: FFmpeg | null = null;\n private isLoaded = false;\n /** The mediabunny conversion running now, so `cancel()` can abort it. */\n private activeConversion: Conversion | null = null;\n /** The job `compressVideo` is running right now, for `cancel({ jobId })`. */\n private currentJobId: string | null = null;\n private cancelRequested = false;\n\n async initialize(): Promise<{ success: boolean; message?: string }> {\n // WebCodecs (the primary path) needs no core download, so there is nothing\n // to warm. Eagerly loading the ffmpeg core here would pull ~30 MB for a\n // path most compresses never take. The fallback lazy-loads its core on\n // first use. Kept as a resolving no-op so callers that await it still pass.\n return { success: true, message: 'ready' };\n }\n\n async compressVideo(options: CompressOptions): Promise<CompressResult> {\n this.currentJobId = options.jobId ?? 'default';\n this.cancelRequested = false;\n\n // Checked at every stage boundary, not just around the encode: cancelling\n // an engine only reaches work already handed to it, so a cancel during the\n // source fetch or before init has nothing to interrupt and would otherwise\n // let the compress carry on as if nothing had happened.\n const throwIfCancelled = () => {\n if (this.cancelRequested) throw new CancelledError();\n };\n\n try {\n throwIfCancelled();\n\n const blob = await this.fetchSourceBlob(options.sourcePath);\n throwIfCancelled();\n\n const quality: VideoQuality = options.quality ?? 'MEDIUM';\n\n if (await this.canUseWebCodecs(quality)) {\n try {\n const result = await this.compressWithWebCodecs(blob, quality, throwIfCancelled);\n if (result) return result;\n // null → conversion invalid / tracks discarded → fall back to ffmpeg.\n } catch (e: any) {\n if (e?.code === CANCELLED) throw e; // user cancel: never fall back\n console.warn('[compress] WebCodecs failed, falling back to ffmpeg:', e);\n throwIfCancelled();\n }\n }\n\n return await this.compressWithFfmpeg(blob, quality, throwIfCancelled);\n } catch (error: any) {\n if (error?.code === CANCELLED) {\n console.log('[compress] cancelled');\n } else {\n console.error('[compress] failed:', error);\n }\n throw error;\n } finally {\n this.currentJobId = null;\n this.cancelRequested = false;\n this.activeConversion = null;\n }\n }\n\n /**\n * Kill whichever engine is mid-compress. Only one is ever active at a time,\n * so acting on both is safe.\n *\n * WebCodecs: `conversion.cancel()` makes the in-flight `execute()` reject\n * with `ConversionCanceledError`, surfaced as a `CancelledError` below.\n * ffmpeg: `exec()` runs synchronously inside the worker and never reaches a\n * message poll while encoding, so `worker.terminate()` is the only way to\n * stop it — its cost is the loaded core, which is why the core URLs are\n * cached above.\n */\n async cancel(options?: CancelOptions): Promise<CancelResult> {\n if (!this.currentJobId) return { cancelled: false };\n if (options?.jobId && options.jobId !== this.currentJobId) return { cancelled: false };\n\n // Set even when neither engine exists yet: the job may still be fetching\n // the source or inside loadFFmpeg, which read this flag at their boundaries.\n this.cancelRequested = true;\n\n const conversion = this.activeConversion;\n this.activeConversion = null;\n // Fire-and-forget: the CancelledError surfaces from the execute() rejection,\n // not from here; awaiting would only make cancel() slower.\n conversion?.cancel().catch(() => undefined);\n\n this.ffmpeg?.terminate();\n this.ffmpeg = null;\n this.isLoaded = false;\n\n return { cancelled: true };\n }\n\n private async fetchSourceBlob(sourcePath: string): Promise<Blob> {\n try {\n const response = await fetch(sourcePath);\n return await response.blob();\n } catch (err) {\n console.error('[compress] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n }\n\n private async canUseWebCodecs(quality: VideoQuality): Promise<boolean> {\n try {\n const { bitrate } = WEBCODECS_LADDER[quality];\n return await canEncodeVideo('avc', {\n quality: new Quality({ quantizer: WEBCODECS_QUANTIZER, bitrate }),\n });\n } catch {\n return false;\n }\n }\n\n private async compressWithWebCodecs(\n blob: Blob,\n quality: VideoQuality,\n throwIfCancelled: () => void,\n ): Promise<CompressResult | null> {\n const { height, bitrate } = WEBCODECS_LADDER[quality];\n\n const input = new Input({ formats: ALL_FORMATS, source: new BlobSource(blob) });\n const output = new Output({ format: new Mp4OutputFormat(), target: new BufferTarget() });\n\n // Cap fps at 30, but never upsample: probe the source and take the min.\n // A failed probe just omits frameRate, leaving the source rate untouched.\n let frameRate: number | undefined;\n try {\n const track = await input.getPrimaryVideoTrack();\n if (track) {\n const stats = await track.computePacketStats(100);\n if (stats.averagePacketRate > 0) {\n frameRate = Math.min(FPS_CAP, Math.round(stats.averagePacketRate));\n }\n }\n } catch {\n frameRate = undefined;\n }\n\n throwIfCancelled();\n\n const conversion = await Conversion.init({\n input,\n output,\n video: {\n height,\n quality: new Quality({ quantizer: WEBCODECS_QUANTIZER, bitrate }),\n hardwareAcceleration: 'prefer-hardware',\n ...(frameRate ? { frameRate } : {}),\n },\n });\n\n if (!conversion.isValid) {\n console.warn(\n '[compress] WebCodecs conversion invalid:',\n conversion.discardedTracks.map((t) => t.reason),\n );\n return null;\n }\n\n this.activeConversion = conversion;\n\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n conversion.onProgress = (progress) => {\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n };\n\n throwIfCancelled();\n try {\n await conversion.execute();\n } catch (e: any) {\n if (this.cancelRequested || e instanceof ConversionCanceledError) throw new CancelledError();\n throw e;\n }\n\n const buffer = output.target.buffer;\n if (!buffer) throw new Error('WebCodecs produced no output');\n\n const outBlob = new Blob([buffer], { type: 'video/mp4' });\n return { success: true, destPath: URL.createObjectURL(outBlob) };\n }\n\n private async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg) return;\n\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n\n // Built on a local, published to the field only once it is ready and still\n // wanted. Assigning `this.ffmpeg` up front and setting `isLoaded` after the\n // await left a window where a cancel could null the instance and the load,\n // finishing a moment later, would set `isLoaded = true` on nothing at all —\n // after which every compressVideo threw 'FFmpeg failed to load' for the\n // rest of the page's life.\n const instance = new FFmpeg();\n\n instance.on('progress', ({ progress }) => {\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n\n instance.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n\n try {\n await instance.load(await loadCoreURLs());\n } catch (e: any) {\n instance.terminate();\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n\n // Cancelled while this was loading. Terminate what we just built rather\n // than publish it — the caller asked for none of it.\n if (this.cancelRequested) {\n instance.terminate();\n throw new CancelledError();\n }\n\n this.ffmpeg = instance;\n this.isLoaded = true;\n }\n\n private async compressWithFfmpeg(\n blob: Blob,\n quality: VideoQuality,\n throwIfCancelled: () => void,\n ): Promise<CompressResult> {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n throwIfCancelled();\n\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n // Bound once. `this.ffmpeg` is nulled by cancel(), so reading it after any\n // of the awaits below can hand back null on a reference the compiler\n // narrowed here — surfacing a cancel as a TypeError, not a CancelledError.\n const ffmpeg = this.ffmpeg;\n\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n\n let crf = '28';\n let scale = '';\n let audioBitrate = '128k';\n\n switch (quality) {\n case 'VERY_HIGH':\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH':\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM':\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW':\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P':\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW':\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n\n const inputData = new Uint8Array(await blob.arrayBuffer());\n\n try {\n await ffmpeg.writeFile(inputFileName, inputData);\n } catch (err) {\n if (this.cancelRequested) throw new CancelledError();\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n throwIfCancelled();\n\n const args: string[] = ['-i', inputFileName];\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n if (scale) {\n args.push(...scale.split(' '));\n }\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n args.push(outputFileName);\n\n try {\n await ffmpeg.exec(args);\n } catch (err) {\n // terminate() rejects every in-flight call, so a cancel arrives here\n // looking like a generic failure unless we name it.\n if (this.cancelRequested) throw new CancelledError();\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n throwIfCancelled();\n\n let data: any;\n try {\n data = await ffmpeg.readFile(outputFileName);\n } catch (err) {\n if (this.cancelRequested) throw new CancelledError();\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n\n await ffmpeg.deleteFile(inputFileName);\n await ffmpeg.deleteFile(outputFileName);\n\n const outBlob = new Blob([data as any], { type: 'video/mp4' });\n return { success: true, destPath: URL.createObjectURL(outBlob) };\n }\n}\n"]}
@@ -3,6 +3,7 @@
3
3
  var core = require('@capacitor/core');
4
4
  var ffmpeg = require('@ffmpeg/ffmpeg');
5
5
  var util = require('@ffmpeg/util');
6
+ var mediabunny = require('mediabunny');
6
7
 
7
8
  /**
8
9
  * `code` on the error `compressVideo` rejects with when `cancel()` stopped it.
@@ -18,7 +19,7 @@ const NativeVideoCompressor = core.registerPlugin('NativeVideoCompressor', {
18
19
  /**
19
20
  * Rejection shape for a cancelled compress. `code` matches what `call.reject`
20
21
  * puts on the error object on iOS/Android, so one `error.code === 'CANCELLED'`
21
- * check works on every platform.
22
+ * check works on every platform — and on both web engines below.
22
23
  */
23
24
  class CancelledError extends Error {
24
25
  constructor() {
@@ -27,12 +28,30 @@ class CancelledError extends Error {
27
28
  this.name = 'CancelledError';
28
29
  }
29
30
  }
31
+ /**
32
+ * WebCodecs target per quality tier: cap height + a bitrate ceiling. The
33
+ * quantizer (constant 26, CRF-like) drives quality where the encoder honours
34
+ * it; H.264 on most browsers is bitrate-driven, so `bitrate` is the real knob.
35
+ * fps is capped to 30 at call time (never upsampled). Audio is left to
36
+ * mediabunny, which re-encodes to AAC for the MP4 container.
37
+ */
38
+ const WEBCODECS_LADDER = {
39
+ VERY_HIGH: { height: 1080, bitrate: 4000000 },
40
+ HIGH: { height: 720, bitrate: 2000000 },
41
+ MEDIUM: { height: 540, bitrate: 1200000 },
42
+ LOW: { height: 480, bitrate: 1000000 },
43
+ '360P': { height: 360, bitrate: 700000 },
44
+ VERY_LOW: { height: 240, bitrate: 350000 },
45
+ };
46
+ const WEBCODECS_QUANTIZER = 26;
47
+ const FPS_CAP = 30;
30
48
  const CORE_BASE_URL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
31
49
  /**
32
50
  * Cancelling means terminating the worker, which drops the loaded core with
33
51
  * it — so without this the next compress re-downloads ~30 MB of wasm and
34
52
  * "cancel" quietly costs more than it saves. Module scope, not instance: the
35
- * bytes are identical for every instance and every job.
53
+ * bytes are identical for every instance and every job. Only the ffmpeg
54
+ * fallback ever pays this download; the WebCodecs primary path needs no core.
36
55
  */
37
56
  let corePromise = null;
38
57
  const loadCoreURLs = () => {
@@ -56,10 +75,167 @@ class NativeVideoCompressorWeb extends core.WebPlugin {
56
75
  super(...arguments);
57
76
  this.ffmpeg = null;
58
77
  this.isLoaded = false;
78
+ /** The mediabunny conversion running now, so `cancel()` can abort it. */
79
+ this.activeConversion = null;
59
80
  /** The job `compressVideo` is running right now, for `cancel({ jobId })`. */
60
81
  this.currentJobId = null;
61
82
  this.cancelRequested = false;
62
83
  }
84
+ async initialize() {
85
+ // WebCodecs (the primary path) needs no core download, so there is nothing
86
+ // to warm. Eagerly loading the ffmpeg core here would pull ~30 MB for a
87
+ // path most compresses never take. The fallback lazy-loads its core on
88
+ // first use. Kept as a resolving no-op so callers that await it still pass.
89
+ return { success: true, message: 'ready' };
90
+ }
91
+ async compressVideo(options) {
92
+ var _a, _b;
93
+ this.currentJobId = (_a = options.jobId) !== null && _a !== void 0 ? _a : 'default';
94
+ this.cancelRequested = false;
95
+ // Checked at every stage boundary, not just around the encode: cancelling
96
+ // an engine only reaches work already handed to it, so a cancel during the
97
+ // source fetch or before init has nothing to interrupt and would otherwise
98
+ // let the compress carry on as if nothing had happened.
99
+ const throwIfCancelled = () => {
100
+ if (this.cancelRequested)
101
+ throw new CancelledError();
102
+ };
103
+ try {
104
+ throwIfCancelled();
105
+ const blob = await this.fetchSourceBlob(options.sourcePath);
106
+ throwIfCancelled();
107
+ const quality = (_b = options.quality) !== null && _b !== void 0 ? _b : 'MEDIUM';
108
+ if (await this.canUseWebCodecs(quality)) {
109
+ try {
110
+ const result = await this.compressWithWebCodecs(blob, quality, throwIfCancelled);
111
+ if (result)
112
+ return result;
113
+ // null → conversion invalid / tracks discarded → fall back to ffmpeg.
114
+ }
115
+ catch (e) {
116
+ if ((e === null || e === void 0 ? void 0 : e.code) === CANCELLED)
117
+ throw e; // user cancel: never fall back
118
+ console.warn('[compress] WebCodecs failed, falling back to ffmpeg:', e);
119
+ throwIfCancelled();
120
+ }
121
+ }
122
+ return await this.compressWithFfmpeg(blob, quality, throwIfCancelled);
123
+ }
124
+ catch (error) {
125
+ if ((error === null || error === void 0 ? void 0 : error.code) === CANCELLED) {
126
+ console.log('[compress] cancelled');
127
+ }
128
+ else {
129
+ console.error('[compress] failed:', error);
130
+ }
131
+ throw error;
132
+ }
133
+ finally {
134
+ this.currentJobId = null;
135
+ this.cancelRequested = false;
136
+ this.activeConversion = null;
137
+ }
138
+ }
139
+ /**
140
+ * Kill whichever engine is mid-compress. Only one is ever active at a time,
141
+ * so acting on both is safe.
142
+ *
143
+ * WebCodecs: `conversion.cancel()` makes the in-flight `execute()` reject
144
+ * with `ConversionCanceledError`, surfaced as a `CancelledError` below.
145
+ * ffmpeg: `exec()` runs synchronously inside the worker and never reaches a
146
+ * message poll while encoding, so `worker.terminate()` is the only way to
147
+ * stop it — its cost is the loaded core, which is why the core URLs are
148
+ * cached above.
149
+ */
150
+ async cancel(options) {
151
+ var _a;
152
+ if (!this.currentJobId)
153
+ return { cancelled: false };
154
+ if ((options === null || options === void 0 ? void 0 : options.jobId) && options.jobId !== this.currentJobId)
155
+ return { cancelled: false };
156
+ // Set even when neither engine exists yet: the job may still be fetching
157
+ // the source or inside loadFFmpeg, which read this flag at their boundaries.
158
+ this.cancelRequested = true;
159
+ const conversion = this.activeConversion;
160
+ this.activeConversion = null;
161
+ // Fire-and-forget: the CancelledError surfaces from the execute() rejection,
162
+ // not from here; awaiting would only make cancel() slower.
163
+ conversion === null || conversion === void 0 ? void 0 : conversion.cancel().catch(() => undefined);
164
+ (_a = this.ffmpeg) === null || _a === void 0 ? void 0 : _a.terminate();
165
+ this.ffmpeg = null;
166
+ this.isLoaded = false;
167
+ return { cancelled: true };
168
+ }
169
+ async fetchSourceBlob(sourcePath) {
170
+ try {
171
+ const response = await fetch(sourcePath);
172
+ return await response.blob();
173
+ }
174
+ catch (err) {
175
+ console.error('[compress] Error fetching source file:', err);
176
+ throw new Error('Could not fetch source video file');
177
+ }
178
+ }
179
+ async canUseWebCodecs(quality) {
180
+ try {
181
+ const { bitrate } = WEBCODECS_LADDER[quality];
182
+ return await mediabunny.canEncodeVideo('avc', {
183
+ quality: new mediabunny.Quality({ quantizer: WEBCODECS_QUANTIZER, bitrate }),
184
+ });
185
+ }
186
+ catch (_a) {
187
+ return false;
188
+ }
189
+ }
190
+ async compressWithWebCodecs(blob, quality, throwIfCancelled) {
191
+ const { height, bitrate } = WEBCODECS_LADDER[quality];
192
+ const input = new mediabunny.Input({ formats: mediabunny.ALL_FORMATS, source: new mediabunny.BlobSource(blob) });
193
+ const output = new mediabunny.Output({ format: new mediabunny.Mp4OutputFormat(), target: new mediabunny.BufferTarget() });
194
+ // Cap fps at 30, but never upsample: probe the source and take the min.
195
+ // A failed probe just omits frameRate, leaving the source rate untouched.
196
+ let frameRate;
197
+ try {
198
+ const track = await input.getPrimaryVideoTrack();
199
+ if (track) {
200
+ const stats = await track.computePacketStats(100);
201
+ if (stats.averagePacketRate > 0) {
202
+ frameRate = Math.min(FPS_CAP, Math.round(stats.averagePacketRate));
203
+ }
204
+ }
205
+ }
206
+ catch (_a) {
207
+ frameRate = undefined;
208
+ }
209
+ throwIfCancelled();
210
+ const conversion = await mediabunny.Conversion.init({
211
+ input,
212
+ output,
213
+ video: Object.assign({ height, quality: new mediabunny.Quality({ quantizer: WEBCODECS_QUANTIZER, bitrate }), hardwareAcceleration: 'prefer-hardware' }, (frameRate ? { frameRate } : {})),
214
+ });
215
+ if (!conversion.isValid) {
216
+ console.warn('[compress] WebCodecs conversion invalid:', conversion.discardedTracks.map((t) => t.reason));
217
+ return null;
218
+ }
219
+ this.activeConversion = conversion;
220
+ this.notifyListeners('onProgress', { status: 'started', percent: 0 });
221
+ conversion.onProgress = (progress) => {
222
+ this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
223
+ };
224
+ throwIfCancelled();
225
+ try {
226
+ await conversion.execute();
227
+ }
228
+ catch (e) {
229
+ if (this.cancelRequested || e instanceof mediabunny.ConversionCanceledError)
230
+ throw new CancelledError();
231
+ throw e;
232
+ }
233
+ const buffer = output.target.buffer;
234
+ if (!buffer)
235
+ throw new Error('WebCodecs produced no output');
236
+ const outBlob = new Blob([buffer], { type: 'video/mp4' });
237
+ return { success: true, destPath: URL.createObjectURL(outBlob) };
238
+ }
63
239
  async loadFFmpeg() {
64
240
  if (this.isLoaded && this.ffmpeg)
65
241
  return;
@@ -71,18 +247,13 @@ class NativeVideoCompressorWeb extends core.WebPlugin {
71
247
  // after which every compressVideo threw 'FFmpeg failed to load' for the
72
248
  // rest of the page's life.
73
249
  const instance = new ffmpeg.FFmpeg();
74
- // Listen to FFmpeg progress and map it to capacitor plugin events
75
250
  instance.on('progress', ({ progress }) => {
76
- // progress is a fraction between 0 and 1
77
251
  this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
78
252
  });
79
253
  instance.on('log', ({ message }) => {
80
254
  console.log('[FFmpeg Log]', message);
81
255
  });
82
256
  try {
83
- // Use standard single-threaded version for best compatibility across browsers
84
- // without requiring complex SharedArrayBuffer headers configuration
85
- console.log('Starting to load FFmpeg core from:', CORE_BASE_URL);
86
257
  await instance.load(await loadCoreURLs());
87
258
  }
88
259
  catch (e) {
@@ -98,194 +269,101 @@ class NativeVideoCompressorWeb extends core.WebPlugin {
98
269
  }
99
270
  this.ffmpeg = instance;
100
271
  this.isLoaded = true;
101
- console.log('FFmpeg core loaded successfully');
102
272
  }
103
- /**
104
- * Kill the worker outright — there is no gentler option.
105
- *
106
- * `ffmpeg.exec()` runs *synchronously* inside the worker (see
107
- * @ffmpeg/ffmpeg's worker.js: `ffmpeg.exec(...args)` is not awaited), so the
108
- * worker never reaches a message poll while encoding. The AbortSignal that
109
- * `exec()` accepts only rejects the main-thread promise; the encode keeps
110
- * burning CPU behind it. `worker.terminate()` is what actually stops it.
111
- *
112
- * The cost is the loaded core, which is why the core URLs are cached above.
113
- */
114
- async cancel(options) {
115
- var _a;
116
- if (!this.currentJobId)
117
- return { cancelled: false };
118
- if ((options === null || options === void 0 ? void 0 : options.jobId) && options.jobId !== this.currentJobId)
119
- return { cancelled: false };
120
- // Set even when there is no instance yet: the job may still be inside
121
- // loadFFmpeg, which reads this flag before publishing what it built.
122
- this.cancelRequested = true;
123
- (_a = this.ffmpeg) === null || _a === void 0 ? void 0 : _a.terminate();
124
- this.ffmpeg = null;
125
- this.isLoaded = false;
126
- return { cancelled: true };
127
- }
128
- async initialize() {
129
- try {
130
- if (this.isLoaded) {
131
- return { success: true, message: 'Already initialized' };
132
- }
273
+ async compressWithFfmpeg(blob, quality, throwIfCancelled) {
274
+ if (!this.isLoaded) {
133
275
  await this.loadFFmpeg();
134
- return { success: true, message: 'Initialization successful' };
135
276
  }
136
- catch (e) {
137
- return { success: false, message: e.message || 'Initialization failed' };
277
+ throwIfCancelled();
278
+ if (!this.ffmpeg) {
279
+ throw new Error('FFmpeg failed to load');
138
280
  }
139
- }
140
- async compressVideo(options) {
141
- var _a;
142
- this.currentJobId = (_a = options.jobId) !== null && _a !== void 0 ? _a : 'default';
143
- this.cancelRequested = false;
144
- // Checked at every stage boundary, not just around exec: terminate() only
145
- // reaches work that has already got to the worker, so a cancel during the
146
- // core download or the source fetch has nothing to interrupt and would
147
- // otherwise let the compress carry on as if nothing had happened.
148
- const throwIfCancelled = () => {
281
+ // Bound once. `this.ffmpeg` is nulled by cancel(), so reading it after any
282
+ // of the awaits below can hand back null on a reference the compiler
283
+ // narrowed here — surfacing a cancel as a TypeError, not a CancelledError.
284
+ const ffmpeg = this.ffmpeg;
285
+ this.notifyListeners('onProgress', { status: 'started', percent: 0 });
286
+ const inputFileName = 'input.mp4';
287
+ const outputFileName = 'output.mp4';
288
+ let crf = '28';
289
+ let scale = '';
290
+ let audioBitrate = '128k';
291
+ switch (quality) {
292
+ case 'VERY_HIGH':
293
+ crf = '23';
294
+ scale = '-vf scale=-2:1080';
295
+ audioBitrate = '128k';
296
+ break;
297
+ case 'HIGH':
298
+ crf = '28';
299
+ scale = '-vf scale=-2:720';
300
+ audioBitrate = '128k';
301
+ break;
302
+ case 'MEDIUM':
303
+ crf = '30';
304
+ scale = '-vf scale=-2:540';
305
+ audioBitrate = '96k';
306
+ break;
307
+ case 'LOW':
308
+ crf = '32';
309
+ scale = '-vf scale=-2:480';
310
+ audioBitrate = '96k';
311
+ break;
312
+ case '360P':
313
+ crf = '28';
314
+ scale = '-vf scale=-2:360';
315
+ audioBitrate = '64k';
316
+ break;
317
+ case 'VERY_LOW':
318
+ crf = '35';
319
+ scale = '-vf scale=-2:240';
320
+ audioBitrate = '48k';
321
+ break;
322
+ }
323
+ const inputData = new Uint8Array(await blob.arrayBuffer());
324
+ try {
325
+ await ffmpeg.writeFile(inputFileName, inputData);
326
+ }
327
+ catch (err) {
149
328
  if (this.cancelRequested)
150
329
  throw new CancelledError();
151
- };
330
+ console.error('[FFmpeg] Error writing to FS:', err);
331
+ throw new Error('Could not write to FFmpeg FS');
332
+ }
333
+ throwIfCancelled();
334
+ const args = ['-i', inputFileName];
335
+ args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);
336
+ if (scale) {
337
+ args.push(...scale.split(' '));
338
+ }
339
+ args.push('-c:a', 'aac', '-b:a', audioBitrate);
340
+ args.push(outputFileName);
152
341
  try {
153
- if (!this.isLoaded) {
154
- await this.loadFFmpeg();
155
- }
156
- throwIfCancelled();
157
- if (!this.ffmpeg) {
158
- throw new Error('FFmpeg failed to load');
159
- }
160
- // Bound once. `this.ffmpeg` is nulled by cancel(), so reading it after
161
- // any of the four awaits below can hand back null on a reference the
162
- // compiler narrowed at this line — surfacing a cancel as a TypeError
163
- // instead of a CancelledError.
164
- const ffmpeg = this.ffmpeg;
165
- this.notifyListeners('onProgress', { status: 'started', percent: 0 });
166
- const { sourcePath, quality = 'MEDIUM' } = options;
167
- const inputFileName = 'input.mp4';
168
- const outputFileName = 'output.mp4';
169
- // Map quality to CRF, Scale, and Audio Bitrate
170
- let crf = '28';
171
- let scale = ''; // default keep resolution
172
- let audioBitrate = '128k';
173
- switch (quality) {
174
- case 'VERY_HIGH': // 1080p
175
- crf = '23';
176
- scale = '-vf scale=-2:1080';
177
- audioBitrate = '128k';
178
- break;
179
- case 'HIGH': // 720p
180
- crf = '28';
181
- scale = '-vf scale=-2:720';
182
- audioBitrate = '128k';
183
- break;
184
- case 'MEDIUM': // 540p
185
- crf = '30';
186
- scale = '-vf scale=-2:540';
187
- audioBitrate = '96k';
188
- break;
189
- case 'LOW': // 480p
190
- crf = '32';
191
- scale = '-vf scale=-2:480';
192
- audioBitrate = '96k';
193
- break;
194
- case '360P': // 360p
195
- crf = '28';
196
- scale = '-vf scale=-2:360';
197
- audioBitrate = '64k';
198
- break;
199
- case 'VERY_LOW': // 240p
200
- crf = '35';
201
- scale = '-vf scale=-2:240';
202
- audioBitrate = '48k';
203
- break;
204
- }
205
- console.log('[FFmpeg] Fetching source file...', sourcePath);
206
- let inputData;
207
- try {
208
- inputData = await util.fetchFile(sourcePath);
209
- }
210
- catch (err) {
211
- console.error('[FFmpeg] Error fetching source file:', err);
212
- throw new Error('Could not fetch source video file');
213
- }
214
- throwIfCancelled();
215
- console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');
216
- try {
217
- await ffmpeg.writeFile(inputFileName, inputData);
218
- }
219
- catch (err) {
220
- if (this.cancelRequested)
221
- throw new CancelledError();
222
- console.error('[FFmpeg] Error writing to FS:', err);
223
- throw new Error('Could not write to FFmpeg FS');
224
- }
225
- throwIfCancelled();
226
- // Construct FFmpeg command array
227
- const args = ['-i', inputFileName];
228
- // Video codec and compression parameters (superfast provides much better size than ultrafast)
229
- args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);
230
- // Resize if needed (split by space)
231
- if (scale) {
232
- args.push(...scale.split(' '));
233
- }
234
- // Audio re-encoding to save space
235
- args.push('-c:a', 'aac', '-b:a', audioBitrate);
236
- // Output
237
- args.push(outputFileName);
238
- console.log('[FFmpeg] Executing command:', args.join(' '));
239
- try {
240
- const retCode = await ffmpeg.exec(args);
241
- console.log('[FFmpeg] Exec returned code:', retCode);
242
- }
243
- catch (err) {
244
- // terminate() rejects every in-flight call, so a cancel arrives here
245
- // looking like a generic failure unless we name it.
246
- if (this.cancelRequested)
247
- throw new CancelledError();
248
- console.error('[FFmpeg] Error executing command:', err);
249
- throw new Error('FFmpeg execution failed');
250
- }
251
- throwIfCancelled();
252
- console.log('[FFmpeg] Reading output file...');
253
- let data;
254
- try {
255
- data = await ffmpeg.readFile(outputFileName);
256
- }
257
- catch (err) {
258
- if (this.cancelRequested)
259
- throw new CancelledError();
260
- console.error('[FFmpeg] Error reading output file:', err);
261
- throw new Error('Could not read FFmpeg output');
262
- }
263
- // Free memory
264
- await ffmpeg.deleteFile(inputFileName);
265
- await ffmpeg.deleteFile(outputFileName);
266
- console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');
267
- // Create a blob URL for the resulting file
268
- const blob = new Blob([data], { type: 'video/mp4' });
269
- const destPath = URL.createObjectURL(blob);
270
- console.log('[FFmpeg] Compression complete, returning path:', destPath);
271
- return {
272
- success: true,
273
- destPath: destPath,
274
- };
342
+ await ffmpeg.exec(args);
275
343
  }
276
- catch (error) {
277
- if ((error === null || error === void 0 ? void 0 : error.code) === CANCELLED) {
278
- console.log('[FFmpeg] Compression cancelled');
279
- }
280
- else {
281
- console.error('[FFmpeg] Compression process failed:', error);
282
- }
283
- throw error;
344
+ catch (err) {
345
+ // terminate() rejects every in-flight call, so a cancel arrives here
346
+ // looking like a generic failure unless we name it.
347
+ if (this.cancelRequested)
348
+ throw new CancelledError();
349
+ console.error('[FFmpeg] Error executing command:', err);
350
+ throw new Error('FFmpeg execution failed');
284
351
  }
285
- finally {
286
- this.currentJobId = null;
287
- this.cancelRequested = false;
352
+ throwIfCancelled();
353
+ let data;
354
+ try {
355
+ data = await ffmpeg.readFile(outputFileName);
356
+ }
357
+ catch (err) {
358
+ if (this.cancelRequested)
359
+ throw new CancelledError();
360
+ console.error('[FFmpeg] Error reading output file:', err);
361
+ throw new Error('Could not read FFmpeg output');
288
362
  }
363
+ await ffmpeg.deleteFile(inputFileName);
364
+ await ffmpeg.deleteFile(outputFileName);
365
+ const outBlob = new Blob([data], { type: 'video/mp4' });
366
+ return { success: true, destPath: URL.createObjectURL(outBlob) };
289
367
  }
290
368
  }
291
369