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

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

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

requestAnimationFrame()과의 차이점

이 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!
}

브라우저 지원

Browser Support

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

Source

폴리필

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 인스턴스 간에 프레임이 누락되었는지 확인할 수 있습니다.
  • processingDuration(double 유형): 이 프레임과 동일한 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로 새로고침된다고 가정)이면 프레임과 동기화된 것입니다.

데모

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

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는 Thomas Guilbert에 의해 지정되고 구현되었습니다. 이 게시물은 조 메들리케이시 바스크가 검토했습니다. Unsplash의 데니스 얀스가 촬영한 히어로 이미지