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ả 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.
Nhờ đó, nhà phát triển có thể 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 như vẽ một khung hình video vào canvas bằng drawImage()
được thực hiện thông qua API này sẽ đượ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.
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ần số 60Hz.
Nên đặt tên như thế nào?
Do có điểm 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()
. Sau một cuộc thảo luận dài, chúng tôi đã thống nhất về tên này.
Phát hiện đối tượng
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
// The API is supported!
}
Hỗ trợ trình duyệt
Polyfill
Có sẵn một polyfill cho phương thức requestVideoFrameCallback()
dựa trên Window.requestAnimationFrame()
và 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ẽ cảm thấy quen thuộc ngay 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 DOMHighResTimeStamp
và metadata
là một từ điển VideoFrameMetadata
có các thuộc tính sau:
presentationTime
, thuộc loạiDOMHighResTimeStamp
: Thời gian mà tác nhân người dùng gửi khung hình để tạo thành phần.expectedDisplayTime
, thuộc loạiDOMHighResTimeStamp
: 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ạiunsigned 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ạiunsigned 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ạidouble
: 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 (ví dụ: dấu thời gian của khung hình trên dòng thời gianvideo.currentTime
).presentedFrames
, thuộc loạiunsigned long
: Số lượng khung hình được gửi để tạo thành phần. 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ủaVideoFrameRequestCallback
hay không.processingDuration
, thuộc loạidouble
: 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ạiDOMHighResTimeStamp
: Đố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ạiDOMHighResTimeStamp
: Đố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ạiunsigned 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ục đặ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.
mediaTime
là những gì bạn nên sử dụng 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ẻ lệch một khung hình...
Đồng bộ hoá dọc (hoặc chỉ là 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 ở chế độ nền, 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ỳ đảm bảo nghiêm ngặt nào.
Có thể API bị trễ một vsync so với thời điểm 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ể đang đ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/màn hình 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
Từ lâu, người ta đã xử lý ở cấp khung hình mà không có quyền truy cập vào 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 Medley và Kayce Basques xem xét.
Hình ảnh chính của Denise Jans trên Unsplash.