remotion — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited remotion (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Use this skill when working with Remotion code. Remotion lets you create videos programmatically using React components.
npx create-video@latest my-video
cd my-video
npm start # Preview in browser
npm run render # Render to MP4A <Composition> defines a renderable video with dimensions, fps, and duration.
import { Composition } from "remotion";
import { MyVideo } from "./MyVideo";
export const RemotionRoot = () => (
<Composition
id="MyVideo"
component={MyVideo}
durationInFrames={300}
fps={30}
width={1920}
height={1080}
defaultProps={{ title: "Hello World" } satisfies MyVideoProps}
/>
);Use type declarations for props (not interface) for defaultProps type safety.
Use <Folder> to organize compositions in the sidebar. Use <Still> for single-frame outputs (thumbnails, OG images).
All animations MUST use useCurrentFrame(). CSS transitions and Tailwind animation classes are FORBIDDEN -- they will not render correctly.
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
export const FadeIn = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateRight: "clamp",
});
return <div style={{ opacity }}>Hello World!</div>;
};Springs provide natural motion. They animate from 0 to 1.
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps });
// With config:
const smooth = spring({ frame, fps, config: { damping: 200 } }); // No bounce
const snappy = spring({ frame, fps, config: { damping: 20, stiffness: 200 } });
const bouncy = spring({ frame, fps, config: { damping: 8 } });
const heavy = spring({ frame, fps, config: { damping: 15, stiffness: 80, mass: 2 } });Use delay parameter to delay spring start:
const entrance = spring({ frame, fps, delay: 20 });// Linear interpolation with clamping
const opacity = interpolate(frame, [0, 100], [0, 1], {
extrapolateRight: "clamp",
extrapolateLeft: "clamp",
});
// Multi-step interpolation
const y = interpolate(frame, [0, 30, 60, 90], [100, 0, 0, -100]);Use <Sequence> to delay when elements appear. Always use premountFor to preload components.
import { Sequence } from "remotion";
const { fps } = useVideoConfig();
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Title />
</Sequence>
<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Subtitle />
</Sequence>Use layout="none" to prevent absolute positioning wrapper.
Use <Series> for sequential playback without overlap:
import { Series } from "remotion";
<Series>
<Series.Sequence durationInFrames={45}><Intro /></Series.Sequence>
<Series.Sequence durationInFrames={60}><MainContent /></Series.Sequence>
<Series.Sequence durationInFrames={30}><Outro /></Series.Sequence>
</Series>Negative offset creates overlapping transitions:
<Series.Sequence offset={-15} durationInFrames={60}>
<SceneB />
</Series.Sequence>npx remotion add @remotion/transitionsimport { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 15 })}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>import { LightLeak } from "@remotion/light-leaks";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Overlay durationInFrames={20}>
<LightLeak />
</TransitionSeries.Overlay>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>An overlay cannot be adjacent to a transition or another overlay.
npx remotion add @remotion/mediaimport { Audio } from "@remotion/media";
import { staticFile } from "remotion";
// Basic audio
<Audio src={staticFile("audio.mp3")} />
// With trimming (values in frames)
<Audio src={staticFile("audio.mp3")} trimBefore={2 * fps} trimAfter={10 * fps} />
// Volume control
<Audio src={staticFile("audio.mp3")} volume={0.5} />
// Dynamic volume (callback receives frame)
<Audio src={staticFile("audio.mp3")} volume={(f) =>
interpolate(f, [0, 30], [0, 1], { extrapolateRight: "clamp" })
} />
// Speed change
<Audio src={staticFile("audio.mp3")} playbackRate={1.5} />
// Delay audio start
<Sequence from={1 * fps}>
<Audio src={staticFile("audio.mp3")} />
</Sequence>Multiple <Audio> components layer automatically.
import { Video } from "@remotion/media";
// Basic video
<Video src={staticFile("video.mp4")} />
// With trimming
<Video src={staticFile("video.mp4")} trimBefore={2 * fps} trimAfter={10 * fps} />
// Speed, volume, looping
<Video src={staticFile("video.mp4")} playbackRate={2} volume={0.5} loop />
// For rendering performance, use OffthreadVideo
import { OffthreadVideo } from "remotion";
<OffthreadVideo src={staticFile("video.mp4")} />ALWAYS use <OffthreadVideo> for better rendering performance. Use <Video> only during preview.
Generate speech per scene, then use calculateMetadata to size the composition dynamically.
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: "POST",
headers: {
"xi-api-key": process.env.ELEVENLABS_API_KEY!,
"Content-Type": "application/json",
Accept: "audio/mpeg",
},
body: JSON.stringify({
text: "Welcome to the show.",
model_id: "eleven_multilingual_v2",
voice_settings: { stability: 0.5, similarity_boost: 0.75, style: 0.3 },
}),
},
);
const audioBuffer = Buffer.from(await response.arrayBuffer());
writeFileSync(`public/voiceover/scene-01.mp3`, audioBuffer);import { CalculateMetadataFunction, staticFile } from "remotion";
const FPS = 30;
const SCENES = ["voiceover/scene-01.mp3", "voiceover/scene-02.mp3"];
export const calculateMetadata: CalculateMetadataFunction<Props> = async () => {
const durations = await Promise.all(
SCENES.map((file) => getAudioDuration(staticFile(file)))
);
const totalFrames = durations.reduce((sum, d) => sum + Math.ceil(d * FPS), 0);
return { durationInFrames: totalFrames };
};Place files in public/ and reference with staticFile():
import { staticFile, Img } from "remotion";
<Img src={staticFile("logo.png")} />NEVER use require() or import for media files. Always use staticFile().
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();
// Or local fonts
const { fontFamily } = loadFont({
url: staticFile("fonts/MyFont.woff2"),
display: "swap",
});FFmpeg is bundled -- no install needed:
bunx remotion ffmpeg -i input.mp4 output.mp3
bunx remotion ffprobe input.mp4For trimming, prefer the trimBefore/trimAfter props over FFmpeg. Use FFmpeg for format conversion or operations Remotion cannot do natively.
When using FFmpeg to trim, MUST re-encode to avoid frozen frames:
bunx remotion ffmpeg -ss 00:00:05 -i public/input.mp4 -to 00:00:10 -c:v libx264 -c:a aac public/output.mp4# Render specific composition
npx remotion render MyVideo output.mp4
# Render a still frame
npx remotion still MyVideo --frame=100 thumbnail.png
# With custom props
npx remotion render MyVideo output.mp4 --props='{"title": "Custom"}'
# Specific codec
npx remotion render MyVideo output.webm --codec=vp8const SCENES = [
{ component: TitleScene, duration: 90 },
{ component: ProblemScene, duration: 150 },
{ component: SolutionScene, duration: 120 },
{ component: CTAScene, duration: 90 },
];
export const MyVideo = () => {
let offset = 0;
return (
<>
{SCENES.map(({ component: Component, duration }, i) => {
const from = offset;
offset += duration;
return (
<Sequence key={i} from={from} durationInFrames={duration}>
<Component />
</Sequence>
);
})}
</>
);
};<>
{/* Background music at low volume */}
<Audio src={staticFile("music/bg.mp3")} volume={0.12} loop />
{/* Scene voiceovers at full volume */}
<Sequence from={0} durationInFrames={scene1Frames}>
<Audio src={staticFile("voiceover/scene-01.mp3")} volume={1} />
</Sequence>
<Sequence from={scene1Frames} durationInFrames={scene2Frames}>
<Audio src={staticFile("voiceover/scene-02.mp3")} volume={1} />
</Sequence>
</>useCurrentFrame() + interpolate()/spring()require()premountFor<video> for renderingfpsdefaultProps type safetySource: remotion-dev/skills, remotion.dev
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.