requestVideoFrameCallback()을 사용하여 동영상에서 효율적인 동영상 프레임별 작업 수행

requestVideoFrameCallback()를 사용하여 브라우저에서 동영상을 더 효율적으로 작업하는 방법을 알아보세요.

HTMLVideoElement.requestVideoFrameCallback() 메서드를 사용하면 웹 작성자가 새 동영상 프레임이 컴포저이터로 전송될 때 렌더링 단계에서 실행되는 콜백을 등록할 수 있습니다. 이를 통해 개발자는 동영상 처리 및 캔버스에 그리기, 동영상 분석, 외부 오디오 소스와의 동기화와 같은 동영상 프레임별 작업을 효율적으로 실행할 수 있습니다.

이 API를 통해 drawImage()를 사용하여 캔버스에 동영상 프레임을 그리는 등의 작업은 화면에서 재생 중인 동영상의 프레임 속도와 최대한 동기화됩니다. 일반적으로 초당 약 60회 실행되는 window.requestAnimationFrame()와 달리 requestVideoFrameCallback()는 실제 동영상 프레임 속도와 관련이 있습니다. 단, 중요한 예외가 있습니다.

콜백이 실행되는 유효 비율은 동영상 비율과 브라우저 비율 중 더 낮은 비율입니다. 즉, 60Hz로 페인트하는 브라우저에서 재생되는 25fps 동영상은 25Hz에서 콜백을 실행합니다. 동일한 60Hz 브라우저에서 120fps 동영상을 재생하면 60Hz에서 콜백이 실행됩니다.

이름에 포함되어야 하는 항목은 무엇인가요?

window.requestAnimationFrame()와 유사하여 이 메서드는 처음에 video.requestAnimationFrame()로 제안되었고 requestVideoFrameCallback()로 이름이 바뀌었습니다. 이 이름은 장시간의 논의 끝에 합의되었습니다.

특성 감지

if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
  // The API is supported!
}

브라우저 지원

브라우저 지원

  • Chrome: 83
  • Edge: 83.
  • Firefox: 132.
  • Safari: 15.4

소스

폴리필

Window.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality()를 기반으로 하는 requestVideoFrameCallback() 메서드의 폴리필을 사용할 수 있습니다. 이 기능을 사용하기 전에 README에 언급된 제한사항을 알아야 합니다.

requestVideoFrameCallback() 메서드 사용

requestAnimationFrame() 메서드를 사용해 본 적이 있다면 requestVideoFrameCallback() 메서드가 즉시 익숙해질 것입니다. 최초 콜백을 한 번 등록한 다음 콜백이 실행될 때마다 다시 등록합니다.

const doSomethingWithTheFrame = (now, metadata) => {
  // Do something with the frame.
  console.log(now, metadata);
  // Re-register the callback to be notified about the next frame.
  video.requestVideoFrameCallback(doSomethingWithTheFrame);
};
// Initially register the callback to be notified about the first frame.
video.requestVideoFrameCallback(doSomethingWithTheFrame);

콜백에서 nowDOMHighResTimeStamp이고 metadata는 다음과 같은 속성이 있는 VideoFrameMetadata 사전입니다.

  • presentationTime(DOMHighResTimeStamp 유형): 사용자 에이전트가 컴포지션을 위해 프레임을 제출한 시간입니다.
  • expectedDisplayTime(DOMHighResTimeStamp 유형): 사용자 에이전트가 프레임이 표시될 것으로 예상하는 시간입니다.
  • width(unsigned long 유형): 동영상 프레임의 너비(미디어 픽셀)입니다.
  • height(unsigned long 유형): 동영상 프레임의 높이(미디어 픽셀)입니다.
  • mediaTime(double 유형): 표시된 프레임의 초 단위 미디어 프레젠테이션 타임스탬프(PTS)입니다(예: video.currentTime 타임라인의 타임스탬프).
  • presentedFrames(unsigned long 유형): 컴포지션에 제출된 프레임 수입니다. 클라이언트가 VideoFrameRequestCallback 인스턴스 간에 프레임이 누락되었는지 확인할 수 있습니다.
  • double 유형의 processingDuration: 디코더에 이 프레임과 동일한 프레젠테이션 타임스탬프(PTS)(예: mediaTime와 동일)가 있는 인코딩된 패킷을 제출한 시점부터 디코딩된 프레임을 표시할 준비가 될 때까지 경과한 시간(초)입니다.

WebRTC 애플리케이션의 경우 추가 속성이 표시될 수 있습니다.

  • captureTime(DOMHighResTimeStamp 유형): 로컬 또는 원격 소스에서 가져온 동영상 프레임의 경우 카메라에 의해 프레임이 캡처된 시간입니다. 원격 소스의 경우 캡처 시간은 시계 동기화 및 RTCP 송신자 보고서를 사용하여 추정되며 이를 통해 RTP 타임스탬프를 캡처 시간으로 변환합니다.
  • receiveTime(DOMHighResTimeStamp 유형): 원격 소스에서 오는 동영상 프레임의 경우 플랫폼에서 인코딩된 프레임을 수신한 시간입니다. 즉, 이 프레임에 속한 마지막 패킷이 네트워크를 통해 수신된 시간입니다.
  • rtpTimestamp(unsigned long 유형): 이 동영상 프레임과 연결된 RTP 타임스탬프입니다.

이 목록에서 특히 흥미로운 사항은 mediaTime입니다. Chromium의 구현은 오디오 시계를 video.currentTime를 지원하는 시간 소스로 사용하지만 mediaTime는 프레임의 presentationTimestamp로 직접 채워집니다. 놓친 프레임을 정확히 식별하는 등 재현 가능한 방식으로 프레임을 정확하게 식별하려면 mediaTime를 사용해야 합니다.

프레임이 하나씩 어긋나는 것처럼 보이는 경우…

세로 동기화(줄여서 vsync)는 동영상의 프레임 속도와 모니터의 새로고침 빈도를 동기화하는 그래픽 기술입니다. requestVideoFrameCallback()는 기본 스레드에서 실행되지만 내부적으로는 동영상 합성 작업이 컴포저 스레드에서 이루어지므로 이 API의 모든 작업은 최선의 방식으로 실행되며 브라우저는 엄격한 보장을 제공하지 않습니다. API가 동영상 프레임이 렌더링될 때보다 1번 늦게 vsync될 수 있습니다. API를 통해 웹페이지의 변경사항이 화면에 표시되려면 vsync가 한 번 필요합니다 (window.requestAnimationFrame()와 동일). 따라서 웹페이지에서 mediaTime 또는 프레임 번호를 계속 업데이트하고 번호가 매겨진 동영상 프레임과 비교하면 결국 동영상이 한 프레임 앞에 있는 것처럼 보입니다.

실제로는 프레임이 vsync x에서 준비되고 콜백이 실행되고 프레임이 vsync x+1에서 렌더링되며 콜백에서 변경된 사항이 vsync x+2에서 렌더링됩니다. metadata.expectedDisplayTime가 대략 now인지 또는 향후 1번의 vsync인지 확인하여 콜백이 vsync 지연인지(그리고 프레임이 이미 화면에 렌더링되었는지) 확인할 수 있습니다. now의 약 5~10마이크로초 이내라면 프레임이 이미 렌더링된 것입니다. expectedDisplayTime가 향후 약 16밀리초라면 (브라우저/화면이 60Hz에서 새로고침된다고 가정) 프레임과 동기화된 것입니다.

데모

저는 동영상의 프레임 속도로 캔버스에 프레임이 그려지는 방식과 디버깅을 위해 프레임 메타데이터가 로깅되는 위치를 보여주는 작은 Glitch 데모를 만들었습니다.

let paintCount = 0;
let startTime = 0.0;

const updateCanvas = (now, metadata) => {
  if (startTime === 0.0) {
    startTime = now;
  }

  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

  const elapsed = (now - startTime) / 1000.0;
  const fps = (++paintCount / elapsed).toFixed(3);
  fpsInfo.innerText = `video fps: ${fps}`;
  metadataInfo.innerText = JSON.stringify(metadata, null, 2);

  video.requestVideoFrameCallback(updateCanvas);
};

video.requestVideoFrameCallback(updateCanvas);

결론

사람들은 실제 프레임에 액세스하지 않고 video.currentTime만을 기반으로 오랫동안 프레임 수준 처리를 진행해 왔습니다. requestVideoFrameCallback() 메서드는 이 해결 방법을 크게 개선합니다.

감사의 말씀

requestVideoFrameCallback API는 토마스 길버트가 지정하고 구현했습니다. 이 게시물은 조 미들리님과 케이스 바스케스님이 검토했습니다. Unsplash의 Denise Jans님이 제공한 히어로 이미지