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

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

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

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

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

อัตราที่มีประสิทธิภาพซึ่งใช้เรียกใช้ฟังก์ชันเรียกกลับคืออัตราที่ต่ำกว่าระหว่างอัตราของวิดีโอ กับอัตราของเบราว์เซอร์ ซึ่งหมายความว่าวิดีโอ 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!
}

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

Browser Support

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

Source

Polyfill

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

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

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

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() ทำงานในเทรดหลัก แต่การคอมโพสิตวิดีโอจะเกิดขึ้นในเทรดของ Compositor ทุกอย่างจาก API นี้จึงเป็นความพยายามอย่างดีที่สุด และเบราว์เซอร์ไม่รับประกันอย่างเคร่งครัด สิ่งที่อาจเกิดขึ้นคือ API อาจช้ากว่า 1 Vsync เมื่อเทียบกับเวลาที่แสดงผลเฟรมวิดีโอ การเปลี่ยนแปลงที่เกิดขึ้นกับหน้าเว็บผ่าน API จะใช้เวลา 1 Vsync จึงจะปรากฏบนหน้าจอ (เช่นเดียวกับ window.requestAnimationFrame()) ดังนั้นหากคุณอัปเดต mediaTime หรือหมายเลขเฟรมในหน้าเว็บอย่างต่อเนื่องและเปรียบเทียบกับเฟรมวิดีโอที่มีหมายเลข ในที่สุดวิดีโอจะดูเหมือนว่าอยู่ล้ำหน้าไป 1 เฟรม

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

สาธิต

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

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