Remotion LabRemotion Lab
建構應用驗證使用者上傳的影片

驗證使用者上傳的影片

學習如何在上傳前驗證使用者影片的瀏覽器相容性,並處理不相容格式

在建構接受使用者影片上傳的應用程式時,你可能想要在上傳之前驗證瀏覽器是否可以播放該影片。

檢查影片相容性

使用 canDecode() 函數來檢查影片是否可以被瀏覽器解碼。

canDecode() 函數會檢查影片是否可以在瀏覽器中播放,以及是否可以載入到 @remotion/media 中的 <Video>

注意:不同的 Remotion 影片組件具有不同的格式相容性:

  • <Video>:基於 Mediabunny 的自定義影片標籤,支援最重要的影片格式。
  • <Html5Video>:使用瀏覽器的原生解碼,基於 <video> 元素。
  • <OffthreadVideo>:在渲染時支援更多格式,在 Player 和 Studio 中預覽時使用 <Html5Video>

了解更多支援的媒體格式:影片格式

注意:在 @remotion/media 中,H.265 編碼格式的影片在普通瀏覽器中受到支援,但在渲染期間會回退到 <OffthreadVideo>

React 中的驗證範例

注意:這是一個顯示驗證流程的簡化範例。在實際應用程式中,實作方式會根據你的上傳策略(直接上傳、預簽名 URL、分段上傳等)以及儲存影片的位置(S3、GCS、你自己的伺服器等)而有所不同。

ValidatedUploader.tsx
import { canDecode } from '@remotion/media';
 
const upload = async (file: File): Promise<string> => {
  // 你的上傳實作
  return 'https://example.com/video.mp4';
};
 
const handleUpload = async (file: File) => {
  // 先檢查瀏覽器是否可以解碼此影片
  const isCompatible = await canDecode(file);
 
  if (!isCompatible) {
    // 通知使用者或拒絕影片
    alert('不支援此影片格式。');
    return;
  }
 
  try {
    const url = await upload(file);
    console.log('影片上傳成功:', url);
  } catch (error) {
    console.error('處理影片失敗:', error);
    alert('影片上傳失敗');
  }
};

完整的 React 組件範例

以下是一個完整的帶有驗證功能的上傳組件:

ValidatedVideoUploader.tsx
import React, { useCallback, useState } from 'react';
import { Player } from '@remotion/player';
import { MyComposition } from './MyComposition';
 
// 模擬 canDecode 函數(實際使用 @remotion/media 中的版本)
const canDecode = async (src: string | Blob): Promise<boolean> => {
  return new Promise((resolve) => {
    const video = document.createElement('video');
    video.preload = 'metadata';
 
    const url = src instanceof Blob ? URL.createObjectURL(src) : src;
 
    video.onloadedmetadata = () => {
      if (src instanceof Blob) URL.revokeObjectURL(url);
      resolve(true);
    };
 
    video.onerror = () => {
      if (src instanceof Blob) URL.revokeObjectURL(url);
      resolve(false);
    };
 
    video.src = url;
  });
};
 
const upload = async (file: File): Promise<string> => {
  // 你的上傳邏輯
  return 'https://example.com/video.mp4';
};
 
type UploadStatus =
  | { type: 'idle' }
  | { type: 'validating' }
  | { type: 'error'; message: string }
  | { type: 'uploading' }
  | { type: 'done'; url: string };
 
export const ValidatedVideoUploader: React.FC = () => {
  const [status, setStatus] = useState<UploadStatus>({ type: 'idle' });
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
 
  const handleFileSelect = useCallback(
    async (event: React.ChangeEvent<HTMLInputElement>) => {
      if (!event.target.files?.length) return;
 
      const file = event.target.files[0];
 
      // 步驟 1:驗證影片格式
      setStatus({ type: 'validating' });
 
      const isCompatible = await canDecode(file);
 
      if (!isCompatible) {
        setStatus({
          type: 'error',
          message: `不支援的影片格式。請上傳 MP4、WebM 或 MOV 格式的影片。`,
        });
        return;
      }
 
      // 步驟 2:建立本地預覽(樂觀更新)
      const blobUrl = URL.createObjectURL(file);
      setPreviewUrl(blobUrl);
      setStatus({ type: 'uploading' });
 
      try {
        // 步驟 3:上傳到雲端
        const cloudUrl = await upload(file);
 
        // 步驟 4:切換到雲端 URL
        setPreviewUrl(cloudUrl);
        setStatus({ type: 'done', url: cloudUrl });
 
        // 釋放 blob URL
        URL.revokeObjectURL(blobUrl);
      } catch (error) {
        setStatus({
          type: 'error',
          message: '上傳失敗,請重試。',
        });
        URL.revokeObjectURL(blobUrl);
        setPreviewUrl(null);
      }
    },
    []
  );
 
  return (
    <div style={{ padding: '20px' }}>
      <h2>影片上傳</h2>
 
      {/* 狀態顯示 */}
      {status.type === 'validating' && (
        <div style={{ color: 'blue' }}>正在驗證影片格式...</div>
      )}
 
      {status.type === 'error' && (
        <div style={{ color: 'red', padding: '10px', background: '#fee' }}>
          {status.message}
        </div>
      )}
 
      {status.type === 'uploading' && (
        <div style={{ color: 'orange' }}>正在上傳影片...</div>
      )}
 
      {status.type === 'done' && (
        <div style={{ color: 'green' }}>影片上傳成功!</div>
      )}
 
      {/* 影片預覽 */}
      {previewUrl && (
        <div style={{ margin: '20px 0' }}>
          <Player
            component={MyComposition}
            inputProps={{ videoURL: previewUrl }}
            durationInFrames={150}
            fps={30}
            compositionWidth={1920}
            compositionHeight={1080}
            style={{ width: '100%', maxWidth: '800px' }}
            controls
          />
        </div>
      )}
 
      {/* 檔案輸入 */}
      <input
        type="file"
        accept="video/*"
        onChange={handleFileSelect}
        disabled={status.type === 'validating' || status.type === 'uploading'}
      />
 
      <p style={{ color: '#666', fontSize: '14px' }}>
        支援的格式:MP4、WebM、MOV、AVI
      </p>
    </div>
  );
};

處理不相容的影片

當影片無法被解碼時,你有兩個選擇:

選項 1:拒絕影片並要求使用者上傳不同格式

這是最簡單的方法,直接告知使用者格式不受支援:

if (!isCompatible) {
  alert('此影片格式不受支援。請嘗試以下格式:MP4 (H.264)、WebM (VP8/VP9)');
  return;
}

選項 2:在後端重新編碼影片

如果你需要支援各種格式,可以在後端重新編碼影片:

server/transcode.ts
import { execSync } from 'child_process';
import path from 'path';
 
export const transcodeVideo = async (
  inputPath: string,
  outputPath: string
): Promise<void> => {
  // 使用 FFmpeg 將影片轉換為 H.264/MP4 格式
  execSync(
    `ffmpeg -i "${inputPath}" -c:v libx264 -c:a aac -movflags +faststart "${outputPath}"`,
    { stdio: 'inherit' }
  );
};
server/upload-handler.ts
import { transcodeVideo } from './transcode';
import fs from 'fs';
import path from 'path';
 
export const handleVideoUpload = async (file: Express.Multer.File) => {
  const inputPath = file.path;
  const outputPath = path.join('uploads', `${Date.now()}.mp4`);
 
  // 嘗試轉碼
  try {
    await transcodeVideo(inputPath, outputPath);
 
    // 刪除原始上傳的檔案
    fs.unlinkSync(inputPath);
 
    return {
      success: true,
      url: `/uploads/${path.basename(outputPath)}`,
    };
  } catch (error) {
    fs.unlinkSync(inputPath);
    throw new Error('影片轉碼失敗');
  }
};

支援的影片格式

以下是不同組件的格式支援情況:

格式<Video><Html5Video><OffthreadVideo> (渲染)
MP4 (H.264)
MP4 (H.265)是*瀏覽器相依
WebM (VP8/VP9)
WebM (AV1)瀏覽器相依
MOV瀏覽器相依
GIF-

*H.265 在渲染時回退到 <OffthreadVideo>

另請參閱