@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.
- package/dist/esm/index.mjs +31 -13
- package/dist/esm/worker.mjs +506 -0
- package/dist/extract-frames-on-web-worker.d.ts +12 -0
- package/dist/extract-frames-on-web-worker.js +15 -0
- package/dist/extract-frames-on-worker.d.ts +0 -0
- package/dist/extract-frames-on-worker.js +1 -0
- package/dist/extract-frames.d.ts +6 -8
- package/dist/extract-frames.js +3 -139
- package/dist/index.d.ts +2 -2
- package/dist/internal-extract-frames.d.ts +16 -0
- package/dist/internal-extract-frames.js +156 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +5 -0
- package/package.json +14 -5
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
-
|
|
5600
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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";
|
package/dist/extract-frames.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
}
|
|
10
|
+
};
|
|
11
|
+
export type ExtractFrames = (options: ExtractFramesProps) => Promise<void>;
|
|
12
|
+
export declare const extractFrames: ExtractFrames;
|
package/dist/extract-frames.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/worker.d.ts
ADDED
|
@@ -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.
|
|
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/
|
|
23
|
-
"@remotion/
|
|
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.
|
|
33
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
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
|
]
|