Sửa đổi các thành phần của luồng video.
Công nghệ web hiện đại cung cấp nhiều cách thức để làm việc với video. API Luồng truyền thông, API Ghi nội dung nghe nhìn, API Nguồn nội dung nghe nhìn và API WebRTC bổ sung vào một bộ công cụ đa dạng để ghi, truyền và phát luồng video. Trong khi giải quyết một số tác vụ cấp cao, các API này không cho phép trình lập trình 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ư các khung hình và các đoạn video hoặc âm thanh chưa mã hoá. Để có quyền truy cập cấp thấp vào các thành phần cơ bản này, các nhà phát triển đã sử dụng WebAssembly để đưa 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 đã cung cấp nhiều bộ mã hoá và giải mã (thường được tăng tốc bằng phần cứng) nên việc đóng gói lại chúng dưới dạng WebAssembly có vẻ lãng phí tài nguyên của con người và máy tính.
WebCodecs API loại bỏ tình trạng kém hiệu quả này bằng cách cung cấp cho các lập trình viên cách sử dụng các thành phần nội dung nghe nhì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ô
- Bộ giải mã hình ảnh
WebCodecs API 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 nghe nhìn, chẳng hạn như trình chỉnh sửa video, hội nghị truyền hình, phát trực tuyến video, v.v.
Quy trình xử lý video
Khung là tâm điểm trong quá trình xử lý video. Do đó, trong WebCodecs, hầu hết các lớp đều sử dụng hoặc tạo khung. Bộ mã hoá video sẽ chuyển đổi khung hình thành các đoạn được mã hoá. Bộ giải mã video làm 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 trở thành CanvasImageSource
và có một hàm khởi tạo chấp nhận CanvasImageSource
.
Do đó, bạn có thể dùng các hàm này trong các hàm như drawImage()
và texImage2D()
. Ngoài ra, hình ảnh cũng có thể được tạo từ canvas, bitmap, các thành phần video và các khung video khác.
API WebCodecs hoạt động tốt song song với các lớp trong API luồng có thể chèn, giúp kết nối WebCodecs với các bản luồng nội dung đa phương tiện.
MediaStreamTrackProcessor
chia bản nhạc đa phương tiện thành từng khung hình riêng lẻ.MediaStreamTrackGenerator
tạo một bản nhạc đa phương tiện từ một luồng khung.
WebCodec và trình thực thi web
Theo thiết kế, API WebCodecs thực hiện mọi công việc khó khăn một cách không đồng bộ và ra khỏi luồng chính. Tuy nhiên, vì các lệnh gọi lại khung và phân đoạn thường có thể được gọi nhiều lần trong một giây, nên chúng có thể làm lộn xộn luồng chính, khiến trang web phản hồi kém hơn. Do đó, bạn nên di chuyển thao tác xử lý các khung và phần được mã hoá riêng lẻ vào một trình thực thi web.
Để hỗ trợ việc này, ReadableStream cung cấp một cách thuận tiện để tự động chuyển tất cả các khung hình đến từ bản theo dõi nội dung đa phương tiện sang worker. Ví dụ: Bạn có thể sử dụng MediaStreamTrackProcessor
để lấy ReadableStream
cho bản nhạc luồng đa phương tiện từ máy ảnh web. Sau đó, luồng được chuyển sang một trình thực thi web, trong đó các khung được đọc lần lượt từng khung và đưa vào hàng đợi vào VideoEncoder
.
Với HTMLCanvasElement.transferControlToOffscreen
, bạn thậm chí có thể kết xuất ngoài luồng chính. Tuy nhiên, nếu tất cả công cụ cấp cao trở nên bất tiện, thì bản thân VideoFrame
có thể chuyển được và có thể di chuyển giữa các trình thực thi.
WebCodec trong thực tế
Mã hoá
Tất cả đều bắt đầu bằng một VideoFrame
.
Có ba 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 hình 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 bằng cách biểu diễn 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ể bạn đến từ đâu, khung hình có thể được mã hoá 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 với 2 hàm để xử lý các đoạn 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 đến hàm khởi tạo
VideoEncoder
. - Đối tượng cấu hình bộ mã hoá chứa các tham số cho luồng video đầu ra. Bạn có thể thay đổi các tham số này vào lúc khác bằng cách gọi
configure()
.
Phương thức configure()
sẽ gửi NotSupportedError
nếu cấu hình không được trình duyệt hỗ trợ. 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 xong, bộ mã hoá đã sẵn sàng chấp nhận khung hình bằng 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. Tính năng này cho phép một số khung hình xếp hàng để mã hoá cùng 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.
Bạn có thể báo cáo lỗi bằng cách ngay lập tức gửi một ngoại lệ, trong trường hợp các đối số hoặc thứ tự các lệnh 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()
đối với các sự cố gặp phải khi 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 được mã hoá mới làm đối số.
Một chi tiết quan trọng khác ở đây là bạn cần thông báo cho các khung hình khi không cần 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ã hoá bằng cách viết một hàm xử lý các đoạn video đã mã hoá khi chúng thoát ra khỏi bộ mã hoá. Thông thường, hàm này sẽ gửi các phần dữ liệu qua mạng hoặc kết hợp các phần đó vào một vùng chứa nội dung nghe nhì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 vào một thời điểm nào đó, 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, thì bạn có thể gọi flush()
và chờ lời hứa của lệnh gọi đó.
await encoder.flush();
Giải mã
Việc thiết lập VideoDecoder
tương tự như cách thiết lập cho VideoEncoder
: 2 hàm được truyền khi bộ giải mã được tạo và các tham số bộ mã hoá và giải mã được cấp cho configure()
.
Bộ tham số của bộ mã hoá và giải mã khác nhau 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 tệp đó được mã hoá theo đị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 cấp dữ liệu cho bộ giải mã bằng các đối tượng EncodedVideoChunk
.
Để tạo một đoạn, bạn cần có:
BufferSource
dữ liệu video đã mã hoá- dấu thời gian bắt đầu của phân đoạn tính bằng micrô giây (thời gian nội dung đa phương tiện của khung được mã hoá đầu tiên trong phân đoạn)
- loại phân đoạn, một trong các loại sau:
key
nếu phân đoạn có thể được giải mã độc lập với các phân đoạn trước đódelta
nếu phân đoạn trước đó chỉ có thể được giải mã sau khi giải mã một hoặc nhiều phân đ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à tính không đồng bộ của các phương thức của bộ mã hoá cũng đúng đối 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();
Đây là lúc cho thấy cách hiển thị một khung mới được 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, tuỳ chọn này chỉ thêm một khung vào hàng đợi gồm các 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:
- Đang chờ thời điểm thích hợp để hiện khung hình.
- Vẽ khung trên canvas.
Khi một khung không còn cần thiết 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ý. Việc này sẽ làm giảm dung lượng bộ nhớ trung bình mà ứng dụng web sử dụng.
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 cho nhà phát triển
Sử dụng Bảng điều khiển nội dung nghe nhìn trong Công cụ của Chrome cho nhà phát triển để xem nhật ký nội dung nghe nhìn và gỡ lỗi WebCodec.
Bản minh hoạ
Bản minh hoạ dưới đây cho thấy cách thức khung ảnh động từ canvas:
- được
MediaStreamTrackProcessor
chụp ở tốc độ 25 khung hình/giây vàoReadableStream
- được chuyển cho một nhân viên web
- được mã hoá thành định dạng video H.264
- đã được giải mã lại thành một chuỗi các khung video
- và kết xuất trên canvas thứ hai bằng
transferControlToOffscreen()
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:
- Giải mã ảnh GIF bằng ImageDecoder
- Ghi lại dữ liệu đầu vào của máy ảnh vào một tệp
- Phát MP4
- Các mẫu khác
Sử dụng API WebCodecs
Phát hiện tính năng
Để kiểm tra xem WebCodecs có hỗ trợ hay không:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Xin lưu ý rằng API WebCodecs chỉ hoạt động trong ngữ cảnh bảo mật, vì vậy, việc phát hiện sẽ không thành công nếu self.isSecureContext
sai.
Ý kiến phản hồi
Nhóm Chrome muốn biết trải nghiệm của bạn với API WebCodecs.
Cho chúng tôi biết về thiết kế của API
Có điều gì về API không hoạt động như bạn 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 không? Bạn có thắc mắc hoặc nhận xét về mô hình bảo mật? 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 vào vấn đề hiện có.
Báo cáo sự cố với quá trình triển khai
Bạn có phát hiện thấy lỗi khi triển khai Chrome không? Hay cách triển khai có khác với thông số kỹ thuật không? Gửi lỗi tại new.crbug.com. Hãy nhớ cung cấp nhiều thông tin chi tiết nhất có thể, hướng dẫn đơn giản để tái tạo rồi nhập Blink>Media>WebCodecs
vào hộp Components (Thành phần).
Sự cố rất hữu ích trong việc chia sẻ các bản sao nhanh và dễ dàng.
Hỗ trợ API
Bạn có định sử dụng API WebCodecs không? Sự hỗ trợ 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 biết tầm quan trọng của việc hỗ trợ họ.
Bạn có thể gửi email đến media-dev@chromium.org hoặc gửi tweet đến @ChromiumDev kèm theo hashtag #WebCodecs
, đồng thời cho chúng tôi biết vị trí và cách bạn đang sử dụng.
Hình ảnh chính của Denise Jans trên Unsplash.