@remotion/media 4.0.361 → 4.0.363
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio/audio-for-rendering.js +14 -5
- package/dist/audio/audio.js +0 -1
- package/dist/caches.js +4 -2
- package/dist/esm/index.mjs +111 -82
- package/dist/video/media-player.d.ts +1 -0
- package/dist/video/media-player.js +13 -3
- package/dist/video/timeout-utils.d.ts +3 -0
- package/dist/video/timeout-utils.js +7 -1
- package/dist/video/video-for-rendering.js +8 -6
- package/dist/video/video.js +2 -1
- package/dist/video-extraction/extract-frame-via-broadcast-channel.js +3 -1
- package/dist/video-extraction/get-frames-since-keyframe.d.ts +2 -1
- package/dist/video-extraction/get-frames-since-keyframe.js +2 -1
- package/dist/video-extraction/keyframe-bank.d.ts +7 -4
- package/dist/video-extraction/keyframe-bank.js +36 -30
- package/dist/video-extraction/keyframe-manager.js +18 -7
- package/package.json +3 -3
|
@@ -5,7 +5,7 @@ import { applyVolume } from '../convert-audiodata/apply-volume';
|
|
|
5
5
|
import { TARGET_SAMPLE_RATE } from '../convert-audiodata/resample-audiodata';
|
|
6
6
|
import { frameForVolumeProp } from '../looped-frame';
|
|
7
7
|
import { extractFrameViaBroadcastChannel } from '../video-extraction/extract-frame-via-broadcast-channel';
|
|
8
|
-
export const AudioForRendering = ({ volume: volumeProp, playbackRate, src, muted, loopVolumeCurveBehavior, delayRenderRetries, delayRenderTimeoutInMilliseconds, logLevel
|
|
8
|
+
export const AudioForRendering = ({ volume: volumeProp, playbackRate, src, muted, loopVolumeCurveBehavior, delayRenderRetries, delayRenderTimeoutInMilliseconds, logLevel, loop, fallbackHtml5AudioProps, audioStreamIndex, showInTimeline, style, name, disallowFallbackToHtml5Audio, toneFrequency, trimAfter, trimBefore, }) => {
|
|
9
9
|
const frame = useCurrentFrame();
|
|
10
10
|
const absoluteFrame = Internals.useTimelinePosition();
|
|
11
11
|
const videoConfig = Internals.useUnsafeVideoConfig();
|
|
@@ -54,7 +54,7 @@ export const AudioForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
54
54
|
timeInSeconds: timestamp,
|
|
55
55
|
durationInSeconds,
|
|
56
56
|
playbackRate: playbackRate ?? 1,
|
|
57
|
-
logLevel,
|
|
57
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
58
58
|
includeAudio: shouldRenderAudio,
|
|
59
59
|
includeVideo: false,
|
|
60
60
|
isClientSideRendering: environment.isClientSideRendering,
|
|
@@ -69,7 +69,10 @@ export const AudioForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
69
69
|
if (disallowFallbackToHtml5Audio) {
|
|
70
70
|
cancelRender(new Error(`Unknown container format ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
|
|
71
71
|
}
|
|
72
|
-
Internals.Log.warn({
|
|
72
|
+
Internals.Log.warn({
|
|
73
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
74
|
+
tag: '@remotion/media',
|
|
75
|
+
}, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
|
|
73
76
|
setReplaceWithHtml5Audio(true);
|
|
74
77
|
return;
|
|
75
78
|
}
|
|
@@ -77,7 +80,10 @@ export const AudioForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
77
80
|
if (disallowFallbackToHtml5Audio) {
|
|
78
81
|
cancelRender(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
|
|
79
82
|
}
|
|
80
|
-
Internals.Log.warn({
|
|
83
|
+
Internals.Log.warn({
|
|
84
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
85
|
+
tag: '@remotion/media',
|
|
86
|
+
}, `Cannot decode ${src}, falling back to <Html5Audio>`);
|
|
81
87
|
setReplaceWithHtml5Audio(true);
|
|
82
88
|
return;
|
|
83
89
|
}
|
|
@@ -88,7 +94,10 @@ export const AudioForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
88
94
|
if (disallowFallbackToHtml5Audio) {
|
|
89
95
|
cancelRender(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
|
|
90
96
|
}
|
|
91
|
-
Internals.Log.warn({
|
|
97
|
+
Internals.Log.warn({
|
|
98
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
99
|
+
tag: '@remotion/media',
|
|
100
|
+
}, `Network error fetching ${src}, falling back to <Html5Audio>`);
|
|
92
101
|
setReplaceWithHtml5Audio(true);
|
|
93
102
|
return;
|
|
94
103
|
}
|
package/dist/audio/audio.js
CHANGED
package/dist/caches.js
CHANGED
|
@@ -14,7 +14,8 @@ export const getTotalCacheStats = async () => {
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
const getUncachedMaxCacheSize = (logLevel) => {
|
|
17
|
-
if (window
|
|
17
|
+
if (typeof window !== 'undefined' &&
|
|
18
|
+
window.remotion_mediaCacheSizeInBytes !== undefined &&
|
|
18
19
|
window.remotion_mediaCacheSizeInBytes !== null) {
|
|
19
20
|
if (window.remotion_mediaCacheSizeInBytes < 240 * 1024 * 1024) {
|
|
20
21
|
cancelRender(new Error(`The minimum value for the "mediaCacheSizeInBytes" prop is 240MB (${240 * 1024 * 1024}), got: ${window.remotion_mediaCacheSizeInBytes}`));
|
|
@@ -25,7 +26,8 @@ const getUncachedMaxCacheSize = (logLevel) => {
|
|
|
25
26
|
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Using cache size set using "mediaCacheSizeInBytes": ${(window.remotion_mediaCacheSizeInBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
26
27
|
return window.remotion_mediaCacheSizeInBytes;
|
|
27
28
|
}
|
|
28
|
-
if (window
|
|
29
|
+
if (typeof window !== 'undefined' &&
|
|
30
|
+
window.remotion_initialMemoryAvailable !== undefined &&
|
|
29
31
|
window.remotion_initialMemoryAvailable !== null) {
|
|
30
32
|
const value = window.remotion_initialMemoryAvailable / 2;
|
|
31
33
|
if (value < 500 * 1024 * 1024) {
|
package/dist/esm/index.mjs
CHANGED
|
@@ -226,11 +226,18 @@ function isNetworkError(error) {
|
|
|
226
226
|
|
|
227
227
|
// src/video/timeout-utils.ts
|
|
228
228
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
229
|
+
|
|
230
|
+
class TimeoutError extends Error {
|
|
231
|
+
constructor(message = "Operation timed out") {
|
|
232
|
+
super(message);
|
|
233
|
+
this.name = "TimeoutError";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
229
236
|
function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") {
|
|
230
237
|
let timeoutId = null;
|
|
231
238
|
const timeoutPromise = new Promise((_, reject) => {
|
|
232
239
|
timeoutId = window.setTimeout(() => {
|
|
233
|
-
reject(new
|
|
240
|
+
reject(new TimeoutError(errorMessage));
|
|
234
241
|
}, timeoutMs);
|
|
235
242
|
});
|
|
236
243
|
return Promise.race([
|
|
@@ -280,6 +287,7 @@ class MediaPlayer {
|
|
|
280
287
|
audioBufferHealth = 0;
|
|
281
288
|
audioIteratorStarted = false;
|
|
282
289
|
HEALTHY_BUFER_THRESHOLD_SECONDS = 1;
|
|
290
|
+
mediaEnded = false;
|
|
283
291
|
onVideoFrameCallback;
|
|
284
292
|
constructor({
|
|
285
293
|
canvas,
|
|
@@ -440,6 +448,8 @@ class MediaPlayer {
|
|
|
440
448
|
src: this.src
|
|
441
449
|
});
|
|
442
450
|
if (newTime === null) {
|
|
451
|
+
this.videoAsyncId++;
|
|
452
|
+
this.nextFrame = null;
|
|
443
453
|
this.clearCanvas();
|
|
444
454
|
await this.cleanAudioIteratorAndNodes();
|
|
445
455
|
return;
|
|
@@ -449,6 +459,7 @@ class MediaPlayer {
|
|
|
449
459
|
if (isSignificantSeek) {
|
|
450
460
|
this.nextFrame = null;
|
|
451
461
|
this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
|
|
462
|
+
this.mediaEnded = false;
|
|
452
463
|
if (this.audioSink) {
|
|
453
464
|
await this.cleanAudioIteratorAndNodes();
|
|
454
465
|
}
|
|
@@ -646,6 +657,7 @@ class MediaPlayer {
|
|
|
646
657
|
while (true) {
|
|
647
658
|
const newNextFrame = (await this.videoFrameIterator.next()).value ?? null;
|
|
648
659
|
if (!newNextFrame) {
|
|
660
|
+
this.mediaEnded = true;
|
|
649
661
|
break;
|
|
650
662
|
}
|
|
651
663
|
const playbackTime = this.getPlaybackTime();
|
|
@@ -716,12 +728,15 @@ class MediaPlayer {
|
|
|
716
728
|
let result;
|
|
717
729
|
try {
|
|
718
730
|
result = await withTimeout(this.audioBufferIterator.next(), BUFFERING_TIMEOUT_MS, "Iterator timeout");
|
|
719
|
-
} catch {
|
|
720
|
-
this.
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (error instanceof TimeoutError && !this.mediaEnded) {
|
|
733
|
+
this.setBufferingState(true);
|
|
734
|
+
}
|
|
721
735
|
await sleep(10);
|
|
722
736
|
continue;
|
|
723
737
|
}
|
|
724
738
|
if (result.done || !result.value) {
|
|
739
|
+
this.mediaEnded = true;
|
|
725
740
|
break;
|
|
726
741
|
}
|
|
727
742
|
const { buffer, timestamp, duration } = result.value;
|
|
@@ -1576,13 +1591,39 @@ var makeKeyframeBank = ({
|
|
|
1576
1591
|
startTimestampInSeconds,
|
|
1577
1592
|
endTimestampInSeconds,
|
|
1578
1593
|
sampleIterator,
|
|
1579
|
-
logLevel: parentLogLevel
|
|
1594
|
+
logLevel: parentLogLevel,
|
|
1595
|
+
src
|
|
1580
1596
|
}) => {
|
|
1581
1597
|
Internals8.Log.verbose({ logLevel: parentLogLevel, tag: "@remotion/media" }, `Creating keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
1582
1598
|
const frames = {};
|
|
1583
1599
|
const frameTimestamps = [];
|
|
1584
1600
|
let lastUsed = Date.now();
|
|
1585
1601
|
let allocationSize = 0;
|
|
1602
|
+
const deleteFramesBeforeTimestamp = ({
|
|
1603
|
+
logLevel,
|
|
1604
|
+
timestampInSeconds
|
|
1605
|
+
}) => {
|
|
1606
|
+
const deletedTimestamps = [];
|
|
1607
|
+
for (const frameTimestamp of frameTimestamps.slice()) {
|
|
1608
|
+
const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
|
|
1609
|
+
if (isLast) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
if (frameTimestamp < timestampInSeconds) {
|
|
1613
|
+
if (!frames[frameTimestamp]) {
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
allocationSize -= frames[frameTimestamp].allocationSize();
|
|
1617
|
+
frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
|
|
1618
|
+
frames[frameTimestamp].close();
|
|
1619
|
+
delete frames[frameTimestamp];
|
|
1620
|
+
deletedTimestamps.push(frameTimestamp);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (deletedTimestamps.length > 0) {
|
|
1624
|
+
Internals8.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)}`);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1586
1627
|
const hasDecodedEnoughForTimestamp = (timestamp) => {
|
|
1587
1628
|
const lastFrameTimestamp = frameTimestamps[frameTimestamps.length - 1];
|
|
1588
1629
|
if (!lastFrameTimestamp) {
|
|
@@ -1600,8 +1641,8 @@ var makeKeyframeBank = ({
|
|
|
1600
1641
|
allocationSize += frame.allocationSize();
|
|
1601
1642
|
lastUsed = Date.now();
|
|
1602
1643
|
};
|
|
1603
|
-
const ensureEnoughFramesForTimestamp = async (
|
|
1604
|
-
while (!hasDecodedEnoughForTimestamp(
|
|
1644
|
+
const ensureEnoughFramesForTimestamp = async (timestampInSeconds) => {
|
|
1645
|
+
while (!hasDecodedEnoughForTimestamp(timestampInSeconds)) {
|
|
1605
1646
|
const sample = await sampleIterator.next();
|
|
1606
1647
|
if (sample.value) {
|
|
1607
1648
|
addFrame(sample.value);
|
|
@@ -1609,6 +1650,10 @@ var makeKeyframeBank = ({
|
|
|
1609
1650
|
if (sample.done) {
|
|
1610
1651
|
break;
|
|
1611
1652
|
}
|
|
1653
|
+
deleteFramesBeforeTimestamp({
|
|
1654
|
+
logLevel: parentLogLevel,
|
|
1655
|
+
timestampInSeconds: timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS
|
|
1656
|
+
});
|
|
1612
1657
|
}
|
|
1613
1658
|
lastUsed = Date.now();
|
|
1614
1659
|
};
|
|
@@ -1643,6 +1688,7 @@ var makeKeyframeBank = ({
|
|
|
1643
1688
|
}
|
|
1644
1689
|
return null;
|
|
1645
1690
|
});
|
|
1691
|
+
let framesDeleted = 0;
|
|
1646
1692
|
for (const frameTimestamp of frameTimestamps) {
|
|
1647
1693
|
if (!frames[frameTimestamp]) {
|
|
1648
1694
|
continue;
|
|
@@ -1650,34 +1696,10 @@ var makeKeyframeBank = ({
|
|
|
1650
1696
|
allocationSize -= frames[frameTimestamp].allocationSize();
|
|
1651
1697
|
frames[frameTimestamp].close();
|
|
1652
1698
|
delete frames[frameTimestamp];
|
|
1699
|
+
framesDeleted++;
|
|
1653
1700
|
}
|
|
1654
1701
|
frameTimestamps.length = 0;
|
|
1655
|
-
|
|
1656
|
-
const deleteFramesBeforeTimestamp = ({
|
|
1657
|
-
logLevel,
|
|
1658
|
-
src,
|
|
1659
|
-
timestampInSeconds
|
|
1660
|
-
}) => {
|
|
1661
|
-
const deletedTimestamps = [];
|
|
1662
|
-
for (const frameTimestamp of frameTimestamps.slice()) {
|
|
1663
|
-
const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
|
|
1664
|
-
if (isLast) {
|
|
1665
|
-
continue;
|
|
1666
|
-
}
|
|
1667
|
-
if (frameTimestamp < timestampInSeconds) {
|
|
1668
|
-
if (!frames[frameTimestamp]) {
|
|
1669
|
-
continue;
|
|
1670
|
-
}
|
|
1671
|
-
allocationSize -= frames[frameTimestamp].allocationSize();
|
|
1672
|
-
frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
|
|
1673
|
-
frames[frameTimestamp].close();
|
|
1674
|
-
delete frames[frameTimestamp];
|
|
1675
|
-
deletedTimestamps.push(frameTimestamp);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
if (deletedTimestamps.length > 0) {
|
|
1679
|
-
Internals8.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)}`);
|
|
1680
|
-
}
|
|
1702
|
+
return { framesDeleted };
|
|
1681
1703
|
};
|
|
1682
1704
|
const getOpenFrameCount = () => {
|
|
1683
1705
|
return {
|
|
@@ -1696,13 +1718,11 @@ var makeKeyframeBank = ({
|
|
|
1696
1718
|
queue = queue.then(() => getFrameFromTimestamp(timestamp));
|
|
1697
1719
|
return queue;
|
|
1698
1720
|
},
|
|
1699
|
-
prepareForDeletion
|
|
1700
|
-
queue = queue.then(() => prepareForDeletion(logLevel));
|
|
1701
|
-
return queue;
|
|
1702
|
-
},
|
|
1721
|
+
prepareForDeletion,
|
|
1703
1722
|
hasTimestampInSecond,
|
|
1704
1723
|
addFrame,
|
|
1705
1724
|
deleteFramesBeforeTimestamp,
|
|
1725
|
+
src,
|
|
1706
1726
|
getOpenFrameCount,
|
|
1707
1727
|
getLastUsed
|
|
1708
1728
|
};
|
|
@@ -1814,7 +1834,8 @@ var getFramesSinceKeyframe = async ({
|
|
|
1814
1834
|
packetSink,
|
|
1815
1835
|
videoSampleSink,
|
|
1816
1836
|
startPacket,
|
|
1817
|
-
logLevel
|
|
1837
|
+
logLevel,
|
|
1838
|
+
src
|
|
1818
1839
|
}) => {
|
|
1819
1840
|
const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
|
|
1820
1841
|
verifyKeyPackets: true
|
|
@@ -1824,7 +1845,8 @@ var getFramesSinceKeyframe = async ({
|
|
|
1824
1845
|
startTimestampInSeconds: startPacket.timestamp,
|
|
1825
1846
|
endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
|
|
1826
1847
|
sampleIterator,
|
|
1827
|
-
logLevel
|
|
1848
|
+
logLevel,
|
|
1849
|
+
src
|
|
1828
1850
|
});
|
|
1829
1851
|
return keyframeBank;
|
|
1830
1852
|
};
|
|
@@ -1876,6 +1898,7 @@ var makeKeyframeManager = () => {
|
|
|
1876
1898
|
const getTheKeyframeBankMostInThePast = async () => {
|
|
1877
1899
|
let mostInThePast = null;
|
|
1878
1900
|
let mostInThePastBank = null;
|
|
1901
|
+
let numberOfBanks = 0;
|
|
1879
1902
|
for (const src in sources) {
|
|
1880
1903
|
for (const b in sources[src]) {
|
|
1881
1904
|
const bank = await sources[src][b];
|
|
@@ -1884,26 +1907,38 @@ var makeKeyframeManager = () => {
|
|
|
1884
1907
|
mostInThePast = lastUsed;
|
|
1885
1908
|
mostInThePastBank = { src, bank };
|
|
1886
1909
|
}
|
|
1910
|
+
numberOfBanks++;
|
|
1887
1911
|
}
|
|
1888
1912
|
}
|
|
1889
1913
|
if (!mostInThePastBank) {
|
|
1890
1914
|
throw new Error("No keyframe bank found");
|
|
1891
1915
|
}
|
|
1892
|
-
return mostInThePastBank;
|
|
1916
|
+
return { mostInThePastBank, numberOfBanks };
|
|
1893
1917
|
};
|
|
1894
1918
|
const deleteOldestKeyframeBank = async (logLevel) => {
|
|
1895
|
-
const {
|
|
1919
|
+
const {
|
|
1920
|
+
mostInThePastBank: { bank: mostInThePastBank, src: mostInThePastSrc },
|
|
1921
|
+
numberOfBanks
|
|
1922
|
+
} = await getTheKeyframeBankMostInThePast();
|
|
1923
|
+
if (numberOfBanks < 2) {
|
|
1924
|
+
return { finish: true };
|
|
1925
|
+
}
|
|
1896
1926
|
if (mostInThePastBank) {
|
|
1897
|
-
|
|
1927
|
+
const { framesDeleted } = mostInThePastBank.prepareForDeletion(logLevel);
|
|
1898
1928
|
delete sources[mostInThePastSrc][mostInThePastBank.startTimestampInSeconds];
|
|
1899
|
-
Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
|
|
1929
|
+
Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted ${framesDeleted} frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
|
|
1900
1930
|
}
|
|
1931
|
+
return { finish: false };
|
|
1901
1932
|
};
|
|
1902
1933
|
const ensureToStayUnderMaxCacheSize = async (logLevel) => {
|
|
1903
1934
|
let cacheStats = await getTotalCacheStats();
|
|
1904
1935
|
const maxCacheSize = getMaxVideoCacheSize(logLevel);
|
|
1905
1936
|
while (cacheStats.totalSize > maxCacheSize) {
|
|
1906
|
-
await deleteOldestKeyframeBank(logLevel);
|
|
1937
|
+
const { finish } = await deleteOldestKeyframeBank(logLevel);
|
|
1938
|
+
if (finish) {
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
Internals9.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));
|
|
1907
1942
|
cacheStats = await getTotalCacheStats();
|
|
1908
1943
|
}
|
|
1909
1944
|
};
|
|
@@ -1921,14 +1956,13 @@ var makeKeyframeManager = () => {
|
|
|
1921
1956
|
const bank = await sources[src][startTimeInSeconds];
|
|
1922
1957
|
const { endTimestampInSeconds, startTimestampInSeconds } = bank;
|
|
1923
1958
|
if (endTimestampInSeconds < threshold) {
|
|
1924
|
-
|
|
1959
|
+
bank.prepareForDeletion(logLevel);
|
|
1925
1960
|
Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `[Video] Cleared frames for src ${src} from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
1926
1961
|
delete sources[src][startTimeInSeconds];
|
|
1927
1962
|
} else {
|
|
1928
1963
|
bank.deleteFramesBeforeTimestamp({
|
|
1929
1964
|
timestampInSeconds: threshold,
|
|
1930
|
-
logLevel
|
|
1931
|
-
src
|
|
1965
|
+
logLevel
|
|
1932
1966
|
});
|
|
1933
1967
|
}
|
|
1934
1968
|
}
|
|
@@ -1958,7 +1992,8 @@ var makeKeyframeManager = () => {
|
|
|
1958
1992
|
packetSink,
|
|
1959
1993
|
videoSampleSink,
|
|
1960
1994
|
startPacket,
|
|
1961
|
-
logLevel
|
|
1995
|
+
logLevel,
|
|
1996
|
+
src
|
|
1962
1997
|
});
|
|
1963
1998
|
addKeyframeBank({ src, bank: newKeyframeBank, startTimestampInSeconds });
|
|
1964
1999
|
return newKeyframeBank;
|
|
@@ -1973,7 +2008,8 @@ var makeKeyframeManager = () => {
|
|
|
1973
2008
|
packetSink,
|
|
1974
2009
|
videoSampleSink,
|
|
1975
2010
|
startPacket,
|
|
1976
|
-
logLevel
|
|
2011
|
+
logLevel,
|
|
2012
|
+
src
|
|
1977
2013
|
});
|
|
1978
2014
|
addKeyframeBank({ src, bank: replacementKeybank, startTimestampInSeconds });
|
|
1979
2015
|
return replacementKeybank;
|
|
@@ -2047,7 +2083,7 @@ var getTotalCacheStats = async () => {
|
|
|
2047
2083
|
};
|
|
2048
2084
|
};
|
|
2049
2085
|
var getUncachedMaxCacheSize = (logLevel) => {
|
|
2050
|
-
if (window.remotion_mediaCacheSizeInBytes !== undefined && window.remotion_mediaCacheSizeInBytes !== null) {
|
|
2086
|
+
if (typeof window !== "undefined" && window.remotion_mediaCacheSizeInBytes !== undefined && window.remotion_mediaCacheSizeInBytes !== null) {
|
|
2051
2087
|
if (window.remotion_mediaCacheSizeInBytes < 240 * 1024 * 1024) {
|
|
2052
2088
|
cancelRender(new Error(`The minimum value for the "mediaCacheSizeInBytes" prop is 240MB (${240 * 1024 * 1024}), got: ${window.remotion_mediaCacheSizeInBytes}`));
|
|
2053
2089
|
}
|
|
@@ -2057,7 +2093,7 @@ var getUncachedMaxCacheSize = (logLevel) => {
|
|
|
2057
2093
|
Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Using cache size set using "mediaCacheSizeInBytes": ${(window.remotion_mediaCacheSizeInBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
2058
2094
|
return window.remotion_mediaCacheSizeInBytes;
|
|
2059
2095
|
}
|
|
2060
|
-
if (window.remotion_initialMemoryAvailable !== undefined && window.remotion_initialMemoryAvailable !== null) {
|
|
2096
|
+
if (typeof window !== "undefined" && window.remotion_initialMemoryAvailable !== undefined && window.remotion_initialMemoryAvailable !== null) {
|
|
2061
2097
|
const value = window.remotion_initialMemoryAvailable / 2;
|
|
2062
2098
|
if (value < 500 * 1024 * 1024) {
|
|
2063
2099
|
Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Using cache size set based on minimum value of 500MB (which is more than half of the available system memory!)`);
|
|
@@ -2431,7 +2467,7 @@ var extractFrameAndAudio = async ({
|
|
|
2431
2467
|
};
|
|
2432
2468
|
|
|
2433
2469
|
// src/video-extraction/extract-frame-via-broadcast-channel.ts
|
|
2434
|
-
if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
|
|
2470
|
+
if (typeof window !== "undefined" && window.remotion_broadcastChannel && window.remotion_isMainTab) {
|
|
2435
2471
|
window.remotion_broadcastChannel.addEventListener("message", async (event) => {
|
|
2436
2472
|
const data = event.data;
|
|
2437
2473
|
if (data.type === "request") {
|
|
@@ -2639,7 +2675,7 @@ var AudioForRendering = ({
|
|
|
2639
2675
|
loopVolumeCurveBehavior,
|
|
2640
2676
|
delayRenderRetries,
|
|
2641
2677
|
delayRenderTimeoutInMilliseconds,
|
|
2642
|
-
logLevel
|
|
2678
|
+
logLevel,
|
|
2643
2679
|
loop,
|
|
2644
2680
|
fallbackHtml5AudioProps,
|
|
2645
2681
|
audioStreamIndex,
|
|
@@ -2697,7 +2733,7 @@ var AudioForRendering = ({
|
|
|
2697
2733
|
timeInSeconds: timestamp,
|
|
2698
2734
|
durationInSeconds,
|
|
2699
2735
|
playbackRate: playbackRate ?? 1,
|
|
2700
|
-
logLevel,
|
|
2736
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
2701
2737
|
includeAudio: shouldRenderAudio,
|
|
2702
2738
|
includeVideo: false,
|
|
2703
2739
|
isClientSideRendering: environment.isClientSideRendering,
|
|
@@ -2711,7 +2747,10 @@ var AudioForRendering = ({
|
|
|
2711
2747
|
if (disallowFallbackToHtml5Audio) {
|
|
2712
2748
|
cancelRender2(new Error(`Unknown container format ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
|
|
2713
2749
|
}
|
|
2714
|
-
Internals12.Log.warn({
|
|
2750
|
+
Internals12.Log.warn({
|
|
2751
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
2752
|
+
tag: "@remotion/media"
|
|
2753
|
+
}, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
|
|
2715
2754
|
setReplaceWithHtml5Audio(true);
|
|
2716
2755
|
return;
|
|
2717
2756
|
}
|
|
@@ -2719,7 +2758,10 @@ var AudioForRendering = ({
|
|
|
2719
2758
|
if (disallowFallbackToHtml5Audio) {
|
|
2720
2759
|
cancelRender2(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
|
|
2721
2760
|
}
|
|
2722
|
-
Internals12.Log.warn({
|
|
2761
|
+
Internals12.Log.warn({
|
|
2762
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
2763
|
+
tag: "@remotion/media"
|
|
2764
|
+
}, `Cannot decode ${src}, falling back to <Html5Audio>`);
|
|
2723
2765
|
setReplaceWithHtml5Audio(true);
|
|
2724
2766
|
return;
|
|
2725
2767
|
}
|
|
@@ -2730,7 +2772,10 @@ var AudioForRendering = ({
|
|
|
2730
2772
|
if (disallowFallbackToHtml5Audio) {
|
|
2731
2773
|
cancelRender2(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
|
|
2732
2774
|
}
|
|
2733
|
-
Internals12.Log.warn({
|
|
2775
|
+
Internals12.Log.warn({
|
|
2776
|
+
logLevel: logLevel ?? window.remotion_logLevel,
|
|
2777
|
+
tag: "@remotion/media"
|
|
2778
|
+
}, `Network error fetching ${src}, falling back to <Html5Audio>`);
|
|
2734
2779
|
setReplaceWithHtml5Audio(true);
|
|
2735
2780
|
return;
|
|
2736
2781
|
}
|
|
@@ -3168,26 +3213,6 @@ import {
|
|
|
3168
3213
|
useRemotionEnvironment as useRemotionEnvironment3,
|
|
3169
3214
|
useVideoConfig as useVideoConfig2
|
|
3170
3215
|
} from "remotion";
|
|
3171
|
-
|
|
3172
|
-
// ../core/src/calculate-media-duration.ts
|
|
3173
|
-
var calculateMediaDuration = ({
|
|
3174
|
-
trimAfter,
|
|
3175
|
-
mediaDurationInFrames,
|
|
3176
|
-
playbackRate,
|
|
3177
|
-
trimBefore
|
|
3178
|
-
}) => {
|
|
3179
|
-
let duration = mediaDurationInFrames;
|
|
3180
|
-
if (typeof trimAfter !== "undefined") {
|
|
3181
|
-
duration = trimAfter;
|
|
3182
|
-
}
|
|
3183
|
-
if (typeof trimBefore !== "undefined") {
|
|
3184
|
-
duration -= trimBefore;
|
|
3185
|
-
}
|
|
3186
|
-
const actualDuration = duration / playbackRate;
|
|
3187
|
-
return Math.floor(actualDuration);
|
|
3188
|
-
};
|
|
3189
|
-
|
|
3190
|
-
// src/video/video-for-rendering.tsx
|
|
3191
3216
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
3192
3217
|
var VideoForRendering = ({
|
|
3193
3218
|
volume: volumeProp,
|
|
@@ -3230,6 +3255,8 @@ var VideoForRendering = ({
|
|
|
3230
3255
|
const { delayRender, continueRender } = useDelayRender2();
|
|
3231
3256
|
const canvasRef = useRef3(null);
|
|
3232
3257
|
const [replaceWithOffthreadVideo, setReplaceWithOffthreadVideo] = useState5(false);
|
|
3258
|
+
const audioEnabled = Internals15.useAudioEnabled();
|
|
3259
|
+
const videoEnabled = Internals15.useVideoEnabled();
|
|
3233
3260
|
useLayoutEffect2(() => {
|
|
3234
3261
|
if (!canvasRef.current) {
|
|
3235
3262
|
return;
|
|
@@ -3244,7 +3271,7 @@ var VideoForRendering = ({
|
|
|
3244
3271
|
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? undefined
|
|
3245
3272
|
});
|
|
3246
3273
|
const shouldRenderAudio = (() => {
|
|
3247
|
-
if (!
|
|
3274
|
+
if (!audioEnabled) {
|
|
3248
3275
|
return false;
|
|
3249
3276
|
}
|
|
3250
3277
|
if (muted) {
|
|
@@ -3259,7 +3286,7 @@ var VideoForRendering = ({
|
|
|
3259
3286
|
playbackRate,
|
|
3260
3287
|
logLevel,
|
|
3261
3288
|
includeAudio: shouldRenderAudio,
|
|
3262
|
-
includeVideo:
|
|
3289
|
+
includeVideo: videoEnabled,
|
|
3263
3290
|
isClientSideRendering: environment.isClientSideRendering,
|
|
3264
3291
|
loop,
|
|
3265
3292
|
audioStreamIndex,
|
|
@@ -3329,7 +3356,7 @@ var VideoForRendering = ({
|
|
|
3329
3356
|
context.canvas.style.aspectRatio = `${context.canvas.width} / ${context.canvas.height}`;
|
|
3330
3357
|
context.drawImage(imageBitmap, 0, 0);
|
|
3331
3358
|
imageBitmap.close();
|
|
3332
|
-
} else if (
|
|
3359
|
+
} else if (videoEnabled) {
|
|
3333
3360
|
const context = canvasRef.current?.getContext("2d", {
|
|
3334
3361
|
alpha: true
|
|
3335
3362
|
});
|
|
@@ -3397,7 +3424,9 @@ var VideoForRendering = ({
|
|
|
3397
3424
|
disallowFallbackToOffthreadVideo,
|
|
3398
3425
|
toneFrequency,
|
|
3399
3426
|
trimAfterValue,
|
|
3400
|
-
trimBeforeValue
|
|
3427
|
+
trimBeforeValue,
|
|
3428
|
+
audioEnabled,
|
|
3429
|
+
videoEnabled
|
|
3401
3430
|
]);
|
|
3402
3431
|
const classNameValue = useMemo5(() => {
|
|
3403
3432
|
return [Internals15.OBJECTFIT_CONTAIN_CLASS_NAME, className].filter(Internals15.truthy).join(" ");
|
|
@@ -3443,7 +3472,7 @@ var VideoForRendering = ({
|
|
|
3443
3472
|
}
|
|
3444
3473
|
return /* @__PURE__ */ jsx5(Loop, {
|
|
3445
3474
|
layout: "none",
|
|
3446
|
-
durationInFrames: calculateMediaDuration({
|
|
3475
|
+
durationInFrames: Internals15.calculateMediaDuration({
|
|
3447
3476
|
trimAfter: trimAfterValue,
|
|
3448
3477
|
mediaDurationInFrames: replaceWithOffthreadVideo.durationInSeconds * fps,
|
|
3449
3478
|
playbackRate,
|
|
@@ -3579,7 +3608,7 @@ var Video = ({
|
|
|
3579
3608
|
delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null,
|
|
3580
3609
|
disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false,
|
|
3581
3610
|
fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {},
|
|
3582
|
-
logLevel: logLevel ?? window.remotion_logLevel,
|
|
3611
|
+
logLevel: logLevel ?? (typeof window !== "undefined" ? window.remotion_logLevel : "info"),
|
|
3583
3612
|
loop: loop ?? false,
|
|
3584
3613
|
loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? "repeat",
|
|
3585
3614
|
muted: muted ?? false,
|
|
@@ -45,6 +45,7 @@ export declare class MediaPlayer {
|
|
|
45
45
|
private audioBufferHealth;
|
|
46
46
|
private audioIteratorStarted;
|
|
47
47
|
private readonly HEALTHY_BUFER_THRESHOLD_SECONDS;
|
|
48
|
+
private mediaEnded;
|
|
48
49
|
private onVideoFrameCallback?;
|
|
49
50
|
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, audioStreamIndex, fps, }: {
|
|
50
51
|
canvas: HTMLCanvasElement | null;
|
|
@@ -2,7 +2,7 @@ import { ALL_FORMATS, AudioBufferSink, CanvasSink, Input, UrlSource, } from 'med
|
|
|
2
2
|
import { Internals } from 'remotion';
|
|
3
3
|
import { getTimeInSeconds } from '../get-time-in-seconds';
|
|
4
4
|
import { isNetworkError } from '../is-network-error';
|
|
5
|
-
import { sleep, withTimeout } from './timeout-utils';
|
|
5
|
+
import { sleep, TimeoutError, withTimeout } from './timeout-utils';
|
|
6
6
|
export const SEEK_THRESHOLD = 0.05;
|
|
7
7
|
const AUDIO_BUFFER_TOLERANCE_THRESHOLD = 0.1;
|
|
8
8
|
export class MediaPlayer {
|
|
@@ -30,6 +30,7 @@ export class MediaPlayer {
|
|
|
30
30
|
this.audioBufferHealth = 0;
|
|
31
31
|
this.audioIteratorStarted = false;
|
|
32
32
|
this.HEALTHY_BUFER_THRESHOLD_SECONDS = 1;
|
|
33
|
+
this.mediaEnded = false;
|
|
33
34
|
this.input = null;
|
|
34
35
|
this.render = () => {
|
|
35
36
|
if (this.isBuffering) {
|
|
@@ -100,6 +101,7 @@ export class MediaPlayer {
|
|
|
100
101
|
while (true) {
|
|
101
102
|
const newNextFrame = (await this.videoFrameIterator.next()).value ?? null;
|
|
102
103
|
if (!newNextFrame) {
|
|
104
|
+
this.mediaEnded = true;
|
|
103
105
|
break;
|
|
104
106
|
}
|
|
105
107
|
const playbackTime = this.getPlaybackTime();
|
|
@@ -138,12 +140,16 @@ export class MediaPlayer {
|
|
|
138
140
|
try {
|
|
139
141
|
result = await withTimeout(this.audioBufferIterator.next(), BUFFERING_TIMEOUT_MS, 'Iterator timeout');
|
|
140
142
|
}
|
|
141
|
-
catch {
|
|
142
|
-
this.
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error instanceof TimeoutError && !this.mediaEnded) {
|
|
145
|
+
this.setBufferingState(true);
|
|
146
|
+
}
|
|
143
147
|
await sleep(10);
|
|
144
148
|
continue;
|
|
145
149
|
}
|
|
150
|
+
// media has ended
|
|
146
151
|
if (result.done || !result.value) {
|
|
152
|
+
this.mediaEnded = true;
|
|
147
153
|
break;
|
|
148
154
|
}
|
|
149
155
|
const { buffer, timestamp, duration } = result.value;
|
|
@@ -337,6 +343,9 @@ export class MediaPlayer {
|
|
|
337
343
|
src: this.src,
|
|
338
344
|
});
|
|
339
345
|
if (newTime === null) {
|
|
346
|
+
// invalidate in-flight video operations
|
|
347
|
+
this.videoAsyncId++;
|
|
348
|
+
this.nextFrame = null;
|
|
340
349
|
this.clearCanvas();
|
|
341
350
|
await this.cleanAudioIteratorAndNodes();
|
|
342
351
|
return;
|
|
@@ -347,6 +356,7 @@ export class MediaPlayer {
|
|
|
347
356
|
if (isSignificantSeek) {
|
|
348
357
|
this.nextFrame = null;
|
|
349
358
|
this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
|
|
359
|
+
this.mediaEnded = false;
|
|
350
360
|
if (this.audioSink) {
|
|
351
361
|
await this.cleanAudioIteratorAndNodes();
|
|
352
362
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/* eslint-disable no-promise-executor-return */
|
|
2
2
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
export class TimeoutError extends Error {
|
|
4
|
+
constructor(message = 'Operation timed out') {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'TimeoutError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
3
9
|
export function withTimeout(promise, timeoutMs, errorMessage = 'Operation timed out') {
|
|
4
10
|
let timeoutId = null;
|
|
5
11
|
const timeoutPromise = new Promise((_, reject) => {
|
|
6
12
|
timeoutId = window.setTimeout(() => {
|
|
7
|
-
reject(new
|
|
13
|
+
reject(new TimeoutError(errorMessage));
|
|
8
14
|
}, timeoutMs);
|
|
9
15
|
});
|
|
10
16
|
return Promise.race([
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useContext, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { cancelRender, Internals, Loop, random, useCurrentFrame, useDelayRender, useRemotionEnvironment, useVideoConfig, } from 'remotion';
|
|
4
|
-
import { calculateMediaDuration } from '../../../core/src/calculate-media-duration';
|
|
5
4
|
import { applyVolume } from '../convert-audiodata/apply-volume';
|
|
6
5
|
import { TARGET_SAMPLE_RATE } from '../convert-audiodata/resample-audiodata';
|
|
7
6
|
import { frameForVolumeProp } from '../looped-frame';
|
|
@@ -28,6 +27,8 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
28
27
|
const { delayRender, continueRender } = useDelayRender();
|
|
29
28
|
const canvasRef = useRef(null);
|
|
30
29
|
const [replaceWithOffthreadVideo, setReplaceWithOffthreadVideo] = useState(false);
|
|
30
|
+
const audioEnabled = Internals.useAudioEnabled();
|
|
31
|
+
const videoEnabled = Internals.useVideoEnabled();
|
|
31
32
|
useLayoutEffect(() => {
|
|
32
33
|
if (!canvasRef.current) {
|
|
33
34
|
return;
|
|
@@ -42,7 +43,7 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
42
43
|
timeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? undefined,
|
|
43
44
|
});
|
|
44
45
|
const shouldRenderAudio = (() => {
|
|
45
|
-
if (!
|
|
46
|
+
if (!audioEnabled) {
|
|
46
47
|
return false;
|
|
47
48
|
}
|
|
48
49
|
if (muted) {
|
|
@@ -57,7 +58,7 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
57
58
|
playbackRate,
|
|
58
59
|
logLevel,
|
|
59
60
|
includeAudio: shouldRenderAudio,
|
|
60
|
-
includeVideo:
|
|
61
|
+
includeVideo: videoEnabled,
|
|
61
62
|
isClientSideRendering: environment.isClientSideRendering,
|
|
62
63
|
loop,
|
|
63
64
|
audioStreamIndex,
|
|
@@ -131,8 +132,7 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
131
132
|
context.drawImage(imageBitmap, 0, 0);
|
|
132
133
|
imageBitmap.close();
|
|
133
134
|
}
|
|
134
|
-
else if (
|
|
135
|
-
// In the case of https://discord.com/channels/809501355504959528/809501355504959531/1424400511070765086
|
|
135
|
+
else if (videoEnabled) {
|
|
136
136
|
// A video that only starts at time 0.033sec
|
|
137
137
|
// we shall not crash here but clear the canvas
|
|
138
138
|
const context = canvasRef.current?.getContext('2d', {
|
|
@@ -204,6 +204,8 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
204
204
|
toneFrequency,
|
|
205
205
|
trimAfterValue,
|
|
206
206
|
trimBeforeValue,
|
|
207
|
+
audioEnabled,
|
|
208
|
+
videoEnabled,
|
|
207
209
|
]);
|
|
208
210
|
const classNameValue = useMemo(() => {
|
|
209
211
|
return [Internals.OBJECTFIT_CONTAIN_CLASS_NAME, className]
|
|
@@ -218,7 +220,7 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
|
|
|
218
220
|
if (!replaceWithOffthreadVideo.durationInSeconds) {
|
|
219
221
|
cancelRender(new Error(`Cannot render video ${src}: @remotion/media was unable to render, and fell back to <OffthreadVideo>. Also, "loop" was set, but <OffthreadVideo> does not support looping and @remotion/media could also not determine the duration of the video.`));
|
|
220
222
|
}
|
|
221
|
-
return (_jsx(Loop, { layout: "none", durationInFrames: calculateMediaDuration({
|
|
223
|
+
return (_jsx(Loop, { layout: "none", durationInFrames: Internals.calculateMediaDuration({
|
|
222
224
|
trimAfter: trimAfterValue,
|
|
223
225
|
mediaDurationInFrames: replaceWithOffthreadVideo.durationInSeconds * fps,
|
|
224
226
|
playbackRate,
|
package/dist/video/video.js
CHANGED
|
@@ -27,6 +27,7 @@ const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, dela
|
|
|
27
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 }));
|
|
28
28
|
};
|
|
29
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, }) => {
|
|
30
|
-
return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ??
|
|
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' ? window.remotion_logLevel : '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 }));
|
|
31
32
|
};
|
|
32
33
|
Internals.addSequenceStackTraces(Video);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { extractFrameAndAudio } from '../extract-frame-and-audio';
|
|
2
2
|
// Doesn't exist in studio
|
|
3
|
-
if (window
|
|
3
|
+
if (typeof window !== 'undefined' &&
|
|
4
|
+
window.remotion_broadcastChannel &&
|
|
5
|
+
window.remotion_isMainTab) {
|
|
4
6
|
window.remotion_broadcastChannel.addEventListener('message', async (event) => {
|
|
5
7
|
const data = event.data;
|
|
6
8
|
if (data.type === 'request') {
|
|
@@ -21,10 +21,11 @@ export declare const getSinks: (src: string) => Promise<{
|
|
|
21
21
|
getDuration: () => Promise<number>;
|
|
22
22
|
}>;
|
|
23
23
|
export type GetSink = Awaited<ReturnType<typeof getSinks>>;
|
|
24
|
-
export declare const getFramesSinceKeyframe: ({ packetSink, videoSampleSink, startPacket, logLevel, }: {
|
|
24
|
+
export declare const getFramesSinceKeyframe: ({ packetSink, videoSampleSink, startPacket, logLevel, src, }: {
|
|
25
25
|
packetSink: EncodedPacketSink;
|
|
26
26
|
videoSampleSink: VideoSampleSink;
|
|
27
27
|
startPacket: EncodedPacket;
|
|
28
28
|
logLevel: LogLevel;
|
|
29
|
+
src: string;
|
|
29
30
|
}) => Promise<import("./keyframe-bank").KeyframeBank>;
|
|
30
31
|
export {};
|
|
@@ -82,7 +82,7 @@ export const getSinks = async (src) => {
|
|
|
82
82
|
},
|
|
83
83
|
};
|
|
84
84
|
};
|
|
85
|
-
export const getFramesSinceKeyframe = async ({ packetSink, videoSampleSink, startPacket, logLevel, }) => {
|
|
85
|
+
export const getFramesSinceKeyframe = async ({ packetSink, videoSampleSink, startPacket, logLevel, src, }) => {
|
|
86
86
|
const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
|
|
87
87
|
verifyKeyPackets: true,
|
|
88
88
|
});
|
|
@@ -92,6 +92,7 @@ export const getFramesSinceKeyframe = async ({ packetSink, videoSampleSink, star
|
|
|
92
92
|
endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
|
|
93
93
|
sampleIterator,
|
|
94
94
|
logLevel,
|
|
95
|
+
src,
|
|
95
96
|
});
|
|
96
97
|
return keyframeBank;
|
|
97
98
|
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { VideoSample } from 'mediabunny';
|
|
2
2
|
import { type LogLevel } from 'remotion';
|
|
3
3
|
export type KeyframeBank = {
|
|
4
|
+
src: string;
|
|
4
5
|
startTimestampInSeconds: number;
|
|
5
6
|
endTimestampInSeconds: number;
|
|
6
7
|
getFrameFromTimestamp: (timestamp: number) => Promise<VideoSample | null>;
|
|
7
|
-
prepareForDeletion: (logLevel: LogLevel) =>
|
|
8
|
-
|
|
8
|
+
prepareForDeletion: (logLevel: LogLevel) => {
|
|
9
|
+
framesDeleted: number;
|
|
10
|
+
};
|
|
11
|
+
deleteFramesBeforeTimestamp: ({ logLevel, timestampInSeconds, }: {
|
|
9
12
|
timestampInSeconds: number;
|
|
10
13
|
logLevel: LogLevel;
|
|
11
|
-
src: string;
|
|
12
14
|
}) => void;
|
|
13
15
|
hasTimestampInSecond: (timestamp: number) => Promise<boolean>;
|
|
14
16
|
addFrame: (frame: VideoSample) => void;
|
|
@@ -18,9 +20,10 @@ export type KeyframeBank = {
|
|
|
18
20
|
};
|
|
19
21
|
getLastUsed: () => number;
|
|
20
22
|
};
|
|
21
|
-
export declare const makeKeyframeBank: ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, }: {
|
|
23
|
+
export declare const makeKeyframeBank: ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, src, }: {
|
|
22
24
|
startTimestampInSeconds: number;
|
|
23
25
|
endTimestampInSeconds: number;
|
|
24
26
|
sampleIterator: AsyncGenerator<VideoSample, void, unknown>;
|
|
25
27
|
logLevel: LogLevel;
|
|
28
|
+
src: string;
|
|
26
29
|
}) => KeyframeBank;
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { Internals } from 'remotion';
|
|
2
|
+
import { SAFE_BACK_WINDOW_IN_SECONDS } from '../caches';
|
|
2
3
|
import { renderTimestampRange } from '../render-timestamp-range';
|
|
3
4
|
// Round to only 4 digits, because WebM has a timescale of 1_000, e.g. framer.webm
|
|
4
5
|
const roundTo4Digits = (timestamp) => {
|
|
5
6
|
return Math.round(timestamp * 1000) / 1000;
|
|
6
7
|
};
|
|
7
|
-
export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, }) => {
|
|
8
|
+
export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, src, }) => {
|
|
8
9
|
Internals.Log.verbose({ logLevel: parentLogLevel, tag: '@remotion/media' }, `Creating keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
9
10
|
const frames = {};
|
|
10
11
|
const frameTimestamps = [];
|
|
11
12
|
let lastUsed = Date.now();
|
|
12
13
|
let allocationSize = 0;
|
|
14
|
+
const deleteFramesBeforeTimestamp = ({ logLevel, timestampInSeconds, }) => {
|
|
15
|
+
const deletedTimestamps = [];
|
|
16
|
+
for (const frameTimestamp of frameTimestamps.slice()) {
|
|
17
|
+
const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
|
|
18
|
+
// Don't delete the last frame, since it may be the last one in the video!
|
|
19
|
+
if (isLast) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (frameTimestamp < timestampInSeconds) {
|
|
23
|
+
if (!frames[frameTimestamp]) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
allocationSize -= frames[frameTimestamp].allocationSize();
|
|
27
|
+
frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
|
|
28
|
+
frames[frameTimestamp].close();
|
|
29
|
+
delete frames[frameTimestamp];
|
|
30
|
+
deletedTimestamps.push(frameTimestamp);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (deletedTimestamps.length > 0) {
|
|
34
|
+
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)}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
13
37
|
const hasDecodedEnoughForTimestamp = (timestamp) => {
|
|
14
38
|
const lastFrameTimestamp = frameTimestamps[frameTimestamps.length - 1];
|
|
15
39
|
if (!lastFrameTimestamp) {
|
|
@@ -29,8 +53,8 @@ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSecond
|
|
|
29
53
|
allocationSize += frame.allocationSize();
|
|
30
54
|
lastUsed = Date.now();
|
|
31
55
|
};
|
|
32
|
-
const ensureEnoughFramesForTimestamp = async (
|
|
33
|
-
while (!hasDecodedEnoughForTimestamp(
|
|
56
|
+
const ensureEnoughFramesForTimestamp = async (timestampInSeconds) => {
|
|
57
|
+
while (!hasDecodedEnoughForTimestamp(timestampInSeconds)) {
|
|
34
58
|
const sample = await sampleIterator.next();
|
|
35
59
|
if (sample.value) {
|
|
36
60
|
addFrame(sample.value);
|
|
@@ -38,6 +62,10 @@ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSecond
|
|
|
38
62
|
if (sample.done) {
|
|
39
63
|
break;
|
|
40
64
|
}
|
|
65
|
+
deleteFramesBeforeTimestamp({
|
|
66
|
+
logLevel: parentLogLevel,
|
|
67
|
+
timestampInSeconds: timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS,
|
|
68
|
+
});
|
|
41
69
|
}
|
|
42
70
|
lastUsed = Date.now();
|
|
43
71
|
};
|
|
@@ -77,6 +105,7 @@ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSecond
|
|
|
77
105
|
}
|
|
78
106
|
return null;
|
|
79
107
|
});
|
|
108
|
+
let framesDeleted = 0;
|
|
80
109
|
for (const frameTimestamp of frameTimestamps) {
|
|
81
110
|
if (!frames[frameTimestamp]) {
|
|
82
111
|
continue;
|
|
@@ -84,31 +113,10 @@ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSecond
|
|
|
84
113
|
allocationSize -= frames[frameTimestamp].allocationSize();
|
|
85
114
|
frames[frameTimestamp].close();
|
|
86
115
|
delete frames[frameTimestamp];
|
|
116
|
+
framesDeleted++;
|
|
87
117
|
}
|
|
88
118
|
frameTimestamps.length = 0;
|
|
89
|
-
|
|
90
|
-
const deleteFramesBeforeTimestamp = ({ logLevel, src, timestampInSeconds, }) => {
|
|
91
|
-
const deletedTimestamps = [];
|
|
92
|
-
for (const frameTimestamp of frameTimestamps.slice()) {
|
|
93
|
-
const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
|
|
94
|
-
// Don't delete the last frame, since it may be the last one in the video!
|
|
95
|
-
if (isLast) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (frameTimestamp < timestampInSeconds) {
|
|
99
|
-
if (!frames[frameTimestamp]) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
allocationSize -= frames[frameTimestamp].allocationSize();
|
|
103
|
-
frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
|
|
104
|
-
frames[frameTimestamp].close();
|
|
105
|
-
delete frames[frameTimestamp];
|
|
106
|
-
deletedTimestamps.push(frameTimestamp);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (deletedTimestamps.length > 0) {
|
|
110
|
-
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)}`);
|
|
111
|
-
}
|
|
119
|
+
return { framesDeleted };
|
|
112
120
|
};
|
|
113
121
|
const getOpenFrameCount = () => {
|
|
114
122
|
return {
|
|
@@ -127,13 +135,11 @@ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSecond
|
|
|
127
135
|
queue = queue.then(() => getFrameFromTimestamp(timestamp));
|
|
128
136
|
return queue;
|
|
129
137
|
},
|
|
130
|
-
prepareForDeletion
|
|
131
|
-
queue = queue.then(() => prepareForDeletion(logLevel));
|
|
132
|
-
return queue;
|
|
133
|
-
},
|
|
138
|
+
prepareForDeletion,
|
|
134
139
|
hasTimestampInSecond,
|
|
135
140
|
addFrame,
|
|
136
141
|
deleteFramesBeforeTimestamp,
|
|
142
|
+
src,
|
|
137
143
|
getOpenFrameCount,
|
|
138
144
|
getLastUsed,
|
|
139
145
|
};
|
|
@@ -46,6 +46,7 @@ export const makeKeyframeManager = () => {
|
|
|
46
46
|
const getTheKeyframeBankMostInThePast = async () => {
|
|
47
47
|
let mostInThePast = null;
|
|
48
48
|
let mostInThePastBank = null;
|
|
49
|
+
let numberOfBanks = 0;
|
|
49
50
|
for (const src in sources) {
|
|
50
51
|
for (const b in sources[src]) {
|
|
51
52
|
const bank = await sources[src][b];
|
|
@@ -54,26 +55,35 @@ export const makeKeyframeManager = () => {
|
|
|
54
55
|
mostInThePast = lastUsed;
|
|
55
56
|
mostInThePastBank = { src, bank };
|
|
56
57
|
}
|
|
58
|
+
numberOfBanks++;
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
if (!mostInThePastBank) {
|
|
60
62
|
throw new Error('No keyframe bank found');
|
|
61
63
|
}
|
|
62
|
-
return mostInThePastBank;
|
|
64
|
+
return { mostInThePastBank, numberOfBanks };
|
|
63
65
|
};
|
|
64
66
|
const deleteOldestKeyframeBank = async (logLevel) => {
|
|
65
|
-
const { bank: mostInThePastBank, src: mostInThePastSrc } = await getTheKeyframeBankMostInThePast();
|
|
67
|
+
const { mostInThePastBank: { bank: mostInThePastBank, src: mostInThePastSrc }, numberOfBanks, } = await getTheKeyframeBankMostInThePast();
|
|
68
|
+
if (numberOfBanks < 2) {
|
|
69
|
+
return { finish: true };
|
|
70
|
+
}
|
|
66
71
|
if (mostInThePastBank) {
|
|
67
|
-
|
|
72
|
+
const { framesDeleted } = mostInThePastBank.prepareForDeletion(logLevel);
|
|
68
73
|
delete sources[mostInThePastSrc][mostInThePastBank.startTimestampInSeconds];
|
|
69
|
-
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Deleted frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
|
|
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.`);
|
|
70
75
|
}
|
|
76
|
+
return { finish: false };
|
|
71
77
|
};
|
|
72
78
|
const ensureToStayUnderMaxCacheSize = async (logLevel) => {
|
|
73
79
|
let cacheStats = await getTotalCacheStats();
|
|
74
80
|
const maxCacheSize = getMaxVideoCacheSize(logLevel);
|
|
75
81
|
while (cacheStats.totalSize > maxCacheSize) {
|
|
76
|
-
await deleteOldestKeyframeBank(logLevel);
|
|
82
|
+
const { finish } = await deleteOldestKeyframeBank(logLevel);
|
|
83
|
+
if (finish) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
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));
|
|
77
87
|
cacheStats = await getTotalCacheStats();
|
|
78
88
|
}
|
|
79
89
|
};
|
|
@@ -87,7 +97,7 @@ export const makeKeyframeManager = () => {
|
|
|
87
97
|
const bank = await sources[src][startTimeInSeconds];
|
|
88
98
|
const { endTimestampInSeconds, startTimestampInSeconds } = bank;
|
|
89
99
|
if (endTimestampInSeconds < threshold) {
|
|
90
|
-
|
|
100
|
+
bank.prepareForDeletion(logLevel);
|
|
91
101
|
Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `[Video] Cleared frames for src ${src} from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
|
|
92
102
|
delete sources[src][startTimeInSeconds];
|
|
93
103
|
}
|
|
@@ -95,7 +105,6 @@ export const makeKeyframeManager = () => {
|
|
|
95
105
|
bank.deleteFramesBeforeTimestamp({
|
|
96
106
|
timestampInSeconds: threshold,
|
|
97
107
|
logLevel,
|
|
98
|
-
src,
|
|
99
108
|
});
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -124,6 +133,7 @@ export const makeKeyframeManager = () => {
|
|
|
124
133
|
videoSampleSink,
|
|
125
134
|
startPacket,
|
|
126
135
|
logLevel,
|
|
136
|
+
src,
|
|
127
137
|
});
|
|
128
138
|
addKeyframeBank({ src, bank: newKeyframeBank, startTimestampInSeconds });
|
|
129
139
|
return newKeyframeBank;
|
|
@@ -143,6 +153,7 @@ export const makeKeyframeManager = () => {
|
|
|
143
153
|
videoSampleSink,
|
|
144
154
|
startPacket,
|
|
145
155
|
logLevel,
|
|
156
|
+
src,
|
|
146
157
|
});
|
|
147
158
|
addKeyframeBank({ src, bank: replacementKeybank, startTimestampInSeconds });
|
|
148
159
|
return replacementKeybank;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotion/media",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.363",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"mediabunny": "1.23.0",
|
|
25
|
-
"remotion": "4.0.
|
|
25
|
+
"remotion": "4.0.363",
|
|
26
26
|
"webdriverio": "9.19.2"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"react-dom": ">=16.8.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
33
|
+
"@remotion/eslint-config-internal": "4.0.363",
|
|
34
34
|
"@vitest/browser": "^3.2.4",
|
|
35
35
|
"eslint": "9.19.0",
|
|
36
36
|
"react": "19.0.0",
|