การจัดการคอมโพเนนต์ของสตรีมวิดีโอ
เทคโนโลยีเว็บสมัยใหม่มีวิธีมากมายในการทำงานกับวิดีโอ API ของ Media Stream, Media Recording API, Media Source API, และ WebRTC API รวมกัน เป็นชุดเครื่องมือที่สมบูรณ์แบบสำหรับการบันทึก โอน และเล่นสตรีมวิดีโอ แม้ว่า API เหล่านี้จะช่วยแก้ปัญหาบางอย่างในระดับสูงได้ แต่ก็ไม่ได้ช่วยให้นักเขียนโปรแกรมเว็บทำงานกับคอมโพเนนต์แต่ละรายการของสตรีมวิดีโอ เช่น เฟรมและ Chunk ที่ไม่ได้แยกสัญญาณ (Unmuxed) ของวิดีโอหรือเสียงที่เข้ารหัส นักพัฒนาแอปจึงใช้ WebAssembly เพื่อนำตัวแปลงรหัสวิดีโอและเสียงมาไว้ในเบราว์เซอร์เพื่อให้เข้าถึงคอมโพเนนต์พื้นฐานเหล่านี้ได้ในระดับต่ำ แต่เนื่องจากเบราว์เซอร์สมัยใหม่มีตัวแปลงรหัสหลากหลายแบบ (ซึ่งมักจะเร่งความเร็วด้วยฮาร์ดแวร์) การบรรจุตัวแปลงรหัสเหล่านั้นใหม่เป็น WebAssembly จึงดูเหมือนเป็นการสิ้นเปลืองทรัพยากรของมนุษย์และคอมพิวเตอร์
WebCodecs API ช่วยลดความไม่มีประสิทธิภาพนี้ ด้วยการให้นักเขียนโปรแกรมใช้องค์ประกอบสื่อที่มีอยู่ใน เบราว์เซอร์อยู่แล้ว โดยเฉพาะอย่างยิ่ง
- ตัวถอดรหัสวิดีโอและเสียง
- ตัวเข้ารหัสวิดีโอและเสียง
- เฟรมวิดีโอดิบ
- ตัวถอดรหัสรูปภาพ
WebCodecs API มีประโยชน์สำหรับเว็บแอปพลิเคชันที่ต้องควบคุมวิธีประมวลผลเนื้อหาสื่ออย่างเต็มรูปแบบ เช่น โปรแกรมตัดต่อวิดีโอ การประชุมทางวิดีโอ การสตรีมวิดีโอ เป็นต้น
เวิร์กโฟลว์การประมวลผลวิดีโอ
เฟรมเป็นหัวใจสำคัญในการประมวลผลวิดีโอ ดังนั้นใน WebCodecs คลาสส่วนใหญ่จึงใช้หรือสร้างเฟรม ตัวเข้ารหัสวิดีโอจะแปลงเฟรมเป็น Chunk ที่เข้ารหัส ส่วนตัวถอดรหัสวิดีโอจะทำในทางตรงกันข้าม
นอกจากนี้ VideoFrame ยังทำงานร่วมกับ Web API อื่นๆ ได้อย่างราบรื่นด้วยการเป็น CanvasImageSource และมี เครื่องมือสร้าง ที่ยอมรับ CanvasImageSource
จึงสามารถใช้ในฟังก์ชันต่างๆ เช่น drawImage() และtexImage2D() รวมถึงสร้างจาก Canvas, บิตแมป, องค์ประกอบวิดีโอ และเฟรมวิดีโออื่นๆ ได้ด้วย
WebCodecs API ทำงานร่วมกับคลาสจาก Insertable Streams API ได้อย่างราบรื่น ซึ่งจะเชื่อมต่อ WebCodecs กับแทร็กสตรีมสื่อ
MediaStreamTrackProcessorจะแยกแทร็กสื่อออกเป็นเฟรมแต่ละเฟรมMediaStreamTrackGeneratorจะสร้างแทร็กสื่อจากสตรีมเฟรม
WebCodecs และ Web Worker
WebCodecs API ได้รับการออกแบบมาให้ทำงานหนักทั้งหมดแบบไม่พร้อมกันและออกจากชุดข้อความหลัก แต่เนื่องจากระบบอาจเรียกใช้เฟรมและ Chunk Callback หลายครั้งต่อวินาที จึงอาจทำให้ชุดข้อความหลักทำงานหนักและทำให้เว็บไซต์ตอบสนองช้าลง ดังนั้นจึงควรย้ายการจัดการเฟรมแต่ละเฟรมและ Chunk ที่เข้ารหัสไปยัง Web Worker
เพื่อช่วยในเรื่องนั้น ReadableStream
มีวิธีที่สะดวกในการโอนเฟรมทั้งหมดจากแทร็กสื่อ
ไปยัง Worker โดยอัตโนมัติ ตัวอย่างเช่น คุณสามารถใช้ MediaStreamTrackProcessor เพื่อรับ ReadableStream สำหรับแทร็กสตรีมสื่อจากเว็บแคม หลังจากนั้น ระบบจะโอนสตรีมไปยัง Web Worker ซึ่งจะอ่านเฟรมทีละเฟรมและจัดคิวลงใน VideoEncoder
With HTMLCanvasElement.transferControlToOffscreen ช่วยให้แสดงผลนอกชุดข้อความหลักได้ด้วย แต่หากเครื่องมือระดับสูงทั้งหมดไม่สะดวก VideoFrame เองก็ โอนได้ และอาจย้ายระหว่าง Worker ได้
WebCodecs ในการทำงานจริง
การเข้ารหัส
Canvas หรือ ImageBitmap ไปยังเครือข่ายหรือพื้นที่เก็บข้อมูลทุกอย่างเริ่มต้นด้วย VideoFrame
โดยคุณสร้างเฟรมวิดีโอได้ 3 วิธีดังนี้
จากแหล่งที่มาของรูปภาพ เช่น Canvas, บิตแมปรูปภาพ หรือองค์ประกอบวิดีโอ
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });ใช้
MediaStreamTrackProcessorเพื่อดึงเฟ1รมจากMediaStreamTrackconst 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; }สร้างเฟรมจากการแสดงพิกเซลแบบไบนารีใน
BufferSourceconst 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 รายการดังนี้
- พจนานุกรม Init ที่มี 2 ฟังก์ชันสำหรับจัดการ Chunk ที่เข้ารหัสและข้อผิดพลาด ฟังก์ชันเหล่านี้กำหนดโดยนักพัฒนาแอปและจะเปลี่ยนแปลงไม่ได้หลังจากส่งไปยังตัวสร้าง
VideoEncoder - ออบเจ็กต์การกำหนดค่าตัวเข้ารหัส ซึ่งมีพารามิเตอร์สำหรับสตรีมวิดีโอเอาต์พุต คุณเปลี่ยนพารามิเตอร์เหล่านี้ได้ในภายหลังโดยเรียกใช้
configure()
เมธอด configure() จะแสดง NotSupportedError หากเบราว์เซอร์ไม่รองรับการกำหนดค่า เราขอแนะนำให้คุณเรียกใช้เมธอดแบบคงที่ VideoEncoder.isConfigSupported() พร้อมการกำหนดค่าเพื่อตรวจสอบล่วงหน้าว่าเบราว์เซอร์รองรับการกำหนดค่าหรือไม่ และรอ Promise ของเมธอด
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 หรือโดยการเรียกใช้ Callback error() สำหรับปัญหาที่พบในการติดตั้งใช้งานตัวแปลงรหัส
หากการเข้ารหัสเสร็จสมบูรณ์ Callback output() จะถูกเรียกใช้โดยมี Chunk ที่เข้ารหัสใหม่เป็นอาร์กิวเมนต์
รายละเอียดที่สำคัญอีกอย่างคือ คุณต้องแจ้งให้เฟรมทราบเมื่อไม่จำเป็นต้องใช้เฟรมอีกต่อไปโดยเรียกใช้ 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();
}
}
ในที่สุดก็ถึงเวลาเขียนโค้ดการเข้ารหัสให้เสร็จสมบูรณ์โดยเขียนฟังก์ชันที่จัดการ Chunk ของวิดีโอที่เข้ารหัสเมื่อ Chunk ออกจากตัวเข้ารหัส โดยปกติฟังก์ชันนี้จะส่ง Chunk ข้อมูลผ่านเครือข่ายหรือรวม Chunk เหล่านั้นลงในคอนเทนเนอร์สื่อ เพื่อจัดเก็บ
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() และรอ Promise ของคำขอได้
await encoder.flush();
การถอดรหัส
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 ให้ตัวถอดรหัสได้
โดยคุณต้องมีสิ่งต่อไปนี้เพื่อสร้าง Chunk
- A
BufferSourceของข้อมูลวิดีโอที่เข้ารหัส - การประทับเวลาเริ่มต้นของ Chunk เป็นไมโครวินาที (เวลาสื่อของเฟรมแรกที่เข้ารหัสใน Chunk)
- ประเภทของ Chunk ซึ่งมี 1 ในประเภทต่อไปนี้
keyหากถอดรหัส Chunk ได้โดยไม่ขึ้นอยู่กับ Chunk ก่อนหน้าdeltaหากถอดรหัส Chunk ได้หลังจากถอดรหัส Chunk ก่อนหน้าอย่างน้อย 1 Chunk แล้วเท่านั้น
นอกจากนี้ Chunk ที่ตัวเข้ารหัสปล่อยออกมายังพร้อมสำหรับตัวถอดรหัสด้วยเช่นกัน สิ่งที่กล่าวไว้ข้างต้นเกี่ยวกับการรายงานข้อผิดพลาดและลักษณะการทำงานแบบไม่พร้อมกันของเมธอดตัวเข้ารหัสก็ใช้ได้กับตัวถอดรหัสด้วยเช่นกัน
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()) แสดงผลอย่างรวดเร็ว ในตัวอย่างด้านล่าง Callback จะเพิ่มเฟรมลงในคิวเฟรมที่พร้อมสำหรับการแสดงผลเท่านั้น
การแสดงผลจะเกิดขึ้นแยกกันและมี 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 มีลักษณะดังนี้
- บันทึกที่ 25fps ลงใน
ReadableStreamโดยMediaStreamTrackProcessor - โอนไปยัง Web Worker
- เข้ารหัสเป็นรูปแบบวิดีโอ H.264
- ถอดรหัสอีกครั้งเป็นลำดับเฟรมวิดีโอ
- และแสดงผลบน Canvas ที่ 2 โดยใช้
transferControlToOffscreen()
การสาธิตอื่นๆ
ลองดูการสาธิตอื่นๆ ของเราด้วย
การใช้ WebCodecs API
การตรวจหาฟีเจอร์
วิธีตรวจสอบการรองรับ WebCodecs
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
โปรดทราบว่า WebCodecs API พร้อมใช้งานในบริบทที่ปลอดภัยเท่านั้น
ดังนั้นการตรวจหาจะล้มเหลวหาก self.isSecureContext เป็นเท็จ
ดูข้อมูลเพิ่มเติม
หากคุณเพิ่งเริ่มใช้ WebCodecs โปรดดูบทความเชิงลึกพร้อมตัวอย่างมากมายในข้อมูลเบื้องต้นเกี่ยวกับ WebCodecs เพื่อเรียนรู้เพิ่มเติม
ความคิดเห็น
ทีม Chrome อยากทราบความคิดเห็นของคุณเกี่ยวกับประสบการณ์การใช้งาน WebCodecs API
บอกเราเกี่ยวกับการออกแบบ API
มีบางอย่างเกี่ยวกับ API ที่ทำงานไม่เป็นไปตามที่คุณคาดไว้ไหม หรือมีเมธอดหรือพร็อพเพอร์ตี้ที่ขาดหายไปซึ่งคุณต้องใช้เพื่อนำแนวคิดไปใช้ไหม มีคำถามหรือความคิดเห็นเกี่ยวกับโมเดลความปลอดภัยไหม โปรดแจ้งปัญหาเกี่ยวกับข้อกำหนดใน ที่เก็บ GitHub ที่เกี่ยวข้อง หรือเพิ่ม ความคิดเห็นของคุณลงในปัญหาที่มีอยู่
รายงานปัญหาเกี่ยวกับการติดตั้งใช้งาน
คุณพบข้อบกพร่องในการติดตั้งใช้งานของ Chrome ไหม หรือการติดตั้งใช้งานแตกต่างจากข้อกำหนดไหม โปรดแจ้งข้อบกพร่องที่ new.crbug.com
อย่าลืมใส่รายละเอียดให้มากที่สุดเท่าที่จะทำได้ คำแนะนำง่ายๆ สำหรับการ
ทำซ้ำ และป้อน Blink>Media>WebCodecs ในช่องคอมโพเนนต์
แสดงการสนับสนุน API
คุณวางแผนที่จะใช้ WebCodecs API ไหม การสนับสนุนจากสาธารณะจะช่วยให้ทีม Chrome จัดลำดับความสำคัญของฟีเจอร์ได้ และแสดงให้ผู้ให้บริการเบราว์เซอร์รายอื่นๆ เห็นว่าการรองรับฟีเจอร์เหล่านี้มีความสำคัญเพียงใด
ส่งอีเมลไปที่ media-dev@chromium.org หรือส่งทวีต
ไปที่ @ChromiumDev โดยใช้แฮชแท็ก
#WebCodecs
และแจ้งให้เราทราบว่าคุณใช้ฟีเจอร์นี้ที่ใดและอย่างไร