Skip to content

Commit 1e5f596

Browse files
authored
Merge pull request #5324 from remotion-dev/extract-frames
2 parents 9e12aff + b5f2801 commit 1e5f596

34 files changed

+733
-77
lines changed

packages/bundler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"remotion": "workspace:*",
3232
"@remotion/studio": "workspace:*",
3333
"@remotion/studio-shared": "workspace:*",
34+
"@remotion/media-parser": "workspace:*",
3435
"style-loader": "4.0.0",
3536
"source-map": "0.7.3",
3637
"webpack": "5.96.1"

packages/bundler/src/webpack-config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ export const webpackConfig = async ({
170170
'esm',
171171
'index.mjs',
172172
),
173+
'@remotion/media-parser/worker': path.resolve(
174+
require.resolve('@remotion/media-parser'),
175+
'..',
176+
'esm',
177+
'worker.mjs',
178+
),
173179
// test visual controls before removing this
174180
'@remotion/studio': require.resolve('@remotion/studio'),
175181
'react-dom/client': shouldUseReactDomClient

packages/convert/app/components/ConvertUi.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ const ConvertUI = ({
413413
...unrotatedDimensions,
414414
rotation: userRotation - (rotation ?? 0),
415415
resizeOperation,
416-
videoCodec: isH264Reencode ? 'h264' : 'vp8',
416+
needsToBeMultipleOfTwo: isH264Reencode ?? false,
417417
});
418418
}, [
419419
unrotatedDimensions,

packages/docs/docs/webcodecs/TableOfContents.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ export const TableOfContents: React.FC = () => {
122122
Create a <code>VideoDecoder</code> object.
123123
</div>
124124
</TOCItem>
125+
<TOCItem link="/docs/webcodecs/extract-frames">
126+
<strong>{'extractFrames()'}</strong>
127+
<div>Extract frames from a video at specific timestamps.</div>
128+
</TOCItem>
125129
</Grid>
126130
</div>
127131
);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
image: /generated/articles-docs-webcodecs-extract-frames.png
3+
id: extract-frames
4+
title: extractFrames()
5+
slug: /webcodecs/extract-frames
6+
crumb: '@remotion/webcodecs'
7+
---
8+
9+
# extractFrames()<AvailableFrom v="4.0.311"/>
10+
11+
_Part of the [`@remotion/webcodecs`](/docs/webcodecs) package._
12+
13+
Extracts frames from a video at specific timestamps.
14+
15+
```tsx twoslash title="Extracting frames"
16+
import {extractFrames} from '@remotion/webcodecs';
17+
18+
await extractFrames({
19+
src: 'https://parser.media/video.mp4',
20+
timestampsInSeconds: [0, 1, 2, 3, 4],
21+
onFrame: (frame) => {
22+
console.log(frame);
23+
// ^?
24+
},
25+
});
26+
```
27+
28+
## API
29+
30+
### `src`
31+
32+
A URL or `File`/`Blob`.
33+
34+
If it is a remote URL, it must support CORS.
35+
36+
### `timestampsInSeconds`
37+
38+
An array of timestamps in seconds, or a function that returns a promise resolving to an array of timestamps in seconds based on the video track.
39+
40+
Consider you wanting you to create a filmstrip of a video. You can do this by extracting as many frames as fit in a canvas.
41+
42+
```tsx twoslash title="Extracting as many frames as fit in a canvas"
43+
import type {ExtractFramesTimestampsInSecondsFn} from '@remotion/webcodecs';
44+
45+
const toSeconds = 10;
46+
const fromSeconds = 0;
47+
const canvasWidth = 500;
48+
const canvasHeight = 80;
49+
50+
const timestamps: ExtractFramesTimestampsInSecondsFn = async ({track}) => {
51+
const aspectRatio = track.width / track.height;
52+
const amountOfFramesFit = Math.ceil(
53+
canvasWidth / (canvasHeight * aspectRatio),
54+
);
55+
const timestampsInSeconds: number[] = [];
56+
const segmentDuration = toSeconds - fromSeconds;
57+
58+
for (let i = 0; i < amountOfFramesFit; i++) {
59+
timestampsInSeconds.push(
60+
fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5),
61+
);
62+
}
63+
64+
return timestampsInSeconds;
65+
};
66+
```
67+
68+
Note that currently, you can not get the duration of the video in seconds before the extraction.
69+
For this you need currently to make another [`parseMedia()`](/docs/media-parser/parse-media) call beforehand.
70+
71+
### `onFrame`
72+
73+
A callback that will be called with the frame at the given timestamp.
74+
Each frame is a [`VideoFrame`](https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame) object that can for example be drawn to a canvas.
75+
76+
77+
### `acknowledgeRemotionLicense?`
78+
79+
Acknowledge the [Remotion License](/docs/license) to make the console message disappear.
80+
81+
### `signal?`
82+
83+
An optional [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to abort the extraction.
84+
85+
### `logLevel?`
86+
87+
_string_ <TsType type="LogLevel" source="@remotion/media-parser"/>
88+
89+
One of `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`.
90+
Default value: `"info"`, which logs only important information.
91+
92+
## See also
93+
94+
- [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/webcodecs/src/extract-frames.ts)
95+
- [`@remotion/webcodecs`](/docs/webcodecs)
96+
- [`parseMedia()`](/docs/media-parser/parse-media)

packages/docs/docs/webcodecs/index.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ It leverages [`@remotion/media-parser`](/docs/media-parser) to parse the video a
1616
import {LicenseDisclaimer} from './LicenseDisclaimer';
1717
import {UnstableDisclaimer} from './UnstableDisclaimer';
1818

19-
## What can you to with this package?
19+
## What can you do with this package?
2020

2121
In browsers that implement WebCodecs, you can use this package to:
2222

2323
- [Convert videos from one format to another](/docs/webcodecs/convert-a-video) (From .mp4, .webm, .mov, .mkv, .m3u8, .ts, .avi, .mp3, .flac, .wav, .m4a, .aac to .mp4, .webm, .wav)
2424
- [Rotate videos](/docs/webcodecs/rotate-a-video)
25+
- [Efficiently extract frames from a video](/docs/webcodecs/extract-frames)
2526
- Extract audio from a video
2627
- Manipulate the pixels of a video
2728
- [Fix videos that were recorded with `MediaRecorder`](/docs/webcodecs/fix-mediarecorder-video)

packages/docs/sidebars.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ module.exports = {
685685
'webcodecs/convert-audiodata',
686686
'webcodecs/create-audio-decoder',
687687
'webcodecs/create-video-decoder',
688+
'webcodecs/extract-frames',
688689
],
689690
},
690691
{

packages/docs/src/data/articles.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2971,7 +2971,7 @@ export const articles = [
29712971
},
29722972
{
29732973
id: 'webcodecs',
2974-
title: 'Decoding a video with WebCodecs and @remotion/media-parser',
2974+
title: 'Processing video with WebCodecs and @remotion/media-parser',
29752975
relativePath: 'docs/media-parser/webcodecs.mdx',
29762976
compId: 'articles-docs-media-parser-webcodecs',
29772977
crumb: '@remotion/media-parser',
@@ -6045,6 +6045,15 @@ export const articles = [
60456045
noAi: false,
60466046
slug: 'webcodecs/default-on-video-track-handler',
60476047
},
6048+
{
6049+
id: 'extract-frames',
6050+
title: 'extractFrames()',
6051+
relativePath: 'docs/webcodecs/extract-frames.mdx',
6052+
compId: 'articles-docs-webcodecs-extract-frames',
6053+
crumb: '@remotion/webcodecs',
6054+
noAi: false,
6055+
slug: 'webcodecs/extract-frames',
6056+
},
60486057
{
60496058
id: 'fix-mediarecorder-video',
60506059
title: 'Fixing a MediaRecorder video',
Loading

packages/media-parser/src/parse-media-on-worker-entry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ const convertToWorkerPayload = (
9393
postM3uAssociatedPlaylistsSelection: Boolean(selectM3uAssociatedPlaylists),
9494
postOnAudioTrack: Boolean(onAudioTrack),
9595
postOnVideoTrack: Boolean(onVideoTrack),
96-
src,
96+
// URL cannot be serialized, so we convert it to a string
97+
src: src instanceof URL ? src.toString() : src,
9798
};
9899
};
99100

packages/media-parser/src/worker/serialize-error.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ export const serializeError = ({
5454
};
5555
}
5656

57+
if (error instanceof TypeError) {
58+
return {
59+
type: 'response-error',
60+
errorName: 'TypeError',
61+
errorMessage: error.message,
62+
errorStack: error.stack ?? '',
63+
};
64+
}
65+
5766
if (error.name === 'AbortError') {
5867
return {
5968
type: 'response-error',
@@ -121,6 +130,8 @@ export const deserializeError = (error: ResponseError): Error => {
121130
// TODO: Document 2GB limit
122131
case 'NotReadableError':
123132
return new Error(error.errorMessage);
133+
case 'TypeError':
134+
return new TypeError(error.errorMessage);
124135
default:
125136
throw new Error(`Unknown error name: ${error satisfies never}`);
126137
}

packages/media-parser/src/worker/worker-types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ type NotReadableError = BaseError & {
125125
errorName: 'NotReadableError';
126126
};
127127

128+
type TypeError = BaseError & {
129+
errorName: 'TypeError';
130+
};
131+
128132
type IsAnImageError = BaseError & {
129133
errorName: 'IsAnImageError';
130134
imageType: ImageType;
@@ -161,7 +165,8 @@ type AnyError =
161165
| MediaParserAbortError
162166
// browser native errors
163167
| AbortError
164-
| NotReadableError;
168+
| NotReadableError
169+
| TypeError;
165170

166171
export type ResponseError = {
167172
type: 'response-error';

packages/studio/bundle.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const external = [
1313
'@remotion/renderer/client',
1414
'@remotion/renderer/pure',
1515
'@remotion/renderer/error-handling',
16+
'@remotion/media-parser/worker',
17+
'@remotion/webcodecs',
1618
'source-map',
1719
'zod',
1820
'remotion/no-react',

packages/studio/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@remotion/media-parser": "workspace:*",
3232
"@remotion/renderer": "workspace:*",
3333
"@remotion/studio-shared": "workspace:*",
34+
"@remotion/webcodecs": "workspace:*",
3435
"@remotion/zod-types": "workspace:*",
3536
"memfs": "3.4.3",
3637
"source-map": "0.7.3",

packages/studio/src/components/Timeline/TimelineDragHandler.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const container: React.CSSProperties = {
5555
const style: React.CSSProperties = {
5656
width: '100%',
5757
height: '100%',
58+
userSelect: 'none',
59+
WebkitUserSelect: 'none',
5860
};
5961

6062
const getClientXWithScroll = (x: number) => {

packages/studio/src/components/Timeline/TimelineSequence.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,14 @@ const Inner: React.FC<{
120120
playbackRate={s.playbackRate}
121121
/>
122122
) : null}
123-
{s.type === 'video' ? <TimelineVideoInfo src={s.src} /> : null}
123+
{s.type === 'video' ? (
124+
<TimelineVideoInfo
125+
src={s.src}
126+
visualizationWidth={width}
127+
startFrom={s.startMediaFrom}
128+
durationInFrames={s.duration}
129+
/>
130+
) : null}
124131
{s.loopDisplay === undefined ? null : (
125132
<LoopedTimelineIndicator loops={s.loopDisplay.numberOfTimes} />
126133
)}

0 commit comments

Comments
 (0)