Thực hiện các thao tác hiệu quả trên mỗi khung hình video

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.

Xuất bản: Ngày 8 tháng 1 năm 2023

Browser Support

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

Source

Phương thức HTMLVideoElement.requestVideoFrameCallback() cho phép 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 hình video mới được gửi đến trình kết 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ả trên mỗi khung hình video, chẳng hạn như xử lý video và vẽ lên canvas, phân tích video hoặc đồng bộ hoá với các nguồn âm thanh bên ngoài.

Khác biệt với requestAnimationFrame()

Các thao tác được thực hiện bằng API này, chẳng hạn như vẽ một khung hình video vào canvas bằng drawImage(), được đồng bộ hoá một cách tốt nhất với tốc độ khung hình của video đang phát trên màn hình. Điều này khác với window.requestAnimationFrame(), thường kích hoạt khoảng 60 lần mỗi giây.

requestVideoFrameCallback() được liên kết với tốc độ khung hình thực tế của video, với một ngoại lệ quan trọng:

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

Phát hiện đối tượng

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

Polyfill

Có sẵn 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

Nếu sử dụng phương thức requestAnimationFrame(), bạn sẽ nhận ra phương thức requestVideoFrameCallback(). Đă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 gian mà tác nhân người dùng gửi khung hình để tạo thành.
  • expectedDisplayTime, thuộc loại DOMHighResTimeStamp: Thời gian 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 hình video, tính bằng pixel trên nội dung nghe nhìn.
  • height, thuộc loại unsigned long: Chiều cao của khung hình video, tính bằng pixel trên nội dung nghe nhìn.
  • mediaTime, thuộc loại double: Dấu thời gian trình chiếu nội dung nghe nhìn (PTS) tính bằng giây của khung hình được trình chiếu (chẳng hạn như dấu thời gian của khung hình đó trên dòng thời gian video.currentTime).
  • presentedFrames, thuộc loại unsigned long: Số lượng khung hình được gửi để tạo thành hình ảnh. Cho phép các ứng dụng xác định xem có khung hình nào bị bỏ lỡ giữa các phiên bản của VideoFrameRequestCallback hay không.
  • processingDuration, thuộc loại double: Khoảng thời gian đã trôi qua tính bằng giây kể từ khi gửi gói được mã hoá có cùng dấu thời gian trình bày (PTS) với khung hình này (ví dụ: giống như mediaTime) đến bộ giải mã cho đến khi khung hình được giải mã sẵn sàng để trình bày.

Đố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 camera chụp khung hình. Đối với nguồn từ xa, thời gian ghi hình đượ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 của người gửi RTCP để chuyển đổi dấu thời gian RTP thành thời gian ghi hình.
  • receiveTime, thuộc loại DOMHighResTimeStamp: Đối với các khung hình video đến từ một nguồn từ xa, đây là thời điểm nền tảng nhận được khung hình đã mã hoá, tức là thời điểm gói cuối cùng thuộc khung hình này được nhận 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.

Trong danh sách này, mediaTime là một điểm đặc biệt thú vị. Quá trình triển khai của Chromium sử dụng đồng hồ âm thanh làm nguồn thời gian hỗ trợ video.currentTime, trong khi mediaTime được presentationTimestamp của khung hình điền trực tiếp. 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, kể cả để xác định chính xác những khung hình mà bạn đã bỏ lỡ.

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

Đồng bộ hoá dọc (hoặc chỉ là vsync) là một công nghệ đồ hoạ giúp đồ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 ở phía sau, quá trình 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ỳ đảm bảo nghiêm ngặt nào.

Có thể API này bị trễ một vsync so với thời điểm một khung hình video được kết xuất. Cần có một vsync để các thay đổi được thực hiện đối với trang web thông qua API 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ẽ trông như thể đi trước một khung hình.

Điều thực sự 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 sẽ đượ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à lệnh gọi lại vsync muộn (và khung hình đã được kết xuất trên màn hình) hay không bằng cách kiểm tra xem metadata.expectedDisplayTime có xấp xỉ 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, thì 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 của bạn đang làm mới ở tần số 60 Hz), thì bạn đang đồng bộ hoá với khung hình.

Bản minh hoạ

Tôi đã tạo một bản minh hoạ nhỏ cho thấy cách các khung hình được vẽ trên canvas ở đúng tốc độ khung hình 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ó 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() giúp cải thiện đáng kể giải pháp này.

Lời cảm ơn

API requestVideoFrameCallback được Thomas Guilbert chỉ định và triển khai. Bài đăng này được Joe MedleyKayce Basques xem xét.