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