remotion-claude-agent-demo 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/README.md +160 -0
- package/apps/web/README.md +36 -0
- package/apps/web/env.example +20 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package-lock.json +10348 -0
- package/apps/web/package.json +35 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
- package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
- package/apps/web/src/app/events/stream/route.ts +58 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +174 -0
- package/apps/web/src/app/layout.tsx +34 -0
- package/apps/web/src/app/messages/answer/route.ts +57 -0
- package/apps/web/src/app/messages/stream/route.ts +381 -0
- package/apps/web/src/app/page.tsx +358 -0
- package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
- package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
- package/apps/web/src/app/tasks/route.ts +13 -0
- package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
- package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
- package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
- package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
- package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
- package/apps/web/src/lib/a2a/activity-store.ts +150 -0
- package/apps/web/src/lib/a2a/client.ts +357 -0
- package/apps/web/src/lib/a2a/sse.ts +19 -0
- package/apps/web/src/lib/a2a/task-store.ts +111 -0
- package/apps/web/src/lib/a2a/types.ts +216 -0
- package/apps/web/src/lib/agent/answer-store.ts +109 -0
- package/apps/web/src/lib/agent/background-delivery.ts +343 -0
- package/apps/web/src/lib/agent/background-tool.ts +78 -0
- package/apps/web/src/lib/agent/background.ts +452 -0
- package/apps/web/src/lib/agent/chat.ts +543 -0
- package/apps/web/src/lib/agent/session-store.ts +26 -0
- package/apps/web/src/lib/chat/types.ts +44 -0
- package/apps/web/src/lib/env.ts +31 -0
- package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
- package/apps/web/src/lib/state/chat-atoms.ts +52 -0
- package/apps/web/src/lib/workspace.ts +9 -0
- package/apps/web/tsconfig.json +35 -0
- package/bin/remotion-agent.js +451 -0
- package/package.json +34 -0
- package/templates/.claude/CLAUDE.md +95 -0
- package/templates/.claude/README.md +129 -0
- package/templates/.claude/agents/composer-agent.md +188 -0
- package/templates/.claude/agents/crafter.md +181 -0
- package/templates/.claude/agents/creator.md +134 -0
- package/templates/.claude/agents/perceiver.md +92 -0
- package/templates/.claude/settings.json +36 -0
- package/templates/.claude/settings.local.json +39 -0
- package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
- package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
- package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
- package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
- package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
- package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
- package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
- package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
- package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
- package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
- package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
- package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
- package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
- package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
- package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
- package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
- package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
- package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
- package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
- package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
- package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
- package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
- package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
- package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
- package/templates/.claude/skills/render-config/SKILL.md +290 -0
- package/templates/.claude/skills/script-writer/SKILL.md +59 -0
- package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
- package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
- package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
- package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
- package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
- package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
- package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
- package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
- package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
- package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
- package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
- package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
- package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
- package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
- package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
- package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: compositions
|
|
3
|
+
description: Defining compositions, stills, folders, default props and dynamic metadata
|
|
4
|
+
metadata:
|
|
5
|
+
tags: composition, still, folder, props, metadata
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A `<Composition>` defines the component, width, height, fps and duration of a renderable video.
|
|
9
|
+
|
|
10
|
+
It normally is placed in the `src/Root.tsx` file.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import {Composition} from 'remotion';
|
|
14
|
+
import {MyComposition} from './MyComposition';
|
|
15
|
+
|
|
16
|
+
export const RemotionRoot = () => {
|
|
17
|
+
return <Composition id="MyComposition" component={MyComposition} durationInFrames={100} fps={30} width={1080} height={1080} />;
|
|
18
|
+
};
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Default Props
|
|
22
|
+
|
|
23
|
+
Pass `defaultProps` to provide initial values for your component.
|
|
24
|
+
Values must be JSON-serializable (`Date`, `Map`, `Set`, and `staticFile()` are supported).
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import {Composition} from 'remotion';
|
|
28
|
+
import {MyComposition, MyCompositionProps} from './MyComposition';
|
|
29
|
+
|
|
30
|
+
export const RemotionRoot = () => {
|
|
31
|
+
return (
|
|
32
|
+
<Composition
|
|
33
|
+
id="MyComposition"
|
|
34
|
+
component={MyComposition}
|
|
35
|
+
durationInFrames={100}
|
|
36
|
+
fps={30}
|
|
37
|
+
width={1080}
|
|
38
|
+
height={1080}
|
|
39
|
+
defaultProps={
|
|
40
|
+
{
|
|
41
|
+
title: 'Hello World',
|
|
42
|
+
color: '#ff0000',
|
|
43
|
+
} satisfies MyCompositionProps
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Use `type` declarations for props rather than `interface` to ensure `defaultProps` type safety.
|
|
51
|
+
|
|
52
|
+
## Folders
|
|
53
|
+
|
|
54
|
+
Use `<Folder>` to organize compositions in the sidebar.
|
|
55
|
+
Folder names can only contain letters, numbers, and hyphens.
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import {Composition, Folder} from 'remotion';
|
|
59
|
+
|
|
60
|
+
export const RemotionRoot = () => {
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<Folder name="Marketing">
|
|
64
|
+
<Composition id="Promo" /* ... */ />
|
|
65
|
+
<Composition id="Ad" /* ... */ />
|
|
66
|
+
</Folder>
|
|
67
|
+
<Folder name="Social">
|
|
68
|
+
<Folder name="Instagram">
|
|
69
|
+
<Composition id="Story" /* ... */ />
|
|
70
|
+
<Composition id="Reel" /* ... */ />
|
|
71
|
+
</Folder>
|
|
72
|
+
</Folder>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Stills
|
|
79
|
+
|
|
80
|
+
Use `<Still>` for single-frame images. It does not require `durationInFrames` or `fps`.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import {Still} from 'remotion';
|
|
84
|
+
import {Thumbnail} from './Thumbnail';
|
|
85
|
+
|
|
86
|
+
export const RemotionRoot = () => {
|
|
87
|
+
return <Still id="Thumbnail" component={Thumbnail} width={1280} height={720} />;
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Calculate Metadata
|
|
92
|
+
|
|
93
|
+
Use `calculateMetadata` to make dimensions, duration, or props dynamic based on data.
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
import {Composition, CalculateMetadataFunction} from 'remotion';
|
|
97
|
+
import {MyComposition, MyCompositionProps} from './MyComposition';
|
|
98
|
+
|
|
99
|
+
const calculateMetadata: CalculateMetadataFunction<MyCompositionProps> = async ({props, abortSignal}) => {
|
|
100
|
+
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
|
|
101
|
+
signal: abortSignal,
|
|
102
|
+
}).then((res) => res.json());
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
durationInFrames: Math.ceil(data.duration * 30),
|
|
106
|
+
props: {
|
|
107
|
+
...props,
|
|
108
|
+
videoUrl: data.url,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const RemotionRoot = () => {
|
|
114
|
+
return (
|
|
115
|
+
<Composition
|
|
116
|
+
id="MyComposition"
|
|
117
|
+
component={MyComposition}
|
|
118
|
+
durationInFrames={100} // Placeholder, will be overridden
|
|
119
|
+
fps={30}
|
|
120
|
+
width={1080}
|
|
121
|
+
height={1080}
|
|
122
|
+
defaultProps={{videoId: 'abc123'}}
|
|
123
|
+
calculateMetadata={calculateMetadata}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The function can return `props`, `durationInFrames`, `width`, `height`, `fps`, and codec-related defaults. It runs once before rendering begins.
|
|
130
|
+
|
|
131
|
+
## Nesting compositions within another
|
|
132
|
+
|
|
133
|
+
To add a composition within another composition, you can use the `<Sequence>` component with a `width` and `height` prop to specify the size of the composition.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<AbsoluteFill>
|
|
137
|
+
<Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>
|
|
138
|
+
<CompositionComponent />
|
|
139
|
+
</Sequence>
|
|
140
|
+
</AbsoluteFill>
|
|
141
|
+
```
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: display-captions
|
|
3
|
+
description: Displaying captions in Remotion with TikTok-style pages and word highlighting
|
|
4
|
+
metadata:
|
|
5
|
+
tags: captions, subtitles, display, tiktok, highlight
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Displaying captions in Remotion
|
|
9
|
+
|
|
10
|
+
This guide explains how to display captions in Remotion, assuming you already have captions in the `Caption` format.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
First, the @remotion/captions package needs to be installed.
|
|
15
|
+
If it is not installed, use the following command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx remotion add @remotion/captions # If project uses npm
|
|
19
|
+
bunx remotion add @remotion/captions # If project uses bun
|
|
20
|
+
yarn remotion add @remotion/captions # If project uses yarn
|
|
21
|
+
pnpm exec remotion add @remotion/captions # If project uses pnpm
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Creating pages
|
|
25
|
+
|
|
26
|
+
Use `createTikTokStyleCaptions()` to group captions into pages. The `combineTokensWithinMilliseconds` option controls how many words appear at once:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {useMemo} from 'react';
|
|
30
|
+
import {createTikTokStyleCaptions} from '@remotion/captions';
|
|
31
|
+
import type {Caption} from '@remotion/captions';
|
|
32
|
+
|
|
33
|
+
// How often captions should switch (in milliseconds)
|
|
34
|
+
// Higher values = more words per page
|
|
35
|
+
// Lower values = fewer words (more word-by-word)
|
|
36
|
+
const SWITCH_CAPTIONS_EVERY_MS = 1200;
|
|
37
|
+
|
|
38
|
+
const {pages} = useMemo(() => {
|
|
39
|
+
return createTikTokStyleCaptions({
|
|
40
|
+
captions,
|
|
41
|
+
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
|
|
42
|
+
});
|
|
43
|
+
}, [captions]);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Rendering with Sequences
|
|
47
|
+
|
|
48
|
+
Map over the pages and render each one in a `<Sequence>`. Calculate the start frame and duration from the page timing:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import {Sequence, useVideoConfig, AbsoluteFill} from 'remotion';
|
|
52
|
+
import type {TikTokPage} from '@remotion/captions';
|
|
53
|
+
|
|
54
|
+
const CaptionedContent: React.FC = () => {
|
|
55
|
+
const {fps} = useVideoConfig();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<AbsoluteFill>
|
|
59
|
+
{pages.map((page, index) => {
|
|
60
|
+
const nextPage = pages[index + 1] ?? null;
|
|
61
|
+
const startFrame = (page.startMs / 1000) * fps;
|
|
62
|
+
const endFrame = Math.min(
|
|
63
|
+
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
|
|
64
|
+
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
|
|
65
|
+
);
|
|
66
|
+
const durationInFrames = endFrame - startFrame;
|
|
67
|
+
|
|
68
|
+
if (durationInFrames <= 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Sequence
|
|
74
|
+
key={index}
|
|
75
|
+
from={startFrame}
|
|
76
|
+
durationInFrames={durationInFrames}
|
|
77
|
+
>
|
|
78
|
+
<CaptionPage page={page} />
|
|
79
|
+
</Sequence>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</AbsoluteFill>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Word highlighting
|
|
88
|
+
|
|
89
|
+
A caption page contains `tokens` which you can use to highlight the currently spoken word:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion';
|
|
93
|
+
import type {TikTokPage} from '@remotion/captions';
|
|
94
|
+
|
|
95
|
+
const HIGHLIGHT_COLOR = '#39E508';
|
|
96
|
+
|
|
97
|
+
const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => {
|
|
98
|
+
const frame = useCurrentFrame();
|
|
99
|
+
const {fps} = useVideoConfig();
|
|
100
|
+
|
|
101
|
+
// Current time relative to the start of the sequence
|
|
102
|
+
const currentTimeMs = (frame / fps) * 1000;
|
|
103
|
+
// Convert to absolute time by adding the page start
|
|
104
|
+
const absoluteTimeMs = page.startMs + currentTimeMs;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<AbsoluteFill style={{justifyContent: 'center', alignItems: 'center'}}>
|
|
108
|
+
<div style={{fontSize: 80, fontWeight: 'bold', whiteSpace: 'pre'}}>
|
|
109
|
+
{page.tokens.map((token) => {
|
|
110
|
+
const isActive =
|
|
111
|
+
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<span
|
|
115
|
+
key={token.fromMs}
|
|
116
|
+
style={{color: isActive ? HIGHLIGHT_COLOR : 'white'}}
|
|
117
|
+
>
|
|
118
|
+
{token.text}
|
|
119
|
+
</span>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
</AbsoluteFill>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
```
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extract-frames
|
|
3
|
+
description: Extract frames from videos at specific timestamps using Mediabunny
|
|
4
|
+
metadata:
|
|
5
|
+
tags: frames, extract, video, thumbnail, filmstrip, canvas
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Extracting frames from videos
|
|
9
|
+
|
|
10
|
+
Use Mediabunny to extract frames from videos at specific timestamps. This is useful for generating thumbnails, filmstrips, or processing individual frames.
|
|
11
|
+
|
|
12
|
+
## The `extractFrames()` function
|
|
13
|
+
|
|
14
|
+
This function can be copy-pasted into any project.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import {
|
|
18
|
+
ALL_FORMATS,
|
|
19
|
+
Input,
|
|
20
|
+
UrlSource,
|
|
21
|
+
VideoSample,
|
|
22
|
+
VideoSampleSink,
|
|
23
|
+
} from "mediabunny";
|
|
24
|
+
|
|
25
|
+
type Options = {
|
|
26
|
+
track: { width: number; height: number };
|
|
27
|
+
container: string;
|
|
28
|
+
durationInSeconds: number | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ExtractFramesTimestampsInSecondsFn = (
|
|
32
|
+
options: Options
|
|
33
|
+
) => Promise<number[]> | number[];
|
|
34
|
+
|
|
35
|
+
export type ExtractFramesProps = {
|
|
36
|
+
src: string;
|
|
37
|
+
timestampsInSeconds: number[] | ExtractFramesTimestampsInSecondsFn;
|
|
38
|
+
onVideoSample: (sample: VideoSample) => void;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export async function extractFrames({
|
|
43
|
+
src,
|
|
44
|
+
timestampsInSeconds,
|
|
45
|
+
onVideoSample,
|
|
46
|
+
signal,
|
|
47
|
+
}: ExtractFramesProps): Promise<void> {
|
|
48
|
+
using input = new Input({
|
|
49
|
+
formats: ALL_FORMATS,
|
|
50
|
+
source: new UrlSource(src),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const [durationInSeconds, format, videoTrack] = await Promise.all([
|
|
54
|
+
input.computeDuration(),
|
|
55
|
+
input.getFormat(),
|
|
56
|
+
input.getPrimaryVideoTrack(),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
if (!videoTrack) {
|
|
60
|
+
throw new Error("No video track found in the input");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (signal?.aborted) {
|
|
64
|
+
throw new Error("Aborted");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timestamps =
|
|
68
|
+
typeof timestampsInSeconds === "function"
|
|
69
|
+
? await timestampsInSeconds({
|
|
70
|
+
track: {
|
|
71
|
+
width: videoTrack.displayWidth,
|
|
72
|
+
height: videoTrack.displayHeight,
|
|
73
|
+
},
|
|
74
|
+
container: format.name,
|
|
75
|
+
durationInSeconds,
|
|
76
|
+
})
|
|
77
|
+
: timestampsInSeconds;
|
|
78
|
+
|
|
79
|
+
if (timestamps.length === 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (signal?.aborted) {
|
|
84
|
+
throw new Error("Aborted");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const sink = new VideoSampleSink(videoTrack);
|
|
88
|
+
|
|
89
|
+
for await (using videoSample of sink.samplesAtTimestamps(timestamps)) {
|
|
90
|
+
if (signal?.aborted) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!videoSample) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onVideoSample(videoSample);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Basic usage
|
|
104
|
+
|
|
105
|
+
Extract frames at specific timestamps:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
await extractFrames({
|
|
109
|
+
src: "https://remotion.media/video.mp4",
|
|
110
|
+
timestampsInSeconds: [0, 1, 2, 3, 4],
|
|
111
|
+
onVideoSample: (sample) => {
|
|
112
|
+
const canvas = document.createElement("canvas");
|
|
113
|
+
canvas.width = sample.displayWidth;
|
|
114
|
+
canvas.height = sample.displayHeight;
|
|
115
|
+
const ctx = canvas.getContext("2d");
|
|
116
|
+
sample.draw(ctx!, 0, 0);
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Creating a filmstrip
|
|
122
|
+
|
|
123
|
+
Use a callback function to dynamically calculate timestamps based on video metadata:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const canvasWidth = 500;
|
|
127
|
+
const canvasHeight = 80;
|
|
128
|
+
const fromSeconds = 0;
|
|
129
|
+
const toSeconds = 10;
|
|
130
|
+
|
|
131
|
+
await extractFrames({
|
|
132
|
+
src: "https://remotion.media/video.mp4",
|
|
133
|
+
timestampsInSeconds: async ({ track, durationInSeconds }) => {
|
|
134
|
+
const aspectRatio = track.width / track.height;
|
|
135
|
+
const amountOfFramesFit = Math.ceil(
|
|
136
|
+
canvasWidth / (canvasHeight * aspectRatio)
|
|
137
|
+
);
|
|
138
|
+
const segmentDuration = toSeconds - fromSeconds;
|
|
139
|
+
const timestamps: number[] = [];
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < amountOfFramesFit; i++) {
|
|
142
|
+
timestamps.push(
|
|
143
|
+
fromSeconds + (segmentDuration / amountOfFramesFit) * (i + 0.5)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return timestamps;
|
|
148
|
+
},
|
|
149
|
+
onVideoSample: (sample) => {
|
|
150
|
+
console.log(`Frame at ${sample.timestamp}s`);
|
|
151
|
+
|
|
152
|
+
const canvas = document.createElement("canvas");
|
|
153
|
+
canvas.width = sample.displayWidth;
|
|
154
|
+
canvas.height = sample.displayHeight;
|
|
155
|
+
const ctx = canvas.getContext("2d");
|
|
156
|
+
sample.draw(ctx!, 0, 0);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Cancellation with AbortSignal
|
|
162
|
+
|
|
163
|
+
Cancel frame extraction after a timeout:
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
|
|
168
|
+
setTimeout(() => controller.abort(), 5000);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
await extractFrames({
|
|
172
|
+
src: "https://remotion.media/video.mp4",
|
|
173
|
+
timestampsInSeconds: [0, 1, 2, 3, 4],
|
|
174
|
+
onVideoSample: (sample) => {
|
|
175
|
+
using frame = sample;
|
|
176
|
+
const canvas = document.createElement("canvas");
|
|
177
|
+
canvas.width = frame.displayWidth;
|
|
178
|
+
canvas.height = frame.displayHeight;
|
|
179
|
+
const ctx = canvas.getContext("2d");
|
|
180
|
+
frame.draw(ctx!, 0, 0);
|
|
181
|
+
},
|
|
182
|
+
signal: controller.signal,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
console.log("Frame extraction complete!");
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error("Frame extraction was aborted or failed:", error);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Timeout with Promise.race
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
|
|
196
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
197
|
+
const timeoutId = setTimeout(() => {
|
|
198
|
+
controller.abort();
|
|
199
|
+
reject(new Error("Frame extraction timed out after 10 seconds"));
|
|
200
|
+
}, 10000);
|
|
201
|
+
|
|
202
|
+
controller.signal.addEventListener("abort", () => clearTimeout(timeoutId), {
|
|
203
|
+
once: true,
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await Promise.race([
|
|
209
|
+
extractFrames({
|
|
210
|
+
src: "https://remotion.media/video.mp4",
|
|
211
|
+
timestampsInSeconds: [0, 1, 2, 3, 4],
|
|
212
|
+
onVideoSample: (sample) => {
|
|
213
|
+
using frame = sample;
|
|
214
|
+
const canvas = document.createElement("canvas");
|
|
215
|
+
canvas.width = frame.displayWidth;
|
|
216
|
+
canvas.height = frame.displayHeight;
|
|
217
|
+
const ctx = canvas.getContext("2d");
|
|
218
|
+
frame.draw(ctx!, 0, 0);
|
|
219
|
+
},
|
|
220
|
+
signal: controller.signal,
|
|
221
|
+
}),
|
|
222
|
+
timeoutPromise,
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
console.log("Frame extraction complete!");
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error("Frame extraction was aborted or failed:", error);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fonts
|
|
3
|
+
description: Loading Google Fonts and local fonts in Remotion
|
|
4
|
+
metadata:
|
|
5
|
+
tags: fonts, google-fonts, typography, text
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Using fonts in Remotion
|
|
9
|
+
|
|
10
|
+
## Google Fonts with @remotion/google-fonts
|
|
11
|
+
|
|
12
|
+
The recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.
|
|
13
|
+
|
|
14
|
+
### Prerequisites
|
|
15
|
+
|
|
16
|
+
First, the @remotion/google-fonts package needs to be installed.
|
|
17
|
+
If it is not installed, use the following command:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx remotion add @remotion/google-fonts # If project uses npm
|
|
21
|
+
bunx remotion add @remotion/google-fonts # If project uses bun
|
|
22
|
+
yarn remotion add @remotion/google-fonts # If project uses yarn
|
|
23
|
+
pnpm exec remotion add @remotion/google-fonts # If project uses pnpm
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { loadFont } from "@remotion/google-fonts/Lobster";
|
|
28
|
+
|
|
29
|
+
const { fontFamily } = loadFont();
|
|
30
|
+
|
|
31
|
+
export const MyComposition = () => {
|
|
32
|
+
return <div style={{ fontFamily }}>Hello World</div>;
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Preferrably, specify only needed weights and subsets to reduce file size:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { loadFont } from "@remotion/google-fonts/Roboto";
|
|
40
|
+
|
|
41
|
+
const { fontFamily } = loadFont("normal", {
|
|
42
|
+
weights: ["400", "700"],
|
|
43
|
+
subsets: ["latin"],
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Waiting for font to load
|
|
48
|
+
|
|
49
|
+
Use `waitUntilDone()` if you need to know when the font is ready:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { loadFont } from "@remotion/google-fonts/Lobster";
|
|
53
|
+
|
|
54
|
+
const { fontFamily, waitUntilDone } = loadFont();
|
|
55
|
+
|
|
56
|
+
await waitUntilDone();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Local fonts with @remotion/fonts
|
|
60
|
+
|
|
61
|
+
For local font files, use the `@remotion/fonts` package.
|
|
62
|
+
|
|
63
|
+
### Prerequisites
|
|
64
|
+
|
|
65
|
+
First, install @remotion/fonts:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npx remotion add @remotion/fonts # If project uses npm
|
|
69
|
+
bunx remotion add @remotion/fonts # If project uses bun
|
|
70
|
+
yarn remotion add @remotion/fonts # If project uses yarn
|
|
71
|
+
pnpm exec remotion add @remotion/fonts # If project uses pnpm
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Loading a local font
|
|
75
|
+
|
|
76
|
+
Place your font file in the `public/` folder and use `loadFont()`:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { loadFont } from "@remotion/fonts";
|
|
80
|
+
import { staticFile } from "remotion";
|
|
81
|
+
|
|
82
|
+
await loadFont({
|
|
83
|
+
family: "MyFont",
|
|
84
|
+
url: staticFile("MyFont-Regular.woff2"),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const MyComposition = () => {
|
|
88
|
+
return <div style={{ fontFamily: "MyFont" }}>Hello World</div>;
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Loading multiple weights
|
|
93
|
+
|
|
94
|
+
Load each weight separately with the same family name:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { loadFont } from "@remotion/fonts";
|
|
98
|
+
import { staticFile } from "remotion";
|
|
99
|
+
|
|
100
|
+
await Promise.all([
|
|
101
|
+
loadFont({
|
|
102
|
+
family: "Inter",
|
|
103
|
+
url: staticFile("Inter-Regular.woff2"),
|
|
104
|
+
weight: "400",
|
|
105
|
+
}),
|
|
106
|
+
loadFont({
|
|
107
|
+
family: "Inter",
|
|
108
|
+
url: staticFile("Inter-Bold.woff2"),
|
|
109
|
+
weight: "700",
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Available options
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
loadFont({
|
|
118
|
+
family: "MyFont", // Required: name to use in CSS
|
|
119
|
+
url: staticFile("font.woff2"), // Required: font file URL
|
|
120
|
+
format: "woff2", // Optional: auto-detected from extension
|
|
121
|
+
weight: "400", // Optional: font weight
|
|
122
|
+
style: "normal", // Optional: normal or italic
|
|
123
|
+
display: "block", // Optional: font-display behavior
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Using in components
|
|
128
|
+
|
|
129
|
+
Call `loadFont()` at the top level of your component or in a separate file that's imported early:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { loadFont } from "@remotion/google-fonts/Montserrat";
|
|
133
|
+
|
|
134
|
+
const { fontFamily } = loadFont("normal", {
|
|
135
|
+
weights: ["400", "700"],
|
|
136
|
+
subsets: ["latin"],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export const Title: React.FC<{ text: string }> = ({ text }) => {
|
|
140
|
+
return (
|
|
141
|
+
<h1
|
|
142
|
+
style={{
|
|
143
|
+
fontFamily,
|
|
144
|
+
fontSize: 80,
|
|
145
|
+
fontWeight: "bold",
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
{text}
|
|
149
|
+
</h1>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: get-audio-duration
|
|
3
|
+
description: Getting the duration of an audio file in seconds with Mediabunny
|
|
4
|
+
metadata:
|
|
5
|
+
tags: duration, audio, length, time, seconds, mp3, wav
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Getting audio duration with Mediabunny
|
|
9
|
+
|
|
10
|
+
Mediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.
|
|
11
|
+
|
|
12
|
+
## Getting audio duration
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
|
|
16
|
+
|
|
17
|
+
export const getAudioDuration = async (src: string) => {
|
|
18
|
+
const input = new Input({
|
|
19
|
+
formats: ALL_FORMATS,
|
|
20
|
+
source: new UrlSource(src, {
|
|
21
|
+
getRetryDelay: () => null,
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const durationInSeconds = await input.computeDuration();
|
|
26
|
+
return durationInSeconds;
|
|
27
|
+
};
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
const duration = await getAudioDuration("https://remotion.media/audio.mp3");
|
|
34
|
+
console.log(duration); // e.g. 180.5 (seconds)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Using with local files
|
|
38
|
+
|
|
39
|
+
For local files, use `FileSource` instead of `UrlSource`:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
|
|
43
|
+
|
|
44
|
+
const input = new Input({
|
|
45
|
+
formats: ALL_FORMATS,
|
|
46
|
+
source: new FileSource(file), // File object from input or drag-drop
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const durationInSeconds = await input.computeDuration();
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Using with staticFile in Remotion
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { staticFile } from "remotion";
|
|
56
|
+
|
|
57
|
+
const duration = await getAudioDuration(staticFile("audio.mp3"));
|
|
58
|
+
```
|