Remotion LabRemotion Lab
影片影片操作與 Canvas 繪圖

影片操作與 Canvas 繪圖

學習如何在 Remotion 中使用 onVideoFrame 回呼將影片幀繪製到 Canvas,並套用灰階濾鏡、綠幕去背等影像處理效果

影片操作與 Canvas 繪圖

Remotion 支援將 <OffthreadVideo><Video><Html5Video> 的每一幀繪製到 <canvas> 元素上,讓你可以透過 Canvas 2D API 對影片進行逐幀的像素級影像處理。

運作原理

透過 onVideoFrame 回呼 prop,你可以在每一幀渲染時取得該幀的圖像資料(以 CanvasImageSource 形式),再使用 drawImage() API 將其繪製到 Canvas 上,並在繪製過程中套用任何 Canvas 支援的濾鏡或像素操作。

瀏覽器支援(預覽模式):

  • Chrome 83+
  • Safari 15.4+
  • Firefox 130+(2024 年 8 月)

在預覽模式下,Remotion 使用 requestVideoFrameCallback() API 來捕捉影片幀。

基礎範例:套用灰階濾鏡

以下範例將影片渲染後隱藏,並將每一幀繪製到 Canvas 上,同時套用 CSS 灰階濾鏡:

import React, {useCallback, useRef} from 'react';
import {
  AbsoluteFill,
  OffthreadVideo,
  useVideoConfig,
} from 'remotion';
 
export const VideoOnCanvas: React.FC = () => {
  const video = useRef<HTMLVideoElement>(null);
  const canvas = useRef<HTMLCanvasElement>(null);
  const {width, height} = useVideoConfig();
 
  // 處理每一幀
  const onVideoFrame = useCallback(
    (frame: CanvasImageSource) => {
      if (!canvas.current) {
        return;
      }
      const context = canvas.current.getContext('2d');
 
      if (!context) {
        return;
      }
 
      // 套用灰階濾鏡後繪製影格
      context.filter = 'grayscale(100%)';
      context.drawImage(frame, 0, 0, width, height);
    },
    [height, width],
  );
 
  return (
    <AbsoluteFill>
      <AbsoluteFill>
        <OffthreadVideo
          // 隱藏原始影片標籤
          style={{opacity: 0}}
          onVideoFrame={onVideoFrame}
          src="https://remotion.media/BigBuckBunny.mp4"
        />
      </AbsoluteFill>
      <AbsoluteFill>
        <canvas ref={canvas} width={width} height={height} />
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

程式碼說明

  • 原始的 <OffthreadVideo> 設為 opacity: 0,讓使用者看不到原始影片,只看到 Canvas 上處理過的結果
  • onVideoFrame 回呼在每一幀都會被觸發,傳入的 frame 參數是當前幀的 CanvasImageSource
  • context.filter = 'grayscale(100%)' 在繪製前套用灰階效果
  • Canvas 的尺寸設為與合成相同(widthheight 來自 useVideoConfig()

進階範例:綠幕去背

以下範例示範如何遍歷每個像素,將綠色像素的 Alpha 通道設為透明,實現綠幕去背效果:

import React, {useCallback, useRef} from 'react';
import {
  AbsoluteFill,
  OffthreadVideo,
  useVideoConfig,
} from 'remotion';
 
export const Greenscreen: React.FC<{
  opacity: number;
}> = ({opacity}) => {
  const canvas = useRef<HTMLCanvasElement>(null);
  const {width, height} = useVideoConfig();
 
  // 處理每一幀:將綠色像素透明化
  const onVideoFrame = useCallback(
    (frame: CanvasImageSource) => {
      if (!canvas.current) {
        return;
      }
      const context = canvas.current.getContext('2d');
 
      if (!context) {
        return;
      }
 
      // 先繪製原始幀
      context.drawImage(frame, 0, 0, width, height);
 
      // 取得像素資料
      const imageFrame = context.getImageData(0, 0, width, height);
      const {length} = imageFrame.data;
 
      // 遍歷每個像素(每個像素佔 4 個陣列元素:R、G、B、A)
      for (let i = 0; i < length; i += 4) {
        const red = imageFrame.data[i + 0];
        const green = imageFrame.data[i + 1];
        const blue = imageFrame.data[i + 2];
 
        // 若像素為綠色(G 高、R 低、B 低),則降低 alpha 值
        if (green > 100 && red < 100 && blue < 100) {
          imageFrame.data[i + 3] = opacity * 255;
        }
      }
 
      // 將處理後的像素寫回 Canvas
      context.putImageData(imageFrame, 0, 0);
    },
    [height, width, opacity],
  );
 
  return (
    <AbsoluteFill>
      <AbsoluteFill>
        <OffthreadVideo
          style={{opacity: 0}}
          onVideoFrame={onVideoFrame}
          src="https://remotion.media/greenscreen.mp4"
        />
      </AbsoluteFill>
      <AbsoluteFill>
        <canvas ref={canvas} width={width} height={height} />
      </AbsoluteFill>
    </AbsoluteFill>
  );
};

綠幕範例說明

  • getImageData() 取得整個 Canvas 的像素資料,每個像素由 4 個數值(RGBA)組成
  • 條件 green > 100 && red < 100 && blue < 100 用於判斷「夠綠」的像素
  • 將符合條件的像素的 Alpha 值(data[i + 3])設為 opacity * 255opacity 為 0 代表完全透明,1 代表不透明
  • putImageData() 將修改後的像素資料寫回 Canvas

其他可套用的 Canvas 效果

透過 Canvas 2D API,你可以實現各種影像處理效果:

效果Canvas API
灰階context.filter = 'grayscale(100%)'
模糊context.filter = 'blur(4px)'
亮度調整context.filter = 'brightness(150%)'
色調旋轉context.filter = 'hue-rotate(90deg)'
反色context.filter = 'invert(100%)'
自定義像素處理getImageData() + putImageData()

v4.0.190 之前的做法

在 v4.0.190 之前,<OffthreadVideo><Html5Video> 尚不支援 onVideoFrame prop。

當時只能對 <Html5Video> 使用原生的 requestVideoFrameCallback API:

// 舊版做法(v4.0.190 之前)
import {Html5Video} from 'remotion';
 
// 使用 ref 取得 HTMLVideoElement
// 再透過 video.requestVideoFrameCallback() 手動處理

建議升級到 v4.0.190 或更新版本以使用更簡潔的 onVideoFrame API。

效能注意事項

  • getImageData()putImageData() 是逐像素操作,效能開銷較大,對大解析度影片(如 4K)可能造成渲染速度下降
  • 若只需套用 CSS 濾鏡效果,建議直接使用 context.filter,效能優於手動像素遍歷
  • 渲染時(非預覽)Remotion 是逐幀獨立處理,因此即使效能較慢,最終輸出仍是正確的

延伸閱讀