Video akışı bileşenleri değiştiriliyor.
Modern web teknolojileri, videoyla çalışmak için geniş kapsamlı yöntemler sunar. Media Stream API, Media Record API, Media Source API ve WebRTC API video akışlarını kaydetmek, aktarmak ve oynatmak için zengin bir araç seti sağlar. Bu API'ler bazı üst düzey görevleri çözerken web programlarının bir video akışının kareler ve kodlanmış video veya ses parçalarından oluşan muğlak olmayan bileşenleri gibi bağımsız bileşenleriyle çalışmasına izin vermez. Geliştiriciler bu temel bileşenlere alt düzey erişim elde etmek için video ve ses codec'lerini tarayıcıya getirmek için WebAssembly hizmetini kullanmaktadır. Ancak modern tarayıcıların halihazırda çeşitli codec'lerle (genellikle donanım tarafından hızlandırılarak) birlikte gönderilmesi, WebAssembly'nin yeniden paketlenmesinin insan ve bilgisayar kaynakları israfı gibi görünmesidir.
WebCodecs API, programcılara zaten tarayıcıda bulunan medya bileşenlerini kullanma olanağı sunarak bu verimsizliği ortadan kaldırır. Özellikle:
- Video ve ses kod çözücüler
- Video ve ses kodlayıcıları
- Ham video kareleri
- Görüntü kod çözücüleri
WebCodecs API; video düzenleyiciler, video konferans, video akışı vb. gibi medya içeriğinin işlenme şekli üzerinde tam kontrol gerektiren web uygulamaları için yararlıdır.
Video işleme iş akışı
Kareler, video işlemenin en önemli parçasıdır. Bu nedenle, WebCodecs'de çoğu sınıf kare tüketir veya oluşturur. Video kodlayıcılar, kareleri kodlanmış parçalara dönüştürür. Video kod çözücüler ise bunun tersini yapar.
Ayrıca VideoFrame
, CanvasImageSource
olması ve CanvasImageSource
kabul eden bir kurucu olması nedeniyle diğer Web API'leriyle uyumlu şekilde çalışır.
Bu nedenle drawImage()
ve texImage2D()
gibi işlevlerde kullanılabilir. Ayrıca tuvallerden, bit eşlemlerden, video öğelerinden ve diğer video karelerinden de oluşturulabilir.
WebCodecs API, WebCodecs'i medya akış kanallarına bağlayan Insertable Streams API sınıflarıyla birlikte iyi bir şekilde çalışır.
MediaStreamTrackProcessor
, medya kanallarını ayrı ayrı karelere ayırıyor.MediaStreamTrackGenerator
, bir kare akışından medya kanalı oluşturur.
WebCodec'ler ve web çalışanları
WebCodecs API, tasarımı gereği tüm ağır işleri eşzamansız olarak ve ana iş parçacığı dışında gerçekleştirir. Ancak çerçeve ve parça geri çağırma işlevleri genellikle saniyede birden çok kez çağrılabildiğinden ana iş parçacığını karıştırabilir ve bu nedenle web sitesinin daha az duyarlı olmasına neden olabilir. Bu nedenle, bağımsız çerçevelerin ve kodlanmış parçaların işlenmesini bir web çalışanına taşımak tercih edilir.
ReadableStream, bir medya kanalından gelen tüm kareleri çalışana otomatik olarak aktarmak için rahat bir yol sunuyor. Örneğin, web kamerasından gelen bir medya akışı kanalı için ReadableStream
elde etmek üzere MediaStreamTrackProcessor
kullanılabilir. Ardından akış bir web çalışanına aktarılır. Burada çerçeveler tek tek okunur ve VideoEncoder
için sıraya alınır.
HTMLCanvasElement.transferControlToOffscreen
ile oluşturma işlemi ana ileti dizisi dışında da yapılabilir. Ancak üst düzey araçların hiçbiri sorun yaratırsa VideoFrame
ürününün kendisi aktarılabilir ve çalışanlar arasında taşınabilir.
WebCodecs'in işleyiş şekli
Kodlama
Her şey bir VideoFrame
ile başlar.
Video karesi oluşturmanın üç yolu vardır.
Kanvas, resim bit eşlemi veya video öğesi gibi bir resim kaynağından.
const canvas = document.createElement("canvas"); // Draw something on the canvas... const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
Bir
MediaStreamTrack
öğesinden kare çekmek içinMediaStreamTrackProcessor
öğesini kullanınconst 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
da ikili piksel gösteriminden bir çerçeve oluşturun.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);
Nereden geldiğine bakılmaksızın kareler bir VideoEncoder
ile EncodedVideoChunk
nesnelerine kodlanabilir.
Kodlamadan önce, VideoEncoder
öğesine iki JavaScript nesnesi verilmesi gerekir:
- Kodlanmış parçaları ve hataları işlemek için iki işlevli sözlüğü başlatın. Bu işlevler geliştirici tarafından tanımlanır ve
VideoEncoder
oluşturucuya geçirildikten sonra değiştirilemez. - Çıkış video akışı için parametreleri içeren kodlayıcı yapılandırma nesnesi. Bu parametreleri daha sonra
configure()
yöntemini çağırarak değiştirebilirsiniz.
Tarayıcı, yapılandırmayı desteklemiyorsa configure()
yöntemi NotSupportedError
hatasını verir. Yapılandırmanın desteklenip desteklenmediğini önceden kontrol etmek için yapılandırma ile birlikte statik yöntemi VideoEncoder.isConfigSupported()
çağırmanız ve sözünü beklemeniz önerilir.
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.
}
Kodlayıcı, ayarlandıktan sonra encode()
yöntemiyle kare kabul etmeye hazır hâle gelir.
Hem configure()
hem de encode()
, asıl çalışmanın tamamlanmasını beklemeden hemen geri döner. Bu işlev, birkaç karenin kodlama için aynı anda sıraya girmesine olanak tanır. encodeQueueSize
ise önceki kodlamaların tamamlanması için sırada bekleyen istek sayısını gösterir.
Bağımsız değişkenler veya yöntem çağrılarının sırası API sözleşmesini ihlal ediyorsa ya da codec uygulamasında karşılaşılan sorunlar için error()
geri çağırması çağrılıp hatalar hemen bir istisna gönderilerek bildirilir.
Kodlama başarıyla tamamlanırsa output()
geri çağırma, bağımsız değişken olarak yeni bir kodlanmış yığınla çağrılır.
Buradaki bir diğer önemli ayrıntı, çerçevelerin artık ihtiyaç kalmadığında close()
çağrısı yapılarak bildirilmesi gerektiğidir.
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();
}
}
Son olarak, kodlayıcıdan çıkan kodlanmış video parçalarını işleyen bir işlev yazarak kodlama kodunu bitirme zamanı geldi. Genellikle bu işlev, veri parçalarını ağ üzerinden gönderir veya bunları depolama amacıyla bir medya kapsayıcısında mux oluşturur.
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,
});
}
Herhangi bir noktada bekleyen tüm kodlama isteklerinin tamamlandığından emin olmanız gerekirse flush()
yöntemini çağırabilir ve sözünü bekleyebilirsiniz.
await encoder.flush();
Kod çözülüyor
VideoDecoder
kurulumu VideoEncoder
için yapılanlara benzer: Kod çözücü oluşturulduğunda iki işlev aktarılır ve configure()
öğesine codec parametreleri verilir.
Codec parametreleri grubu codec'ten codec'e farklılık gösterir. Örneğin H.264 codec'i, Ek B biçiminde (encoderConfig.avc = { format: "annexb" }
) olarak kodlanmadığı sürece AVCC'nin ikili blob'una ihtiyaç duyabilir.
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.
}
Kod çözücü başlatıldıktan sonra EncodedVideoChunk
nesneleriyle beslemeye başlayabilirsiniz.
Parça oluşturmak için aşağıdakilere ihtiyacınız vardır:
- Kodlanmış video verileri için
BufferSource
. - parçanın başlangıç zaman damgası (mikrosaniye cinsinden) (parçadaki ilk kodlanmış karenin medya zamanı)
- Bu parçanın türü şunlardan biri olacaktır:
- Yığın, önceki parçalardan bağımsız olarak çözülebiliyorsa
key
- Yığının kodu yalnızca bir veya daha fazla önceki parçanın kodu çözüldükten sonra çözüleebiliyorsa
delta
- Yığın, önceki parçalardan bağımsız olarak çözülebiliyorsa
Ayrıca kodlayıcı tarafından yayınlanan tüm parçalar, kod çözücü için olduğu gibi hazırdır. Yukarıda hata raporlama ve kodlayıcının yöntemlerinin eşzamansız doğasıyla ilgili söylenenlerin hepsi, kod çözücüler için de eşit derecede geçerlidir.
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();
Şimdi şifresi yeni çözülmüş bir karenin sayfada nasıl gösterilebileceğini gösterme zamanı geldi. Kod çözücü çıkışı geri çağırma işlevinin (handleFrame()
) hızlı bir şekilde geri döndüğünden emin olmak daha iyi olur. Aşağıdaki örnekte, yalnızca oluşturulmaya hazır olan kareler sırasına bir kare eklenir.
Oluşturma ayrı olarak gerçekleşir ve iki adımdan oluşur:
- Karenin gösterilmesi için doğru zaman bekleniyor.
- Çerçeve kanvasa çiziliyor.
Bir çerçeveye artık ihtiyaç duyulmadığında, çöp toplayıcı kendisine ulaşmadan önce temel belleği serbest bırakmak için close()
yöntemini çağırın. Bu işlem, web uygulamasının kullandığı ortalama bellek miktarını azaltır.
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);
}
Geliştiricilere Yönelik İpuçları
Medya günlüklerini görüntülemek ve WebCodecs'deki hataları ayıklamak için Chrome Geliştirici Araçları'ndaki Medya Paneli'ni kullanın.
Demo
Aşağıdaki demoda, tuvaldeki animasyon karelerinin nasıl olduğu gösterilmektedir:
- 25 fps'de
MediaStreamTrackProcessor
tarafındanReadableStream
ile yakalandı - bir web çalışanına aktarıldı
- H.264 video biçimine kodlanmış
- kod tekrar bir video karesi dizisine dönüştürülür
- ve
transferControlToOffscreen()
ile ikinci kanvasta oluşturuldu.
Diğer demolar
Diğer demolarımıza da göz atın:
WebCodecs API'yi kullanma
Özellik algılama
WebCodecs desteğini kontrol etmek için:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
WebCodecs API'nin yalnızca güvenli bağlamlarda kullanılabildiğini, bu nedenle self.isSecureContext
değeri yanlış olduğunda algılamanın başarısız olacağını unutmayın.
Geri bildirim
Chrome ekibi, WebCodecs API ile ilgili deneyimlerinizi öğrenmek istiyor.
Bize API tasarımı hakkında bilgi verin
API'de beklediğiniz gibi çalışmayan bir durum mu var? Yoksa fikrinizi uygulamak için gereken eksik yöntemler veya özellikler mi var? Güvenlik modeliyle ilgili bir sorunuz veya yorumunuz mu var? İlgili GitHub deposuna teknik özellik sorunu kaydedin veya mevcut bir soruna düşüncelerinizi ekleyin.
Uygulamayla ilgili bir sorunu bildirin
Chrome'un uygulamasında bir hata buldunuz mu? Yoksa uygulama, spesifikasyondan
farklı mı? new.crbug.com adresinden hata bildiriminde bulunun. Mümkün olduğunca çok ayrıntı eklediğinizden ve basit yeniden oluşturma talimatlarını eklediğinizden emin olun ve Bileşenler kutusuna Blink>Media>WebCodecs
yazın.
Glitch, hızlı ve kolay yeniden oluşturmalar paylaşmak için idealdir.
API'ye desteği gösterin
WebCodecs API'yi kullanmayı planlıyor musunuz? Herkese açık desteğiniz, Chrome ekibinin özellikleri öncelik sırasına koymasına yardımcı olur ve diğer tarayıcı satıcılarına onları desteklemenin ne kadar kritik olduğunu gösterir.
media-dev@chromium.org adresine e-posta gönderin veya #WebCodecs
hashtag'ini kullanarak @ChromiumDev adresine tweet göndererek etiketi nerede ve nasıl kullandığınızı bize bildirin.
Denise Jans'ın Unsplash'teki hero resmi.