capacitor-native-video-compressor 1.0.4 → 1.0.5
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 +88 -5
- package/android/src/main/java/com/atomic/videocompressor/NativeVideoCompressorPlugin.java +88 -5
- package/dist/docs.json +58 -0
- package/dist/esm/definitions.d.ts +35 -0
- package/dist/esm/definitions.js +6 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +16 -1
- package/dist/esm/web.js +135 -23
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +142 -23
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +142 -23
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/NativeVideoCompressorPlugin/NativeVideoCompressorPlugin.swift +112 -8
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -1,45 +1,126 @@
|
|
|
1
1
|
var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `code` on the error `compressVideo` rejects with when `cancel()` stopped it.
|
|
6
|
+
* Callers should treat it as "the user asked for this" — no error toast, no
|
|
7
|
+
* retry prompt — and tell it apart from a genuine compression failure.
|
|
8
|
+
*/
|
|
9
|
+
const CANCELLED = 'CANCELLED';
|
|
10
|
+
|
|
4
11
|
const NativeVideoCompressor = core.registerPlugin('NativeVideoCompressor', {
|
|
5
12
|
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.NativeVideoCompressorWeb()),
|
|
6
13
|
});
|
|
7
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Rejection shape for a cancelled compress. `code` matches what `call.reject`
|
|
17
|
+
* puts on the error object on iOS/Android, so one `error.code === 'CANCELLED'`
|
|
18
|
+
* check works on every platform.
|
|
19
|
+
*/
|
|
20
|
+
class CancelledError extends Error {
|
|
21
|
+
constructor() {
|
|
22
|
+
super('Đã hủy nén');
|
|
23
|
+
this.code = CANCELLED;
|
|
24
|
+
this.name = 'CancelledError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const CORE_BASE_URL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
|
28
|
+
/**
|
|
29
|
+
* Cancelling means terminating the worker, which drops the loaded core with
|
|
30
|
+
* it — so without this the next compress re-downloads ~30 MB of wasm and
|
|
31
|
+
* "cancel" quietly costs more than it saves. Module scope, not instance: the
|
|
32
|
+
* bytes are identical for every instance and every job.
|
|
33
|
+
*/
|
|
34
|
+
let corePromise = null;
|
|
35
|
+
const loadCoreURLs = () => {
|
|
36
|
+
if (!corePromise) {
|
|
37
|
+
corePromise = Promise.all([
|
|
38
|
+
util.toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.js`, 'text/javascript'),
|
|
39
|
+
util.toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.wasm`, 'application/wasm'),
|
|
40
|
+
])
|
|
41
|
+
.then(([coreURL, wasmURL]) => ({ coreURL, wasmURL }))
|
|
42
|
+
.catch((e) => {
|
|
43
|
+
// Don't cache a failure — a flaky network shouldn't make every later
|
|
44
|
+
// compress in this session fail too.
|
|
45
|
+
corePromise = null;
|
|
46
|
+
throw e;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return corePromise;
|
|
50
|
+
};
|
|
8
51
|
class NativeVideoCompressorWeb extends core.WebPlugin {
|
|
9
52
|
constructor() {
|
|
10
53
|
super(...arguments);
|
|
11
54
|
this.ffmpeg = null;
|
|
12
55
|
this.isLoaded = false;
|
|
56
|
+
/** The job `compressVideo` is running right now, for `cancel({ jobId })`. */
|
|
57
|
+
this.currentJobId = null;
|
|
58
|
+
this.cancelRequested = false;
|
|
13
59
|
}
|
|
14
60
|
async loadFFmpeg() {
|
|
15
61
|
if (this.isLoaded && this.ffmpeg)
|
|
16
62
|
return;
|
|
17
63
|
this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });
|
|
64
|
+
// Built on a local, published to the field only once it is ready and still
|
|
65
|
+
// wanted. Assigning `this.ffmpeg` up front and setting `isLoaded` after the
|
|
66
|
+
// await left a window where a cancel could null the instance and the load,
|
|
67
|
+
// finishing a moment later, would set `isLoaded = true` on nothing at all —
|
|
68
|
+
// after which every compressVideo threw 'FFmpeg failed to load' for the
|
|
69
|
+
// rest of the page's life.
|
|
70
|
+
const instance = new ffmpeg.FFmpeg();
|
|
71
|
+
// Listen to FFmpeg progress and map it to capacitor plugin events
|
|
72
|
+
instance.on('progress', ({ progress }) => {
|
|
73
|
+
// progress is a fraction between 0 and 1
|
|
74
|
+
this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
|
|
75
|
+
});
|
|
76
|
+
instance.on('log', ({ message }) => {
|
|
77
|
+
console.log('[FFmpeg Log]', message);
|
|
78
|
+
});
|
|
18
79
|
try {
|
|
19
|
-
this.ffmpeg = new ffmpeg.FFmpeg();
|
|
20
|
-
// Listen to FFmpeg progress and map it to capacitor plugin events
|
|
21
|
-
this.ffmpeg.on('progress', ({ progress }) => {
|
|
22
|
-
// progress is a fraction between 0 and 1
|
|
23
|
-
this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });
|
|
24
|
-
});
|
|
25
|
-
this.ffmpeg.on('log', ({ message }) => {
|
|
26
|
-
console.log('[FFmpeg Log]', message);
|
|
27
|
-
});
|
|
28
80
|
// Use standard single-threaded version for best compatibility across browsers
|
|
29
81
|
// without requiring complex SharedArrayBuffer headers configuration
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await this.ffmpeg.load({
|
|
33
|
-
coreURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
|
|
34
|
-
wasmURL: await util.toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
|
|
35
|
-
});
|
|
36
|
-
this.isLoaded = true;
|
|
37
|
-
console.log('FFmpeg core loaded successfully');
|
|
82
|
+
console.log('Starting to load FFmpeg core from:', CORE_BASE_URL);
|
|
83
|
+
await instance.load(await loadCoreURLs());
|
|
38
84
|
}
|
|
39
85
|
catch (e) {
|
|
86
|
+
instance.terminate();
|
|
40
87
|
console.error('Failed to load FFmpeg:', e);
|
|
41
88
|
throw e;
|
|
42
89
|
}
|
|
90
|
+
// Cancelled while this was loading. Terminate what we just built rather
|
|
91
|
+
// than publish it — the caller asked for none of it.
|
|
92
|
+
if (this.cancelRequested) {
|
|
93
|
+
instance.terminate();
|
|
94
|
+
throw new CancelledError();
|
|
95
|
+
}
|
|
96
|
+
this.ffmpeg = instance;
|
|
97
|
+
this.isLoaded = true;
|
|
98
|
+
console.log('FFmpeg core loaded successfully');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Kill the worker outright — there is no gentler option.
|
|
102
|
+
*
|
|
103
|
+
* `ffmpeg.exec()` runs *synchronously* inside the worker (see
|
|
104
|
+
* @ffmpeg/ffmpeg's worker.js: `ffmpeg.exec(...args)` is not awaited), so the
|
|
105
|
+
* worker never reaches a message poll while encoding. The AbortSignal that
|
|
106
|
+
* `exec()` accepts only rejects the main-thread promise; the encode keeps
|
|
107
|
+
* burning CPU behind it. `worker.terminate()` is what actually stops it.
|
|
108
|
+
*
|
|
109
|
+
* The cost is the loaded core, which is why the core URLs are cached above.
|
|
110
|
+
*/
|
|
111
|
+
async cancel(options) {
|
|
112
|
+
var _a;
|
|
113
|
+
if (!this.currentJobId)
|
|
114
|
+
return { cancelled: false };
|
|
115
|
+
if ((options === null || options === void 0 ? void 0 : options.jobId) && options.jobId !== this.currentJobId)
|
|
116
|
+
return { cancelled: false };
|
|
117
|
+
// Set even when there is no instance yet: the job may still be inside
|
|
118
|
+
// loadFFmpeg, which reads this flag before publishing what it built.
|
|
119
|
+
this.cancelRequested = true;
|
|
120
|
+
(_a = this.ffmpeg) === null || _a === void 0 ? void 0 : _a.terminate();
|
|
121
|
+
this.ffmpeg = null;
|
|
122
|
+
this.isLoaded = false;
|
|
123
|
+
return { cancelled: true };
|
|
43
124
|
}
|
|
44
125
|
async initialize() {
|
|
45
126
|
try {
|
|
@@ -54,13 +135,30 @@ var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
|
|
|
54
135
|
}
|
|
55
136
|
}
|
|
56
137
|
async compressVideo(options) {
|
|
138
|
+
var _a;
|
|
139
|
+
this.currentJobId = (_a = options.jobId) !== null && _a !== void 0 ? _a : 'default';
|
|
140
|
+
this.cancelRequested = false;
|
|
141
|
+
// Checked at every stage boundary, not just around exec: terminate() only
|
|
142
|
+
// reaches work that has already got to the worker, so a cancel during the
|
|
143
|
+
// core download or the source fetch has nothing to interrupt and would
|
|
144
|
+
// otherwise let the compress carry on as if nothing had happened.
|
|
145
|
+
const throwIfCancelled = () => {
|
|
146
|
+
if (this.cancelRequested)
|
|
147
|
+
throw new CancelledError();
|
|
148
|
+
};
|
|
57
149
|
try {
|
|
58
150
|
if (!this.isLoaded) {
|
|
59
151
|
await this.loadFFmpeg();
|
|
60
152
|
}
|
|
153
|
+
throwIfCancelled();
|
|
61
154
|
if (!this.ffmpeg) {
|
|
62
155
|
throw new Error('FFmpeg failed to load');
|
|
63
156
|
}
|
|
157
|
+
// Bound once. `this.ffmpeg` is nulled by cancel(), so reading it after
|
|
158
|
+
// any of the four awaits below can hand back null on a reference the
|
|
159
|
+
// compiler narrowed at this line — surfacing a cancel as a TypeError
|
|
160
|
+
// instead of a CancelledError.
|
|
161
|
+
const ffmpeg = this.ffmpeg;
|
|
64
162
|
this.notifyListeners('onProgress', { status: 'started', percent: 0 });
|
|
65
163
|
const { sourcePath, quality = 'MEDIUM' } = options;
|
|
66
164
|
const inputFileName = 'input.mp4';
|
|
@@ -110,14 +208,18 @@ var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
|
|
|
110
208
|
console.error('[FFmpeg] Error fetching source file:', err);
|
|
111
209
|
throw new Error('Could not fetch source video file');
|
|
112
210
|
}
|
|
211
|
+
throwIfCancelled();
|
|
113
212
|
console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');
|
|
114
213
|
try {
|
|
115
|
-
await
|
|
214
|
+
await ffmpeg.writeFile(inputFileName, inputData);
|
|
116
215
|
}
|
|
117
216
|
catch (err) {
|
|
217
|
+
if (this.cancelRequested)
|
|
218
|
+
throw new CancelledError();
|
|
118
219
|
console.error('[FFmpeg] Error writing to FS:', err);
|
|
119
220
|
throw new Error('Could not write to FFmpeg FS');
|
|
120
221
|
}
|
|
222
|
+
throwIfCancelled();
|
|
121
223
|
// Construct FFmpeg command array
|
|
122
224
|
const args = ['-i', inputFileName];
|
|
123
225
|
// Video codec and compression parameters (superfast provides much better size than ultrafast)
|
|
@@ -132,25 +234,32 @@ var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
|
|
|
132
234
|
args.push(outputFileName);
|
|
133
235
|
console.log('[FFmpeg] Executing command:', args.join(' '));
|
|
134
236
|
try {
|
|
135
|
-
const retCode = await
|
|
237
|
+
const retCode = await ffmpeg.exec(args);
|
|
136
238
|
console.log('[FFmpeg] Exec returned code:', retCode);
|
|
137
239
|
}
|
|
138
240
|
catch (err) {
|
|
241
|
+
// terminate() rejects every in-flight call, so a cancel arrives here
|
|
242
|
+
// looking like a generic failure unless we name it.
|
|
243
|
+
if (this.cancelRequested)
|
|
244
|
+
throw new CancelledError();
|
|
139
245
|
console.error('[FFmpeg] Error executing command:', err);
|
|
140
246
|
throw new Error('FFmpeg execution failed');
|
|
141
247
|
}
|
|
248
|
+
throwIfCancelled();
|
|
142
249
|
console.log('[FFmpeg] Reading output file...');
|
|
143
250
|
let data;
|
|
144
251
|
try {
|
|
145
|
-
data = await
|
|
252
|
+
data = await ffmpeg.readFile(outputFileName);
|
|
146
253
|
}
|
|
147
254
|
catch (err) {
|
|
255
|
+
if (this.cancelRequested)
|
|
256
|
+
throw new CancelledError();
|
|
148
257
|
console.error('[FFmpeg] Error reading output file:', err);
|
|
149
258
|
throw new Error('Could not read FFmpeg output');
|
|
150
259
|
}
|
|
151
260
|
// Free memory
|
|
152
|
-
await
|
|
153
|
-
await
|
|
261
|
+
await ffmpeg.deleteFile(inputFileName);
|
|
262
|
+
await ffmpeg.deleteFile(outputFileName);
|
|
154
263
|
console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');
|
|
155
264
|
// Create a blob URL for the resulting file
|
|
156
265
|
const blob = new Blob([data], { type: 'video/mp4' });
|
|
@@ -162,9 +271,18 @@ var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
|
|
|
162
271
|
};
|
|
163
272
|
}
|
|
164
273
|
catch (error) {
|
|
165
|
-
|
|
274
|
+
if ((error === null || error === void 0 ? void 0 : error.code) === CANCELLED) {
|
|
275
|
+
console.log('[FFmpeg] Compression cancelled');
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.error('[FFmpeg] Compression process failed:', error);
|
|
279
|
+
}
|
|
166
280
|
throw error;
|
|
167
281
|
}
|
|
282
|
+
finally {
|
|
283
|
+
this.currentJobId = null;
|
|
284
|
+
this.cancelRequested = false;
|
|
285
|
+
}
|
|
168
286
|
}
|
|
169
287
|
}
|
|
170
288
|
|
|
@@ -173,6 +291,7 @@ var capacitorNativeVideoCompressor = (function (exports, core, ffmpeg, util) {
|
|
|
173
291
|
NativeVideoCompressorWeb: NativeVideoCompressorWeb
|
|
174
292
|
});
|
|
175
293
|
|
|
294
|
+
exports.CANCELLED = CANCELLED;
|
|
176
295
|
exports.NativeVideoCompressor = NativeVideoCompressor;
|
|
177
296
|
|
|
178
297
|
return exports;
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst NativeVideoCompressor = registerPlugin('NativeVideoCompressor', {\n web: () => import('./web').then((m) => new m.NativeVideoCompressorWeb()),\n});\nexport * from './definitions';\nexport { NativeVideoCompressor };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nimport { FFmpeg } from '@ffmpeg/ffmpeg';\nimport { fetchFile, toBlobURL } from '@ffmpeg/util';\nexport class NativeVideoCompressorWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.ffmpeg = null;\n this.isLoaded = false;\n }\n async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg)\n return;\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n try {\n this.ffmpeg = new FFmpeg();\n // Listen to FFmpeg progress and map it to capacitor plugin events\n this.ffmpeg.on('progress', ({ progress }) => {\n // progress is a fraction between 0 and 1\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n this.ffmpeg.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n // Use standard single-threaded version for best compatibility across browsers\n // without requiring complex SharedArrayBuffer headers configuration\n const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n console.log('Starting to load FFmpeg core from:', baseURL);\n await this.ffmpeg.load({\n coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),\n wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),\n });\n this.isLoaded = true;\n console.log('FFmpeg core loaded successfully');\n }\n catch (e) {\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n }\n async initialize() {\n try {\n if (this.isLoaded) {\n return { success: true, message: 'Already initialized' };\n }\n await this.loadFFmpeg();\n return { success: true, message: 'Initialization successful' };\n }\n catch (e) {\n return { success: false, message: e.message || 'Initialization failed' };\n }\n }\n async compressVideo(options) {\n try {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n const { sourcePath, quality = 'MEDIUM' } = options;\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n // Map quality to CRF, Scale, and Audio Bitrate\n let crf = '28';\n let scale = ''; // default keep resolution\n let audioBitrate = '128k';\n switch (quality) {\n case 'VERY_HIGH': // 1080p\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH': // 720p\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM': // 540p\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW': // 480p\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P': // 360p\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW': // 240p\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n console.log('[FFmpeg] Fetching source file...', sourcePath);\n let inputData;\n try {\n inputData = await fetchFile(sourcePath);\n }\n catch (err) {\n console.error('[FFmpeg] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');\n try {\n await this.ffmpeg.writeFile(inputFileName, inputData);\n }\n catch (err) {\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n // Construct FFmpeg command array\n const args = ['-i', inputFileName];\n // Video codec and compression parameters (superfast provides much better size than ultrafast)\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n // Resize if needed (split by space)\n if (scale) {\n args.push(...scale.split(' '));\n }\n // Audio re-encoding to save space\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n // Output\n args.push(outputFileName);\n console.log('[FFmpeg] Executing command:', args.join(' '));\n try {\n const retCode = await this.ffmpeg.exec(args);\n console.log('[FFmpeg] Exec returned code:', retCode);\n }\n catch (err) {\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n console.log('[FFmpeg] Reading output file...');\n let data;\n try {\n data = await this.ffmpeg.readFile(outputFileName);\n }\n catch (err) {\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n // Free memory\n await this.ffmpeg.deleteFile(inputFileName);\n await this.ffmpeg.deleteFile(outputFileName);\n console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');\n // Create a blob URL for the resulting file\n const blob = new Blob([data], { type: 'video/mp4' });\n const destPath = URL.createObjectURL(blob);\n console.log('[FFmpeg] Compression complete, returning path:', destPath);\n return {\n success: true,\n destPath: destPath,\n };\n }\n catch (error) {\n console.error('[FFmpeg] Compression process failed:', error);\n throw error;\n }\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin","FFmpeg","toBlobURL","fetchFile"],"mappings":"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"}
|
|
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 { fetchFile, toBlobURL } from '@ffmpeg/util';\nimport { CANCELLED } from './definitions';\n/**\n * Rejection shape for a cancelled compress. `code` matches what `call.reject`\n * puts on the error object on iOS/Android, so one `error.code === 'CANCELLED'`\n * check works on every platform.\n */\nclass CancelledError extends Error {\n constructor() {\n super('Đã hủy nén');\n this.code = CANCELLED;\n this.name = 'CancelledError';\n }\n}\nconst CORE_BASE_URL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';\n/**\n * Cancelling means terminating the worker, which drops the loaded core with\n * it — so without this the next compress re-downloads ~30 MB of wasm and\n * \"cancel\" quietly costs more than it saves. Module scope, not instance: the\n * bytes are identical for every instance and every job.\n */\nlet corePromise = null;\nconst loadCoreURLs = () => {\n if (!corePromise) {\n corePromise = Promise.all([\n toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.js`, 'text/javascript'),\n toBlobURL(`${CORE_BASE_URL}/ffmpeg-core.wasm`, 'application/wasm'),\n ])\n .then(([coreURL, wasmURL]) => ({ coreURL, wasmURL }))\n .catch((e) => {\n // Don't cache a failure — a flaky network shouldn't make every later\n // compress in this session fail too.\n corePromise = null;\n throw e;\n });\n }\n return corePromise;\n};\nexport class NativeVideoCompressorWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.ffmpeg = null;\n this.isLoaded = false;\n /** The job `compressVideo` is running right now, for `cancel({ jobId })`. */\n this.currentJobId = null;\n this.cancelRequested = false;\n }\n async loadFFmpeg() {\n if (this.isLoaded && this.ffmpeg)\n return;\n this.notifyListeners('onProgress', { status: 'loading_core', percent: 0 });\n // Built on a local, published to the field only once it is ready and still\n // wanted. Assigning `this.ffmpeg` up front and setting `isLoaded` after the\n // await left a window where a cancel could null the instance and the load,\n // finishing a moment later, would set `isLoaded = true` on nothing at all —\n // after which every compressVideo threw 'FFmpeg failed to load' for the\n // rest of the page's life.\n const instance = new FFmpeg();\n // Listen to FFmpeg progress and map it to capacitor plugin events\n instance.on('progress', ({ progress }) => {\n // progress is a fraction between 0 and 1\n this.notifyListeners('onProgress', { status: 'progress', percent: Math.round(progress * 100) });\n });\n instance.on('log', ({ message }) => {\n console.log('[FFmpeg Log]', message);\n });\n try {\n // Use standard single-threaded version for best compatibility across browsers\n // without requiring complex SharedArrayBuffer headers configuration\n console.log('Starting to load FFmpeg core from:', CORE_BASE_URL);\n await instance.load(await loadCoreURLs());\n }\n catch (e) {\n instance.terminate();\n console.error('Failed to load FFmpeg:', e);\n throw e;\n }\n // Cancelled while this was loading. Terminate what we just built rather\n // than publish it — the caller asked for none of it.\n if (this.cancelRequested) {\n instance.terminate();\n throw new CancelledError();\n }\n this.ffmpeg = instance;\n this.isLoaded = true;\n console.log('FFmpeg core loaded successfully');\n }\n /**\n * Kill the worker outright — there is no gentler option.\n *\n * `ffmpeg.exec()` runs *synchronously* inside the worker (see\n * @ffmpeg/ffmpeg's worker.js: `ffmpeg.exec(...args)` is not awaited), so the\n * worker never reaches a message poll while encoding. The AbortSignal that\n * `exec()` accepts only rejects the main-thread promise; the encode keeps\n * burning CPU behind it. `worker.terminate()` is what actually stops it.\n *\n * The cost is the loaded core, which is why the core URLs are cached above.\n */\n async cancel(options) {\n var _a;\n if (!this.currentJobId)\n return { cancelled: false };\n if ((options === null || options === void 0 ? void 0 : options.jobId) && options.jobId !== this.currentJobId)\n return { cancelled: false };\n // Set even when there is no instance yet: the job may still be inside\n // loadFFmpeg, which reads this flag before publishing what it built.\n this.cancelRequested = true;\n (_a = this.ffmpeg) === null || _a === void 0 ? void 0 : _a.terminate();\n this.ffmpeg = null;\n this.isLoaded = false;\n return { cancelled: true };\n }\n async initialize() {\n try {\n if (this.isLoaded) {\n return { success: true, message: 'Already initialized' };\n }\n await this.loadFFmpeg();\n return { success: true, message: 'Initialization successful' };\n }\n catch (e) {\n return { success: false, message: e.message || 'Initialization failed' };\n }\n }\n async compressVideo(options) {\n var _a;\n this.currentJobId = (_a = options.jobId) !== null && _a !== void 0 ? _a : 'default';\n this.cancelRequested = false;\n // Checked at every stage boundary, not just around exec: terminate() only\n // reaches work that has already got to the worker, so a cancel during the\n // core download or the source fetch has nothing to interrupt and would\n // otherwise let the compress carry on as if nothing had happened.\n const throwIfCancelled = () => {\n if (this.cancelRequested)\n throw new CancelledError();\n };\n try {\n if (!this.isLoaded) {\n await this.loadFFmpeg();\n }\n throwIfCancelled();\n if (!this.ffmpeg) {\n throw new Error('FFmpeg failed to load');\n }\n // Bound once. `this.ffmpeg` is nulled by cancel(), so reading it after\n // any of the four awaits below can hand back null on a reference the\n // compiler narrowed at this line — surfacing a cancel as a TypeError\n // instead of a CancelledError.\n const ffmpeg = this.ffmpeg;\n this.notifyListeners('onProgress', { status: 'started', percent: 0 });\n const { sourcePath, quality = 'MEDIUM' } = options;\n const inputFileName = 'input.mp4';\n const outputFileName = 'output.mp4';\n // Map quality to CRF, Scale, and Audio Bitrate\n let crf = '28';\n let scale = ''; // default keep resolution\n let audioBitrate = '128k';\n switch (quality) {\n case 'VERY_HIGH': // 1080p\n crf = '23';\n scale = '-vf scale=-2:1080';\n audioBitrate = '128k';\n break;\n case 'HIGH': // 720p\n crf = '28';\n scale = '-vf scale=-2:720';\n audioBitrate = '128k';\n break;\n case 'MEDIUM': // 540p\n crf = '30';\n scale = '-vf scale=-2:540';\n audioBitrate = '96k';\n break;\n case 'LOW': // 480p\n crf = '32';\n scale = '-vf scale=-2:480';\n audioBitrate = '96k';\n break;\n case '360P': // 360p\n crf = '28';\n scale = '-vf scale=-2:360';\n audioBitrate = '64k';\n break;\n case 'VERY_LOW': // 240p\n crf = '35';\n scale = '-vf scale=-2:240';\n audioBitrate = '48k';\n break;\n }\n console.log('[FFmpeg] Fetching source file...', sourcePath);\n let inputData;\n try {\n inputData = await fetchFile(sourcePath);\n }\n catch (err) {\n console.error('[FFmpeg] Error fetching source file:', err);\n throw new Error('Could not fetch source video file');\n }\n throwIfCancelled();\n console.log('[FFmpeg] Writing file to FS...', inputData.byteLength, 'bytes');\n try {\n await ffmpeg.writeFile(inputFileName, inputData);\n }\n catch (err) {\n if (this.cancelRequested)\n throw new CancelledError();\n console.error('[FFmpeg] Error writing to FS:', err);\n throw new Error('Could not write to FFmpeg FS');\n }\n throwIfCancelled();\n // Construct FFmpeg command array\n const args = ['-i', inputFileName];\n // Video codec and compression parameters (superfast provides much better size than ultrafast)\n args.push('-c:v', 'libx264', '-preset', 'superfast', '-crf', crf);\n // Resize if needed (split by space)\n if (scale) {\n args.push(...scale.split(' '));\n }\n // Audio re-encoding to save space\n args.push('-c:a', 'aac', '-b:a', audioBitrate);\n // Output\n args.push(outputFileName);\n console.log('[FFmpeg] Executing command:', args.join(' '));\n try {\n const retCode = await ffmpeg.exec(args);\n console.log('[FFmpeg] Exec returned code:', retCode);\n }\n catch (err) {\n // terminate() rejects every in-flight call, so a cancel arrives here\n // looking like a generic failure unless we name it.\n if (this.cancelRequested)\n throw new CancelledError();\n console.error('[FFmpeg] Error executing command:', err);\n throw new Error('FFmpeg execution failed');\n }\n throwIfCancelled();\n console.log('[FFmpeg] Reading output file...');\n let data;\n try {\n data = await ffmpeg.readFile(outputFileName);\n }\n catch (err) {\n if (this.cancelRequested)\n throw new CancelledError();\n console.error('[FFmpeg] Error reading output file:', err);\n throw new Error('Could not read FFmpeg output');\n }\n // Free memory\n await ffmpeg.deleteFile(inputFileName);\n await ffmpeg.deleteFile(outputFileName);\n console.log('[FFmpeg] Creating Blob URL from output...', data.byteLength, 'bytes');\n // Create a blob URL for the resulting file\n const blob = new Blob([data], { type: 'video/mp4' });\n const destPath = URL.createObjectURL(blob);\n console.log('[FFmpeg] Compression complete, returning path:', destPath);\n return {\n success: true,\n destPath: destPath,\n };\n }\n catch (error) {\n if ((error === null || error === void 0 ? void 0 : error.code) === CANCELLED) {\n console.log('[FFmpeg] Compression cancelled');\n }\n else {\n console.error('[FFmpeg] Compression process failed:', error);\n }\n throw error;\n }\n finally {\n this.currentJobId = null;\n this.cancelRequested = false;\n }\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","toBlobURL","WebPlugin","FFmpeg","fetchFile"],"mappings":"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"}
|
|
@@ -8,18 +8,69 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
8
8
|
|
|
9
9
|
public let identifier = "NativeVideoCompressorPlugin"
|
|
10
10
|
public let jsName = "NativeVideoCompressor"
|
|
11
|
+
// Every method callable from JS must be listed here. `initialize` was
|
|
12
|
+
// missing, so calling it threw "not implemented" on device — harmless
|
|
13
|
+
// (the web build needs it, native doesn't) but it buried a real signal in
|
|
14
|
+
// a console error every single compress.
|
|
11
15
|
public let pluginMethods:[CAPPluginMethod] = [
|
|
12
|
-
CAPPluginMethod(name: "compressVideo", returnType: CAPPluginReturnPromise)
|
|
16
|
+
CAPPluginMethod(name: "compressVideo", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "cancel", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise)
|
|
13
19
|
]
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
|
|
21
|
+
/// 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.
|
|
28
|
+
private let stateLock = NSLock()
|
|
29
|
+
|
|
30
|
+
/// Nothing to do natively; AVFoundation needs no warm-up. Answered rather
|
|
31
|
+
/// than left unimplemented so a caller can await it unconditionally.
|
|
32
|
+
@objc func initialize(_ call: CAPPluginCall) {
|
|
33
|
+
call.resolve(["success": true, "message": "No initialization needed on iOS"])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stop the running compress.
|
|
38
|
+
*
|
|
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.
|
|
43
|
+
*/
|
|
44
|
+
@objc func cancel(_ call: CAPPluginCall) {
|
|
45
|
+
stateLock.lock()
|
|
46
|
+
let running = currentJobId
|
|
47
|
+
let requested = call.getString("jobId")
|
|
48
|
+
// A cancel for a job that isn't the one running would otherwise kill
|
|
49
|
+
// an unrelated caller's compress — there is only ever one.
|
|
50
|
+
guard let running = running, requested == nil || requested == running else {
|
|
51
|
+
stateLock.unlock()
|
|
52
|
+
call.resolve(["cancelled": false])
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
isCancelled = true
|
|
56
|
+
let reader = currentReader
|
|
57
|
+
stateLock.unlock()
|
|
58
|
+
|
|
59
|
+
reader?.cancelReading()
|
|
60
|
+
call.resolve(["cancelled": true])
|
|
61
|
+
}
|
|
16
62
|
|
|
17
63
|
@objc func compressVideo(_ call: CAPPluginCall) {
|
|
18
64
|
guard let sourcePath = call.getString("sourcePath") else {
|
|
19
65
|
call.reject("Missing sourcePath")
|
|
20
66
|
return
|
|
21
67
|
}
|
|
22
|
-
|
|
68
|
+
|
|
69
|
+
stateLock.lock()
|
|
70
|
+
currentJobId = call.getString("jobId") ?? "default"
|
|
71
|
+
isCancelled = false
|
|
72
|
+
stateLock.unlock()
|
|
73
|
+
|
|
23
74
|
let videoURL = sourcePath.hasPrefix("file://") ? URL(string: sourcePath)! : URL(fileURLWithPath: sourcePath)
|
|
24
75
|
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("compressed_\(UUID().uuidString).mp4")
|
|
25
76
|
|
|
@@ -52,7 +103,6 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
52
103
|
let asset = AVAsset(url: videoURL)
|
|
53
104
|
|
|
54
105
|
self.notifyListeners("onProgress", data:["status": "started"])
|
|
55
|
-
self.isCompressing = true
|
|
56
106
|
|
|
57
107
|
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
|
58
108
|
backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "VideoCompression") {
|
|
@@ -83,6 +133,7 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
83
133
|
backgroundTask: UIBackgroundTaskIdentifier
|
|
84
134
|
) {
|
|
85
135
|
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
|
|
136
|
+
clearJobState()
|
|
86
137
|
call.reject("Không tìm thấy track video")
|
|
87
138
|
return
|
|
88
139
|
}
|
|
@@ -92,6 +143,18 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
92
143
|
let reader = try AVAssetReader(asset: asset)
|
|
93
144
|
let writer = try AVAssetWriter(url: outputURL, fileType: .mp4)
|
|
94
145
|
writer.shouldOptimizeForNetworkUse = true // Tối ưu cho Web/Network
|
|
146
|
+
|
|
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)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
95
158
|
|
|
96
159
|
// --- XỬ LÝ KÍCH THƯỚC VÀ CHIỀU XOAY (ORIENTATION) ---
|
|
97
160
|
let naturalSize = videoTrack.naturalSize
|
|
@@ -212,11 +275,24 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
212
275
|
|
|
213
276
|
// --- KẾT THÚC ---
|
|
214
277
|
group.notify(queue: .main) {
|
|
215
|
-
self.isCompressing = false
|
|
216
278
|
if backgroundTask != .invalid {
|
|
217
279
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
218
280
|
}
|
|
219
|
-
|
|
281
|
+
|
|
282
|
+
self.stateLock.lock()
|
|
283
|
+
let wasCancelled = self.isCancelled
|
|
284
|
+
self.currentReader = nil
|
|
285
|
+
self.currentJobId = nil
|
|
286
|
+
self.isCancelled = false
|
|
287
|
+
self.stateLock.unlock()
|
|
288
|
+
|
|
289
|
+
if wasCancelled {
|
|
290
|
+
writer.cancelWriting()
|
|
291
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
292
|
+
call.reject("Đã hủy nén", NativeVideoCompressorPlugin.cancelledCode)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
220
296
|
if reader.status == .completed {
|
|
221
297
|
writer.finishWriting {
|
|
222
298
|
DispatchQueue.main.async {
|
|
@@ -232,12 +308,40 @@ public class NativeVideoCompressorPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
232
308
|
}
|
|
233
309
|
} else {
|
|
234
310
|
writer.cancelWriting()
|
|
311
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
235
312
|
call.reject("Lỗi khi đọc file: \(reader.error?.localizedDescription ?? "Unknown")")
|
|
236
313
|
}
|
|
237
314
|
}
|
|
238
|
-
|
|
315
|
+
|
|
239
316
|
} catch {
|
|
317
|
+
clearJobState()
|
|
240
318
|
call.reject("Lỗi khởi tạo bộ nén: \(error.localizedDescription)")
|
|
241
319
|
}
|
|
242
320
|
}
|
|
321
|
+
|
|
322
|
+
/// Cancelled before the reader started — nothing to stop, just clean up.
|
|
323
|
+
private func finishCancelled(
|
|
324
|
+
outputURL: URL,
|
|
325
|
+
call: CAPPluginCall,
|
|
326
|
+
backgroundTask: UIBackgroundTaskIdentifier
|
|
327
|
+
) {
|
|
328
|
+
if backgroundTask != .invalid {
|
|
329
|
+
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
330
|
+
}
|
|
331
|
+
clearJobState()
|
|
332
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
333
|
+
call.reject("Đã hủy nén", NativeVideoCompressorPlugin.cancelledCode)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private func clearJobState() {
|
|
337
|
+
stateLock.lock()
|
|
338
|
+
currentReader = nil
|
|
339
|
+
currentJobId = nil
|
|
340
|
+
isCancelled = false
|
|
341
|
+
stateLock.unlock()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Matches the `CANCELLED` constant the JS side exports, so one
|
|
345
|
+
/// `error.code` check covers web, iOS and Android alike.
|
|
346
|
+
private static let cancelledCode = "CANCELLED"
|
|
243
347
|
}
|
package/package.json
CHANGED