@remotion/media 4.0.428 → 4.0.430
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 -7
- package/dist/audio/allow-wait.js +15 -0
- package/dist/audio/audio-for-preview.d.ts +0 -1
- package/dist/audio/audio-for-preview.js +304 -0
- package/dist/audio/audio-for-rendering.js +194 -0
- package/dist/audio/audio-preview-iterator.d.ts +4 -2
- package/dist/audio/audio-preview-iterator.js +176 -0
- package/dist/audio/audio.js +20 -0
- package/dist/audio/props.js +1 -0
- package/dist/audio-extraction/audio-cache.js +66 -0
- package/dist/audio-extraction/audio-iterator.js +132 -0
- package/dist/audio-extraction/audio-manager.js +113 -0
- package/dist/audio-extraction/extract-audio.js +132 -0
- package/dist/audio-iterator-manager.d.ts +10 -9
- package/dist/audio-iterator-manager.js +228 -0
- package/dist/browser-can-use-webgl2.js +13 -0
- package/dist/caches.js +61 -0
- package/dist/calculate-playbacktime.js +4 -0
- package/dist/convert-audiodata/apply-volume.js +17 -0
- package/dist/convert-audiodata/combine-audiodata.js +23 -0
- package/dist/convert-audiodata/convert-audiodata.js +73 -0
- package/dist/convert-audiodata/resample-audiodata.js +94 -0
- package/dist/debug-overlay/preview-overlay.d.ts +9 -7
- package/dist/debug-overlay/preview-overlay.js +42 -0
- package/dist/esm/index.mjs +246 -103
- package/dist/extract-frame-and-audio.js +101 -0
- package/dist/get-sink.js +15 -0
- package/dist/get-time-in-seconds.js +40 -0
- package/dist/helpers/round-to-4-digits.js +4 -0
- package/dist/index.js +12 -0
- package/dist/is-type-of-error.js +20 -0
- package/dist/looped-frame.js +10 -0
- package/dist/media-player.d.ts +9 -5
- package/dist/media-player.js +431 -0
- package/dist/nonce-manager.js +13 -0
- package/dist/prewarm-iterator-for-looping.js +56 -0
- package/dist/render-timestamp-range.js +9 -0
- package/dist/show-in-timeline.js +31 -0
- package/dist/use-media-in-timeline.d.ts +3 -2
- package/dist/use-media-in-timeline.js +103 -0
- package/dist/video/props.js +1 -0
- package/dist/video/video-for-preview.js +331 -0
- package/dist/video/video-for-rendering.js +263 -0
- package/dist/video/video-preview-iterator.js +122 -0
- package/dist/video/video.js +35 -0
- package/dist/video-extraction/add-broadcast-channel-listener.js +125 -0
- package/dist/video-extraction/extract-frame-via-broadcast-channel.js +113 -0
- package/dist/video-extraction/extract-frame.js +85 -0
- package/dist/video-extraction/get-allocation-size.js +6 -0
- package/dist/video-extraction/get-frames-since-keyframe.js +108 -0
- package/dist/video-extraction/keyframe-bank.js +159 -0
- package/dist/video-extraction/keyframe-manager.js +206 -0
- package/dist/video-extraction/remember-actual-matroska-timestamps.js +19 -0
- package/dist/video-extraction/rotate-frame.js +34 -0
- package/dist/video-iterator-manager.js +109 -0
- package/package.json +7 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { roundTo4Digits } from '../helpers/round-to-4-digits';
|
|
2
|
+
export const createVideoIterator = (timeToSeek, cache) => {
|
|
3
|
+
let destroyed = false;
|
|
4
|
+
const iterator = cache.makeIteratorOrUsePrewarmed(timeToSeek);
|
|
5
|
+
let lastReturnedFrame = null;
|
|
6
|
+
let iteratorEnded = false;
|
|
7
|
+
const getNextOrNullIfNotAvailable = async () => {
|
|
8
|
+
const next = iterator.next();
|
|
9
|
+
const result = await Promise.race([
|
|
10
|
+
next,
|
|
11
|
+
new Promise((resolve) => {
|
|
12
|
+
Promise.resolve().then(() => resolve());
|
|
13
|
+
}),
|
|
14
|
+
]);
|
|
15
|
+
if (!result) {
|
|
16
|
+
return {
|
|
17
|
+
type: 'need-to-wait-for-it',
|
|
18
|
+
waitPromise: async () => {
|
|
19
|
+
const res = await next;
|
|
20
|
+
if (res.value) {
|
|
21
|
+
lastReturnedFrame = res.value;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
iteratorEnded = true;
|
|
25
|
+
}
|
|
26
|
+
return res.value;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (result.value) {
|
|
31
|
+
lastReturnedFrame = result.value;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
iteratorEnded = true;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
type: 'got-frame-or-end',
|
|
38
|
+
frame: result.value ?? null,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
const destroy = () => {
|
|
42
|
+
destroyed = true;
|
|
43
|
+
lastReturnedFrame = null;
|
|
44
|
+
iterator.return().catch(() => undefined);
|
|
45
|
+
};
|
|
46
|
+
const tryToSatisfySeek = async (time) => {
|
|
47
|
+
if (lastReturnedFrame) {
|
|
48
|
+
const frameTimestamp = roundTo4Digits(lastReturnedFrame.timestamp);
|
|
49
|
+
if (roundTo4Digits(time) < frameTimestamp) {
|
|
50
|
+
return {
|
|
51
|
+
type: 'not-satisfied',
|
|
52
|
+
reason: `iterator is too far, most recently returned ${frameTimestamp}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const frameEndTimestamp = roundTo4Digits(lastReturnedFrame.timestamp + lastReturnedFrame.duration);
|
|
56
|
+
const timestamp = roundTo4Digits(time);
|
|
57
|
+
if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) {
|
|
58
|
+
return {
|
|
59
|
+
type: 'satisfied',
|
|
60
|
+
frame: lastReturnedFrame,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (iteratorEnded) {
|
|
65
|
+
if (lastReturnedFrame) {
|
|
66
|
+
return {
|
|
67
|
+
type: 'satisfied',
|
|
68
|
+
frame: lastReturnedFrame,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
type: 'not-satisfied',
|
|
73
|
+
reason: 'iterator ended',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
while (true) {
|
|
77
|
+
const frame = await getNextOrNullIfNotAvailable();
|
|
78
|
+
if (frame.type === 'need-to-wait-for-it') {
|
|
79
|
+
return {
|
|
80
|
+
type: 'not-satisfied',
|
|
81
|
+
reason: 'iterator did not have frame ready',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (frame.type === 'got-frame-or-end') {
|
|
85
|
+
if (frame.frame === null) {
|
|
86
|
+
iteratorEnded = true;
|
|
87
|
+
if (lastReturnedFrame) {
|
|
88
|
+
return {
|
|
89
|
+
type: 'satisfied',
|
|
90
|
+
frame: lastReturnedFrame,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
type: 'not-satisfied',
|
|
95
|
+
reason: 'iterator ended and did not have frame ready',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const frameTimestamp = roundTo4Digits(frame.frame.timestamp);
|
|
99
|
+
const frameEndTimestamp = roundTo4Digits(frame.frame.timestamp + frame.frame.duration);
|
|
100
|
+
const timestamp = roundTo4Digits(time);
|
|
101
|
+
if (frameTimestamp <= timestamp && frameEndTimestamp > timestamp) {
|
|
102
|
+
return {
|
|
103
|
+
type: 'satisfied',
|
|
104
|
+
frame: frame.frame,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new Error('Unreachable');
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
destroy,
|
|
114
|
+
getNext: () => {
|
|
115
|
+
return iterator.next();
|
|
116
|
+
},
|
|
117
|
+
isDestroyed: () => {
|
|
118
|
+
return destroyed;
|
|
119
|
+
},
|
|
120
|
+
tryToSatisfySeek,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Internals, useRemotionEnvironment } from 'remotion';
|
|
3
|
+
import { VideoForPreview } from './video-for-preview';
|
|
4
|
+
import { VideoForRendering } from './video-for-rendering';
|
|
5
|
+
const { validateMediaTrimProps, resolveTrimProps, validateMediaProps } = Internals;
|
|
6
|
+
const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, style, trimAfter, trimBefore, volume, stack, toneFrequency, showInTimeline, debugOverlay, headless, }) => {
|
|
7
|
+
const environment = useRemotionEnvironment();
|
|
8
|
+
if (typeof src !== 'string') {
|
|
9
|
+
throw new TypeError(`The \`<Video>\` tag requires a string for \`src\`, but got ${JSON.stringify(src)} instead.`);
|
|
10
|
+
}
|
|
11
|
+
validateMediaTrimProps({
|
|
12
|
+
startFrom: undefined,
|
|
13
|
+
endAt: undefined,
|
|
14
|
+
trimBefore,
|
|
15
|
+
trimAfter,
|
|
16
|
+
});
|
|
17
|
+
const { trimBeforeValue, trimAfterValue } = resolveTrimProps({
|
|
18
|
+
startFrom: undefined,
|
|
19
|
+
endAt: undefined,
|
|
20
|
+
trimBefore,
|
|
21
|
+
trimAfter,
|
|
22
|
+
});
|
|
23
|
+
validateMediaProps({ playbackRate, volume }, 'Video');
|
|
24
|
+
if (environment.isRendering) {
|
|
25
|
+
return (_jsx(VideoForRendering, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, name: name, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, stack: stack, style: style, volume: volume, toneFrequency: toneFrequency, trimAfterValue: trimAfterValue, trimBeforeValue: trimBeforeValue, headless: headless }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsx(VideoForPreview, { audioStreamIndex: audioStreamIndex ?? 0, className: className, name: name, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume, showInTimeline: showInTimeline, trimAfter: trimAfterValue, trimBefore: trimBeforeValue, stack: stack ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, debugOverlay: debugOverlay ?? false, headless: headless ?? false }));
|
|
28
|
+
};
|
|
29
|
+
export const Video = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, showInTimeline, style, trimAfter, trimBefore, volume, stack, toneFrequency, debugOverlay, headless, }) => {
|
|
30
|
+
return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ??
|
|
31
|
+
(typeof window !== 'undefined'
|
|
32
|
+
? (window.remotion_logLevel ?? 'info')
|
|
33
|
+
: 'info'), loop: loop ?? false, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', muted: muted ?? false, name: name, onVideoFrame: onVideoFrame, playbackRate: playbackRate ?? 1, showInTimeline: showInTimeline ?? true, src: src, style: style ?? {}, trimAfter: trimAfter, trimBefore: trimBefore, volume: volume ?? 1, toneFrequency: toneFrequency ?? 1, stack: stack, debugOverlay: debugOverlay ?? false, headless: headless ?? false }));
|
|
34
|
+
};
|
|
35
|
+
Internals.addSequenceStackTraces(Video);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { extractFrameAndAudio } from '../extract-frame-and-audio';
|
|
2
|
+
// Send to other channels a message to let them know that the
|
|
3
|
+
// tab was loaded and is ready to receive requests.
|
|
4
|
+
// Emit "readiness" messages for approximately 10 seconds.
|
|
5
|
+
const emitReadiness = (channel) => {
|
|
6
|
+
channel.postMessage({
|
|
7
|
+
type: 'main-tab-ready',
|
|
8
|
+
});
|
|
9
|
+
let times = 0;
|
|
10
|
+
const interval = setInterval(() => {
|
|
11
|
+
channel.postMessage({
|
|
12
|
+
type: 'main-tab-ready',
|
|
13
|
+
});
|
|
14
|
+
times++;
|
|
15
|
+
if (times > 30) {
|
|
16
|
+
clearInterval(interval);
|
|
17
|
+
}
|
|
18
|
+
}, 300);
|
|
19
|
+
};
|
|
20
|
+
export const addBroadcastChannelListener = () => {
|
|
21
|
+
// Doesn't exist in studio
|
|
22
|
+
if (!(typeof window !== 'undefined' &&
|
|
23
|
+
window.remotion_broadcastChannel &&
|
|
24
|
+
window.remotion_isMainTab)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
window.remotion_broadcastChannel.addEventListener('message', async (event) => {
|
|
28
|
+
const data = event.data;
|
|
29
|
+
if (data.type === 'request') {
|
|
30
|
+
try {
|
|
31
|
+
const result = await extractFrameAndAudio({
|
|
32
|
+
src: data.src,
|
|
33
|
+
timeInSeconds: data.timeInSeconds,
|
|
34
|
+
logLevel: data.logLevel,
|
|
35
|
+
durationInSeconds: data.durationInSeconds,
|
|
36
|
+
playbackRate: data.playbackRate,
|
|
37
|
+
includeAudio: data.includeAudio,
|
|
38
|
+
includeVideo: data.includeVideo,
|
|
39
|
+
loop: data.loop,
|
|
40
|
+
audioStreamIndex: data.audioStreamIndex,
|
|
41
|
+
trimAfter: data.trimAfter,
|
|
42
|
+
trimBefore: data.trimBefore,
|
|
43
|
+
fps: data.fps,
|
|
44
|
+
maxCacheSize: data.maxCacheSize,
|
|
45
|
+
});
|
|
46
|
+
if (result.type === 'cannot-decode') {
|
|
47
|
+
const cannotDecodeResponse = {
|
|
48
|
+
type: 'response-cannot-decode',
|
|
49
|
+
id: data.id,
|
|
50
|
+
durationInSeconds: result.durationInSeconds,
|
|
51
|
+
};
|
|
52
|
+
window.remotion_broadcastChannel.postMessage(cannotDecodeResponse);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (result.type === 'cannot-decode-alpha') {
|
|
56
|
+
const cannotDecodeAlphaResponse = {
|
|
57
|
+
type: 'response-cannot-decode-alpha',
|
|
58
|
+
id: data.id,
|
|
59
|
+
durationInSeconds: result.durationInSeconds,
|
|
60
|
+
};
|
|
61
|
+
window.remotion_broadcastChannel.postMessage(cannotDecodeAlphaResponse);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (result.type === 'network-error') {
|
|
65
|
+
const networkErrorResponse = {
|
|
66
|
+
type: 'response-network-error',
|
|
67
|
+
id: data.id,
|
|
68
|
+
};
|
|
69
|
+
window.remotion_broadcastChannel.postMessage(networkErrorResponse);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (result.type === 'unknown-container-format') {
|
|
73
|
+
const unknownContainerFormatResponse = {
|
|
74
|
+
type: 'response-unknown-container-format',
|
|
75
|
+
id: data.id,
|
|
76
|
+
};
|
|
77
|
+
window.remotion_broadcastChannel.postMessage(unknownContainerFormatResponse);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const { frame, audio, durationInSeconds } = result;
|
|
81
|
+
const imageBitmap = frame ? await createImageBitmap(frame) : null;
|
|
82
|
+
if (frame) {
|
|
83
|
+
frame.close();
|
|
84
|
+
}
|
|
85
|
+
const response = {
|
|
86
|
+
type: 'response-success',
|
|
87
|
+
id: data.id,
|
|
88
|
+
frame: imageBitmap,
|
|
89
|
+
audio,
|
|
90
|
+
durationInSeconds: durationInSeconds ?? null,
|
|
91
|
+
};
|
|
92
|
+
window.remotion_broadcastChannel.postMessage(response);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const response = {
|
|
96
|
+
type: 'response-error',
|
|
97
|
+
id: data.id,
|
|
98
|
+
errorStack: error.stack ?? 'No stack trace',
|
|
99
|
+
};
|
|
100
|
+
window.remotion_broadcastChannel.postMessage(response);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
throw new Error('Invalid message: ' + JSON.stringify(data));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
emitReadiness(window.remotion_broadcastChannel);
|
|
108
|
+
};
|
|
109
|
+
let mainTabIsReadyProm = null;
|
|
110
|
+
export const waitForMainTabToBeReady = (channel) => {
|
|
111
|
+
if (mainTabIsReadyProm) {
|
|
112
|
+
return mainTabIsReadyProm;
|
|
113
|
+
}
|
|
114
|
+
mainTabIsReadyProm = new Promise((resolve) => {
|
|
115
|
+
const onMessage = (event) => {
|
|
116
|
+
const data = event.data;
|
|
117
|
+
if (data.type === 'main-tab-ready') {
|
|
118
|
+
resolve();
|
|
119
|
+
channel.removeEventListener('message', onMessage);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
channel.addEventListener('message', onMessage);
|
|
123
|
+
});
|
|
124
|
+
return mainTabIsReadyProm;
|
|
125
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { extractFrameAndAudio } from '../extract-frame-and-audio';
|
|
2
|
+
import { addBroadcastChannelListener, waitForMainTabToBeReady, } from './add-broadcast-channel-listener';
|
|
3
|
+
addBroadcastChannelListener();
|
|
4
|
+
export const extractFrameViaBroadcastChannel = async ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, isClientSideRendering, loop, audioStreamIndex, trimAfter, trimBefore, fps, maxCacheSize, }) => {
|
|
5
|
+
if (isClientSideRendering || window.remotion_isMainTab) {
|
|
6
|
+
return extractFrameAndAudio({
|
|
7
|
+
logLevel,
|
|
8
|
+
src,
|
|
9
|
+
timeInSeconds,
|
|
10
|
+
durationInSeconds,
|
|
11
|
+
playbackRate,
|
|
12
|
+
includeAudio,
|
|
13
|
+
includeVideo,
|
|
14
|
+
loop,
|
|
15
|
+
audioStreamIndex,
|
|
16
|
+
trimAfter,
|
|
17
|
+
trimBefore,
|
|
18
|
+
fps,
|
|
19
|
+
maxCacheSize,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
await waitForMainTabToBeReady(window.remotion_broadcastChannel);
|
|
23
|
+
const requestId = crypto.randomUUID();
|
|
24
|
+
const resolvePromise = new Promise((resolve, reject) => {
|
|
25
|
+
const onMessage = (event) => {
|
|
26
|
+
const data = event.data;
|
|
27
|
+
if (!data) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (data.type === 'main-tab-ready') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (data.id !== requestId) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (data.type === 'response-success') {
|
|
37
|
+
resolve({
|
|
38
|
+
type: 'success',
|
|
39
|
+
frame: data.frame ? data.frame : null,
|
|
40
|
+
audio: data.audio ? data.audio : null,
|
|
41
|
+
durationInSeconds: data.durationInSeconds
|
|
42
|
+
? data.durationInSeconds
|
|
43
|
+
: null,
|
|
44
|
+
});
|
|
45
|
+
window.remotion_broadcastChannel.removeEventListener('message', onMessage);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (data.type === 'response-error') {
|
|
49
|
+
reject(data.errorStack);
|
|
50
|
+
window.remotion_broadcastChannel.removeEventListener('message', onMessage);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (data.type === 'response-cannot-decode') {
|
|
54
|
+
resolve({
|
|
55
|
+
type: 'cannot-decode',
|
|
56
|
+
durationInSeconds: data.durationInSeconds,
|
|
57
|
+
});
|
|
58
|
+
window.remotion_broadcastChannel.removeEventListener('message', onMessage);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (data.type === 'response-network-error') {
|
|
62
|
+
resolve({ type: 'network-error' });
|
|
63
|
+
window.remotion_broadcastChannel.removeEventListener('message', onMessage);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (data.type === 'response-unknown-container-format') {
|
|
67
|
+
resolve({ type: 'unknown-container-format' });
|
|
68
|
+
window.remotion_broadcastChannel.removeEventListener('message', onMessage);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (data.type === 'response-cannot-decode-alpha') {
|
|
72
|
+
resolve({
|
|
73
|
+
type: 'cannot-decode-alpha',
|
|
74
|
+
durationInSeconds: data.durationInSeconds,
|
|
75
|
+
});
|
|
76
|
+
window.remotion_broadcastChannel.removeEventListener('message', onMessage);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Invalid message: ${JSON.stringify(data)}`);
|
|
80
|
+
};
|
|
81
|
+
window.remotion_broadcastChannel.addEventListener('message', onMessage);
|
|
82
|
+
});
|
|
83
|
+
const request = {
|
|
84
|
+
type: 'request',
|
|
85
|
+
src,
|
|
86
|
+
timeInSeconds,
|
|
87
|
+
id: requestId,
|
|
88
|
+
logLevel,
|
|
89
|
+
durationInSeconds,
|
|
90
|
+
playbackRate,
|
|
91
|
+
includeAudio,
|
|
92
|
+
includeVideo,
|
|
93
|
+
loop,
|
|
94
|
+
audioStreamIndex,
|
|
95
|
+
trimAfter,
|
|
96
|
+
trimBefore,
|
|
97
|
+
fps,
|
|
98
|
+
maxCacheSize,
|
|
99
|
+
};
|
|
100
|
+
window.remotion_broadcastChannel.postMessage(request);
|
|
101
|
+
let timeoutId;
|
|
102
|
+
return Promise.race([
|
|
103
|
+
resolvePromise.then((res) => {
|
|
104
|
+
clearTimeout(timeoutId);
|
|
105
|
+
return res;
|
|
106
|
+
}),
|
|
107
|
+
new Promise((_, reject) => {
|
|
108
|
+
timeoutId = setTimeout(() => {
|
|
109
|
+
reject(new Error(`Timeout while extracting frame at time ${timeInSeconds}sec from ${src}`));
|
|
110
|
+
}, Math.max(3000, window.remotion_puppeteerTimeout - 5000));
|
|
111
|
+
}),
|
|
112
|
+
]);
|
|
113
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
import { keyframeManager } from '../caches';
|
|
3
|
+
import { getSink } from '../get-sink';
|
|
4
|
+
import { getTimeInSeconds } from '../get-time-in-seconds';
|
|
5
|
+
const extractFrameInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds, logLevel, loop, trimAfter, trimBefore, playbackRate, fps, maxCacheSize, }) => {
|
|
6
|
+
const sink = await getSink(src, logLevel);
|
|
7
|
+
const [video, mediaDurationInSecondsRaw] = await Promise.all([
|
|
8
|
+
sink.getVideo(),
|
|
9
|
+
loop ? sink.getDuration() : Promise.resolve(null),
|
|
10
|
+
]);
|
|
11
|
+
const mediaDurationInSeconds = loop
|
|
12
|
+
? mediaDurationInSecondsRaw
|
|
13
|
+
: null;
|
|
14
|
+
if (video === 'no-video-track') {
|
|
15
|
+
throw new Error(`No video track found for ${src}`);
|
|
16
|
+
}
|
|
17
|
+
if (video === 'cannot-decode') {
|
|
18
|
+
return { type: 'cannot-decode', durationInSeconds: mediaDurationInSeconds };
|
|
19
|
+
}
|
|
20
|
+
if (video === 'unknown-container-format') {
|
|
21
|
+
return { type: 'unknown-container-format' };
|
|
22
|
+
}
|
|
23
|
+
if (video === 'network-error') {
|
|
24
|
+
return { type: 'network-error' };
|
|
25
|
+
}
|
|
26
|
+
const timeInSeconds = getTimeInSeconds({
|
|
27
|
+
loop,
|
|
28
|
+
mediaDurationInSeconds,
|
|
29
|
+
unloopedTimeInSeconds,
|
|
30
|
+
src,
|
|
31
|
+
trimAfter,
|
|
32
|
+
playbackRate,
|
|
33
|
+
trimBefore,
|
|
34
|
+
fps,
|
|
35
|
+
ifNoMediaDuration: 'fail',
|
|
36
|
+
});
|
|
37
|
+
if (timeInSeconds === null) {
|
|
38
|
+
return {
|
|
39
|
+
type: 'success',
|
|
40
|
+
frame: null,
|
|
41
|
+
durationInSeconds: await sink.getDuration(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Must catch https://github.com/Vanilagy/mediabunny/issues/235
|
|
45
|
+
// https://discord.com/channels/@me/1127949286789881897/1455728482150518906
|
|
46
|
+
// Should be able to remove once upgraded to Chrome 145
|
|
47
|
+
try {
|
|
48
|
+
const keyframeBank = await keyframeManager.requestKeyframeBank({
|
|
49
|
+
packetSink: video.packetSink,
|
|
50
|
+
videoSampleSink: video.sampleSink,
|
|
51
|
+
timestamp: timeInSeconds,
|
|
52
|
+
src,
|
|
53
|
+
logLevel,
|
|
54
|
+
maxCacheSize,
|
|
55
|
+
});
|
|
56
|
+
if (keyframeBank === 'has-alpha') {
|
|
57
|
+
return {
|
|
58
|
+
type: 'cannot-decode-alpha',
|
|
59
|
+
durationInSeconds: await sink.getDuration(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!keyframeBank) {
|
|
63
|
+
return {
|
|
64
|
+
type: 'success',
|
|
65
|
+
frame: null,
|
|
66
|
+
durationInSeconds: await sink.getDuration(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const frame = await keyframeBank.getFrameFromTimestamp(timeInSeconds);
|
|
70
|
+
return {
|
|
71
|
+
type: 'success',
|
|
72
|
+
frame,
|
|
73
|
+
durationInSeconds: await sink.getDuration(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
Internals.Log.info({ logLevel, tag: '@remotion/media' }, `Error decoding ${src} at time ${timeInSeconds}: ${err}`, err);
|
|
78
|
+
return { type: 'cannot-decode', durationInSeconds: mediaDurationInSeconds };
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
let queue = Promise.resolve(undefined);
|
|
82
|
+
export const extractFrame = (params) => {
|
|
83
|
+
queue = queue.then(() => extractFrameInternal(params));
|
|
84
|
+
return queue;
|
|
85
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ALL_FORMATS, AudioSampleSink, EncodedPacketSink, Input, MATROSKA, UrlSource, VideoSampleSink, WEBM, } from 'mediabunny';
|
|
2
|
+
import { isNetworkError } from '../is-type-of-error';
|
|
3
|
+
import { makeKeyframeBank } from './keyframe-bank';
|
|
4
|
+
import { rememberActualMatroskaTimestamps } from './remember-actual-matroska-timestamps';
|
|
5
|
+
const getRetryDelay = (() => {
|
|
6
|
+
return null;
|
|
7
|
+
});
|
|
8
|
+
const getFormatOrNullOrNetworkError = async (input) => {
|
|
9
|
+
try {
|
|
10
|
+
return await input.getFormat();
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (isNetworkError(err)) {
|
|
14
|
+
return 'network-error';
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export const getSinks = async (src) => {
|
|
20
|
+
const input = new Input({
|
|
21
|
+
formats: ALL_FORMATS,
|
|
22
|
+
source: new UrlSource(src, {
|
|
23
|
+
getRetryDelay,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
const format = await getFormatOrNullOrNetworkError(input);
|
|
27
|
+
const isMatroska = format === MATROSKA || format === WEBM;
|
|
28
|
+
const getVideoSinks = async () => {
|
|
29
|
+
if (format === 'network-error') {
|
|
30
|
+
return 'network-error';
|
|
31
|
+
}
|
|
32
|
+
if (format === null) {
|
|
33
|
+
return 'unknown-container-format';
|
|
34
|
+
}
|
|
35
|
+
const videoTrack = await input.getPrimaryVideoTrack();
|
|
36
|
+
if (!videoTrack) {
|
|
37
|
+
return 'no-video-track';
|
|
38
|
+
}
|
|
39
|
+
const canDecode = await videoTrack.canDecode();
|
|
40
|
+
if (!canDecode) {
|
|
41
|
+
return 'cannot-decode';
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
sampleSink: new VideoSampleSink(videoTrack),
|
|
45
|
+
packetSink: new EncodedPacketSink(videoTrack),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
let videoSinksPromise = null;
|
|
49
|
+
const getVideoSinksPromise = () => {
|
|
50
|
+
if (videoSinksPromise) {
|
|
51
|
+
return videoSinksPromise;
|
|
52
|
+
}
|
|
53
|
+
videoSinksPromise = getVideoSinks();
|
|
54
|
+
return videoSinksPromise;
|
|
55
|
+
};
|
|
56
|
+
// audioSinksPromise is now a record indexed by audio track index
|
|
57
|
+
const audioSinksPromise = {};
|
|
58
|
+
const getAudioSinks = async (index) => {
|
|
59
|
+
if (format === null) {
|
|
60
|
+
return 'unknown-container-format';
|
|
61
|
+
}
|
|
62
|
+
if (format === 'network-error') {
|
|
63
|
+
return 'network-error';
|
|
64
|
+
}
|
|
65
|
+
const audioTracks = await input.getAudioTracks();
|
|
66
|
+
const audioTrack = audioTracks[index];
|
|
67
|
+
if (!audioTrack) {
|
|
68
|
+
return 'no-audio-track';
|
|
69
|
+
}
|
|
70
|
+
const canDecode = await audioTrack.canDecode();
|
|
71
|
+
if (!canDecode) {
|
|
72
|
+
return 'cannot-decode-audio';
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
sampleSink: new AudioSampleSink(audioTrack),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const getAudioSinksPromise = (index) => {
|
|
79
|
+
if (audioSinksPromise[index]) {
|
|
80
|
+
return audioSinksPromise[index];
|
|
81
|
+
}
|
|
82
|
+
audioSinksPromise[index] = getAudioSinks(index);
|
|
83
|
+
return audioSinksPromise[index];
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
getVideo: () => getVideoSinksPromise(),
|
|
87
|
+
getAudio: (index) => getAudioSinksPromise(index),
|
|
88
|
+
actualMatroskaTimestamps: rememberActualMatroskaTimestamps(isMatroska),
|
|
89
|
+
isMatroska,
|
|
90
|
+
getDuration: () => {
|
|
91
|
+
return input.computeDuration();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
export const getFramesSinceKeyframe = async ({ packetSink, videoSampleSink, startPacket, logLevel, src, }) => {
|
|
96
|
+
const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
|
|
97
|
+
verifyKeyPackets: true,
|
|
98
|
+
});
|
|
99
|
+
const sampleIterator = videoSampleSink.samples(startPacket.timestamp, nextKeyPacket ? nextKeyPacket.timestamp : Infinity);
|
|
100
|
+
const keyframeBank = makeKeyframeBank({
|
|
101
|
+
startTimestampInSeconds: startPacket.timestamp,
|
|
102
|
+
endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
|
|
103
|
+
sampleIterator,
|
|
104
|
+
logLevel,
|
|
105
|
+
src,
|
|
106
|
+
});
|
|
107
|
+
return keyframeBank;
|
|
108
|
+
};
|