@remotion/timeline-utils 4.0.460
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/esm/index.mjs +389 -0
- package/dist/extract-frames.d.ts +18 -0
- package/dist/extract-frames.js +85 -0
- package/dist/frame-database.d.ts +16 -0
- package/dist/frame-database.js +64 -0
- package/dist/get-duration-or-compute.d.ts +2 -0
- package/dist/get-duration-or-compute.js +10 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/dist/loop-display.d.ts +10 -0
- package/dist/loop-display.js +14 -0
- package/dist/render-frame-strip.d.ts +53 -0
- package/dist/render-frame-strip.js +136 -0
- package/dist/resize-video-frame.d.ts +4 -0
- package/dist/resize-video-frame.js +38 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// src/extract-frames.ts
|
|
2
|
+
import {
|
|
3
|
+
ALL_FORMATS,
|
|
4
|
+
Input,
|
|
5
|
+
InputDisposedError,
|
|
6
|
+
UrlSource,
|
|
7
|
+
VideoSampleSink
|
|
8
|
+
} from "mediabunny";
|
|
9
|
+
|
|
10
|
+
// src/get-duration-or-compute.ts
|
|
11
|
+
var getDurationOrCompute = async (input) => {
|
|
12
|
+
return await input.getDurationFromMetadata(undefined, {
|
|
13
|
+
skipLiveWait: true
|
|
14
|
+
}) ?? input.computeDuration(undefined, { skipLiveWait: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/extract-frames.ts
|
|
18
|
+
async function extractFrames({
|
|
19
|
+
src,
|
|
20
|
+
timestampsInSeconds,
|
|
21
|
+
onVideoSample,
|
|
22
|
+
signal
|
|
23
|
+
}) {
|
|
24
|
+
const input = new Input({
|
|
25
|
+
formats: ALL_FORMATS,
|
|
26
|
+
source: new UrlSource(src)
|
|
27
|
+
});
|
|
28
|
+
const dispose = () => {
|
|
29
|
+
input.dispose();
|
|
30
|
+
};
|
|
31
|
+
if (signal) {
|
|
32
|
+
signal.addEventListener("abort", dispose, { once: true });
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const [durationInSeconds, format, videoTrack] = await Promise.all([
|
|
36
|
+
getDurationOrCompute(input),
|
|
37
|
+
input.getFormat(),
|
|
38
|
+
input.getPrimaryVideoTrack()
|
|
39
|
+
]);
|
|
40
|
+
if (!videoTrack) {
|
|
41
|
+
throw new Error("No video track found in the input");
|
|
42
|
+
}
|
|
43
|
+
if (await videoTrack.isLive()) {
|
|
44
|
+
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + src);
|
|
45
|
+
}
|
|
46
|
+
if (await videoTrack.isRelativeToUnixEpoch()) {
|
|
47
|
+
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + src);
|
|
48
|
+
}
|
|
49
|
+
const timestamps = typeof timestampsInSeconds === "function" ? await timestampsInSeconds({
|
|
50
|
+
track: {
|
|
51
|
+
width: await videoTrack.getDisplayWidth(),
|
|
52
|
+
height: await videoTrack.getDisplayHeight()
|
|
53
|
+
},
|
|
54
|
+
container: format.name,
|
|
55
|
+
durationInSeconds
|
|
56
|
+
}) : timestampsInSeconds;
|
|
57
|
+
if (timestamps.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const sink = new VideoSampleSink(videoTrack);
|
|
61
|
+
const sampleIterator = sink.samplesAtTimestamps(timestamps);
|
|
62
|
+
try {
|
|
63
|
+
for await (const videoSample of sampleIterator) {
|
|
64
|
+
if (signal?.aborted) {
|
|
65
|
+
videoSample?.close();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (!videoSample) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
onVideoSample(videoSample);
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
try {
|
|
75
|
+
await sampleIterator.return?.();
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof InputDisposedError) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
dispose();
|
|
85
|
+
if (signal) {
|
|
86
|
+
signal.removeEventListener("abort", dispose);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// src/frame-database.ts
|
|
91
|
+
var KEY_SEPARATOR = "|";
|
|
92
|
+
var makeFrameDatabaseKey = (src, timestamp) => `${src}${KEY_SEPARATOR}${timestamp}`;
|
|
93
|
+
var getFrameDatabaseKeyPrefix = (src) => {
|
|
94
|
+
return `${src}${KEY_SEPARATOR}`;
|
|
95
|
+
};
|
|
96
|
+
var MAX_CACHE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
97
|
+
var totalCacheSize = 0;
|
|
98
|
+
var frameDatabase = new Map;
|
|
99
|
+
var aspectRatioCache = new Map;
|
|
100
|
+
var getTimestampFromFrameDatabaseKey = (key) => {
|
|
101
|
+
const split = key.split(KEY_SEPARATOR);
|
|
102
|
+
return Number(split[split.length - 1]);
|
|
103
|
+
};
|
|
104
|
+
var getAspectRatioFromCache = (src) => {
|
|
105
|
+
const cached = aspectRatioCache.get(src);
|
|
106
|
+
if (cached) {
|
|
107
|
+
return cached;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
var evictLRU = () => {
|
|
112
|
+
while (totalCacheSize > MAX_CACHE_SIZE_BYTES && frameDatabase.size > 0) {
|
|
113
|
+
let oldestKey;
|
|
114
|
+
let oldestTime = Infinity;
|
|
115
|
+
for (const [key, candidate] of frameDatabase) {
|
|
116
|
+
if (candidate.lastUsed < oldestTime) {
|
|
117
|
+
oldestTime = candidate.lastUsed;
|
|
118
|
+
oldestKey = key;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!oldestKey) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
const entry = frameDatabase.get(oldestKey);
|
|
125
|
+
if (entry) {
|
|
126
|
+
totalCacheSize -= entry.size;
|
|
127
|
+
entry.frame.close();
|
|
128
|
+
frameDatabase.delete(oldestKey);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var addFrameToCache = (key, frame) => {
|
|
133
|
+
const existing = frameDatabase.get(key);
|
|
134
|
+
if (existing) {
|
|
135
|
+
totalCacheSize -= existing.size;
|
|
136
|
+
existing.frame.close();
|
|
137
|
+
}
|
|
138
|
+
const size = frame.allocationSize();
|
|
139
|
+
totalCacheSize += size;
|
|
140
|
+
frameDatabase.set(key, {
|
|
141
|
+
frame,
|
|
142
|
+
lastUsed: Date.now(),
|
|
143
|
+
size
|
|
144
|
+
});
|
|
145
|
+
evictLRU();
|
|
146
|
+
};
|
|
147
|
+
// src/loop-display.ts
|
|
148
|
+
var shouldTileLoopDisplay = (loopDisplay) => {
|
|
149
|
+
return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
|
|
150
|
+
};
|
|
151
|
+
var getLoopDisplayWidth = ({
|
|
152
|
+
visualizationWidth,
|
|
153
|
+
loopDisplay
|
|
154
|
+
}) => {
|
|
155
|
+
if (!shouldTileLoopDisplay(loopDisplay)) {
|
|
156
|
+
return visualizationWidth;
|
|
157
|
+
}
|
|
158
|
+
return visualizationWidth / loopDisplay.numberOfTimes;
|
|
159
|
+
};
|
|
160
|
+
// src/render-frame-strip.ts
|
|
161
|
+
var WEBCODECS_TIMESCALE = 1e6;
|
|
162
|
+
var MAX_TIME_DEVIATION = WEBCODECS_TIMESCALE * 0.05;
|
|
163
|
+
var getDurationOfOneFrame = ({
|
|
164
|
+
visualizationWidth,
|
|
165
|
+
aspectRatio,
|
|
166
|
+
segmentDuration,
|
|
167
|
+
frameHeight
|
|
168
|
+
}) => {
|
|
169
|
+
const framesFitInWidthUnrounded = visualizationWidth / (frameHeight * aspectRatio);
|
|
170
|
+
return segmentDuration / framesFitInWidthUnrounded * WEBCODECS_TIMESCALE;
|
|
171
|
+
};
|
|
172
|
+
var fixRounding = (value) => {
|
|
173
|
+
if (value % 1 >= 0.49999999) {
|
|
174
|
+
return Math.ceil(value);
|
|
175
|
+
}
|
|
176
|
+
return Math.floor(value);
|
|
177
|
+
};
|
|
178
|
+
var calculateTimestampSlots = ({
|
|
179
|
+
visualizationWidth,
|
|
180
|
+
fromSeconds,
|
|
181
|
+
segmentDuration,
|
|
182
|
+
aspectRatio,
|
|
183
|
+
frameHeight
|
|
184
|
+
}) => {
|
|
185
|
+
const framesFitInWidthUnrounded = visualizationWidth / (frameHeight * aspectRatio);
|
|
186
|
+
const framesFitInWidth = Math.ceil(framesFitInWidthUnrounded);
|
|
187
|
+
const durationOfOneFrame = getDurationOfOneFrame({
|
|
188
|
+
visualizationWidth,
|
|
189
|
+
aspectRatio,
|
|
190
|
+
segmentDuration,
|
|
191
|
+
frameHeight
|
|
192
|
+
});
|
|
193
|
+
const timestampTargets = [];
|
|
194
|
+
for (let i = 0;i < framesFitInWidth + 1; i++) {
|
|
195
|
+
const target = fromSeconds * WEBCODECS_TIMESCALE + durationOfOneFrame * (i + 0.5);
|
|
196
|
+
const snappedToDuration = (Math.round(fixRounding(target / durationOfOneFrame)) - 1) * durationOfOneFrame;
|
|
197
|
+
timestampTargets.push(snappedToDuration);
|
|
198
|
+
}
|
|
199
|
+
return timestampTargets;
|
|
200
|
+
};
|
|
201
|
+
var ensureSlots = ({
|
|
202
|
+
filledSlots,
|
|
203
|
+
naturalWidth,
|
|
204
|
+
fromSeconds,
|
|
205
|
+
toSeconds,
|
|
206
|
+
aspectRatio,
|
|
207
|
+
frameHeight
|
|
208
|
+
}) => {
|
|
209
|
+
const segmentDuration = toSeconds - fromSeconds;
|
|
210
|
+
const timestampTargets = calculateTimestampSlots({
|
|
211
|
+
visualizationWidth: naturalWidth,
|
|
212
|
+
fromSeconds,
|
|
213
|
+
segmentDuration,
|
|
214
|
+
aspectRatio,
|
|
215
|
+
frameHeight
|
|
216
|
+
});
|
|
217
|
+
for (const timestamp of timestampTargets) {
|
|
218
|
+
if (!filledSlots.has(timestamp)) {
|
|
219
|
+
filledSlots.set(timestamp, undefined);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var drawSlot = ({
|
|
224
|
+
frame,
|
|
225
|
+
ctx,
|
|
226
|
+
filledSlots,
|
|
227
|
+
visualizationWidth,
|
|
228
|
+
timestamp,
|
|
229
|
+
segmentDuration,
|
|
230
|
+
fromSeconds,
|
|
231
|
+
devicePixelRatio,
|
|
232
|
+
frameHeight
|
|
233
|
+
}) => {
|
|
234
|
+
const durationOfOneFrame = getDurationOfOneFrame({
|
|
235
|
+
visualizationWidth,
|
|
236
|
+
aspectRatio: frame.displayWidth / frame.displayHeight,
|
|
237
|
+
segmentDuration,
|
|
238
|
+
frameHeight
|
|
239
|
+
});
|
|
240
|
+
const relativeTimestamp = timestamp - fromSeconds * WEBCODECS_TIMESCALE;
|
|
241
|
+
const frameIndex = relativeTimestamp / durationOfOneFrame;
|
|
242
|
+
const thumbnailWidth = frame.displayWidth / devicePixelRatio;
|
|
243
|
+
const left = Math.floor(frameIndex * thumbnailWidth);
|
|
244
|
+
const right = Math.ceil((frameIndex + 1) * thumbnailWidth);
|
|
245
|
+
ctx.drawImage(frame, left, 0, right - left, frame.displayHeight / devicePixelRatio);
|
|
246
|
+
filledSlots.set(timestamp, frame.timestamp);
|
|
247
|
+
};
|
|
248
|
+
var fillWithCachedFrames = ({
|
|
249
|
+
ctx,
|
|
250
|
+
naturalWidth,
|
|
251
|
+
filledSlots,
|
|
252
|
+
src,
|
|
253
|
+
segmentDuration,
|
|
254
|
+
fromSeconds,
|
|
255
|
+
devicePixelRatio,
|
|
256
|
+
frameHeight
|
|
257
|
+
}) => {
|
|
258
|
+
const prefix = getFrameDatabaseKeyPrefix(src);
|
|
259
|
+
const keys = Array.from(frameDatabase.keys()).filter((k) => k.startsWith(prefix));
|
|
260
|
+
const targets = Array.from(filledSlots.keys());
|
|
261
|
+
for (const timestamp of targets) {
|
|
262
|
+
let bestKey;
|
|
263
|
+
let bestDistance = Infinity;
|
|
264
|
+
for (const key of keys) {
|
|
265
|
+
const distance = Math.abs(getTimestampFromFrameDatabaseKey(key) - timestamp);
|
|
266
|
+
if (distance < bestDistance) {
|
|
267
|
+
bestDistance = distance;
|
|
268
|
+
bestKey = key;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!bestKey) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const frame = frameDatabase.get(bestKey);
|
|
275
|
+
if (!frame) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const alreadyFilled = filledSlots.get(timestamp);
|
|
279
|
+
if (alreadyFilled && Math.abs(alreadyFilled - timestamp) <= Math.abs(frame.frame.timestamp - timestamp)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
frame.lastUsed = Date.now();
|
|
283
|
+
drawSlot({
|
|
284
|
+
ctx,
|
|
285
|
+
frame: frame.frame,
|
|
286
|
+
filledSlots,
|
|
287
|
+
visualizationWidth: naturalWidth,
|
|
288
|
+
timestamp,
|
|
289
|
+
segmentDuration,
|
|
290
|
+
fromSeconds,
|
|
291
|
+
devicePixelRatio,
|
|
292
|
+
frameHeight
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
var fillFrameWhereItFits = ({
|
|
297
|
+
frame,
|
|
298
|
+
filledSlots,
|
|
299
|
+
ctx,
|
|
300
|
+
visualizationWidth,
|
|
301
|
+
segmentDuration,
|
|
302
|
+
fromSeconds,
|
|
303
|
+
devicePixelRatio,
|
|
304
|
+
frameHeight
|
|
305
|
+
}) => {
|
|
306
|
+
const slots = Array.from(filledSlots.keys());
|
|
307
|
+
for (let i = 0;i < slots.length; i++) {
|
|
308
|
+
const slot = slots[i];
|
|
309
|
+
if (Math.abs(slot - frame.timestamp) > MAX_TIME_DEVIATION) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const filled = filledSlots.get(slot);
|
|
313
|
+
if (filled && Math.abs(filled - slot) <= Math.abs(filled - frame.timestamp)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
drawSlot({
|
|
317
|
+
ctx,
|
|
318
|
+
frame,
|
|
319
|
+
filledSlots,
|
|
320
|
+
visualizationWidth,
|
|
321
|
+
timestamp: slot,
|
|
322
|
+
segmentDuration,
|
|
323
|
+
fromSeconds,
|
|
324
|
+
devicePixelRatio,
|
|
325
|
+
frameHeight
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
// src/resize-video-frame.ts
|
|
330
|
+
var calculateNewDimensionsFromScale = ({
|
|
331
|
+
width,
|
|
332
|
+
height,
|
|
333
|
+
scale
|
|
334
|
+
}) => {
|
|
335
|
+
const scaledWidth = Math.round(width * scale);
|
|
336
|
+
const scaledHeight = Math.round(height * scale);
|
|
337
|
+
return {
|
|
338
|
+
width: scaledWidth,
|
|
339
|
+
height: scaledHeight
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
var resizeVideoFrame = ({
|
|
343
|
+
frame,
|
|
344
|
+
scale
|
|
345
|
+
}) => {
|
|
346
|
+
if (scale === 1) {
|
|
347
|
+
return frame;
|
|
348
|
+
}
|
|
349
|
+
const { width, height } = calculateNewDimensionsFromScale({
|
|
350
|
+
height: frame.displayHeight,
|
|
351
|
+
width: frame.displayWidth,
|
|
352
|
+
scale
|
|
353
|
+
});
|
|
354
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
355
|
+
const ctx = canvas.getContext("2d");
|
|
356
|
+
if (!ctx) {
|
|
357
|
+
throw new Error("Could not get 2d context");
|
|
358
|
+
}
|
|
359
|
+
canvas.width = width;
|
|
360
|
+
canvas.height = height;
|
|
361
|
+
ctx.scale(scale, scale);
|
|
362
|
+
ctx.drawImage(frame, 0, 0);
|
|
363
|
+
return new VideoFrame(canvas, {
|
|
364
|
+
displayHeight: height,
|
|
365
|
+
displayWidth: width,
|
|
366
|
+
duration: frame.duration ?? undefined,
|
|
367
|
+
timestamp: frame.timestamp
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
export {
|
|
371
|
+
shouldTileLoopDisplay,
|
|
372
|
+
resizeVideoFrame,
|
|
373
|
+
makeFrameDatabaseKey,
|
|
374
|
+
getTimestampFromFrameDatabaseKey,
|
|
375
|
+
getLoopDisplayWidth,
|
|
376
|
+
getFrameDatabaseKeyPrefix,
|
|
377
|
+
getDurationOfOneFrame,
|
|
378
|
+
getAspectRatioFromCache,
|
|
379
|
+
frameDatabase,
|
|
380
|
+
fillWithCachedFrames,
|
|
381
|
+
fillFrameWhereItFits,
|
|
382
|
+
extractFrames,
|
|
383
|
+
ensureSlots,
|
|
384
|
+
drawSlot,
|
|
385
|
+
calculateTimestampSlots,
|
|
386
|
+
aspectRatioCache,
|
|
387
|
+
addFrameToCache,
|
|
388
|
+
WEBCODECS_TIMESCALE
|
|
389
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { VideoSample } from 'mediabunny';
|
|
2
|
+
type Options = {
|
|
3
|
+
track: {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
container: string;
|
|
8
|
+
durationInSeconds: number | null;
|
|
9
|
+
};
|
|
10
|
+
export type ExtractFramesTimestampsInSecondsFn = (options: Options) => Promise<number[]> | number[];
|
|
11
|
+
export type ExtractFramesProps = {
|
|
12
|
+
src: string;
|
|
13
|
+
timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
|
|
14
|
+
onVideoSample: (sample: VideoSample) => void;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
};
|
|
17
|
+
export declare function extractFrames({ src, timestampsInSeconds, onVideoSample, signal }: ExtractFramesProps): Promise<void>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractFrames = extractFrames;
|
|
4
|
+
const mediabunny_1 = require("mediabunny");
|
|
5
|
+
const get_duration_or_compute_1 = require("./get-duration-or-compute");
|
|
6
|
+
async function extractFrames({ src, timestampsInSeconds, onVideoSample, signal, }) {
|
|
7
|
+
var _a;
|
|
8
|
+
const input = new mediabunny_1.Input({
|
|
9
|
+
formats: mediabunny_1.ALL_FORMATS,
|
|
10
|
+
source: new mediabunny_1.UrlSource(src),
|
|
11
|
+
});
|
|
12
|
+
const dispose = () => {
|
|
13
|
+
input.dispose();
|
|
14
|
+
};
|
|
15
|
+
if (signal) {
|
|
16
|
+
signal.addEventListener('abort', dispose, { once: true });
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const [durationInSeconds, format, videoTrack] = await Promise.all([
|
|
20
|
+
(0, get_duration_or_compute_1.getDurationOrCompute)(input),
|
|
21
|
+
input.getFormat(),
|
|
22
|
+
input.getPrimaryVideoTrack(),
|
|
23
|
+
]);
|
|
24
|
+
if (!videoTrack) {
|
|
25
|
+
throw new Error('No video track found in the input');
|
|
26
|
+
}
|
|
27
|
+
if (await videoTrack.isLive()) {
|
|
28
|
+
throw new Error('Live streams are not currently supported by Remotion. Sorry! Source: ' +
|
|
29
|
+
src);
|
|
30
|
+
}
|
|
31
|
+
if (await videoTrack.isRelativeToUnixEpoch()) {
|
|
32
|
+
throw new Error('Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: ' +
|
|
33
|
+
src);
|
|
34
|
+
}
|
|
35
|
+
const timestamps = typeof timestampsInSeconds === 'function'
|
|
36
|
+
? await timestampsInSeconds({
|
|
37
|
+
track: {
|
|
38
|
+
width: await videoTrack.getDisplayWidth(),
|
|
39
|
+
height: await videoTrack.getDisplayHeight(),
|
|
40
|
+
},
|
|
41
|
+
container: format.name,
|
|
42
|
+
durationInSeconds,
|
|
43
|
+
})
|
|
44
|
+
: timestampsInSeconds;
|
|
45
|
+
if (timestamps.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const sink = new mediabunny_1.VideoSampleSink(videoTrack);
|
|
49
|
+
const sampleIterator = sink.samplesAtTimestamps(timestamps);
|
|
50
|
+
try {
|
|
51
|
+
for await (const videoSample of sampleIterator) {
|
|
52
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
53
|
+
videoSample === null || videoSample === void 0 ? void 0 : videoSample.close();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (!videoSample) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
onVideoSample(videoSample);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
// When input.dispose() causes the iterator to throw InputDisposedError,
|
|
64
|
+
// for-await does not call .return() on the iterator.
|
|
65
|
+
try {
|
|
66
|
+
await ((_a = sampleIterator.return) === null || _a === void 0 ? void 0 : _a.call(sampleIterator));
|
|
67
|
+
}
|
|
68
|
+
catch (_b) {
|
|
69
|
+
// Iterator already done or input disposed
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof mediabunny_1.InputDisposedError) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
dispose();
|
|
81
|
+
if (signal) {
|
|
82
|
+
signal.removeEventListener('abort', dispose);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type FrameDatabaseKey = string & {
|
|
2
|
+
__brand: 'FrameDatabaseKey';
|
|
3
|
+
};
|
|
4
|
+
export declare const makeFrameDatabaseKey: (src: string, timestamp: number) => FrameDatabaseKey;
|
|
5
|
+
export declare const getFrameDatabaseKeyPrefix: (src: string) => string;
|
|
6
|
+
type VideoFrameAndLastUsed = {
|
|
7
|
+
frame: VideoFrame;
|
|
8
|
+
lastUsed: number;
|
|
9
|
+
size: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const frameDatabase: Map<FrameDatabaseKey, VideoFrameAndLastUsed>;
|
|
12
|
+
export declare const aspectRatioCache: Map<string, number>;
|
|
13
|
+
export declare const getTimestampFromFrameDatabaseKey: (key: FrameDatabaseKey) => number;
|
|
14
|
+
export declare const getAspectRatioFromCache: (src: string) => number | null;
|
|
15
|
+
export declare const addFrameToCache: (key: FrameDatabaseKey, frame: VideoFrame) => void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addFrameToCache = exports.getAspectRatioFromCache = exports.getTimestampFromFrameDatabaseKey = exports.aspectRatioCache = exports.frameDatabase = exports.getFrameDatabaseKeyPrefix = exports.makeFrameDatabaseKey = void 0;
|
|
4
|
+
const KEY_SEPARATOR = '|';
|
|
5
|
+
const makeFrameDatabaseKey = (src, timestamp) => `${src}${KEY_SEPARATOR}${timestamp}`;
|
|
6
|
+
exports.makeFrameDatabaseKey = makeFrameDatabaseKey;
|
|
7
|
+
const getFrameDatabaseKeyPrefix = (src) => {
|
|
8
|
+
return `${src}${KEY_SEPARATOR}`;
|
|
9
|
+
};
|
|
10
|
+
exports.getFrameDatabaseKeyPrefix = getFrameDatabaseKeyPrefix;
|
|
11
|
+
const MAX_CACHE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
12
|
+
let totalCacheSize = 0;
|
|
13
|
+
exports.frameDatabase = new Map();
|
|
14
|
+
exports.aspectRatioCache = new Map();
|
|
15
|
+
const getTimestampFromFrameDatabaseKey = (key) => {
|
|
16
|
+
const split = key.split(KEY_SEPARATOR);
|
|
17
|
+
return Number(split[split.length - 1]);
|
|
18
|
+
};
|
|
19
|
+
exports.getTimestampFromFrameDatabaseKey = getTimestampFromFrameDatabaseKey;
|
|
20
|
+
const getAspectRatioFromCache = (src) => {
|
|
21
|
+
const cached = exports.aspectRatioCache.get(src);
|
|
22
|
+
if (cached) {
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
exports.getAspectRatioFromCache = getAspectRatioFromCache;
|
|
28
|
+
const evictLRU = () => {
|
|
29
|
+
while (totalCacheSize > MAX_CACHE_SIZE_BYTES && exports.frameDatabase.size > 0) {
|
|
30
|
+
let oldestKey;
|
|
31
|
+
let oldestTime = Infinity;
|
|
32
|
+
for (const [key, candidate] of exports.frameDatabase) {
|
|
33
|
+
if (candidate.lastUsed < oldestTime) {
|
|
34
|
+
oldestTime = candidate.lastUsed;
|
|
35
|
+
oldestKey = key;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (!oldestKey) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
const entry = exports.frameDatabase.get(oldestKey);
|
|
42
|
+
if (entry) {
|
|
43
|
+
totalCacheSize -= entry.size;
|
|
44
|
+
entry.frame.close();
|
|
45
|
+
exports.frameDatabase.delete(oldestKey);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const addFrameToCache = (key, frame) => {
|
|
50
|
+
const existing = exports.frameDatabase.get(key);
|
|
51
|
+
if (existing) {
|
|
52
|
+
totalCacheSize -= existing.size;
|
|
53
|
+
existing.frame.close();
|
|
54
|
+
}
|
|
55
|
+
const size = frame.allocationSize();
|
|
56
|
+
totalCacheSize += size;
|
|
57
|
+
exports.frameDatabase.set(key, {
|
|
58
|
+
frame,
|
|
59
|
+
lastUsed: Date.now(),
|
|
60
|
+
size,
|
|
61
|
+
});
|
|
62
|
+
evictLRU();
|
|
63
|
+
};
|
|
64
|
+
exports.addFrameToCache = addFrameToCache;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDurationOrCompute = void 0;
|
|
4
|
+
const getDurationOrCompute = async (input) => {
|
|
5
|
+
var _a;
|
|
6
|
+
return ((_a = (await input.getDurationFromMetadata(undefined, {
|
|
7
|
+
skipLiveWait: true,
|
|
8
|
+
}))) !== null && _a !== void 0 ? _a : input.computeDuration(undefined, { skipLiveWait: true }));
|
|
9
|
+
};
|
|
10
|
+
exports.getDurationOrCompute = getDurationOrCompute;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { extractFrames } from './extract-frames';
|
|
2
|
+
export type { ExtractFramesProps, ExtractFramesTimestampsInSecondsFn, } from './extract-frames';
|
|
3
|
+
export { addFrameToCache, aspectRatioCache, frameDatabase, getAspectRatioFromCache, getFrameDatabaseKeyPrefix, getTimestampFromFrameDatabaseKey, makeFrameDatabaseKey, } from './frame-database';
|
|
4
|
+
export type { FrameDatabaseKey } from './frame-database';
|
|
5
|
+
export { getLoopDisplayWidth, shouldTileLoopDisplay } from './loop-display';
|
|
6
|
+
export type { TimelineLoopDisplay } from './loop-display';
|
|
7
|
+
export { calculateTimestampSlots, drawSlot, ensureSlots, fillFrameWhereItFits, fillWithCachedFrames, getDurationOfOneFrame, WEBCODECS_TIMESCALE, } from './render-frame-strip';
|
|
8
|
+
export { resizeVideoFrame } from './resize-video-frame';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resizeVideoFrame = exports.WEBCODECS_TIMESCALE = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = void 0;
|
|
4
|
+
const extract_frames_1 = require("./extract-frames");
|
|
5
|
+
Object.defineProperty(exports, "extractFrames", { enumerable: true, get: function () { return extract_frames_1.extractFrames; } });
|
|
6
|
+
const frame_database_1 = require("./frame-database");
|
|
7
|
+
Object.defineProperty(exports, "addFrameToCache", { enumerable: true, get: function () { return frame_database_1.addFrameToCache; } });
|
|
8
|
+
Object.defineProperty(exports, "aspectRatioCache", { enumerable: true, get: function () { return frame_database_1.aspectRatioCache; } });
|
|
9
|
+
Object.defineProperty(exports, "frameDatabase", { enumerable: true, get: function () { return frame_database_1.frameDatabase; } });
|
|
10
|
+
Object.defineProperty(exports, "getAspectRatioFromCache", { enumerable: true, get: function () { return frame_database_1.getAspectRatioFromCache; } });
|
|
11
|
+
Object.defineProperty(exports, "getFrameDatabaseKeyPrefix", { enumerable: true, get: function () { return frame_database_1.getFrameDatabaseKeyPrefix; } });
|
|
12
|
+
Object.defineProperty(exports, "getTimestampFromFrameDatabaseKey", { enumerable: true, get: function () { return frame_database_1.getTimestampFromFrameDatabaseKey; } });
|
|
13
|
+
Object.defineProperty(exports, "makeFrameDatabaseKey", { enumerable: true, get: function () { return frame_database_1.makeFrameDatabaseKey; } });
|
|
14
|
+
const loop_display_1 = require("./loop-display");
|
|
15
|
+
Object.defineProperty(exports, "getLoopDisplayWidth", { enumerable: true, get: function () { return loop_display_1.getLoopDisplayWidth; } });
|
|
16
|
+
Object.defineProperty(exports, "shouldTileLoopDisplay", { enumerable: true, get: function () { return loop_display_1.shouldTileLoopDisplay; } });
|
|
17
|
+
const render_frame_strip_1 = require("./render-frame-strip");
|
|
18
|
+
Object.defineProperty(exports, "calculateTimestampSlots", { enumerable: true, get: function () { return render_frame_strip_1.calculateTimestampSlots; } });
|
|
19
|
+
Object.defineProperty(exports, "drawSlot", { enumerable: true, get: function () { return render_frame_strip_1.drawSlot; } });
|
|
20
|
+
Object.defineProperty(exports, "ensureSlots", { enumerable: true, get: function () { return render_frame_strip_1.ensureSlots; } });
|
|
21
|
+
Object.defineProperty(exports, "fillFrameWhereItFits", { enumerable: true, get: function () { return render_frame_strip_1.fillFrameWhereItFits; } });
|
|
22
|
+
Object.defineProperty(exports, "fillWithCachedFrames", { enumerable: true, get: function () { return render_frame_strip_1.fillWithCachedFrames; } });
|
|
23
|
+
Object.defineProperty(exports, "getDurationOfOneFrame", { enumerable: true, get: function () { return render_frame_strip_1.getDurationOfOneFrame; } });
|
|
24
|
+
Object.defineProperty(exports, "WEBCODECS_TIMESCALE", { enumerable: true, get: function () { return render_frame_strip_1.WEBCODECS_TIMESCALE; } });
|
|
25
|
+
const resize_video_frame_1 = require("./resize-video-frame");
|
|
26
|
+
Object.defineProperty(exports, "resizeVideoFrame", { enumerable: true, get: function () { return resize_video_frame_1.resizeVideoFrame; } });
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type TimelineLoopDisplay = {
|
|
2
|
+
durationInFrames: number;
|
|
3
|
+
numberOfTimes: number;
|
|
4
|
+
startOffset: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const shouldTileLoopDisplay: (loopDisplay: TimelineLoopDisplay | undefined) => loopDisplay is TimelineLoopDisplay;
|
|
7
|
+
export declare const getLoopDisplayWidth: ({ visualizationWidth, loopDisplay, }: {
|
|
8
|
+
visualizationWidth: number;
|
|
9
|
+
loopDisplay: TimelineLoopDisplay | undefined;
|
|
10
|
+
}) => number;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getLoopDisplayWidth = exports.shouldTileLoopDisplay = void 0;
|
|
4
|
+
const shouldTileLoopDisplay = (loopDisplay) => {
|
|
5
|
+
return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
|
|
6
|
+
};
|
|
7
|
+
exports.shouldTileLoopDisplay = shouldTileLoopDisplay;
|
|
8
|
+
const getLoopDisplayWidth = ({ visualizationWidth, loopDisplay, }) => {
|
|
9
|
+
if (!(0, exports.shouldTileLoopDisplay)(loopDisplay)) {
|
|
10
|
+
return visualizationWidth;
|
|
11
|
+
}
|
|
12
|
+
return visualizationWidth / loopDisplay.numberOfTimes;
|
|
13
|
+
};
|
|
14
|
+
exports.getLoopDisplayWidth = getLoopDisplayWidth;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const WEBCODECS_TIMESCALE = 1000000;
|
|
2
|
+
export declare const getDurationOfOneFrame: ({ visualizationWidth, aspectRatio, segmentDuration, frameHeight, }: {
|
|
3
|
+
visualizationWidth: number;
|
|
4
|
+
aspectRatio: number;
|
|
5
|
+
segmentDuration: number;
|
|
6
|
+
frameHeight: number;
|
|
7
|
+
}) => number;
|
|
8
|
+
export declare const calculateTimestampSlots: ({ visualizationWidth, fromSeconds, segmentDuration, aspectRatio, frameHeight, }: {
|
|
9
|
+
visualizationWidth: number;
|
|
10
|
+
fromSeconds: number;
|
|
11
|
+
segmentDuration: number;
|
|
12
|
+
aspectRatio: number;
|
|
13
|
+
frameHeight: number;
|
|
14
|
+
}) => number[];
|
|
15
|
+
export declare const ensureSlots: ({ filledSlots, naturalWidth, fromSeconds, toSeconds, aspectRatio, frameHeight, }: {
|
|
16
|
+
filledSlots: Map<number, number | undefined>;
|
|
17
|
+
naturalWidth: number;
|
|
18
|
+
fromSeconds: number;
|
|
19
|
+
toSeconds: number;
|
|
20
|
+
aspectRatio: number;
|
|
21
|
+
frameHeight: number;
|
|
22
|
+
}) => void;
|
|
23
|
+
export declare const drawSlot: ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }: {
|
|
24
|
+
frame: VideoFrame;
|
|
25
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
26
|
+
filledSlots: Map<number, number | undefined>;
|
|
27
|
+
visualizationWidth: number;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
segmentDuration: number;
|
|
30
|
+
fromSeconds: number;
|
|
31
|
+
devicePixelRatio: number;
|
|
32
|
+
frameHeight: number;
|
|
33
|
+
}) => void;
|
|
34
|
+
export declare const fillWithCachedFrames: ({ ctx, naturalWidth, filledSlots, src, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }: {
|
|
35
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
36
|
+
naturalWidth: number;
|
|
37
|
+
filledSlots: Map<number, number | undefined>;
|
|
38
|
+
src: string;
|
|
39
|
+
segmentDuration: number;
|
|
40
|
+
fromSeconds: number;
|
|
41
|
+
devicePixelRatio: number;
|
|
42
|
+
frameHeight: number;
|
|
43
|
+
}) => void;
|
|
44
|
+
export declare const fillFrameWhereItFits: ({ frame, filledSlots, ctx, visualizationWidth, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }: {
|
|
45
|
+
frame: VideoFrame;
|
|
46
|
+
filledSlots: Map<number, number | undefined>;
|
|
47
|
+
ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
48
|
+
visualizationWidth: number;
|
|
49
|
+
segmentDuration: number;
|
|
50
|
+
fromSeconds: number;
|
|
51
|
+
devicePixelRatio: number;
|
|
52
|
+
frameHeight: number;
|
|
53
|
+
}) => void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fillFrameWhereItFits = exports.fillWithCachedFrames = exports.drawSlot = exports.ensureSlots = exports.calculateTimestampSlots = exports.getDurationOfOneFrame = exports.WEBCODECS_TIMESCALE = void 0;
|
|
4
|
+
const frame_database_1 = require("./frame-database");
|
|
5
|
+
exports.WEBCODECS_TIMESCALE = 1000000;
|
|
6
|
+
const MAX_TIME_DEVIATION = exports.WEBCODECS_TIMESCALE * 0.05;
|
|
7
|
+
const getDurationOfOneFrame = ({ visualizationWidth, aspectRatio, segmentDuration, frameHeight, }) => {
|
|
8
|
+
const framesFitInWidthUnrounded = visualizationWidth / (frameHeight * aspectRatio);
|
|
9
|
+
return (segmentDuration / framesFitInWidthUnrounded) * exports.WEBCODECS_TIMESCALE;
|
|
10
|
+
};
|
|
11
|
+
exports.getDurationOfOneFrame = getDurationOfOneFrame;
|
|
12
|
+
const fixRounding = (value) => {
|
|
13
|
+
if (value % 1 >= 0.49999999) {
|
|
14
|
+
return Math.ceil(value);
|
|
15
|
+
}
|
|
16
|
+
return Math.floor(value);
|
|
17
|
+
};
|
|
18
|
+
const calculateTimestampSlots = ({ visualizationWidth, fromSeconds, segmentDuration, aspectRatio, frameHeight, }) => {
|
|
19
|
+
const framesFitInWidthUnrounded = visualizationWidth / (frameHeight * aspectRatio);
|
|
20
|
+
const framesFitInWidth = Math.ceil(framesFitInWidthUnrounded);
|
|
21
|
+
const durationOfOneFrame = (0, exports.getDurationOfOneFrame)({
|
|
22
|
+
visualizationWidth,
|
|
23
|
+
aspectRatio,
|
|
24
|
+
segmentDuration,
|
|
25
|
+
frameHeight,
|
|
26
|
+
});
|
|
27
|
+
const timestampTargets = [];
|
|
28
|
+
for (let i = 0; i < framesFitInWidth + 1; i++) {
|
|
29
|
+
const target = fromSeconds * exports.WEBCODECS_TIMESCALE + durationOfOneFrame * (i + 0.5);
|
|
30
|
+
const snappedToDuration = (Math.round(fixRounding(target / durationOfOneFrame)) - 1) *
|
|
31
|
+
durationOfOneFrame;
|
|
32
|
+
timestampTargets.push(snappedToDuration);
|
|
33
|
+
}
|
|
34
|
+
return timestampTargets;
|
|
35
|
+
};
|
|
36
|
+
exports.calculateTimestampSlots = calculateTimestampSlots;
|
|
37
|
+
const ensureSlots = ({ filledSlots, naturalWidth, fromSeconds, toSeconds, aspectRatio, frameHeight, }) => {
|
|
38
|
+
const segmentDuration = toSeconds - fromSeconds;
|
|
39
|
+
const timestampTargets = (0, exports.calculateTimestampSlots)({
|
|
40
|
+
visualizationWidth: naturalWidth,
|
|
41
|
+
fromSeconds,
|
|
42
|
+
segmentDuration,
|
|
43
|
+
aspectRatio,
|
|
44
|
+
frameHeight,
|
|
45
|
+
});
|
|
46
|
+
for (const timestamp of timestampTargets) {
|
|
47
|
+
if (!filledSlots.has(timestamp)) {
|
|
48
|
+
filledSlots.set(timestamp, undefined);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
exports.ensureSlots = ensureSlots;
|
|
53
|
+
const drawSlot = ({ frame, ctx, filledSlots, visualizationWidth, timestamp, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }) => {
|
|
54
|
+
const durationOfOneFrame = (0, exports.getDurationOfOneFrame)({
|
|
55
|
+
visualizationWidth,
|
|
56
|
+
aspectRatio: frame.displayWidth / frame.displayHeight,
|
|
57
|
+
segmentDuration,
|
|
58
|
+
frameHeight,
|
|
59
|
+
});
|
|
60
|
+
const relativeTimestamp = timestamp - fromSeconds * exports.WEBCODECS_TIMESCALE;
|
|
61
|
+
const frameIndex = relativeTimestamp / durationOfOneFrame;
|
|
62
|
+
const thumbnailWidth = frame.displayWidth / devicePixelRatio;
|
|
63
|
+
const left = Math.floor(frameIndex * thumbnailWidth);
|
|
64
|
+
const right = Math.ceil((frameIndex + 1) * thumbnailWidth);
|
|
65
|
+
ctx.drawImage(frame, left, 0, right - left, frame.displayHeight / devicePixelRatio);
|
|
66
|
+
filledSlots.set(timestamp, frame.timestamp);
|
|
67
|
+
};
|
|
68
|
+
exports.drawSlot = drawSlot;
|
|
69
|
+
const fillWithCachedFrames = ({ ctx, naturalWidth, filledSlots, src, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }) => {
|
|
70
|
+
const prefix = (0, frame_database_1.getFrameDatabaseKeyPrefix)(src);
|
|
71
|
+
const keys = Array.from(frame_database_1.frameDatabase.keys()).filter((k) => k.startsWith(prefix));
|
|
72
|
+
const targets = Array.from(filledSlots.keys());
|
|
73
|
+
for (const timestamp of targets) {
|
|
74
|
+
let bestKey;
|
|
75
|
+
let bestDistance = Infinity;
|
|
76
|
+
for (const key of keys) {
|
|
77
|
+
const distance = Math.abs((0, frame_database_1.getTimestampFromFrameDatabaseKey)(key) - timestamp);
|
|
78
|
+
if (distance < bestDistance) {
|
|
79
|
+
bestDistance = distance;
|
|
80
|
+
bestKey = key;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!bestKey) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const frame = frame_database_1.frameDatabase.get(bestKey);
|
|
87
|
+
if (!frame) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const alreadyFilled = filledSlots.get(timestamp);
|
|
91
|
+
if (alreadyFilled &&
|
|
92
|
+
Math.abs(alreadyFilled - timestamp) <=
|
|
93
|
+
Math.abs(frame.frame.timestamp - timestamp)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
frame.lastUsed = Date.now();
|
|
97
|
+
(0, exports.drawSlot)({
|
|
98
|
+
ctx,
|
|
99
|
+
frame: frame.frame,
|
|
100
|
+
filledSlots,
|
|
101
|
+
visualizationWidth: naturalWidth,
|
|
102
|
+
timestamp,
|
|
103
|
+
segmentDuration,
|
|
104
|
+
fromSeconds,
|
|
105
|
+
devicePixelRatio,
|
|
106
|
+
frameHeight,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
exports.fillWithCachedFrames = fillWithCachedFrames;
|
|
111
|
+
const fillFrameWhereItFits = ({ frame, filledSlots, ctx, visualizationWidth, segmentDuration, fromSeconds, devicePixelRatio, frameHeight, }) => {
|
|
112
|
+
const slots = Array.from(filledSlots.keys());
|
|
113
|
+
for (let i = 0; i < slots.length; i++) {
|
|
114
|
+
const slot = slots[i];
|
|
115
|
+
if (Math.abs(slot - frame.timestamp) > MAX_TIME_DEVIATION) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const filled = filledSlots.get(slot);
|
|
119
|
+
if (filled &&
|
|
120
|
+
Math.abs(filled - slot) <= Math.abs(filled - frame.timestamp)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
(0, exports.drawSlot)({
|
|
124
|
+
ctx,
|
|
125
|
+
frame,
|
|
126
|
+
filledSlots,
|
|
127
|
+
visualizationWidth,
|
|
128
|
+
timestamp: slot,
|
|
129
|
+
segmentDuration,
|
|
130
|
+
fromSeconds,
|
|
131
|
+
devicePixelRatio,
|
|
132
|
+
frameHeight,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
exports.fillFrameWhereItFits = fillFrameWhereItFits;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resizeVideoFrame = void 0;
|
|
4
|
+
const calculateNewDimensionsFromScale = ({ width, height, scale, }) => {
|
|
5
|
+
const scaledWidth = Math.round(width * scale);
|
|
6
|
+
const scaledHeight = Math.round(height * scale);
|
|
7
|
+
return {
|
|
8
|
+
width: scaledWidth,
|
|
9
|
+
height: scaledHeight,
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
const resizeVideoFrame = ({ frame, scale, }) => {
|
|
13
|
+
var _a;
|
|
14
|
+
if (scale === 1) {
|
|
15
|
+
return frame;
|
|
16
|
+
}
|
|
17
|
+
const { width, height } = calculateNewDimensionsFromScale({
|
|
18
|
+
height: frame.displayHeight,
|
|
19
|
+
width: frame.displayWidth,
|
|
20
|
+
scale,
|
|
21
|
+
});
|
|
22
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
23
|
+
const ctx = canvas.getContext('2d');
|
|
24
|
+
if (!ctx) {
|
|
25
|
+
throw new Error('Could not get 2d context');
|
|
26
|
+
}
|
|
27
|
+
canvas.width = width;
|
|
28
|
+
canvas.height = height;
|
|
29
|
+
ctx.scale(scale, scale);
|
|
30
|
+
ctx.drawImage(frame, 0, 0);
|
|
31
|
+
return new VideoFrame(canvas, {
|
|
32
|
+
displayHeight: height,
|
|
33
|
+
displayWidth: width,
|
|
34
|
+
duration: (_a = frame.duration) !== null && _a !== void 0 ? _a : undefined,
|
|
35
|
+
timestamp: frame.timestamp,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
exports.resizeVideoFrame = resizeVideoFrame;
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repository": {
|
|
3
|
+
"url": "https://github.com/remotion-dev/remotion/tree/main/packages/timeline-utils"
|
|
4
|
+
},
|
|
5
|
+
"name": "@remotion/timeline-utils",
|
|
6
|
+
"version": "4.0.460",
|
|
7
|
+
"description": "Internal utilities for rendering Remotion timelines",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"scripts": {
|
|
11
|
+
"formatting": "oxfmt src --check",
|
|
12
|
+
"format": "oxfmt src",
|
|
13
|
+
"lint": "eslint src",
|
|
14
|
+
"make": "tsgo -d && bun --env-file=../.env.bundle bundle.ts"
|
|
15
|
+
},
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"module": "dist/esm/index.mjs",
|
|
18
|
+
"author": "Jonny Burger <jonny@remotion.dev>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/remotion-dev/remotion/issues"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"mediabunny": "1.42.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@remotion/eslint-config-internal": "4.0.460",
|
|
28
|
+
"eslint": "9.19.0",
|
|
29
|
+
"@typescript/native-preview": "7.0.0-dev.20260217.1"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": "./package.json",
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"module": "./dist/esm/index.mjs",
|
|
39
|
+
"import": "./dist/esm/index.mjs",
|
|
40
|
+
"require": "./dist/index.js"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|