@remotion/media 4.0.402 → 4.0.404

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.
Files changed (77) hide show
  1. package/dist/audio/audio-for-preview.d.ts +2 -0
  2. package/dist/audio/audio-preview-iterator.d.ts +8 -3
  3. package/dist/audio/props.d.ts +2 -0
  4. package/dist/audio-extraction/audio-iterator.d.ts +5 -4
  5. package/dist/audio-extraction/audio-manager.d.ts +23 -7
  6. package/dist/audio-extraction/extract-audio.d.ts +2 -4
  7. package/dist/audio-iterator-manager.d.ts +2 -2
  8. package/dist/caches.d.ts +34 -17
  9. package/dist/convert-audiodata/apply-volume.d.ts +1 -1
  10. package/dist/convert-audiodata/resample-audiodata.d.ts +2 -2
  11. package/dist/debug-overlay/preview-overlay.d.ts +83 -5
  12. package/dist/esm/index.mjs +524 -476
  13. package/dist/extract-frame-and-audio.d.ts +1 -2
  14. package/dist/get-sink.d.ts +1 -2
  15. package/dist/index.d.ts +2 -1
  16. package/dist/media-player.d.ts +1 -1
  17. package/dist/on-error.d.ts +12 -0
  18. package/dist/show-in-timeline.d.ts +2 -2
  19. package/dist/use-media-in-timeline.d.ts +2 -2
  20. package/dist/video/props.d.ts +5 -0
  21. package/dist/video/video-for-preview.d.ts +2 -0
  22. package/dist/video/video-for-rendering.d.ts +2 -0
  23. package/dist/video/video-preview-iterator.d.ts +7 -2
  24. package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +1 -2
  25. package/dist/video-extraction/extract-frame.d.ts +2 -4
  26. package/dist/video-extraction/get-frames-since-keyframe.d.ts +2 -12
  27. package/dist/video-extraction/keyframe-bank.d.ts +14 -12
  28. package/dist/video-extraction/keyframe-manager.d.ts +7 -9
  29. package/dist/video-iterator-manager.d.ts +4 -5
  30. package/package.json +8 -7
  31. package/dist/audio/allow-wait.js +0 -15
  32. package/dist/audio/audio-for-preview.js +0 -304
  33. package/dist/audio/audio-for-rendering.js +0 -194
  34. package/dist/audio/audio-preview-iterator.js +0 -176
  35. package/dist/audio/audio.js +0 -20
  36. package/dist/audio/props.js +0 -1
  37. package/dist/audio-extraction/audio-cache.js +0 -66
  38. package/dist/audio-extraction/audio-iterator.js +0 -132
  39. package/dist/audio-extraction/audio-manager.js +0 -113
  40. package/dist/audio-extraction/extract-audio.js +0 -132
  41. package/dist/audio-iterator-manager.js +0 -228
  42. package/dist/browser-can-use-webgl2.js +0 -13
  43. package/dist/caches.js +0 -61
  44. package/dist/calculate-playbacktime.js +0 -4
  45. package/dist/convert-audiodata/apply-volume.js +0 -17
  46. package/dist/convert-audiodata/combine-audiodata.js +0 -23
  47. package/dist/convert-audiodata/convert-audiodata.js +0 -73
  48. package/dist/convert-audiodata/resample-audiodata.js +0 -94
  49. package/dist/debug-overlay/preview-overlay.js +0 -44
  50. package/dist/extract-frame-and-audio.js +0 -101
  51. package/dist/get-sink.js +0 -15
  52. package/dist/get-time-in-seconds.js +0 -40
  53. package/dist/helpers/round-to-4-digits.js +0 -4
  54. package/dist/index.js +0 -12
  55. package/dist/is-type-of-error.js +0 -20
  56. package/dist/looped-frame.js +0 -10
  57. package/dist/media-player.js +0 -445
  58. package/dist/nonce-manager.js +0 -13
  59. package/dist/prewarm-iterator-for-looping.js +0 -56
  60. package/dist/render-timestamp-range.js +0 -9
  61. package/dist/show-in-timeline.js +0 -31
  62. package/dist/use-media-in-timeline.js +0 -103
  63. package/dist/video/props.js +0 -1
  64. package/dist/video/video-for-preview.js +0 -329
  65. package/dist/video/video-for-rendering.js +0 -263
  66. package/dist/video/video-preview-iterator.js +0 -122
  67. package/dist/video/video.js +0 -35
  68. package/dist/video-extraction/add-broadcast-channel-listener.js +0 -125
  69. package/dist/video-extraction/extract-frame-via-broadcast-channel.js +0 -113
  70. package/dist/video-extraction/extract-frame.js +0 -85
  71. package/dist/video-extraction/get-allocation-size.js +0 -6
  72. package/dist/video-extraction/get-frames-since-keyframe.js +0 -108
  73. package/dist/video-extraction/keyframe-bank.js +0 -159
  74. package/dist/video-extraction/keyframe-manager.js +0 -206
  75. package/dist/video-extraction/remember-actual-matroska-timestamps.js +0 -19
  76. package/dist/video-extraction/rotate-frame.js +0 -34
  77. package/dist/video-iterator-manager.js +0 -109
@@ -946,7 +946,7 @@ class MediaPlayer {
946
946
  }) {
947
947
  this.canvas = canvas ?? null;
948
948
  this.src = src;
949
- this.logLevel = logLevel ?? window.remotion_logLevel;
949
+ this.logLevel = logLevel;
950
950
  this.sharedAudioContext = sharedAudioContext;
951
951
  this.playbackRate = playbackRate;
952
952
  this.globalPlaybackRate = globalPlaybackRate;
@@ -1344,6 +1344,27 @@ class MediaPlayer {
1344
1344
  };
1345
1345
  }
1346
1346
 
1347
+ // src/on-error.ts
1348
+ var callOnErrorAndResolve = ({
1349
+ onError,
1350
+ error,
1351
+ disallowFallback,
1352
+ isClientSideRendering,
1353
+ clientSideError
1354
+ }) => {
1355
+ const result = onError?.(error);
1356
+ if (isClientSideRendering) {
1357
+ return ["fail", clientSideError];
1358
+ }
1359
+ if (result) {
1360
+ return [result, error];
1361
+ }
1362
+ if (disallowFallback) {
1363
+ return ["fail", error];
1364
+ }
1365
+ return ["fallback", error];
1366
+ };
1367
+
1347
1368
  // src/show-in-timeline.ts
1348
1369
  import { useMemo } from "react";
1349
1370
  import { Internals as Internals4, useVideoConfig } from "remotion";
@@ -1538,7 +1559,8 @@ var AudioForPreviewAssertedShowing = ({
1538
1559
  disallowFallbackToHtml5Audio,
1539
1560
  toneFrequency,
1540
1561
  audioStreamIndex,
1541
- fallbackHtml5AudioProps
1562
+ fallbackHtml5AudioProps,
1563
+ onError
1542
1564
  }) => {
1543
1565
  const videoConfig = useUnsafeVideoConfig();
1544
1566
  const frame = useCurrentFrame2();
@@ -1637,36 +1659,35 @@ var AudioForPreviewAssertedShowing = ({
1637
1659
  if (result.type === "disposed") {
1638
1660
  return;
1639
1661
  }
1640
- if (result.type === "unknown-container-format") {
1641
- if (disallowFallbackToHtml5Audio) {
1642
- throw new Error(`Unknown container format ${preloadedSrc}, and 'disallowFallbackToHtml5Audio' was set.`);
1662
+ const handleError = (error, fallbackMessage) => {
1663
+ const [action, errorToUse] = callOnErrorAndResolve({
1664
+ onError,
1665
+ error,
1666
+ disallowFallback: disallowFallbackToHtml5Audio,
1667
+ isClientSideRendering: false,
1668
+ clientSideError: error
1669
+ });
1670
+ if (action === "fail") {
1671
+ throw errorToUse;
1672
+ } else {
1673
+ Internals6.Log.warn({ logLevel, tag: "@remotion/media" }, fallbackMessage);
1674
+ setShouldFallbackToNativeAudio(true);
1643
1675
  }
1644
- Internals6.Log.warn({ logLevel, tag: "@remotion/media" }, `Unknown container format for ${preloadedSrc} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
1645
- setShouldFallbackToNativeAudio(true);
1676
+ };
1677
+ if (result.type === "unknown-container-format") {
1678
+ handleError(new Error(`Unknown container format ${preloadedSrc}.`), `Unknown container format for ${preloadedSrc} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
1646
1679
  return;
1647
1680
  }
1648
1681
  if (result.type === "network-error") {
1649
- if (disallowFallbackToHtml5Audio) {
1650
- throw new Error(`Network error fetching ${preloadedSrc}, and 'disallowFallbackToHtml5Audio' was set.`);
1651
- }
1652
- Internals6.Log.warn({ logLevel, tag: "@remotion/media" }, `Network error fetching ${preloadedSrc}, falling back to <Html5Audio>`);
1653
- setShouldFallbackToNativeAudio(true);
1682
+ handleError(new Error(`Network error fetching ${preloadedSrc}.`), `Network error fetching ${preloadedSrc}, falling back to <Html5Audio>`);
1654
1683
  return;
1655
1684
  }
1656
1685
  if (result.type === "cannot-decode") {
1657
- if (disallowFallbackToHtml5Audio) {
1658
- throw new Error(`Cannot decode ${preloadedSrc}, and 'disallowFallbackToHtml5Audio' was set.`);
1659
- }
1660
- Internals6.Log.warn({ logLevel, tag: "@remotion/media" }, `Cannot decode ${preloadedSrc}, falling back to <Html5Audio>`);
1661
- setShouldFallbackToNativeAudio(true);
1686
+ handleError(new Error(`Cannot decode ${preloadedSrc}.`), `Cannot decode ${preloadedSrc}, falling back to <Html5Audio>`);
1662
1687
  return;
1663
1688
  }
1664
1689
  if (result.type === "no-tracks") {
1665
- if (disallowFallbackToHtml5Audio) {
1666
- throw new Error(`No video or audio tracks found for ${preloadedSrc}, and 'disallowFallbackToHtml5Audio' was set.`);
1667
- }
1668
- Internals6.Log.warn({ logLevel, tag: "@remotion/media" }, `No video or audio tracks found for ${preloadedSrc}, falling back to <Html5Audio>`);
1669
- setShouldFallbackToNativeAudio(true);
1690
+ handleError(new Error(`No video or audio tracks found for ${preloadedSrc}.`), `No video or audio tracks found for ${preloadedSrc}, falling back to <Html5Audio>`);
1670
1691
  return;
1671
1692
  }
1672
1693
  if (result.type === "success") {
@@ -1675,11 +1696,32 @@ var AudioForPreviewAssertedShowing = ({
1675
1696
  Internals6.Log.trace({ logLevel, tag: "@remotion/media" }, `[AudioForPreview] MediaPlayer initialized successfully`);
1676
1697
  }
1677
1698
  }).catch((error) => {
1678
- Internals6.Log.error({ logLevel, tag: "@remotion/media" }, "[AudioForPreview] Failed to initialize MediaPlayer", error);
1679
- setShouldFallbackToNativeAudio(true);
1699
+ const [action, errorToUse] = callOnErrorAndResolve({
1700
+ onError,
1701
+ error,
1702
+ disallowFallback: disallowFallbackToHtml5Audio,
1703
+ isClientSideRendering: false,
1704
+ clientSideError: error
1705
+ });
1706
+ if (action === "fail") {
1707
+ throw errorToUse;
1708
+ } else {
1709
+ Internals6.Log.error({ logLevel, tag: "@remotion/media" }, "[AudioForPreview] Failed to initialize MediaPlayer", error);
1710
+ setShouldFallbackToNativeAudio(true);
1711
+ }
1680
1712
  });
1681
1713
  } catch (error) {
1682
- Internals6.Log.error({ logLevel, tag: "@remotion/media" }, "[AudioForPreview] MediaPlayer initialization failed", error);
1714
+ const [action, errorToUse] = callOnErrorAndResolve({
1715
+ error,
1716
+ onError,
1717
+ disallowFallback: disallowFallbackToHtml5Audio,
1718
+ isClientSideRendering: false,
1719
+ clientSideError: error
1720
+ });
1721
+ if (action === "fail") {
1722
+ throw errorToUse;
1723
+ }
1724
+ Internals6.Log.error({ logLevel, tag: "@remotion/media" }, "[AudioForPreview] MediaPlayer initialization failed", errorToUse);
1683
1725
  setShouldFallbackToNativeAudio(true);
1684
1726
  }
1685
1727
  return () => {
@@ -1700,7 +1742,8 @@ var AudioForPreviewAssertedShowing = ({
1700
1742
  videoConfig.fps,
1701
1743
  audioStreamIndex,
1702
1744
  disallowFallbackToHtml5Audio,
1703
- buffer
1745
+ buffer,
1746
+ onError
1704
1747
  ]);
1705
1748
  useLayoutEffect(() => {
1706
1749
  const audioPlayer = mediaPlayerRef.current;
@@ -1827,9 +1870,11 @@ var AudioForPreview = ({
1827
1870
  disallowFallbackToHtml5Audio,
1828
1871
  toneFrequency,
1829
1872
  audioStreamIndex,
1830
- fallbackHtml5AudioProps
1873
+ fallbackHtml5AudioProps,
1874
+ onError
1831
1875
  }) => {
1832
1876
  const preloadedSrc = usePreload(src);
1877
+ const defaultLogLevel = Internals6.useLogLevel();
1833
1878
  const frame = useCurrentFrame2();
1834
1879
  const videoConfig = useVideoConfig2();
1835
1880
  const currentTime = frame / videoConfig.fps;
@@ -1861,7 +1906,7 @@ var AudioForPreview = ({
1861
1906
  audioStreamIndex: audioStreamIndex ?? 0,
1862
1907
  src: preloadedSrc,
1863
1908
  playbackRate: playbackRate ?? 1,
1864
- logLevel: logLevel ?? (typeof window !== "undefined" ? window.remotion_logLevel ?? "info" : "info"),
1909
+ logLevel: logLevel ?? defaultLogLevel,
1865
1910
  muted: muted ?? false,
1866
1911
  volume: volume ?? 1,
1867
1912
  loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? "repeat",
@@ -1873,6 +1918,7 @@ var AudioForPreview = ({
1873
1918
  stack,
1874
1919
  disallowFallbackToHtml5Audio: disallowFallbackToHtml5Audio ?? false,
1875
1920
  toneFrequency,
1921
+ onError,
1876
1922
  fallbackHtml5AudioProps
1877
1923
  });
1878
1924
  };
@@ -2011,7 +2057,7 @@ var makeAudioIterator2 = ({
2011
2057
  const getSamples = async (timestamp, durationInSeconds) => {
2012
2058
  lastUsed = Date.now();
2013
2059
  if (fullDuration !== null && timestamp > fullDuration) {
2014
- cache.clearBeforeThreshold(fullDuration - SAFE_BACK_WINDOW_IN_SECONDS);
2060
+ cache.clearBeforeThreshold(fullDuration - SAFE_WINDOW_OF_MONOTONICITY);
2015
2061
  return [];
2016
2062
  }
2017
2063
  const samples = cache.getSamples(timestamp, durationInSeconds);
@@ -2024,7 +2070,7 @@ var makeAudioIterator2 = ({
2024
2070
  while (true) {
2025
2071
  const sample = await getNextSample();
2026
2072
  const deleteBefore = fullDuration === null ? timestamp : Math.min(timestamp, fullDuration);
2027
- cache.clearBeforeThreshold(deleteBefore - SAFE_BACK_WINDOW_IN_SECONDS);
2073
+ cache.clearBeforeThreshold(deleteBefore - SAFE_WINDOW_OF_MONOTONICITY);
2028
2074
  if (sample === null) {
2029
2075
  break;
2030
2076
  }
@@ -2154,8 +2200,14 @@ var makeAudioManager = () => {
2154
2200
  logLevel,
2155
2201
  maxCacheSize
2156
2202
  }) => {
2157
- while ((await getTotalCacheStats()).totalSize > maxCacheSize) {
2203
+ let attempts = 0;
2204
+ const maxAttempts = 3;
2205
+ while ((await getTotalCacheStats()).totalSize > maxCacheSize && attempts < maxAttempts) {
2158
2206
  deleteOldestIterator();
2207
+ attempts++;
2208
+ }
2209
+ if ((await getTotalCacheStats()).totalSize > maxCacheSize && attempts >= maxAttempts) {
2210
+ Internals8.Log.warn({ logLevel, tag: "@remotion/media" }, `Audio cache: Exceeded max cache size after ${maxAttempts} attempts. Still ${(await getTotalCacheStats()).totalSize} bytes used, target was ${maxCacheSize} bytes.`);
2159
2211
  }
2160
2212
  for (const iterator of iterators) {
2161
2213
  if (iterator.src === src && await iterator.waitForCompletion() && iterator.canSatisfyRequestedTime(timeInSeconds)) {
@@ -2226,21 +2278,6 @@ var makeAudioManager = () => {
2226
2278
  // src/video-extraction/keyframe-manager.ts
2227
2279
  import { Internals as Internals10 } from "remotion";
2228
2280
 
2229
- // src/browser-can-use-webgl2.ts
2230
- var browserCanUseWebGl2 = null;
2231
- var browserCanUseWebGl2Uncached = () => {
2232
- const canvas = new OffscreenCanvas(1, 1);
2233
- const context = canvas.getContext("webgl2");
2234
- return context !== null;
2235
- };
2236
- var canBrowserUseWebGl2 = () => {
2237
- if (browserCanUseWebGl2 !== null) {
2238
- return browserCanUseWebGl2;
2239
- }
2240
- browserCanUseWebGl2 = browserCanUseWebGl2Uncached();
2241
- return browserCanUseWebGl2;
2242
- };
2243
-
2244
2281
  // src/render-timestamp-range.ts
2245
2282
  var renderTimestampRange = (timestamps) => {
2246
2283
  if (timestamps.length === 0) {
@@ -2252,18 +2289,6 @@ var renderTimestampRange = (timestamps) => {
2252
2289
  return `${timestamps[0].toFixed(3)}...${timestamps[timestamps.length - 1].toFixed(3)}`;
2253
2290
  };
2254
2291
 
2255
- // src/video-extraction/get-frames-since-keyframe.ts
2256
- import {
2257
- ALL_FORMATS as ALL_FORMATS2,
2258
- AudioSampleSink,
2259
- EncodedPacketSink,
2260
- Input as Input2,
2261
- MATROSKA,
2262
- UrlSource as UrlSource2,
2263
- VideoSampleSink,
2264
- WEBM
2265
- } from "mediabunny";
2266
-
2267
2292
  // src/video-extraction/keyframe-bank.ts
2268
2293
  import { Internals as Internals9 } from "remotion";
2269
2294
 
@@ -2276,36 +2301,42 @@ var getAllocationSize = (sample) => {
2276
2301
  };
2277
2302
 
2278
2303
  // src/video-extraction/keyframe-bank.ts
2279
- var makeKeyframeBank = ({
2280
- startTimestampInSeconds,
2281
- endTimestampInSeconds,
2282
- sampleIterator,
2304
+ var BIGGEST_ALLOWED_JUMP_FORWARD_SECONDS = 3;
2305
+ var makeKeyframeBank = async ({
2283
2306
  logLevel: parentLogLevel,
2284
- src
2307
+ src,
2308
+ videoSampleSink,
2309
+ requestedTimestamp
2285
2310
  }) => {
2286
- Internals9.Log.verbose({ logLevel: parentLogLevel, tag: "@remotion/media" }, `Creating keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
2311
+ const sampleIterator = videoSampleSink.samples(roundTo4Digits(requestedTimestamp));
2287
2312
  const frames = {};
2288
2313
  const frameTimestamps = [];
2314
+ let hasReachedEndOfVideo = false;
2289
2315
  let lastUsed = Date.now();
2290
2316
  let allocationSize = 0;
2317
+ const deleteFrameAtTimestamp = (timestamp) => {
2318
+ allocationSize -= getAllocationSize(frames[timestamp]);
2319
+ frameTimestamps.splice(frameTimestamps.indexOf(timestamp), 1);
2320
+ frames[timestamp].close();
2321
+ delete frames[timestamp];
2322
+ };
2291
2323
  const deleteFramesBeforeTimestamp = ({
2292
2324
  logLevel,
2293
2325
  timestampInSeconds
2294
2326
  }) => {
2295
2327
  const deletedTimestamps = [];
2296
2328
  for (const frameTimestamp of frameTimestamps.slice()) {
2297
- const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
2298
- if (isLast) {
2299
- continue;
2329
+ if (hasReachedEndOfVideo) {
2330
+ const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
2331
+ if (isLast) {
2332
+ continue;
2333
+ }
2300
2334
  }
2301
2335
  if (frameTimestamp < timestampInSeconds) {
2302
2336
  if (!frames[frameTimestamp]) {
2303
2337
  continue;
2304
2338
  }
2305
- allocationSize -= getAllocationSize(frames[frameTimestamp]);
2306
- frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
2307
- frames[frameTimestamp].close();
2308
- delete frames[frameTimestamp];
2339
+ deleteFrameAtTimestamp(frameTimestamp);
2309
2340
  deletedTimestamps.push(frameTimestamp);
2310
2341
  }
2311
2342
  }
@@ -2322,32 +2353,31 @@ var makeKeyframeBank = ({
2322
2353
  if (!lastFrame) {
2323
2354
  return true;
2324
2355
  }
2325
- return roundTo4Digits(lastFrame.timestamp + lastFrame.duration) > roundTo4Digits(timestamp) + 0.001;
2356
+ return roundTo4Digits(lastFrame.timestamp + lastFrame.duration) > roundTo4Digits(timestamp) + SAFE_WINDOW_OF_MONOTONICITY;
2326
2357
  };
2327
- const addFrame = (frame) => {
2358
+ const addFrame = (frame, logLevel) => {
2328
2359
  if (frames[frame.timestamp]) {
2329
- allocationSize -= getAllocationSize(frames[frame.timestamp]);
2330
- frameTimestamps.splice(frameTimestamps.indexOf(frame.timestamp), 1);
2331
- frames[frame.timestamp].close();
2332
- delete frames[frame.timestamp];
2360
+ deleteFrameAtTimestamp(frame.timestamp);
2333
2361
  }
2334
2362
  frames[frame.timestamp] = frame;
2335
2363
  frameTimestamps.push(frame.timestamp);
2336
2364
  allocationSize += getAllocationSize(frame);
2337
2365
  lastUsed = Date.now();
2366
+ Internals9.Log.trace({ logLevel, tag: "@remotion/media" }, `Added frame at ${frame.timestamp}sec to bank`);
2338
2367
  };
2339
- const ensureEnoughFramesForTimestamp = async (timestampInSeconds) => {
2368
+ const ensureEnoughFramesForTimestamp = async (timestampInSeconds, logLevel) => {
2340
2369
  while (!hasDecodedEnoughForTimestamp(timestampInSeconds)) {
2341
2370
  const sample = await sampleIterator.next();
2342
2371
  if (sample.value) {
2343
- addFrame(sample.value);
2372
+ addFrame(sample.value, logLevel);
2344
2373
  }
2345
2374
  if (sample.done) {
2375
+ hasReachedEndOfVideo = true;
2346
2376
  break;
2347
2377
  }
2348
2378
  deleteFramesBeforeTimestamp({
2349
2379
  logLevel: parentLogLevel,
2350
- timestampInSeconds: timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS
2380
+ timestampInSeconds: timestampInSeconds - SAFE_WINDOW_OF_MONOTONICITY
2351
2381
  });
2352
2382
  }
2353
2383
  lastUsed = Date.now();
@@ -2355,13 +2385,10 @@ var makeKeyframeBank = ({
2355
2385
  const getFrameFromTimestamp = async (timestampInSeconds) => {
2356
2386
  lastUsed = Date.now();
2357
2387
  let adjustedTimestamp = timestampInSeconds;
2358
- if (roundTo4Digits(timestampInSeconds) < roundTo4Digits(startTimestampInSeconds)) {
2359
- adjustedTimestamp = startTimestampInSeconds;
2388
+ if (hasReachedEndOfVideo && roundTo4Digits(adjustedTimestamp) > roundTo4Digits(frameTimestamps[frameTimestamps.length - 1])) {
2389
+ adjustedTimestamp = frameTimestamps[frameTimestamps.length - 1];
2360
2390
  }
2361
- if (roundTo4Digits(adjustedTimestamp) > roundTo4Digits(endTimestampInSeconds)) {
2362
- adjustedTimestamp = endTimestampInSeconds;
2363
- }
2364
- await ensureEnoughFramesForTimestamp(adjustedTimestamp);
2391
+ await ensureEnoughFramesForTimestamp(adjustedTimestamp, parentLogLevel);
2365
2392
  for (let i = frameTimestamps.length - 1;i >= 0; i--) {
2366
2393
  const sample = frames[frameTimestamps[i]];
2367
2394
  if (!sample) {
@@ -2371,32 +2398,11 @@ var makeKeyframeBank = ({
2371
2398
  return sample;
2372
2399
  }
2373
2400
  }
2374
- return null;
2401
+ return frames[frameTimestamps[0]] ?? null;
2375
2402
  };
2376
2403
  const hasTimestampInSecond = async (timestamp) => {
2377
2404
  return await getFrameFromTimestamp(timestamp) !== null;
2378
2405
  };
2379
- const prepareForDeletion = (logLevel) => {
2380
- Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `Preparing for deletion of keyframe bank from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
2381
- sampleIterator.return().then((result) => {
2382
- if (result.value) {
2383
- result.value.close();
2384
- }
2385
- return null;
2386
- });
2387
- let framesDeleted = 0;
2388
- for (const frameTimestamp of frameTimestamps) {
2389
- if (!frames[frameTimestamp]) {
2390
- continue;
2391
- }
2392
- allocationSize -= getAllocationSize(frames[frameTimestamp]);
2393
- frames[frameTimestamp].close();
2394
- delete frames[frameTimestamp];
2395
- framesDeleted++;
2396
- }
2397
- frameTimestamps.length = 0;
2398
- return { framesDeleted };
2399
- };
2400
2406
  const getOpenFrameCount = () => {
2401
2407
  return {
2402
2408
  size: allocationSize,
@@ -2407,173 +2413,91 @@ var makeKeyframeBank = ({
2407
2413
  return lastUsed;
2408
2414
  };
2409
2415
  let queue = Promise.resolve(undefined);
2410
- const keyframeBank = {
2411
- startTimestampInSeconds,
2412
- endTimestampInSeconds,
2413
- getFrameFromTimestamp: (timestamp) => {
2414
- queue = queue.then(() => getFrameFromTimestamp(timestamp));
2415
- return queue;
2416
- },
2417
- prepareForDeletion,
2418
- hasTimestampInSecond,
2419
- addFrame,
2420
- deleteFramesBeforeTimestamp,
2421
- src,
2422
- getOpenFrameCount,
2423
- getLastUsed
2424
- };
2425
- return keyframeBank;
2426
- };
2427
-
2428
- // src/video-extraction/remember-actual-matroska-timestamps.ts
2429
- var rememberActualMatroskaTimestamps = (isMatroska) => {
2430
- const observations = [];
2431
- const observeTimestamp = (startTime) => {
2432
- if (!isMatroska) {
2433
- return;
2434
- }
2435
- observations.push(startTime);
2436
- };
2437
- const getRealTimestamp = (observedTimestamp) => {
2438
- if (!isMatroska) {
2439
- return observedTimestamp;
2440
- }
2441
- return observations.find((observation) => Math.abs(observedTimestamp - observation) < 0.001) ?? null;
2442
- };
2443
- return {
2444
- observeTimestamp,
2445
- getRealTimestamp
2446
- };
2447
- };
2448
-
2449
- // src/video-extraction/get-frames-since-keyframe.ts
2450
- var getRetryDelay = () => {
2451
- return null;
2452
- };
2453
- var getFormatOrNullOrNetworkError = async (input) => {
2454
- try {
2455
- return await input.getFormat();
2456
- } catch (err) {
2457
- if (isNetworkError(err)) {
2458
- return "network-error";
2459
- }
2460
- return null;
2416
+ const firstFrame = await sampleIterator.next();
2417
+ if (!firstFrame.value) {
2418
+ throw new Error("No first frame found");
2461
2419
  }
2462
- };
2463
- var getSinks = async (src) => {
2464
- const input = new Input2({
2465
- formats: ALL_FORMATS2,
2466
- source: new UrlSource2(src, {
2467
- getRetryDelay
2468
- })
2469
- });
2470
- const format = await getFormatOrNullOrNetworkError(input);
2471
- const isMatroska = format === MATROSKA || format === WEBM;
2472
- const getVideoSinks = async () => {
2473
- if (format === "network-error") {
2474
- return "network-error";
2475
- }
2476
- if (format === null) {
2477
- return "unknown-container-format";
2478
- }
2479
- const videoTrack = await input.getPrimaryVideoTrack();
2480
- if (!videoTrack) {
2481
- return "no-video-track";
2482
- }
2483
- const canDecode = await videoTrack.canDecode();
2484
- if (!canDecode) {
2485
- return "cannot-decode";
2420
+ const startTimestampInSeconds = firstFrame.value.timestamp;
2421
+ Internals9.Log.verbose({ logLevel: parentLogLevel, tag: "@remotion/media" }, `Creating keyframe bank from ${startTimestampInSeconds}sec`);
2422
+ addFrame(firstFrame.value, parentLogLevel);
2423
+ const getRangeOfTimestamps = () => {
2424
+ if (frameTimestamps.length === 0) {
2425
+ return null;
2486
2426
  }
2487
2427
  return {
2488
- sampleSink: new VideoSampleSink(videoTrack),
2489
- packetSink: new EncodedPacketSink(videoTrack)
2428
+ firstTimestamp: frameTimestamps[0],
2429
+ lastTimestamp: frameTimestamps[frameTimestamps.length - 1]
2490
2430
  };
2491
2431
  };
2492
- let videoSinksPromise = null;
2493
- const getVideoSinksPromise = () => {
2494
- if (videoSinksPromise) {
2495
- return videoSinksPromise;
2432
+ const prepareForDeletion = (logLevel, reason) => {
2433
+ const range = getRangeOfTimestamps();
2434
+ if (range) {
2435
+ Internals9.Log.verbose({ logLevel, tag: "@remotion/media" }, `Preparing for deletion (${reason}) of keyframe bank from ${range?.firstTimestamp}sec to ${range?.lastTimestamp}sec`);
2496
2436
  }
2497
- videoSinksPromise = getVideoSinks();
2498
- return videoSinksPromise;
2499
- };
2500
- const audioSinksPromise = {};
2501
- const getAudioSinks = async (index) => {
2502
- if (format === null) {
2503
- return "unknown-container-format";
2437
+ let framesDeleted = 0;
2438
+ for (const frameTimestamp of frameTimestamps.slice()) {
2439
+ if (!frames[frameTimestamp]) {
2440
+ continue;
2441
+ }
2442
+ deleteFrameAtTimestamp(frameTimestamp);
2443
+ framesDeleted++;
2504
2444
  }
2505
- if (format === "network-error") {
2506
- return "network-error";
2445
+ sampleIterator.return();
2446
+ frameTimestamps.length = 0;
2447
+ return { framesDeleted };
2448
+ };
2449
+ const canSatisfyTimestamp = (timestamp) => {
2450
+ if (frameTimestamps.length === 0) {
2451
+ return false;
2507
2452
  }
2508
- const audioTracks = await input.getAudioTracks();
2509
- const audioTrack = audioTracks[index];
2510
- if (!audioTrack) {
2511
- return "no-audio-track";
2453
+ const roundedTimestamp = roundTo4Digits(timestamp);
2454
+ const firstFrameTimestamp = roundTo4Digits(frameTimestamps[0]);
2455
+ const lastFrameTimestamp = roundTo4Digits(frameTimestamps[frameTimestamps.length - 1]);
2456
+ if (hasReachedEndOfVideo && roundedTimestamp > lastFrameTimestamp) {
2457
+ return true;
2512
2458
  }
2513
- const canDecode = await audioTrack.canDecode();
2514
- if (!canDecode) {
2515
- return "cannot-decode-audio";
2459
+ if (roundedTimestamp < firstFrameTimestamp) {
2460
+ return false;
2516
2461
  }
2517
- return {
2518
- sampleSink: new AudioSampleSink(audioTrack)
2519
- };
2520
- };
2521
- const getAudioSinksPromise = (index) => {
2522
- if (audioSinksPromise[index]) {
2523
- return audioSinksPromise[index];
2462
+ if (roundedTimestamp - BIGGEST_ALLOWED_JUMP_FORWARD_SECONDS > lastFrameTimestamp) {
2463
+ return false;
2524
2464
  }
2525
- audioSinksPromise[index] = getAudioSinks(index);
2526
- return audioSinksPromise[index];
2465
+ return true;
2527
2466
  };
2528
- return {
2529
- getVideo: () => getVideoSinksPromise(),
2530
- getAudio: (index) => getAudioSinksPromise(index),
2531
- actualMatroskaTimestamps: rememberActualMatroskaTimestamps(isMatroska),
2532
- isMatroska,
2533
- getDuration: () => {
2534
- return input.computeDuration();
2535
- }
2467
+ const keyframeBank = {
2468
+ getFrameFromTimestamp: (timestamp) => {
2469
+ queue = queue.then(() => getFrameFromTimestamp(timestamp));
2470
+ return queue;
2471
+ },
2472
+ prepareForDeletion,
2473
+ hasTimestampInSecond: (timestamp) => {
2474
+ queue = queue.then(() => hasTimestampInSecond(timestamp));
2475
+ return queue;
2476
+ },
2477
+ addFrame,
2478
+ deleteFramesBeforeTimestamp,
2479
+ src,
2480
+ getOpenFrameCount,
2481
+ getLastUsed,
2482
+ canSatisfyTimestamp,
2483
+ getRangeOfTimestamps
2536
2484
  };
2537
- };
2538
- var getFramesSinceKeyframe = async ({
2539
- packetSink,
2540
- videoSampleSink,
2541
- startPacket,
2542
- logLevel,
2543
- src
2544
- }) => {
2545
- const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
2546
- verifyKeyPackets: true
2547
- });
2548
- const sampleIterator = videoSampleSink.samples(startPacket.timestamp, nextKeyPacket ? nextKeyPacket.timestamp : Infinity);
2549
- const keyframeBank = makeKeyframeBank({
2550
- startTimestampInSeconds: startPacket.timestamp,
2551
- endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
2552
- sampleIterator,
2553
- logLevel,
2554
- src
2555
- });
2556
2485
  return keyframeBank;
2557
2486
  };
2558
2487
 
2559
2488
  // src/video-extraction/keyframe-manager.ts
2560
2489
  var makeKeyframeManager = () => {
2561
- const sources = {};
2562
- const addKeyframeBank = ({
2563
- src,
2564
- bank,
2565
- startTimestampInSeconds
2566
- }) => {
2567
- sources[src] = sources[src] ?? {};
2568
- sources[src][startTimestampInSeconds] = bank;
2490
+ let sources = {};
2491
+ const addKeyframeBank = ({ src, bank }) => {
2492
+ sources[src] = sources[src] ?? [];
2493
+ sources[src].push(bank);
2569
2494
  };
2570
- const logCacheStats = async (logLevel) => {
2495
+ const logCacheStats = (logLevel) => {
2571
2496
  let count = 0;
2572
2497
  let totalSize = 0;
2573
2498
  for (const src in sources) {
2574
- for (const bank in sources[src]) {
2575
- const v = await sources[src][bank];
2576
- const { size, timestamps } = v.getOpenFrameCount();
2499
+ for (const bank of sources[src]) {
2500
+ const { size, timestamps } = bank.getOpenFrameCount();
2577
2501
  count += timestamps.length;
2578
2502
  totalSize += size;
2579
2503
  if (size === 0) {
@@ -2584,13 +2508,12 @@ var makeKeyframeManager = () => {
2584
2508
  }
2585
2509
  Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Video cache stats: ${count} open frames, ${totalSize} bytes`);
2586
2510
  };
2587
- const getCacheStats = async () => {
2511
+ const getCacheStats = () => {
2588
2512
  let count = 0;
2589
2513
  let totalSize = 0;
2590
2514
  for (const src in sources) {
2591
- for (const bank in sources[src]) {
2592
- const v = await sources[src][bank];
2593
- const { timestamps, size } = v.getOpenFrameCount();
2515
+ for (const bank of sources[src]) {
2516
+ const { timestamps, size } = bank.getOpenFrameCount();
2594
2517
  count += timestamps.length;
2595
2518
  totalSize += size;
2596
2519
  if (size === 0) {
@@ -2600,17 +2523,17 @@ var makeKeyframeManager = () => {
2600
2523
  }
2601
2524
  return { count, totalSize };
2602
2525
  };
2603
- const getTheKeyframeBankMostInThePast = async () => {
2526
+ const getTheKeyframeBankMostInThePast = () => {
2604
2527
  let mostInThePast = null;
2605
2528
  let mostInThePastBank = null;
2606
2529
  let numberOfBanks = 0;
2607
2530
  for (const src in sources) {
2608
- for (const b in sources[src]) {
2609
- const bank = await sources[src][b];
2531
+ for (const bank of sources[src]) {
2532
+ const index = sources[src].indexOf(bank);
2610
2533
  const lastUsed = bank.getLastUsed();
2611
2534
  if (mostInThePast === null || lastUsed < mostInThePast) {
2612
2535
  mostInThePast = lastUsed;
2613
- mostInThePastBank = { src, bank };
2536
+ mostInThePastBank = { src, bank, index };
2614
2537
  }
2615
2538
  numberOfBanks++;
2616
2539
  }
@@ -2620,49 +2543,66 @@ var makeKeyframeManager = () => {
2620
2543
  }
2621
2544
  return { mostInThePastBank, numberOfBanks };
2622
2545
  };
2623
- const deleteOldestKeyframeBank = async (logLevel) => {
2546
+ const deleteOldestKeyframeBank = (logLevel) => {
2624
2547
  const {
2625
- mostInThePastBank: { bank: mostInThePastBank, src: mostInThePastSrc },
2548
+ mostInThePastBank: {
2549
+ bank: mostInThePastBank,
2550
+ src: mostInThePastSrc,
2551
+ index: mostInThePastIndex
2552
+ },
2626
2553
  numberOfBanks
2627
- } = await getTheKeyframeBankMostInThePast();
2554
+ } = getTheKeyframeBankMostInThePast();
2628
2555
  if (numberOfBanks < 2) {
2629
2556
  return { finish: true };
2630
2557
  }
2631
2558
  if (mostInThePastBank) {
2632
- const { framesDeleted } = mostInThePastBank.prepareForDeletion(logLevel);
2633
- delete sources[mostInThePastSrc][mostInThePastBank.startTimestampInSeconds];
2634
- Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted ${framesDeleted} frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
2559
+ const range = mostInThePastBank.getRangeOfTimestamps();
2560
+ const { framesDeleted } = mostInThePastBank.prepareForDeletion(logLevel, "deleted oldest keyframe bank to stay under max cache size");
2561
+ sources[mostInThePastSrc].splice(mostInThePastIndex, 1);
2562
+ if (range) {
2563
+ Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Deleted ${framesDeleted} frames for src ${mostInThePastSrc} from ${range?.firstTimestamp}sec to ${range?.lastTimestamp}sec to free up memory.`);
2564
+ }
2635
2565
  }
2636
2566
  return { finish: false };
2637
2567
  };
2638
- const ensureToStayUnderMaxCacheSize = async (logLevel, maxCacheSize) => {
2639
- let cacheStats = await getTotalCacheStats();
2640
- while (cacheStats.totalSize > maxCacheSize) {
2641
- const { finish } = await deleteOldestKeyframeBank(logLevel);
2568
+ const ensureToStayUnderMaxCacheSize = (logLevel, maxCacheSize) => {
2569
+ let cacheStats = getTotalCacheStats();
2570
+ let attempts = 0;
2571
+ const maxAttempts = 3;
2572
+ while (cacheStats.totalSize > maxCacheSize && attempts < maxAttempts) {
2573
+ const { finish } = deleteOldestKeyframeBank(logLevel);
2642
2574
  if (finish) {
2643
2575
  break;
2644
2576
  }
2645
2577
  Internals10.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));
2646
- cacheStats = await getTotalCacheStats();
2578
+ cacheStats = getTotalCacheStats();
2579
+ attempts++;
2580
+ }
2581
+ if (cacheStats.totalSize > maxCacheSize && attempts >= maxAttempts) {
2582
+ Internals10.Log.warn({ logLevel, tag: "@remotion/media" }, `Exceeded max cache size after ${maxAttempts} attempts. Remaining cache size: ${(cacheStats.totalSize / 1024 / 1024).toFixed(1)} MB, target was ${(maxCacheSize / 1024 / 1024).toFixed(1)} MB.`);
2647
2583
  }
2648
2584
  };
2649
- const clearKeyframeBanksBeforeTime = async ({
2585
+ const clearKeyframeBanksBeforeTime = ({
2650
2586
  timestampInSeconds,
2651
2587
  src,
2652
2588
  logLevel
2653
2589
  }) => {
2654
- const threshold = timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS;
2590
+ const threshold = timestampInSeconds - SAFE_WINDOW_OF_MONOTONICITY;
2655
2591
  if (!sources[src]) {
2656
2592
  return;
2657
2593
  }
2658
- const banks = Object.keys(sources[src]);
2659
- for (const startTimeInSeconds of banks) {
2660
- const bank = await sources[src][startTimeInSeconds];
2661
- const { endTimestampInSeconds, startTimestampInSeconds } = bank;
2662
- if (endTimestampInSeconds < threshold) {
2663
- bank.prepareForDeletion(logLevel);
2664
- Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `[Video] Cleared frames for src ${src} from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
2665
- delete sources[src][startTimeInSeconds];
2594
+ const banks = sources[src];
2595
+ for (const bank of banks) {
2596
+ const range = bank.getRangeOfTimestamps();
2597
+ if (!range) {
2598
+ continue;
2599
+ }
2600
+ const { lastTimestamp } = range;
2601
+ if (lastTimestamp < threshold) {
2602
+ bank.prepareForDeletion(logLevel, "cleared before threshold");
2603
+ Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `[Video] Cleared frames for src ${src} from ${range.firstTimestamp}sec to ${range.lastTimestamp}sec`);
2604
+ const bankIndex = banks.indexOf(bank);
2605
+ delete sources[src][bankIndex];
2666
2606
  } else {
2667
2607
  bank.deleteFramesBeforeTimestamp({
2668
2608
  timestampInSeconds: threshold,
@@ -2670,70 +2610,58 @@ var makeKeyframeManager = () => {
2670
2610
  });
2671
2611
  }
2672
2612
  }
2673
- await logCacheStats(logLevel);
2613
+ sources[src] = sources[src].filter((bank) => bank !== undefined);
2614
+ logCacheStats(logLevel);
2674
2615
  };
2675
2616
  const getKeyframeBankOrRefetch = async ({
2676
- packetSink,
2677
2617
  timestamp,
2678
2618
  videoSampleSink,
2679
2619
  src,
2680
2620
  logLevel
2681
2621
  }) => {
2682
- const startPacket = await packetSink.getKeyPacket(timestamp, {
2683
- verifyKeyPackets: true
2684
- }) ?? await packetSink.getFirstPacket({ verifyKeyPackets: true });
2685
- const hasAlpha = startPacket?.sideData.alpha;
2686
- if (hasAlpha && !canBrowserUseWebGl2()) {
2687
- return "has-alpha";
2688
- }
2689
- if (!startPacket) {
2690
- return null;
2691
- }
2692
- const startTimestampInSeconds = startPacket.timestamp;
2693
- const existingBank = sources[src]?.[startTimestampInSeconds];
2622
+ const existingBanks = sources[src] ?? [];
2623
+ const existingBank = existingBanks?.find((bank) => bank.canSatisfyTimestamp(timestamp));
2694
2624
  if (!existingBank) {
2695
- const newKeyframeBank = getFramesSinceKeyframe({
2696
- packetSink,
2625
+ Internals10.Log.trace({ logLevel, tag: "@remotion/media" }, `Creating new keyframe bank for src ${src} at timestamp ${timestamp}`);
2626
+ const newKeyframeBank = await makeKeyframeBank({
2697
2627
  videoSampleSink,
2698
- startPacket,
2699
2628
  logLevel,
2700
- src
2629
+ src,
2630
+ requestedTimestamp: timestamp
2701
2631
  });
2702
- addKeyframeBank({ src, bank: newKeyframeBank, startTimestampInSeconds });
2632
+ addKeyframeBank({ src, bank: newKeyframeBank });
2703
2633
  return newKeyframeBank;
2704
2634
  }
2705
- if (await (await existingBank).hasTimestampInSecond(timestamp)) {
2635
+ if (existingBank.canSatisfyTimestamp(timestamp)) {
2636
+ Internals10.Log.trace({ logLevel, tag: "@remotion/media" }, `Keyframe bank exists and satisfies timestamp ${timestamp}`);
2706
2637
  return existingBank;
2707
2638
  }
2708
2639
  Internals10.Log.verbose({ logLevel, tag: "@remotion/media" }, `Keyframe bank exists but frame at time ${timestamp} does not exist anymore.`);
2709
- await (await existingBank).prepareForDeletion(logLevel);
2710
- delete sources[src][startTimestampInSeconds];
2711
- const replacementKeybank = getFramesSinceKeyframe({
2712
- packetSink,
2640
+ existingBank.prepareForDeletion(logLevel, "already existed but evicted");
2641
+ sources[src] = sources[src].filter((bank) => bank !== existingBank);
2642
+ const replacementKeybank = await makeKeyframeBank({
2713
2643
  videoSampleSink,
2714
- startPacket,
2644
+ requestedTimestamp: timestamp,
2715
2645
  logLevel,
2716
2646
  src
2717
2647
  });
2718
- addKeyframeBank({ src, bank: replacementKeybank, startTimestampInSeconds });
2648
+ addKeyframeBank({ src, bank: replacementKeybank });
2719
2649
  return replacementKeybank;
2720
2650
  };
2721
2651
  const requestKeyframeBank = async ({
2722
- packetSink,
2723
2652
  timestamp,
2724
2653
  videoSampleSink,
2725
2654
  src,
2726
2655
  logLevel,
2727
2656
  maxCacheSize
2728
2657
  }) => {
2729
- await ensureToStayUnderMaxCacheSize(logLevel, maxCacheSize);
2730
- await clearKeyframeBanksBeforeTime({
2658
+ ensureToStayUnderMaxCacheSize(logLevel, maxCacheSize);
2659
+ clearKeyframeBanksBeforeTime({
2731
2660
  timestampInSeconds: timestamp,
2732
2661
  src,
2733
2662
  logLevel
2734
2663
  });
2735
2664
  const keyframeBank = await getKeyframeBankOrRefetch({
2736
- packetSink,
2737
2665
  timestamp,
2738
2666
  videoSampleSink,
2739
2667
  src,
@@ -2741,21 +2669,20 @@ var makeKeyframeManager = () => {
2741
2669
  });
2742
2670
  return keyframeBank;
2743
2671
  };
2744
- const clearAll = async (logLevel) => {
2672
+ const clearAll = (logLevel) => {
2745
2673
  const srcs = Object.keys(sources);
2746
2674
  for (const src of srcs) {
2747
- const banks = Object.keys(sources[src]);
2748
- for (const startTimeInSeconds of banks) {
2749
- const bank = await sources[src][startTimeInSeconds];
2750
- bank.prepareForDeletion(logLevel);
2751
- delete sources[src][startTimeInSeconds];
2675
+ const banks = sources[src];
2676
+ for (const bank of banks) {
2677
+ bank.prepareForDeletion(logLevel, "clearAll");
2752
2678
  }
2679
+ sources[src] = [];
2753
2680
  }
2681
+ sources = {};
2754
2682
  };
2755
2683
  let queue = Promise.resolve(undefined);
2756
2684
  return {
2757
2685
  requestKeyframeBank: ({
2758
- packetSink,
2759
2686
  timestamp,
2760
2687
  videoSampleSink,
2761
2688
  src,
@@ -2763,7 +2690,6 @@ var makeKeyframeManager = () => {
2763
2690
  maxCacheSize
2764
2691
  }) => {
2765
2692
  queue = queue.then(() => requestKeyframeBank({
2766
- packetSink,
2767
2693
  timestamp,
2768
2694
  videoSampleSink,
2769
2695
  src,
@@ -2778,11 +2704,11 @@ var makeKeyframeManager = () => {
2778
2704
  };
2779
2705
 
2780
2706
  // src/caches.ts
2781
- var SAFE_BACK_WINDOW_IN_SECONDS = 1;
2707
+ var SAFE_WINDOW_OF_MONOTONICITY = 0.2;
2782
2708
  var keyframeManager = makeKeyframeManager();
2783
2709
  var audioManager = makeAudioManager();
2784
- var getTotalCacheStats = async () => {
2785
- const keyframeManagerCacheStats = await keyframeManager.getCacheStats();
2710
+ var getTotalCacheStats = () => {
2711
+ const keyframeManagerCacheStats = keyframeManager.getCacheStats();
2786
2712
  const audioManagerCacheStats = audioManager.getCacheStats();
2787
2713
  return {
2788
2714
  count: keyframeManagerCacheStats.count + audioManagerCacheStats.count,
@@ -3050,6 +2976,154 @@ var combineAudioDataAndClosePrevious = (audioDataArray) => {
3050
2976
 
3051
2977
  // src/get-sink.ts
3052
2978
  import { Internals as Internals12 } from "remotion";
2979
+
2980
+ // src/video-extraction/get-frames-since-keyframe.ts
2981
+ import {
2982
+ ALL_FORMATS as ALL_FORMATS2,
2983
+ AudioSampleSink,
2984
+ EncodedPacketSink,
2985
+ Input as Input2,
2986
+ MATROSKA,
2987
+ UrlSource as UrlSource2,
2988
+ VideoSampleSink,
2989
+ WEBM
2990
+ } from "mediabunny";
2991
+
2992
+ // src/browser-can-use-webgl2.ts
2993
+ var browserCanUseWebGl2 = null;
2994
+ var browserCanUseWebGl2Uncached = () => {
2995
+ const canvas = new OffscreenCanvas(1, 1);
2996
+ const context = canvas.getContext("webgl2");
2997
+ return context !== null;
2998
+ };
2999
+ var canBrowserUseWebGl2 = () => {
3000
+ if (browserCanUseWebGl2 !== null) {
3001
+ return browserCanUseWebGl2;
3002
+ }
3003
+ browserCanUseWebGl2 = browserCanUseWebGl2Uncached();
3004
+ return browserCanUseWebGl2;
3005
+ };
3006
+
3007
+ // src/video-extraction/remember-actual-matroska-timestamps.ts
3008
+ var rememberActualMatroskaTimestamps = (isMatroska) => {
3009
+ const observations = [];
3010
+ const observeTimestamp = (startTime) => {
3011
+ if (!isMatroska) {
3012
+ return;
3013
+ }
3014
+ observations.push(startTime);
3015
+ };
3016
+ const getRealTimestamp = (observedTimestamp) => {
3017
+ if (!isMatroska) {
3018
+ return observedTimestamp;
3019
+ }
3020
+ return observations.find((observation) => Math.abs(observedTimestamp - observation) < 0.001) ?? null;
3021
+ };
3022
+ return {
3023
+ observeTimestamp,
3024
+ getRealTimestamp
3025
+ };
3026
+ };
3027
+
3028
+ // src/video-extraction/get-frames-since-keyframe.ts
3029
+ var getRetryDelay = () => {
3030
+ return null;
3031
+ };
3032
+ var getFormatOrNullOrNetworkError = async (input) => {
3033
+ try {
3034
+ return await input.getFormat();
3035
+ } catch (err) {
3036
+ if (isNetworkError(err)) {
3037
+ return "network-error";
3038
+ }
3039
+ return null;
3040
+ }
3041
+ };
3042
+ var getSinks = async (src) => {
3043
+ const input = new Input2({
3044
+ formats: ALL_FORMATS2,
3045
+ source: new UrlSource2(src, {
3046
+ getRetryDelay
3047
+ })
3048
+ });
3049
+ const format = await getFormatOrNullOrNetworkError(input);
3050
+ const isMatroska = format === MATROSKA || format === WEBM;
3051
+ const getVideoSinks = async () => {
3052
+ if (format === "network-error") {
3053
+ return "network-error";
3054
+ }
3055
+ if (format === null) {
3056
+ return "unknown-container-format";
3057
+ }
3058
+ const videoTrack = await input.getPrimaryVideoTrack();
3059
+ if (!videoTrack) {
3060
+ return "no-video-track";
3061
+ }
3062
+ const canDecode = await videoTrack.canDecode();
3063
+ if (!canDecode) {
3064
+ return "cannot-decode";
3065
+ }
3066
+ const sampleSink = new VideoSampleSink(videoTrack);
3067
+ const packetSink = new EncodedPacketSink(videoTrack);
3068
+ const startPacket = await packetSink.getFirstPacket({
3069
+ verifyKeyPackets: true
3070
+ });
3071
+ const hasAlpha = startPacket?.sideData.alpha;
3072
+ if (hasAlpha && !canBrowserUseWebGl2()) {
3073
+ return "cannot-decode-alpha";
3074
+ }
3075
+ return {
3076
+ sampleSink
3077
+ };
3078
+ };
3079
+ let videoSinksPromise = null;
3080
+ const getVideoSinksPromise = () => {
3081
+ if (videoSinksPromise) {
3082
+ return videoSinksPromise;
3083
+ }
3084
+ videoSinksPromise = getVideoSinks();
3085
+ return videoSinksPromise;
3086
+ };
3087
+ const audioSinksPromise = {};
3088
+ const getAudioSinks = async (index) => {
3089
+ if (format === null) {
3090
+ return "unknown-container-format";
3091
+ }
3092
+ if (format === "network-error") {
3093
+ return "network-error";
3094
+ }
3095
+ const audioTracks = await input.getAudioTracks();
3096
+ const audioTrack = audioTracks[index];
3097
+ if (!audioTrack) {
3098
+ return "no-audio-track";
3099
+ }
3100
+ const canDecode = await audioTrack.canDecode();
3101
+ if (!canDecode) {
3102
+ return "cannot-decode-audio";
3103
+ }
3104
+ return {
3105
+ sampleSink: new AudioSampleSink(audioTrack)
3106
+ };
3107
+ };
3108
+ const getAudioSinksPromise = (index) => {
3109
+ if (audioSinksPromise[index]) {
3110
+ return audioSinksPromise[index];
3111
+ }
3112
+ audioSinksPromise[index] = getAudioSinks(index);
3113
+ return audioSinksPromise[index];
3114
+ };
3115
+ return {
3116
+ getVideo: () => getVideoSinksPromise(),
3117
+ getAudio: (index) => getAudioSinksPromise(index),
3118
+ actualMatroskaTimestamps: rememberActualMatroskaTimestamps(isMatroska),
3119
+ isMatroska,
3120
+ getDuration: () => {
3121
+ return input.computeDuration();
3122
+ }
3123
+ };
3124
+ };
3125
+
3126
+ // src/get-sink.ts
3053
3127
  var sinkPromises = {};
3054
3128
  var getSink = (src, logLevel) => {
3055
3129
  let promise = sinkPromises[src];
@@ -3224,6 +3298,12 @@ var extractFrameInternal = async ({
3224
3298
  if (video === "network-error") {
3225
3299
  return { type: "network-error" };
3226
3300
  }
3301
+ if (video === "cannot-decode-alpha") {
3302
+ return {
3303
+ type: "cannot-decode-alpha",
3304
+ durationInSeconds: mediaDurationInSeconds
3305
+ };
3306
+ }
3227
3307
  const timeInSeconds = getTimeInSeconds({
3228
3308
  loop,
3229
3309
  mediaDurationInSeconds,
@@ -3238,36 +3318,29 @@ var extractFrameInternal = async ({
3238
3318
  if (timeInSeconds === null) {
3239
3319
  return {
3240
3320
  type: "success",
3241
- frame: null,
3321
+ sample: null,
3242
3322
  durationInSeconds: await sink.getDuration()
3243
3323
  };
3244
3324
  }
3245
3325
  try {
3246
3326
  const keyframeBank = await keyframeManager.requestKeyframeBank({
3247
- packetSink: video.packetSink,
3248
3327
  videoSampleSink: video.sampleSink,
3249
3328
  timestamp: timeInSeconds,
3250
3329
  src,
3251
3330
  logLevel,
3252
3331
  maxCacheSize
3253
3332
  });
3254
- if (keyframeBank === "has-alpha") {
3255
- return {
3256
- type: "cannot-decode-alpha",
3257
- durationInSeconds: await sink.getDuration()
3258
- };
3259
- }
3260
3333
  if (!keyframeBank) {
3261
3334
  return {
3262
3335
  type: "success",
3263
- frame: null,
3336
+ sample: null,
3264
3337
  durationInSeconds: await sink.getDuration()
3265
3338
  };
3266
3339
  }
3267
3340
  const frame = await keyframeBank.getFrameFromTimestamp(timeInSeconds);
3268
3341
  return {
3269
3342
  type: "success",
3270
- frame,
3343
+ sample: frame,
3271
3344
  durationInSeconds: await sink.getDuration()
3272
3345
  };
3273
3346
  } catch (err) {
@@ -3331,7 +3404,7 @@ var extractFrameAndAudio = async ({
3331
3404
  maxCacheSize
3332
3405
  }) => {
3333
3406
  try {
3334
- const [frame, audio] = await Promise.all([
3407
+ const [video, audio] = await Promise.all([
3335
3408
  includeVideo ? extractFrame({
3336
3409
  src,
3337
3410
  timeInSeconds,
@@ -3357,59 +3430,42 @@ var extractFrameAndAudio = async ({
3357
3430
  maxCacheSize
3358
3431
  }) : null
3359
3432
  ]);
3360
- if (frame?.type === "cannot-decode") {
3433
+ if (video?.type === "cannot-decode") {
3361
3434
  return {
3362
3435
  type: "cannot-decode",
3363
- durationInSeconds: frame.durationInSeconds
3436
+ durationInSeconds: video.durationInSeconds
3364
3437
  };
3365
3438
  }
3366
- if (frame?.type === "unknown-container-format") {
3439
+ if (video?.type === "unknown-container-format") {
3367
3440
  return { type: "unknown-container-format" };
3368
3441
  }
3369
- if (frame?.type === "cannot-decode-alpha") {
3442
+ if (video?.type === "cannot-decode-alpha") {
3370
3443
  return {
3371
3444
  type: "cannot-decode-alpha",
3372
- durationInSeconds: frame.durationInSeconds
3445
+ durationInSeconds: video.durationInSeconds
3373
3446
  };
3374
3447
  }
3375
- if (frame?.type === "network-error") {
3448
+ if (video?.type === "network-error") {
3376
3449
  return { type: "network-error" };
3377
3450
  }
3378
3451
  if (audio === "unknown-container-format") {
3379
- if (frame !== null) {
3380
- frame?.frame?.close();
3381
- }
3382
3452
  return { type: "unknown-container-format" };
3383
3453
  }
3384
3454
  if (audio === "network-error") {
3385
- if (frame !== null) {
3386
- frame?.frame?.close();
3387
- }
3388
3455
  return { type: "network-error" };
3389
3456
  }
3390
3457
  if (audio === "cannot-decode") {
3391
- if (frame?.type === "success" && frame.frame !== null) {
3392
- frame?.frame.close();
3393
- }
3394
3458
  return {
3395
3459
  type: "cannot-decode",
3396
- durationInSeconds: frame?.type === "success" ? frame.durationInSeconds : null
3397
- };
3398
- }
3399
- if (!frame?.frame) {
3400
- return {
3401
- type: "success",
3402
- frame: null,
3403
- audio: audio?.data ?? null,
3404
- durationInSeconds: audio?.durationInSeconds ?? null
3460
+ durationInSeconds: video?.type === "success" ? video.durationInSeconds : null
3405
3461
  };
3406
3462
  }
3407
3463
  return {
3408
3464
  type: "success",
3409
- frame: await rotateFrame({
3410
- frame: frame.frame.toVideoFrame(),
3411
- rotation: frame.frame.rotation
3412
- }),
3465
+ frame: video?.sample ? await rotateFrame({
3466
+ frame: video.sample.toVideoFrame(),
3467
+ rotation: video.sample.rotation
3468
+ }) : null,
3413
3469
  audio: audio?.data ?? null,
3414
3470
  durationInSeconds: audio?.durationInSeconds ?? null
3415
3471
  };
@@ -3676,7 +3732,7 @@ var AudioForRendering = ({
3676
3732
  loopVolumeCurveBehavior,
3677
3733
  delayRenderRetries,
3678
3734
  delayRenderTimeoutInMilliseconds,
3679
- logLevel = window.remotion_logLevel ?? "info",
3735
+ logLevel: overriddenLogLevel,
3680
3736
  loop,
3681
3737
  fallbackHtml5AudioProps,
3682
3738
  audioStreamIndex,
@@ -3686,8 +3742,11 @@ var AudioForRendering = ({
3686
3742
  disallowFallbackToHtml5Audio,
3687
3743
  toneFrequency,
3688
3744
  trimAfter,
3689
- trimBefore
3745
+ trimBefore,
3746
+ onError
3690
3747
  }) => {
3748
+ const defaultLogLevel = Internals14.useLogLevel();
3749
+ const logLevel = overriddenLogLevel ?? defaultLogLevel;
3691
3750
  const frame = useCurrentFrame3();
3692
3751
  const absoluteFrame = Internals14.useTimelinePosition();
3693
3752
  const videoConfig = Internals14.useUnsafeVideoConfig();
@@ -3710,7 +3769,7 @@ var AudioForRendering = ({
3710
3769
  sequenceContext?.relativeFrom,
3711
3770
  sequenceContext?.durationInFrames
3712
3771
  ]);
3713
- const maxCacheSize = useMaxMediaCacheSize(logLevel ?? window.remotion_logLevel);
3772
+ const maxCacheSize = useMaxMediaCacheSize(logLevel);
3714
3773
  const audioEnabled = Internals14.useAudioEnabled();
3715
3774
  useLayoutEffect2(() => {
3716
3775
  const timestamp = frame / fps;
@@ -3739,7 +3798,7 @@ var AudioForRendering = ({
3739
3798
  timeInSeconds: timestamp,
3740
3799
  durationInSeconds,
3741
3800
  playbackRate: playbackRate ?? 1,
3742
- logLevel: logLevel ?? window.remotion_logLevel,
3801
+ logLevel,
3743
3802
  includeAudio: shouldRenderAudio,
3744
3803
  includeVideo: false,
3745
3804
  isClientSideRendering: environment.isClientSideRendering,
@@ -3750,52 +3809,33 @@ var AudioForRendering = ({
3750
3809
  fps,
3751
3810
  maxCacheSize
3752
3811
  }).then((result) => {
3753
- if (result.type === "unknown-container-format") {
3754
- if (environment.isClientSideRendering) {
3755
- cancelRender2(new Error(`Cannot render audio "${src}": Unknown container format. See supported formats: https://www.remotion.dev/docs/mediabunny/formats`));
3756
- return;
3757
- }
3758
- if (disallowFallbackToHtml5Audio) {
3759
- cancelRender2(new Error(`Unknown container format ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
3812
+ const handleError = (error, clientSideError, fallbackMessage) => {
3813
+ const [action, errorToUse] = callOnErrorAndResolve({
3814
+ onError,
3815
+ error,
3816
+ disallowFallback: disallowFallbackToHtml5Audio ?? false,
3817
+ isClientSideRendering: environment.isClientSideRendering,
3818
+ clientSideError
3819
+ });
3820
+ if (action === "fail") {
3821
+ cancelRender2(errorToUse);
3760
3822
  }
3761
- Internals14.Log.warn({
3762
- logLevel: logLevel ?? window.remotion_logLevel,
3763
- tag: "@remotion/media"
3764
- }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
3823
+ Internals14.Log.warn({ logLevel, tag: "@remotion/media" }, fallbackMessage);
3765
3824
  setReplaceWithHtml5Audio(true);
3825
+ };
3826
+ if (result.type === "unknown-container-format") {
3827
+ handleError(new Error(`Unknown container format ${src}.`), new Error(`Cannot render audio "${src}": Unknown container format. See supported formats: https://www.remotion.dev/docs/mediabunny/formats`), `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <Html5Audio>`);
3766
3828
  return;
3767
3829
  }
3768
3830
  if (result.type === "cannot-decode") {
3769
- if (environment.isClientSideRendering) {
3770
- cancelRender2(new Error(`Cannot render audio "${src}": The audio could not be decoded by the browser.`));
3771
- return;
3772
- }
3773
- if (disallowFallbackToHtml5Audio) {
3774
- cancelRender2(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
3775
- }
3776
- Internals14.Log.warn({
3777
- logLevel: logLevel ?? window.remotion_logLevel,
3778
- tag: "@remotion/media"
3779
- }, `Cannot decode ${src}, falling back to <Html5Audio>`);
3780
- setReplaceWithHtml5Audio(true);
3831
+ handleError(new Error(`Cannot decode ${src}.`), new Error(`Cannot render audio "${src}": The audio could not be decoded by the browser.`), `Cannot decode ${src}, falling back to <Html5Audio>`);
3781
3832
  return;
3782
3833
  }
3783
3834
  if (result.type === "cannot-decode-alpha") {
3784
3835
  throw new Error(`Cannot decode alpha component for ${src}, and 'disallowFallbackToHtml5Audio' was set. But this should never happen, since you used the <Audio> tag. Please report this as a bug.`);
3785
3836
  }
3786
3837
  if (result.type === "network-error") {
3787
- if (environment.isClientSideRendering) {
3788
- cancelRender2(new Error(`Cannot render audio "${src}": Network error while fetching the audio (possibly CORS).`));
3789
- return;
3790
- }
3791
- if (disallowFallbackToHtml5Audio) {
3792
- cancelRender2(new Error(`Cannot decode ${src}, and 'disallowFallbackToHtml5Audio' was set. Failing the render.`));
3793
- }
3794
- Internals14.Log.warn({
3795
- logLevel: logLevel ?? window.remotion_logLevel,
3796
- tag: "@remotion/media"
3797
- }, `Network error fetching ${src}, falling back to <Html5Audio>`);
3798
- setReplaceWithHtml5Audio(true);
3838
+ handleError(new Error(`Network error fetching ${src}.`), new Error(`Cannot render audio "${src}": Network error while fetching the audio (possibly CORS).`), `Network error fetching ${src}, falling back to <Html5Audio>`);
3799
3839
  return;
3800
3840
  }
3801
3841
  const { audio, durationInSeconds: assetDurationInSeconds } = result;
@@ -3860,7 +3900,8 @@ var AudioForRendering = ({
3860
3900
  trimBefore,
3861
3901
  replaceWithHtml5Audio,
3862
3902
  maxCacheSize,
3863
- audioEnabled
3903
+ audioEnabled,
3904
+ onError
3864
3905
  ]);
3865
3906
  if (replaceWithHtml5Audio) {
3866
3907
  return /* @__PURE__ */ jsx2(Html5Audio, {
@@ -3961,7 +4002,8 @@ var VideoForPreviewAssertedShowing = ({
3961
4002
  fallbackOffthreadVideoProps,
3962
4003
  audioStreamIndex,
3963
4004
  debugOverlay,
3964
- headless
4005
+ headless,
4006
+ onError
3965
4007
  }) => {
3966
4008
  const src = usePreload2(unpreloadedSrc);
3967
4009
  const canvasRef = useRef2(null);
@@ -4060,36 +4102,34 @@ var VideoForPreviewAssertedShowing = ({
4060
4102
  if (result.type === "disposed") {
4061
4103
  return;
4062
4104
  }
4063
- if (result.type === "unknown-container-format") {
4064
- if (disallowFallbackToOffthreadVideo) {
4065
- throw new Error(`Unknown container format ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
4105
+ const handleError = (error, fallbackMessage) => {
4106
+ const [action, errorToUse] = callOnErrorAndResolve({
4107
+ onError,
4108
+ error,
4109
+ disallowFallback: disallowFallbackToOffthreadVideo,
4110
+ isClientSideRendering: false,
4111
+ clientSideError: error
4112
+ });
4113
+ if (action === "fail") {
4114
+ throw errorToUse;
4066
4115
  }
4067
- Internals16.Log.warn({ logLevel, tag: "@remotion/media" }, `Unknown container format for ${preloadedSrc} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <OffthreadVideo>`);
4116
+ Internals16.Log.warn({ logLevel, tag: "@remotion/media" }, fallbackMessage);
4068
4117
  setShouldFallbackToNativeVideo(true);
4118
+ };
4119
+ if (result.type === "unknown-container-format") {
4120
+ handleError(new Error(`Unknown container format ${preloadedSrc}.`), `Unknown container format for ${preloadedSrc} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <OffthreadVideo>`);
4069
4121
  return;
4070
4122
  }
4071
4123
  if (result.type === "network-error") {
4072
- if (disallowFallbackToOffthreadVideo) {
4073
- throw new Error(`Network error fetching ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
4074
- }
4075
- Internals16.Log.warn({ logLevel, tag: "@remotion/media" }, `Network error fetching ${preloadedSrc}, falling back to <OffthreadVideo>`);
4076
- setShouldFallbackToNativeVideo(true);
4124
+ handleError(new Error(`Network error fetching ${preloadedSrc}.`), `Network error fetching ${preloadedSrc}, falling back to <OffthreadVideo>`);
4077
4125
  return;
4078
4126
  }
4079
4127
  if (result.type === "cannot-decode") {
4080
- if (disallowFallbackToOffthreadVideo) {
4081
- throw new Error(`Cannot decode ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
4082
- }
4083
- Internals16.Log.warn({ logLevel, tag: "@remotion/media" }, `Cannot decode ${preloadedSrc}, falling back to <OffthreadVideo>`);
4084
- setShouldFallbackToNativeVideo(true);
4128
+ handleError(new Error(`Cannot decode ${preloadedSrc}.`), `Cannot decode ${preloadedSrc}, falling back to <OffthreadVideo>`);
4085
4129
  return;
4086
4130
  }
4087
4131
  if (result.type === "no-tracks") {
4088
- if (disallowFallbackToOffthreadVideo) {
4089
- throw new Error(`No video or audio tracks found for ${preloadedSrc}, and 'disallowFallbackToOffthreadVideo' was set.`);
4090
- }
4091
- Internals16.Log.warn({ logLevel, tag: "@remotion/media" }, `No video or audio tracks found for ${preloadedSrc}, falling back to <OffthreadVideo>`);
4092
- setShouldFallbackToNativeVideo(true);
4132
+ handleError(new Error(`No video or audio tracks found for ${preloadedSrc}.`), `No video or audio tracks found for ${preloadedSrc}, falling back to <OffthreadVideo>`);
4093
4133
  return;
4094
4134
  }
4095
4135
  if (result.type === "success") {
@@ -4097,11 +4137,31 @@ var VideoForPreviewAssertedShowing = ({
4097
4137
  setMediaDurationInSeconds(result.durationInSeconds);
4098
4138
  }
4099
4139
  }).catch((error) => {
4100
- Internals16.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] Failed to initialize MediaPlayer", error);
4140
+ const [action, errorToUse] = callOnErrorAndResolve({
4141
+ onError,
4142
+ error,
4143
+ disallowFallback: disallowFallbackToOffthreadVideo,
4144
+ isClientSideRendering: false,
4145
+ clientSideError: error
4146
+ });
4147
+ if (action === "fail") {
4148
+ throw errorToUse;
4149
+ }
4150
+ Internals16.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] Failed to initialize MediaPlayer", errorToUse);
4101
4151
  setShouldFallbackToNativeVideo(true);
4102
4152
  });
4103
4153
  } catch (error) {
4104
- Internals16.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] MediaPlayer initialization failed", error);
4154
+ const [action, errorToUse] = callOnErrorAndResolve({
4155
+ error,
4156
+ onError,
4157
+ disallowFallback: disallowFallbackToOffthreadVideo,
4158
+ isClientSideRendering: false,
4159
+ clientSideError: error
4160
+ });
4161
+ if (action === "fail") {
4162
+ throw errorToUse;
4163
+ }
4164
+ Internals16.Log.error({ logLevel, tag: "@remotion/media" }, "[VideoForPreview] MediaPlayer initialization failed", errorToUse);
4105
4165
  setShouldFallbackToNativeVideo(true);
4106
4166
  }
4107
4167
  return () => {
@@ -4122,7 +4182,8 @@ var VideoForPreviewAssertedShowing = ({
4122
4182
  loop,
4123
4183
  preloadedSrc,
4124
4184
  sharedAudioContext,
4125
- videoConfig.fps
4185
+ videoConfig.fps,
4186
+ onError
4126
4187
  ]);
4127
4188
  const classNameValue = useMemo4(() => {
4128
4189
  return [Internals16.OBJECTFIT_CONTAIN_CLASS_NAME, className].filter(Internals16.truthy).join(" ");
@@ -4335,7 +4396,8 @@ var VideoForRendering = ({
4335
4396
  toneFrequency,
4336
4397
  trimAfterValue,
4337
4398
  trimBeforeValue,
4338
- headless
4399
+ headless,
4400
+ onError
4339
4401
  }) => {
4340
4402
  if (!src) {
4341
4403
  throw new TypeError("No `src` was passed to <Video>.");
@@ -4404,64 +4466,43 @@ var VideoForRendering = ({
4404
4466
  fps,
4405
4467
  maxCacheSize
4406
4468
  }).then((result) => {
4407
- if (result.type === "unknown-container-format") {
4469
+ const handleError = (err, clientSideError, fallbackMessage, mediaDurationInSeconds) => {
4408
4470
  if (environment.isClientSideRendering) {
4409
- cancelRender3(new Error(`Cannot render video "${src}": Unknown container format. See supported formats: https://www.remotion.dev/docs/mediabunny/formats`));
4471
+ cancelRender3(clientSideError);
4410
4472
  return;
4411
4473
  }
4412
- if (disallowFallbackToOffthreadVideo) {
4413
- cancelRender3(new Error(`Unknown container format ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
4414
- }
4415
- if (window.remotion_isMainTab) {
4416
- Internals17.Log.info({ logLevel, tag: "@remotion/media" }, `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <OffthreadVideo>`);
4417
- }
4418
- setReplaceWithOffthreadVideo({ durationInSeconds: null });
4419
- return;
4420
- }
4421
- if (result.type === "cannot-decode") {
4422
- if (environment.isClientSideRendering) {
4423
- cancelRender3(new Error(`Cannot render video "${src}": The video could not be decoded by the browser.`));
4474
+ const [action, errorToUse] = callOnErrorAndResolve({
4475
+ onError,
4476
+ error: err,
4477
+ disallowFallback: disallowFallbackToOffthreadVideo,
4478
+ isClientSideRendering: environment.isClientSideRendering,
4479
+ clientSideError: err
4480
+ });
4481
+ if (action === "fail") {
4482
+ cancelRender3(errorToUse);
4424
4483
  return;
4425
4484
  }
4426
- if (disallowFallbackToOffthreadVideo) {
4427
- cancelRender3(new Error(`Cannot decode ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
4428
- }
4429
4485
  if (window.remotion_isMainTab) {
4430
- Internals17.Log.warn({ logLevel, tag: "@remotion/media" }, `Cannot decode ${src}, falling back to <OffthreadVideo>`);
4486
+ Internals17.Log.warn({ logLevel, tag: "@remotion/media" }, fallbackMessage);
4431
4487
  }
4432
4488
  setReplaceWithOffthreadVideo({
4433
- durationInSeconds: result.durationInSeconds
4489
+ durationInSeconds: mediaDurationInSeconds
4434
4490
  });
4491
+ };
4492
+ if (result.type === "unknown-container-format") {
4493
+ handleError(new Error(`Unknown container format ${src}.`), new Error(`Cannot render video "${src}": Unknown container format. See supported formats: https://www.remotion.dev/docs/mediabunny/formats`), `Unknown container format for ${src} (Supported formats: https://www.remotion.dev/docs/mediabunny/formats), falling back to <OffthreadVideo>`, null);
4494
+ return;
4495
+ }
4496
+ if (result.type === "cannot-decode") {
4497
+ handleError(new Error(`Cannot decode ${src}.`), new Error(`Cannot render video "${src}": The video could not be decoded by the browser.`), `Cannot decode ${src}, falling back to <OffthreadVideo>`, result.durationInSeconds);
4435
4498
  return;
4436
4499
  }
4437
4500
  if (result.type === "cannot-decode-alpha") {
4438
- if (environment.isClientSideRendering) {
4439
- cancelRender3(new Error(`Cannot render video "${src}": The alpha channel could not be decoded by the browser.`));
4440
- return;
4441
- }
4442
- if (disallowFallbackToOffthreadVideo) {
4443
- cancelRender3(new Error(`Cannot decode alpha component for ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
4444
- }
4445
- if (window.remotion_isMainTab) {
4446
- Internals17.Log.info({ logLevel, tag: "@remotion/media" }, `Cannot decode alpha component for ${src}, falling back to <OffthreadVideo>`);
4447
- }
4448
- setReplaceWithOffthreadVideo({
4449
- durationInSeconds: result.durationInSeconds
4450
- });
4501
+ handleError(new Error(`Cannot decode alpha component for ${src}.`), new Error(`Cannot render video "${src}": The alpha channel could not be decoded by the browser.`), `Cannot decode alpha component for ${src}, falling back to <OffthreadVideo>`, result.durationInSeconds);
4451
4502
  return;
4452
4503
  }
4453
4504
  if (result.type === "network-error") {
4454
- if (environment.isClientSideRendering) {
4455
- cancelRender3(new Error(`Cannot render video "${src}": Network error while fetching the video (possibly CORS).`));
4456
- return;
4457
- }
4458
- if (disallowFallbackToOffthreadVideo) {
4459
- cancelRender3(new Error(`Cannot decode ${src}, and 'disallowFallbackToOffthreadVideo' was set. Failing the render.`));
4460
- }
4461
- if (window.remotion_isMainTab) {
4462
- Internals17.Log.warn({ logLevel, tag: "@remotion/media" }, `Network error fetching ${src} (no CORS?), falling back to <OffthreadVideo>`);
4463
- }
4464
- setReplaceWithOffthreadVideo({ durationInSeconds: null });
4505
+ handleError(new Error(`Network error fetching ${src}.`), new Error(`Cannot render video "${src}": Network error while fetching the video (possibly CORS).`), `Network error fetching ${src} (no CORS?), falling back to <OffthreadVideo>`, null);
4465
4506
  return;
4466
4507
  }
4467
4508
  const {
@@ -4554,7 +4595,8 @@ var VideoForRendering = ({
4554
4595
  videoEnabled,
4555
4596
  maxCacheSize,
4556
4597
  cancelRender3,
4557
- headless
4598
+ headless,
4599
+ onError
4558
4600
  ]);
4559
4601
  const classNameValue = useMemo5(() => {
4560
4602
  return [Internals17.OBJECTFIT_CONTAIN_CLASS_NAME, className].filter(Internals17.truthy).join(" ");
@@ -4649,7 +4691,8 @@ var InnerVideo = ({
4649
4691
  toneFrequency,
4650
4692
  showInTimeline,
4651
4693
  debugOverlay,
4652
- headless
4694
+ headless,
4695
+ onError
4653
4696
  }) => {
4654
4697
  const environment = useRemotionEnvironment4();
4655
4698
  if (typeof src !== "string") {
@@ -4690,7 +4733,8 @@ var InnerVideo = ({
4690
4733
  toneFrequency,
4691
4734
  trimAfterValue,
4692
4735
  trimBeforeValue,
4693
- headless
4736
+ headless,
4737
+ onError
4694
4738
  });
4695
4739
  }
4696
4740
  return /* @__PURE__ */ jsx6(VideoForPreview, {
@@ -4713,7 +4757,8 @@ var InnerVideo = ({
4713
4757
  disallowFallbackToOffthreadVideo,
4714
4758
  fallbackOffthreadVideoProps,
4715
4759
  debugOverlay: debugOverlay ?? false,
4716
- headless: headless ?? false
4760
+ headless: headless ?? false,
4761
+ onError
4717
4762
  });
4718
4763
  };
4719
4764
  var Video = ({
@@ -4739,8 +4784,10 @@ var Video = ({
4739
4784
  stack,
4740
4785
  toneFrequency,
4741
4786
  debugOverlay,
4742
- headless
4787
+ headless,
4788
+ onError
4743
4789
  }) => {
4790
+ const fallbackLogLevel = Internals18.useLogLevel();
4744
4791
  return /* @__PURE__ */ jsx6(InnerVideo, {
4745
4792
  audioStreamIndex: audioStreamIndex ?? 0,
4746
4793
  className,
@@ -4748,7 +4795,7 @@ var Video = ({
4748
4795
  delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null,
4749
4796
  disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false,
4750
4797
  fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {},
4751
- logLevel: logLevel ?? (typeof window !== "undefined" ? window.remotion_logLevel ?? "info" : "info"),
4798
+ logLevel: logLevel ?? fallbackLogLevel,
4752
4799
  loop: loop ?? false,
4753
4800
  loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? "repeat",
4754
4801
  muted: muted ?? false,
@@ -4764,7 +4811,8 @@ var Video = ({
4764
4811
  toneFrequency: toneFrequency ?? 1,
4765
4812
  stack,
4766
4813
  debugOverlay: debugOverlay ?? false,
4767
- headless: headless ?? false
4814
+ headless: headless ?? false,
4815
+ onError
4768
4816
  });
4769
4817
  };
4770
4818
  Internals18.addSequenceStackTraces(Video);