R RenderComp
remotion offthreadvideo video-editing b-roll tutorial

Embedding Video Files in Remotion: OffthreadVideo, Trimming, and Layering Explained

Embedding Video Files in Remotion: OffthreadVideo, Trimming, and Layering Explained

Most Remotion tutorials focus on things you generate from scratch — animated text, charts, shapes driven by interpolate() and spring(). But real production work almost always mixes generated graphics with footage: a screen recording under a product walkthrough, an interview clip with b-roll cutaways, stock motion backgrounds behind a title card.

Embedding video files is where Remotion behaves least like ordinary React. A <video> tag that works fine in a web app will flicker, repeat frames, or drift out of sync when Remotion renders it frame by frame. Remotion ships a dedicated component — <OffthreadVideo> — specifically to solve this, along with a small but complete API for trimming, muting, volume ramps, and playback speed.

This guide covers the full workflow: choosing the right video component, loading clips with staticFile(), trimming and offsetting them on the timeline, controlling audio, and compositing multiple clips into overlays, picture-in-picture layouts, and split screens. Every example is copy-paste ready for Remotion 4.x.


Video vs OffthreadVideo: Which One and Why

Remotion currently has three ways to embed a video file, and the naming has shifted over time, so it is worth being precise:

  • <OffthreadVideo> from remotion — the battle-tested default for rendered output. During rendering, it extracts the exact frame you need using a native frame extractor running off the main thread, and displays it as an image. During preview in the Studio or the <Player>, it falls back to a regular HTML5 video element for smooth playback.
  • <Video> from @remotion/media — the newest component, built on WebCodecs and Mediabunny. Remotion recommends it for new projects, and it shares most of the prop API described in this guide (trimBefore, trimAfter, volume, playbackRate, muted).
  • <Html5Video> from remotion — the legacy component, previously named <Video> in the core package. It renders a plain <video> tag even during rendering.

Why does the off-thread approach matter? Browsers do not guarantee frame-accurate seeking on a <video> element. When Remotion renders, it seeks to a precise timestamp for every single frame, hundreds or thousands of times per composition. Under that load, an HTML5 video element can return the previous frame, a duplicated frame, or a blank frame — which shows up in your output as flicker and stutter. <OffthreadVideo> sidesteps the browser entirely at render time and extracts frames directly from the file, making it frame-perfect.

The practical rule: use <OffthreadVideo> (or the new @remotion/media <Video>) for anything you render to a file. Reach for <Html5Video> only in interactive <Player> scenarios where its streaming behavior is specifically needed.

The rest of this guide uses <OffthreadVideo>, since it targets Remotion’s most common rendering path — but nearly everything transfers directly to the newer <Video> component.


Adding Clips with staticFile and Remote URLs

Local video files belong in the public/ folder at your project root. Reference them with staticFile() — never with relative paths or require():

import { AbsoluteFill, OffthreadVideo, staticFile } from 'remotion';

export const FootageBackground: React.FC = () => {
  return (
    <AbsoluteFill style={{ backgroundColor: '#000' }}>
      <OffthreadVideo
        src={staticFile('footage/city-timelapse.mp4')}
        style={{
          width: '100%',
          height: '100%',
          objectFit: 'cover',
        }}
      />
    </AbsoluteFill>
  );
};

Remote URLs work too:

<OffthreadVideo src="https://assets.example.com/clips/intro.mp4" />

Two things to know about remote sources:

  1. <OffthreadVideo> downloads the entire file before extracting frames. For a 200 MB clip on a slow host, that download happens inside Remotion’s delayRender() window and can hit the timeout. You can raise it per-component with delayRenderTimeoutInMilliseconds, but the better fix is to keep production assets local in public/ or on fast storage close to your render infrastructure.
  2. No CORS configuration is required for <OffthreadVideo> remote sources during rendering — the frame extractor fetches the file outside the browser. This is a quiet advantage over HTML5-based playback, which needs proper CORS headers.

Trimming and Offsetting Clips on the Timeline

Trimming — using only a slice of a source clip — is handled by two props measured in frames:

  • trimBefore — skip this many frames from the start of the file
  • trimAfter — stop using the file at this frame mark

Offsetting — deciding when the clip appears in your composition — is handled by wrapping the video in a <Sequence>. These are independent axes: trim controls which part of the file plays, the sequence controls where on the timeline it plays.

Here is a talking-head edit that plays seconds 3–8 of an interview file, starting at the 2-second mark of the composition:

import {
  AbsoluteFill,
  OffthreadVideo,
  Sequence,
  staticFile,
  useVideoConfig,
} from 'remotion';

export const InterviewCut: React.FC = () => {
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{ backgroundColor: '#0a0a0a' }}>
      <Sequence from={2 * fps} durationInFrames={5 * fps}>
        <OffthreadVideo
          src={staticFile('interview.mp4')}
          trimBefore={3 * fps} // skip the first 3 seconds of the file
          trimAfter={8 * fps}  // stop at the 8-second mark of the file
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      </Sequence>
    </AbsoluteFill>
  );
};

Always compute frame values from useVideoConfig().fps rather than hardcoding numbers — the same component then works in 24, 30, and 60 fps compositions.

Migration note: older Remotion code uses startFrom and endAt for the same purpose. Those props are deprecated as of Remotion 4.0.319 in favor of trimBefore and trimAfter (they cannot be combined — pick the new names). If you are copying snippets from older tutorials, translate them: startFromtrimBefore, endAttrimAfter.

Chaining several trimmed clips back to back — a multi-cut edit — is just a series of <Sequence> blocks with consecutive from values. For the full set of timeline composition patterns, including the <Series> helper that removes the offset bookkeeping, see our guide to Sequence and Series timing.


Muting, Volume Ramps, and Playback Rate

Muting

B-roll should almost never carry its own audio. The muted prop drops the clip’s audio track entirely:

<OffthreadVideo src={staticFile('b-roll/workshop.mp4')} muted />

Muting is not just cosmetic — a muted clip’s audio is excluded from audio extraction during the render, so liberal use of muted on overlay footage keeps renders lean and guarantees no stray ambience leaks into your mix.

Volume and Volume Ramps

The volume prop accepts a static number between 0 and 1, or a callback that receives the frame number (relative to when the clip started playing) and returns the volume for that frame. The callback form gives you frame-accurate fades and ducking:

import { interpolate, OffthreadVideo, staticFile, useVideoConfig } from 'remotion';

export const FadedInterview: React.FC = () => {
  const { fps } = useVideoConfig();

  return (
    <OffthreadVideo
      src={staticFile('interview.mp4')}
      volume={(f) =>
        interpolate(f, [0, 1 * fps], [0, 1], {
          extrapolateLeft: 'clamp',
          extrapolateRight: 'clamp',
        })
      }
    />
  );
};

This fades the interview audio in over the first second. The same pattern handles ducking: interpolate the volume down to 0.2 across the frame range where a voiceover plays, then back up. This is the exact same volume API used by Remotion’s audio component — the patterns in our Remotion Audio component guide apply to video audio unchanged.

Playback Rate

playbackRate speeds up or slows down the clip:

<OffthreadVideo src={staticFile('drone-shot.mp4')} playbackRate={0.5} /> {/* half speed */}
<OffthreadVideo src={staticFile('screen-capture.mp4')} playbackRate={2} /> {/* double speed */}

Two caveats: reverse playback (playbackRate={-1}) is not supported — reverse the file beforehand with FFmpeg if you need it. And remember that a clip played at 0.5× consumes source frames half as fast, so a 10-second file fills 20 seconds of timeline.

Looping

Unlike <Html5Video>, <OffthreadVideo> has no loop prop. To loop a short background clip for the length of a longer composition, wrap it in <Loop>:

import { Loop, OffthreadVideo, staticFile, useVideoConfig } from 'remotion';

export const LoopingBackground: React.FC = () => {
  const { fps } = useVideoConfig();

  return (
    <Loop durationInFrames={6 * fps}> {/* the source clip is 6 seconds long */}
      <OffthreadVideo src={staticFile('ambient-loop.mp4')} muted />
    </Loop>
  );
};

Layering B-Roll: Overlays, Picture-in-Picture, and Split Screens

This is where Remotion’s React model pays off. Layering footage is just stacking children inside <AbsoluteFill> — later children render on top, exactly like z-order in the DOM.

The Classic B-Roll Cutaway

The standard documentary pattern: the interview (a-roll) runs continuously and carries all the audio; b-roll covers the picture for a few seconds while the interview audio continues underneath.

import {
  AbsoluteFill,
  interpolate,
  OffthreadVideo,
  Sequence,
  staticFile,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';

const FadeIn: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 12], [0, 1], {
    extrapolateRight: 'clamp',
  });
  return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>;
};

export const InterviewWithBRoll: React.FC = () => {
  const { fps } = useVideoConfig();

  return (
    <AbsoluteFill style={{ backgroundColor: '#000' }}>
      {/* A-roll: runs the entire composition, carries the audio */}
      <OffthreadVideo
        src={staticFile('interview.mp4')}
        style={{ width: '100%', height: '100%', objectFit: 'cover' }}
      />

      {/* B-roll: covers the picture from 4s to 9s, muted */}
      <Sequence from={4 * fps} durationInFrames={5 * fps}>
        <FadeIn>
          <OffthreadVideo
            src={staticFile('b-roll/workshop.mp4')}
            muted
            trimBefore={2 * fps}
            style={{ width: '100%', height: '100%', objectFit: 'cover' }}
          />
        </FadeIn>
      </Sequence>
    </AbsoluteFill>
  );
};

Because the a-roll is never unmounted, its audio plays straight through the cutaway — the b-roll simply paints over the picture. This is the b-roll compositing behavior editors expect from a traditional NLE, expressed in about thirty lines of TypeScript.

Picture-in-Picture

A webcam bubble over a screen recording is a position: absolute container with rounded corners and a spring entrance:

import {
  AbsoluteFill,
  interpolate,
  OffthreadVideo,
  spring,
  staticFile,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';

export const ScreencastWithWebcam: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const enter = spring({
    frame,
    fps,
    config: { mass: 0.8, stiffness: 120, damping: 16 },
  });
  const translateY = interpolate(enter, [0, 1], [60, 0]);
  const opacity = interpolate(enter, [0, 1], [0, 1], {
    extrapolateRight: 'clamp',
  });

  return (
    <AbsoluteFill style={{ backgroundColor: '#111' }}>
      {/* Base layer: the screen recording, muted */}
      <OffthreadVideo
        src={staticFile('screen-recording.mp4')}
        muted
        style={{ width: '100%', height: '100%', objectFit: 'contain' }}
      />

      {/* PiP layer: webcam clip carries the narration audio */}
      <div
        style={{
          position: 'absolute',
          right: 48,
          bottom: 48,
          width: 420,
          height: 236,
          borderRadius: 16,
          overflow: 'hidden',
          boxShadow: '0 12px 40px rgba(0, 0, 0, 0.4)',
          transform: `translateY(${translateY}px)`,
          opacity,
        }}
      >
        <OffthreadVideo
          src={staticFile('webcam.mp4')}
          style={{ width: '100%', height: '100%', objectFit: 'cover' }}
        />
      </div>
    </AbsoluteFill>
  );
};

The overflow: 'hidden' on the container is what makes borderRadius clip the footage. Everything about the bubble — position, size, entrance physics — is a prop away from being parameterized.

Split Screens

<AbsoluteFill> is a flex container, so a two-up comparison layout is plain flexbox:

<AbsoluteFill style={{ flexDirection: 'row' }}>
  <div style={{ flex: 1, overflow: 'hidden' }}>
    <OffthreadVideo
      src={staticFile('before.mp4')}
      muted
      style={{ width: '100%', height: '100%', objectFit: 'cover' }}
    />
  </div>
  <div style={{ flex: 1, overflow: 'hidden' }}>
    <OffthreadVideo
      src={staticFile('after.mp4')}
      muted
      style={{ width: '100%', height: '100%', objectFit: 'cover' }}
    />
  </div>
</AbsoluteFill>

Three-up grids, 2×2 walls, or animated split positions (interpolate the flex values or widths over frames) all follow from the same idea.


Sizing and Fitting Footage Inside AbsoluteFill

Footage rarely matches your composition’s aspect ratio — 16:9 source in a 9:16 vertical composition is the everyday case. The fix is standard CSS object-fit on the video’s style:

  • objectFit: 'cover' — fills the container completely, cropping the overflow. The right default for backgrounds and full-bleed b-roll.
  • objectFit: 'contain' — shows the whole frame, adding letterbox/pillarbox space. Right for screen recordings where cropping would cut off UI.
  • No objectFit with explicit dimensions — stretches and distorts. Almost never what you want.
{/* 16:9 clip in a 1080×1920 vertical composition, center-cropped */}
<OffthreadVideo
  src={staticFile('landscape-clip.mp4')}
  muted
  style={{
    width: '100%',
    height: '100%',
    objectFit: 'cover',
    objectPosition: 'center 30%', // bias the crop toward the upper part of the frame
  }}
/>

objectPosition controls which part of the frame survives the crop — useful when the subject is not centered. For a subtle Ken Burns feel, wrap the video in a container with overflow: 'hidden' and animate a scale transform on the video from 1 to 1.08 across the scene.

<AbsoluteFill> itself is doing more work here than it looks: it is a position: absolute flex container spanning the full composition, which is why stacked layers align perfectly without manual coordinates. Its layout behavior — and when to use it versus regular divs — is covered in depth in our AbsoluteFill layout guide.


Codec Gotchas and “Could Not Decode” Errors

<OffthreadVideo> supports H.264, H.265/HEVC, VP8, VP9, AV1, and ProRes. That covers most files — but “most” is not “all,” and decode errors are the top support question around embedded footage. The usual suspects:

  • Exotic pixel formats. Some cameras and screen recorders output 10-bit or 4:4:4 chroma files that decoders reject or render incorrectly.
  • Variable frame rate (VFR) footage. Phone screen recordings and OBS captures often have VFR, which can cause seek inaccuracy and audio drift when the file is sampled frame by frame.
  • Broken container metadata. Files cut off mid-write (missing moov atom) or produced by unusual muxers may play in VLC but fail frame extraction.

The universal fix is to normalize the file with FFmpeg before putting it in public/:

ffmpeg -i input.mov \
  -c:v libx264 -pix_fmt yuv420p -r 30 \
  -c:a aac \
  -movflags +faststart \
  output.mp4

This re-encodes to H.264 with the widely supported yuv420p pixel format, a constant 30 fps, and AAC audio. Ten seconds of preprocessing eliminates an entire class of render failures.

Two more defenses worth knowing:

  • onError lets you catch a failing source and render a fallback instead of crashing the render.
  • transparent enables alpha-channel extraction for footage that has one (ProRes 4444, VP9 with alpha). Frames are extracted as PNG instead of BMP, which is slower — only enable it when you actually need transparency.

If a remote clip fails with a timeout rather than a decode error, revisit the earlier point: <OffthreadVideo> downloads the whole file. Move the asset local or raise delayRenderTimeoutInMilliseconds.


Drop-In B-Roll and Motion Object Packs

Everything above treats footage as something you already have. In practice, the footage layer is often the bottleneck: you need an animated background here, a motion accent there, a texture loop behind a title — and shooting or sourcing each one interrupts the actual edit.

This is where pre-built motion assets earn their place in a Remotion workflow. Because a clip is just an <OffthreadVideo> with a src, any well-encoded MP4 or transparent WebM drops straight into the layering patterns from this guide: loop it with <Loop>, mute it, trim it, stack it under or over your generated graphics.

The RenderComp template library includes motion object and background packs built exactly for this — clean, loopable clips and animated elements designed to composite into Remotion projects without color or codec surprises, alongside full template compositions with typed props. If you would rather start from a working edit than a blank <AbsoluteFill>, browse the collection at rendercomp.com.


FAQ

Q: Why does my embedded video flicker or repeat frames in the rendered output?

You are almost certainly rendering with an HTML5-based video component. Browser <video> seeking is not frame-accurate under render load. Switch to <OffthreadVideo> (or <Video> from @remotion/media) and the flicker disappears, because frames are extracted directly from the file instead of sampled from a playing video element.

Q: Are trimBefore and trimAfter in frames or seconds?

Frames. Multiply seconds by useVideoConfig().fps — e.g. trimBefore={3 * fps} skips the first three seconds. The older startFrom/endAt props (also frames) are deprecated aliases from before Remotion 4.0.319.

Q: Can I play a clip backwards?

Not with a negative playbackRate — reverse playback is unsupported. Pre-process the file instead: ffmpeg -i input.mp4 -vf reverse -af areverse reversed.mp4, then embed the reversed file normally.

Q: How do I loop a short clip for the whole composition?

<OffthreadVideo> has no loop prop. Wrap it in <Loop durationInFrames={clipLengthInFrames}> from the core remotion package. Set durationInFrames to the source clip’s length so each iteration plays fully before restarting.

Q: Does <OffthreadVideo> work in the Remotion Player?

Yes. In the <Player> and in Remotion Studio it renders a regular HTML5 video element for smooth real-time preview; the off-thread frame extraction only happens during actual rendering. You get interactive playback in development and frame-perfect output in production from the same component.

Q: How do I make the composition exactly as long as the video file?

Use calculateMetadata on your <Composition> to read the file’s duration asynchronously and return durationInFrames computed from it. This keeps the timeline length in sync with the asset even when the file changes.

Q: My clip’s audio sounds wrong at 0.5× speed. What can I do?

Slowing audio down stretches it audibly. For b-roll, the simple answer is muted — slow-motion footage rarely needs its location sound. If the audio matters, keep the video at 1× and create the slow-motion version in FFmpeg with proper time-stretching before embedding.


Summary

Embedding footage in Remotion comes down to a handful of reliable patterns:

  1. Use <OffthreadVideo> (or @remotion/media’s <Video>) for rendered output — never a bare HTML5 video element
  2. Load local files from public/ with staticFile(); keep large assets local rather than remote
  3. Trim with trimBefore/trimAfter (in frames), and position clips on the timeline with <Sequence from={...}>
  4. Mute b-roll, ramp volume with the (frame) => number callback, and adjust speed with playbackRate
  5. Layer clips by stacking them in <AbsoluteFill> — cutaways, picture-in-picture, and split screens are all just absolutely positioned React elements
  6. Fit mismatched aspect ratios with objectFit: 'cover' or 'contain', and normalize problem files with a quick FFmpeg re-encode

Skip the blank canvas — explore production-ready Remotion templates and motion asset packs at RenderComp

Every template ships with editable TypeScript source and typed props, so trimmed clips, b-roll layers, and picture-in-picture slots are ready to accept your footage on day one.

Now available

Get 1,000+ Remotion Templates

Pay once — no subscription. Lifetime updates. TypeScript-first.

View pricing →