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/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
- const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
31
- console.log('Starting to load FFmpeg core from:', baseURL);
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 this.ffmpeg.writeFile(inputFileName, inputData);
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 this.ffmpeg.exec(args);
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 this.ffmpeg.readFile(outputFileName);
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 this.ffmpeg.deleteFile(inputFileName);
153
- await this.ffmpeg.deleteFile(outputFileName);
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
- console.error('[FFmpeg] Compression process failed:', error);
274
+ if ((error === null || error === void 0 ? void 0 : error.code) === CANCELLED) {
275
+ console.log('[FFmpeg] Compression cancelled');
276
+ }
277
+ else {
278
+ console.error('[FFmpeg] Compression process failed:', error);
279
+ }
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;
@@ -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
- private var isCompressing = false
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-native-video-compressor",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Native video compressor using LightCompressor and AVFoundation",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",