為 Player 建立自訂控制項
學習兩種為 Remotion Player 建立自訂控制項的方法:自訂行內控制項和 Player 外部的完整自訂控制項。
為 Player 建立自訂控制項
你可能想為 <Player> 元件實作自訂控制項。
有兩種方式可以達成:
自訂行內控制項
適用情境:
- 喜歡預設控制項,但想自訂其中一些
- 希望控制項覆蓋在 Player 上方
確保在 <Player> 中設定 controls prop。使用以下 API 來自訂各個控制項:
控制項插槽(Controls Slot)
Player 在主要播放控制項和全螢幕按鈕之間提供一個插槽,你可以使用 renderCustomControls prop 在其中渲染自訂控制項。
當你想要新增自訂按鈕或指示器,並讓其與預設控制列融合時,請使用此方法。
import { Player, PlayerRef, RenderCustomControls } from '@remotion/player';
import React, { useCallback, useEffect, useRef, useState } from 'react';
const DownloadButton: React.FC<{
playerRef: React.RefObject<PlayerRef>;
}> = ({ playerRef }) => {
const [frame, setFrame] = useState(0);
useEffect(() => {
const { current } = playerRef;
if (!current) return;
const onFrameUpdate = () => {
setFrame(current.getCurrentFrame());
};
current.addEventListener('frameupdate', onFrameUpdate);
return () => {
current.removeEventListener('frameupdate', onFrameUpdate);
};
}, [playerRef]);
return (
<button
onClick={() => console.log('在幀', frame, '下載')}
style={{
background: 'transparent',
border: 'none',
color: 'white',
cursor: 'pointer',
}}
>
下載
</button>
);
};
const MyVideo: React.FC = () => null;
export const App: React.FC = () => {
const playerRef = useRef<PlayerRef>(null);
const renderCustomControls: RenderCustomControls = useCallback(() => {
return <DownloadButton playerRef={playerRef} />;
}, []);
return (
<Player
ref={playerRef}
component={MyVideo}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
controls
renderCustomControls={renderCustomControls}
/>
);
};注意:使用
PlayerRef從自訂控制項中存取 Player 狀態和方法。下方「Player 外部控制項」章節中展示的模式在控制項插槽中同樣適用。
Player 外部的控制項
適用情境:
- 想在頁面任意位置實作自訂控制項
- 需要完全控制控制項的外觀和行為
使用以下起始範本來實作你自己的控制項。你需要以下先決條件:
- 確保
<Player>中未設定controlsprop - 取得
<Player>的PlayerRef型別的ref - 某些元件需要
durationInFrames和fpsprops,將這些值放在共用變數中供<Player>和這些元件使用 <Player>元件可選擇性接受inFrame和outFrameprops,它們與傳給<Player>的值相同(也是可選的)
播放 / 暫停按鈕
import type { PlayerRef } from '@remotion/player';
import { useCallback, useEffect, useState } from 'react';
export const PlayPauseButton: React.FC<{
playerRef: React.RefObject<PlayerRef>;
}> = ({ playerRef }) => {
const [playing, setPlaying] = useState(false);
useEffect(() => {
const { current } = playerRef;
setPlaying(current?.isPlaying() ?? false);
if (!current) return;
const onPlay = () => setPlaying(true);
const onPause = () => setPlaying(false);
current.addEventListener('play', onPlay);
current.addEventListener('pause', onPause);
return () => {
current.removeEventListener('play', onPlay);
current.removeEventListener('pause', onPause);
};
}, [playerRef]);
const onToggle = useCallback(() => {
playerRef.current?.toggle();
}, [playerRef]);
return (
<button onClick={onToggle}>
{playing ? '暫停' : '播放'}
</button>
);
};注意:此程式碼片段中未實作緩衝指示器。
時間顯示
import type { PlayerRef } from '@remotion/player';
import React, { useEffect, useState } from 'react';
export const formatTime = (frame: number, fps: number): string => {
const hours = Math.floor(frame / fps / 3600);
const remainingMinutes = frame - hours * fps * 3600;
const minutes = Math.floor(remainingMinutes / 60 / fps);
const remainingSec = frame - hours * fps * 3600 - minutes * fps * 60;
const seconds = Math.floor(remainingSec / fps);
const frameAfterSec = Math.round(frame % fps);
const hoursStr = String(hours);
const minutesStr = String(minutes).padStart(2, '0');
const secondsStr = String(seconds).padStart(2, '0');
const frameStr = String(frameAfterSec).padStart(2, '0');
if (hours > 0) {
return `${hoursStr}:${minutesStr}:${secondsStr}.${frameStr}`;
}
return `${minutesStr}:${secondsStr}.${frameStr}`;
};
export const TimeDisplay: React.FC<{
durationInFrames: number;
fps: number;
playerRef: React.RefObject<PlayerRef>;
}> = ({ durationInFrames, fps, playerRef }) => {
const [time, setTime] = useState(0);
useEffect(() => {
const { current } = playerRef;
if (!current) return;
const onFrameUpdate = () => {
setTime(current.getCurrentFrame());
};
current.addEventListener('frameupdate', onFrameUpdate);
return () => {
current.removeEventListener('frameupdate', onFrameUpdate);
};
}, [playerRef]);
return (
<div>
{formatTime(time, fps)} / {formatTime(durationInFrames - 1, fps)}
</div>
);
};進度條 / 搜尋列
import type { PlayerRef } from '@remotion/player';
import React, { useCallback, useEffect, useRef, useState } from 'react';
export const SeekBar: React.FC<{
durationInFrames: number;
playerRef: React.RefObject<PlayerRef>;
inFrame?: number | null;
outFrame?: number | null;
}> = ({ durationInFrames, playerRef, inFrame, outFrame }) => {
const [frame, setFrame] = useState(0);
const [dragging, setDragging] = useState(false);
const barRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const { current } = playerRef;
if (!current) return;
const onFrameUpdate = () => {
if (!dragging) {
setFrame(current.getCurrentFrame());
}
};
current.addEventListener('frameupdate', onFrameUpdate);
return () => {
current.removeEventListener('frameupdate', onFrameUpdate);
};
}, [dragging, playerRef]);
const onPointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
const { current } = barRef;
if (!current) return;
const rect = current.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const newFrame = Math.round(pos * (durationInFrames - 1));
setDragging(true);
setFrame(newFrame);
playerRef.current?.seekTo(newFrame);
const onPointerMove = (moveEvent: PointerEvent) => {
const movePos = (moveEvent.clientX - rect.left) / rect.width;
const moveFrame = Math.round(
Math.max(0, Math.min(1, movePos)) * (durationInFrames - 1)
);
setFrame(moveFrame);
playerRef.current?.seekTo(moveFrame);
};
const onPointerUp = () => {
setDragging(false);
window.removeEventListener('pointermove', onPointerMove);
};
window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerUp, { once: true });
},
[durationInFrames, playerRef],
);
const progress = frame / (durationInFrames - 1);
return (
<div
ref={barRef}
onPointerDown={onPointerDown}
style={{
height: 4,
backgroundColor: '#333',
cursor: 'pointer',
position: 'relative',
}}
>
<div
style={{
height: '100%',
width: `${progress * 100}%`,
backgroundColor: '#0B84F3',
}}
/>
</div>
);
};音量控制
import type { PlayerRef } from '@remotion/player';
import React, { useCallback, useEffect, useState } from 'react';
export const VolumeControl: React.FC<{
playerRef: React.RefObject<PlayerRef>;
}> = ({ playerRef }) => {
const [volume, setVolume] = useState(1);
const [muted, setMuted] = useState(false);
useEffect(() => {
const { current } = playerRef;
if (!current) return;
setVolume(current.getVolume());
setMuted(current.isMuted());
const onVolumeChange = () => {
setVolume(current.getVolume());
setMuted(current.isMuted());
};
current.addEventListener('volumechange', onVolumeChange);
return () => {
current.removeEventListener('volumechange', onVolumeChange);
};
}, [playerRef]);
const onMuteToggle = useCallback(() => {
if (muted) {
playerRef.current?.unmute();
} else {
playerRef.current?.mute();
}
}, [muted, playerRef]);
const onVolumeChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseFloat(e.target.value);
playerRef.current?.setVolume(newVolume);
},
[playerRef],
);
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<button onClick={onMuteToggle}>{muted ? '取消靜音' : '靜音'}</button>
<input
type="range"
min={0}
max={1}
step={0.01}
value={muted ? 0 : volume}
onChange={onVolumeChange}
/>
</div>
);
};組合所有控制項
import { Player, PlayerRef } from '@remotion/player';
import React, { useRef } from 'react';
import { MyVideo } from './remotion/MyVideo';
import { PlayPauseButton } from './PlayPauseButton';
import { SeekBar } from './SeekBar';
import { TimeDisplay } from './TimeDisplay';
import { VolumeControl } from './VolumeControl';
const DURATION_IN_FRAMES = 300;
const FPS = 30;
export const VideoPlayer: React.FC = () => {
const playerRef = useRef<PlayerRef>(null);
return (
<div>
<Player
ref={playerRef}
component={MyVideo}
durationInFrames={DURATION_IN_FRAMES}
compositionWidth={1920}
compositionHeight={1080}
fps={FPS}
// 不要設定 controls prop
/>
<div style={{ padding: '8px 0' }}>
<SeekBar
durationInFrames={DURATION_IN_FRAMES}
playerRef={playerRef}
/>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<PlayPauseButton playerRef={playerRef} />
<TimeDisplay
durationInFrames={DURATION_IN_FRAMES}
fps={FPS}
playerRef={playerRef}
/>
<VolumeControl playerRef={playerRef} />
</div>
</div>
</div>
);
};PlayerRef 可用的方法
以下是 PlayerRef 上可用的主要方法:
| 方法 | 說明 |
|---|---|
play(e?) | 播放(可選擇傳遞使用者事件) |
pause() | 暫停 |
toggle(e?) | 切換播放 / 暫停 |
seekTo(frame) | 跳至指定幀 |
getCurrentFrame() | 取得目前幀 |
isPlaying() | 是否正在播放 |
isMuted() | 是否靜音 |
mute() | 靜音 |
unmute() | 取消靜音 |
setVolume(num) | 設定音量(0 到 1) |
getVolume() | 取得音量 |
requestFullscreen() | 請求全螢幕 |
exitFullscreen() | 退出全螢幕 |
isFullscreen() | 是否全螢幕 |
pauseAndReturnToPlayStart() | 暫停並返回播放起始點 |