Thao túng các thành phần trong luồng video.
Các công nghệ web hiện đại mang đến nhiều cách làm việc với video. API Luồng nội dung nghe nhìn, API Ghi nội dung nghe nhìn. API nguồn nội dung nghe nhìn, và API WebRTC thành một bộ công cụ đa dạng dùng để ghi, chuyển và phát video trực tuyến. Trong khi giải quyết một số nhiệm vụ cấp cao, các API này không cho phép các lập trình viên làm việc với các thành phần riêng lẻ của luồng video như khung và các đoạn video hoặc âm thanh chưa được mã hoá. Để có quyền truy cập cấp thấp vào những thành phần cơ bản này, 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. Nhưng được cung cấp các trình duyệt hiện đại đã gửi kèm nhiều bộ mã hoá và giải mã (thường được tăng tốc bằng phần cứng), việc đóng gói lại chúng vì WebAssembly có vẻ lãng phí con người và máy tính.
API WebCodecs loại bỏ sự 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 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ô
- Bộ giải mã hình ảnh
WebCodecs API rất hữu ích cho các ứng dụng web yêu cầu toàn quyền kiểm soát đối với cách xử lý nội dung truyền thông, 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, v.v.
Quy trình xử lý video
Khung hình là thành phần trung tâm trong quá trình xử lý video. Do đó, trong WebCodecs, hầu hết các lớp sử dụng hoặc tạo khung hình. Bộ mã hoá video chuyển đổi khung hình thành khung hình được mã hoá phân đoạn. Bộ giải mã video có chức năng 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à CanvasImageSource
và có một hàm khởi tạo chấp nhận CanvasImageSource
.
Vì vậy, bạn có thể dùng thuộc tính này trong các hàm như drawImage()
và texImage2D()
. Ngoài ra, thành phần này có thể được xây dựng từ canvas, bitmap, phần tử video và các khung video khác.
WebCodecs API 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 nhạc luồng nội dung nghe nhìn.
MediaStreamTrackProcessor
chia các bản nhạc nội dung đa phương tiện thành từng khung hình riêng lẻ.MediaStreamTrackGenerator
tạo một bản nhạc nội dung nghe nhìn từ một luồng khung hình.
WebCodec và nhân viên web
Theo thiết kế, API WebCodecs thực hiện tất cả các công việc khó khăn một cách không đồng bộ và ra khỏi luồng chính. Nhưng 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, chúng có thể làm lộn xộn luồng chính, từ đó làm cho trang web kém phản hồi hơn. Do đó, bạn nên di chuyển việc xử lý từng khung hình riêng lẻ và các đoạn được mã hoá thành một nhân viên web.
Để giúp bạn làm việc đó, ReadableStream
cung cấp một cách thuận tiện để tự động chuyển tất cả khung hình từ nội dung nghe nhìn
theo dõi đối với worker. Ví dụ: bạn có thể dùng MediaStreamTrackProcessor
để lấy một
ReadableStream
cho bản nhạc luồng nội dung nghe nhìn đến từ máy ảnh web. Sau đó
luồng được chuyển đến một worker web, trong đó các khung được đọc từng khung một và được đưa vào hàng đợi
vào VideoEncoder
.
Với HTMLCanvasElement.transferControlToOffscreen
, bạn thậm chí có thể kết xuất cả ngoài luồng chính. Nhưng nếu tất cả các công cụ cấp cao đều
thực sự bất tiện, bản thân VideoFrame
có thể chuyển nhượng và có thể
được 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ó 3 cách để tạo khung 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 từ cách biểu diễn pixel nhị phân của 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ể khung hình đến từ đâu, khung hình đều có thể được mã hoá thành
Đối tượng EncodedVideoChunk
có 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 hai hàm để xử lý các đoạn mã hoá và
. Các hàm này do nhà phát triển xác định và không thể thay đổi sau
chúng sẽ đượ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 đầu ra
luồng video. 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 cấu hình không được
mà trình duyệt hỗ trợ. Bạn nên gọi phương thức tĩnh
VideoEncoder.isConfigSupported()
với cấu hình để kiểm tra trước xem
cấu hình này được hỗ trợ và chờ đợi cấu hình được hứa hẹn.
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á có thể chấp nhận khung hình thông qua phương thức encode()
.
Cả configure()
và encode()
đều sẽ quay lại ngay lập tức mà không cần đợi
công việc thực tế cần hoàn thành. Tính năng này cho phép một vài khung hình xếp hàng đợi để mã hoá ở
encodeQueueSize
cho biết số lượng yêu cầu đang chờ trong hàng đợi
để các mã hoá trước đó kết thúc.
Lỗi được báo cáo 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ủa 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 error()
lệnh gọi lại cho các sự cố 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, output()
lệnh gọi lại đượ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à khung hình cần được thông báo khi không có
cần nhiều thời gian hơn 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 đi ra khỏi 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 chúng vào một phương tiện vùng chứa để 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à chờ lời hứa.
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 bộ giải mã được tạo và bộ mã hoá và giải mã
tham số được cấp cho configure()
.
Các bộ tham số của bộ mã hoá và giải mã sẽ thay đổi theo từng 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á 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 cung cấp 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 phát 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:
key
nếu đoạn có thể được giải mã độc lập với các đoạn trướcdelta
nếu đoạn chỉ có thể được giải mã sau khi một hoặc nhiều đoạn trước đó đã được giải mã
Ngoài ra, bất kỳ đoạn nào do bộ mã hoá đưa ra cũng đã sẵn sàng cho bộ giải mã như nguyên trạng. Tất cả những điều đã nói ở trên về báo cáo lỗi và tính chất không đồng bộ phương thức của bộ mã hoá cũng đúng như nhau đố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();
Bây giờ, đã đến lúc cho thấy cách hiển thị một khung được giải mã mới trên trang. Bây giờ
để đảm bảo rằng lệnh gọi lại đầu ra của bộ giải mã (handleFrame()
)
nhanh chóng quay trở lại. Trong ví dụ bên dưới, nó chỉ thêm một khung vào hàng đợi của
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 2 bước:
- Chờ thời điểm thích hợp để hiển thị khung hình.
- Vẽ khung trên canvas.
Khi không cần khung hình nữa, hãy gọi close()
để giải phóng bộ nhớ cơ bản
trước khi bộ thu gom rác đến được, việc này sẽ làm giảm lượng
bộ nhớ được ứ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 dành 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ý phương tiện và gỡ lỗi WebCodec.
Bản minh hoạ
Bản minh hoạ dưới đây cho thấy cách tạo khung ảnh động trong canvas:
- được
MediaStreamTrackProcessor
chụp ở tốc độ 25 khung hình/giây vàoReadableStream
- được chuyển sang 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à hiển thị trên canvas thứ hai bằng cách sử dụ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 API WebCodecs
Phát hiện tính năng
Cách kiểm tra tính năng hỗ trợ của WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
Xin lưu ý rằng API WebCodecs chỉ dùng được trong bối 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
là false.
Phản hồi
Nhóm Chrome muốn biết ý kiến của bạn về trải nghiệm với API WebCodecs.
Cho chúng tôi biết về thiết kế API
Có điều gì về API không hoạt động như bạn mong đợi không? Hoặc có thiếu phương thức hoặc thuộc tính nào mà bạn cần để triển khai ý tưởng của mình không? Có bạn có câu hỏi hoặc nhận xét gì về mô hình bảo mật không? Báo cáo vấn đề về thông số kỹ thuật trên kho lưu trữ GitHub tương ứng, hoặc thêm suy nghĩ của bạn về vấn đề hiện tại.
Báo cáo sự cố về triển khai
Bạn có phát hiện lỗi trong quá trình triển khai Chrome không? Hoặc là triển khai
khác với thông số kỹ thuật không? Báo cáo 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 tạo và nhập Blink>Media>WebCodecs
vào hộp Components (Thành phần).
Glitch rất hữu ích khi chia sẻ các bản dựng lại nhanh chóng và dễ dàng.
Hiện thông tin hỗ trợ về API này
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 thấy mức độ quan trọng đó là hỗ trợ họ.
Gửi email đến media-dev@chromium.org hoặc gửi bài đăng trên Twitter
tới @ChromiumDev bằng cách sử dụng hashtag
#WebCodecs
đồng thời 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 Tháng 1 chuyên biệt trên Unsplash.