@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.
@@ -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 = window.remotion_logLevel, loop, fallbackHtml5AudioProps, audioStreamIndex, showInTimeline, style, name, disallowFallbackToHtml5Audio, toneFrequency, trimAfter, trimBefore, }) => {
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({ logLevel, tag: '@remotion/media' }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
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({ logLevel, tag: '@remotion/media' }, `Cannot decode ${src}, falling back to <Html5Audio>`);
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({ logLevel, tag: '@remotion/media' }, `Network error fetching ${src}, falling back to <Html5Audio>`);
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
  }
@@ -17,5 +17,4 @@ export const Audio = (props) => {
17
17
  }
18
18
  return _jsx(AudioForPreview, { name: name, ...otherProps, stack: stack ?? null });
19
19
  };
20
- // TODO: Doesn't work
21
20
  Internals.addSequenceStackTraces(Audio);
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.remotion_mediaCacheSizeInBytes !== undefined &&
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.remotion_initialMemoryAvailable !== undefined &&
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) {
@@ -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 Error(errorMessage));
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.setBufferingState(true);
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 (timestamp) => {
1604
- while (!hasDecodedEnoughForTimestamp(timestamp)) {
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: (logLevel) => {
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 { bank: mostInThePastBank, src: mostInThePastSrc } = await getTheKeyframeBankMostInThePast();
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
- await mostInThePastBank.prepareForDeletion(logLevel);
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
- await bank.prepareForDeletion(logLevel);
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 = window.remotion_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({ logLevel, tag: "@remotion/media" }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
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({ logLevel, tag: "@remotion/media" }, `Cannot decode ${src}, falling back to <Html5Audio>`);
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({ logLevel, tag: "@remotion/media" }, `Network error fetching ${src}, falling back to <Html5Audio>`);
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 (!window.remotion_audioEnabled) {
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: window.remotion_videoEnabled,
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 (window.remotion_videoEnabled) {
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.setBufferingState(true);
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,2 +1,5 @@
1
1
  export declare const sleep: (ms: number) => Promise<unknown>;
2
+ export declare class TimeoutError extends Error {
3
+ constructor(message?: string);
4
+ }
2
5
  export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage?: string): Promise<T>;
@@ -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 Error(errorMessage));
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 (!window.remotion_audioEnabled) {
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: window.remotion_videoEnabled,
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 (window.remotion_videoEnabled) {
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,
@@ -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 ?? window.remotion_logLevel, 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 }));
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.remotion_broadcastChannel && window.remotion_isMainTab) {
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) => void;
8
- deleteFramesBeforeTimestamp: ({ logLevel, src, timestampInSeconds, }: {
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 (timestamp) => {
33
- while (!hasDecodedEnoughForTimestamp(timestamp)) {
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: (logLevel) => {
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
- await mostInThePastBank.prepareForDeletion(logLevel);
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
- await bank.prepareForDeletion(logLevel);
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.361",
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.361",
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.361",
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",