Remotion LabRemotion Lab
建構應用購買預建影片編輯器

購買預建影片編輯器

了解可購買的預建 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
從頭開始建構自定義編輯器自行建構(參見下方指南)

自行建構的起點

如果你想自行建構影片編輯器,以下是一些有用的資源和起始程式碼:

基本編輯器結構

Editor.tsx(基本結構)
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>
  );
};

另請參閱