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 tác giả trang web đăng ký lệnh gọi lại chạy trong các bước kết xuất khi 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ả trên từng khung hình video trên 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.

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

Các thao tác như vẽ một 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á một cách hiệu quả nhất với tốc độ khung hình của video 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() 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à lệnh gọi lại 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 trình duyệt hiển thị ở tốc độ 60 Hz sẽ kích hoạt các lệnh gọi lại ở tần số 25 Hz. Một video 120 khung hình/giây trong cùng một trình duyệt 60 Hz sẽ kích hoạt các cuộc gọi lại ở tốc độ 60 Hz.

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

Do sự tương đồng với window.requestAnimationFrame(), nên ban đầu phương thức này được đề xuất là video.requestAnimationFrame() và đổi tên thành requestVideoFrameCallback(), như đã được thống nhất sau một cuộc thảo luận dà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

  • 83
  • 83
  • x
  • 15,4

Nguồn

Vải polyfill

Hiện 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 điểm 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ý 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 được 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 với 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 để kết hợp.
  • expectedDisplayTime, thuộc loại DOMHighResTimeStamp: Thời gian mà tác nhân người dùng muốn hiển thị khung.
  • width, thuộc loại unsigned long: Chiều rộng của khung video, tính bằng pixel nội dung đa phương tiện.
  • height, thuộc loại unsigned long: Chiều cao của khung hình video, tính bằng pixel.
  • mediaTime, thuộc loại double: Dấu thời gian trình bày nội dung đa phương tiện (PTS) tính bằng giây của khung hình được trình chiếu (ví dụ: dấu thời gian trên tiến trình video.currentTime).
  • presentedFrames, thuộc loại unsigned long: Số lượng khung được gửi để kết hợp. Cho phép ứng dụng xác định xem khung hình có bị bỏ lỡ 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 kể từ khi gửi gói đã mã hoá có cùng dấu thời gian trình bày (PTS) với khung này (ví dụ như mediaTime) đến bộ giải mã cho đến khi khung đã 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 khác 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 nguồn từ xa, đây là thời điểm máy ảnh chụp khung hình. Đối với nguồn từ xa, thời gian ghi đượ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 chụp.
  • 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 gian nền tảng nhận được khung mã hoá, tức là thời điểm gói cuối cùng của khung 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.

Đặc biệt quan tâm trong danh sách này là mediaTime. Quá trình triển khai 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. mediaTime là tính năng bạn nên sử dụng nếu muốn xác định chính xác khung hình theo cách có thể mô phỏng, bao gồm cả việc xác định chính xác khung hình bạn đã bỏ lỡ.

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

Đồng bộ hoá theo chiều dọc (hay chỉ vsync) là một 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 về cơ bản, việc tổng hợp video diễn ra trên luồng của trình tổng hợp, nên mọi thứ của API này đều là nỗ lực tối đa và trình duyệt không đưa ra bất kỳ đảm bảo nghiêm ngặt nào. Vấn đề có thể xảy ra là API có thể bị vsync một cách trễ so với thời điểm khung hình video được kết xuất. Cần phải thực hiện một vsync để các thay đổi được thực hiện trên 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 của mình và so sánh với số khung hình video được đánh số, thì cuối cùng video sẽ trông như một khung hình phía trước.

Điều thực sự đang xảy ra là khung đã sẵn sàng khi ở vsync x, lệnh gọi lại được kích hoạt và khung hình được kết xuất ở vsync x+1, và các thay đổi được thực hiện trong lệnh gọi lại được hiển thị ở vsync x+2. Bạn có thể kiểm tra xem lệnh gọi lại có phải là vsync muộn (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 là khoảng now hay một vsync trong tương lai. Nếu trong khoảng 5 đến 10 micrô giây của now, khung hình đã được kết xuất; nếu expectedDisplayTime còn 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ì khung hình sẽ được đồng bộ hoá.

Bản minh hoạ

Tôi đã tạo một bản minh hoạ trên Glitch nhỏ cho thấy cách khung hình được vẽ trên canvas ở chính xác tốc độ khung hình của video và nơi siêu dữ liệu khung đượ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 khung thực tế mà chỉ dựa trên video.currentTime. Phương thức requestVideoFrameCallback() được cải thiện đáng kể khi áp dụng giải pháp này.

Xác nhận

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