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ý 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 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 từng khung hình video, chẳng hạn như xử lý và vẽ video 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ẽ khung video lên 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 video thực tế, ngoại trừ một trường hợp ngoại lệ quan trọng:
Tốc độ hiệu quả mà 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à 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 khung hình/giây sẽ kích hoạt lệnh gọi lại ở tốc độ 60 khung hình/giây.
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
Polyfill
Có sẵn 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ẽ 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à DOMHighResTimeStamp
và metadata
là từ điển VideoFrameMetadata
có các thuộc tính sau:
presentationTime
, thuộc loạiDOMHighResTimeStamp
: 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ạiDOMHighResTimeStamp
: 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ạiunsigned long
: Chiều rộng của khung video, tính bằng pixel 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 nội dung nghe nhìn.mediaTime
, thuộc loạidouble
: 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 gianvideo.currentTime
).presentedFrames
, thuộc loạiunsigned 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ủaVideoFrameRequestCallback
hay không.processingDuration
, thuộc loạidouble
: 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ớimediaTime
) đế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ạ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 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 trình gửi RTCP để chuyển đổi dấu thời gian RTP thành thời gian ghi.receiveTime
, thuộc loạiDOMHighResTimeStamp
: Đố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ạiunsigned 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 (hoặc 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 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 Medley
và Kayce Basques xem xét.
Hình ảnh chính của Denise Jans trên Unsplash.