การประมวลผลวิดีโอด้วย WebCodecs

การจัดการคอมโพเนนต์สตรีมวิดีโอ

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

เทคโนโลยีเว็บสมัยใหม่มีวิธีมากมายในการจัดการวิดีโอ Media Stream API, Media Recording API, Media Source API และ WebRTC API ประกอบกันเป็นชุดเครื่องมือที่สมบูรณ์สำหรับการบันทึก โอน และเล่นสตรีมวิดีโอ แม้ว่าจะแก้ปัญหาระดับสูงได้ แต่ API เหล่านี้ไม่อนุญาตให้นักเขียนโปรแกรมเว็บทำงานกับคอมโพเนนต์แต่ละรายการของสตรีมวิดีโอ เช่น เฟรมและกลุ่มวิดีโอหรือเสียงที่เข้ารหัสซึ่งไม่ได้แยกข้อมูล นักพัฒนาแอปใช้ WebAssembly เพื่อนำตัวแปลงรหัสวิดีโอและเสียงมาไว้ในเบราว์เซอร์เพื่อให้เข้าถึงคอมโพเนนต์พื้นฐานเหล่านี้ในระดับต่ำ แต่เนื่องจากเบราว์เซอร์สมัยใหม่มีโค้ดรูปแบบต่างๆ อยู่แล้ว (ซึ่งมักจะเร่งด้วยฮาร์ดแวร์) การบรรจุโค้ดเหล่านั้นอีกครั้งเป็น WebAssembly จึงดูเหมือนว่าจะเป็นการสิ้นเปลืองทรัพยากรมนุษย์และคอมพิวเตอร์

WebCodecs API ช่วยขจัดปัญหานี้ด้วยการมอบวิธีใช้คอมโพเนนต์สื่อที่มีอยู่ในเบราว์เซอร์อยู่แล้วให้แก่โปรแกรมเมอร์ กล่าวอย่างเจาะจงคือ

  • ตัวถอดรหัสวิดีโอและเสียง
  • ตัวแปลงรหัสวิดีโอและเสียง
  • เฟรมวิดีโอดิบ
  • ตัวถอดรหัสรูปภาพ

WebCodecs API มีประโยชน์สําหรับเว็บแอปพลิเคชันที่ต้องควบคุมวิธีประมวลผลเนื้อหาสื่ออย่างเต็มรูปแบบ เช่น โปรแกรมตัดต่อวิดีโอ การประชุมทางวิดีโอ สตรีมมิงวิดีโอ ฯลฯ

เวิร์กโฟลว์การประมวลผลวิดีโอ

เฟรมเป็นหัวใจสำคัญในการประมวลผลวิดีโอ ดังนั้นใน WebCodecs คลาสส่วนใหญ่จะบริโภคหรือสร้างเฟรม โปรแกรมเปลี่ยนไฟล์วิดีโอจะแปลงเฟรมเป็นกลุ่มที่ผ่านการเข้ารหัส ส่วนโปรแกรมถอดรหัสวิดีโอจะทำงานในทางตรงกันข้าม

นอกจากนี้ VideoFrame ยังทำงานร่วมกับ Web API อื่นๆ ได้อย่างราบรื่นโดยเป็น CanvasImageSource และมีคอนสตรคเตอร์ที่ยอมรับ CanvasImageSource จึงนำไปใช้ในฟังก์ชันต่างๆ ได้ เช่น drawImage() และ texImage2D() นอกจากนี้ยังสร้างจากภาพพิมพ์แคนวาส บิตแมป องค์ประกอบวิดีโอ และเฟรมวิดีโออื่นๆ ได้ด้วย

WebCodecs API ทำงานร่วมกับคลาสจาก Insertable Streams API ได้ดีมาก ซึ่งจะเชื่อมต่อ WebCodecs กับแทร็กสตรีมสื่อ

  • MediaStreamTrackProcessor แบ่งแทร็กสื่อออกเป็นเฟรมเดี่ยวๆ
  • MediaStreamTrackGenerator สร้างแทร็กสื่อจากสตรีมเฟรม

WebCodecs และผู้ปฏิบัติงานเกี่ยวกับเว็บ

WebCodecs API ออกแบบมาเพื่อทำงานหนักทั้งหมดแบบไม่พร้อมกันและไม่ได้อยู่ในเธรดหลัก แต่เนื่องจากระบบมักจะเรียกใช้การเรียกคืนเฟรมและข้อมูลโค้ดหลายครั้งต่อวินาที การดำเนินการเหล่านี้จึงอาจทำให้เธรดหลักมีความยุ่งเหยิงและทำให้เว็บไซต์ตอบสนองช้าลง ดังนั้น คุณควรย้ายการจัดการเฟรมแต่ละเฟรมและกลุ่มที่โค้ดแล้วไปยัง Web Worker

ReadableStream จึงเป็นวิธีที่สะดวกในการโอนเฟรมทั้งหมดที่มาจากแทร็กสื่อไปยังผู้ปฏิบัติงานโดยอัตโนมัติ เช่น MediaStreamTrackProcessor สามารถใช้รับ ReadableStream สำหรับแทร็กสตรีมสื่อที่มาจากเว็บแคม หลังจากนั้น ระบบจะโอนสตรีมไปยังเว็บเวิร์กเกอร์ ซึ่งจะอ่านเฟรมทีละเฟรมและจัดคิวเป็น VideoEncoder

HTMLCanvasElement.transferControlToOffscreen ช่วยให้เรนเดอร์ได้แม้ไม่ได้อยู่ในเธรดหลัก แต่หากเครื่องมือระดับสูงทั้งหมดไม่สะดวก VideoFrame เองก็โอนได้และอาจย้ายไปมาระหว่างผู้ปฏิบัติงาน

การใช้งาน WebCodecs

การเข้ารหัส

เส้นทางจาก Canvas หรือ ImageBitmap ไปยังเครือข่ายหรือพื้นที่เก็บข้อมูล
เส้นทางจาก Canvas หรือ ImageBitmap ไปยังเครือข่ายหรือพื้นที่เก็บข้อมูล

ทุกอย่างเริ่มต้นด้วย VideoFrame การสร้างเฟรมวิดีโอทำได้ 3 วิธี

  • จากแหล่งที่มาของรูปภาพ เช่น ภาพพิมพ์แคนวาส บิตแมปรูปภาพ หรือองค์ประกอบวิดีโอ

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • ใช้ MediaStreamTrackProcessor เพื่อดึงเฟรมจาก MediaStreamTrack

    const stream = await navigator.mediaDevices.getUserMedia({});
    const track = stream.getTracks()[0];
    
    const trackProcessor = new MediaStreamTrackProcessor(track);
    
    const reader = trackProcessor.readable.getReader();
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      const frameFromCamera = result.value;
    }
    
  • สร้างเฟรมจากการแสดงผลพิกเซลแบบไบนารีใน BufferSource

    const pixelSize = 4;
    const init = {
      timestamp: 0,
      codedWidth: 320,
      codedHeight: 200,
      format: "RGBA",
    };
    const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
    for (let x = 0; x < init.codedWidth; x++) {
      for (let y = 0; y < init.codedHeight; y++) {
        const offset = (y * init.codedWidth + x) * pixelSize;
        data[offset] = 0x7f;      // Red
        data[offset + 1] = 0xff;  // Green
        data[offset + 2] = 0xd4;  // Blue
        data[offset + 3] = 0x0ff; // Alpha
      }
    }
    const frame = new VideoFrame(data, init);
    

เฟรมสามารถเข้ารหัสเป็นออบเจ็กต์ EncodedVideoChunk ด้วย VideoEncoder ไม่ว่าเฟรมจะมาจากที่ใดก็ตาม

ก่อนการแปลงไฟล์ VideoEncoder จะต้องได้รับออบเจ็กต์ JavaScript 2 รายการต่อไปนี้

  • เริ่มต้นพจนานุกรมด้วยฟังก์ชัน 2 รายการสำหรับจัดการกับข้อมูลโค้ดและข้อผิดพลาด ฟังก์ชันเหล่านี้จะกำหนดโดยนักพัฒนาแอปและจะเปลี่ยนแปลงไม่ได้หลังจากส่งไปยังตัวสร้าง VideoEncoder
  • ออบเจ็กต์การกําหนดค่าโปรแกรมเปลี่ยนไฟล์ซึ่งมีพารามิเตอร์สําหรับสตรีมวิดีโอเอาต์พุต คุณเปลี่ยนพารามิเตอร์เหล่านี้ในภายหลังได้โดยเรียกใช้ configure()

เมธอด configure() จะแสดง NotSupportedError หากเบราว์เซอร์ไม่รองรับการกําหนดค่า เราขอแนะนำให้เรียกใช้เมธอดแบบคงที่ VideoEncoder.isConfigSupported() ด้วยการกำหนดค่าเพื่อตรวจสอบล่วงหน้าว่าระบบรองรับการกำหนดค่าหรือไม่ และรอการตอบสนอง

const init = {
  output: handleChunk,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};

const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
  const encoder = new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

หลังจากตั้งค่าโปรแกรมเปลี่ยนไฟล์แล้ว โปรแกรมจะพร้อมรับเฟรมผ่านวิธีการ encode() ทั้ง configure() และ encode() จะแสดงผลทันทีโดยไม่ต้องรอให้งานจริงเสร็จสมบูรณ์ ซึ่งจะช่วยให้หลายเฟรมอยู่ในคิวรอการเข้ารหัสได้พร้อมกัน ขณะที่ encodeQueueSize จะแสดงจํานวนคําขอที่รออยู่ในคิวเพื่อให้การเข้ารหัสก่อนหน้าเสร็จสมบูรณ์ ระบบจะรายงานข้อผิดพลาดโดยการยกเว้นทันทีในกรณีที่อาร์กิวเมนต์หรือลําดับการเรียกเมธอดละเมิดสัญญา API หรือโดยการเรียก error() callback สําหรับปัญหาที่พบในการใช้งานตัวแปลงรหัส หากการเข้ารหัสเสร็จสมบูรณ์ ระบบจะเรียกใช้ output() callback พร้อมส่งข้อมูลโค้ดใหม่ที่เข้ารหัสแล้วเป็นอาร์กิวเมนต์ รายละเอียดสำคัญอีกอย่างหนึ่งคือต้องบอกเฟรมเมื่อไม่ต้องการแล้วโดยการเรียกใช้ close()

let frameCounter = 0;

const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);

const reader = trackProcessor.readable.getReader();
while (true) {
  const result = await reader.read();
  if (result.done) break;

  const frame = result.value;
  if (encoder.encodeQueueSize > 2) {
    // Too many frames in flight, encoder is overwhelmed
    // let's drop this frame.
    frame.close();
  } else {
    frameCounter++;
    const keyFrame = frameCounter % 150 == 0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

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

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }

  // actual bytes of encoded data
  const chunkData = new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);

  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

หากต้องการตรวจสอบว่าคำขอเข้ารหัสที่รอดำเนินการทั้งหมดเสร็จสมบูรณ์แล้ว ให้โทรไปที่ flush() แล้วรอรับคำตอบ

await encoder.flush();

การถอดรหัส

เส้นทางจากเครือข่ายหรือพื้นที่เก็บข้อมูลไปยัง Canvas หรือ ImageBitmap
เส้นทางจากเครือข่ายหรือพื้นที่เก็บข้อมูลไปยัง Canvas หรือ ImageBitmap

การตั้งค่า VideoDecoder คล้ายกับการตั้งค่า VideoEncoder โดยระบบจะส่งผ่านฟังก์ชัน 2 รายการเมื่อสร้างตัวถอดรหัส และส่งพารามิเตอร์ตัวแปลงรหัสไปยัง configure()

ชุดพารามิเตอร์ตัวแปลงรหัสจะแตกต่างกันไปในแต่ละตัวแปลงรหัส เช่น ตัวแปลงรหัส H.264 อาจต้องใช้ Binary Blob ของ AVCC เว้นแต่จะมีการเข้ารหัสในรูปแบบที่เรียกว่า Annex B (encoderConfig.avc = { format: "annexb" })

const init = {
  output: handleFrame,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};

const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder = new VideoDecoder(init);
  decoder.configure(config);
} else {
  // Try another config.
}

เมื่อเริ่มต้นโปรแกรมถอดรหัสแล้ว คุณสามารถเริ่มป้อนออบเจ็กต์ EncodedVideoChunk ให้กับโปรแกรมได้ คุณต้องมีสิ่งต่อไปนี้เพื่อสร้างข้อมูล

  • BufferSource ของข้อมูลวิดีโอที่เข้ารหัส
  • การประทับเวลาเริ่มต้นของข้อมูลโค้ดเป็นไมโครวินาที (เวลาของสื่อของเฟรมที่เข้ารหัสแรกในข้อมูลโค้ด)
  • ประเภทของข้อมูล ซึ่งอาจเป็นอย่างใดอย่างหนึ่งต่อไปนี้
    • key หากสามารถถอดรหัสข้อมูลโค้ดได้โดยไม่ขึ้นอยู่กับข้อมูลโค้ดก่อนหน้า
    • delta หากถอดรหัสข้อมูลได้ก็ต่อเมื่อถอดรหัสข้อมูลก่อนหน้าอย่างน้อย 1 รายการแล้ว

นอกจากนี้ ข้อมูลโค้ดที่ตัวเข้ารหัสส่งออกมาจะพร้อมสําหรับโปรแกรมถอดรหัสโดยทันทีเช่นกัน สิ่งที่กล่าวไว้ข้างต้นเกี่ยวกับการรายงานข้อผิดพลาดและลักษณะการทำงานแบบไม่พร้อมกันของเมธอดตัวเข้ารหัสก็ใช้ได้กับโปรแกรมถอดรหัสด้วยเช่นกัน

const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
  const chunk = new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

ตอนนี้ถึงเวลาแสดงวิธีแสดงเฟรมที่ถอดรหัสใหม่ในหน้าเว็บ คุณควรตรวจสอบว่าการเรียกกลับเอาต์พุตตัวถอดรหัส (handleFrame()) แสดงผลอย่างรวดเร็ว ในตัวอย่างด้านล่าง มีเพียงการเพิ่มเฟรมลงในคิวเฟรมที่พร้อมสำหรับการเรนเดอร์เท่านั้น การแสดงผลจะเกิดขึ้นแยกกันและมี 2 ขั้นตอน ดังนี้

  1. รอเวลาที่เหมาะสมในการแสดงเฟรม
  2. การวาดเฟรมบน Canvas

เมื่อไม่จำเป็นต้องใช้เฟรมอีกต่อไป ให้เรียกใช้ close() เพื่อปล่อยหน่วยความจำพื้นฐานก่อนที่จะถึงมือตัวเก็บขยะ ซึ่งจะช่วยลดปริมาณหน่วยความจำเฉลี่ยที่เว็บแอปพลิเคชันใช้

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime == 0) baseTime = performance.now();
  let mediaTime = performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow = pendingFrames.length == 0;
  if (underflow) return;

  const frame = pendingFrames.shift();

  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r) => {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();

  // Immediately schedule rendering of the next frame
  setTimeout(renderFrame, 0);
}

เคล็ดลับสำหรับนักพัฒนาซอฟต์แวร์

ใช้แผงสื่อในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome เพื่อดูบันทึกสื่อและแก้ไขข้อบกพร่องของ WebCodec

ภาพหน้าจอของแผงสื่อสำหรับการแก้ไขข้อบกพร่อง WebCodecs
แผงสื่อในเครื่องมือสำหรับนักพัฒนาเว็บของ Chrome สำหรับการแก้ไขข้อบกพร่อง WebCodecs

สาธิต

ตัวอย่างด้านล่างแสดงลักษณะของเฟรมภาพเคลื่อนไหวจากผืนผ้าใบ

  • บันทึกที่ 25 fps ลงใน ReadableStream โดย MediaStreamTrackProcessor
  • โอนไปยัง Web Worker
  • เข้ารหัสเป็นรูปแบบวิดีโอ H.264
  • ถอดรหัสอีกครั้งเป็นลำดับเฟรมวิดีโอ
  • และแสดงผลใน Canvas ที่ 2 โดยใช้ transferControlToOffscreen()

การสาธิตอื่นๆ

นอกจากนี้ โปรดดูการสาธิตอื่นๆ ของเราด้วย

การใช้ WebCodecs API

การตรวจหาองค์ประกอบ

วิธีตรวจสอบการรองรับ WebCodecs

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

โปรดทราบว่า WebCodecs API ใช้ได้เฉพาะในบริบทที่ปลอดภัยเท่านั้น ดังนั้นการตรวจหาจะไม่สำเร็จหาก self.isSecureContext เป็นเท็จ

ความคิดเห็น

ทีม Chrome อยากทราบความคิดเห็นของคุณเกี่ยวกับประสบการณ์การใช้งาน WebCodecs API

บอกเราเกี่ยวกับการออกแบบ API

มีสิ่งใดเกี่ยวกับ API ที่ไม่ทำงานตามที่คาดไว้ไหม หรือมีเมธอดหรือพร็อพเพอร์ตี้ที่ขาดหายไปซึ่งคุณต้องนำไปใช้กับไอเดียของคุณ หากมีคำถามหรือความคิดเห็นเกี่ยวกับรูปแบบการรักษาความปลอดภัย แจ้งปัญหาเกี่ยวกับข้อกำหนดในที่เก็บ GitHub ที่เกี่ยวข้อง หรือเพิ่มความคิดเห็นในปัญหาที่มีอยู่

รายงานปัญหาเกี่ยวกับการติดตั้งใช้งาน

หากพบข้อบกพร่องในการใช้งาน Chrome หรือการใช้งานแตกต่างจากข้อกําหนดหรือไม่ รายงานข้อบกพร่องที่ new.crbug.com โปรดระบุรายละเอียดให้มากที่สุดเท่าที่จะเป็นไปได้ รวมถึงวิธีการง่ายๆ ในการจำลองข้อบกพร่อง และป้อน Blink>Media>WebCodecs ในช่องคอมโพเนนต์ Glitch เหมาะอย่างยิ่งสำหรับการแชร์การจำลองข้อบกพร่องที่รวดเร็วและง่ายดาย

แสดงการสนับสนุน API

คุณกำลังวางแผนที่จะใช้ WebCodecs API ใช่ไหม การสนับสนุนแบบสาธารณะของคุณจะช่วยให้ทีม Chrome จัดลําดับความสําคัญของฟีเจอร์ต่างๆ และแสดงให้เห็นว่าการสนับสนุนฟีเจอร์เหล่านี้สำคัญกับผู้ให้บริการเบราว์เซอร์รายอื่นๆ เพียงใด

ส่งอีเมลไปที่ media-dev@chromium.org หรือทวีตไปที่ @ChromiumDev โดยใช้แฮชแท็ก #WebCodecs และแจ้งให้เราทราบว่าคุณใช้ฟีเจอร์นี้ที่ไหนและอย่างไร

รูปภาพหลักโดย Denise Jans ใน Unsplash