購買預建影片編輯器
了解可購買的預建 Remotion 影片編輯器範本和組件,快速啟動你的影片編輯應用程式
如果你想快速建構一個影片編輯器應用程式,你可以購買預建的編輯器範本和組件,而不是從頭開始建構所有內容。以下是可用的選項:
Editor Starter(Remotion 官方)
由 Remotion 官方提供的範本,提供全面的影片編輯器基礎。
這是由 Remotion 團隊建構的編輯器,包含我們推薦的最佳實踐。它的功能包括:
- 可縮放的時間軸,支援拖放、多選、縮略圖和波形
- 互動式畫布,支援移動和調整圖層大小以及內聯文字編輯
- 將資源上傳到 S3 和本地快取
- 豐富的字型支援,包含 Google Fonts
- 使用 OpenAI Whisper 生成字幕
- 使用 Remotion Lambda 渲染影片
更多詳情請訪問:remotion.dev/editor-starter
Timeline 組件(Remotion 官方)
一個最簡化的時間軸介面組件,遵循 Remotion 的所有最佳實踐和建議。
功能:
- 播放頭
- 可調整大小、可移動的圖層
- 建立和移動軌道
- 影片縮略圖膠片條
如果你只需要時間軸功能,並想在其基礎上建構其他編輯器功能,則推薦使用此組件。
更多詳情請訪問:remotion.pro/timeline
React Video Editor(第三方)
最受歡迎的 Remotion 影片編輯器範本,被許多公司使用,功能豐富:
- 時間軸控制
- 全面的影片編輯功能
- 過渡效果和特效
- 字幕
- 關鍵幀動畫
- 匯出功能
這是一個成熟的解決方案,提供廣泛的影片編輯功能,已被許多開發者在生產環境中驗證。
DesignCombo(第三方)
另一個已被生態系統中各種產品使用的第三方選項。
功能:
- 文字、影片、音頻、圖片圖層
- 各種過渡效果
- AI 語音和字幕
- 匯出功能
如何選擇適合的解決方案
根據你的需求選擇合適的方案:
| 需求 | 推薦方案 |
|---|---|
| 完整功能的影片編輯器,快速啟動 | Editor Starter |
| 只需要時間軸 UI 組件 | Timeline 組件 |
| 需要豐富功能且有社群支援 | React Video Editor |
| 需要 AI 功能(語音、字幕) | DesignCombo |
| 從頭開始建構自定義編輯器 | 自行建構(參見下方指南) |
自行建構的起點
如果你想自行建構影片編輯器,以下是一些有用的資源和起始程式碼:
基本編輯器結構
import React, { useMemo, useState } from 'react';
import { Player } from '@remotion/player';
// 定義影片元素類型
type BaseItem = {
id: string;
from: number;
durationInFrames: number;
};
type TextItem = BaseItem & {
type: 'text';
text: string;
color: string;
fontSize: number;
};
type VideoItem = BaseItem & {
type: 'video';
src: string;
};
type AudioItem = BaseItem & {
type: 'audio';
src: string;
volume: number;
};
type Item = TextItem | VideoItem | AudioItem;
type Track = {
id: string;
name: string;
items: Item[];
};
// 主組件(Remotion 影片內容)
const Main: React.FC<{ tracks: Track[] }> = ({ tracks }) => {
// ... 渲染邏輯
return null;
};
// 編輯器組件
export const VideoEditor: React.FC = () => {
const [tracks, setTracks] = useState<Track[]>([
{ id: '1', name: '影片軌道', items: [] },
{ id: '2', name: '文字軌道', items: [] },
{ id: '3', name: '音樂軌道', items: [] },
]);
const [currentFrame, setCurrentFrame] = useState(0);
const FPS = 30;
const DURATION_SECS = 60;
const TOTAL_FRAMES = DURATION_SECS * FPS;
const inputProps = useMemo(() => ({ tracks }), [tracks]);
// 新增文字元素
const addText = () => {
const newItem: TextItem = {
id: crypto.randomUUID(),
type: 'text',
text: '新文字',
color: '#ffffff',
fontSize: 48,
from: currentFrame,
durationInFrames: 3 * FPS,
};
setTracks((prev) =>
prev.map((track) =>
track.name === '文字軌道'
? { ...track, items: [...track.items, newItem] }
: track
)
);
};
// 刪除元素
const deleteItem = (trackId: string, itemId: string) => {
setTracks((prev) =>
prev.map((track) =>
track.id === trackId
? { ...track, items: track.items.filter((item) => item.id !== itemId) }
: track
)
);
};
// 移動元素
const moveItem = (trackId: string, itemId: string, newFrom: number) => {
setTracks((prev) =>
prev.map((track) =>
track.id === trackId
? {
...track,
items: track.items.map((item) =>
item.id === itemId ? { ...item, from: newFrom } : item
),
}
: track
)
);
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#1a1a1a', color: 'white' }}>
{/* 頂部工具列 */}
<div style={{ padding: '8px', background: '#2a2a2a', display: 'flex', gap: '8px' }}>
<button onClick={addText}>新增文字</button>
<button>新增影片</button>
<button>新增音訊</button>
</div>
{/* 預覽區域 */}
<div style={{ flex: '0 0 auto', padding: '16px' }}>
<Player
component={Main}
inputProps={inputProps}
durationInFrames={TOTAL_FRAMES}
fps={FPS}
compositionWidth={1920}
compositionHeight={1080}
style={{ width: '100%', maxWidth: '800px' }}
controls
/>
</div>
{/* 時間軸區域 */}
<div style={{ flex: 1, overflowX: 'auto', overflowY: 'auto', padding: '16px' }}>
{tracks.map((track) => (
<div key={track.id} style={{ marginBottom: '8px' }}>
<div style={{ background: '#2a2a2a', padding: '4px 8px', fontSize: '12px' }}>
{track.name}
</div>
<div style={{ position: 'relative', height: '40px', background: '#1a1a1a', minWidth: '600px' }}>
{track.items.map((item) => (
<div
key={item.id}
style={{
position: 'absolute',
left: item.from * 2,
width: item.durationInFrames * 2,
height: '36px',
top: '2px',
background: item.type === 'text' ? '#4CAF50' :
item.type === 'video' ? '#2196F3' : '#FF9800',
borderRadius: '4px',
padding: '4px 8px',
fontSize: '12px',
overflow: 'hidden',
cursor: 'pointer',
}}
onClick={() => deleteItem(track.id, item.id)}
>
{item.type === 'text' ? item.text : item.type}
</div>
))}
</div>
</div>
))}
</div>
</div>
);
};