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

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

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

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

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

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

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

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

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

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

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

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

  • 83
  • 83
  • x
  • 15.4

แหล่งที่มา

ใยโพลีเอสเตอร์

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

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

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

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

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

ถ้าทุกอย่างดูออกแค่เฟรมเดียว...

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

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

ข้อมูลประชากร

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

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() ปรับปรุงวิธีแก้ปัญหานี้ได้อย่างมาก

ข้อความแสดงการยอมรับ

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