basic-captions.tsx
import type {Caption} from '@remotion/captions';
import {createTikTokStyleCaptions} from '@remotion/captions';
import React, {useMemo} from 'react';
import {
Interactive,
useCurrentFrame,
useVideoConfig,
type InteractiveTransformProps,
type InteractivitySchema,
type SequenceProps,
} from 'remotion';
type BasicCaptionsProps = InteractiveTransformProps &
Pick<SequenceProps, 'width'> & {
readonly captions: Caption[];
readonly combineTokensWithinMilliseconds?: number;
};
const defaultCombineTokensWithinMilliseconds = 2000;
const defaultWidth = 900;
const basicCaptionsSchema = {
...Interactive.captionsSchema,
width: {
type: 'number',
min: 1,
step: 1,
default: undefined,
description: 'Caption area width',
hiddenFromList: false,
},
combineTokensWithinMilliseconds: {
type: 'number',
min: 0,
step: 50,
default: defaultCombineTokensWithinMilliseconds,
description: 'Time between caption pages',
hiddenFromList: false,
},
} as const satisfies InteractivitySchema;
const BasicCaptionsContent: React.FC<BasicCaptionsProps> = ({
captions,
combineTokensWithinMilliseconds = defaultCombineTokensWithinMilliseconds,
style,
width = defaultWidth,
}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const pages = useMemo(
() =>
createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds,
}).pages,
[captions, combineTokensWithinMilliseconds],
);
const currentTimeMs = (frame / fps) * 1000;
const page = pages.find(
(candidate) =>
currentTimeMs >= candidate.startMs &&
currentTimeMs < candidate.startMs + candidate.durationMs,
);
return (
<div
style={{
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
width,
...style,
}}
>
{page ? (
<div
style={{
backgroundColor: 'rgba(64, 64, 64, 0.75)',
color: '#ffffff',
fontFamily: 'Arial, Helvetica, sans-serif',
fontSize: 64,
fontWeight: 400,
lineHeight: 1.2,
padding: '14px 22px',
textAlign: 'center',
textWrap: 'balance',
whiteSpace: 'pre-wrap',
}}
>
{page.text.trim()}
</div>
) : null}
</div>
);
};
export const BasicCaptions = Interactive.withSchema({
Component: BasicCaptionsContent,
componentName: '<BasicCaptions>',
schema: basicCaptionsSchema,
wrapInSequence: true,
});