Thao tác với các thành phần của 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 thành một bộ công cụ đa dạng để ghi, chuyển và phát các luồng video. Mặc dù giải quyết một số tác vụ cấp cao, 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 một luồng video, chẳng hạn như khung hình và các khối video hoặc âm thanh được mã hoá chưa được tách. Để 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 codec video và âm thanh vào trình duyệt. Nhưng vì 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 tăng tốc bằng phần cứng), nên việc đóng gói lại các bộ mã hoá và giải mã này dưới dạng WebAssembly có vẻ như là một sự lãng phí tài nguyên của con người và máy tính.
WebCodecs API giúp loại bỏ sự thiếu hiệu quả này bằng cách cho phép lập trình viên 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
WebCodecs API rất hữu ích cho các ứng dụng web cần kiểm soát hoàn toàn 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, truyền phát video trực tiếp, v.v.
Quy trình xử lý video
Khung hình là yếu tố trung tâ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 hình. Bộ mã hoá video chuyển đổi các khung hình thành các đoạn được mã hoá. Bộ giải mã video làm điều 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 constructor chấp nhận CanvasImageSource
. Vì vậy, bạn có thể dùng hàm này trong các hàm như drawImage()
và texImage2D()
. Ngoài ra, video này có thể được tạo từ các canvas, bitmap, phần tử video và khung hình video khác.
WebCodecs API hoạt động hiệu quả cùng với các lớp trong Insertable Streams API (API luồng có thể chèn) kết nối WebCodecs với các bản ghi luồng đa phương tiện.
MediaStreamTrackProcessor
chia các bản ghi nội dung nghe nhìn thành các khung hình riêng lẻ.MediaStreamTrackGenerator
tạo một bản âm thanh và phụ đề từ một luồng khung hình.
WebCodecs và web worker
Theo thiết kế, WebCodecs API thực hiện tất cả các thao tác 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 mỗi giây, nên chúng có thể làm lộn xộn luồng chính và do đó khiến trang web ít phản hồi hơn. Do đó, bạn nên chuyển việc xử lý các khung hình riêng lẻ và các đoạn được mã hoá vào một web worker.
Để 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ừ một bản âm thanh và phụ đề sang worker. Ví dụ: MediaStreamTrackProcessor
có thể được dùng để lấy ReadableStream
cho một bản nhạc luồng nội dung nghe nhìn đến từ camera web. Sau đó, luồng này sẽ được chuyển đến một web worker, nơi các khung hình được đọc từng khung hình một và được xếp vào hàng đợi trong một VideoEncoder
.
Với HTMLCanvasElement.transferControlToOffscreen
, ngay cả quá trình kết xuất cũng có thể được thực hiện ngoài luồng chính. Nhưng nếu tất cả các công cụ cấp cao đều bất tiện, thì bản thân VideoFrame
có thể chuyển nhượng và có thể được di chuyển giữa các nhân viên.
WebCodecs trong thực tế
Mã hoá

Canvas
hoặc ImageBitmap
đến mạng hoặc bộ nhớ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
để kéo 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 hình từ biểu diễn pixel nhị phân của khung hình đó 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ể đến từ đâu, các khung hình đều có thể được mã hoá thành các đối tượng EncodedVideoChunk
bằng VideoEncoder
.
Trước khi mã hoá, VideoEncoder
cần được cung cấp 2 đối tượng JavaScript:
- Khởi tạo từ điển bằng 2 hàm để xử lý các đoạn mã được mã hoá và lỗi. 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. Sau này, bạn có thể thay đổi các tham số này 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 phương thức này.
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 được 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. Thao tác 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 có bao nhiêu yêu cầu đang chờ trong hàng đợi để các hoạt động mã hoá trước đó hoàn tất. Lỗi được báo cáo bằng cách ngay lập tức tạo ra 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 codec. 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à các khung hình cần được thông báo khi chúng không còn cần thiết 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 việc mã hoá mã bằng cách viết một hàm xử lý các đoạn video được mã hoá khi chúng xuất hiện từ bộ mã hoá. Thông thường, hàm này sẽ gửi các khối dữ liệu qua mạng hoặc ghép kênh chúng 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 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 yêu cầu đó.
await encoder.flush();
Giải mã

Canvas
hoặc một ImageBitmap
.Việc thiết lập VideoDecoder
tương tự như những gì đã được thực hiện cho VideoEncoder
: 2 hàm được truyền khi bộ giải mã được tạo và các tham số codec được cung cấp cho configure()
.
Tập hợp các tham số bộ mã hoá và giải mã sẽ khác nhau tuỳ 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 bộ mã hoá và giải mã đó đượ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:
- Một
BufferSource
dữ liệu video được mã hoá - dấu thời gian bắt đầu của đoạn tính bằng micrô giây (thời gian của nội dung nghe nhìn của khung hình được mã hoá đầu tiên trong đoạn)
- loại của khối, một trong các loại sau:
key
nếu đoạn có thể được giải mã độc lập với các đoạn trước đódelta
nếu chỉ có thể giải mã khối sau khi một hoặc nhiều khối trước đó đã được giải mã
Ngoài ra, mọi đoạn được 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ề 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ờ là lúc cho thấy cách một khung hình vừa được giải mã có thể xuất hiện trên trang. Tốt hơn là bạn nên đảm bảo lệnh gọi lại đầu ra của bộ giải mã (handleFrame()
) trả về nhanh chóng. Trong ví dụ bên dưới, thao tác này chỉ thêm một khung hình vào hàng đợi 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 2 bước:
- Đợi 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 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 truy cập vào bộ nhớ đó. Việc này sẽ giảm 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 WebCodecs.

Bản minh hoạ
Bản minh hoạ cho thấy cách các khung hình động từ một canvas:
- được quay ở tốc độ 25 khung hình/giây vào
ReadableStream
bởiMediaStreamTrackProcessor
- được chuyển đến một web worker
- được mã hoá thành định dạng video H.264
- giải mã lại thành một chuỗi khung hình video
- và được kết xuất trên canvas thứ hai bằng cách sử dụng
transferControlToOffscreen()
Các bản minh hoạ khác
Bạn cũng có thể 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 từ camera vào một tệp
- Phát tệp MP4
- Các mẫu khác
Sử dụng WebCodecs API
Phát hiện đối tượ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ẽ thất bại 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 khi sử dụng WebCodecs API.
Hãy cho chúng tôi biết về thiết kế API
Có vấn đề gì về API khiến bạn không hài lòng không? Hoặc 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 bình luận về mô hình bảo mật không? Gửi vấn đề về quy cách 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 thấy lỗi trong quá trình triển khai của Chrome không? Hay việc triển khai có khác với quy cách không? Báo cáo lỗi tại new.crbug.com. 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 và nhập Blink>Media>WebCodecs
vào hộp Thành phần.
Thể hiện sự ủng hộ đối với API
Bạn có dự định sử dụng WebCodecs API 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 này.
Gửi email đến [email protected] hoặc gửi một tweet đến @ChromiumDev bằng thẻ bắt đầu bằng #WebCodecs
và cho chúng tôi biết bạn đang sử dụng tính năng này ở đâu và như thế nào.
Hình ảnh chính của Denise Jans trên Unsplash.