React+TypeScript中返回<Series.Sequence/>报[object Object]错误排查
问题
在Remotion的React+TypeScript项目中封装了NarratedSeriesSequence组件,内部返回<Series.Sequence/>用于根据旁白音频时长自动设置序列时长,但将该组件作为<Series/>的子组件使用时,抛出错误:
The <Series /> component only accepts a list of <Series.Sequence /> components as it's children, but got [object Object] instead
错误源于Remotion源码的子组件类型校验逻辑:
if (castedChild.type !== SeriesSequence) { throw new TypeError(`The <Series /> component only accepts a list of <Series.Sequence /> components as it's children, but got ${castedChild} instead`); }
原因
Remotion的Series组件会直接校验子组件的type是否等于内部的SeriesSequence类型(即<Series.Sequence/>的类型)。而自定义的NarratedSeriesSequence是独立的React函数组件,它的type是自定义函数,并非SeriesSequence,因此即使组件内部返回<Series.Sequence/>,外层的自定义组件依然无法通过校验。
解决方案
方案1:修改自定义组件的type属性
直接将NarratedSeriesSequence组件的type属性指向<Series.Sequence/>的type,让Remotion的校验逻辑认为它是合法的Series.Sequence组件。
修改NarratedSeriesSequence.tsx,在组件定义后添加一行代码:
import React, {useCallback, useEffect, useState} from 'react'; import {staticFile, Series, useVideoConfig} from 'remotion'; import {getAudioDurationInSeconds} from '@remotion/media-utils'; export interface NarratedSeriesSequenceProps { narrationAudioFile: string; children: JSX.Element; framePaddingBefore?: number; framePaddingAfter?: number; } export const NarratedSeriesSequence: React.FC<NarratedSeriesSequenceProps> = ({ narrationAudioFile, framePaddingBefore = 0, framePaddingAfter = 0, children, }: NarratedSeriesSequenceProps) => { const {fps} = useVideoConfig(); const [durationFrames, setDurationFrames] = useState(0); const getDurationFrames = useCallback(async () => { try { const duration = await getAudioDurationInSeconds( staticFile(narrationAudioFile) ); const durationInFrames = duration * fps + framePaddingBefore + framePaddingAfter; setDurationFrames(durationInFrames); } catch (err) { console.log(err); } }, [narrationAudioFile, framePaddingBefore, framePaddingAfter, fps]); useEffect(() => { getDurationFrames(); }, [getDurationFrames]); if (durationFrames === 0) { return null; } return ( <Series.Sequence durationInFrames={durationFrames}> {/* 可在此添加旁白音频组件 */} {children} </Series.Sequence> ); }; // 添加此行,让自定义组件通过Series的类型校验 (NarratedSeriesSequence as any).type = Series.Sequence.type;
修改后无需调整Composition.tsx的使用方式,<NarratedSeriesSequence/>可直接作为<Series/>的子组件。
方案2:直接调用组件函数而非JSX语法
跳过React组件的渲染包装,直接调用NarratedSeriesSequence函数,让它返回的<Series.Sequence/>直接作为<Series/>的子元素。
修改Composition.tsx中的使用代码:
<Series> <Series.Sequence durationInFrames={220}> <AbsoluteFill className="bg-gray-100 items-center justify-center"> <div className="m-10" /> <Logo logoColor={propThree} /> <div className="m-3" /> <Title titleText={propOne} titleColor={propTwo} /> <Subtitle /> </AbsoluteFill> </Series.Sequence> {/* 直接调用组件函数,返回Series.Sequence作为子元素 */} {NarratedSeriesSequence({ narrationAudioFile: "audio/narration/00-this-video-was.mp3", children: ( <div> This video was designed and rendered using React and Remotion. </div> ) })} {NarratedSeriesSequence({ narrationAudioFile: "audio/narration/01-remotion-gives-web-developers.mp3", children: ( <div> Remotion gives web developers and designers a set of libraries to bring familiar tools into video projects, allowing for innovative creativity that traditional video production tools handle very differently. </div> ) })} </Series>
这种方式下,<Series/>接收到的子元素就是直接的<Series.Sequence/>,自然通过校验。
方案3:预计算音频时长(构建时同步获取)
如果旁白音频是静态文件,可以在项目构建前预计算所有音频的时长,避免组件内部的异步状态,让组件同步返回<Series.Sequence/>。
- 编写预计算脚本
precompute-audio-durations.ts:
import {getAudioDurationInSeconds} from '@remotion/media-utils'; import fs from 'fs'; import path from 'path'; async function precomputeDurations() { const audioPaths = [ "audio/narration/00-this-video-was.mp3", "audio/narration/01-remotion-gives-web-developers.mp3" ]; const durationMap: Record<string, number> = {}; for (const relativePath of audioPaths) { const fullPath = path.join(process.cwd(), 'public', relativePath); const duration = await getAudioDurationInSeconds(fullPath); durationMap[relativePath] = duration; } fs.writeFileSync( path.join(process.cwd(), 'src', 'audio-durations.json'), JSON.stringify(durationMap, null, 2) ); } precomputeDurations();
- 在
package.json中添加脚本,在构建前执行:
{ "scripts": { "precompute-durations": "ts-node src/precompute-audio-durations.ts", "build": "npm run precompute-durations && remotion render" } }
- 修改
NarratedSeriesSequence.tsx,使用预计算的时长:
import React from 'react'; import {Series, useVideoConfig} from 'remotion'; import audioDurations from './audio-durations.json'; export interface NarratedSeriesSequenceProps { narrationAudioFile: string; children: JSX.Element; framePaddingBefore?: number; framePaddingAfter?: number; } export const NarratedSeriesSequence: React.FC<NarratedSeriesSequenceProps> = ({ narrationAudioFile, framePaddingBefore = 0, framePaddingAfter = 0, children, }) => { const {fps} = useVideoConfig(); const duration = audioDurations[narrationAudioFile]; if (!duration) { throw new Error(`未找到音频文件 ${narrationAudioFile} 的时长数据`); } const durationInFrames = duration * fps + framePaddingBefore + framePaddingAfter; return ( <Series.Sequence durationInFrames={durationInFrames}> {/* 可在此添加旁白音频组件 */} {children} </Series.Sequence> ); }; // 同样需要添加此行以通过类型校验,或使用方案2的调用方式 (NarratedSeriesSequence as any).type = Series.Sequence.type;
这种方式避免了组件内部的异步状态,同时保证了组件的性能和稳定性。
内容的提问来源于stack exchange,提问作者Christopher Stevens

