การประมวลผลวิดีโอด้วย 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 ออกแบบมาให้รับภาระการทำงานทั้งหมดแบบไม่พร้อมกันและนอกเทรดหลัก แต่เนื่องจาก Frame และ chunk Callback มักมีชื่อเรียกได้หลายครั้งต่อวินาที อาจทำให้เทรดหลักรกรุงรัง และทำให้เว็บไซต์ตอบสนองน้อยลง ดังนั้นจึงควรย้ายการจัดการแต่ละเฟรมและกลุ่มที่เข้ารหัสไปไว้ใน Web Worker

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

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

การทำงานของตัวแปลงรหัสเว็บ

การเข้ารหัส

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

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

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

    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 อาจต้องใช้ 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();

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

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

เมื่อไม่จำเป็นต้องใช้เฟรมแล้ว ให้โทรหา 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 DevTools เพื่อดูบันทึกสื่อและแก้ไขข้อบกพร่องของ WebCodecs

วันที่ ภาพหน้าจอของแผงสื่อสำหรับการแก้ไขข้อบกพร่อง WebCodecs
แผงสื่อใน Chrome DevTools สำหรับการแก้ไขข้อบกพร่องของ WebCodecs

สาธิต

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

  • จับภาพที่ 25 FPS ลงใน ReadableStream ภายในวันที่ MediaStreamTrackProcessor
  • โอนไปยัง Web Worker
  • เข้ารหัสเป็นรูปแบบวิดีโอ H.264
  • ถูกถอดรหัสอีกครั้งเป็นลำดับเฟรมวิดีโอ
  • และแสดงผลบนผืนผ้าใบที่ 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 และแจ้งให้เราทราบถึงตำแหน่งและวิธีที่คุณใช้งาน

รูปภาพหลักโดย เดนิส แจนส์ บน UnSplash