capacitor-native-video-compressor 1.0.5 → 1.1.0

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