Thực hiện hiệu quả các thao tác trên mỗi khung hình video trên video bằng requestVideoFrameCallback()

Tìm hiểu cách sử dụng requestVideoFrameCallback() để làm việc hiệu quả hơn với video trong trình duyệt.

Phương thức HTMLVideoElement.requestVideoFrameCallback() cho phép các tác giả web đăng ký một lệnh gọi lại chạy trong các bước kết xuất khi một khung video mới được gửi đến trình tổng hợp. Điều này cho phép nhà phát triển thực hiện các thao tác hiệu quả theo từng khung hình đối với video, chẳng hạn như xử lý và vẽ video vào canvas, phân tích video hoặc đồng bộ hoá với các nguồn âm thanh bên ngoài.

Sự khác biệt với requestAnimationFrame()

Các thao tác như vẽ khung hình video vào canvas bằng drawImage() thực hiện thông qua API này sẽ được đồng bộ hoá với tốc độ khung hình của video đang phát trên màn hình. Khác với window.requestAnimationFrame() (thường kích hoạt khoảng 60 lần mỗi giây), requestVideoFrameCallback() bị liên kết với tốc độ khung hình video thực tế – với một ngoại lệ quan trọng:

Tốc độ hiệu quả mà lệnh gọi lại chạy là tốc độ giữa tốc độ của video và trình duyệt thấp hơn. Điều này có nghĩa là video 25 khung hình/giây phát trong trình duyệt vẽ ở tốc độ 60 Hz sẽ kích hoạt lệnh gọi lại ở tốc độ 25 Hz. Video 120 khung hình/giây trong cùng một trình duyệt 60 Hz đó sẽ kích hoạt lệnh gọi lại ở tốc độ 60 Hz.

Nên đặt tên như thế nào?

Do có sự tương đồng với window.requestAnimationFrame(), ban đầu phương thức này được đề xuất là video.requestAnimationFrame() và đổi tên thành requestVideoFrameCallback(). Đây là phương thức được đồng ý sau một cuộc thảo luận dài hơi.

Phát hiện tính năng

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

Hỗ trợ trình duyệt

Hỗ trợ trình duyệt

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

Nguồn

Ống polyfill

Có một polyfill cho phương thức requestVideoFrameCallback() dựa trên Window.requestAnimationFrame()HTMLVideoElement.getVideoPlaybackQuality(). Trước khi sử dụng, hãy lưu ý đến các hạn chế được đề cập trong README.

Sử dụng phương thức requestVideoFrameCallback()

Nếu từng sử dụng phương thức requestAnimationFrame(), bạn sẽ ngay lập tức cảm thấy quen thuộc với phương thức requestVideoFrameCallback(). Bạn đăng ký một lệnh gọi lại ban đầu một lần, sau đó đăng ký lại bất cứ khi nào lệnh gọi lại kích hoạt.

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);

Trong lệnh gọi lại, now là một DOMHighResTimeStampmetadata là một từ điển VideoFrameMetadata có các thuộc tính sau:

  • presentationTime, thuộc loại DOMHighResTimeStamp: Thời điểm mà tác nhân người dùng gửi khung hình để kết hợp.
  • expectedDisplayTime, thuộc loại DOMHighResTimeStamp: Thời điểm mà tác nhân người dùng dự kiến khung hình sẽ hiển thị.
  • width, thuộc loại unsigned long: Chiều rộng của khung video, tính bằng pixel nội dung nghe nhìn.
  • height, thuộc loại unsigned long: Chiều cao của khung video, tính bằng pixel nội dung nghe nhìn.
  • mediaTime, thuộc loại double: Dấu thời gian hiển thị nội dung nghe nhìn (PTS) tính bằng giây của khung được hiển thị (ví dụ: dấu thời gian của khung đó trên dòng thời gian video.currentTime).
  • presentedFrames, thuộc loại unsigned long: Số lượng khung hình đã gửi để kết hợp. Cho phép ứng dụng xác định xem có bỏ lỡ khung hình nào giữa các thực thể của VideoFrameRequestCallback hay không.
  • processingDuration, thuộc loại double: Thời lượng đã trôi qua tính bằng giây từ khi gửi gói đã mã hoá có cùng dấu thời gian hiển thị (PTS) với khung này (ví dụ: giống với mediaTime) đến bộ giải mã cho đến khi khung đã giải mã sẵn sàng để hiển thị.

Đối với các ứng dụng WebRTC, các thuộc tính bổ sung có thể xuất hiện:

  • captureTime, thuộc loại DOMHighResTimeStamp: Đối với các khung hình video đến từ nguồn cục bộ hoặc từ xa, đây là thời điểm máy ảnh chụp khung hình. Đối với một nguồn từ xa, thời gian chụp được ước tính bằng cách sử dụng tính năng đồng bộ hoá đồng hồ và báo cáo người gửi RTCP để chuyển đổi dấu thời gian RTP thành thời gian.
  • receiveTime, thuộc loại DOMHighResTimeStamp: Đối với các khung video đến từ một nguồn từ xa, đây là thời điểm nền tảng nhận được khung đã mã hoá, tức là thời điểm nhận được gói cuối cùng thuộc khung này qua mạng.
  • rtpTimestamp, thuộc loại unsigned long: Dấu thời gian RTP liên kết với khung hình video này.

Đặc biệt quan tâm trong danh sách này là mediaTime. Phương thức triển khai của Chromium sử dụng đồng hồ âm thanh làm nguồn thời gian sao lưu video.currentTime, trong khi mediaTime được điền trực tiếp bằng presentationTimestamp của khung. Bạn nên sử dụng mediaTime nếu muốn xác định chính xác các khung hình theo cách có thể tái tạo, bao gồm cả việc xác định chính xác những khung hình bạn đã bỏ lỡ.

Nếu mọi thứ có vẻ bị lệch một khung hình...

Đồng bộ hoá theo chiều dọc (hay chỉ vsync) là công nghệ đồ hoạ đồng bộ hoá tốc độ khung hình của video và tốc độ làm mới của màn hình. Vì requestVideoFrameCallback() chạy trên luồng chính, nhưng trong thực tế, việc kết hợp video diễn ra trên luồng trình kết hợp, mọi thứ từ API này đều là nỗ lực tốt nhất và trình duyệt không đưa ra bất kỳ cam kết nghiêm ngặt nào. Có thể API bị trễ một vsync so với thời điểm kết xuất khung hình video. Phải mất một vsync thì các thay đổi đối với trang web thông qua API mới xuất hiện trên màn hình (tương tự như window.requestAnimationFrame()). Vì vậy, nếu bạn tiếp tục cập nhật mediaTime hoặc số khung hình trên trang web và so sánh số đó với các khung hình video được đánh số, thì cuối cùng video sẽ có vẻ như bị trễ một khung hình.

Điều thực sự đang xảy ra là khung hình đã sẵn sàng tại vsync x, lệnh gọi lại được kích hoạt và khung hình được kết xuất tại vsync x+1, đồng thời các thay đổi được thực hiện trong lệnh gọi lại được kết xuất tại vsync x+2. Bạn có thể kiểm tra xem lệnh gọi lại có phải là vsync trễ (và khung hình đã hiển thị trên màn hình) hay không bằng cách kiểm tra xem metadata.expectedDisplayTime có gần bằng now hay một vsync trong tương lai hay không. Nếu nằm trong khoảng từ 5 đến 10 micro giây của now, khung hình đã được kết xuất; nếu expectedDisplayTime là khoảng 16 mili giây trong tương lai (giả sử trình duyệt/màn hình của bạn đang làm mới ở tốc độ 60 Hz), thì bạn đang đồng bộ với khung hình.

Bản minh hoạ

Tôi đã tạo một bản minh hoạ nhỏ trên Glitch cho thấy cách các khung hình được vẽ trên canvas ở tốc độ khung hình chính xác của video và nơi siêu dữ liệu khung hình được ghi lại cho mục đích gỡ lỗi.

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);

Kết luận

Mọi người đã xử lý ở cấp khung hình trong một thời gian dài mà không cần quyền truy cập vào các khung hình thực tế, chỉ dựa trên video.currentTime. Phương thức requestVideoFrameCallback() cải thiện đáng kể giải pháp này.

Lời cảm ơn

API requestVideoFrameCallback do Thomas Guilbert chỉ định và triển khai. Bài đăng này đã được Joe MedleyKayce Basques xem xét. Hình ảnh chính của Denise Jans trên Unsplash.