Remotionに動画素材を埋め込む — OffthreadVideoの使い方とトリム・レイヤー合成
Remotionに動画素材を埋め込む — OffthreadVideoの使い方とトリム・レイヤー合成
Remotionのチュートリアルの多くは、テキストアニメーションやチャートなど「コードからゼロで生成するもの」を扱っています。しかし実務の動画制作では、生成グラフィックスと実写・収録素材を混ぜる場面がほとんどです。プロダクト解説の下に敷く画面収録、インタビュー映像に重ねるb-rollカット、タイトルカードの背景に流すモーション素材——動画ファイルの埋め込みは、実需としては最上位の作業です。
そして、この動画埋め込みこそ、Remotionが「普通のReact」から最も離れて見える領域でもあります。Webアプリでは問題なく動く <video> タグが、Remotionのフレーム単位レンダリングではチラつき、フレームの重複、音ズレを起こします。Remotionはこの問題を解決する専用コンポーネント <OffthreadVideo> を用意しており、トリム・ミュート・音量ランプ・再生速度まで、小さいながら完結したAPIを備えています。
この記事では、動画コンポーネントの選び方から、staticFile() での読み込み、タイムライン上のトリムとオフセット、音声制御、そしてオーバーレイ・ピクチャーインピクチャー・分割画面といったレイヤー合成まで、一連のワークフローを解説します。コード例はすべてRemotion 4.x でそのまま使えます。
Video と OffthreadVideo — どれを使うべきか
Remotionには動画ファイルを埋め込む方法が現在3つあり、名前が時期によって変わってきた経緯があるため、まず正確に整理します。
<OffthreadVideo>(remotionパッケージ)— レンダリング出力向けの実績あるデフォルト。レンダリング時はメインスレッド外で動くネイティブのフレーム抽出器が必要なフレームを正確に取り出し、画像として表示します。StudioやPlayerでのプレビュー時は通常のHTML5 video要素にフォールバックし、滑らかに再生されます。<Video>(@remotion/mediaパッケージ)— WebCodecsベースの最新コンポーネント。新規プロジェクトにはこちらが推奨されており、この記事で扱うprops(trimBefore/trimAfter/volume/playbackRate/muted)の大半をそのまま共有しています。<Html5Video>(remotionパッケージ)— レガシーコンポーネント。以前はコアパッケージの<Video>という名前でした。レンダリング時も素の<video>タグを描画します。
なぜ「オフスレッド」方式が重要なのでしょうか。ブラウザの <video> 要素は、フレーム精度のシークを保証していません。Remotionのレンダリングでは、コンポジションの全フレームぶん——数百回、数千回——正確なタイムスタンプへのシークが発生します。この負荷の下では、HTML5 video要素は直前のフレームや重複フレーム、空白フレームを返すことがあり、それが出力のチラつき・カクつきとして現れます。<OffthreadVideo> はレンダリング時にブラウザを完全に迂回し、ファイルから直接フレームを抽出するため、フレーム単位で正確です。
実務上のルールはシンプルです。ファイルに書き出すものには <OffthreadVideo>(または @remotion/media の <Video>)を使う。 <Html5Video> は、ストリーミング的な挙動が明確に必要なインタラクティブな <Player> の場面に限定してください。
以降の解説は <OffthreadVideo> を使いますが、新しい <Video> コンポーネントにもほぼそのまま当てはまります。
staticFile とリモートURLでクリップを読み込む
ローカルの動画ファイルはプロジェクトルートの public/ フォルダに置きます。参照は必ず staticFile() で行い、相対パスや 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>
);
};
リモートURLも使えます。
<OffthreadVideo src="https://assets.example.com/clips/intro.mp4" />
リモートソースについて知っておくべき点が2つあります。
<OffthreadVideo>はフレーム抽出の前にファイル全体をダウンロードします。 遅いホスト上の200MBのクリップだと、このダウンロードはdelayRender()のタイムアウト枠内で行われ、時間切れになることがあります。delayRenderTimeoutInMillisecondspropでコンポーネント単位に延長もできますが、本番素材はpublic/に置くか、レンダリング環境に近い高速なストレージに置くのが本筋です。- レンダリング時のリモートソースにCORS設定は不要です。 フレーム抽出器はブラウザの外でファイルを取得するためです。CORSヘッダーが必須のHTML5ベース再生に対する、地味ながら大きな利点です。
タイムライン上でのトリムとオフセット
トリム——素材ファイルの一部分だけを使うこと——は、フレーム単位で指定する2つのpropsで行います。
trimBefore— ファイル先頭からこのフレーム数をスキップするtrimAfter— ファイルのこのフレーム位置で使用を打ち切る
一方、オフセット——クリップがコンポジションのどの時点で登場するか——は、動画を <Sequence> で囲んで決めます。この2つは独立した軸です。トリムは「ファイルのどこを再生するか」、Sequenceは「タイムラインのどこで再生するか」を制御します。
インタビュー素材の3〜8秒部分を、コンポジションの2秒地点から再生する例です。
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} // ファイル先頭の3秒をスキップ
trimAfter={8 * fps} // ファイルの8秒地点で打ち切り
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</Sequence>
</AbsoluteFill>
);
};
フレーム値は数字をハードコードせず、必ず useVideoConfig().fps から計算してください。同じコンポーネントが24fps・30fps・60fpsのどのコンポジションでもそのまま動きます。
移行メモ: 古いRemotionコードでは同じ目的に startFrom と endAt が使われています。これらはRemotion 4.0.319で trimBefore / trimAfter に置き換えられ、非推奨になりました(新旧の併用は不可)。古いチュートリアルからコードを持ってくる場合は、startFrom → trimBefore、endAt → trimAfter と読み替えてください。
トリム済みクリップを連続でつなぐマルチカット編集は、from を順にずらした <Sequence> の並びです。オフセット計算を不要にする <Series> ヘルパーを含むタイムライン構成パターンの全体像は、SequenceとSeriesのタイミング解説を参照してください。
ミュート・音量ランプ・再生速度
ミュート
b-rollが自前の音声を持ち込むべき場面はほぼありません。muted propでクリップの音声トラックを丸ごと落とします。
<OffthreadVideo src={staticFile('b-roll/workshop.mp4')} muted />
ミュートは見た目だけの話ではありません。ミュートされたクリップの音声はレンダリング時の音声抽出から除外されるため、オーバーレイ素材に muted を徹底することでレンダリングが軽くなり、環境音がミックスに紛れ込む事故も防げます。
音量と音量ランプ
volume propは0〜1の固定値のほか、フレーム番号(クリップ再生開始からの相対値)を受け取って音量を返すコールバックも受け付けます。コールバック形式なら、フレーム精度のフェードやダッキングが書けます。
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',
})
}
/>
);
};
最初の1秒でインタビュー音声をフェードインさせています。同じパターンでダッキングも実装できます。ナレーションが流れるフレーム範囲で音量を0.2まで下げ、終わったら戻すだけです。この volume APIはRemotionのオーディオコンポーネントとまったく同じもので、Audioコンポーネント解説で紹介しているパターンがそのまま動画音声にも使えます。
再生速度
playbackRate でクリップを高速・低速再生できます。
<OffthreadVideo src={staticFile('drone-shot.mp4')} playbackRate={0.5} /> {/* 半速 */}
<OffthreadVideo src={staticFile('screen-capture.mp4')} playbackRate={2} /> {/* 倍速 */}
注意点は2つ。逆再生(playbackRate={-1})はサポートされていないため、必要ならFFmpegで事前にファイルを反転させます。また、0.5倍で再生するクリップはソースフレームの消費も半分になるため、10秒のファイルはタイムライン上で20秒を埋めます。
ループ
<Html5Video> と違い、<OffthreadVideo> には loop propがありません。短い背景クリップを長いコンポジションの間ループさせるには <Loop> で囲みます。
import { Loop, OffthreadVideo, staticFile, useVideoConfig } from 'remotion';
export const LoopingBackground: React.FC = () => {
const { fps } = useVideoConfig();
return (
<Loop durationInFrames={6 * fps}> {/* ソースクリップの長さが6秒 */}
<OffthreadVideo src={staticFile('ambient-loop.mp4')} muted />
</Loop>
);
};
b-rollのレイヤー合成 — オーバーレイ・PiP・分割画面
ここがRemotionのReactモデルの真価が出るところです。映像のレイヤー合成は、<AbsoluteFill> の中に子要素を積むだけ——DOMのz-orderと同じで、後の子が上に描画されます。
定番のb-rollカットアウェイ
ドキュメンタリーの標準パターンです。インタビュー(a-roll)は最初から最後まで流れ続けて音声をすべて担い、b-rollは数秒間だけ映像を覆います。その間もインタビュー音声は下で流れ続けます。
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: コンポジション全体を通して再生・音声を担当 */}
<OffthreadVideo
src={staticFile('interview.mp4')}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
{/* b-roll: 4秒〜9秒の間だけ映像を覆う・ミュート */}
<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>
);
};
a-rollは一度もアンマウントされないため、音声はカットアウェイの間も途切れず流れます。b-rollは映像の上に「絵だけを重ねる」わけです。従来のノンリニア編集ソフトで編集者が期待するb-roll合成の挙動が、30行ほどのTypeScriptで表現できます。
ピクチャーインピクチャー
画面収録の上に載せるWebカメラの小窓は、角丸の position: absolute コンテナ + スプリングの登場アニメーションです。
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' }}>
{/* ベースレイヤー: 画面収録・ミュート */}
<OffthreadVideo
src={staticFile('screen-recording.mp4')}
muted
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
{/* PiPレイヤー: Webカメラクリップがナレーション音声を担当 */}
<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>
);
};
borderRadius で映像を切り抜くには、コンテナ側の overflow: 'hidden' が必須です。小窓の位置・サイズ・登場の物理演算はすべて、propsとしてパラメータ化する一歩手前まで来ています。
分割画面
<AbsoluteFill> はflexコンテナなので、2画面比較レイアウトは素の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>
3分割、2×2のグリッド、分割位置のアニメーション(flex 値や幅をフレームで interpolate する)も、すべて同じ考え方の延長です。
AbsoluteFill内での映像のサイズ調整とフィット
素材のアスペクト比がコンポジションと一致することは稀です。16:9のソースを9:16の縦型コンポジションに入れるのは日常茶飯事です。解決策は標準CSSの object-fit を動画の style に指定することです。
objectFit: 'cover'— コンテナを完全に埋め、はみ出しをクロップ。背景や全面b-rollの正解デフォルトobjectFit: 'contain'— フレーム全体を表示し、上下左右に余白(レターボックス)を追加。クロップするとUIが切れる画面収録に最適objectFitなしで明示サイズ指定 — 引き伸ばされて歪む。ほぼ常に不正解
{/* 16:9のクリップを1080×1920の縦型コンポジションに中央クロップで配置 */}
<OffthreadVideo
src={staticFile('landscape-clip.mp4')}
muted
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center 30%', // クロップをフレーム上部寄りに
}}
/>
objectPosition は「クロップ後にフレームのどこを残すか」を制御します。被写体が中央にいない素材で有効です。さりげないKen Burns効果がほしければ、overflow: 'hidden' のコンテナで囲み、動画の scale をシーン全体で1から1.08へアニメーションさせます。
<AbsoluteFill> は見た目以上の仕事をしています。コンポジション全体に広がる position: absolute のflexコンテナだからこそ、積んだレイヤーが座標指定なしでぴったり揃うのです。そのレイアウト挙動と通常のdivとの使い分けは、AbsoluteFillレイアウト解説で詳しく扱っています。
コーデックの罠と「デコードできない」エラー
<OffthreadVideo> はH.264、H.265/HEVC、VP8、VP9、AV1、ProResに対応しています。大半のファイルはカバーされますが、「大半」は「すべて」ではなく、デコードエラーは動画埋め込みまわりで最も多いトラブルです。典型的な原因は次の通りです。
- 特殊なピクセルフォーマット。 一部のカメラや画面収録ソフトは10bitや4:4:4クロマのファイルを出力し、デコーダが拒否したり描画が崩れたりします。
- 可変フレームレート(VFR)素材。 スマホの画面収録やOBSキャプチャはVFRであることが多く、フレーム単位のサンプリング時にシークのズレや音ズレを起こします。
- コンテナメタデータの破損。 書き込み途中で切れたファイル(moov atom欠落)や特殊なmuxerの出力は、VLCでは再生できてもフレーム抽出で失敗することがあります。
万能の対処法は、public/ に入れる前にFFmpegでファイルを正規化することです。
ffmpeg -i input.mov \
-c:v libx264 -pix_fmt yuv420p -r 30 \
-c:a aac \
-movflags +faststart \
output.mp4
これで、広くサポートされる yuv420p ピクセルフォーマットのH.264・固定30fps・AAC音声に再エンコードされます。10秒の前処理で、レンダリング失敗の一大カテゴリが丸ごと消えます。
あと2つ、知っておくと役立つ防御策があります。
onError— ソースの失敗を捕捉し、レンダリングをクラッシュさせる代わりにフォールバックを描画できます。transparent— アルファチャンネル付き素材(ProRes 4444、アルファ付きVP9)の透過抽出を有効にします。フレームがBMPではなくPNGで抽出されるため遅くなります。透過が本当に必要なときだけ有効にしてください。
リモートクリップがデコードエラーではなくタイムアウトで失敗する場合は、前述の通り <OffthreadVideo> がファイル全体をダウンロードする仕様を思い出してください。素材をローカルに移すか、delayRenderTimeoutInMilliseconds を延ばします。
ドロップインで使えるb-roll・モーション素材パック
ここまでの内容は「素材がすでに手元にある」前提でした。しかし実際には、素材レイヤーこそがボトルネックになりがちです。ここにアニメーション背景がほしい、あそこにモーションのアクセントがほしい、タイトルの後ろにテクスチャループを敷きたい——そのたびに撮影や素材探しで編集の手が止まります。
Remotionのワークフローで事前制作されたモーション素材が活きるのはまさにここです。クリップは結局「src を持つ <OffthreadVideo>」にすぎないため、正しくエンコードされたMP4や透過WebMなら、この記事のレイヤーパターンにそのまま落とし込めます。<Loop> でループさせ、ミュートし、トリムし、生成グラフィックスの上や下に積むだけです。
RenderCompのテンプレートライブラリには、まさにこの用途のために作られたモーションオブジェクト・背景素材パックが含まれています。色やコーデックの事故なしにRemotionプロジェクトへ合成できる、クリーンでループ可能なクリップとアニメーション要素です。型付きpropsを備えたフルテンプレートのコンポジションも揃っています。空の <AbsoluteFill> からではなく動く編集から始めたい方は、rendercomp.com のコレクションをご覧ください。
よくある質問(FAQ)
Q: 埋め込んだ動画がレンダリング出力でチラつく・フレームが重複するのはなぜですか?
ほぼ間違いなく、HTML5ベースの動画コンポーネントでレンダリングしています。ブラウザの <video> のシークは、レンダリング負荷の下ではフレーム精度が保証されません。<OffthreadVideo>(または @remotion/media の <Video>)に切り替えれば、フレームが再生中のvideo要素からではなくファイルから直接抽出されるため、チラつきは消えます。
Q: trimBefore と trimAfter の単位はフレームですか、秒ですか?
フレームです。秒に useVideoConfig().fps を掛けてください。例えば trimBefore={3 * fps} で先頭3秒をスキップします。旧propsの startFrom / endAt(同じくフレーム単位)はRemotion 4.0.319で非推奨になった旧エイリアスです。
Q: クリップを逆再生できますか?
playbackRate に負の値を渡す方法はサポートされていません。事前にファイルを処理してください: ffmpeg -i input.mp4 -vf reverse -af areverse reversed.mp4 で反転したファイルを通常通り埋め込みます。
Q: 短いクリップをコンポジション全体でループさせるには?
<OffthreadVideo> に loop propはありません。コアパッケージの <Loop durationInFrames={クリップのフレーム数}> で囲みます。durationInFrames をソースクリップの長さに合わせると、各周回が最後まで再生されてからリスタートします。
Q: <OffthreadVideo> はRemotion Playerでも動きますか?
動きます。<Player> とRemotion Studioでは通常のHTML5 video要素として描画され、リアルタイムで滑らかにプレビューできます。オフスレッドのフレーム抽出は実際のレンダリング時のみ行われます。開発中はインタラクティブな再生、本番はフレーム精度の出力——同じコンポーネントで両立します。
Q: コンポジションの長さを動画ファイルとぴったり同じにするには?
<Composition> の calculateMetadata でファイルのdurationを非同期に読み取り、そこから計算した durationInFrames を返します。ファイルを差し替えてもタイムラインの長さが自動で追従します。詳しくはcalculateMetadata解説を参照してください。
Q: 0.5倍速にするとクリップの音声がおかしく聞こえます。どうすればいいですか?
音声を遅くすれば当然引き伸ばされて聞こえます。b-rollであれば答えはシンプルに muted です。スローモーション素材に現場音が必要な場面はほぼありません。音声が重要な場合は playbackRate を使わず、FFmpegで適切なタイムストレッチをかけたスローモーション版を事前に作ってから埋め込みます。
まとめ
Remotionへの動画素材の埋め込みは、少数の確実なパターンに集約されます。
- レンダリング出力には
<OffthreadVideo>(または@remotion/mediaの<Video>)を使う。素のHTML5 video要素は使わない - ローカルファイルは
public/に置いてstaticFile()で読み込む。大きな素材はリモートよりローカルに - トリムは
trimBefore/trimAfter(フレーム単位)、タイムライン上の配置は<Sequence from={...}> - b-rollはミュートし、音量は
(frame) => numberコールバックでランプさせ、速度はplaybackRateで調整する - レイヤー合成は
<AbsoluteFill>に積むだけ。カットアウェイもPiPも分割画面も、絶対配置のReact要素にすぎない - アスペクト比の不一致は
objectFit: 'cover'/'contain'で解決し、問題のあるファイルはFFmpegで再エンコードして正規化する
白紙から始める必要はありません — 本番品質のRemotionテンプレートとモーション素材パックは RenderComp で公開中 →
すべてのテンプレートは編集可能なTypeScriptソースと型付きpropsを同梱。トリム済みクリップ、b-rollレイヤー、ピクチャーインピクチャーのスロットが、初日からあなたの素材を受け入れる状態で届きます。