capacitor-native-video-compressor 2.0.0 → 2.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.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 finishUpload() {\n // Nothing holds a process alive on the web — the tab either lives or the\n // whole compress is gone with it. Resolving no-op so the one call site\n // can fire unconditionally instead of branching on platform.\n return { released: false };\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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"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/definitions.js","esm/index.js","esm/opfs-output.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/**\n * `code` on the error `compressVideo` rejects with on web when WebCodecs can't\n * encode the video and it is larger than 200 MB, the cap for the ffmpeg.wasm\n * fallback. ffmpeg.wasm copies the whole input into wasm memory (~2 GB\n * ceiling), so a large file there crashes or hangs the tab rather than failing\n * cleanly.\n */\nexport const TOO_LARGE_FOR_WEB = 'TOO_LARGE_FOR_WEB';\n/**\n * `code` on the error `compressVideo` rejects with on web when the browser\n * refuses to store more of the output (storage quota). Retryable once space\n * is freed.\n */\nexport const STORAGE_FULL = 'STORAGE_FULL';\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","/**\n * WebCodecs output goes to the origin private file system so a long video\n * never sits whole in memory. The File handed back stays readable only while\n * its OPFS entry exists, and callers keep it well past `compressVideo` (until\n * the upload is done, or longer for a retry), so a successful output stays\n * until the caller releases it with `finishUpload({ jobId })` or the tab\n * closes. Each tab writes under its own directory and holds a Web Lock named\n * after it for its whole life; every compress removes the directories whose\n * lock nobody holds any more, i.e. those of closed tabs. A failed or cancelled\n * job deletes its own file straight away.\n */\nconst OPFS_ROOT = 'native-video-compressor';\nconst lockName = (tabId) => `${OPFS_ROOT}:${tabId}`;\nlet tabId = null;\nconst getTabId = () => (tabId !== null && tabId !== void 0 ? tabId : (tabId = crypto.randomUUID()));\nlet fileCount = 0;\n// Memoised apart from the directories: they are reopened after site data is\n// cleared, but this tab still holds its lock, and requesting it again would\n// wait forever on ourselves.\nlet tabLock = null;\nlet tabDirsPromise = null;\n// A rejected request (e.g. a sandboxed iframe) rejects here, so openTabDirs\n// fails and the caller buffers in memory instead of waiting forever.\nconst holdTabLock = () => new Promise((granted, rejected) => {\n navigator.locks\n .request(lockName(getTabId()), () => {\n granted();\n return new Promise(() => undefined);\n })\n .catch(rejected);\n});\nconst sweepClosedTabs = async (base) => {\n // Names first, locks second: a tab that creates its directory between the two\n // reads already holds its lock, so it is never mistaken for a closed one.\n const names = [];\n for await (const name of base.keys())\n names.push(name);\n const { held = [] } = await navigator.locks.query();\n const live = new Set(held.map((l) => l.name));\n for (const name of names) {\n if (!live.has(lockName(name)))\n await base.removeEntry(name, { recursive: true }).catch(() => undefined);\n }\n};\nconst openTabDirs = async () => {\n const base = await (await navigator.storage.getDirectory()).getDirectoryHandle(OPFS_ROOT, { create: true });\n if (navigator.locks) {\n await (tabLock !== null && tabLock !== void 0 ? tabLock : (tabLock = holdTabLock().catch((e) => {\n tabLock = null;\n throw e;\n })));\n }\n return { base, dir: await base.getDirectoryHandle(getTabId(), { create: true }) };\n};\n/** Null when OPFS or `createWritable` (missing on Safari's main thread) isn't there. */\nexport const createOpfsOutput = async () => {\n var _a;\n try {\n if (typeof navigator === 'undefined' || !((_a = navigator.storage) === null || _a === void 0 ? void 0 : _a.getDirectory))\n return null;\n const { base, dir } = await (tabDirsPromise !== null && tabDirsPromise !== void 0 ? tabDirsPromise : (tabDirsPromise = openTabDirs().catch((e) => {\n tabDirsPromise = null;\n throw e;\n })));\n // Without Web Locks another tab's live files can't be told apart: no sweep.\n if (navigator.locks)\n await sweepClosedTabs(base).catch(() => undefined);\n const name = `${++fileCount}.mp4`;\n const handle = await dir.getFileHandle(name, { create: true }).catch((e) => {\n // The directory was removed under us (site data cleared): reopen it next time.\n if ((e === null || e === void 0 ? void 0 : e.name) === 'NotFoundError')\n tabDirsPromise = null;\n throw e;\n });\n if (typeof handle.createWritable !== 'function') {\n await dir.removeEntry(name).catch(() => undefined);\n return null;\n }\n const writable = await handle.createWritable();\n const discard = async () => {\n // Rejects harmlessly once mediabunny holds the writer; it closes it then.\n await writable.abort().catch(() => undefined);\n await dir.removeEntry(name).catch(() => undefined);\n };\n // OPFS files carry no MIME type, and callers derive the extension from it.\n const read = async () => (await handle.getFile()).slice(0, undefined, 'video/mp4');\n return { writable, read, discard };\n }\n catch (e) {\n console.warn('[compress] OPFS unavailable, buffering output in memory:', e);\n return null;\n }\n};\n//# sourceMappingURL=opfs-output.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, StreamTarget, canEncodeVideo, } from 'mediabunny';\nimport { CANCELLED, STORAGE_FULL, TOO_LARGE_FOR_WEB } from './definitions';\nimport { createOpfsOutput } from './opfs-output';\n/**\n * Rejection shape for every coded failure. `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 CompressError extends Error {\n constructor(code, message) {\n super(message);\n this.code = code;\n this.name = 'CompressError';\n }\n}\nconst cancelledError = () => new CompressError(CANCELLED, 'Đã hủy nén');\nconst isQuotaError = (e) => {\n const { name, cause } = (e !== null && e !== void 0 ? e : {});\n return name === 'QuotaExceededError' || (!!cause && isQuotaError(cause));\n};\n/** ffmpeg.wasm copies the whole input into wasm memory (~2 GB ceiling). */\nconst MAX_FFMPEG_INPUT_BYTES = 200 * 1024 * 1024;\n/**\n * Per quality tier. `shortSide` caps the shorter display side, so portrait and\n * landscape get the same box as on iOS. WebCodecs: that + a bitrate ceiling; the quantizer\n * (constant 26, CRF-like) drives quality where the encoder honours it, but\n * H.264 on most browsers is bitrate-driven, so `bitrate` is the real knob.\n * ffmpeg: same short side, `crf` and `audioBitrate`. fps is capped to 30 at call\n * time (never upsampled). On WebCodecs audio is left to mediabunny, which\n * re-encodes to AAC for the MP4 container.\n */\nconst QUALITY_LADDER = {\n VERY_HIGH: { shortSide: 1080, bitrate: 4000000, crf: 23, audioBitrate: '128k' },\n HIGH: { shortSide: 720, bitrate: 2000000, crf: 28, audioBitrate: '128k' },\n MEDIUM: { shortSide: 540, bitrate: 1200000, crf: 30, audioBitrate: '96k' },\n LOW: { shortSide: 480, bitrate: 1000000, crf: 32, audioBitrate: '96k' },\n '360P': { shortSide: 360, bitrate: 700000, crf: 28, audioBitrate: '64k' },\n VERY_LOW: { shortSide: 240, bitrate: 350000, crf: 35, audioBitrate: '48k' },\n};\nconst WEBCODECS_QUANTIZER = 26;\n// Never upscales. Mediabunny derives the other side from the aspect ratio and rounds both up to even.\nexport const webCodecsSize = (displayWidth, displayHeight, shortSide) => {\n const side = Math.min(shortSide, displayWidth, displayHeight);\n return displayHeight > displayWidth ? { width: side } : { height: side };\n};\n// iw/ih are post-autorotate here: ffmpeg inserts the transpose ahead of -vf.\n// Never upscales; trunc keeps an odd source side even for libx264.\nexport const ffmpegScaleFilter = (shortSide) => `scale=w='if(gt(iw,ih),-2,min(${shortSide},trunc(iw/2)*2))':h='if(gt(iw,ih),min(${shortSide},trunc(ih/2)*2),-2)'`;\nconst FPS_CAP = 30;\nconst MIN_VIDEO_BITRATE = 200000;\n// A lean source re-encoded at the tier bitrate comes out larger than it went in.\n// The floor stops a broken source estimate from producing an unwatchable file.\nexport const capBitrate = (target, sourceBitrate, floor, headroom = 1) => sourceBitrate && sourceBitrate > 0 ? Math.min(target, Math.max(floor, Math.round(sourceBitrate * headroom))) : target;\n// H.264 needs about 1.5x the bitrate of these codecs for the same picture.\nexport const codecHeadroom = (codec) => codec === 'hevc' || codec === 'vp9' || codec === 'av1' ? 1.5 : 1;\n// Cap fps at 30, but never upsample. A failed probe leaves the source rate and bitrate untouched.\nconst probeVideoStats = async (track) => {\n try {\n const stats = await (track === null || track === void 0 ? void 0 : track.computePacketStats(100));\n if (!stats)\n return {};\n return {\n frameRate: stats.averagePacketRate > 0 ? Math.min(FPS_CAP, Math.round(stats.averagePacketRate)) : undefined,\n bitrate: stats.averageBitrate,\n };\n }\n catch (_a) {\n return {};\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. 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; a fresh object per call, so two calls never look alike. */\n this.currentJob = null;\n this.cancelRequested = false;\n /** Kept OPFS outputs by jobId, deleted by `finishUpload`. */\n this.keptOutputs = new Map();\n }\n async initialize() {\n // No-op: preloading the ~30 MB ffmpeg core would cost most compresses a download they never use.\n return { success: true, message: 'ready' };\n }\n async finishUpload({ jobId }) {\n const output = this.keptOutputs.get(jobId);\n if (!output)\n return { released: false };\n this.keptOutputs.delete(jobId);\n await output.discard();\n return { released: true };\n }\n async compressVideo(options) {\n var _a, _b;\n const job = { id: (_a = options.jobId) !== null && _a !== void 0 ? _a : 'default' };\n if (this.currentJob)\n this.stopEngines();\n this.currentJob = job;\n this.cancelRequested = false;\n // A job the caller gave up on (e.g. a stall timeout) runs on until its next\n // await returns. Once another job has started, the shared flags above speak\n // for that job, not this one, so a superseded job counts as cancelled.\n const isSuperseded = () => this.currentJob !== job;\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 || isSuperseded())\n throw 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, job.id, throwIfCancelled);\n if (result)\n return result;\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 // Not to ffmpeg either: it needs even more room, and its size cap would misreport this as TOO_LARGE_FOR_WEB.\n if (isQuotaError(e))\n throw new CompressError(STORAGE_FULL, 'Không đủ dung lượng lưu trữ để nén video');\n console.warn('[compress] WebCodecs failed, falling back to ffmpeg:', e);\n }\n }\n throwIfCancelled();\n if (blob.size > MAX_FFMPEG_INPUT_BYTES)\n throw new CompressError(TOO_LARGE_FOR_WEB, 'Video quá lớn để nén trên trình duyệt này');\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 if (!isSuperseded()) {\n this.currentJob = 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 CANCELLED `CompressError` 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 if (!this.currentJob)\n return { cancelled: false };\n if ((options === null || options === void 0 ? void 0 : options.jobId) && options.jobId !== this.currentJob.id)\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 this.stopEngines();\n return { cancelled: true };\n }\n stopEngines() {\n var _a;\n const conversion = this.activeConversion;\n this.activeConversion = null;\n // Fire-and-forget: the CANCELLED error 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 }\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 } = QUALITY_LADDER[quality];\n return await canEncodeVideo('avc', {\n hardwareAcceleration: 'no-preference',\n quality: new Quality({ quantizer: WEBCODECS_QUANTIZER, bitrate }),\n });\n }\n catch (_a) {\n return false;\n }\n }\n async compressWithWebCodecs(blob, quality, jobId, throwIfCancelled) {\n const opfs = await createOpfsOutput();\n console.debug(`[compress] engine=webcodecs target=${opfs ? 'opfs' : 'buffer'}`);\n let output;\n let kept = false;\n try {\n const input = new Input({ formats: ALL_FORMATS, source: new BlobSource(blob) });\n output = new Output({\n format: new Mp4OutputFormat(),\n target: opfs ? new StreamTarget(opfs.writable, { chunked: true }) : new BufferTarget(),\n });\n if (!(await this.runConversion(input, output, QUALITY_LADDER[quality], throwIfCancelled)))\n return null;\n let outBlob;\n if (opfs) {\n outBlob = await opfs.read();\n }\n else {\n const buffer = output.target.buffer;\n if (!buffer)\n throw new Error('WebCodecs produced no output');\n outBlob = new Blob([buffer], { type: 'video/mp4' });\n }\n kept = true;\n if (opfs)\n this.keptOutputs.set(jobId, opfs);\n return { success: true, destPath: URL.createObjectURL(outBlob) };\n }\n finally {\n if (!kept) {\n // Closes the writable first, which otherwise keeps the entry locked against removal.\n await (output === null || output === void 0 ? void 0 : output.cancel().catch(() => undefined));\n await (opfs === null || opfs === void 0 ? void 0 : opfs.discard());\n }\n }\n }\n /** False when mediabunny can't convert this input. */\n async runConversion(input, output, { shortSide, bitrate }, throwIfCancelled) {\n const track = await input.getPrimaryVideoTrack();\n const { frameRate, bitrate: sourceBitrate } = await probeVideoStats(track);\n const size = track ? webCodecsSize(await track.getDisplayWidth(), await track.getDisplayHeight(), shortSide) : {};\n throwIfCancelled();\n const conversion = await Conversion.init({\n input,\n output,\n video: {\n ...size,\n quality: new Quality({\n quantizer: WEBCODECS_QUANTIZER,\n bitrate: capBitrate(bitrate, sourceBitrate, MIN_VIDEO_BITRATE, codecHeadroom(track === null || track === void 0 ? void 0 : track.codec)),\n }),\n // Not 'prefer-hardware': machines without a hardware H.264 encoder\n // (Linux, VMs, headless) then fail mid-encode and drop to ffmpeg.wasm.\n // The browser still picks the hardware encoder when it has one, and the\n // canEncodeVideo probe asks the same question, so probe and encode agree.\n hardwareAcceleration: 'no-preference',\n ...(frameRate ? { frameRate } : {}),\n },\n // The default copies the input's raw udta metadata tags, GPS location included.\n tags: {},\n });\n if (!conversion.isValid) {\n console.warn('[compress] WebCodecs conversion invalid:', conversion.discardedTracks.map((t) => t.reason));\n return false;\n }\n throwIfCancelled();\n this.activeConversion = conversion;\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n conversion.onProgress = (progress) => {\n if (this.activeConversion === conversion)\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n };\n try {\n await conversion.execute();\n }\n catch (e) {\n if (this.cancelRequested || e instanceof ConversionCanceledError)\n throw cancelledError();\n throw e;\n }\n throwIfCancelled();\n return true;\n }\n async loadFFmpeg(throwIfCancelled) {\n if (this.isLoaded && this.ffmpeg)\n return;\n throwIfCancelled();\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n // Published only once loaded and still wanted, so a cancel mid-load can't leave `isLoaded` set on a dropped instance.\n const instance = new FFmpeg();\n instance.on('progress', ({ progress }) => {\n // Once published the instance serves every later job; before that, or\n // after cancel() dropped it, its events belong to no job.\n if (this.ffmpeg === instance)\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n instance.on('log', ({ message }) => {\n console.debug('[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 try {\n throwIfCancelled();\n }\n catch (e) {\n instance.terminate();\n throw e;\n }\n this.ffmpeg = instance;\n this.isLoaded = true;\n }\n async compressWithFfmpeg(blob, quality, throwIfCancelled) {\n if (!this.isLoaded) {\n await this.loadFFmpeg(throwIfCancelled);\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 CANCELLED error.\n const ffmpeg = this.ffmpeg;\n console.debug('[compress] engine=ffmpeg');\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n const { shortSide, crf, audioBitrate } = QUALITY_LADDER[quality];\n const inputData = new Uint8Array(await blob.arrayBuffer());\n try {\n await ffmpeg.writeFile(inputFileName, inputData);\n }\n catch (err) {\n throwIfCancelled();\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n throwIfCancelled();\n // ffmpeg copies global metadata by default, including the GPS location (©xyz / loci).\n // prettier-ignore\n const args = [\n '-i', inputFileName, '-map_metadata', '-1',\n '-c:v', 'libx264', '-preset', 'superfast', '-crf', String(crf),\n '-vf', ffmpegScaleFilter(shortSide),\n '-c:a', 'aac', '-b:a', audioBitrate,\n outputFileName,\n ];\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 throwIfCancelled();\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 throwIfCancelled();\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","StreamTarget","BufferTarget","Conversion","ConversionCanceledError","FFmpeg"],"mappings":"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"}
@@ -1,8 +1,42 @@
1
1
  import Foundation
2
+ import CoreGraphics
3
+ import CoreMedia
2
4
 
3
5
  @objc public class NativeVideoCompressor: NSObject {
4
6
  @objc public func echo(_ value: String) -> String {
5
7
  print(value)
6
8
  return value
7
9
  }
10
+
11
+ /// Encoder size in the track's natural orientation. `shortSide` caps the
12
+ /// shorter displayed side, like the web `QUALITY_LADDER`; the long side keeps
13
+ /// the source aspect ratio. A 90/270 degree `preferredTransform` only swaps
14
+ /// width and height, so the short/long pair is the same in natural and
15
+ /// display orientation and the transform is not needed here.
16
+ /// Nil for an empty track: dividing by zero would trap.
17
+ static func outputSize(naturalSize: CGSize, shortSide: Int) -> (width: Int, height: Int)? {
18
+ let width = abs(naturalSize.width), height = abs(naturalSize.height)
19
+ let sourceShort = min(width, height)
20
+ guard sourceShort > 0 else { return nil }
21
+ let short = min(CGFloat(shortSide), sourceShort)
22
+ // Floor, not round: rounding an odd side up to even would upscale by a pixel.
23
+ func even(_ value: CGFloat) -> Int { max(2, Int(value / 2) * 2) }
24
+ let outShort = even(short)
25
+ let outLong = even(max(width, height) * short / sourceShort)
26
+ return width >= height ? (outLong, outShort) : (outShort, outLong)
27
+ }
28
+
29
+ /// A lean source re-encoded at the tier bitrate comes out larger than it went in,
30
+ /// so never target more than the source track's own rate times `headroom`. The
31
+ /// floor stops a broken estimate from producing garbage; 0 means unknown and
32
+ /// keeps the target. Clamped before `Int()`, which traps on a huge Float.
33
+ static func cappedBitrate(target: Int, sourceRate: Float, floor: Int, headroom: Float = 1) -> Int {
34
+ guard sourceRate > 0, sourceRate.isFinite else { return target }
35
+ return min(target, max(floor, Int(min(sourceRate * headroom, Float(target)))))
36
+ }
37
+
38
+ /// H.264 needs about 1.5x the bitrate of these codecs for the same picture.
39
+ static func codecHeadroom(_ subtype: FourCharCode) -> Float {
40
+ [kCMVideoCodecType_HEVC, kCMVideoCodecType_VP9, kCMVideoCodecType_AV1].contains(subtype) ? 1.5 : 1
41
+ }
8
42
  }
@@ -2,6 +2,7 @@ import Foundation
2
2
  import Capacitor
3
3
  import AVFoundation
4
4
  import UIKit
5
+ import WebKit
5
6
 
6
7
  @objc(NativeVideoCompressorPlugin)
7
8
  public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
@@ -18,13 +19,18 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
18
19
  CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
19
20
  ]
20
21
 
22
+ /// One compress with its own flag, so cancelling an orphaned job can't
23
+ /// cancel (or un-cancel) the job that replaced it.
24
+ private final class Job {
25
+ let id: String
26
+ var isCancelled = false
27
+ init(id: String) { self.id = id }
28
+ }
29
+
21
30
  /// The job `compressVideo` is running right now, for `cancel(jobId:)`.
22
- private var currentJobId: String?
23
- private var isCancelled = false
24
- /// Held only so `cancel` can stop it; the compress owns it otherwise.
25
- private var currentReader: AVAssetReader?
26
- /// Guards the three fields above — `cancel` arrives on the main thread
27
- /// while the reader is being driven from the compression queues.
31
+ private var currentJob: Job?
32
+ /// Guards `currentJob` and every `Job.isCancelled` — `cancel` arrives on the
33
+ /// main thread while the reader is being driven from the compression queues.
28
34
  private let stateLock = NSLock()
29
35
 
30
36
  /// Nothing to do natively; AVFoundation needs no warm-up. Answered rather
@@ -36,39 +42,66 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
36
42
  /**
37
43
  * Stop the running compress.
38
44
  *
39
- * `cancelReading()` is all it takes: the frame loops below already check
40
- * `reader.status == .reading` every pass, so a cancelled reader sends them
41
- * down the `else` branch — `markAsFinished()`, `group.leave()` — and the
42
- * existing completion handler runs. No new exit path, no polling.
45
+ * Only raises the flag. Calling `cancelReading()` from here tore the reader
46
+ * down while a frame loop was inside `copyNextSampleBuffer()` on its own
47
+ * queue and crashed (`_os_unfair_lock_corruption_abort`). The loops check
48
+ * the flag before every read and finish themselves; the completion handler
49
+ * cancels the reader once neither loop can touch it.
43
50
  */
44
51
  @objc func cancel(_ call: CAPPluginCall) {
45
52
  stateLock.lock()
46
- let running = currentJobId
47
53
  let requested = call.getString("jobId")
48
54
  // A cancel for a job that isn't the one running would otherwise kill
49
55
  // an unrelated caller's compress — there is only ever one.
50
- guard let running = running, requested == nil || requested == running else {
56
+ guard let running = currentJob, requested == nil || requested == running.id else {
51
57
  stateLock.unlock()
52
58
  call.resolve(["cancelled": false])
53
59
  return
54
60
  }
55
- isCancelled = true
56
- let reader = currentReader
61
+ running.isCancelled = true
57
62
  stateLock.unlock()
58
63
 
59
- reader?.cancelReading()
60
64
  call.resolve(["cancelled": true])
61
65
  }
62
66
 
67
+ /**
68
+ * Capacitor iOS resets the bridge on every main-frame navigation but has no
69
+ * plugin hook for it, so this is the closest one. A reload (pull-to-refresh,
70
+ * web content process restart) or any same-origin page load ends the JS
71
+ * session that owns the job; left running it encodes for nobody while the
72
+ * new session starts its own. Returning nil keeps Capacitor's own policy.
73
+ */
74
+ override public func shouldOverrideLoad(_ navigationAction: WKNavigationAction) -> NSNumber? {
75
+ if navigationAction.targetFrame?.isMainFrame == true,
76
+ navigationAction.navigationType == .reload
77
+ || (navigationAction.request.url?.host == bridge?.webView?.url?.host
78
+ && !Self.isFragmentNavigation(to: navigationAction.request.url, from: bridge?.webView?.url)) {
79
+ stateLock.lock()
80
+ currentJob?.isCancelled = true
81
+ stateLock.unlock()
82
+ }
83
+ return nil
84
+ }
85
+
86
+ // Only a target with a fragment stays in the document; dropping the fragment reloads the page.
87
+ static func isFragmentNavigation(to target: URL?, from current: URL?) -> Bool {
88
+ guard let target = target, let current = current, target.fragment != nil else { return false }
89
+ return target.absoluteString.split(separator: "#", maxSplits: 1).first
90
+ == current.absoluteString.split(separator: "#", maxSplits: 1).first
91
+ }
92
+
63
93
  @objc func compressVideo(_ call: CAPPluginCall) {
64
94
  guard let sourcePath = call.getString("sourcePath") else {
65
95
  call.reject("Missing sourcePath")
66
96
  return
67
97
  }
68
98
 
99
+ let job = Job(id: call.getString("jobId") ?? "default")
69
100
  stateLock.lock()
70
- currentJobId = call.getString("jobId") ?? "default"
71
- isCancelled = false
101
+ // The app runs one compress at a time, so a job still here was orphaned
102
+ // by a JS session the shouldOverrideLoad hook missed.
103
+ currentJob?.isCancelled = true
104
+ currentJob = job
72
105
  stateLock.unlock()
73
106
 
74
107
  let videoURL = sourcePath.hasPrefix("file://") ? URL(string: sourcePath)! : URL(fileURLWithPath: sourcePath)
@@ -77,27 +110,25 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
77
110
  let qualityStr = call.getString("quality") ?? "MEDIUM"
78
111
 
79
112
  // 1. Định nghĩa Kích thước và Bitrate (Quyết định dung lượng file)
80
- var targetWidth: CGFloat
81
- var targetHeight: CGFloat
113
+ // Short side of the displayed picture, matching the web QUALITY_LADDER.
114
+ var shortSide: Int
82
115
  var targetBitrate: Int // Bitrate càng thấp file càng nhẹ
83
-
84
- print("====== VIDEO COMPRESSION QUALITY: \(qualityStr) ======")
85
-
116
+
86
117
  switch qualityStr {
87
118
  case "VERY_HIGH":
88
- targetWidth = 1920; targetHeight = 1080; targetBitrate = 4_500_000 // ~4.5 Mbps
119
+ shortSide = 1080; targetBitrate = 4_500_000 // ~4.5 Mbps
89
120
  case "HIGH":
90
- targetWidth = 1280; targetHeight = 720; targetBitrate = 2_500_000 // ~2.5 Mbps
121
+ shortSide = 720; targetBitrate = 2_500_000 // ~2.5 Mbps
91
122
  case "MEDIUM":
92
- targetWidth = 960; targetHeight = 540; targetBitrate = 1_500_000 // ~1.5 Mbps (Giảm dung lượng cực tốt)
123
+ shortSide = 540; targetBitrate = 1_500_000 // ~1.5 Mbps (Giảm dung lượng cực tốt)
93
124
  case "LOW":
94
- targetWidth = 640; targetHeight = 480; targetBitrate = 1_000_000 // ~1.0 Mbps
125
+ shortSide = 480; targetBitrate = 1_000_000 // ~1.0 Mbps
95
126
  case "360P":
96
- targetWidth = 640; targetHeight = 360; targetBitrate = 700_000 // ~0.7 Mbps
127
+ shortSide = 360; targetBitrate = 700_000 // ~0.7 Mbps
97
128
  case "VERY_LOW":
98
- targetWidth = 426; targetHeight = 240; targetBitrate = 400_000 // ~0.4 Mbps
129
+ shortSide = 240; targetBitrate = 400_000 // ~0.4 Mbps
99
130
  default:
100
- targetWidth = 960; targetHeight = 540; targetBitrate = 1_500_000
131
+ shortSide = 540; targetBitrate = 1_500_000
101
132
  }
102
133
 
103
134
  let asset = AVAsset(url: videoURL)
@@ -113,9 +144,9 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
113
144
  // 2. Gọi hàm nén Custom bằng AVAssetWriter
114
145
  self.compressWithAVAssetWriter(
115
146
  asset: asset,
147
+ job: job,
116
148
  outputURL: outputURL,
117
- targetWidth: targetWidth,
118
- targetHeight: targetHeight,
149
+ shortSide: shortSide,
119
150
  targetBitrate: targetBitrate,
120
151
  call: call,
121
152
  backgroundTask: backgroundTask
@@ -125,15 +156,16 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
125
156
  // MARK: - Core Compression Logic (AVAssetWriter)
126
157
  private func compressWithAVAssetWriter(
127
158
  asset: AVAsset,
159
+ job: Job,
128
160
  outputURL: URL,
129
- targetWidth: CGFloat,
130
- targetHeight: CGFloat,
161
+ shortSide: Int,
131
162
  targetBitrate: Int,
132
163
  call: CAPPluginCall,
133
164
  backgroundTask: UIBackgroundTaskIdentifier
134
165
  ) {
135
- guard let videoTrack = asset.tracks(withMediaType: .video).first else {
136
- clearJobState()
166
+ guard let videoTrack = asset.tracks(withMediaType: .video).first,
167
+ let outputSize = NativeVideoCompressor.outputSize(naturalSize: videoTrack.naturalSize, shortSide: shortSide) else {
168
+ clearJobState(job)
137
169
  call.reject("Không tìm thấy track video")
138
170
  return
139
171
  }
@@ -144,48 +176,45 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
144
176
  let writer = try AVAssetWriter(url: outputURL, fileType: .mp4)
145
177
  writer.shouldOptimizeForNetworkUse = true // Tối ưu cho Web/Network
146
178
 
147
- stateLock.lock()
148
- currentReader = reader
149
- // A cancel that landed between compressVideo and here has nothing
150
- // to call cancelReading() on, so honour it before any work starts.
151
- let cancelledEarly = isCancelled
152
- stateLock.unlock()
153
-
154
- if cancelledEarly {
155
- finishCancelled(outputURL: outputURL, call: call, backgroundTask: backgroundTask)
179
+ // A cancel that landed before the loops start would otherwise only
180
+ // be noticed after the writer had started, so honour it now.
181
+ if cancelRequested(job) {
182
+ finishCancelled(job: job, outputURL: outputURL, call: call, backgroundTask: backgroundTask)
156
183
  return
157
184
  }
158
185
 
159
- // --- XỬ LÝ KÍCH THƯỚC VÀ CHIỀU XOAY (ORIENTATION) ---
160
- let naturalSize = videoTrack.naturalSize
161
186
  let transform = videoTrack.preferredTransform
162
-
163
- // Tính toán chiều thực tế khi hiển thị
164
- var visualSize = naturalSize.applying(transform)
165
- visualSize.width = abs(visualSize.width)
166
- visualSize.height = abs(visualSize.height)
167
-
168
- let isVisualPortrait = visualSize.height > visualSize.width
169
- let boundingWidth = isVisualPortrait ? min(targetWidth, targetHeight) : max(targetWidth, targetHeight)
170
- let boundingHeight = isVisualPortrait ? max(targetWidth, targetHeight) : min(targetWidth, targetHeight)
171
-
172
- let isNaturalPortrait = naturalSize.height > naturalSize.width
173
- let outputWidth = isNaturalPortrait ? min(boundingWidth, boundingHeight) : max(boundingWidth, boundingHeight)
174
- let outputHeight = isNaturalPortrait ? max(boundingWidth, boundingHeight) : min(boundingWidth, boundingHeight)
175
-
187
+ let sourceCodec = videoTrack.formatDescriptions.first
188
+ .map { CMFormatDescriptionGetMediaSubType($0 as! CMFormatDescription) } ?? 0
189
+
176
190
  // --- CẤU HÌNH VIDEO ---
191
+ // Decoder and H.264 encoder both work in YUV 4:2:0; asking for BGRA cost two colour conversions per frame.
177
192
  let videoOutputSettings: [String: Any] = [
178
- kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
193
+ kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
179
194
  ]
180
195
  let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: videoOutputSettings)
196
+ // Each buffer goes straight to the writer and is never mutated, so the per-sample copy is wasted work.
197
+ videoOutput.alwaysCopiesSampleData = false
181
198
  if reader.canAdd(videoOutput) { reader.add(videoOutput) }
182
199
 
183
200
  let videoInputSettings:[String: Any] = [
184
201
  AVVideoCodecKey: AVVideoCodecType.h264,
185
- AVVideoWidthKey: Int(outputWidth),
186
- AVVideoHeightKey: Int(outputHeight),
202
+ AVVideoWidthKey: outputSize.width,
203
+ AVVideoHeightKey: outputSize.height,
204
+ // The default mode is Resize, which stretches. The size already matches the source aspect
205
+ // within even-rounding, so Fill only crops that sub-pixel instead of adding a black line (ResizeAspect).
206
+ AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
207
+ // 8-bit H.264 can't carry HDR: without this an HLG/PQ source keeps its BT.2020 tags on SDR-range data.
208
+ // The writer colour-converts (tone-maps) tagged buffers to these properties; BT.709 sources pass through unchanged.
209
+ AVVideoColorPropertiesKey: [
210
+ AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
211
+ AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
212
+ AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
213
+ ],
187
214
  AVVideoCompressionPropertiesKey:[
188
- AVVideoAverageBitRateKey: targetBitrate, // ÉP BITRATE TẠI ĐÂY
215
+ AVVideoAverageBitRateKey: NativeVideoCompressor.cappedBitrate(
216
+ target: targetBitrate, sourceRate: videoTrack.estimatedDataRate, floor: 200_000,
217
+ headroom: NativeVideoCompressor.codecHeadroom(sourceCodec)),
189
218
  AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
190
219
  ]
191
220
  ]
@@ -199,14 +228,19 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
199
228
 
200
229
  if let audioTrack = audioTrack {
201
230
  let audioOutputSettings:[String: Any] = [AVFormatIDKey: kAudioFormatLinearPCM]
202
- audioOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioOutputSettings)
203
- if reader.canAdd(audioOutput!) { reader.add(audioOutput!) }
231
+ let output = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: audioOutputSettings)
232
+ output.alwaysCopiesSampleData = false
233
+ if reader.canAdd(output) { reader.add(output) }
234
+ audioOutput = output
204
235
 
205
236
  let audioInputSettings:[String: Any] = [
206
237
  AVFormatIDKey: kAudioFormatMPEG4AAC,
207
238
  AVNumberOfChannelsKey: 2,
208
239
  AVSampleRateKey: 44100,
209
- AVEncoderBitRateKey: 128000 // Audio nén về 128kbps
240
+ // AAC takes only discrete rates and AVAssetWriterInput raises on an unsupported one,
241
+ // so snap down to 64/96/128 kbps.
242
+ AVEncoderBitRateKey: NativeVideoCompressor.cappedBitrate(
243
+ target: 128_000, sourceRate: audioTrack.estimatedDataRate, floor: 64_000) / 32_000 * 32_000
210
244
  ]
211
245
  audioInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioInputSettings)
212
246
  if writer.canAdd(audioInput!) { writer.add(audioInput!) }
@@ -229,7 +263,7 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
229
263
  videoInput.requestMediaDataWhenReady(on: videoQueue) {
230
264
  while videoInput.isReadyForMoreMediaData {
231
265
  autoreleasepool {
232
- if reader.status == .reading, let buffer = videoOutput.copyNextSampleBuffer() {
266
+ if !self.cancelRequested(job), reader.status == .reading, let buffer = videoOutput.copyNextSampleBuffer() {
233
267
  videoInput.append(buffer)
234
268
 
235
269
  // Tính toán Progress
@@ -262,7 +296,7 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
262
296
  aInput.requestMediaDataWhenReady(on: audioQueue) {
263
297
  while aInput.isReadyForMoreMediaData {
264
298
  autoreleasepool {
265
- if reader.status == .reading, let buffer = aOutput.copyNextSampleBuffer() {
299
+ if !self.cancelRequested(job), reader.status == .reading, let buffer = aOutput.copyNextSampleBuffer() {
266
300
  aInput.append(buffer)
267
301
  } else {
268
302
  aInput.markAsFinished()
@@ -280,13 +314,13 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
280
314
  }
281
315
 
282
316
  self.stateLock.lock()
283
- let wasCancelled = self.isCancelled
284
- self.currentReader = nil
285
- self.currentJobId = nil
286
- self.isCancelled = false
317
+ let wasCancelled = job.isCancelled
318
+ if self.currentJob === job { self.currentJob = nil }
287
319
  self.stateLock.unlock()
288
320
 
289
321
  if wasCancelled {
322
+ // Both loops have left the group, so nothing else is reading.
323
+ reader.cancelReading()
290
324
  writer.cancelWriting()
291
325
  try? FileManager.default.removeItem(at: outputURL)
292
326
  call.reject("Đã hủy nén", NativeVideoCompressorPlugin.cancelledCode)
@@ -314,13 +348,14 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
314
348
  }
315
349
 
316
350
  } catch {
317
- clearJobState()
351
+ clearJobState(job)
318
352
  call.reject("Lỗi khởi tạo bộ nén: \(error.localizedDescription)")
319
353
  }
320
354
  }
321
355
 
322
356
  /// Cancelled before the reader started — nothing to stop, just clean up.
323
357
  private func finishCancelled(
358
+ job: Job,
324
359
  outputURL: URL,
325
360
  call: CAPPluginCall,
326
361
  backgroundTask: UIBackgroundTaskIdentifier
@@ -328,16 +363,20 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
328
363
  if backgroundTask != .invalid {
329
364
  UIApplication.shared.endBackgroundTask(backgroundTask)
330
365
  }
331
- clearJobState()
366
+ clearJobState(job)
332
367
  try? FileManager.default.removeItem(at: outputURL)
333
368
  call.reject("Đã hủy nén", NativeVideoCompressorPlugin.cancelledCode)
334
369
  }
335
370
 
336
- private func clearJobState() {
371
+ private func cancelRequested(_ job: Job) -> Bool {
372
+ stateLock.lock()
373
+ defer { stateLock.unlock() }
374
+ return job.isCancelled
375
+ }
376
+
377
+ private func clearJobState(_ job: Job) {
337
378
  stateLock.lock()
338
- currentReader = nil
339
- currentJobId = nil
340
- isCancelled = false
379
+ if currentJob === job { currentJob = nil }
341
380
  stateLock.unlock()
342
381
  }
343
382
 
@@ -1,4 +1,5 @@
1
1
  import XCTest
2
+ import CoreMedia
2
3
  @testable import NativeVideoCompressorPlugin
3
4
 
4
5
  class NativeVideoCompressorTests: XCTestCase {
@@ -12,4 +13,47 @@ class NativeVideoCompressorTests: XCTestCase {
12
13
 
13
14
  XCTAssertEqual(value, result)
14
15
  }
16
+
17
+ func testOutputSizeCapsShortSideKeepsAspectNeverUpscales() {
18
+ func size(_ w: CGFloat, _ h: CGFloat, _ short: Int = 540) -> [Int]? {
19
+ NativeVideoCompressor.outputSize(naturalSize: CGSize(width: w, height: h), shortSide: short).map { [$0.width, $0.height] }
20
+ }
21
+ // Also portrait iPhone video: natural 1920x1080 plus a 90 degree transform displays as 540x960.
22
+ XCTAssertEqual(size(1920, 1080), [960, 540])
23
+ XCTAssertEqual(size(1080, 1920), [540, 960])
24
+ XCTAssertEqual(size(1440, 1080), [720, 540])
25
+ XCTAssertEqual(size(1080, 1080), [540, 540])
26
+ XCTAssertEqual(size(1170, 2532), [540, 1168])
27
+ XCTAssertEqual(size(480, 360), [480, 360])
28
+ XCTAssertEqual(size(641, 361), [640, 360])
29
+ XCTAssertEqual(size(1920, 1080, 1080), [1920, 1080])
30
+ XCTAssertNil(size(0, 0))
31
+ }
32
+
33
+ func testCappedBitrateNeverExceedsSourceAndKeepsFloor() {
34
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 1_000_000, floor: 200_000), 1_000_000)
35
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 9_000_000, floor: 200_000), 1_500_000)
36
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 1_000, floor: 200_000), 200_000)
37
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 0, floor: 200_000), 1_500_000)
38
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 128_000, sourceRate: 96_000, floor: 64_000), 96_000)
39
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 1e30, floor: 200_000), 1_500_000)
40
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 800_000, floor: 200_000, headroom: 1.5), 1_200_000)
41
+ XCTAssertEqual(NativeVideoCompressor.cappedBitrate(target: 1_500_000, sourceRate: 1_200_000, floor: 200_000, headroom: 1.5), 1_500_000)
42
+ }
43
+
44
+ func testCodecHeadroom() {
45
+ XCTAssertEqual(NativeVideoCompressor.codecHeadroom(kCMVideoCodecType_HEVC), 1.5)
46
+ XCTAssertEqual(NativeVideoCompressor.codecHeadroom(kCMVideoCodecType_H264), 1)
47
+ }
48
+
49
+ func testOnlyFragmentNavigationsKeepTheJob() {
50
+ func isFragment(_ to: String, _ from: String) -> Bool {
51
+ NativeVideoCompressorPlugin.isFragmentNavigation(to: URL(string: to), from: URL(string: from))
52
+ }
53
+ XCTAssertTrue(isFragment("capacitor://localhost/feed#x", "capacitor://localhost/feed"))
54
+ XCTAssertTrue(isFragment("capacitor://localhost/feed#y", "capacitor://localhost/feed#x"))
55
+ XCTAssertFalse(isFragment("capacitor://localhost/feed", "capacitor://localhost/feed#x"))
56
+ XCTAssertFalse(isFragment("capacitor://localhost/feed", "capacitor://localhost/feed"))
57
+ XCTAssertFalse(isFragment("capacitor://localhost/other#x", "capacitor://localhost/feed"))
58
+ }
15
59
  }