Thao tác với các thành phần luồng video.
Các công nghệ web hiện đại cung cấp nhiều cách để xử lý video. Media Stream API, Media Recording API, Media Source API và WebRTC API tạo nên một bộ công cụ phong phú để ghi lại, chuyển và phát luồng video. Mặc dù giải quyết một số tác vụ cấp cao nhất định, nhưng các API này không cho phép lập trình viên web làm việc với các thành phần riêng lẻ của luồng video, chẳng hạn như khung hình và các đoạn video hoặc âm thanh đã mã hoá chưa được kết hợp. Để có quyền truy cập cấp thấp vào các thành phần cơ bản này, nhà phát triển đã sử dụng WebAssembly để đưa các bộ mã hoá và giải mã video và âm thanh vào trình duyệt. Tuy nhiên, do các trình duyệt hiện đại đã đi kèm với nhiều bộ mã hoá và giải mã (thường được phần cứng tăng tốc), việc đóng gói lại các bộ mã hoá và giải mã đó dưới dạng WebAssembly có vẻ như lãng phí tài nguyên máy tính và con người.
WebCodecs API loại bỏ sự kém hiệu quả này bằng cách cung cấp cho lập trình viên một cách để sử dụng các thành phần đa phương tiện đã có trong trình duyệt. Cụ thể:
- Bộ giải mã video và âm thanh
- Bộ mã hoá video và âm thanh
- Khung hình video thô
- Trình giải mã hình ảnh
API WebCodecs hữu ích cho các ứng dụng web yêu cầu toàn quyền kiểm soát cách xử lý nội dung đa phương tiện, chẳng hạn như trình chỉnh sửa video, hội nghị truyền hình, truyền trực tuyến video, v.v.
Quy trình xử lý video
Khung hình là trung tâm của quá trình xử lý video. Do đó, trong WebCodecs, hầu hết các lớp đều tiêu thụ hoặc tạo khung. Bộ mã hoá video chuyển đổi các khung hình thành các khối đã mã hoá. Bộ giải mã video thực hiện thao tác ngược lại.
Ngoài ra, VideoFrame
hoạt động tốt với các API Web khác bằng cách là một CanvasImageSource
và có một hàm khởi tạo chấp nhận CanvasImageSource
.
Vì vậy, bạn có thể sử dụng hàm này trong các hàm như drawImage()
vàtexImage2D()
. Ngoài ra, bạn có thể tạo hình ảnh này từ canvas, bitmap, thành phần video và các khung hình video khác.
WebCodecs API hoạt động hiệu quả cùng với các lớp từ Insertable Streams API (API Luồng có thể chèn) giúp kết nối WebCodecs với các kênh truyền phát nội dung đa phương tiện.
MediaStreamTrackProcessor
chia các kênh nội dung nghe nhìn thành các khung riêng lẻ.MediaStreamTrackGenerator
tạo một kênh nội dung đa phương tiện từ một luồng khung hình.
WebCodecs và worker web
Theo thiết kế, WebCodecs API thực hiện tất cả các tác vụ nặng một cách không đồng bộ và ngoài luồng chính. Tuy nhiên, vì các lệnh gọi lại khung và đoạn thường có thể được gọi nhiều lần trong một giây, nên các lệnh gọi lại này có thể làm lộn xộn luồng chính và do đó làm cho trang web kém phản hồi hơn. Do đó, bạn nên chuyển việc xử lý từng khung và các đoạn đã mã hoá vào một worker web.
Để giúp giải quyết vấn đề đó, ReadableStream cung cấp một cách thuận tiện để tự động chuyển tất cả các khung hình từ một kênh nội dung nghe nhìn đến worker. Ví dụ: bạn có thể dùng MediaStreamTrackProcessor
để lấy ReadableStream
cho một kênh truyền phát nội dung đa phương tiện đến từ webcam. Sau đó, luồng sẽ được chuyển sang một worker web, trong đó các khung được đọc lần lượt và xếp hàng vào VideoEncoder
.
Với HTMLCanvasElement.transferControlToOffscreen
, ngay cả việc kết xuất cũng có thể được thực hiện ngoài luồng chính. Tuy nhiên, nếu tất cả các công cụ cấp cao đều không thuận tiện, thì bản thân VideoFrame
có thể chuyển và có thể được di chuyển giữa các worker.
WebCodecs trong thực tế
Mã hoá
Tất cả đều bắt đầu bằng VideoFrame
.
Có 3 cách để tạo khung hình video.
Từ một nguồn hình ảnh như canvas, bitmap hình ảnh hoặc phần tử video.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Sử dụng
MediaStreamTrackProcessor
để lấy khung từ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; }
Tạo một khung từ bản trình bày pixel nhị phân trong
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);
Bất kể nguồn gốc của khung hình là gì, bạn đều có thể mã hoá các khung hình đó thành đối tượng EncodedVideoChunk
bằng VideoEncoder
.
Trước khi mã hoá, VideoEncoder
cần được cung cấp hai đối tượng JavaScript:
- Khởi tạo từ điển bằng hai hàm để xử lý các khối và lỗi đã mã hoá. Các hàm này do nhà phát triển xác định và không thể thay đổi sau khi được truyền vào hàm khởi tạo
VideoEncoder
. - Đối tượng cấu hình bộ mã hoá, chứa các thông số cho luồng video đầu ra. Bạn có thể thay đổi các tham số này sau bằng cách gọi
configure()
.
Phương thức configure()
sẽ gửi NotSupportedError
nếu trình duyệt không hỗ trợ cấu hình. Bạn nên gọi phương thức tĩnh VideoEncoder.isConfigSupported()
bằng cấu hình để kiểm tra trước xem cấu hình có được hỗ trợ hay không và chờ lời hứa của cấu hình đó.
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.
}
Sau khi thiết lập, bộ mã hoá sẽ sẵn sàng chấp nhận các khung hình thông qua phương thức encode()
.
Cả configure()
và encode()
đều trả về ngay lập tức mà không cần chờ công việc thực tế hoàn tất. Phương thức này cho phép một số khung hình xếp hàng để mã hoá cùng một lúc, trong khi encodeQueueSize
cho biết số lượng yêu cầu đang chờ trong hàng đợi để các quá trình mã hoá trước đó hoàn tất.
Lỗi được báo cáo bằng cách gửi ngay một ngoại lệ, trong trường hợp các đối số hoặc thứ tự gọi phương thức vi phạm hợp đồng API, hoặc bằng cách gọi lệnh gọi lại error()
cho các vấn đề gặp phải trong quá trình triển khai bộ mã hoá và giải mã.
Nếu quá trình mã hoá hoàn tất thành công, lệnh gọi lại output()
sẽ được gọi với một đoạn mã hoá mới làm đối số.
Một chi tiết quan trọng khác ở đây là bạn cần cho các khung biết khi nào không cần đến chúng nữa bằng cách gọi 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();
}
}
Cuối cùng, đã đến lúc hoàn tất mã mã hoá bằng cách viết một hàm xử lý các đoạn video đã mã hoá khi chúng xuất ra từ bộ mã hoá. Thông thường, hàm này sẽ gửi các đoạn dữ liệu qua mạng hoặc kết hợp các đoạn dữ liệu đó vào một vùng chứa nội dung đa phương tiện để lưu trữ.
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,
});
}
Nếu tại một thời điểm nào đó, bạn cần đảm bảo rằng tất cả các yêu cầu mã hoá đang chờ xử lý đã hoàn tất, bạn có thể gọi flush()
và đợi lời hứa của phương thức này.
await encoder.flush();
Giải mã
Việc thiết lập VideoDecoder
tương tự như những gì đã thực hiện cho VideoEncoder
: hai hàm được truyền khi tạo bộ giải mã và các tham số bộ mã hoá và giải mã được cung cấp cho configure()
.
Bộ tham số bộ mã hoá và giải mã thay đổi tuỳ theo bộ mã hoá và giải mã. Ví dụ: bộ mã hoá và giải mã H.264 có thể cần một blob nhị phân của AVCC, trừ phi được mã hoá ở định dạng Phụ lục 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.
}
Sau khi khởi chạy bộ giải mã, bạn có thể bắt đầu cung cấp cho bộ giải mã các đối tượng EncodedVideoChunk
.
Để tạo một đoạn, bạn cần có:
BufferSource
chứa dữ liệu video đã mã hoá- dấu thời gian bắt đầu của đoạn dữ liệu tính bằng micrô giây (thời gian nội dung nghe nhìn của khung hình được mã hoá đầu tiên trong đoạn dữ liệu)
- loại của đoạn, một trong các loại sau:
key
nếu có thể giải mã đoạn này độc lập với các đoạn trước đódelta
nếu bạn chỉ có thể giải mã đoạn này sau khi giải mã một hoặc nhiều đoạn trước đó
Ngoài ra, mọi đoạn do bộ mã hoá phát ra đều sẵn sàng cho bộ giải mã. Tất cả những điều đã nói ở trên về việc báo cáo lỗi và bản chất không đồng bộ của các phương thức của bộ mã hoá cũng đúng với bộ giải mã.
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();
Bây giờ, hãy xem cách hiển thị một khung hình mới giải mã trên trang. Bạn nên đảm bảo rằng lệnh gọi lại đầu ra của bộ giải mã (handleFrame()
) sẽ nhanh chóng trả về. Trong ví dụ bên dưới, lớp này chỉ thêm một khung vào hàng đợi khung hình sẵn sàng kết xuất.
Quá trình kết xuất diễn ra riêng biệt và bao gồm hai bước:
- Chờ thời điểm thích hợp để hiển thị khung.
- Vẽ khung trên canvas.
Khi không cần đến một khung nữa, hãy gọi close()
để giải phóng bộ nhớ cơ bản trước khi trình thu gom rác xử lý khung đó. Việc này sẽ làm giảm mức sử dụng bộ nhớ trung bình của ứng dụng web.
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);
}
Mẹo dành cho nhà phát triển
Sử dụng Media Panel (Bảng điều khiển nội dung đa phương tiện) trong Công cụ của Chrome cho nhà phát triển để xem nhật ký nội dung đa phương tiện và gỡ lỗi WebCodec.
Bản minh hoạ
Bản minh hoạ dưới đây cho thấy các khung ảnh động từ canvas:
- được
MediaStreamTrackProcessor
quay ở tốc độ 25 khung hình/giây vàoReadableStream
- được chuyển sang một worker web
- được mã hoá thành định dạng video H.264
- được giải mã lại thành một trình tự khung hình video
- và kết xuất trên canvas thứ hai bằng
transferControlToOffscreen()
Các bản minh hoạ khác
Ngoài ra, hãy xem các bản minh hoạ khác của chúng tôi:
Sử dụng WebCodecs API
Phát hiện tính năng
Cách kiểm tra xem WebCodecs có được hỗ trợ hay không:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Xin lưu ý rằng WebCodecs API chỉ có trong ngữ cảnh an toàn, vì vậy, quá trình phát hiện sẽ không thành công nếu self.isSecureContext
là sai.
Phản hồi
Nhóm Chrome muốn biết trải nghiệm của bạn với WebCodecs API.
Giới thiệu cho chúng tôi về thiết kế API
API có hoạt động như mong đợi không? Hay có phương thức hoặc thuộc tính nào bị thiếu mà bạn cần để triển khai ý tưởng của mình không? Bạn có câu hỏi hoặc nhận xét về mô hình bảo mật không? Gửi vấn đề về thông số kỹ thuật trên kho lưu trữ GitHub tương ứng hoặc thêm ý kiến của bạn vào một vấn đề hiện có.
Báo cáo vấn đề về việc triển khai
Bạn có phát hiện lỗi khi triển khai Chrome không? Hay cách triển khai khác với thông số kỹ thuật? Gửi lỗi tại new.crbug.com. Hãy nhớ cung cấp càng nhiều thông tin chi tiết càng tốt, hướng dẫn đơn giản để tái hiện lỗi và nhập Blink>Media>WebCodecs
vào hộp Components (Thành phần).
Glitch rất hữu ích để chia sẻ các bản tái hiện nhanh chóng và dễ dàng.
Hỗ trợ API
Bạn có dự định sử dụng API WebCodecs không? Sự ủng hộ công khai của bạn giúp nhóm Chrome ưu tiên các tính năng và cho các nhà cung cấp trình duyệt khác thấy tầm quan trọng của việc hỗ trợ các tính năng đó.
Hãy gửi email đến media-dev@chromium.org hoặc tweet đến @ChromiumDev bằng hashtag #WebCodecs
để cho chúng tôi biết bạn đang sử dụng ở đâu và như thế nào.
Hình ảnh chính của Denise Jans trên Unsplash.