ดำเนินการกับวิดีโอต่อเฟรมอย่างมีประสิทธิภาพด้วย requestVideoFrameCallback()

ดูวิธีใช้ requestVideoFrameCallback() เพื่อทำงานกับวิดีโอในเบราว์เซอร์ได้อย่างมีประสิทธิภาพมากขึ้น

เมธอด HTMLVideoElement.requestVideoFrameCallback() ช่วยให้ผู้เขียนเว็บลงทะเบียนการเรียกกลับซึ่งจะทํางานในขั้นตอนการแสดงผลเมื่อส่งเฟรมวิดีโอใหม่ไปยังโปรแกรมคอมโพสิเตอร์ วิธีนี้ช่วยให้นักพัฒนาซอฟต์แวร์ทำงานต่อเฟรมวิดีโอได้อย่างมีประสิทธิภาพในวิดีโอ เช่น การประมวลผลและการลงสีวิดีโอบนผืนผ้าใบ การวิเคราะห์วิดีโอ หรือการซิงค์ข้อมูลกับแหล่งที่มาของเสียงภายนอก

ความแตกต่างกับ requestAnimationFrame()

การดำเนินการต่างๆ เช่น การวาดเฟรมวิดีโอลงในผืนผ้าใบโดยใช้ drawImage() ที่ทำผ่าน API นี้จะซิงค์ตามอัตราเฟรมของวิดีโอที่เล่นบนหน้าจอ requestVideoFrameCallback() แตกต่างจากwindow.requestAnimationFrame()ตรงที่requestVideoFrameCallback()มักจะทำงานประมาณ 60 ครั้งต่อวินาที แต่requestVideoFrameCallback()จะเชื่อมโยงกับอัตราเฟรมวิดีโอจริง โดยมีข้อยกเว้นที่สำคัญดังนี้

อัตราที่มีประสิทธิภาพในการเรียกใช้การเรียกกลับคืออัตราที่น้อยกว่าระหว่างอัตราของวิดีโอกับอัตราของเบราว์เซอร์ ซึ่งหมายความว่าวิดีโอ 25 FPS ที่เล่นในเบราว์เซอร์ซึ่งแสดงผลที่ 60 Hz จะเรียกใช้การเรียกกลับที่ 25 Hz วิดีโอ 120 fps ในเบราว์เซอร์ 60 Hz เดียวกันจะเรียกใช้การเรียกกลับที่ 60 Hz

ชื่อสื่อถึงอะไรบ้าง

เนื่องจากมีความคล้ายคลึงกับ window.requestAnimationFrame() วิธีการนี้จึงได้รับการเสนอเป็น video.requestAnimationFrame() ในช่วงแรก และเปลี่ยนชื่อเป็น requestVideoFrameCallback() ซึ่งได้รับความเห็นชอบหลังจากการพูดคุยกันเป็นเวลานาน

การตรวจหาฟีเจอร์

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

การสนับสนุนเบราว์เซอร์

การรองรับเบราว์เซอร์

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

แหล่งที่มา

โพลีฟิลล์

Polyfill สำหรับเมธอด requestVideoFrameCallback() ที่อิงตาม Window.requestAnimationFrame() และ HTMLVideoElement.getVideoPlaybackQuality() พร้อมใช้งาน โปรดคำนึงถึงข้อจำกัดที่กล่าวถึงใน README ก่อนใช้

การใช้เมธอด requestVideoFrameCallback()

หากเคยใช้เมธอด requestAnimationFrame() มาก่อน คุณจะคุ้นเคยกับเมธอด requestVideoFrameCallback() ทันที คุณจะลงทะเบียน Callback เริ่มต้นครั้งเดียว จากนั้นลงทะเบียนใหม่ทุกครั้งที่ Callback เริ่มทำงาน

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

ใน Callback now คือ DOMHighResTimeStamp และ metadata คือพจนานุกรม VideoFrameMetadata ที่มีพร็อพเพอร์ตี้ต่อไปนี้

  • presentationTime ประเภท DOMHighResTimeStamp: เวลาที่ User Agent ส่งเฟรมสำหรับการจัดองค์ประกอบ
  • expectedDisplayTime ประเภท DOMHighResTimeStamp: เวลาที่ User Agent คาดหวังว่าเฟรมจะปรากฏ
  • width ประเภท unsigned long: ความกว้างของเฟรมวิดีโอเป็นพิกเซลสื่อ
  • height ประเภท unsigned long: ความสูงของเฟรมวิดีโอเป็นพิกเซลสื่อ
  • mediaTime ประเภท double: การประทับเวลาของการแสดงสื่อ (PTS) เป็นวินาทีของเฟรมที่แสดง (เช่น การประทับเวลาในไทม์ไลน์ video.currentTime)
  • presentedFrames ประเภท unsigned long: จํานวนเฟรมที่ส่งมาเพื่อการจัดองค์ประกอบ ช่วยให้ไคลเอ็นต์ระบุได้ว่ามีการข้ามเฟรมระหว่างอินสแตนซ์ของ VideoFrameRequestCallback หรือไม่
  • processingDuration ประเภท double: ระยะเวลาที่ผ่านไปเป็นวินาทีนับจากการส่งแพ็กเก็ตที่เข้ารหัสซึ่งมีการประทับเวลาการนำเสนอ (PTS) เดียวกันกับเฟรมนี้ (เช่น เหมือนกับ mediaTime) ไปยังโปรแกรมถอดรหัสจนกว่าเฟรมที่ถอดรหัสจะพร้อมสำหรับการนำเสนอ

สำหรับแอปพลิเคชัน WebRTC คุณสมบัติเพิ่มเติมอาจปรากฏขึ้น:

  • captureTime ประเภท DOMHighResTimeStamp: สำหรับเฟรมวิดีโอที่มาจากแหล่งที่มาในเครื่องหรือจากระยะไกล ข้อมูลนี้คือเวลาที่กล้องจับเฟรม สําหรับแหล่งที่มาระยะไกล ระบบจะประมาณเวลาในการบันทึกโดยใช้การซิงค์นาฬิกาและรายงานผู้ส่ง RTCP เพื่อแปลงการประทับเวลา RTP เป็นเวลาในการบันทึก
  • receiveTime ประเภท DOMHighResTimeStamp: สำหรับเฟรมวิดีโอที่มาจากแหล่งที่มาระยะไกล นี่คือเวลาที่แพลตฟอร์มได้รับเฟรมที่เข้ารหัส ซึ่งก็คือเวลาที่ได้รับแพ็กเก็ตสุดท้ายที่เป็นของเฟรมนี้ผ่านเครือข่าย
  • rtpTimestamp ประเภท unsigned long: การประทับเวลา RTP ที่เชื่อมโยงกับเฟรมวิดีโอนี้

รายการที่น่าสนใจเป็นพิเศษในรายการนี้คือ mediaTime การใช้งานของ Chromium ใช้นาฬิกาเสียงเป็นแหล่งเวลาสำรอง video.currentTime ส่วน mediaTime จะสร้างขึ้นโดย presentationTimestamp ของเฟรมโดยตรง คุณควรใช้ mediaTime หากต้องการระบุเฟรมอย่างละเอียดในลักษณะที่ซ้ำได้ รวมถึงระบุเฟรมที่คุณพลาด

หากดูเหมือนว่าภาพจะเลื่อนไป 1 เฟรม…

การซิงค์แนวตั้ง (หรือเรียกสั้นๆ ว่า vsync) เป็นเทคโนโลยีกราฟิกที่ซิงค์อัตราเฟรมของวิดีโอและอัตราการรีเฟรชของจอภาพ เนื่องจาก requestVideoFrameCallback() ทำงานบนเธรดหลัก แต่การคอมโพสวิดีโอจะเกิดขึ้นบนเธรดคอมโพสเซอร์ ทุกอย่างจาก API นี้จะดำเนินการอย่างดีที่สุด และเบราว์เซอร์ไม่ได้รับประกันอย่างเข้มงวด สิ่งที่อาจเกิดขึ้นคือ API อาจช้ากว่า vsync 1 ครั้งเมื่อเทียบกับเวลาที่แสดงผลเฟรมวิดีโอ การเปลี่ยนแปลงในหน้าเว็บผ่าน API จะใช้เวลา 1 vsync จึงจะปรากฏบนหน้าจอ (เหมือนกับ window.requestAnimationFrame()) ดังนั้น หากคุณอัปเดต mediaTime หรือหมายเลขเฟรมในหน้าเว็บอยู่เรื่อยๆ และเปรียบเทียบกับเฟรมวิดีโอที่มีหมายเลข วิดีโอจะดูเหมือนว่าเล่นไปก่อน 1 เฟรม

สิ่งที่เกิดขึ้นจริงคือเฟรมจะพร้อมใช้งานที่ Vsync x ระบบจะเรียกใช้การเรียกคืนและแสดงผลเฟรมที่ Vsync x+1 และการเปลี่ยนแปลงที่ทำในการเรียกคืนจะแสดงผลที่ Vsync x+2 คุณสามารถตรวจสอบว่า callback ช้ากว่า vsync หรือไม่ (และเฟรมแสดงบนหน้าจอแล้ว) โดยดูว่า metadata.expectedDisplayTime อยู่ห่างจาก now หรือ 1 vsync ในอนาคตโดยประมาณหรือไม่ หากอยู่ในระยะประมาณ 5 ถึง 10 ไมโครวินาทีของ now แสดงว่าเฟรมแสดงผลแล้ว หาก expectedDisplayTime มีระยะเวลาประมาณ 16 มิลลิวินาทีในอนาคต (สมมติว่าเบราว์เซอร์/หน้าจอมีการรีเฟรชที่ 60 Hz) แสดงว่าคุณซิงค์อยู่กับเฟรม

สาธิต

เราได้สร้างการสาธิตเล็กๆ เกี่ยวกับภาพแตก ซึ่งแสดงวิธีการวาดเฟรมบนผืนผ้าใบตามอัตราเฟรมของวิดีโอทุกประการและที่การบันทึกข้อมูลเมตาของเฟรมเพื่อวัตถุประสงค์ในการแก้ไขข้อบกพร่อง

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

สรุป

ผู้ใช้ประมวลผลระดับเฟรมมาเป็นเวลานานโดยที่ไม่มีสิทธิ์เข้าถึงเฟรมจริง โดยอิงตาม video.currentTime เท่านั้น เมธอด requestVideoFrameCallback() จะปรับปรุงชั่วคราวนี้ให้ดีขึ้นอย่างมาก

กิตติกรรมประกาศ

requestVideoFrameCallback API ได้รับการระบุและติดตั้งใช้งานโดย Thomas Guilbert โพสต์นี้ผ่านการตรวจสอบโดย Joe Medley และ Kayce Basques รูปภาพหลักโดย Denise Jans จาก Unsplash