varg.ai-sdk 0.1.0
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.
- package/.claude/settings.local.json +7 -0
- package/.env.example +24 -0
- package/CLAUDE.md +118 -0
- package/README.md +231 -0
- package/SKILLS.md +157 -0
- package/STRUCTURE.md +92 -0
- package/TEST_RESULTS.md +122 -0
- package/action/captions/SKILL.md +170 -0
- package/action/captions/index.ts +227 -0
- package/action/edit/SKILL.md +235 -0
- package/action/edit/index.ts +493 -0
- package/action/image/SKILL.md +140 -0
- package/action/image/index.ts +112 -0
- package/action/sync/SKILL.md +136 -0
- package/action/sync/index.ts +187 -0
- package/action/transcribe/SKILL.md +179 -0
- package/action/transcribe/index.ts +227 -0
- package/action/video/SKILL.md +116 -0
- package/action/video/index.ts +135 -0
- package/action/voice/SKILL.md +125 -0
- package/action/voice/index.ts +201 -0
- package/biome.json +33 -0
- package/index.ts +38 -0
- package/lib/README.md +144 -0
- package/lib/ai-sdk/fal.ts +106 -0
- package/lib/ai-sdk/replicate.ts +107 -0
- package/lib/elevenlabs.ts +382 -0
- package/lib/fal.ts +478 -0
- package/lib/ffmpeg.ts +467 -0
- package/lib/fireworks.ts +235 -0
- package/lib/groq.ts +246 -0
- package/lib/higgsfield.ts +176 -0
- package/lib/remotion/SKILL.md +823 -0
- package/lib/remotion/cli.ts +115 -0
- package/lib/remotion/functions.ts +283 -0
- package/lib/remotion/index.ts +19 -0
- package/lib/remotion/templates.ts +73 -0
- package/lib/replicate.ts +304 -0
- package/output.txt +1 -0
- package/package.json +35 -0
- package/pipeline/cookbooks/SKILL.md +285 -0
- package/pipeline/cookbooks/remotion-video.md +585 -0
- package/pipeline/cookbooks/round-video-character.md +337 -0
- package/pipeline/cookbooks/talking-character.md +59 -0
- package/test-import.ts +7 -0
- package/test-services.ts +97 -0
- package/tsconfig.json +29 -0
- package/utilities/s3.ts +147 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# remotion skill
|
|
2
|
+
|
|
3
|
+
## overview
|
|
4
|
+
programmatic video creation with react components using remotion
|
|
5
|
+
|
|
6
|
+
## quick start
|
|
7
|
+
```bash
|
|
8
|
+
# 1. create composition with template files
|
|
9
|
+
bun run lib/remotion/index.ts create MyVideo
|
|
10
|
+
# creates: lib/remotion/compositions/MyVideo.tsx (composition component)
|
|
11
|
+
# lib/remotion/compositions/MyVideo.root.tsx (root with registerRoot)
|
|
12
|
+
|
|
13
|
+
# 2. copy media files to public directory
|
|
14
|
+
mkdir -p lib/remotion/public
|
|
15
|
+
cp media/video.mp4 media/audio.mp3 lib/remotion/public/
|
|
16
|
+
|
|
17
|
+
# 3. customize the generated composition files
|
|
18
|
+
# - edit MyVideo.tsx to add your video/image/audio content
|
|
19
|
+
# - edit MyVideo.root.tsx to set fps, duration, width, height
|
|
20
|
+
|
|
21
|
+
# 4. render
|
|
22
|
+
bun run lib/remotion/index.ts render lib/remotion/compositions/MyVideo.root.tsx MyVideo output.mp4
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**important**: always use `staticFile("filename.ext")` for media paths, never absolute paths
|
|
26
|
+
|
|
27
|
+
## what you can use remotion for
|
|
28
|
+
|
|
29
|
+
### 1. video editing
|
|
30
|
+
- trim videos to specific frame ranges
|
|
31
|
+
- adjust playback speed (slow motion, time-lapse)
|
|
32
|
+
- apply filters and color grading with CSS
|
|
33
|
+
- overlay graphics and text
|
|
34
|
+
- create picture-in-picture effects
|
|
35
|
+
|
|
36
|
+
### 2. zooming and panning
|
|
37
|
+
- smooth zoom in/out effects with `interpolate()`
|
|
38
|
+
- ken burns effect on static images
|
|
39
|
+
- dynamic camera movements
|
|
40
|
+
- focus on specific areas frame-by-frame
|
|
41
|
+
|
|
42
|
+
### 3. combining multiple videos
|
|
43
|
+
- concatenate videos sequentially (one after another)
|
|
44
|
+
- play videos side-by-side or in grid layouts
|
|
45
|
+
- layer videos with opacity/blend modes
|
|
46
|
+
- transition between scenes with crossfades
|
|
47
|
+
|
|
48
|
+
### 4. audio mixing
|
|
49
|
+
- combine multiple audio tracks
|
|
50
|
+
- sync audio with video
|
|
51
|
+
- adjust volume levels with `interpolate()`
|
|
52
|
+
- add background music and sound effects
|
|
53
|
+
- fade in/out audio
|
|
54
|
+
|
|
55
|
+
### 5. beautiful subtitles
|
|
56
|
+
- word-by-word animated captions
|
|
57
|
+
- styled text with custom fonts and colors
|
|
58
|
+
- background boxes for readability
|
|
59
|
+
- position captions anywhere on screen
|
|
60
|
+
- karaoke-style highlighting
|
|
61
|
+
- emoji support and rich formatting
|
|
62
|
+
|
|
63
|
+
### 6. thumbnail generation
|
|
64
|
+
- render specific frames as stills (using remotion's renderStill)
|
|
65
|
+
- create custom thumbnail compositions with text/graphics
|
|
66
|
+
- generate multiple preview frames at different timestamps
|
|
67
|
+
- design animated thumbnail previews
|
|
68
|
+
|
|
69
|
+
### 7. advanced effects
|
|
70
|
+
- motion graphics and animations
|
|
71
|
+
- data visualizations synchronized with narration
|
|
72
|
+
- dynamic text reveals
|
|
73
|
+
- progress bars and timers
|
|
74
|
+
- responsive layouts that adapt to content
|
|
75
|
+
|
|
76
|
+
## capabilities
|
|
77
|
+
|
|
78
|
+
### composition creation
|
|
79
|
+
- create composition structure with `bun run lib/remotion/index.ts create <name>`
|
|
80
|
+
- automatically generates template files:
|
|
81
|
+
- `<name>.tsx` - composition component with all necessary imports
|
|
82
|
+
- `<name>.root.tsx` - root file with registerRoot() already configured
|
|
83
|
+
- files are ready to customize with your content
|
|
84
|
+
- media files go in `lib/remotion/public/`
|
|
85
|
+
|
|
86
|
+
### composition editing
|
|
87
|
+
- write react components to create video scenes
|
|
88
|
+
- use remotion's `<OffthreadVideo>`, `<Audio>`, `<Img>` components
|
|
89
|
+
- reference media with `staticFile("filename.mp4")` helper
|
|
90
|
+
- add animations with `useCurrentFrame()` and `interpolate()`
|
|
91
|
+
- parse and display subtitles/captions
|
|
92
|
+
- combine multiple videos sequentially or in parallel
|
|
93
|
+
|
|
94
|
+
### root file setup
|
|
95
|
+
- must use `registerRoot()` function (not export)
|
|
96
|
+
- register compositions with `<Composition>` component
|
|
97
|
+
- specify id, component, durationInFrames, fps, width, height
|
|
98
|
+
|
|
99
|
+
### rendering
|
|
100
|
+
- bundle project with webpack automatically
|
|
101
|
+
- render compositions to mp4 video with h264 codec
|
|
102
|
+
- render single frames as images (thumbnails)
|
|
103
|
+
- track rendering progress in real-time
|
|
104
|
+
|
|
105
|
+
## common patterns
|
|
106
|
+
|
|
107
|
+
### 1. create video with captions
|
|
108
|
+
```typescript
|
|
109
|
+
import { createProject, render } from "lib/remotion";
|
|
110
|
+
|
|
111
|
+
// create project
|
|
112
|
+
const project = await createProject();
|
|
113
|
+
|
|
114
|
+
// edit composition (add to src/MyComp.tsx)
|
|
115
|
+
// - add Video component with staticFile("video.mp4")
|
|
116
|
+
// - parse SRT file and display captions
|
|
117
|
+
// - use useCurrentFrame() to sync captions with video
|
|
118
|
+
|
|
119
|
+
// render
|
|
120
|
+
await render({
|
|
121
|
+
entryPoint: project.entryPoint,
|
|
122
|
+
compositionId: "MyComp",
|
|
123
|
+
outputPath: "output.mp4"
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2. concatenate videos
|
|
128
|
+
```typescript
|
|
129
|
+
// in composition:
|
|
130
|
+
const frame = useCurrentFrame();
|
|
131
|
+
const video1Duration = 1430; // frames
|
|
132
|
+
|
|
133
|
+
{frame < video1Duration ? (
|
|
134
|
+
<Video src={staticFile("video1.mp4")} />
|
|
135
|
+
) : (
|
|
136
|
+
<Video
|
|
137
|
+
src={staticFile("video2.mp4")}
|
|
138
|
+
startFrom={frame - video1Duration}
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3. add styled captions
|
|
144
|
+
```typescript
|
|
145
|
+
interface Subtitle {
|
|
146
|
+
startTime: number;
|
|
147
|
+
endTime: number;
|
|
148
|
+
text: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const currentTime = frame / fps;
|
|
152
|
+
const subtitle = subtitles.find(
|
|
153
|
+
s => currentTime >= s.startTime && currentTime <= s.endTime
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
{subtitle && (
|
|
157
|
+
<div style={{
|
|
158
|
+
fontSize: 48,
|
|
159
|
+
fontWeight: "bold",
|
|
160
|
+
color: "white",
|
|
161
|
+
backgroundColor: "rgba(0,0,0,0.7)",
|
|
162
|
+
padding: "20px 40px",
|
|
163
|
+
borderRadius: 12
|
|
164
|
+
}}>
|
|
165
|
+
{subtitle.text}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## important notes
|
|
171
|
+
|
|
172
|
+
### audio/voiceover duration
|
|
173
|
+
- always probe audio files before setting composition duration
|
|
174
|
+
- voiceovers are often much longer than you think (can be 30s, 60s, or more)
|
|
175
|
+
- never assume voiceover duration - always check with ffmpeg probe
|
|
176
|
+
- common mistake: setting durationInFrames too short, cutting off audio
|
|
177
|
+
- workflow:
|
|
178
|
+
1. probe voiceover: `bun run lib/ffmpeg.ts probe media/voiceover.mp3`
|
|
179
|
+
2. note the duration (e.g., 45.2 seconds)
|
|
180
|
+
3. calculate frames: `45.2 * 30fps = 1356 frames`
|
|
181
|
+
4. set composition `durationInFrames` to match or exceed this
|
|
182
|
+
- if video is shorter than audio, you need to either:
|
|
183
|
+
- extend video with images/broll to match audio length
|
|
184
|
+
- trim the audio to match video length
|
|
185
|
+
- verify before rendering: check that composition duration >= audio duration
|
|
186
|
+
|
|
187
|
+
### image dimensions and aspect ratios
|
|
188
|
+
- be mindful of image aspect ratios vs composition dimensions
|
|
189
|
+
- images may not fill the frame properly (leaving black bars or getting cropped)
|
|
190
|
+
- common solutions:
|
|
191
|
+
- `objectFit: "cover"` - fills frame but crops image
|
|
192
|
+
- `objectFit: "contain"` - fits full image but may leave black bars
|
|
193
|
+
- **blurred background technique** - best of both worlds:
|
|
194
|
+
1. layer 1 (background): same image scaled to fill, with blur filter
|
|
195
|
+
2. layer 2 (foreground): full image fitted with `objectFit: "contain"`
|
|
196
|
+
3. result: no black bars, full image visible, aesthetic blurred background
|
|
197
|
+
- always check how images look in final composition, especially portrait images in landscape frames
|
|
198
|
+
|
|
199
|
+
### media file paths
|
|
200
|
+
- copy media to `public/` directory in project
|
|
201
|
+
- use `staticFile("filename.mp4")` to reference
|
|
202
|
+
- absolute paths won't work in remotion
|
|
203
|
+
- **CRITICAL**: staticFile() caches based on filename
|
|
204
|
+
- if you overwrite files (e.g., `before.jpg`, `after.jpg`) between renders, Remotion will cache the LAST version for ALL renders
|
|
205
|
+
- solution: use unique filenames for each variation (e.g., `woman-01-before.jpg`, `woman-02-before.jpg`)
|
|
206
|
+
- for variations: pass unique identifiers as props and use template strings: `staticFile(\`image-${id}.jpg\`)`
|
|
207
|
+
|
|
208
|
+
### frame-based timing
|
|
209
|
+
- everything in remotion is frame-based
|
|
210
|
+
- calculate duration: `frames = seconds * fps`
|
|
211
|
+
- get current time: `currentTime = frame / fps`
|
|
212
|
+
|
|
213
|
+
### video concatenation
|
|
214
|
+
- calculate end frame of first video
|
|
215
|
+
- start second video at that frame
|
|
216
|
+
- adjust `startFrom` prop for proper timing
|
|
217
|
+
|
|
218
|
+
### composition registration
|
|
219
|
+
- register compositions in `src/Root.tsx`
|
|
220
|
+
- specify id, width, height, fps, durationInFrames
|
|
221
|
+
- use unique composition ids
|
|
222
|
+
- for multiple variations: use `Array.from()` to generate compositions programmatically
|
|
223
|
+
- example: `Array.from({ length: 15 }, (_, i) => { ... })`
|
|
224
|
+
- pass unique props via `defaultProps: { variationId: "01" }`
|
|
225
|
+
- each composition can render different content based on props
|
|
226
|
+
|
|
227
|
+
## typical workflow
|
|
228
|
+
|
|
229
|
+
1. **probe all media files** (especially audio/voiceover - they're often very long)
|
|
230
|
+
```bash
|
|
231
|
+
# probe video to get duration, fps, resolution
|
|
232
|
+
bun run lib/ffmpeg.ts probe media/video.mp4
|
|
233
|
+
|
|
234
|
+
# probe voiceover/audio to get true duration
|
|
235
|
+
bun run lib/ffmpeg.ts probe media/voiceover.mp3
|
|
236
|
+
# voiceovers can be 30s, 60s, or more - never assume
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
2. **create composition with templates**
|
|
240
|
+
```bash
|
|
241
|
+
bun run lib/remotion/index.ts create MyVideo
|
|
242
|
+
```
|
|
243
|
+
this automatically creates:
|
|
244
|
+
- `lib/remotion/compositions/MyVideo.tsx` (composition component)
|
|
245
|
+
- `lib/remotion/compositions/MyVideo.root.tsx` (root file with registerRoot)
|
|
246
|
+
|
|
247
|
+
3. **copy media to public directory**
|
|
248
|
+
```bash
|
|
249
|
+
mkdir -p lib/remotion/public
|
|
250
|
+
cp media/video.mp4 media/audio.mp3 media/*.png lib/remotion/public/
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
4. **customize composition** (lib/remotion/compositions/MyVideo.tsx)
|
|
254
|
+
- template already has all imports: `OffthreadVideo`, `Audio`, `Img`, `staticFile`
|
|
255
|
+
- replace placeholder content with your media
|
|
256
|
+
- use `staticFile("filename.mp4")` for all media references
|
|
257
|
+
- add animations with `useCurrentFrame()` and `interpolate()`
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// example customization
|
|
261
|
+
const video = staticFile("video.mp4");
|
|
262
|
+
const audio = staticFile("audio.mp3");
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<AbsoluteFill>
|
|
266
|
+
<OffthreadVideo src={video} />
|
|
267
|
+
<Audio src={audio} />
|
|
268
|
+
</AbsoluteFill>
|
|
269
|
+
);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
5. **configure settings** (lib/remotion/compositions/MyVideo.root.tsx)
|
|
273
|
+
- template already uses `registerRoot()` correctly
|
|
274
|
+
- update fps, durationInFrames, width, height as needed
|
|
275
|
+
```tsx
|
|
276
|
+
const fps = 30;
|
|
277
|
+
const durationInFrames = 150; // 5 seconds
|
|
278
|
+
const width = 1920;
|
|
279
|
+
const height = 1080;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
6. **render**
|
|
283
|
+
```bash
|
|
284
|
+
bun run lib/remotion/index.ts render lib/remotion/compositions/MyVideo.root.tsx MyVideo output.mp4
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## tools available
|
|
288
|
+
|
|
289
|
+
### lib/remotion/index.ts
|
|
290
|
+
```bash
|
|
291
|
+
# setup composition directory
|
|
292
|
+
bun run lib/remotion/index.ts create <name>
|
|
293
|
+
|
|
294
|
+
# list compositions
|
|
295
|
+
bun run lib/remotion/index.ts compositions <root-file.tsx>
|
|
296
|
+
|
|
297
|
+
# render video
|
|
298
|
+
bun run lib/remotion/index.ts render <root-file.tsx> <comp-id> <output.mp4>
|
|
299
|
+
|
|
300
|
+
# render still frame
|
|
301
|
+
bun run lib/remotion/index.ts still <root-file.tsx> <comp-id> <frame> <out.png>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### lib/ffmpeg.ts
|
|
305
|
+
```bash
|
|
306
|
+
# get video metadata
|
|
307
|
+
bun run lib/ffmpeg.ts probe <input.mp4>
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## examples
|
|
311
|
+
|
|
312
|
+
### complete workflow: video + images montage with audio
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# 1. probe video to get metadata
|
|
316
|
+
bun run lib/ffmpeg.ts probe media/video.mp4
|
|
317
|
+
# output: 1920x1080 @ 24fps, 5.041667s
|
|
318
|
+
|
|
319
|
+
# 2. create composition structure
|
|
320
|
+
bun run lib/remotion/index.ts create MediaMontage
|
|
321
|
+
# creates: lib/remotion/compositions/MediaMontage.tsx
|
|
322
|
+
# lib/remotion/compositions/MediaMontage.root.tsx
|
|
323
|
+
|
|
324
|
+
# 3. copy specific media files to public directory
|
|
325
|
+
mkdir -p lib/remotion/public
|
|
326
|
+
cp media/video.mp4 media/audio.ogg media/image1.png media/image2.png media/image3.png media/image4.png lib/remotion/public/
|
|
327
|
+
|
|
328
|
+
# 4. create composition file (MediaMontage.tsx)
|
|
329
|
+
cat > lib/remotion/compositions/MediaMontage.tsx << 'EOF'
|
|
330
|
+
import React from "react";
|
|
331
|
+
import { AbsoluteFill, OffthreadVideo, Audio, Img, useCurrentFrame, useVideoConfig, interpolate, staticFile } from "remotion";
|
|
332
|
+
|
|
333
|
+
export const MediaMontage: React.FC = () => {
|
|
334
|
+
const frame = useCurrentFrame();
|
|
335
|
+
const { fps } = useVideoConfig();
|
|
336
|
+
|
|
337
|
+
const imageDisplayTime = 3;
|
|
338
|
+
const imageFrames = imageDisplayTime * fps;
|
|
339
|
+
const videoFrames = Math.floor(5.041667 * fps);
|
|
340
|
+
|
|
341
|
+
const videoPath = staticFile("video.mp4");
|
|
342
|
+
const audioPath = staticFile("audio.ogg");
|
|
343
|
+
const images = [
|
|
344
|
+
staticFile("image1.png"),
|
|
345
|
+
staticFile("image2.png"),
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const videoEnd = videoFrames;
|
|
349
|
+
let content: React.ReactNode = null;
|
|
350
|
+
|
|
351
|
+
if (frame < videoEnd) {
|
|
352
|
+
content = <OffthreadVideo src={videoPath} />;
|
|
353
|
+
} else {
|
|
354
|
+
const imageFrame = frame - videoEnd;
|
|
355
|
+
const imageIndex = Math.floor(imageFrame / imageFrames);
|
|
356
|
+
|
|
357
|
+
if (imageIndex < images.length) {
|
|
358
|
+
const localFrame = imageFrame % imageFrames;
|
|
359
|
+
const scale = interpolate(localFrame, [0, imageFrames], [1, 1.15], { extrapolateRight: "clamp" });
|
|
360
|
+
content = (
|
|
361
|
+
<div style={{ width: "100%", height: "100%", transform: `scale(${scale})` }}>
|
|
362
|
+
<Img src={images[imageIndex] as string} style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<AbsoluteFill style={{ backgroundColor: "black" }}>
|
|
370
|
+
{content}
|
|
371
|
+
<Audio src={audioPath} />
|
|
372
|
+
</AbsoluteFill>
|
|
373
|
+
);
|
|
374
|
+
};
|
|
375
|
+
EOF
|
|
376
|
+
|
|
377
|
+
# 5. create root file (MediaMontage.root.tsx)
|
|
378
|
+
cat > lib/remotion/compositions/MediaMontage.root.tsx << 'EOF'
|
|
379
|
+
import React from "react";
|
|
380
|
+
import { Composition, registerRoot } from "remotion";
|
|
381
|
+
import { MediaMontage } from "./MediaMontage";
|
|
382
|
+
|
|
383
|
+
const fps = 30;
|
|
384
|
+
const videoFrames = Math.floor(5.041667 * fps);
|
|
385
|
+
const imageFrames = 4 * 3 * fps; // 4 images, 3 seconds each
|
|
386
|
+
const totalFrames = videoFrames + imageFrames;
|
|
387
|
+
|
|
388
|
+
registerRoot(() => {
|
|
389
|
+
return (
|
|
390
|
+
<>
|
|
391
|
+
<Composition
|
|
392
|
+
id="MediaMontage"
|
|
393
|
+
component={MediaMontage}
|
|
394
|
+
durationInFrames={totalFrames}
|
|
395
|
+
fps={fps}
|
|
396
|
+
width={1920}
|
|
397
|
+
height={1080}
|
|
398
|
+
/>
|
|
399
|
+
</>
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
EOF
|
|
403
|
+
|
|
404
|
+
# 6. render composition
|
|
405
|
+
bun run lib/remotion/index.ts render lib/remotion/compositions/MediaMontage.root.tsx MediaMontage media/output.mp4
|
|
406
|
+
|
|
407
|
+
# 7. verify output
|
|
408
|
+
bun run lib/ffmpeg.ts probe media/output.mp4
|
|
409
|
+
# output: 1920x1080 @ 30fps, 17.033s
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### render specific frame as thumbnail
|
|
413
|
+
```bash
|
|
414
|
+
# render frame 100 as image (useful for video preview)
|
|
415
|
+
bun run lib/remotion.ts still /path/to/project/src/index.ts MyVideo 100 thumbnail.png
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// create custom thumbnail composition with graphics
|
|
420
|
+
export const Thumbnail: React.FC = () => {
|
|
421
|
+
return (
|
|
422
|
+
<AbsoluteFill>
|
|
423
|
+
<Video src={staticFile("video.mp4")} />
|
|
424
|
+
{/* add title overlay */}
|
|
425
|
+
<div style={{
|
|
426
|
+
position: "absolute",
|
|
427
|
+
bottom: 50,
|
|
428
|
+
fontSize: 60,
|
|
429
|
+
fontWeight: "bold",
|
|
430
|
+
color: "white",
|
|
431
|
+
}}>
|
|
432
|
+
My Video Title
|
|
433
|
+
</div>
|
|
434
|
+
</AbsoluteFill>
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// then render frame 0 of this composition
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### zoom in effect
|
|
442
|
+
```typescript
|
|
443
|
+
import { interpolate } from "remotion";
|
|
444
|
+
|
|
445
|
+
const frame = useCurrentFrame();
|
|
446
|
+
|
|
447
|
+
// zoom from 1x to 2x over 60 frames
|
|
448
|
+
const scale = interpolate(frame, [0, 60], [1, 2], {
|
|
449
|
+
extrapolateRight: "clamp"
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<AbsoluteFill>
|
|
454
|
+
<div style={{
|
|
455
|
+
transform: `scale(${scale})`,
|
|
456
|
+
transformOrigin: "center center"
|
|
457
|
+
}}>
|
|
458
|
+
<Video src={staticFile("video.mp4")} />
|
|
459
|
+
</div>
|
|
460
|
+
</AbsoluteFill>
|
|
461
|
+
);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### ken burns effect (pan + zoom)
|
|
465
|
+
```typescript
|
|
466
|
+
const scale = interpolate(frame, [0, 150], [1, 1.3]);
|
|
467
|
+
const translateX = interpolate(frame, [0, 150], [0, -100]);
|
|
468
|
+
const translateY = interpolate(frame, [0, 150], [0, -50]);
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<div style={{
|
|
472
|
+
transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,
|
|
473
|
+
}}>
|
|
474
|
+
<Img src={staticFile("image.jpg")} />
|
|
475
|
+
</div>
|
|
476
|
+
);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### image with blurred background (aspect ratio fix)
|
|
480
|
+
```typescript
|
|
481
|
+
// fits any image into frame without black bars
|
|
482
|
+
// works great for portrait images in landscape compositions
|
|
483
|
+
const imageSrc = staticFile("portrait-image.jpg");
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<AbsoluteFill>
|
|
487
|
+
{/* blurred background layer - fills entire frame */}
|
|
488
|
+
<AbsoluteFill>
|
|
489
|
+
<Img
|
|
490
|
+
src={imageSrc}
|
|
491
|
+
style={{
|
|
492
|
+
width: "100%",
|
|
493
|
+
height: "100%",
|
|
494
|
+
objectFit: "cover",
|
|
495
|
+
filter: "blur(40px)",
|
|
496
|
+
opacity: 0.6
|
|
497
|
+
}}
|
|
498
|
+
/>
|
|
499
|
+
</AbsoluteFill>
|
|
500
|
+
|
|
501
|
+
{/* foreground layer - full image fitted */}
|
|
502
|
+
<AbsoluteFill style={{
|
|
503
|
+
display: "flex",
|
|
504
|
+
justifyContent: "center",
|
|
505
|
+
alignItems: "center"
|
|
506
|
+
}}>
|
|
507
|
+
<Img
|
|
508
|
+
src={imageSrc}
|
|
509
|
+
style={{
|
|
510
|
+
maxWidth: "100%",
|
|
511
|
+
maxHeight: "100%",
|
|
512
|
+
objectFit: "contain"
|
|
513
|
+
}}
|
|
514
|
+
/>
|
|
515
|
+
</AbsoluteFill>
|
|
516
|
+
</AbsoluteFill>
|
|
517
|
+
);
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### combine multiple audio tracks
|
|
521
|
+
```typescript
|
|
522
|
+
import { Audio } from "remotion";
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<AbsoluteFill>
|
|
526
|
+
<Video src={staticFile("video.mp4")} />
|
|
527
|
+
{/* background music at 30% volume */}
|
|
528
|
+
<Audio src={staticFile("music.mp3")} volume={0.3} />
|
|
529
|
+
{/* voiceover at full volume */}
|
|
530
|
+
<Audio src={staticFile("narration.mp3")} volume={1} />
|
|
531
|
+
</AbsoluteFill>
|
|
532
|
+
);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### audio fade in/out
|
|
536
|
+
```typescript
|
|
537
|
+
const audioVolume = interpolate(
|
|
538
|
+
frame,
|
|
539
|
+
[0, 30, 270, 300], // fade in first 30 frames, out last 30
|
|
540
|
+
[0, 1, 1, 0],
|
|
541
|
+
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
<Audio src={staticFile("music.mp3")} volume={audioVolume} />
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### side-by-side videos
|
|
548
|
+
```typescript
|
|
549
|
+
const { width, height } = useVideoConfig();
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<AbsoluteFill>
|
|
553
|
+
{/* left video */}
|
|
554
|
+
<AbsoluteFill style={{ width: width / 2, left: 0 }}>
|
|
555
|
+
<Video src={staticFile("video1.mp4")} />
|
|
556
|
+
</AbsoluteFill>
|
|
557
|
+
|
|
558
|
+
{/* right video */}
|
|
559
|
+
<AbsoluteFill style={{ width: width / 2, left: width / 2 }}>
|
|
560
|
+
<Video src={staticFile("video2.mp4")} />
|
|
561
|
+
</AbsoluteFill>
|
|
562
|
+
</AbsoluteFill>
|
|
563
|
+
);
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### grid layout (4 videos)
|
|
567
|
+
```typescript
|
|
568
|
+
return (
|
|
569
|
+
<AbsoluteFill style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
|
|
570
|
+
<Video src={staticFile("video1.mp4")} />
|
|
571
|
+
<Video src={staticFile("video2.mp4")} />
|
|
572
|
+
<Video src={staticFile("video3.mp4")} />
|
|
573
|
+
<Video src={staticFile("video4.mp4")} />
|
|
574
|
+
</AbsoluteFill>
|
|
575
|
+
);
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### video with word-by-word captions
|
|
579
|
+
```typescript
|
|
580
|
+
// parse SRT
|
|
581
|
+
const subtitles = parseSRT(srtContent);
|
|
582
|
+
|
|
583
|
+
// in component
|
|
584
|
+
const frame = useCurrentFrame();
|
|
585
|
+
const { fps } = useVideoConfig();
|
|
586
|
+
const currentTime = frame / fps;
|
|
587
|
+
|
|
588
|
+
const currentSubtitle = subtitles.find(
|
|
589
|
+
sub => currentTime >= sub.startTime && currentTime <= sub.endTime
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<AbsoluteFill>
|
|
594
|
+
<OffthreadVideo src={staticFile("video.mp4")} />
|
|
595
|
+
{currentSubtitle && (
|
|
596
|
+
<div className="caption">{currentSubtitle.text}</div>
|
|
597
|
+
)}
|
|
598
|
+
</AbsoluteFill>
|
|
599
|
+
);
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### sequential video concatenation
|
|
603
|
+
```typescript
|
|
604
|
+
const fitnessEnd = 1430; // 47.67s * 30fps
|
|
605
|
+
const kangarooStart = fitnessEnd;
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
<AbsoluteFill>
|
|
609
|
+
{frame < fitnessEnd ? (
|
|
610
|
+
<OffthreadVideo src={staticFile("fitness.mp4")} />
|
|
611
|
+
) : (
|
|
612
|
+
<OffthreadVideo
|
|
613
|
+
src={staticFile("kangaroo.mp4")}
|
|
614
|
+
startFrom={Math.floor((frame - kangarooStart) * (24/30))}
|
|
615
|
+
/>
|
|
616
|
+
)}
|
|
617
|
+
</AbsoluteFill>
|
|
618
|
+
);
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### crossfade transition between videos
|
|
622
|
+
```typescript
|
|
623
|
+
const transitionStart = 140;
|
|
624
|
+
const transitionDuration = 20;
|
|
625
|
+
|
|
626
|
+
const opacity1 = interpolate(
|
|
627
|
+
frame,
|
|
628
|
+
[transitionStart, transitionStart + transitionDuration],
|
|
629
|
+
[1, 0],
|
|
630
|
+
{ extrapolateRight: "clamp" }
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const opacity2 = interpolate(
|
|
634
|
+
frame,
|
|
635
|
+
[transitionStart, transitionStart + transitionDuration],
|
|
636
|
+
[0, 1],
|
|
637
|
+
{ extrapolateRight: "clamp" }
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<AbsoluteFill>
|
|
642
|
+
<AbsoluteFill style={{ opacity: opacity1 }}>
|
|
643
|
+
<Video src={staticFile("video1.mp4")} />
|
|
644
|
+
</AbsoluteFill>
|
|
645
|
+
<AbsoluteFill style={{ opacity: opacity2 }}>
|
|
646
|
+
<Video src={staticFile("video2.mp4")} />
|
|
647
|
+
</AbsoluteFill>
|
|
648
|
+
</AbsoluteFill>
|
|
649
|
+
);
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### beautiful animated captions
|
|
653
|
+
```typescript
|
|
654
|
+
// word appears from bottom with bounce
|
|
655
|
+
const captionY = interpolate(
|
|
656
|
+
frame - subtitle.startFrame,
|
|
657
|
+
[0, 10],
|
|
658
|
+
[50, 0],
|
|
659
|
+
{ extrapolateRight: "clamp", easing: Easing.bounce }
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const captionOpacity = interpolate(
|
|
663
|
+
frame - subtitle.startFrame,
|
|
664
|
+
[0, 5],
|
|
665
|
+
[0, 1],
|
|
666
|
+
{ extrapolateRight: "clamp" }
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
{currentSubtitle && (
|
|
670
|
+
<div style={{
|
|
671
|
+
fontFamily: "Inter",
|
|
672
|
+
fontSize: 60,
|
|
673
|
+
fontWeight: "900",
|
|
674
|
+
color: "#FFD700",
|
|
675
|
+
textAlign: "center",
|
|
676
|
+
textShadow: "4px 4px 8px rgba(0,0,0,0.8)",
|
|
677
|
+
background: "linear-gradient(135deg, rgba(0,0,0,0.9), rgba(20,20,50,0.9))",
|
|
678
|
+
padding: "30px 50px",
|
|
679
|
+
borderRadius: 20,
|
|
680
|
+
border: "3px solid #FFD700",
|
|
681
|
+
transform: `translateY(${captionY}px)`,
|
|
682
|
+
opacity: captionOpacity,
|
|
683
|
+
}}>
|
|
684
|
+
{currentSubtitle.text.toUpperCase()}
|
|
685
|
+
</div>
|
|
686
|
+
)}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## troubleshooting
|
|
690
|
+
|
|
691
|
+
### "registerRoot" error when rendering
|
|
692
|
+
- **error**: `This file does not contain "registerRoot"`
|
|
693
|
+
- **cause**: root file exports component instead of calling registerRoot()
|
|
694
|
+
- **fix**: use `registerRoot(() => { return (<>...</>) })` instead of `export const RemotionRoot`
|
|
695
|
+
- **example**:
|
|
696
|
+
```tsx
|
|
697
|
+
// ❌ wrong
|
|
698
|
+
export const RemotionRoot: React.FC = () => { return (<>...</>) };
|
|
699
|
+
|
|
700
|
+
// ✅ correct
|
|
701
|
+
import { registerRoot } from "remotion";
|
|
702
|
+
registerRoot(() => { return (<>...</>) });
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### video not loading (404 error)
|
|
706
|
+
- **error**: `Received a status code of 404 while downloading file`
|
|
707
|
+
- **cause**: using absolute file paths instead of staticFile()
|
|
708
|
+
- **fix**: copy media to `lib/remotion/public/` and use `staticFile()`
|
|
709
|
+
- **example**:
|
|
710
|
+
```tsx
|
|
711
|
+
// ❌ wrong
|
|
712
|
+
const video = "/Users/aleks/project/media/video.mp4";
|
|
713
|
+
|
|
714
|
+
// ✅ correct - copy file first
|
|
715
|
+
// cp media/video.mp4 lib/remotion/public/
|
|
716
|
+
import { staticFile } from "remotion";
|
|
717
|
+
const video = staticFile("video.mp4");
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### deprecated components warnings
|
|
721
|
+
- **warning**: `Video` and `Audio` are deprecated
|
|
722
|
+
- **fix**: use `OffthreadVideo` instead of `Video`, `Audio` is still usable but may change
|
|
723
|
+
- **example**:
|
|
724
|
+
```tsx
|
|
725
|
+
// ❌ deprecated
|
|
726
|
+
import { Video } from "remotion";
|
|
727
|
+
<Video src={staticFile("video.mp4")} />
|
|
728
|
+
|
|
729
|
+
// ✅ recommended
|
|
730
|
+
import { OffthreadVideo } from "remotion";
|
|
731
|
+
<OffthreadVideo src={staticFile("video.mp4")} />
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### type errors with array indexing
|
|
735
|
+
- **error**: `Type 'string | undefined' is not assignable to type 'string'`
|
|
736
|
+
- **cause**: typescript doesn't know array index is valid
|
|
737
|
+
- **fix**: use type assertion `as string` or check bounds
|
|
738
|
+
- **example**:
|
|
739
|
+
```tsx
|
|
740
|
+
// ❌ type error
|
|
741
|
+
<Img src={images[index]} />
|
|
742
|
+
|
|
743
|
+
// ✅ with type assertion
|
|
744
|
+
<Img src={images[index] as string} />
|
|
745
|
+
|
|
746
|
+
// ✅ with bounds check
|
|
747
|
+
{index < images.length && <Img src={images[index]} />}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### composition not found
|
|
751
|
+
- verify composition is registered with registerRoot()
|
|
752
|
+
- check composition id matches exactly
|
|
753
|
+
- run `compositions` command to list available
|
|
754
|
+
|
|
755
|
+
### audio/voiceover gets cut off
|
|
756
|
+
- error: rendered video ends before audio finishes
|
|
757
|
+
- cause: composition `durationInFrames` is shorter than audio duration
|
|
758
|
+
- fix: probe audio first, calculate correct frame count
|
|
759
|
+
- example:
|
|
760
|
+
```bash
|
|
761
|
+
# probe the voiceover
|
|
762
|
+
bun run lib/ffmpeg.ts probe media/voiceover.mp3
|
|
763
|
+
# output: duration: 47.2s
|
|
764
|
+
|
|
765
|
+
# calculate frames for 30fps
|
|
766
|
+
# 47.2 * 30 = 1416 frames
|
|
767
|
+
|
|
768
|
+
# in root file, set durationInFrames to at least 1416
|
|
769
|
+
const durationInFrames = 1416; // or higher if adding more content
|
|
770
|
+
```
|
|
771
|
+
- prevention: always probe audio files before setting composition duration
|
|
772
|
+
- common mistake: assuming voiceover is only 5-10 seconds when it's actually 30-60+ seconds
|
|
773
|
+
|
|
774
|
+
### wrong video duration
|
|
775
|
+
- probe video to get exact duration and fps
|
|
776
|
+
- calculate frames: `durationInFrames = duration * fps`
|
|
777
|
+
- account for fps differences when concatenating
|
|
778
|
+
|
|
779
|
+
### captions not syncing
|
|
780
|
+
- verify SRT timestamps are in seconds
|
|
781
|
+
- convert to frame-based timing: `frame / fps`
|
|
782
|
+
- check start/end time comparisons
|
|
783
|
+
|
|
784
|
+
### all renders showing same content (caching issue)
|
|
785
|
+
- **error**: batch rendering multiple variations but all videos show the same content
|
|
786
|
+
- **cause**: overwriting files in `public/` folder between renders causes staticFile() to cache the last version
|
|
787
|
+
- **symptoms**:
|
|
788
|
+
- renders complete successfully
|
|
789
|
+
- all videos have correct file size/duration
|
|
790
|
+
- but all videos show identical content (usually the last variation)
|
|
791
|
+
- **fix**: use unique filenames for each variation instead of overwriting
|
|
792
|
+
- pass variation ID as prop: `defaultProps: { variationId: "01" }`
|
|
793
|
+
- use template strings in staticFile: `staticFile(\`woman-${variationId}-before.jpg\`)`
|
|
794
|
+
- ensure all unique files exist in `public/` before rendering
|
|
795
|
+
- **example**:
|
|
796
|
+
```tsx
|
|
797
|
+
// ❌ wrong - overwrites same file
|
|
798
|
+
// render loop: copy woman1 → before.jpg, render, copy woman2 → before.jpg, render...
|
|
799
|
+
const beforeImg = staticFile("before.jpg"); // caches last file!
|
|
800
|
+
|
|
801
|
+
// ✅ correct - unique filenames
|
|
802
|
+
interface Props { variationId?: string }
|
|
803
|
+
const MyComp: React.FC<Props> = ({ variationId = "01" }) => {
|
|
804
|
+
const beforeImg = staticFile(\`woman-${variationId}-before.jpg\`);
|
|
805
|
+
// each render uses different file, no caching issues
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
## best practices
|
|
810
|
+
|
|
811
|
+
1. **always probe videos first** - get accurate duration/fps using `bun run lib/ffmpeg.ts probe`
|
|
812
|
+
2. **probe audio files too** - voiceovers/narration are often 30s, 60s, or longer - never guess duration
|
|
813
|
+
3. **verify composition duration vs audio** - make sure `durationInFrames` is >= audio duration, or audio will be cut off
|
|
814
|
+
4. **copy media to public/** - copy all media files to `lib/remotion/public/` before rendering
|
|
815
|
+
5. **use staticFile() for all media** - never use absolute paths in compositions
|
|
816
|
+
6. **use unique filenames for variations** - never overwrite files in `public/` between renders (staticFile caches by filename)
|
|
817
|
+
7. **use registerRoot()** - root files must call `registerRoot()`, not export a component
|
|
818
|
+
8. **use OffthreadVideo** - prefer `OffthreadVideo` over deprecated `Video` component
|
|
819
|
+
9. **calculate frames correctly** - `durationInFrames = duration * fps`
|
|
820
|
+
10. **test compositions** - run `compositions` command to verify before rendering
|
|
821
|
+
11. **handle fps differences** - adjust startFrom when concatenating videos with different fps
|
|
822
|
+
12. **use descriptive ids** - make composition names clear and unique
|
|
823
|
+
13. **batch render with props** - for multiple variations, register multiple compositions with unique defaultProps instead of file overwriting
|