@remotion/media 4.0.371 → 4.0.373

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.
@@ -13,7 +13,7 @@ const AudioForPreviewAssertedShowing = ({ src, playbackRate, logLevel, muted, vo
13
13
  const [mediaPlayerReady, setMediaPlayerReady] = useState(false);
14
14
  const [shouldFallbackToNativeAudio, setShouldFallbackToNativeAudio] = useState(false);
15
15
  const [playing] = Timeline.usePlayingState();
16
- const timelineContext = useContext(Timeline.TimelineContext);
16
+ const timelineContext = useContext(Internals.TimelineContext);
17
17
  const globalPlaybackRate = timelineContext.playbackRate;
18
18
  const sharedAudioContext = useContext(SharedAudioContext);
19
19
  const buffer = useBufferState();
@@ -44,6 +44,7 @@ const extractAudioInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
44
44
  const durationInSeconds = durationNotYetApplyingPlaybackRate * playbackRate;
45
45
  const samples = await sampleIterator.getSamples(timeInSeconds, durationInSeconds);
46
46
  audioManager.logOpenFrames();
47
+ const trimStartToleranceInSeconds = 0.002;
47
48
  const audioDataArray = [];
48
49
  for (let i = 0; i < samples.length; i++) {
49
50
  const sample = samples[i];
@@ -65,7 +66,8 @@ const extractAudioInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
65
66
  let trimEndInSeconds = 0;
66
67
  if (isFirstSample) {
67
68
  trimStartInSeconds = timeInSeconds - sample.timestamp;
68
- if (trimStartInSeconds < 0 && trimStartInSeconds > -1e-10) {
69
+ if (trimStartInSeconds < 0 &&
70
+ trimStartInSeconds > -trimStartToleranceInSeconds) {
69
71
  trimStartInSeconds = 0;
70
72
  }
71
73
  if (trimStartInSeconds < 0) {
@@ -1,4 +1,4 @@
1
- import { AudioBufferSink } from 'mediabunny';
1
+ import { AudioBufferSink, InputDisposedError } from 'mediabunny';
2
2
  import { isAlreadyQueued, makeAudioIterator, } from './audio/audio-preview-iterator';
3
3
  export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, }) => {
4
4
  let muted = false;
@@ -48,30 +48,42 @@ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremo
48
48
  const iterator = makeAudioIterator(audioSink, startFromSecond);
49
49
  audioIteratorsCreated++;
50
50
  audioBufferIterator = iterator;
51
- // Schedule up to 3 buffers ahead of the current time
52
- for (let i = 0; i < 3; i++) {
53
- const result = await iterator.getNext();
54
- if (iterator.isDestroyed()) {
55
- delayHandle.unblock();
56
- return;
57
- }
58
- if (nonce.isStale()) {
59
- delayHandle.unblock();
60
- return;
51
+ try {
52
+ // Schedule up to 3 buffers ahead of the current time
53
+ for (let i = 0; i < 3; i++) {
54
+ const result = await iterator.getNext();
55
+ if (iterator.isDestroyed()) {
56
+ delayHandle.unblock();
57
+ return;
58
+ }
59
+ if (nonce.isStale()) {
60
+ delayHandle.unblock();
61
+ return;
62
+ }
63
+ if (!result.value) {
64
+ // media ended
65
+ delayHandle.unblock();
66
+ return;
67
+ }
68
+ onAudioChunk({
69
+ getIsPlaying,
70
+ buffer: result.value,
71
+ playbackRate,
72
+ scheduleAudioNode,
73
+ });
61
74
  }
62
- if (!result.value) {
63
- // media ended
64
- delayHandle.unblock();
75
+ }
76
+ catch (e) {
77
+ if (e instanceof InputDisposedError) {
78
+ // iterator was disposed by a newer startAudioIterator call
79
+ // this is expected during rapid seeking
65
80
  return;
66
81
  }
67
- onAudioChunk({
68
- getIsPlaying,
69
- buffer: result.value,
70
- playbackRate,
71
- scheduleAudioNode,
72
- });
82
+ throw e;
83
+ }
84
+ finally {
85
+ delayHandle.unblock();
73
86
  }
74
- delayHandle.unblock();
75
87
  };
76
88
  const pausePlayback = () => {
77
89
  if (!audioBufferIterator) {
@@ -55,7 +55,7 @@ import { ALL_FORMATS, Input, UrlSource } from "mediabunny";
55
55
  import { Internals as Internals3 } from "remotion";
56
56
 
57
57
  // src/audio-iterator-manager.ts
58
- import { AudioBufferSink } from "mediabunny";
58
+ import { AudioBufferSink, InputDisposedError } from "mediabunny";
59
59
 
60
60
  // src/helpers/round-to-4-digits.ts
61
61
  var roundTo4Digits = (timestamp) => {
@@ -337,28 +337,36 @@ var audioIteratorManager = ({
337
337
  const iterator = makeAudioIterator(audioSink, startFromSecond);
338
338
  audioIteratorsCreated++;
339
339
  audioBufferIterator = iterator;
340
- for (let i = 0;i < 3; i++) {
341
- const result = await iterator.getNext();
342
- if (iterator.isDestroyed()) {
343
- delayHandle.unblock();
344
- return;
345
- }
346
- if (nonce.isStale()) {
347
- delayHandle.unblock();
348
- return;
340
+ try {
341
+ for (let i = 0;i < 3; i++) {
342
+ const result = await iterator.getNext();
343
+ if (iterator.isDestroyed()) {
344
+ delayHandle.unblock();
345
+ return;
346
+ }
347
+ if (nonce.isStale()) {
348
+ delayHandle.unblock();
349
+ return;
350
+ }
351
+ if (!result.value) {
352
+ delayHandle.unblock();
353
+ return;
354
+ }
355
+ onAudioChunk({
356
+ getIsPlaying,
357
+ buffer: result.value,
358
+ playbackRate,
359
+ scheduleAudioNode
360
+ });
349
361
  }
350
- if (!result.value) {
351
- delayHandle.unblock();
362
+ } catch (e) {
363
+ if (e instanceof InputDisposedError) {
352
364
  return;
353
365
  }
354
- onAudioChunk({
355
- getIsPlaying,
356
- buffer: result.value,
357
- playbackRate,
358
- scheduleAudioNode
359
- });
366
+ throw e;
367
+ } finally {
368
+ delayHandle.unblock();
360
369
  }
361
- delayHandle.unblock();
362
370
  };
363
371
  const pausePlayback = () => {
364
372
  if (!audioBufferIterator) {
@@ -1354,7 +1362,7 @@ var AudioForPreviewAssertedShowing = ({
1354
1362
  const [mediaPlayerReady, setMediaPlayerReady] = useState2(false);
1355
1363
  const [shouldFallbackToNativeAudio, setShouldFallbackToNativeAudio] = useState2(false);
1356
1364
  const [playing] = Timeline.usePlayingState();
1357
- const timelineContext = useContext2(Timeline.TimelineContext);
1365
+ const timelineContext = useContext2(Internals6.TimelineContext);
1358
1366
  const globalPlaybackRate = timelineContext.playbackRate;
1359
1367
  const sharedAudioContext = useContext2(SharedAudioContext);
1360
1368
  const buffer = useBufferState();
@@ -2262,19 +2270,26 @@ var makeKeyframeBank = ({
2262
2270
  };
2263
2271
  const getFrameFromTimestamp = async (timestampInSeconds) => {
2264
2272
  lastUsed = Date.now();
2265
- if (timestampInSeconds < startTimestampInSeconds) {
2266
- return Promise.reject(new Error(`Timestamp is before start timestamp (requested: ${timestampInSeconds}sec, start: ${startTimestampInSeconds})`));
2273
+ const maxClampToleranceInSeconds = 0.1;
2274
+ let adjustedTimestamp = timestampInSeconds;
2275
+ if (roundTo4Digits(timestampInSeconds) < roundTo4Digits(startTimestampInSeconds)) {
2276
+ const differenceInSeconds = startTimestampInSeconds - timestampInSeconds;
2277
+ if (differenceInSeconds <= maxClampToleranceInSeconds) {
2278
+ adjustedTimestamp = startTimestampInSeconds;
2279
+ } else {
2280
+ return Promise.reject(new Error(`Timestamp is before start timestamp (requested: ${timestampInSeconds}sec, start: ${startTimestampInSeconds}sec, difference: ${differenceInSeconds.toFixed(3)}sec exceeds tolerance of ${maxClampToleranceInSeconds}sec)`));
2281
+ }
2267
2282
  }
2268
- if (timestampInSeconds > endTimestampInSeconds) {
2269
- return Promise.reject(new Error(`Timestamp is after end timestamp (requested: ${timestampInSeconds}sec, end: ${endTimestampInSeconds})`));
2283
+ if (roundTo4Digits(adjustedTimestamp) > roundTo4Digits(endTimestampInSeconds)) {
2284
+ return Promise.reject(new Error(`Timestamp is after end timestamp (requested: ${timestampInSeconds}sec, end: ${endTimestampInSeconds}sec)`));
2270
2285
  }
2271
- await ensureEnoughFramesForTimestamp(timestampInSeconds);
2286
+ await ensureEnoughFramesForTimestamp(adjustedTimestamp);
2272
2287
  for (let i = frameTimestamps.length - 1;i >= 0; i--) {
2273
2288
  const sample = frames[frameTimestamps[i]];
2274
2289
  if (!sample) {
2275
2290
  return null;
2276
2291
  }
2277
- if (roundTo4Digits(sample.timestamp) <= roundTo4Digits(timestampInSeconds) || Math.abs(sample.timestamp - timestampInSeconds) <= 0.001) {
2292
+ if (roundTo4Digits(sample.timestamp) <= roundTo4Digits(adjustedTimestamp) || Math.abs(sample.timestamp - adjustedTimestamp) <= 0.001) {
2278
2293
  return sample;
2279
2294
  }
2280
2295
  }
@@ -2580,7 +2595,7 @@ var makeKeyframeManager = () => {
2580
2595
  }) => {
2581
2596
  const startPacket = await packetSink.getKeyPacket(timestamp, {
2582
2597
  verifyKeyPackets: true
2583
- });
2598
+ }) ?? await packetSink.getFirstPacket({ verifyKeyPackets: true });
2584
2599
  const hasAlpha = startPacket?.sideData.alpha;
2585
2600
  if (hasAlpha && !canBrowserUseWebGl2()) {
2586
2601
  return "has-alpha";
@@ -2882,6 +2897,7 @@ var extractAudioInternal = async ({
2882
2897
  const durationInSeconds = durationNotYetApplyingPlaybackRate * playbackRate;
2883
2898
  const samples = await sampleIterator.getSamples(timeInSeconds, durationInSeconds);
2884
2899
  audioManager.logOpenFrames();
2900
+ const trimStartToleranceInSeconds = 0.002;
2885
2901
  const audioDataArray = [];
2886
2902
  for (let i = 0;i < samples.length; i++) {
2887
2903
  const sample = samples[i];
@@ -2898,7 +2914,7 @@ var extractAudioInternal = async ({
2898
2914
  let trimEndInSeconds = 0;
2899
2915
  if (isFirstSample) {
2900
2916
  trimStartInSeconds = timeInSeconds - sample.timestamp;
2901
- if (trimStartInSeconds < 0 && trimStartInSeconds > -0.0000000001) {
2917
+ if (trimStartInSeconds < 0 && trimStartInSeconds > -trimStartToleranceInSeconds) {
2902
2918
  trimStartInSeconds = 0;
2903
2919
  }
2904
2920
  if (trimStartInSeconds < 0) {
@@ -3575,7 +3591,7 @@ var VideoForPreviewAssertedShowing = ({
3575
3591
  const [mediaPlayerReady, setMediaPlayerReady] = useState4(false);
3576
3592
  const [shouldFallbackToNativeVideo, setShouldFallbackToNativeVideo] = useState4(false);
3577
3593
  const [playing] = Timeline2.usePlayingState();
3578
- const timelineContext = useContext4(Timeline2.TimelineContext);
3594
+ const timelineContext = useContext4(Internals15.TimelineContext);
3579
3595
  const globalPlaybackRate = timelineContext.playbackRate;
3580
3596
  const sharedAudioContext = useContext4(SharedAudioContext2);
3581
3597
  const buffer = useBufferState2();
@@ -15,7 +15,7 @@ const VideoForPreviewAssertedShowing = ({ src: unpreloadedSrc, style, playbackRa
15
15
  const [mediaPlayerReady, setMediaPlayerReady] = useState(false);
16
16
  const [shouldFallbackToNativeVideo, setShouldFallbackToNativeVideo] = useState(false);
17
17
  const [playing] = Timeline.usePlayingState();
18
- const timelineContext = useContext(Timeline.TimelineContext);
18
+ const timelineContext = useContext(Internals.TimelineContext);
19
19
  const globalPlaybackRate = timelineContext.playbackRate;
20
20
  const sharedAudioContext = useContext(SharedAudioContext);
21
21
  const buffer = useBufferState();
@@ -68,23 +68,35 @@ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSecond
68
68
  };
69
69
  const getFrameFromTimestamp = async (timestampInSeconds) => {
70
70
  lastUsed = Date.now();
71
- if (timestampInSeconds < startTimestampInSeconds) {
72
- return Promise.reject(new Error(`Timestamp is before start timestamp (requested: ${timestampInSeconds}sec, start: ${startTimestampInSeconds})`));
71
+ // Videos may start slightly after timestamp 0 due to encoding, but if the requested timestamp is too far before the bank start, something is likely wrong.
72
+ const maxClampToleranceInSeconds = 0.1;
73
+ // If the requested timestamp is before the start of this bank, clamp it to the start if within tolerance. This handles videos that don't start at timestamp 0.
74
+ // For example, requesting frame at 0sec when video starts at 0.04sec should return the frame at 0.04sec.
75
+ // Test case: https://github.com/remotion-dev/remotion/issues/5915
76
+ let adjustedTimestamp = timestampInSeconds;
77
+ if (roundTo4Digits(timestampInSeconds) <
78
+ roundTo4Digits(startTimestampInSeconds)) {
79
+ const differenceInSeconds = startTimestampInSeconds - timestampInSeconds;
80
+ if (differenceInSeconds <= maxClampToleranceInSeconds) {
81
+ adjustedTimestamp = startTimestampInSeconds;
82
+ }
83
+ else {
84
+ return Promise.reject(new Error(`Timestamp is before start timestamp (requested: ${timestampInSeconds}sec, start: ${startTimestampInSeconds}sec, difference: ${differenceInSeconds.toFixed(3)}sec exceeds tolerance of ${maxClampToleranceInSeconds}sec)`));
85
+ }
73
86
  }
74
- if (timestampInSeconds > endTimestampInSeconds) {
75
- return Promise.reject(new Error(`Timestamp is after end timestamp (requested: ${timestampInSeconds}sec, end: ${endTimestampInSeconds})`));
87
+ if (roundTo4Digits(adjustedTimestamp) > roundTo4Digits(endTimestampInSeconds)) {
88
+ return Promise.reject(new Error(`Timestamp is after end timestamp (requested: ${timestampInSeconds}sec, end: ${endTimestampInSeconds}sec)`));
76
89
  }
77
- await ensureEnoughFramesForTimestamp(timestampInSeconds);
90
+ await ensureEnoughFramesForTimestamp(adjustedTimestamp);
78
91
  for (let i = frameTimestamps.length - 1; i >= 0; i--) {
79
92
  const sample = frames[frameTimestamps[i]];
80
93
  if (!sample) {
81
94
  return null;
82
95
  }
83
- if (roundTo4Digits(sample.timestamp) <=
84
- roundTo4Digits(timestampInSeconds) ||
96
+ if (roundTo4Digits(sample.timestamp) <= roundTo4Digits(adjustedTimestamp) ||
85
97
  // Match 0.3333333333 to 0.33355555
86
98
  // this does not satisfy the previous condition, since one rounds up and one rounds down
87
- Math.abs(sample.timestamp - timestampInSeconds) <= 0.001) {
99
+ Math.abs(sample.timestamp - adjustedTimestamp) <= 0.001) {
88
100
  return sample;
89
101
  }
90
102
  }
@@ -111,9 +111,12 @@ export const makeKeyframeManager = () => {
111
111
  await logCacheStats(logLevel);
112
112
  };
113
113
  const getKeyframeBankOrRefetch = async ({ packetSink, timestamp, videoSampleSink, src, logLevel, }) => {
114
- const startPacket = await packetSink.getKeyPacket(timestamp, {
114
+ // Try to get the keypacket at the requested timestamp.
115
+ // If it returns null (timestamp is before the first keypacket), fall back to the first packet.
116
+ // This matches mediabunny's internal behavior and handles videos that don't start at timestamp 0.
117
+ const startPacket = (await packetSink.getKeyPacket(timestamp, {
115
118
  verifyKeyPackets: true,
116
- });
119
+ })) ?? (await packetSink.getFirstPacket({ verifyKeyPackets: true }));
117
120
  const hasAlpha = startPacket?.sideData.alpha;
118
121
  if (hasAlpha && !canBrowserUseWebGl2()) {
119
122
  return 'has-alpha';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.371",
3
+ "version": "4.0.373",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -22,20 +22,20 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "mediabunny": "1.24.3",
25
- "remotion": "4.0.371",
26
- "webdriverio": "9.19.2"
25
+ "remotion": "4.0.373"
27
26
  },
28
27
  "peerDependencies": {
29
28
  "react": ">=16.8.0",
30
29
  "react-dom": ">=16.8.0"
31
30
  },
32
31
  "devDependencies": {
33
- "@remotion/eslint-config-internal": "4.0.371",
34
- "@vitest/browser": "^3.2.4",
32
+ "@remotion/eslint-config-internal": "4.0.373",
33
+ "@vitest/browser-webdriverio": "4.0.7",
35
34
  "eslint": "9.19.0",
36
35
  "react": "19.0.0",
37
36
  "react-dom": "19.0.0",
38
- "vitest": "3.2.4"
37
+ "vitest": "4.0.7",
38
+ "webdriverio": "9.19.2"
39
39
  },
40
40
  "keywords": [],
41
41
  "publishConfig": {