การจัดการคอมโพเนนต์สตรีมวิดีโอ
เทคโนโลยีเว็บสมัยใหม่มีวิธีมากมายในการจัดการวิดีโอ 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 ออกแบบมาเพื่อทำงานหนักทั้งหมดแบบไม่พร้อมกันและไม่ได้อยู่ในเธรดหลัก แต่เนื่องจากมักเรียกใช้ Callback ที่เป็นเฟรมและชิ้นส่วนซ้ำได้หลายครั้งต่อวินาที จึงอาจทำให้เทรดหลักรกและส่งผลให้เว็บไซต์ตอบสนองน้อยลง ดังนั้นจึงควรย้ายการจัดการแต่ละเฟรมและกลุ่มที่เข้ารหัสไปยัง Web Worker
ReadableStream จะช่วยแก้ปัญหานี้ด้วยวิธีง่ายๆ ในการโอนเฟรมทั้งหมดที่มาจากแทร็กสื่อไปยังผู้ปฏิบัติงานโดยอัตโนมัติ เช่น MediaStreamTrackProcessor
สามารถใช้รับ ReadableStream
สำหรับแทร็กสตรีมสื่อที่มาจากเว็บแคม หลังจากนั้น ระบบจะโอนสตรีมไปยังเว็บเวิร์กเกอร์ ซึ่งจะอ่านเฟรมทีละเฟรมและจัดคิวเป็น VideoEncoder
HTMLCanvasElement.transferControlToOffscreen
ช่วยให้เรนเดอร์ได้แม้ไม่ได้อยู่ในเธรดหลัก แต่หากเครื่องมือระดับสูงทั้งหมดไม่สะดวก VideoFrame
เองก็โอนได้และอาจย้ายไปมาระหว่างผู้ปฏิบัติงาน
การทำงานของตัวแปลงรหัสเว็บ
การเข้ารหัส
ทุกอย่างเริ่มต้นที่ 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()
ด้วย config เพื่อตรวจสอบล่วงหน้าว่าระบบรองรับ config หรือไม่ และรอให้ 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 หรือโดยการเรียก 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();
กำลังถอดรหัส
การตั้งค่า 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();
ตอนนี้ได้เวลาแสดงวิธีแสดงเฟรมที่ถอดรหัสใหม่ในหน้าเว็บแล้ว ทางที่ดีคุณควรตรวจสอบว่า Callback เอาต์พุตตัวถอดรหัส (handleFrame()
)
แสดงผลอย่างรวดเร็ว ในตัวอย่างด้านล่าง มีเพียงการเพิ่มเฟรมลงในคิวเฟรมที่พร้อมสำหรับการเรนเดอร์เท่านั้น
การแสดงผลจะเกิดขึ้นแยกกันและมี 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 เพื่อดูบันทึกสื่อและแก้ไขข้อบกพร่องของ 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
และแจ้งให้เราทราบว่าคุณใช้ฟีเจอร์นี้ที่ไหนและอย่างไร
รูปภาพหลักโดย Denise Jans ใน Unsplash