影片操作與 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參數是當前幀的CanvasImageSourcecontext.filter = 'grayscale(100%)'在繪製前套用灰階效果- Canvas 的尺寸設為與合成相同(
width和height來自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 * 255,opacity為 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 是逐幀獨立處理,因此即使效能較慢,最終輸出仍是正確的