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