capacitor-native-video-compressor 1.2.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.
- package/README.md +53 -33
- package/android/build.gradle +6 -4
- package/android/src/main/AndroidManifest.xml +8 -17
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressor.java +134 -4
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +385 -122
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionWorker.java +134 -0
- package/dist/docs.json +24 -24
- package/dist/esm/definitions.d.ts +52 -9
- package/dist/esm/definitions.js +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/opfs-output.d.ts +7 -0
- package/dist/esm/opfs-output.js +94 -0
- package/dist/esm/opfs-output.js.map +1 -0
- package/dist/esm/web.d.ts +21 -3
- package/dist/esm/web.js +194 -129
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +306 -128
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +307 -129
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressor.swift +34 -0
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +118 -79
- package/ios/Tests/NativeVideoCompressorPluginTests/NativeVideoCompressorTests.swift +44 -0
- package/package.json +9 -4
- package/android/src/main/java/com/atomic/videocompressor/VideoCompressionService.java +0 -310
package/dist/plugin.js.map
CHANGED
|
@@ -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 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":";;;IAAA;IACA;IACA;IACA;IACA;AACY,UAAC,SAAS,GAAG;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;AACY,UAAC,iBAAiB,GAAG;IACjC;IACA;IACA;IACA;IACA;AACY,UAAC,YAAY,GAAG;;AClBvB,UAAC,qBAAqB,GAAGA,mBAAc,CAAC,uBAAuB,EAAE;IACtE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,wBAAwB,EAAE,CAAC;IAC5E,CAAC;;ICHD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,MAAM,SAAS,GAAG,yBAAyB;IAC3C,MAAM,QAAQ,GAAG,CAAC,KAAK,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IACnD,IAAI,KAAK,GAAG,IAAI;IAChB,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,GAAG,KAAK,IAAI,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACnG,IAAI,SAAS,GAAG,CAAC;IACjB;IACA;IACA;IACA,IAAI,OAAO,GAAG,IAAI;IAClB,IAAI,cAAc,GAAG,IAAI;IACzB;IACA;IACA,MAAM,WAAW,GAAG,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,QAAQ,KAAK;IAC7D,IAAI,SAAS,CAAC;IACd,SAAS,OAAO,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM;IAC7C,QAAQ,OAAO,EAAE;IACjB,QAAQ,OAAO,IAAI,OAAO,CAAC,MAAM,SAAS,CAAC;IAC3C,IAAI,CAAC;IACL,SAAS,KAAK,CAAC,QAAQ,CAAC;IACxB,CAAC,CAAC;IACF,MAAM,eAAe,GAAG,OAAO,IAAI,KAAK;IACxC;IACA;IACA,IAAI,MAAM,KAAK,GAAG,EAAE;IACpB,IAAI,WAAW,MAAM,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE;IACxC,QAAQ,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;IACxB,IAAI,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE;IACvD,IAAI,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE;IAC9B,QAAQ,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,YAAY,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;IACpF,IAAI;IACJ,CAAC;IACD,MAAM,WAAW,GAAG,YAAY;IAChC,IAAI,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,SAAS,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,kBAAkB,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC/G,IAAI,IAAI,SAAS,CAAC,KAAK,EAAE;IACzB,QAAQ,OAAO,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,IAAI,OAAO,GAAG,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK;IACxG,YAAY,OAAO,GAAG,IAAI;IAC1B,YAAY,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,CAAC,CAAC;IACZ,IAAI;IACJ,IAAI,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,IAAI,CAAC,kBAAkB,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE;IACrF,CAAC;IACD;IACO,MAAM,gBAAgB,GAAG,YAAY;IAC5C,IAAI,IAAI,EAAE;IACV,IAAI,IAAI;IACR,QAAQ,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,CAAC,OAAO,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC;IAChI,YAAY,OAAO,IAAI;IACvB,QAAQ,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,OAAO,cAAc,KAAK,IAAI,IAAI,cAAc,KAAK,KAAK,CAAC,GAAG,cAAc,IAAI,cAAc,GAAG,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK;IAC1J,YAAY,cAAc,GAAG,IAAI;IACjC,YAAY,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,CAAC,CAAC;IACZ;IACA,QAAQ,IAAI,SAAS,CAAC,KAAK;IAC3B,YAAY,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;IAC9D,QAAQ,MAAM,IAAI,GAAG,CAAC,EAAE,EAAE,SAAS,CAAC,IAAI,CAAC;IACzC,QAAQ,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK;IACpF;IACA,YAAY,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,eAAe;IAClF,gBAAgB,cAAc,GAAG,IAAI;IACrC,YAAY,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC;IACV,QAAQ,IAAI,OAAO,MAAM,CAAC,cAAc,KAAK,UAAU,EAAE;IACzD,YAAY,MAAM,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;IAC9D,YAAY,OAAO,IAAI;IACvB,QAAQ;IACR,QAAQ,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,cAAc,EAAE;IACtD,QAAQ,MAAM,OAAO,GAAG,YAAY;IACpC;IACA,YAAY,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;IACzD,YAAY,MAAM,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;IAC9D,QAAQ,CAAC;IACT;IACA,QAAQ,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAC,EAAE,SAAS,EAAE,WAAW,CAAC;IAC1F,QAAQ,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IAC1C,IAAI;IACJ,IAAI,OAAO,CAAC,EAAE;IACd,QAAQ,OAAO,CAAC,IAAI,CAAC,0DAA0D,EAAE,CAAC,CAAC;IACnF,QAAQ,OAAO,IAAI;IACnB,IAAI;IACJ,CAAC;;ICtFD;IACA;IACA;IACA;IACA;IACA,MAAM,aAAa,SAAS,KAAK,CAAC;IAClC,IAAI,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE;IAC/B,QAAQ,KAAK,CAAC,OAAO,CAAC;IACtB,QAAQ,IAAI,CAAC,IAAI,GAAG,IAAI;IACxB,QAAQ,IAAI,CAAC,IAAI,GAAG,eAAe;IACnC,IAAI;IACJ;IACA,MAAM,cAAc,GAAG,MAAM,IAAI,aAAa,CAAC,SAAS,EAAE,YAAY,CAAC;IACvE,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK;IAC5B,IAAI,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;IACjE,IAAI,OAAO,IAAI,KAAK,oBAAoB,KAAK,CAAC,CAAC,KAAK,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;IAC5E,CAAC;IACD;IACA,MAAM,sBAAsB,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI;IAChD;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,MAAM,cAAc,GAAG;IACvB,IAAI,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE;IACnF,IAAI,IAAI,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE;IAC7E,IAAI,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE;IAC9E,IAAI,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE;IAC3E,IAAI,MAAM,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE;IAC7E,IAAI,QAAQ,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE;IAC/E,CAAC;IACD,MAAM,mBAAmB,GAAG,EAAE;IAC9B;IACO,MAAM,aAAa,GAAG,CAAC,YAAY,EAAE,aAAa,EAAE,SAAS,KAAK;IACzE,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,EAAE,aAAa,CAAC;IACjE,IAAI,OAAO,aAAa,GAAG,YAAY,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAC5E,CAAC;IACD;IACA;IACO,MAAM,iBAAiB,GAAG,CAAC,SAAS,KAAK,CAAC,6BAA6B,EAAE,SAAS,CAAC,sCAAsC,EAAE,SAAS,CAAC,oBAAoB,CAAC;IACjK,MAAM,OAAO,GAAG,EAAE;IAClB,MAAM,iBAAiB,GAAG,MAAM;IAChC;IACA;IACO,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,GAAG,CAAC,KAAK,aAAa,IAAI,aAAa,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,aAAa,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,MAAM;IAC/L;IACO,MAAM,aAAa,GAAG,CAAC,KAAK,KAAK,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,GAAG,GAAG,GAAG,CAAC;IACxG;IACA,MAAM,eAAe,GAAG,OAAO,KAAK,KAAK;IACzC,IAAI,IAAI;IACR,QAAQ,MAAM,KAAK,GAAG,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACzG,QAAQ,IAAI,CAAC,KAAK;IAClB,YAAY,OAAO,EAAE;IACrB,QAAQ,OAAO;IACf,YAAY,SAAS,EAAE,KAAK,CAAC,iBAAiB,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,GAAG,SAAS;IACvH,YAAY,OAAO,EAAE,KAAK,CAAC,cAAc;IACzC,SAAS;IACT,IAAI;IACJ,IAAI,OAAO,EAAE,EAAE;IACf,QAAQ,OAAO,EAAE;IACjB,IAAI;IACJ,CAAC;IACD,MAAM,aAAa,GAAG,gDAAgD;IACtE;IACA;IACA;IACA;IACA;IACA;IACA;IACA,IAAI,WAAW,GAAG,IAAI;IACtB,MAAM,YAAY,GAAG,MAAM;IAC3B,IAAI,IAAI,CAAC,WAAW,EAAE;IACtB,QAAQ,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAClC,YAAYC,cAAS,CAAC,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,EAAE,iBAAiB,CAAC;IAC3E,YAAYA,cAAS,CAAC,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,EAAE,kBAAkB,CAAC;IAC9E,SAAS;IACT,aAAa,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;IAChE,aAAa,KAAK,CAAC,CAAC,CAAC,KAAK;IAC1B;IACA;IACA,YAAY,WAAW,GAAG,IAAI;IAC9B,YAAY,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC;IACV,IAAI;IACJ,IAAI,OAAO,WAAW;IACtB,CAAC;IACM,MAAM,wBAAwB,SAASC,cAAS,CAAC;IACxD,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,MAAM,GAAG,IAAI;IAC1B,QAAQ,IAAI,CAAC,QAAQ,GAAG,KAAK;IAC7B;IACA,QAAQ,IAAI,CAAC,gBAAgB,GAAG,IAAI;IACpC;IACA,QAAQ,IAAI,CAAC,UAAU,GAAG,IAAI;IAC9B,QAAQ,IAAI,CAAC,eAAe,GAAG,KAAK;IACpC;IACA,QAAQ,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,EAAE;IACpC,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB;IACA,QAAQ,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE;IAClD,IAAI;IACJ,IAAI,MAAM,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE;IAClC,QAAQ,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;IAClD,QAAQ,IAAI,CAAC,MAAM;IACnB,YAAY,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE;IACtC,QAAQ,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC;IACtC,QAAQ,MAAM,MAAM,CAAC,OAAO,EAAE;IAC9B,QAAQ,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;IACjC,IAAI;IACJ,IAAI,MAAM,aAAa,CAAC,OAAO,EAAE;IACjC,QAAQ,IAAI,EAAE,EAAE,EAAE;IAClB,QAAQ,MAAM,GAAG,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,KAAK,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,EAAE,GAAG,SAAS,EAAE;IAC3F,QAAQ,IAAI,IAAI,CAAC,UAAU;IAC3B,YAAY,IAAI,CAAC,WAAW,EAAE;IAC9B,QAAQ,IAAI,CAAC,UAAU,GAAG,GAAG;IAC7B,QAAQ,IAAI,CAAC,eAAe,GAAG,KAAK;IACpC;IACA;IACA;IACA,QAAQ,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,UAAU,KAAK,GAAG;IAC1D;IACA;IACA;IACA;IACA,QAAQ,MAAM,gBAAgB,GAAG,MAAM;IACvC,YAAY,IAAI,IAAI,CAAC,eAAe,IAAI,YAAY,EAAE;IACtD,gBAAgB,MAAM,cAAc,EAAE;IACtC,QAAQ,CAAC;IACT,QAAQ,IAAI;IACZ,YAAY,gBAAgB,EAAE;IAC9B,YAAY,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC;IACvE,YAAY,gBAAgB,EAAE;IAC9B,YAAY,MAAM,OAAO,GAAG,CAAC,EAAE,GAAG,OAAO,CAAC,OAAO,MAAM,IAAI,IAAI,EAAE,KAAK,KAAK,CAAC,GAAG,EAAE,GAAG,QAAQ;IAC5F,YAAY,IAAI,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE;IACrD,gBAAgB,IAAI;IACpB,oBAAoB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,gBAAgB,CAAC;IAC5G,oBAAoB,IAAI,MAAM;IAC9B,wBAAwB,OAAO,MAAM;IACrC,gBAAgB;IAChB,gBAAgB,OAAO,CAAC,EAAE;IAC1B,oBAAoB,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,SAAS;IACpF,wBAAwB,MAAM,CAAC,CAAC;IAChC;IACA,oBAAoB,IAAI,YAAY,CAAC,CAAC,CAAC;IACvC,wBAAwB,MAAM,IAAI,aAAa,CAAC,YAAY,EAAE,0CAA0C,CAAC;IACzG,oBAAoB,OAAO,CAAC,IAAI,CAAC,sDAAsD,EAAE,CAAC,CAAC;IAC3F,gBAAgB;IAChB,YAAY;IACZ,YAAY,gBAAgB,EAAE;IAC9B,YAAY,IAAI,IAAI,CAAC,IAAI,GAAG,sBAAsB;IAClD,gBAAgB,MAAM,IAAI,aAAa,CAAC,iBAAiB,EAAE,2CAA2C,CAAC;IACvG,YAAY,OAAO,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,gBAAgB,CAAC;IACjF,QAAQ;IACR,QAAQ,OAAO,KAAK,EAAE;IACtB,YAAY,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC,IAAI,MAAM,SAAS,EAAE;IAC1F,gBAAgB,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACnD,YAAY;IACZ,iBAAiB;IACjB,gBAAgB,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,CAAC;IAC1D,YAAY;IACZ,YAAY,MAAM,KAAK;IACvB,QAAQ;IACR,gBAAgB;IAChB,YAAY,IAAI,CAAC,YAAY,EAAE,EAAE;IACjC,gBAAgB,IAAI,CAAC,UAAU,GAAG,IAAI;IACtC,gBAAgB,IAAI,CAAC,eAAe,GAAG,KAAK;IAC5C,gBAAgB,IAAI,CAAC,gBAAgB,GAAG,IAAI;IAC5C,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,IAAI,MAAM,MAAM,CAAC,OAAO,EAAE;IAC1B,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU;IAC5B,YAAY,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE;IACvC,QAAQ,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,UAAU,CAAC,EAAE;IACrH,YAAY,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE;IACvC;IACA;IACA,QAAQ,IAAI,CAAC,eAAe,GAAG,IAAI;IACnC,QAAQ,IAAI,CAAC,WAAW,EAAE;IAC1B,QAAQ,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE;IAClC,IAAI;IACJ,IAAI,WAAW,GAAG;IAClB,QAAQ,IAAI,EAAE;IACd,QAAQ,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB;IAChD,QAAQ,IAAI,CAAC,gBAAgB,GAAG,IAAI;IACpC;IACA;IACA,QAAQ,UAAU,KAAK,IAAI,IAAI,UAAU,KAAK,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC;IAC1G,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,MAAM,IAAI,IAAI,EAAE,KAAK,MAAM,GAAG,MAAM,GAAG,EAAE,CAAC,SAAS,EAAE;IAC9E,QAAQ,IAAI,CAAC,MAAM,GAAG,IAAI;IAC1B,QAAQ,IAAI,CAAC,QAAQ,GAAG,KAAK;IAC7B,IAAI;IACJ,IAAI,MAAM,eAAe,CAAC,UAAU,EAAE;IACtC,QAAQ,IAAI;IACZ,YAAY,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC;IACpD,YAAY,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE;IACxC,QAAQ;IACR,QAAQ,OAAO,GAAG,EAAE;IACpB,YAAY,OAAO,CAAC,KAAK,CAAC,wCAAwC,EAAE,GAAG,CAAC;IACxE,YAAY,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC;IAChE,QAAQ;IACR,IAAI;IACJ,IAAI,MAAM,eAAe,CAAC,OAAO,EAAE;IACnC,QAAQ,IAAI;IACZ,YAAY,MAAM,EAAE,OAAO,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC;IACvD,YAAY,OAAO,MAAMC,yBAAc,CAAC,KAAK,EAAE;IAC/C,gBAAgB,oBAAoB,EAAE,eAAe;IACrD,gBAAgB,OAAO,EAAE,IAAIC,kBAAO,CAAC,EAAE,SAAS,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAC;IACjF,aAAa,CAAC;IACd,QAAQ;IACR,QAAQ,OAAO,EAAE,EAAE;IACnB,YAAY,OAAO,KAAK;IACxB,QAAQ;IACR,IAAI;IACJ,IAAI,MAAM,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACxE,QAAQ,MAAM,IAAI,GAAG,MAAM,gBAAgB,EAAE;IAC7C,QAAQ,OAAO,CAAC,KAAK,CAAC,CAAC,mCAAmC,EAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC;IACvF,QAAQ,IAAI,MAAM;IAClB,QAAQ,IAAI,IAAI,GAAG,KAAK;IACxB,QAAQ,IAAI;IACZ,YAAY,MAAM,KAAK,GAAG,IAAIC,gBAAK,CAAC,EAAE,OAAO,EAAEC,sBAAW,EAAE,MAAM,EAAE,IAAIC,qBAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3F,YAAY,MAAM,GAAG,IAAIC,iBAAM,CAAC;IAChC,gBAAgB,MAAM,EAAE,IAAIC,0BAAe,EAAE;IAC7C,gBAAgB,MAAM,EAAE,IAAI,GAAG,IAAIC,uBAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,IAAIC,uBAAY,EAAE;IACtG,aAAa,CAAC;IACd,YAAY,IAAI,EAAE,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,OAAO,CAAC,EAAE,gBAAgB,CAAC,CAAC;IACrG,gBAAgB,OAAO,IAAI;IAC3B,YAAY,IAAI,OAAO;IACvB,YAAY,IAAI,IAAI,EAAE;IACtB,gBAAgB,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE;IAC3C,YAAY;IACZ,iBAAiB;IACjB,gBAAgB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM;IACnD,gBAAgB,IAAI,CAAC,MAAM;IAC3B,oBAAoB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;IACnE,gBAAgB,OAAO,GAAG,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IACnE,YAAY;IACZ,YAAY,IAAI,GAAG,IAAI;IACvB,YAAY,IAAI,IAAI;IACpB,gBAAgB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC;IACjD,YAAY,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE;IAC5E,QAAQ;IACR,gBAAgB;IAChB,YAAY,IAAI,CAAC,IAAI,EAAE;IACvB;IACA,gBAAgB,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,SAAS,CAAC,CAAC;IAC9G,gBAAgB,OAAO,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;IAClF,YAAY;IACZ,QAAQ;IACR,IAAI;IACJ;IACA,IAAI,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,gBAAgB,EAAE;IACjF,QAAQ,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,oBAAoB,EAAE;IACxD,QAAQ,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC;IAClF,QAAQ,MAAM,IAAI,GAAG,KAAK,GAAG,aAAa,CAAC,MAAM,KAAK,CAAC,eAAe,EAAE,EAAE,MAAM,KAAK,CAAC,gBAAgB,EAAE,EAAE,SAAS,CAAC,GAAG,EAAE;IACzH,QAAQ,gBAAgB,EAAE;IAC1B,QAAQ,MAAM,UAAU,GAAG,MAAMC,qBAAU,CAAC,IAAI,CAAC;IACjD,YAAY,KAAK;IACjB,YAAY,MAAM;IAClB,YAAY,KAAK,EAAE;IACnB,gBAAgB,GAAG,IAAI;IACvB,gBAAgB,OAAO,EAAE,IAAIR,kBAAO,CAAC;IACrC,oBAAoB,SAAS,EAAE,mBAAmB;IAClD,oBAAoB,OAAO,EAAE,UAAU,CAAC,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;IAC5J,iBAAiB,CAAC;IAClB;IACA;IACA;IACA;IACA,gBAAgB,oBAAoB,EAAE,eAAe;IACrD,gBAAgB,IAAI,SAAS,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;IACnD,aAAa;IACb;IACA,YAAY,IAAI,EAAE,EAAE;IACpB,SAAS,CAAC;IACV,QAAQ,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;IACjC,YAAY,OAAO,CAAC,IAAI,CAAC,0CAA0C,EAAE,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC;IACrH,YAAY,OAAO,KAAK;IACxB,QAAQ;IACR,QAAQ,gBAAgB,EAAE;IAC1B,QAAQ,IAAI,CAAC,gBAAgB,GAAG,UAAU;IAC1C,QAAQ,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC7E,QAAQ,UAAU,CAAC,UAAU,GAAG,CAAC,QAAQ,KAAK;IAC9C,YAAY,IAAI,IAAI,CAAC,gBAAgB,KAAK,UAAU;IACpD,gBAAgB,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC;IAC/G,QAAQ,CAAC;IACT,QAAQ,IAAI;IACZ,YAAY,MAAM,UAAU,CAAC,OAAO,EAAE;IACtC,QAAQ;IACR,QAAQ,OAAO,CAAC,EAAE;IAClB,YAAY,IAAI,IAAI,CAAC,eAAe,IAAI,CAAC,YAAYS,kCAAuB;IAC5E,gBAAgB,MAAM,cAAc,EAAE;IACtC,YAAY,MAAM,CAAC;IACnB,QAAQ;IACR,QAAQ,gBAAgB,EAAE;IAC1B,QAAQ,OAAO,IAAI;IACnB,IAAI;IACJ,IAAI,MAAM,UAAU,CAAC,gBAAgB,EAAE;IACvC,QAAQ,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM;IACxC,YAAY;IACZ,QAAQ,gBAAgB,EAAE;IAC1B,QAAQ,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAClF;IACA,QAAQ,MAAM,QAAQ,GAAG,IAAIC,aAAM,EAAE;IACrC,QAAQ,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK;IAClD;IACA;IACA,YAAY,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ;IACxC,gBAAgB,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC;IAC/G,QAAQ,CAAC,CAAC;IACV,QAAQ,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK;IAC5C,YAAY,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,OAAO,CAAC;IAClD,QAAQ,CAAC,CAAC;IACV,QAAQ,IAAI;IACZ,YAAY,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,YAAY,EAAE,CAAC;IACrD,QAAQ;IACR,QAAQ,OAAO,CAAC,EAAE;IAClB,YAAY,QAAQ,CAAC,SAAS,EAAE;IAChC,YAAY,OAAO,CAAC,KAAK,CAAC,wBAAwB,EAAE,CAAC,CAAC;IACtD,YAAY,MAAM,CAAC;IACnB,QAAQ;IACR;IACA;IACA,QAAQ,IAAI;IACZ,YAAY,gBAAgB,EAAE;IAC9B,QAAQ;IACR,QAAQ,OAAO,CAAC,EAAE;IAClB,YAAY,QAAQ,CAAC,SAAS,EAAE;IAChC,YAAY,MAAM,CAAC;IACnB,QAAQ;IACR,QAAQ,IAAI,CAAC,MAAM,GAAG,QAAQ;IAC9B,QAAQ,IAAI,CAAC,QAAQ,GAAG,IAAI;IAC5B,IAAI;IACJ,IAAI,MAAM,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE;IAC9D,QAAQ,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE;IAC5B,YAAY,MAAM,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC;IACnD,QAAQ;IACR,QAAQ,gBAAgB,EAAE;IAC1B,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;IAC1B,YAAY,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC;IACpD,QAAQ;IACR;IACA;IACA;IACA,QAAQ,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM;IAClC,QAAQ,OAAO,CAAC,KAAK,CAAC,0BAA0B,CAAC;IACjD,QAAQ,IAAI,CAAC,eAAe,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAC7E,QAAQ,MAAM,aAAa,GAAG,WAAW;IACzC,QAAQ,MAAM,cAAc,GAAG,YAAY;IAC3C,QAAQ,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,cAAc,CAAC,OAAO,CAAC;IACxE,QAAQ,MAAM,SAAS,GAAG,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IAClE,QAAQ,IAAI;IACZ,YAAY,MAAM,MAAM,CAAC,SAAS,CAAC,aAAa,EAAE,SAAS,CAAC;IAC5D,QAAQ;IACR,QAAQ,OAAO,GAAG,EAAE;IACpB,YAAY,gBAAgB,EAAE;IAC9B,YAAY,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,GAAG,CAAC;IAC/D,YAAY,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;IAC3D,QAAQ;IACR,QAAQ,gBAAgB,EAAE;IAC1B;IACA;IACA,QAAQ,MAAM,IAAI,GAAG;IACrB,YAAY,IAAI,EAAE,aAAa,EAAE,eAAe,EAAE,IAAI;IACtD,YAAY,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC;IAC1E,YAAY,KAAK,EAAE,iBAAiB,CAAC,SAAS,CAAC;IAC/C,YAAY,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;IAC/C,YAAY,cAAc;IAC1B,SAAS;IACT,QAAQ,IAAI;IACZ,YAAY,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;IACnC,QAAQ;IACR,QAAQ,OAAO,GAAG,EAAE;IACpB;IACA;IACA,YAAY,gBAAgB,EAAE;IAC9B,YAAY,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,GAAG,CAAC;IACnE,YAAY,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;IACtD,QAAQ;IACR,QAAQ,gBAAgB,EAAE;IAC1B,QAAQ,IAAI,IAAI;IAChB,QAAQ,IAAI;IACZ,YAAY,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;IACxD,QAAQ;IACR,QAAQ,OAAO,GAAG,EAAE;IACpB,YAAY,gBAAgB,EAAE;IAC9B,YAAY,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,GAAG,CAAC;IACrE,YAAY,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC;IAC3D,QAAQ;IACR,QAAQ,MAAM,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC;IAC9C,QAAQ,MAAM,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC;IAC/C,QAAQ,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/D,QAAQ,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE;IACxE,IAAI;IACJ;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -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
|
|
23
|
-
|
|
24
|
-
///
|
|
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()`
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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 =
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
var
|
|
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
|
-
|
|
119
|
+
shortSide = 1080; targetBitrate = 4_500_000 // ~4.5 Mbps
|
|
89
120
|
case "HIGH":
|
|
90
|
-
|
|
121
|
+
shortSide = 720; targetBitrate = 2_500_000 // ~2.5 Mbps
|
|
91
122
|
case "MEDIUM":
|
|
92
|
-
|
|
123
|
+
shortSide = 540; targetBitrate = 1_500_000 // ~1.5 Mbps (Giảm dung lượng cực tốt)
|
|
93
124
|
case "LOW":
|
|
94
|
-
|
|
125
|
+
shortSide = 480; targetBitrate = 1_000_000 // ~1.0 Mbps
|
|
95
126
|
case "360P":
|
|
96
|
-
|
|
127
|
+
shortSide = 360; targetBitrate = 700_000 // ~0.7 Mbps
|
|
97
128
|
case "VERY_LOW":
|
|
98
|
-
|
|
129
|
+
shortSide = 240; targetBitrate = 400_000 // ~0.4 Mbps
|
|
99
130
|
default:
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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(
|
|
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:
|
|
186
|
-
AVVideoHeightKey:
|
|
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:
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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 =
|
|
284
|
-
self.
|
|
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
|
|
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
|
-
|
|
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
|
}
|