Skip to content

Commit f6fe8ac

Browse files
authored
Merge pull request #5338 from remotion-dev/dry-seek
2 parents ebf9ba3 + 4fd9b39 commit f6fe8ac

23 files changed

+276
-16
lines changed

.cursorignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
2+
packages/google-fonts

packages/docs/docs/media-parser/seeking.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,30 @@ The reason for this is that all samples need to be iterated over for these field
5858

5959
An error will be thrown if you attempt to do this.
6060

61+
## Simulate a seek<AvailableFrom v="4.0.312" />
62+
63+
You can simulate what would happen if you seeked to a certain time.
64+
65+
```tsx twoslash title="Basic seeking"
66+
import {parseMedia, mediaParserController} from '@remotion/media-parser';
67+
68+
const controller = mediaParserController();
69+
70+
await parseMedia({
71+
src: 'https://parser.media/video.mp4',
72+
controller,
73+
onVideoTrack: () => {
74+
return async () => {
75+
const result = await controller.simulateSeek(3);
76+
console.log(result); // { "type": "do-seek", byte: 5763, timeInSeconds: 0 }
77+
};
78+
},
79+
});
80+
```
81+
82+
This is useful if you are considering a seek and only want to execute it if the outcome is desired.
83+
The result will be a [`SeekResolution`](/docs/media-parser/types#seekresolution) object.
84+
6185
## How smart is seeking?
6286

6387
The efficiency of seeking depends on the container format.

packages/docs/docs/media-parser/types.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,11 @@ import type {M3uAssociatedPlaylist} from '@remotion/media-parser';
308308
- `name`: The name of the audio track
309309
- `groupId`: The group ID of the audio track
310310
- `channels`: The number of audio channels in the audio track, or `null`.
311+
312+
## `SeekResolution`<AvailableFrom v="4.0.312" />
313+
314+
```tsx twoslash
315+
import type {SeekResolution} from '@remotion/media-parser';
316+
// ^?
317+
```
318+

packages/media-parser/src/containers/aac/get-seeking-byte.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export const getSeekingByteForAac = ({
3737
}
3838

3939
if (bestAudioSample) {
40-
return {type: 'do-seek', byte: bestAudioSample.offset};
40+
return {
41+
type: 'do-seek',
42+
byte: bestAudioSample.offset,
43+
timeInSeconds: bestAudioSample.timeInSeconds,
44+
};
4145
}
4246

4347
return {type: 'valid-but-must-wait'};

packages/media-parser/src/containers/flac/get-seeking-byte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const getSeekingByteForFlac = ({
3535
}
3636

3737
if (bestAudioSample) {
38-
return bestAudioSample.offset;
38+
return bestAudioSample;
3939
}
4040

4141
return null;

packages/media-parser/src/containers/iso-base-media/find-keyframe-before-time.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,5 @@ export const findKeyframeBeforeTime = ({
5858
return null;
5959
}
6060

61-
return videoSample.offset;
61+
return videoSample;
6262
};

packages/media-parser/src/containers/iso-base-media/get-seeking-byte-from-fragmented-mp4.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ export const getSeekingByteFromFragmentedMp4 = async ({
116116
if (kf) {
117117
return {
118118
type: 'do-seek',
119-
byte: kf,
119+
byte: kf.offset,
120+
timeInSeconds:
121+
Math.min(kf.decodingTimestamp, kf.timestamp) /
122+
firstTrack.originalTimescale,
120123
};
121124
}
122125
}

packages/media-parser/src/containers/iso-base-media/get-seeking-byte.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ export const getSeekingByteFromIsoBaseMedia = ({
8686
if (keyframe) {
8787
return Promise.resolve({
8888
type: 'do-seek',
89-
byte: keyframe,
89+
byte: keyframe.offset,
90+
timeInSeconds:
91+
Math.min(keyframe.decodingTimestamp, keyframe.timestamp) /
92+
track.originalTimescale,
9093
});
9194
}
9295

packages/media-parser/src/containers/m3u/get-seeking-byte.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,7 @@ export const getSeekingByteForM3u8 = ({
4747
return {
4848
type: 'do-seek',
4949
byte: currentPosition,
50+
// TODO: This will be imperfect when seeking in playMedia()
51+
timeInSeconds: time,
5052
};
5153
};

packages/media-parser/src/containers/mp3/get-seeking-byte.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@ export const getSeekingByteForMp3 = ({
5353
};
5454
}
5555

56+
const byte = Math.max(...candidates);
57+
const timeInSeconds =
58+
byte === bestAudioSample?.offset ? bestAudioSample.timeInSeconds : time;
59+
5660
return {
5761
type: 'do-seek',
58-
byte: Math.max(...candidates),
62+
byte,
63+
timeInSeconds,
5964
};
6065
};

packages/media-parser/src/containers/riff/get-seeking-byte.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export const getSeekingByteForRiff = async ({
3939
return {
4040
type: 'do-seek',
4141
byte: lastKeyframe.positionInBytes,
42+
timeInSeconds: Math.min(
43+
lastKeyframe.decodingTimeInSeconds,
44+
lastKeyframe.presentationTimeInSeconds,
45+
),
4246
};
4347
}
4448

@@ -85,5 +89,8 @@ export const getSeekingByteForRiff = async ({
8589
return {
8690
type: 'do-seek',
8791
byte: bestEntry.offset + info.moviOffset - 4,
92+
timeInSeconds:
93+
bestEntry.sampleCounts[idx1Entries.videoTrackIndex] /
94+
info.samplesPerSecond,
8895
};
8996
};

packages/media-parser/src/containers/wav/get-seeking-byte.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ export const getSeekingByteFromWav = ({
2323
return Promise.resolve({
2424
type: 'do-seek',
2525
byte: byteOffset + info.mediaSection.start,
26+
timeInSeconds: timeRoundedDown,
2627
});
2728
};

packages/media-parser/src/containers/webm/seek/get-seeking-byte.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const findKeyframeBeforeTime = ({
5959
}
6060
}
6161

62-
return keyframeBeforeTime?.positionInBytes ?? null;
62+
return keyframeBeforeTime ?? null;
6363
};
6464

6565
const getByteFromCues = ({
@@ -92,7 +92,10 @@ const getByteFromCues = ({
9292
return null;
9393
}
9494

95-
return biggestCueBeforeTime.clusterPositionInSegment + segmentOffset;
95+
return {
96+
byte: biggestCueBeforeTime.clusterPositionInSegment + segmentOffset,
97+
timeInSeconds: toSeconds(biggestCueBeforeTime.timeInTimescale, info.track!),
98+
};
9699
};
97100

98101
export const getSeekingByteFromMatroska = async ({
@@ -140,8 +143,8 @@ export const getSeekingByteFromMatroska = async ({
140143
// Don't seek back, if the last seen time is smaller than the time we want to seek to
141144

142145
const seekPossibilities = [
143-
byteFromCues,
144-
byteFromObservedKeyframe,
146+
byteFromCues?.byte ?? null,
147+
byteFromObservedKeyframe?.positionInBytes ?? null,
145148
byteFromFirstMediaSection,
146149
].filter((n) => n !== null);
147150

@@ -163,8 +166,28 @@ export const getSeekingByteFromMatroska = async ({
163166
size: 1,
164167
});
165168

169+
const timeInSeconds = (() => {
170+
if (byteToSeekTo === byteFromObservedKeyframe?.positionInBytes) {
171+
return Math.min(
172+
byteFromObservedKeyframe.decodingTimeInSeconds,
173+
byteFromObservedKeyframe.presentationTimeInSeconds,
174+
);
175+
}
176+
177+
if (byteToSeekTo === byteFromCues?.byte) {
178+
return byteFromCues.timeInSeconds;
179+
}
180+
181+
if (byteToSeekTo === byteFromFirstMediaSection) {
182+
return 0;
183+
}
184+
185+
throw new Error('Should not happen');
186+
})();
187+
166188
return {
167189
type: 'do-seek',
168190
byte: byteToSeekTo,
191+
timeInSeconds,
169192
};
170193
};

packages/media-parser/src/controller/media-parser-controller.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {MediaParserAbortError} from '../errors';
22
import type {SeekingHints} from '../seeking-hints';
3+
import type {SeekResolution} from '../work-on-seek-request';
34
import {MediaParserEmitter} from './emitter';
45
import type {PauseSignal} from './pause-signal';
56
import {makePauseSignal} from './pause-signal';
@@ -14,6 +15,7 @@ export type MediaParserController = {
1415
pause: PauseSignal['pause'];
1516
resume: PauseSignal['resume'];
1617
seek: SeekSignal['seek'];
18+
simulateSeek: (seekInSeconds: number) => Promise<SeekResolution>;
1719
addEventListener: MediaParserEmitter['addEventListener'];
1820
removeEventListener: MediaParserEmitter['removeEventListener'];
1921
getSeekingHints: () => Promise<SeekingHints | null>;
@@ -29,6 +31,9 @@ export type MediaParserController = {
2931
attachSeekingHintResolution: (
3032
callback: () => Promise<SeekingHints | null>,
3133
) => void;
34+
attachSimulateSeekResolution: (
35+
callback: (seekInSeconds: number) => Promise<SeekResolution>,
36+
) => void;
3237
};
3338
};
3439

@@ -53,6 +58,9 @@ export const mediaParserController = (): MediaParserController => {
5358
};
5459

5560
let seekingHintResolution: (() => Promise<SeekingHints | null>) | null = null;
61+
let simulateSeekResolution:
62+
| ((seekInSeconds: number) => Promise<SeekResolution>)
63+
| null = null;
5664

5765
const getSeekingHints = () => {
5866
if (!seekingHintResolution) {
@@ -64,6 +72,16 @@ export const mediaParserController = (): MediaParserController => {
6472
return seekingHintResolution();
6573
};
6674

75+
const simulateSeek = (seekInSeconds: number) => {
76+
if (!simulateSeekResolution) {
77+
throw new Error(
78+
'The mediaParserController() was not yet used in a parseMedia() call',
79+
);
80+
}
81+
82+
return simulateSeekResolution(seekInSeconds);
83+
};
84+
6785
const attachSeekingHintResolution = (
6886
callback: () => Promise<SeekingHints | null>,
6987
) => {
@@ -76,13 +94,26 @@ export const mediaParserController = (): MediaParserController => {
7694
seekingHintResolution = callback;
7795
};
7896

97+
const attachSimulateSeekResolution = (
98+
callback: (seekInSeconds: number) => Promise<SeekResolution>,
99+
) => {
100+
if (simulateSeekResolution) {
101+
throw new Error(
102+
'The mediaParserController() was used in multiple parseMedia() calls. Create a separate controller for each call.',
103+
);
104+
}
105+
106+
simulateSeekResolution = callback;
107+
};
108+
79109
return {
80110
// eslint-disable-next-line @typescript-eslint/no-explicit-any
81111
abort: (reason?: any) => {
82112
abortController.abort(reason);
83113
emitter.dispatchAbort(reason);
84114
},
85115
seek: seekSignal.seek,
116+
simulateSeek,
86117
pause: pauseSignal.pause,
87118
resume: pauseSignal.resume,
88119
addEventListener: emitter.addEventListener,
@@ -95,6 +126,7 @@ export const mediaParserController = (): MediaParserController => {
95126
markAsReadyToEmitEvents: emitter.markAsReady,
96127
performedSeeksSignal,
97128
attachSeekingHintResolution,
129+
attachSimulateSeekResolution,
98130
},
99131
};
100132
};

packages/media-parser/src/get-seeking-byte.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {getSeekingByteFromIsoBaseMedia} from './containers/iso-base-media/get-se
44
import {getSeekingByteForM3u8} from './containers/m3u/get-seeking-byte';
55
import {getSeekingByteForMp3} from './containers/mp3/get-seeking-byte';
66
import {getSeekingByteForRiff} from './containers/riff/get-seeking-byte';
7+
import {MPEG_TIMESCALE} from './containers/transport-stream/handle-avc-packet';
78
import {getSeekingByteFromWav} from './containers/wav/get-seeking-byte';
89
import {getSeekingByteFromMatroska} from './containers/webm/seek/get-seeking-byte';
910
import type {MediaParserLogLevel} from './log';
@@ -86,7 +87,8 @@ export const getSeekingByte = ({
8687
if (byte) {
8788
return Promise.resolve({
8889
type: 'do-seek',
89-
byte,
90+
byte: byte.offset,
91+
timeInSeconds: byte.timeInSeconds,
9092
});
9193
}
9294

@@ -102,12 +104,27 @@ export const getSeekingByte = ({
102104
ptsStartOffset: info.ptsStartOffset,
103105
});
104106

105-
const byte = lastKeyframeBeforeTimeInSeconds?.offset ?? 0;
107+
if (!lastKeyframeBeforeTimeInSeconds) {
108+
transportStream.resetBeforeSeek();
109+
110+
return Promise.resolve({
111+
type: 'do-seek',
112+
byte: 0,
113+
timeInSeconds: 0,
114+
});
115+
}
116+
117+
const byte = lastKeyframeBeforeTimeInSeconds.offset;
106118

107119
transportStream.resetBeforeSeek();
108120
return Promise.resolve({
109121
type: 'do-seek',
110122
byte,
123+
timeInSeconds:
124+
Math.min(
125+
lastKeyframeBeforeTimeInSeconds.pts,
126+
lastKeyframeBeforeTimeInSeconds.dts ?? Infinity,
127+
) / MPEG_TIMESCALE,
111128
});
112129
}
113130

packages/media-parser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export {
124124
} from './controller/media-parser-controller';
125125
export {VERSION} from './version';
126126
export {WEBCODECS_TIMESCALE} from './webcodecs-timescale';
127+
export type {SeekResolution} from './work-on-seek-request';
127128

128129
export type {MediaParserSampleAspectRatio} from './get-tracks';
129130

0 commit comments

Comments
 (0)