@remotion/webcodecs 4.0.329 → 4.0.331

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.
@@ -5593,20 +5593,23 @@ var convertMedia = async function({
5593
5593
  });
5594
5594
  };
5595
5595
  // src/extract-frames.ts
5596
+ import { parseMedia } from "@remotion/media-parser";
5597
+
5598
+ // src/internal-extract-frames.ts
5596
5599
  import {
5597
- hasBeenAborted,
5598
5600
  MediaParserAbortError as MediaParserAbortError4,
5599
- mediaParserController as mediaParserController2,
5600
- WEBCODECS_TIMESCALE
5601
+ WEBCODECS_TIMESCALE,
5602
+ hasBeenAborted,
5603
+ mediaParserController as mediaParserController2
5601
5604
  } from "@remotion/media-parser";
5602
- import { parseMediaOnWebWorker } from "@remotion/media-parser/worker";
5603
5605
  var internalExtractFrames = ({
5604
5606
  src,
5605
5607
  onFrame,
5606
5608
  signal,
5607
5609
  timestampsInSeconds,
5608
5610
  acknowledgeRemotionLicense,
5609
- logLevel
5611
+ logLevel,
5612
+ parseMediaImplementation
5610
5613
  }) => {
5611
5614
  const controller = mediaParserController2();
5612
5615
  const expectedFrames = [];
@@ -5617,7 +5620,9 @@ var internalExtractFrames = ({
5617
5620
  };
5618
5621
  signal?.addEventListener("abort", abortListener, { once: true });
5619
5622
  let dur = null;
5620
- parseMediaOnWebWorker({
5623
+ let lastFrame;
5624
+ let lastFrameEmitted;
5625
+ parseMediaImplementation({
5621
5626
  src: new URL(src, window.location.href),
5622
5627
  acknowledgeRemotionLicense,
5623
5628
  controller,
@@ -5636,7 +5641,6 @@ var internalExtractFrames = ({
5636
5641
  throw new Error("expected at least one timestamp to extract but found zero");
5637
5642
  }
5638
5643
  controller.seek(timestampTargets[0]);
5639
- let lastFrame;
5640
5644
  const decoder = createVideoDecoder({
5641
5645
  onFrame: (frame) => {
5642
5646
  Log.trace(logLevel, "Received frame with timestamp", frame.timestamp);
@@ -5653,12 +5657,20 @@ var internalExtractFrames = ({
5653
5657
  }
5654
5658
  if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame) {
5655
5659
  onFrame(lastFrame);
5660
+ lastFrameEmitted = lastFrame;
5656
5661
  expectedFrames.shift();
5662
+ if (lastFrame) {
5663
+ lastFrame.close();
5664
+ }
5657
5665
  lastFrame = frame;
5658
5666
  return;
5659
5667
  }
5660
5668
  expectedFrames.shift();
5661
5669
  onFrame(frame);
5670
+ if (lastFrame && lastFrame !== lastFrameEmitted) {
5671
+ lastFrame.close();
5672
+ }
5673
+ lastFrameEmitted = frame;
5662
5674
  lastFrame = frame;
5663
5675
  },
5664
5676
  onError: (e) => {
@@ -5678,14 +5690,14 @@ var internalExtractFrames = ({
5678
5690
  if (!sam) {
5679
5691
  throw new Error("Sample is undefined");
5680
5692
  }
5681
- await decoder.waitForQueueToBeLessThan(10);
5693
+ await decoder.waitForQueueToBeLessThan(20);
5682
5694
  Log.trace(logLevel, "Decoding sample", sam.timestamp);
5683
5695
  await decoder.decode(sam);
5684
5696
  }
5685
5697
  };
5686
5698
  return async (sample) => {
5687
5699
  const nextTimestampWeWant = timestampTargets[0];
5688
- Log.trace(logLevel, "Received sample with dts", sample.decodingTimestamp, "and cts", sample.timestamp);
5700
+ Log.trace(logLevel, `Received ${sample.type} sample with dts`, sample.decodingTimestamp, "and cts", sample.timestamp);
5689
5701
  if (sample.type === "key") {
5690
5702
  await decoder.flush();
5691
5703
  queued.length = 0;
@@ -5710,7 +5722,7 @@ var internalExtractFrames = ({
5710
5722
  return async () => {
5711
5723
  await doProcess();
5712
5724
  await decoder.flush();
5713
- if (lastFrame) {
5725
+ if (lastFrame && lastFrameEmitted !== lastFrame) {
5714
5726
  lastFrame.close();
5715
5727
  }
5716
5728
  };
@@ -5725,16 +5737,22 @@ var internalExtractFrames = ({
5725
5737
  resolvers.resolve();
5726
5738
  }
5727
5739
  }).finally(() => {
5740
+ if (lastFrame && lastFrameEmitted !== lastFrame) {
5741
+ lastFrame.close();
5742
+ }
5728
5743
  signal?.removeEventListener("abort", abortListener);
5729
5744
  });
5730
5745
  return resolvers.promise;
5731
5746
  };
5747
+
5748
+ // src/extract-frames.ts
5732
5749
  var extractFrames = (options) => {
5733
5750
  return internalExtractFrames({
5734
5751
  ...options,
5735
5752
  signal: options.signal ?? null,
5736
5753
  acknowledgeRemotionLicense: options.acknowledgeRemotionLicense ?? false,
5737
- logLevel: options.logLevel ?? "info"
5754
+ logLevel: options.logLevel ?? "info",
5755
+ parseMediaImplementation: parseMedia
5738
5756
  });
5739
5757
  };
5740
5758
  // src/get-available-audio-codecs.ts
@@ -5756,7 +5774,7 @@ var getAvailableAudioCodecs = ({
5756
5774
  import {
5757
5775
  hasBeenAborted as hasBeenAborted2,
5758
5776
  mediaParserController as mediaParserController3,
5759
- parseMedia
5777
+ parseMedia as parseMedia2
5760
5778
  } from "@remotion/media-parser";
5761
5779
  var extractOverlappingAudioSamples = ({
5762
5780
  sample,
@@ -5814,7 +5832,7 @@ var getPartialAudioData = async ({
5814
5832
  if (fromSeconds > 0) {
5815
5833
  controller.seek(fromSeconds);
5816
5834
  }
5817
- await parseMedia({
5835
+ await parseMedia2({
5818
5836
  acknowledgeRemotionLicense: true,
5819
5837
  src,
5820
5838
  controller,
@@ -0,0 +1,506 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __toESM = (mod, isNodeMode, target) => {
7
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
8
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
+ for (let key of __getOwnPropNames(mod))
10
+ if (!__hasOwnProp.call(to, key))
11
+ __defProp(to, key, {
12
+ get: () => mod[key],
13
+ enumerable: true
14
+ });
15
+ return to;
16
+ };
17
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
18
+
19
+ // src/extract-frames-on-web-worker.ts
20
+ import { parseMediaOnWebWorker } from "@remotion/media-parser/worker";
21
+
22
+ // src/internal-extract-frames.ts
23
+ import {
24
+ MediaParserAbortError,
25
+ WEBCODECS_TIMESCALE,
26
+ hasBeenAborted,
27
+ mediaParserController
28
+ } from "@remotion/media-parser";
29
+
30
+ // src/create/with-resolvers.ts
31
+ var withResolvers = function() {
32
+ let resolve;
33
+ let reject;
34
+ const promise = new Promise((res, rej) => {
35
+ resolve = res;
36
+ reject = rej;
37
+ });
38
+ return { promise, resolve, reject };
39
+ };
40
+ var withResolversAndWaitForReturn = () => {
41
+ const { promise, reject, resolve } = withResolvers();
42
+ const { promise: returnPromise, resolve: resolveReturn } = withResolvers();
43
+ return {
44
+ getPromiseToImmediatelyReturn: () => {
45
+ resolveReturn(undefined);
46
+ return promise;
47
+ },
48
+ reject: (reason) => {
49
+ returnPromise.then(() => reject(reason));
50
+ },
51
+ resolve
52
+ };
53
+ };
54
+
55
+ // src/flush-pending.ts
56
+ var makeFlushPending = () => {
57
+ const { promise, resolve, reject } = withResolvers();
58
+ return {
59
+ promise,
60
+ resolve,
61
+ reject
62
+ };
63
+ };
64
+
65
+ // src/create/event-emitter.ts
66
+ class IoEventEmitter {
67
+ listeners = {
68
+ input: [],
69
+ output: [],
70
+ processed: [],
71
+ progress: []
72
+ };
73
+ addEventListener(name, callback) {
74
+ this.listeners[name].push(callback);
75
+ }
76
+ removeEventListener(name, callback) {
77
+ this.listeners[name] = this.listeners[name].filter((l) => l !== callback);
78
+ }
79
+ dispatchEvent(dispatchName, context) {
80
+ this.listeners[dispatchName].forEach((callback) => {
81
+ callback({ detail: context });
82
+ });
83
+ }
84
+ }
85
+
86
+ // src/log.ts
87
+ import { MediaParserInternals } from "@remotion/media-parser";
88
+ var { Log } = MediaParserInternals;
89
+
90
+ // src/io-manager/make-timeout-promise.ts
91
+ var makeTimeoutPromise = ({
92
+ label,
93
+ ms,
94
+ controller
95
+ }) => {
96
+ const { promise, reject, resolve } = withResolvers();
97
+ let timeout = null;
98
+ const set = () => {
99
+ timeout = setTimeout(() => {
100
+ reject(new Error(`${label()} (timed out after ${ms}ms)`));
101
+ }, ms);
102
+ };
103
+ set();
104
+ const onPause = () => {
105
+ if (timeout) {
106
+ clearTimeout(timeout);
107
+ }
108
+ };
109
+ const onResume = () => {
110
+ set();
111
+ };
112
+ if (controller) {
113
+ controller.addEventListener("pause", onPause);
114
+ controller.addEventListener("resume", onResume);
115
+ }
116
+ return {
117
+ timeoutPromise: promise,
118
+ clear: () => {
119
+ if (timeout) {
120
+ clearTimeout(timeout);
121
+ }
122
+ resolve();
123
+ if (controller) {
124
+ controller.removeEventListener("pause", onPause);
125
+ controller.removeEventListener("resume", onResume);
126
+ }
127
+ }
128
+ };
129
+ };
130
+
131
+ // src/io-manager/io-synchronizer.ts
132
+ var makeIoSynchronizer = ({
133
+ logLevel,
134
+ label,
135
+ controller
136
+ }) => {
137
+ const eventEmitter = new IoEventEmitter;
138
+ let lastInput = 0;
139
+ let lastOutput = 0;
140
+ let inputsSinceLastOutput = 0;
141
+ let inputs = [];
142
+ let resolvers = [];
143
+ const getQueuedItems = () => {
144
+ inputs = inputs.filter((input) => Math.floor(input) > Math.floor(lastOutput) + 1);
145
+ return inputs.length;
146
+ };
147
+ const printState = (prefix) => {
148
+ Log.trace(logLevel, `[${label}] ${prefix}, state: Last input = ${lastInput} Last output = ${lastOutput} Inputs since last output = ${inputsSinceLastOutput}, Queue = ${getQueuedItems()}`);
149
+ };
150
+ const inputItem = (timestamp) => {
151
+ lastInput = timestamp;
152
+ inputsSinceLastOutput++;
153
+ inputs.push(timestamp);
154
+ eventEmitter.dispatchEvent("input", {
155
+ timestamp
156
+ });
157
+ printState("Input item");
158
+ };
159
+ const onOutput = (timestamp) => {
160
+ lastOutput = timestamp;
161
+ inputsSinceLastOutput = 0;
162
+ eventEmitter.dispatchEvent("output", {
163
+ timestamp
164
+ });
165
+ printState("Got output");
166
+ };
167
+ const waitForOutput = () => {
168
+ const { promise, resolve } = withResolvers();
169
+ const on = () => {
170
+ eventEmitter.removeEventListener("output", on);
171
+ resolve();
172
+ resolvers = resolvers.filter((resolver) => resolver !== resolve);
173
+ };
174
+ eventEmitter.addEventListener("output", on);
175
+ resolvers.push(resolve);
176
+ return promise;
177
+ };
178
+ const makeErrorBanner = () => {
179
+ return [
180
+ `Waited too long for ${label} to finish:`,
181
+ `${getQueuedItems()} queued items`,
182
+ `inputs: ${JSON.stringify(inputs)}`,
183
+ `last output: ${lastOutput}`
184
+ ];
185
+ };
186
+ const waitForQueueSize = async (queueSize) => {
187
+ if (getQueuedItems() <= queueSize) {
188
+ return Promise.resolve();
189
+ }
190
+ const { timeoutPromise, clear } = makeTimeoutPromise({
191
+ label: () => [
192
+ ...makeErrorBanner(),
193
+ `wanted: <${queueSize} queued items`,
194
+ `Report this at https://remotion.dev/report`
195
+ ].join(`
196
+ `),
197
+ ms: 1e4,
198
+ controller
199
+ });
200
+ if (controller) {
201
+ controller._internals._mediaParserController._internals.signal.addEventListener("abort", clear);
202
+ }
203
+ await Promise.race([
204
+ timeoutPromise,
205
+ (async () => {
206
+ while (getQueuedItems() > queueSize) {
207
+ await waitForOutput();
208
+ }
209
+ })()
210
+ ]).finally(() => clear());
211
+ if (controller) {
212
+ controller._internals._mediaParserController._internals.signal.removeEventListener("abort", clear);
213
+ }
214
+ };
215
+ const clearQueue = () => {
216
+ inputs.length = 0;
217
+ lastInput = 0;
218
+ lastOutput = 0;
219
+ inputsSinceLastOutput = 0;
220
+ resolvers.forEach((resolver) => {
221
+ return resolver();
222
+ });
223
+ resolvers.length = 0;
224
+ inputs.length = 0;
225
+ };
226
+ return {
227
+ inputItem,
228
+ onOutput,
229
+ waitForQueueSize,
230
+ clearQueue
231
+ };
232
+ };
233
+
234
+ // src/create-video-decoder.ts
235
+ var internalCreateVideoDecoder = ({
236
+ onFrame,
237
+ onError,
238
+ controller,
239
+ config,
240
+ logLevel
241
+ }) => {
242
+ if (controller && controller._internals._mediaParserController._internals.signal.aborted) {
243
+ throw new Error("Not creating audio decoder, already aborted");
244
+ }
245
+ const ioSynchronizer = makeIoSynchronizer({
246
+ logLevel,
247
+ label: "Video decoder",
248
+ controller
249
+ });
250
+ let mostRecentSampleReceived = null;
251
+ const videoDecoder = new VideoDecoder({
252
+ async output(frame) {
253
+ try {
254
+ await onFrame(frame);
255
+ } catch (err) {
256
+ onError(err);
257
+ frame.close();
258
+ }
259
+ ioSynchronizer.onOutput(frame.timestamp);
260
+ },
261
+ error(error) {
262
+ onError(error);
263
+ }
264
+ });
265
+ const close = () => {
266
+ if (controller) {
267
+ controller._internals._mediaParserController._internals.signal.removeEventListener("abort", onAbort);
268
+ }
269
+ if (videoDecoder.state === "closed") {
270
+ return;
271
+ }
272
+ videoDecoder.close();
273
+ };
274
+ const onAbort = () => {
275
+ close();
276
+ };
277
+ if (controller) {
278
+ controller._internals._mediaParserController._internals.signal.addEventListener("abort", onAbort);
279
+ }
280
+ videoDecoder.configure(config);
281
+ const decode = async (sample) => {
282
+ if (videoDecoder.state === "closed") {
283
+ return;
284
+ }
285
+ try {
286
+ await controller?._internals._mediaParserController._internals.checkForAbortAndPause();
287
+ } catch (err) {
288
+ onError(err);
289
+ return;
290
+ }
291
+ mostRecentSampleReceived = sample.timestamp;
292
+ const encodedChunk = sample instanceof EncodedVideoChunk ? sample : new EncodedVideoChunk(sample);
293
+ videoDecoder.decode(encodedChunk);
294
+ ioSynchronizer.inputItem(sample.timestamp);
295
+ };
296
+ let flushPending = null;
297
+ let lastReset = null;
298
+ return {
299
+ decode,
300
+ close,
301
+ flush: () => {
302
+ if (flushPending) {
303
+ throw new Error("Flush already pending");
304
+ }
305
+ const pendingFlush = makeFlushPending();
306
+ flushPending = pendingFlush;
307
+ Promise.resolve().then(() => {
308
+ return videoDecoder.flush();
309
+ }).catch(() => {}).finally(() => {
310
+ pendingFlush.resolve();
311
+ flushPending = null;
312
+ });
313
+ return pendingFlush.promise;
314
+ },
315
+ waitForQueueToBeLessThan: ioSynchronizer.waitForQueueSize,
316
+ reset: () => {
317
+ lastReset = Date.now();
318
+ flushPending?.resolve();
319
+ ioSynchronizer.clearQueue();
320
+ videoDecoder.reset();
321
+ videoDecoder.configure(config);
322
+ },
323
+ checkReset: () => {
324
+ const initTime = Date.now();
325
+ return {
326
+ wasReset: () => lastReset !== null && lastReset > initTime
327
+ };
328
+ },
329
+ getMostRecentSampleInput() {
330
+ return mostRecentSampleReceived;
331
+ }
332
+ };
333
+ };
334
+ var createVideoDecoder = ({
335
+ onFrame,
336
+ onError,
337
+ controller,
338
+ track,
339
+ logLevel
340
+ }) => {
341
+ return internalCreateVideoDecoder({
342
+ onFrame,
343
+ onError,
344
+ controller: controller ?? null,
345
+ config: track,
346
+ logLevel: logLevel ?? "info"
347
+ });
348
+ };
349
+
350
+ // src/internal-extract-frames.ts
351
+ var internalExtractFrames = ({
352
+ src,
353
+ onFrame,
354
+ signal,
355
+ timestampsInSeconds,
356
+ acknowledgeRemotionLicense,
357
+ logLevel,
358
+ parseMediaImplementation
359
+ }) => {
360
+ const controller = mediaParserController();
361
+ const expectedFrames = [];
362
+ const resolvers = withResolvers();
363
+ const abortListener = () => {
364
+ controller.abort();
365
+ resolvers.reject(new MediaParserAbortError("Aborted by user"));
366
+ };
367
+ signal?.addEventListener("abort", abortListener, { once: true });
368
+ let dur = null;
369
+ let lastFrame;
370
+ let lastFrameEmitted;
371
+ parseMediaImplementation({
372
+ src: new URL(src, window.location.href),
373
+ acknowledgeRemotionLicense,
374
+ controller,
375
+ logLevel,
376
+ onDurationInSeconds(durationInSeconds) {
377
+ dur = durationInSeconds;
378
+ },
379
+ onVideoTrack: async ({ track, container }) => {
380
+ const timestampTargetsUnsorted = typeof timestampsInSeconds === "function" ? await timestampsInSeconds({
381
+ track,
382
+ container,
383
+ durationInSeconds: dur
384
+ }) : timestampsInSeconds;
385
+ const timestampTargets = timestampTargetsUnsorted.sort((a, b) => a - b);
386
+ if (timestampTargets.length === 0) {
387
+ throw new Error("expected at least one timestamp to extract but found zero");
388
+ }
389
+ controller.seek(timestampTargets[0]);
390
+ const decoder = createVideoDecoder({
391
+ onFrame: (frame) => {
392
+ Log.trace(logLevel, "Received frame with timestamp", frame.timestamp);
393
+ if (expectedFrames.length === 0) {
394
+ frame.close();
395
+ return;
396
+ }
397
+ if (frame.timestamp < expectedFrames[0] - 1) {
398
+ if (lastFrame) {
399
+ lastFrame.close();
400
+ }
401
+ lastFrame = frame;
402
+ return;
403
+ }
404
+ if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame) {
405
+ onFrame(lastFrame);
406
+ lastFrameEmitted = lastFrame;
407
+ expectedFrames.shift();
408
+ if (lastFrame) {
409
+ lastFrame.close();
410
+ }
411
+ lastFrame = frame;
412
+ return;
413
+ }
414
+ expectedFrames.shift();
415
+ onFrame(frame);
416
+ if (lastFrame && lastFrame !== lastFrameEmitted) {
417
+ lastFrame.close();
418
+ }
419
+ lastFrameEmitted = frame;
420
+ lastFrame = frame;
421
+ },
422
+ onError: (e) => {
423
+ controller.abort();
424
+ try {
425
+ decoder.close();
426
+ } catch {}
427
+ resolvers.reject(e);
428
+ },
429
+ track
430
+ });
431
+ const queued = [];
432
+ const doProcess = async () => {
433
+ expectedFrames.push(timestampTargets.shift() * WEBCODECS_TIMESCALE);
434
+ while (queued.length > 0) {
435
+ const sam = queued.shift();
436
+ if (!sam) {
437
+ throw new Error("Sample is undefined");
438
+ }
439
+ await decoder.waitForQueueToBeLessThan(20);
440
+ Log.trace(logLevel, "Decoding sample", sam.timestamp);
441
+ await decoder.decode(sam);
442
+ }
443
+ };
444
+ return async (sample) => {
445
+ const nextTimestampWeWant = timestampTargets[0];
446
+ Log.trace(logLevel, `Received ${sample.type} sample with dts`, sample.decodingTimestamp, "and cts", sample.timestamp);
447
+ if (sample.type === "key") {
448
+ await decoder.flush();
449
+ queued.length = 0;
450
+ }
451
+ queued.push(sample);
452
+ if (sample.decodingTimestamp >= timestampTargets[timestampTargets.length - 1] * WEBCODECS_TIMESCALE) {
453
+ await doProcess();
454
+ await decoder.flush();
455
+ controller.abort();
456
+ return;
457
+ }
458
+ if (nextTimestampWeWant === undefined) {
459
+ throw new Error("this should not happen");
460
+ }
461
+ if (sample.decodingTimestamp >= nextTimestampWeWant * WEBCODECS_TIMESCALE) {
462
+ await doProcess();
463
+ if (timestampTargets.length === 0) {
464
+ await decoder.flush();
465
+ controller.abort();
466
+ }
467
+ }
468
+ return async () => {
469
+ await doProcess();
470
+ await decoder.flush();
471
+ if (lastFrame && lastFrameEmitted !== lastFrame) {
472
+ lastFrame.close();
473
+ }
474
+ };
475
+ };
476
+ }
477
+ }).then(() => {
478
+ resolvers.resolve();
479
+ }).catch((e) => {
480
+ if (!hasBeenAborted(e)) {
481
+ resolvers.reject(e);
482
+ } else {
483
+ resolvers.resolve();
484
+ }
485
+ }).finally(() => {
486
+ if (lastFrame && lastFrameEmitted !== lastFrame) {
487
+ lastFrame.close();
488
+ }
489
+ signal?.removeEventListener("abort", abortListener);
490
+ });
491
+ return resolvers.promise;
492
+ };
493
+
494
+ // src/extract-frames-on-web-worker.ts
495
+ var extractFramesOnWebWorker = (options) => {
496
+ return internalExtractFrames({
497
+ ...options,
498
+ signal: options.signal ?? null,
499
+ acknowledgeRemotionLicense: options.acknowledgeRemotionLicense ?? false,
500
+ logLevel: options.logLevel ?? "info",
501
+ parseMediaImplementation: parseMediaOnWebWorker
502
+ });
503
+ };
504
+ export {
505
+ extractFramesOnWebWorker
506
+ };
@@ -0,0 +1,12 @@
1
+ import { type MediaParserLogLevel } from '@remotion/media-parser';
2
+ import { type ExtractFramesTimestampsInSecondsFn } from './internal-extract-frames';
3
+ export type ExtractFramesOnWebWorkerProps = {
4
+ src: string;
5
+ timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
6
+ onFrame: (frame: VideoFrame) => void;
7
+ signal?: AbortSignal;
8
+ acknowledgeRemotionLicense?: boolean;
9
+ logLevel?: MediaParserLogLevel;
10
+ };
11
+ export type ExtractFramesOnWebWorker = (options: ExtractFramesOnWebWorkerProps) => Promise<void>;
12
+ export declare const extractFramesOnWebWorker: ExtractFramesOnWebWorker;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractFramesOnWebWorker = void 0;
4
+ const worker_1 = require("@remotion/media-parser/worker");
5
+ const internal_extract_frames_1 = require("./internal-extract-frames");
6
+ const extractFramesOnWebWorker = (options) => {
7
+ return (0, internal_extract_frames_1.internalExtractFrames)({
8
+ ...options,
9
+ signal: options.signal ?? null,
10
+ acknowledgeRemotionLicense: options.acknowledgeRemotionLicense ?? false,
11
+ logLevel: options.logLevel ?? 'info',
12
+ parseMediaImplementation: worker_1.parseMediaOnWebWorker,
13
+ });
14
+ };
15
+ exports.extractFramesOnWebWorker = extractFramesOnWebWorker;
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -1,14 +1,12 @@
1
- import type { MediaParserContainer, MediaParserLogLevel, MediaParserVideoTrack } from '@remotion/media-parser';
2
- export type ExtractFramesTimestampsInSecondsFn = (options: {
3
- track: MediaParserVideoTrack;
4
- container: MediaParserContainer;
5
- durationInSeconds: number | null;
6
- }) => Promise<number[]> | number[];
7
- export declare const extractFrames: (options: {
1
+ import { type MediaParserLogLevel } from '@remotion/media-parser';
2
+ import { type ExtractFramesTimestampsInSecondsFn } from './internal-extract-frames';
3
+ export type ExtractFramesProps = {
8
4
  src: string;
9
5
  timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
10
6
  onFrame: (frame: VideoFrame) => void;
11
7
  signal?: AbortSignal;
12
8
  acknowledgeRemotionLicense?: boolean;
13
9
  logLevel?: MediaParserLogLevel;
14
- }) => Promise<void>;
10
+ };
11
+ export type ExtractFrames = (options: ExtractFramesProps) => Promise<void>;
12
+ export declare const extractFrames: ExtractFrames;
@@ -2,150 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractFrames = void 0;
4
4
  const media_parser_1 = require("@remotion/media-parser");
5
- const worker_1 = require("@remotion/media-parser/worker");
6
- const create_video_decoder_1 = require("./create-video-decoder");
7
- const with_resolvers_1 = require("./create/with-resolvers");
8
- const log_1 = require("./log");
9
- const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, acknowledgeRemotionLicense, logLevel, }) => {
10
- const controller = (0, media_parser_1.mediaParserController)();
11
- const expectedFrames = [];
12
- const resolvers = (0, with_resolvers_1.withResolvers)();
13
- const abortListener = () => {
14
- controller.abort();
15
- resolvers.reject(new media_parser_1.MediaParserAbortError('Aborted by user'));
16
- };
17
- signal?.addEventListener('abort', abortListener, { once: true });
18
- let dur = null;
19
- (0, worker_1.parseMediaOnWebWorker)({
20
- src: new URL(src, window.location.href),
21
- acknowledgeRemotionLicense,
22
- controller,
23
- logLevel,
24
- onDurationInSeconds(durationInSeconds) {
25
- dur = durationInSeconds;
26
- },
27
- onVideoTrack: async ({ track, container }) => {
28
- const timestampTargetsUnsorted = typeof timestampsInSeconds === 'function'
29
- ? await timestampsInSeconds({
30
- track,
31
- container,
32
- durationInSeconds: dur,
33
- })
34
- : timestampsInSeconds;
35
- const timestampTargets = timestampTargetsUnsorted.sort((a, b) => a - b);
36
- if (timestampTargets.length === 0) {
37
- throw new Error('expected at least one timestamp to extract but found zero');
38
- }
39
- controller.seek(timestampTargets[0]);
40
- let lastFrame;
41
- const decoder = (0, create_video_decoder_1.createVideoDecoder)({
42
- onFrame: (frame) => {
43
- log_1.Log.trace(logLevel, 'Received frame with timestamp', frame.timestamp);
44
- if (expectedFrames.length === 0) {
45
- frame.close();
46
- return;
47
- }
48
- if (frame.timestamp < expectedFrames[0] - 1) {
49
- if (lastFrame) {
50
- lastFrame.close();
51
- }
52
- lastFrame = frame;
53
- return;
54
- }
55
- // A WebM might have a timestamp of 67000 but we request 66666
56
- // See a test with this problem in it-tests/rendering/frame-accuracy.test.ts
57
- // Solution: We allow a 10.000ms - 3.333ms = 6.667ms difference between the requested timestamp and the actual timestamp
58
- if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame) {
59
- onFrame(lastFrame);
60
- expectedFrames.shift();
61
- lastFrame = frame;
62
- return;
63
- }
64
- expectedFrames.shift();
65
- onFrame(frame);
66
- lastFrame = frame;
67
- },
68
- onError: (e) => {
69
- controller.abort();
70
- try {
71
- decoder.close();
72
- }
73
- catch { }
74
- resolvers.reject(e);
75
- },
76
- track,
77
- });
78
- const queued = [];
79
- const doProcess = async () => {
80
- expectedFrames.push(timestampTargets.shift() * media_parser_1.WEBCODECS_TIMESCALE);
81
- while (queued.length > 0) {
82
- const sam = queued.shift();
83
- if (!sam) {
84
- throw new Error('Sample is undefined');
85
- }
86
- await decoder.waitForQueueToBeLessThan(10);
87
- log_1.Log.trace(logLevel, 'Decoding sample', sam.timestamp);
88
- await decoder.decode(sam);
89
- }
90
- };
91
- return async (sample) => {
92
- const nextTimestampWeWant = timestampTargets[0];
93
- log_1.Log.trace(logLevel, 'Received sample with dts', sample.decodingTimestamp, 'and cts', sample.timestamp);
94
- if (sample.type === 'key') {
95
- await decoder.flush();
96
- queued.length = 0;
97
- }
98
- queued.push(sample);
99
- if (sample.decodingTimestamp >=
100
- timestampTargets[timestampTargets.length - 1] * media_parser_1.WEBCODECS_TIMESCALE) {
101
- await doProcess();
102
- await decoder.flush();
103
- controller.abort();
104
- return;
105
- }
106
- if (nextTimestampWeWant === undefined) {
107
- throw new Error('this should not happen');
108
- }
109
- if (sample.decodingTimestamp >=
110
- nextTimestampWeWant * media_parser_1.WEBCODECS_TIMESCALE) {
111
- await doProcess();
112
- if (timestampTargets.length === 0) {
113
- await decoder.flush();
114
- controller.abort();
115
- }
116
- }
117
- return async () => {
118
- await doProcess();
119
- await decoder.flush();
120
- if (lastFrame) {
121
- lastFrame.close();
122
- }
123
- };
124
- };
125
- },
126
- })
127
- .then(() => {
128
- resolvers.resolve();
129
- })
130
- .catch((e) => {
131
- if (!(0, media_parser_1.hasBeenAborted)(e)) {
132
- resolvers.reject(e);
133
- }
134
- else {
135
- resolvers.resolve();
136
- }
137
- })
138
- .finally(() => {
139
- signal?.removeEventListener('abort', abortListener);
140
- });
141
- return resolvers.promise;
142
- };
5
+ const internal_extract_frames_1 = require("./internal-extract-frames");
143
6
  const extractFrames = (options) => {
144
- return internalExtractFrames({
7
+ return (0, internal_extract_frames_1.internalExtractFrames)({
145
8
  ...options,
146
9
  signal: options.signal ?? null,
147
10
  acknowledgeRemotionLicense: options.acknowledgeRemotionLicense ?? false,
148
11
  logLevel: options.logLevel ?? 'info',
12
+ parseMediaImplementation: media_parser_1.parseMedia,
149
13
  });
150
14
  };
151
15
  exports.extractFrames = extractFrames;
package/dist/index.d.ts CHANGED
@@ -13,8 +13,7 @@ export { createVideoDecoder } from './create-video-decoder';
13
13
  export type { WebCodecsVideoDecoder } from './create-video-decoder';
14
14
  export { defaultOnAudioTrackHandler } from './default-on-audio-track-handler';
15
15
  export { defaultOnVideoTrackHandler } from './default-on-video-track-handler';
16
- export { extractFrames } from './extract-frames';
17
- export type { ExtractFramesTimestampsInSecondsFn } from './extract-frames';
16
+ export { extractFrames, ExtractFrames, ExtractFramesProps, } from './extract-frames';
18
17
  export { getAvailableAudioCodecs } from './get-available-audio-codecs';
19
18
  export type { ConvertMediaAudioCodec } from './get-available-audio-codecs';
20
19
  export { getAvailableContainers } from './get-available-containers';
@@ -24,6 +23,7 @@ export type { ConvertMediaVideoCodec } from './get-available-video-codecs';
24
23
  export { getDefaultAudioCodec } from './get-default-audio-codec';
25
24
  export { getDefaultVideoCodec } from './get-default-video-codec';
26
25
  export { getPartialAudioData, GetPartialAudioDataProps, } from './get-partial-audio-data';
26
+ export type { ExtractFramesTimestampsInSecondsFn } from './internal-extract-frames';
27
27
  export type { AudioOperation, ConvertMediaOnAudioTrackHandler, } from './on-audio-track-handler';
28
28
  export type { ConvertMediaOnVideoTrackHandler, VideoOperation, } from './on-video-track-handler';
29
29
  export type { ResizeOperation } from './resizing/mode';
@@ -0,0 +1,16 @@
1
+ import type { MediaParserContainer, MediaParserLogLevel, MediaParserVideoTrack, ParseMedia } from '@remotion/media-parser';
2
+ import type { ParseMediaOnWorker } from '@remotion/media-parser/worker';
3
+ export type ExtractFramesTimestampsInSecondsFn = (options: {
4
+ track: MediaParserVideoTrack;
5
+ container: MediaParserContainer;
6
+ durationInSeconds: number | null;
7
+ }) => Promise<number[]> | number[];
8
+ export declare const internalExtractFrames: ({ src, onFrame, signal, timestampsInSeconds, acknowledgeRemotionLicense, logLevel, parseMediaImplementation, }: {
9
+ timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
10
+ src: string;
11
+ onFrame: (frame: VideoFrame) => void;
12
+ signal: AbortSignal | null;
13
+ acknowledgeRemotionLicense: boolean;
14
+ logLevel: MediaParserLogLevel;
15
+ parseMediaImplementation: ParseMediaOnWorker | ParseMedia;
16
+ }) => Promise<void>;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.internalExtractFrames = void 0;
4
+ const media_parser_1 = require("@remotion/media-parser");
5
+ const create_video_decoder_1 = require("./create-video-decoder");
6
+ const with_resolvers_1 = require("./create/with-resolvers");
7
+ const log_1 = require("./log");
8
+ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, acknowledgeRemotionLicense, logLevel, parseMediaImplementation, }) => {
9
+ const controller = (0, media_parser_1.mediaParserController)();
10
+ const expectedFrames = [];
11
+ const resolvers = (0, with_resolvers_1.withResolvers)();
12
+ const abortListener = () => {
13
+ controller.abort();
14
+ resolvers.reject(new media_parser_1.MediaParserAbortError('Aborted by user'));
15
+ };
16
+ signal?.addEventListener('abort', abortListener, { once: true });
17
+ let dur = null;
18
+ let lastFrame;
19
+ let lastFrameEmitted;
20
+ parseMediaImplementation({
21
+ src: new URL(src, window.location.href),
22
+ acknowledgeRemotionLicense,
23
+ controller,
24
+ logLevel,
25
+ onDurationInSeconds(durationInSeconds) {
26
+ dur = durationInSeconds;
27
+ },
28
+ onVideoTrack: async ({ track, container }) => {
29
+ const timestampTargetsUnsorted = typeof timestampsInSeconds === 'function'
30
+ ? await timestampsInSeconds({
31
+ track,
32
+ container,
33
+ durationInSeconds: dur,
34
+ })
35
+ : timestampsInSeconds;
36
+ const timestampTargets = timestampTargetsUnsorted.sort((a, b) => a - b);
37
+ if (timestampTargets.length === 0) {
38
+ throw new Error('expected at least one timestamp to extract but found zero');
39
+ }
40
+ controller.seek(timestampTargets[0]);
41
+ const decoder = (0, create_video_decoder_1.createVideoDecoder)({
42
+ onFrame: (frame) => {
43
+ log_1.Log.trace(logLevel, 'Received frame with timestamp', frame.timestamp);
44
+ if (expectedFrames.length === 0) {
45
+ frame.close();
46
+ return;
47
+ }
48
+ if (frame.timestamp < expectedFrames[0] - 1) {
49
+ if (lastFrame) {
50
+ lastFrame.close();
51
+ }
52
+ lastFrame = frame;
53
+ return;
54
+ }
55
+ // A WebM might have a timestamp of 67000 but we request 66666
56
+ // See a test with this problem in it-tests/rendering/frame-accuracy.test.ts
57
+ // Solution: We allow a 10.000ms - 3.333ms = 6.667ms difference between the requested timestamp and the actual timestamp
58
+ if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame) {
59
+ onFrame(lastFrame);
60
+ lastFrameEmitted = lastFrame;
61
+ expectedFrames.shift();
62
+ if (lastFrame) {
63
+ lastFrame.close();
64
+ }
65
+ lastFrame = frame;
66
+ return;
67
+ }
68
+ expectedFrames.shift();
69
+ onFrame(frame);
70
+ if (lastFrame && lastFrame !== lastFrameEmitted) {
71
+ lastFrame.close();
72
+ }
73
+ lastFrameEmitted = frame;
74
+ lastFrame = frame;
75
+ },
76
+ onError: (e) => {
77
+ controller.abort();
78
+ try {
79
+ decoder.close();
80
+ }
81
+ catch {
82
+ // Ignore
83
+ }
84
+ resolvers.reject(e);
85
+ },
86
+ track,
87
+ });
88
+ const queued = [];
89
+ const doProcess = async () => {
90
+ expectedFrames.push(timestampTargets.shift() * media_parser_1.WEBCODECS_TIMESCALE);
91
+ while (queued.length > 0) {
92
+ const sam = queued.shift();
93
+ if (!sam) {
94
+ throw new Error('Sample is undefined');
95
+ }
96
+ await decoder.waitForQueueToBeLessThan(20);
97
+ log_1.Log.trace(logLevel, 'Decoding sample', sam.timestamp);
98
+ await decoder.decode(sam);
99
+ }
100
+ };
101
+ return async (sample) => {
102
+ const nextTimestampWeWant = timestampTargets[0];
103
+ log_1.Log.trace(logLevel, `Received ${sample.type} sample with dts`, sample.decodingTimestamp, 'and cts', sample.timestamp);
104
+ if (sample.type === 'key') {
105
+ await decoder.flush();
106
+ queued.length = 0;
107
+ }
108
+ queued.push(sample);
109
+ if (sample.decodingTimestamp >=
110
+ timestampTargets[timestampTargets.length - 1] * media_parser_1.WEBCODECS_TIMESCALE) {
111
+ await doProcess();
112
+ await decoder.flush();
113
+ controller.abort();
114
+ return;
115
+ }
116
+ if (nextTimestampWeWant === undefined) {
117
+ throw new Error('this should not happen');
118
+ }
119
+ if (sample.decodingTimestamp >=
120
+ nextTimestampWeWant * media_parser_1.WEBCODECS_TIMESCALE) {
121
+ await doProcess();
122
+ if (timestampTargets.length === 0) {
123
+ await decoder.flush();
124
+ controller.abort();
125
+ }
126
+ }
127
+ return async () => {
128
+ await doProcess();
129
+ await decoder.flush();
130
+ if (lastFrame && lastFrameEmitted !== lastFrame) {
131
+ lastFrame.close();
132
+ }
133
+ };
134
+ };
135
+ },
136
+ })
137
+ .then(() => {
138
+ resolvers.resolve();
139
+ })
140
+ .catch((e) => {
141
+ if (!(0, media_parser_1.hasBeenAborted)(e)) {
142
+ resolvers.reject(e);
143
+ }
144
+ else {
145
+ resolvers.resolve();
146
+ }
147
+ })
148
+ .finally(() => {
149
+ if (lastFrame && lastFrameEmitted !== lastFrame) {
150
+ lastFrame.close();
151
+ }
152
+ signal?.removeEventListener('abort', abortListener);
153
+ });
154
+ return resolvers.promise;
155
+ };
156
+ exports.internalExtractFrames = internalExtractFrames;
@@ -0,0 +1 @@
1
+ export { ExtractFramesOnWebWorker, ExtractFramesOnWebWorkerProps, extractFramesOnWebWorker, } from './extract-frames-on-web-worker';
package/dist/worker.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractFramesOnWebWorker = void 0;
4
+ var extract_frames_on_web_worker_1 = require("./extract-frames-on-web-worker");
5
+ Object.defineProperty(exports, "extractFramesOnWebWorker", { enumerable: true, get: function () { return extract_frames_on_web_worker_1.extractFramesOnWebWorker; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/webcodecs",
3
- "version": "4.0.329",
3
+ "version": "4.0.331",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -19,8 +19,8 @@
19
19
  "author": "Jonny Burger <jonny@remotion.dev>",
20
20
  "license": "Remotion License (See https://remotion.dev/docs/webcodecs#license)",
21
21
  "dependencies": {
22
- "@remotion/licensing": "4.0.329",
23
- "@remotion/media-parser": "4.0.329"
22
+ "@remotion/media-parser": "4.0.331",
23
+ "@remotion/licensing": "4.0.331"
24
24
  },
25
25
  "peerDependencies": {},
26
26
  "devDependencies": {
@@ -29,8 +29,8 @@
29
29
  "vite": "5.4.19",
30
30
  "@playwright/test": "1.51.1",
31
31
  "eslint": "9.19.0",
32
- "@remotion/example-videos": "4.0.329",
33
- "@remotion/eslint-config-internal": "4.0.329"
32
+ "@remotion/example-videos": "4.0.331",
33
+ "@remotion/eslint-config-internal": "4.0.331"
34
34
  },
35
35
  "keywords": [],
36
36
  "publishConfig": {
@@ -55,6 +55,12 @@
55
55
  "module": "./dist/esm/buffer.mjs",
56
56
  "import": "./dist/esm/buffer.mjs"
57
57
  },
58
+ "./worker": {
59
+ "types": "./dist/worker.d.ts",
60
+ "require": "./dist/worker.js",
61
+ "module": "./dist/esm/worker.mjs",
62
+ "import": "./dist/esm/worker.mjs"
63
+ },
58
64
  "./package.json": "./package.json"
59
65
  },
60
66
  "typesVersions": {
@@ -62,6 +68,9 @@
62
68
  "web-fs": [
63
69
  "dist/writers/web-fs.d.ts"
64
70
  ],
71
+ "worker": [
72
+ "dist/worker.d.ts"
73
+ ],
65
74
  "buffer": [
66
75
  "dist/writers/buffer.d.ts"
67
76
  ]