Remotion LabRemotion Lab
Player取得與顯示 Player 目前時間

取得與顯示 Player 目前時間

學習如何在 Remotion Player 外部取得並顯示目前播放時間,同時避免不必要的重新渲染。

取得與顯示 Player 目前時間

在應用程式中渲染 <Player> 時,必須特別注意,避免在時間變化時造成整個應用程式或 <Player> 元件持續重新渲染。

這也是為什麼 useCurrentFrame() hook 無法在組合外部使用的原因。

警告:不要將此 hook 放入渲染 <Player> 的同一個元件中,否則你將看到持續的重新渲染。請將其放入一個與渲染 Player 的元件相鄰的元件中。

與 Player 時間同步元件

如果你想顯示一個與 Player 時間同步的元件,例如時間軸元件的游標或自訂時間顯示,可以使用以下 hook:

use-current-player-frame.ts
import { CallbackListener, PlayerRef } from '@remotion/player';
import { useCallback, useSyncExternalStore } from 'react';
 
export const useCurrentPlayerFrame = (
  ref: React.RefObject<PlayerRef>,
) => {
  const subscribe = useCallback(
    (onStoreChange: () => void) => {
      const { current } = ref;
      if (!current) {
        return () => undefined;
      }
      const updater: CallbackListener<'frameupdate'> = ({ detail }) => {
        onStoreChange();
      };
      current.addEventListener('frameupdate', updater);
      return () => {
        current.removeEventListener('frameupdate', updater);
      };
    },
    [ref],
  );
 
  const data = useSyncExternalStore(
    subscribe,
    () => ref.current?.getCurrentFrame() ?? 0,
    () => 0,
  );
 
  return data;
};

使用範例

在 React Player 上新增 ref,並將其傳遞給另一個元件:

import { Player, PlayerRef } from '@remotion/player';
import { useRef } from 'react';
import { MyVideo } from './remotion/MyVideo';
import { TimeDisplay } from './remotion/TimeDisplay';
 
export const App: React.FC = () => {
  const playerRef = useRef<PlayerRef>(null);
 
  return (
    <>
      <Player
        ref={playerRef}
        component={MyVideo}
        durationInFrames={120}
        compositionWidth={1920}
        compositionHeight={1080}
        fps={30}
      />
      <TimeDisplay playerRef={playerRef} />
    </>
  );
};

以下是元件存取目前時間的方式:

TimeDisplay.tsx
import React from 'react';
import { PlayerRef } from '@remotion/player';
import { useCurrentPlayerFrame } from './use-current-player-frame';
 
export const TimeDisplay: React.FC<{
  playerRef: React.RefObject<PlayerRef>;
}> = ({ playerRef }) => {
  const frame = useCurrentPlayerFrame(playerRef);
 
  return <div>目前幀:{frame}</div>;
};

這個方法很有效率,因為只有影片本身和依賴時間的元件會重新渲染,<Player> 元件本身不會重新渲染。

為什麼不直接使用 useState?

若直接在父層元件監聽事件並使用 useState 儲存幀數,每次時間更新(每秒最多 60 次)都會導致整個元件樹重新渲染,包含 <Player> 本身,造成效能問題。

useSyncExternalStore 可以讓訂閱邏輯只在需要該值的元件中觸發重新渲染,因此是推薦做法。

使用 seekTo() 設定目前時間

透過 PlayerRef,你也可以程式化地跳轉到特定幀:

import { Player, PlayerRef } from '@remotion/player';
import { useRef } from 'react';
import { MyVideo } from './remotion/MyVideo';
 
export const App: React.FC = () => {
  const playerRef = useRef<PlayerRef>(null);
 
  const seekToMiddle = () => {
    playerRef.current?.seekTo(60);
  };
 
  return (
    <>
      <Player
        ref={playerRef}
        component={MyVideo}
        durationInFrames={120}
        compositionWidth={1920}
        compositionHeight={1080}
        fps={30}
      />
      <button onClick={seekToMiddle}>跳至中間</button>
    </>
  );
};

監聽 frameupdate 事件

除了 hook,你也可以直接監聽 frameupdate 事件:

import { useEffect, useRef } from 'react';
import { Player, PlayerRef } from '@remotion/player';
import { MyVideo } from './remotion/MyVideo';
 
export const App: React.FC = () => {
  const playerRef = useRef<PlayerRef>(null);
 
  useEffect(() => {
    const { current } = playerRef;
    if (!current) return;
 
    const onFrameUpdate = () => {
      const frame = current.getCurrentFrame();
      console.log('目前幀:', frame);
    };
 
    current.addEventListener('frameupdate', onFrameUpdate);
    return () => {
      current.removeEventListener('frameupdate', onFrameUpdate);
    };
  }, []);
 
  return (
    <Player
      ref={playerRef}
      component={MyVideo}
      durationInFrames={120}
      compositionWidth={1920}
      compositionHeight={1080}
      fps={30}
    />
  );
};

另請參閱