Skip to main content

显示字幕

🌐 Displaying captions

本指南解释了如何在 Remotion 中显示字幕,假设你已经拥有 Caption 格式的字幕——有关如何生成字幕,请参见 转录音频

🌐 This guide explains how to display captions in Remotion, assuming you already have captions in the Caption format - see Transcribing audio for how to generate them.

正在获取字幕

🌐 Fetching captions

首先,获取你的字幕 JSON 文件。使用 useDelayRender() 来保持渲染,直到字幕加载完成:

🌐 First, fetch your captions JSON file. Use useDelayRender() to hold the render until the captions are loaded:

Fetching captions
import {useState, useEffect, useCallback} from 'react'; import {AbsoluteFill, staticFile, useDelayRender} from 'remotion'; import type {Caption} from '@remotion/captions'; export const MyComponent: React.FC = () => { const [captions, setCaptions] = useState<Caption[] | null>(null); const {delayRender, continueRender, cancelRender} = useDelayRender(); const [handle] = useState(() => delayRender()); const fetchCaptions = useCallback(async () => { try { const response = await fetch(staticFile('captions.json')); const data = await response.json(); setCaptions(data); continueRender(handle); } catch (e) { cancelRender(e); } }, [continueRender, cancelRender, handle]); useEffect(() => { fetchCaptions(); }, [fetchCaptions]); if (!captions) { return null; } return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>; };

创建页面

🌐 Creating pages

使用 createTikTokStyleCaptions() 将标题分组到页面中。combineTokensWithinMilliseconds 选项控制一次显示多少个单词:

🌐 Use createTikTokStyleCaptions() to group captions into pages. The combineTokensWithinMilliseconds option controls how many words appear at once:

Creating caption pages
const {pages} = useMemo(() => { return createTikTokStyleCaptions({ captions, combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS, }); }, [captions]);

使用序列进行渲染

🌐 Rendering with Sequences

遍历页面并在 <Sequence> 中渲染每一个。根据页面时间计算起始帧和持续时间:

🌐 Map over the pages and render each one in a <Sequence>. Calculate the start frame and duration from the page timing:

Rendering caption pages
const CaptionedContent: React.FC = () => { const {fps} = useVideoConfig(); return ( <AbsoluteFill> {pages.map((page, index) => { const nextPage = pages[index + 1] ?? null; const startFrame = (page.startMs / 1000) * fps; const endFrame = Math.min(nextPage ? (nextPage.startMs / 1000) * fps : Infinity, startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps); const durationInFrames = endFrame - startFrame; if (durationInFrames <= 0) { return null; } return ( <Sequence key={index} from={startFrame} durationInFrames={durationInFrames}> <CaptionPage page={page} /> </Sequence> ); })} </AbsoluteFill> ); };

呈现标题页

🌐 Rendering a caption page

标题页包含 tokens,你可以使用它来高亮当前正在说的单词。以下是一个在单词被说出时高亮的示例:

🌐 A caption page contains tokens which you can use to highlight the currently spoken word. Here's an example that highlights words as they are spoken:

Rendering a caption page with word highlighting
import {AbsoluteFill, useCurrentFrame, useVideoConfig} from 'remotion'; import type {TikTokPage} from '@remotion/captions'; const HIGHLIGHT_COLOR = '#39E508'; const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => { const frame = useCurrentFrame(); const {fps} = useVideoConfig(); // Current time relative to the start of the sequence const currentTimeMs = (frame / fps) * 1000; // Convert to absolute time by adding the page start const absoluteTimeMs = page.startMs + currentTimeMs; return ( <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', }} > <div style={{ fontSize: 80, fontWeight: 'bold', textAlign: 'center', // Preserve whitespace in captions whiteSpace: 'pre', }} > {page.tokens.map((token) => { const isActive = token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs; return ( <span key={token.fromMs} style={{ color: isActive ? HIGHLIGHT_COLOR : 'white', }} > {token.text} </span> ); })} </div> </AbsoluteFill> ); };

完整示例

🌐 Full example

显示完整示例
Full captioned video example
import {useState, useEffect, useCallback, useMemo} from 'react'; import {AbsoluteFill, Sequence, staticFile, useCurrentFrame, useDelayRender, useVideoConfig} from 'remotion'; import {createTikTokStyleCaptions} from '@remotion/captions'; import type {Caption, TikTokPage} from '@remotion/captions'; const SWITCH_CAPTIONS_EVERY_MS = 1200; const HIGHLIGHT_COLOR = '#39E508'; const CaptionPage: React.FC<{page: TikTokPage}> = ({page}) => { const frame = useCurrentFrame(); const {fps} = useVideoConfig(); const currentTimeMs = (frame / fps) * 1000; const absoluteTimeMs = page.startMs + currentTimeMs; return ( <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', }} > <div style={{ fontSize: 80, fontWeight: 'bold', textAlign: 'center', whiteSpace: 'pre', }} > {page.tokens.map((token) => { const isActive = token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs; return ( <span key={token.fromMs} style={{ color: isActive ? HIGHLIGHT_COLOR : 'white', }} > {token.text} </span> ); })} </div> </AbsoluteFill> ); }; export const CaptionedVideo: React.FC = () => { const [captions, setCaptions] = useState<Caption[] | null>(null); const {delayRender, continueRender, cancelRender} = useDelayRender(); const [handle] = useState(() => delayRender()); const {fps} = useVideoConfig(); const fetchCaptions = useCallback(async () => { try { const response = await fetch(staticFile('captions.json')); const data = await response.json(); setCaptions(data); continueRender(handle); } catch (e) { cancelRender(e); } }, [continueRender, cancelRender, handle]); useEffect(() => { fetchCaptions(); }, [fetchCaptions]); const {pages} = useMemo(() => { return createTikTokStyleCaptions({ captions: captions ?? [], combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS, }); }, [captions]); return ( <AbsoluteFill style={{backgroundColor: 'black'}}> {pages.map((page, index) => { const nextPage = pages[index + 1] ?? null; const startFrame = (page.startMs / 1000) * fps; const endFrame = Math.min(nextPage ? (nextPage.startMs / 1000) * fps : Infinity, startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps); const durationInFrames = endFrame - startFrame; if (durationInFrames <= 0) { return null; } return ( <Sequence key={index} from={startFrame} durationInFrames={durationInFrames}> <CaptionPage page={page} /> </Sequence> ); })} </AbsoluteFill> ); };

下一步

🌐 Next steps

你可以自定义字幕的外观:

🌐 You can customize the appearance of your captions:

  • 使用 fitText()@remotion/layout-utils 自动调整文本以适应视频宽度
  • 添加动画 用于进入/退出效果
  • 应用 CSS 文本描边以提高可见性:
<div
  style={{
    WebkitTextStroke: '4px black',
    paintOrder: 'stroke',
  }}
>
  {text}
</div>

另请参阅

🌐 See also