@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,159 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
import { SAFE_BACK_WINDOW_IN_SECONDS } from '../caches';
|
|
3
|
+
import { roundTo4Digits } from '../helpers/round-to-4-digits';
|
|
4
|
+
import { renderTimestampRange } from '../render-timestamp-range';
|
|
5
|
+
import { getAllocationSize } from './get-allocation-size';
|
|
6
|
+
export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, src, }) => {
|
|
7
|
+
Internals.Log.verbose({ logLevel: parentLogLevel, tag: '@remotion/media' }, `Creating keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
8
|
+
const frames = {};
|
|
9
|
+
const frameTimestamps = [];
|
|
10
|
+
let lastUsed = Date.now();
|
|
11
|
+
let allocationSize = 0;
|
|
12
|
+
const deleteFramesBeforeTimestamp = ({ logLevel, timestampInSeconds, }) => {
|
|
13
|
+
const deletedTimestamps = [];
|
|
14
|
+
for (const frameTimestamp of frameTimestamps.slice()) {
|
|
15
|
+
const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
|
|
16
|
+
// Don't delete the last frame, since it may be the last one in the video!
|
|
17
|
+
if (isLast) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (frameTimestamp < timestampInSeconds) {
|
|
21
|
+
if (!frames[frameTimestamp]) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
allocationSize -= getAllocationSize(frames[frameTimestamp]);
|
|
25
|
+
frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
|
|
26
|
+
frames[frameTimestamp].close();
|
|
27
|
+
delete frames[frameTimestamp];
|
|
28
|
+
deletedTimestamps.push(frameTimestamp);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (deletedTimestamps.length > 0) {
|
|
32
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Deleted ${deletedTimestamps.length} frame${deletedTimestamps.length === 1 ? '' : 's'} ${renderTimestampRange(deletedTimestamps)} for src ${src} because it is lower than ${timestampInSeconds}. Remaining: ${renderTimestampRange(frameTimestamps)}`);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const hasDecodedEnoughForTimestamp = (timestamp) => {
|
|
36
|
+
const lastFrameTimestamp = frameTimestamps[frameTimestamps.length - 1];
|
|
37
|
+
if (!lastFrameTimestamp) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const lastFrame = frames[lastFrameTimestamp];
|
|
41
|
+
// Don't decode more, will probably have to re-decode everything
|
|
42
|
+
if (!lastFrame) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return (roundTo4Digits(lastFrame.timestamp + lastFrame.duration) >
|
|
46
|
+
roundTo4Digits(timestamp) + 0.001);
|
|
47
|
+
};
|
|
48
|
+
const addFrame = (frame) => {
|
|
49
|
+
if (frames[frame.timestamp]) {
|
|
50
|
+
allocationSize -= getAllocationSize(frames[frame.timestamp]);
|
|
51
|
+
frameTimestamps.splice(frameTimestamps.indexOf(frame.timestamp), 1);
|
|
52
|
+
frames[frame.timestamp].close();
|
|
53
|
+
delete frames[frame.timestamp];
|
|
54
|
+
}
|
|
55
|
+
frames[frame.timestamp] = frame;
|
|
56
|
+
frameTimestamps.push(frame.timestamp);
|
|
57
|
+
allocationSize += getAllocationSize(frame);
|
|
58
|
+
lastUsed = Date.now();
|
|
59
|
+
};
|
|
60
|
+
const ensureEnoughFramesForTimestamp = async (timestampInSeconds) => {
|
|
61
|
+
while (!hasDecodedEnoughForTimestamp(timestampInSeconds)) {
|
|
62
|
+
const sample = await sampleIterator.next();
|
|
63
|
+
if (sample.value) {
|
|
64
|
+
addFrame(sample.value);
|
|
65
|
+
}
|
|
66
|
+
if (sample.done) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
deleteFramesBeforeTimestamp({
|
|
70
|
+
logLevel: parentLogLevel,
|
|
71
|
+
timestampInSeconds: timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
lastUsed = Date.now();
|
|
75
|
+
};
|
|
76
|
+
const getFrameFromTimestamp = async (timestampInSeconds) => {
|
|
77
|
+
lastUsed = Date.now();
|
|
78
|
+
// If the requested timestamp is before the start of this bank, clamp it to the start.
|
|
79
|
+
// This matches Chrome's behavior: render the first available frame rather than showing black.
|
|
80
|
+
// Videos don't always start at timestamp 0 due to encoding artifacts, container format quirks,
|
|
81
|
+
// and keyframe positioning. Users have no control over this, so we clamp to the first frame.
|
|
82
|
+
// Test case: https://github.com/remotion-dev/remotion/issues/5915
|
|
83
|
+
let adjustedTimestamp = timestampInSeconds;
|
|
84
|
+
if (roundTo4Digits(timestampInSeconds) <
|
|
85
|
+
roundTo4Digits(startTimestampInSeconds)) {
|
|
86
|
+
adjustedTimestamp = startTimestampInSeconds;
|
|
87
|
+
}
|
|
88
|
+
// If we request a timestamp after the end of the video, return the last frame
|
|
89
|
+
// same behavior as <video>
|
|
90
|
+
if (roundTo4Digits(adjustedTimestamp) > roundTo4Digits(endTimestampInSeconds)) {
|
|
91
|
+
adjustedTimestamp = endTimestampInSeconds;
|
|
92
|
+
}
|
|
93
|
+
await ensureEnoughFramesForTimestamp(adjustedTimestamp);
|
|
94
|
+
for (let i = frameTimestamps.length - 1; i >= 0; i--) {
|
|
95
|
+
const sample = frames[frameTimestamps[i]];
|
|
96
|
+
if (!sample) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (roundTo4Digits(sample.timestamp) <= roundTo4Digits(adjustedTimestamp) ||
|
|
100
|
+
// Match 0.3333333333 to 0.33355555
|
|
101
|
+
// this does not satisfy the previous condition, since one rounds up and one rounds down
|
|
102
|
+
Math.abs(sample.timestamp - adjustedTimestamp) <= 0.001) {
|
|
103
|
+
return sample;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
const hasTimestampInSecond = async (timestamp) => {
|
|
109
|
+
return (await getFrameFromTimestamp(timestamp)) !== null;
|
|
110
|
+
};
|
|
111
|
+
const prepareForDeletion = (logLevel) => {
|
|
112
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Preparing for deletion of keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
113
|
+
// Cleanup frames that have been extracted that might not have been retrieved yet
|
|
114
|
+
sampleIterator.return().then((result) => {
|
|
115
|
+
if (result.value) {
|
|
116
|
+
result.value.close();
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
});
|
|
120
|
+
let framesDeleted = 0;
|
|
121
|
+
for (const frameTimestamp of frameTimestamps) {
|
|
122
|
+
if (!frames[frameTimestamp]) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
allocationSize -= getAllocationSize(frames[frameTimestamp]);
|
|
126
|
+
frames[frameTimestamp].close();
|
|
127
|
+
delete frames[frameTimestamp];
|
|
128
|
+
framesDeleted++;
|
|
129
|
+
}
|
|
130
|
+
frameTimestamps.length = 0;
|
|
131
|
+
return { framesDeleted };
|
|
132
|
+
};
|
|
133
|
+
const getOpenFrameCount = () => {
|
|
134
|
+
return {
|
|
135
|
+
size: allocationSize,
|
|
136
|
+
timestamps: frameTimestamps,
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
const getLastUsed = () => {
|
|
140
|
+
return lastUsed;
|
|
141
|
+
};
|
|
142
|
+
let queue = Promise.resolve(undefined);
|
|
143
|
+
const keyframeBank = {
|
|
144
|
+
startTimestampInSeconds,
|
|
145
|
+
endTimestampInSeconds,
|
|
146
|
+
getFrameFromTimestamp: (timestamp) => {
|
|
147
|
+
queue = queue.then(() => getFrameFromTimestamp(timestamp));
|
|
148
|
+
return queue;
|
|
149
|
+
},
|
|
150
|
+
prepareForDeletion,
|
|
151
|
+
hasTimestampInSecond,
|
|
152
|
+
addFrame,
|
|
153
|
+
deleteFramesBeforeTimestamp,
|
|
154
|
+
src,
|
|
155
|
+
getOpenFrameCount,
|
|
156
|
+
getLastUsed,
|
|
157
|
+
};
|
|
158
|
+
return keyframeBank;
|
|
159
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
import { canBrowserUseWebGl2 } from '../browser-can-use-webgl2';
|
|
3
|
+
import { getTotalCacheStats, SAFE_BACK_WINDOW_IN_SECONDS } from '../caches';
|
|
4
|
+
import { renderTimestampRange } from '../render-timestamp-range';
|
|
5
|
+
import { getFramesSinceKeyframe } from './get-frames-since-keyframe';
|
|
6
|
+
export const makeKeyframeManager = () => {
|
|
7
|
+
// src => {[startTimestampInSeconds]: KeyframeBank
|
|
8
|
+
const sources = {};
|
|
9
|
+
const addKeyframeBank = ({ src, bank, startTimestampInSeconds, }) => {
|
|
10
|
+
sources[src] = sources[src] ?? {};
|
|
11
|
+
sources[src][startTimestampInSeconds] = bank;
|
|
12
|
+
};
|
|
13
|
+
const logCacheStats = async (logLevel) => {
|
|
14
|
+
let count = 0;
|
|
15
|
+
let totalSize = 0;
|
|
16
|
+
for (const src in sources) {
|
|
17
|
+
for (const bank in sources[src]) {
|
|
18
|
+
const v = await sources[src][bank];
|
|
19
|
+
const { size, timestamps } = v.getOpenFrameCount();
|
|
20
|
+
count += timestamps.length;
|
|
21
|
+
totalSize += size;
|
|
22
|
+
if (size === 0) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Open frames for src ${src}: ${renderTimestampRange(timestamps)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Video cache stats: ${count} open frames, ${totalSize} bytes`);
|
|
29
|
+
};
|
|
30
|
+
const getCacheStats = async () => {
|
|
31
|
+
let count = 0;
|
|
32
|
+
let totalSize = 0;
|
|
33
|
+
for (const src in sources) {
|
|
34
|
+
for (const bank in sources[src]) {
|
|
35
|
+
const v = await sources[src][bank];
|
|
36
|
+
const { timestamps, size } = v.getOpenFrameCount();
|
|
37
|
+
count += timestamps.length;
|
|
38
|
+
totalSize += size;
|
|
39
|
+
if (size === 0) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { count, totalSize };
|
|
45
|
+
};
|
|
46
|
+
const getTheKeyframeBankMostInThePast = async () => {
|
|
47
|
+
let mostInThePast = null;
|
|
48
|
+
let mostInThePastBank = null;
|
|
49
|
+
let numberOfBanks = 0;
|
|
50
|
+
for (const src in sources) {
|
|
51
|
+
for (const b in sources[src]) {
|
|
52
|
+
const bank = await sources[src][b];
|
|
53
|
+
const lastUsed = bank.getLastUsed();
|
|
54
|
+
if (mostInThePast === null || lastUsed < mostInThePast) {
|
|
55
|
+
mostInThePast = lastUsed;
|
|
56
|
+
mostInThePastBank = { src, bank };
|
|
57
|
+
}
|
|
58
|
+
numberOfBanks++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!mostInThePastBank) {
|
|
62
|
+
throw new Error('No keyframe bank found');
|
|
63
|
+
}
|
|
64
|
+
return { mostInThePastBank, numberOfBanks };
|
|
65
|
+
};
|
|
66
|
+
const deleteOldestKeyframeBank = async (logLevel) => {
|
|
67
|
+
const { mostInThePastBank: { bank: mostInThePastBank, src: mostInThePastSrc }, numberOfBanks, } = await getTheKeyframeBankMostInThePast();
|
|
68
|
+
if (numberOfBanks < 2) {
|
|
69
|
+
return { finish: true };
|
|
70
|
+
}
|
|
71
|
+
if (mostInThePastBank) {
|
|
72
|
+
const { framesDeleted } = mostInThePastBank.prepareForDeletion(logLevel);
|
|
73
|
+
delete sources[mostInThePastSrc][mostInThePastBank.startTimestampInSeconds];
|
|
74
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Deleted ${framesDeleted} frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
|
|
75
|
+
}
|
|
76
|
+
return { finish: false };
|
|
77
|
+
};
|
|
78
|
+
const ensureToStayUnderMaxCacheSize = async (logLevel, maxCacheSize) => {
|
|
79
|
+
let cacheStats = await getTotalCacheStats();
|
|
80
|
+
while (cacheStats.totalSize > maxCacheSize) {
|
|
81
|
+
const { finish } = await deleteOldestKeyframeBank(logLevel);
|
|
82
|
+
if (finish) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, 'Deleted oldest keyframe bank to stay under max cache size', (cacheStats.totalSize / 1024 / 1024).toFixed(1), 'out of', (maxCacheSize / 1024 / 1024).toFixed(1));
|
|
86
|
+
cacheStats = await getTotalCacheStats();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const clearKeyframeBanksBeforeTime = async ({ timestampInSeconds, src, logLevel, }) => {
|
|
90
|
+
const threshold = timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS;
|
|
91
|
+
if (!sources[src]) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const banks = Object.keys(sources[src]);
|
|
95
|
+
for (const startTimeInSeconds of banks) {
|
|
96
|
+
const bank = await sources[src][startTimeInSeconds];
|
|
97
|
+
const { endTimestampInSeconds, startTimestampInSeconds } = bank;
|
|
98
|
+
if (endTimestampInSeconds < threshold) {
|
|
99
|
+
bank.prepareForDeletion(logLevel);
|
|
100
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `[Video] Cleared frames for src ${src} from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
101
|
+
delete sources[src][startTimeInSeconds];
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
bank.deleteFramesBeforeTimestamp({
|
|
105
|
+
timestampInSeconds: threshold,
|
|
106
|
+
logLevel,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await logCacheStats(logLevel);
|
|
111
|
+
};
|
|
112
|
+
const getKeyframeBankOrRefetch = async ({ packetSink, timestamp, videoSampleSink, src, logLevel, }) => {
|
|
113
|
+
// Try to get the keypacket at the requested timestamp.
|
|
114
|
+
// If it returns null (timestamp is before the first keypacket), fall back to the first packet.
|
|
115
|
+
// This matches mediabunny's internal behavior and handles videos that don't start at timestamp 0.
|
|
116
|
+
const startPacket = (await packetSink.getKeyPacket(timestamp, {
|
|
117
|
+
verifyKeyPackets: true,
|
|
118
|
+
})) ?? (await packetSink.getFirstPacket({ verifyKeyPackets: true }));
|
|
119
|
+
const hasAlpha = startPacket?.sideData.alpha;
|
|
120
|
+
if (hasAlpha && !canBrowserUseWebGl2()) {
|
|
121
|
+
return 'has-alpha';
|
|
122
|
+
}
|
|
123
|
+
if (!startPacket) {
|
|
124
|
+
// e.g. https://discord.com/channels/809501355504959528/809501355504959531/1424400511070765086
|
|
125
|
+
// The video has an offset and the first frame is at time 0.033sec
|
|
126
|
+
// we shall not crash here but handle it gracefully
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const startTimestampInSeconds = startPacket.timestamp;
|
|
130
|
+
const existingBank = sources[src]?.[startTimestampInSeconds];
|
|
131
|
+
// Bank does not yet exist, we need to fetch
|
|
132
|
+
if (!existingBank) {
|
|
133
|
+
const newKeyframeBank = getFramesSinceKeyframe({
|
|
134
|
+
packetSink,
|
|
135
|
+
videoSampleSink,
|
|
136
|
+
startPacket,
|
|
137
|
+
logLevel,
|
|
138
|
+
src,
|
|
139
|
+
});
|
|
140
|
+
addKeyframeBank({ src, bank: newKeyframeBank, startTimestampInSeconds });
|
|
141
|
+
return newKeyframeBank;
|
|
142
|
+
}
|
|
143
|
+
// Bank exists and still has the frame we want
|
|
144
|
+
if (await (await existingBank).hasTimestampInSecond(timestamp)) {
|
|
145
|
+
return existingBank;
|
|
146
|
+
}
|
|
147
|
+
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Keyframe bank exists but frame at time ${timestamp} does not exist anymore.`);
|
|
148
|
+
// Bank exists but frames have already been evicted!
|
|
149
|
+
// First delete it entirely
|
|
150
|
+
await (await existingBank).prepareForDeletion(logLevel);
|
|
151
|
+
delete sources[src][startTimestampInSeconds];
|
|
152
|
+
// Then refetch
|
|
153
|
+
const replacementKeybank = getFramesSinceKeyframe({
|
|
154
|
+
packetSink,
|
|
155
|
+
videoSampleSink,
|
|
156
|
+
startPacket,
|
|
157
|
+
logLevel,
|
|
158
|
+
src,
|
|
159
|
+
});
|
|
160
|
+
addKeyframeBank({ src, bank: replacementKeybank, startTimestampInSeconds });
|
|
161
|
+
return replacementKeybank;
|
|
162
|
+
};
|
|
163
|
+
const requestKeyframeBank = async ({ packetSink, timestamp, videoSampleSink, src, logLevel, maxCacheSize, }) => {
|
|
164
|
+
await ensureToStayUnderMaxCacheSize(logLevel, maxCacheSize);
|
|
165
|
+
await clearKeyframeBanksBeforeTime({
|
|
166
|
+
timestampInSeconds: timestamp,
|
|
167
|
+
src,
|
|
168
|
+
logLevel,
|
|
169
|
+
});
|
|
170
|
+
const keyframeBank = await getKeyframeBankOrRefetch({
|
|
171
|
+
packetSink,
|
|
172
|
+
timestamp,
|
|
173
|
+
videoSampleSink,
|
|
174
|
+
src,
|
|
175
|
+
logLevel,
|
|
176
|
+
});
|
|
177
|
+
return keyframeBank;
|
|
178
|
+
};
|
|
179
|
+
const clearAll = async (logLevel) => {
|
|
180
|
+
const srcs = Object.keys(sources);
|
|
181
|
+
for (const src of srcs) {
|
|
182
|
+
const banks = Object.keys(sources[src]);
|
|
183
|
+
for (const startTimeInSeconds of banks) {
|
|
184
|
+
const bank = await sources[src][startTimeInSeconds];
|
|
185
|
+
bank.prepareForDeletion(logLevel);
|
|
186
|
+
delete sources[src][startTimeInSeconds];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
let queue = Promise.resolve(undefined);
|
|
191
|
+
return {
|
|
192
|
+
requestKeyframeBank: ({ packetSink, timestamp, videoSampleSink, src, logLevel, maxCacheSize, }) => {
|
|
193
|
+
queue = queue.then(() => requestKeyframeBank({
|
|
194
|
+
packetSink,
|
|
195
|
+
timestamp,
|
|
196
|
+
videoSampleSink,
|
|
197
|
+
src,
|
|
198
|
+
logLevel,
|
|
199
|
+
maxCacheSize,
|
|
200
|
+
}));
|
|
201
|
+
return queue;
|
|
202
|
+
},
|
|
203
|
+
getCacheStats,
|
|
204
|
+
clearAll,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const rememberActualMatroskaTimestamps = (isMatroska) => {
|
|
2
|
+
const observations = [];
|
|
3
|
+
const observeTimestamp = (startTime) => {
|
|
4
|
+
if (!isMatroska) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
observations.push(startTime);
|
|
8
|
+
};
|
|
9
|
+
const getRealTimestamp = (observedTimestamp) => {
|
|
10
|
+
if (!isMatroska) {
|
|
11
|
+
return observedTimestamp;
|
|
12
|
+
}
|
|
13
|
+
return (observations.find((observation) => Math.abs(observedTimestamp - observation) < 0.001) ?? null);
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
observeTimestamp,
|
|
17
|
+
getRealTimestamp,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const rotateFrame = async ({ frame, rotation, }) => {
|
|
2
|
+
if (rotation === 0) {
|
|
3
|
+
const directBitmap = await createImageBitmap(frame);
|
|
4
|
+
frame.close();
|
|
5
|
+
return directBitmap;
|
|
6
|
+
}
|
|
7
|
+
const width = rotation === 90 || rotation === 270
|
|
8
|
+
? frame.displayHeight
|
|
9
|
+
: frame.displayWidth;
|
|
10
|
+
const height = rotation === 90 || rotation === 270
|
|
11
|
+
? frame.displayWidth
|
|
12
|
+
: frame.displayHeight;
|
|
13
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
14
|
+
const ctx = canvas.getContext('2d');
|
|
15
|
+
if (!ctx) {
|
|
16
|
+
throw new Error('Could not get 2d context');
|
|
17
|
+
}
|
|
18
|
+
canvas.width = width;
|
|
19
|
+
canvas.height = height;
|
|
20
|
+
if (rotation === 90) {
|
|
21
|
+
ctx.translate(width, 0);
|
|
22
|
+
}
|
|
23
|
+
else if (rotation === 180) {
|
|
24
|
+
ctx.translate(width, height);
|
|
25
|
+
}
|
|
26
|
+
else if (rotation === 270) {
|
|
27
|
+
ctx.translate(0, height);
|
|
28
|
+
}
|
|
29
|
+
ctx.rotate(rotation * (Math.PI / 180));
|
|
30
|
+
ctx.drawImage(frame, 0, 0);
|
|
31
|
+
const bitmap = await createImageBitmap(canvas);
|
|
32
|
+
frame.close();
|
|
33
|
+
return bitmap;
|
|
34
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CanvasSink } from 'mediabunny';
|
|
2
|
+
import { Internals } from 'remotion';
|
|
3
|
+
import { makePrewarmedVideoIteratorCache } from './prewarm-iterator-for-looping';
|
|
4
|
+
import { createVideoIterator, } from './video/video-preview-iterator';
|
|
5
|
+
export const videoIteratorManager = ({ delayPlaybackHandleIfNotPremounting, canvas, context, drawDebugOverlay, logLevel, getOnVideoFrameCallback, videoTrack, getEndTime, getStartTime, getIsLooping, }) => {
|
|
6
|
+
let videoIteratorsCreated = 0;
|
|
7
|
+
let videoFrameIterator = null;
|
|
8
|
+
let framesRendered = 0;
|
|
9
|
+
let currentDelayHandle = null;
|
|
10
|
+
if (canvas) {
|
|
11
|
+
canvas.width = videoTrack.displayWidth;
|
|
12
|
+
canvas.height = videoTrack.displayHeight;
|
|
13
|
+
}
|
|
14
|
+
const canvasSink = new CanvasSink(videoTrack, {
|
|
15
|
+
poolSize: 2,
|
|
16
|
+
fit: 'contain',
|
|
17
|
+
alpha: true,
|
|
18
|
+
});
|
|
19
|
+
const prewarmedVideoIteratorCache = makePrewarmedVideoIteratorCache(canvasSink);
|
|
20
|
+
const drawFrame = (frame) => {
|
|
21
|
+
if (context && canvas) {
|
|
22
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
23
|
+
context.drawImage(frame.canvas, 0, 0);
|
|
24
|
+
}
|
|
25
|
+
framesRendered++;
|
|
26
|
+
drawDebugOverlay();
|
|
27
|
+
const callback = getOnVideoFrameCallback();
|
|
28
|
+
if (callback) {
|
|
29
|
+
callback(frame.canvas);
|
|
30
|
+
}
|
|
31
|
+
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew frame ${frame.timestamp.toFixed(3)}s`);
|
|
32
|
+
};
|
|
33
|
+
const startVideoIterator = async (timeToSeek, nonce) => {
|
|
34
|
+
videoFrameIterator?.destroy();
|
|
35
|
+
const iterator = createVideoIterator(timeToSeek, prewarmedVideoIteratorCache);
|
|
36
|
+
videoIteratorsCreated++;
|
|
37
|
+
videoFrameIterator = iterator;
|
|
38
|
+
const delayHandle = delayPlaybackHandleIfNotPremounting();
|
|
39
|
+
currentDelayHandle = delayHandle;
|
|
40
|
+
let frameResult;
|
|
41
|
+
try {
|
|
42
|
+
frameResult = await iterator.getNext();
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
delayHandle.unblock();
|
|
46
|
+
currentDelayHandle = null;
|
|
47
|
+
}
|
|
48
|
+
if (iterator.isDestroyed()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (nonce.isStale()) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (videoFrameIterator.isDestroyed()) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (!frameResult.value) {
|
|
58
|
+
// media ended
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
drawFrame(frameResult.value);
|
|
62
|
+
};
|
|
63
|
+
const seek = async ({ newTime, nonce }) => {
|
|
64
|
+
if (!videoFrameIterator) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (getIsLooping()) {
|
|
68
|
+
// If less than 1 second from the end away, we pre-warm a new iterator
|
|
69
|
+
if (getEndTime() - newTime < 1) {
|
|
70
|
+
prewarmedVideoIteratorCache.prewarmIteratorForLooping({
|
|
71
|
+
timeToSeek: getStartTime(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Should return immediately, so it's okay to not use Promise.all here
|
|
76
|
+
const videoSatisfyResult = await videoFrameIterator.tryToSatisfySeek(newTime);
|
|
77
|
+
// Doing this before the staleness check, because
|
|
78
|
+
// frame might be better than what we currently have
|
|
79
|
+
// TODO: check if this is actually true
|
|
80
|
+
if (videoSatisfyResult.type === 'satisfied') {
|
|
81
|
+
drawFrame(videoSatisfyResult.frame);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (nonce.isStale()) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await startVideoIterator(newTime, nonce);
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
startVideoIterator,
|
|
91
|
+
getVideoIteratorsCreated: () => videoIteratorsCreated,
|
|
92
|
+
seek,
|
|
93
|
+
destroy: () => {
|
|
94
|
+
prewarmedVideoIteratorCache.destroy();
|
|
95
|
+
videoFrameIterator?.destroy();
|
|
96
|
+
if (context && canvas) {
|
|
97
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
98
|
+
}
|
|
99
|
+
if (currentDelayHandle) {
|
|
100
|
+
currentDelayHandle.unblock();
|
|
101
|
+
currentDelayHandle = null;
|
|
102
|
+
}
|
|
103
|
+
videoFrameIterator = null;
|
|
104
|
+
},
|
|
105
|
+
getVideoFrameIterator: () => videoFrameIterator,
|
|
106
|
+
drawFrame,
|
|
107
|
+
getFramesRendered: () => framesRendered,
|
|
108
|
+
};
|
|
109
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotion/media",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.430",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -14,22 +14,24 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"scripts": {
|
|
16
16
|
"if-node-18+": "node -e \"const [maj]=process.versions.node.split('.').map(Number); process.exit(maj>=18?0:1)\"",
|
|
17
|
-
"formatting": "
|
|
17
|
+
"formatting": "oxfmt src --check",
|
|
18
|
+
"format": "oxfmt src",
|
|
18
19
|
"lint": "eslint src",
|
|
19
20
|
"watch": "tsgo -w",
|
|
20
21
|
"test": "node src/test/execute.mjs",
|
|
21
22
|
"make": "tsgo && bun --env-file=../.env.bundle bundle.ts"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"mediabunny": "1.
|
|
25
|
-
"remotion": "4.0.
|
|
25
|
+
"mediabunny": "1.35.1",
|
|
26
|
+
"remotion": "4.0.429",
|
|
27
|
+
"zod": "4.3.6"
|
|
26
28
|
},
|
|
27
29
|
"peerDependencies": {
|
|
28
30
|
"react": ">=16.8.0",
|
|
29
31
|
"react-dom": ">=16.8.0"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
34
|
+
"@remotion/eslint-config-internal": "4.0.429",
|
|
33
35
|
"@vitest/browser-webdriverio": "4.0.9",
|
|
34
36
|
"eslint": "9.19.0",
|
|
35
37
|
"react": "19.2.3",
|