การจัดการคอมโพเนนต์สตรีมวิดีโอ
เทคโนโลยีเว็บสมัยใหม่มีวิธีมากมายในการทำงานกับวิดีโอ 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()
ได้ นอกจากนี้ยังสร้างจาก Canvas, บิตแมป, องค์ประกอบวิดีโอ และเฟรมวิดีโออื่นๆ ได้ด้วย
API ของ WebCodecs ทำงานควบคู่กับคลาสจาก Insertable Streams API ได้เป็นอย่างดี ซึ่งเชื่อมต่อ WebCodecs กับแทร็กสตรีมสื่อ
MediaStreamTrackProcessor
แบ่งแทร็กสื่อออกเป็นเฟรมแต่ละเฟรมMediaStreamTrackGenerator
สร้างแทร็กสื่อจากสตรีมของเฟรม
WebCodecs และ Web Workers
WebCodecs API ได้รับการออกแบบมาให้ทำงานหนักทั้งหมดแบบไม่พร้อมกันและนอกเทรดหลัก แต่เนื่องจากมักจะมีการเรียกใช้แฮนเดิลการเรียกกลับของเฟรมและก้อนข้อมูลหลายครั้งต่อวินาที จึงอาจทำให้เธรดหลักรกและทำให้เว็บไซต์ตอบสนองน้อยลง ดังนั้นจึงควรย้ายการจัดการเฟรมแต่ละเฟรมและก้อนข้อมูลที่เข้ารหัสไปยัง Web Worker
ReadableStream
มีวิธีที่สะดวกในการโอนเฟรมทั้งหมดจากแทร็กสื่อไปยัง Worker โดยอัตโนมัติ เช่น MediaStreamTrackProcessor
สามารถใช้เพื่อรับ ReadableStream
สำหรับแทร็กสตรีมสื่อที่มาจากเว็บแคม หลังจากนั้น
ระบบจะโอนสตรีมไปยัง Web Worker ซึ่งจะอ่านเฟรมทีละเฟรมและจัดคิว
ลงใน VideoEncoder
HTMLCanvasElement.transferControlToOffscreen
ช่วยให้คุณทำการแสดงผลนอกชุดข้อความหลักได้ แต่หากเครื่องมือระดับสูงทั้งหมดไม่สะดวก VideoFrame
เองก็โอนได้และอาจย้ายระหว่างผู้ปฏิบัติงานได้
การใช้งาน WebCodecs
การเข้ารหัส

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
การตั้งค่า 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
ของข้อมูลวิดีโอที่เข้ารหัส- การประทับเวลาเริ่มต้นของก้อนข้อมูลเป็นไมโครวินาที (เวลาสื่อของเฟรมแรกที่เข้ารหัสในก้อนข้อมูล)
- ประเภทของ Chunk ซึ่งเป็นหนึ่งในประเภทต่อไปนี้
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 ขั้นตอน ดังนี้
- รอเวลาที่เหมาะสมเพื่อแสดงเฟรม
- วาดเฟรมบน 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 เพื่อดูบันทึกสื่อและแก้ไขข้อบกพร่องของ WebCodecs

สาธิต
การสาธิตแสดงวิธีที่เฟรมภาพเคลื่อนไหวจาก Canvas มีลักษณะดังนี้
- บันทึกที่ 25 เฟรมต่อวินาทีลงใน
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
ในช่องคอมโพเนนต์
แสดงการสนับสนุน API
คุณวางแผนที่จะใช้ WebCodecs API ไหม การสนับสนุนแบบสาธารณะของคุณจะช่วยให้ ทีม Chrome จัดลําดับความสําคัญของฟีเจอร์และแสดงให้ผู้ให้บริการเบราว์เซอร์รายอื่นๆ เห็นว่าการสนับสนุนฟีเจอร์เหล่านี้มีความสําคัญเพียงใด
ส่งอีเมลไปที่ media-dev@chromium.org หรือทวีตถึง @ChromiumDev โดยใช้แฮชแท็ก
#WebCodecs
และแจ้งให้เราทราบว่าคุณใช้ฟีเจอร์นี้ที่ใดและอย่างไร
รูปภาพหลักโดย Denise Jans ใน Unsplash