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

브라우저 지원

브라우저 지원

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

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

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

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

한 프레임에서 벗어난 것 같다면...

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

실제로 발생하는 것은 프레임이 vsync x에서 준비되고 콜백이 실행되어 프레임이 vsync x+1에서 렌더링되고, 콜백의 변경사항이 vsync x+2에서 렌더링된다는 점입니다. metadata.expectedDisplayTime가 대략 now인지 또는 향후 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는 토마스 길버트가 지정하고 구현했습니다. 이 게시물을 Joe MedleyKayce Basques가 검토했습니다. 히어로 이미지(Unsplash) 데니스 얀스