Gelişmiş tarayıcı tabanlı video işlemenin kilidini açın. Özel efektler ve analizler için WebCodecs API'si ile ham VideoFrame düzlem verilerine doğrudan erişmeyi ve bunları değiştirmeyi öğrenin.
WebCodecs VideoFrame Düzlem Erişimi: Ham Video Veri Manipülasyonuna Derinlemesine Bir Bakış
Yıllardır, web tarayıcısında yüksek performanslı video işleme uzak bir hayal gibiydi. Geliştiriciler genellikle <video> elementinin ve 2D Canvas API'sinin sınırlamalarına bağlı kalıyordu; bu araçlar güçlü olsalar da performans darboğazları yaratıyor ve altta yatan ham video verilerine sınırlı erişim sunuyordu. WebCodecs API'sinin gelişi, tarayıcının yerleşik medya kodeklerine düşük seviyeli erişim sağlayarak bu manzarayı temelden değiştirdi. En devrim niteliğindeki özelliklerinden biri, VideoFrame nesnesi aracılığıyla bireysel video karelerinin ham verilerine doğrudan erişme ve bunları değiştirme yeteneğidir.
Bu makale, basit video oynatmanın ötesine geçmek isteyen geliştiriciler için kapsamlı bir rehberdir. VideoFrame düzlem erişiminin inceliklerini keşfedecek, renk uzayları ve bellek düzeni gibi kavramları açıklayacak ve gerçek zamanlı filtrelerden sofistike bilgisayarla görme görevlerine kadar yeni nesil tarayıcı içi video uygulamaları geliştirmeniz için size güç verecek pratik örnekler sunacağız.
Ön Gereksinimler
Bu rehberden en iyi şekilde yararlanmak için aşağıdaki konularda sağlam bir anlayışa sahip olmalısınız:
- Modern JavaScript: Asenkron programlama (
async/await, Promise'ler) dahil. - Temel Video Kavramları: Kareler, çözünürlük ve kodekler gibi terimlere aşinalık faydalı olacaktır.
- Tarayıcı API'leri: Canvas 2D veya WebGL gibi API'lerle deneyim faydalı olacaktır ancak kesinlikle gerekli değildir.
Video Karelerini, Renk Uzaylarını ve Düzlemleri Anlamak
API'ye dalmadan önce, bir video karesinin verilerinin gerçekte neye benzediğine dair sağlam bir zihinsel model oluşturmalıyız. Dijital bir video, bir dizi hareketsiz görüntü veya kareden oluşur. Her kare piksellerden oluşan bir ızgaradır ve her pikselin bir rengi vardır. Bu rengin nasıl saklandığı renk uzayı ve piksel formatı tarafından tanımlanır.
RGBA: Web'in Ana Dili
Çoğu web geliştiricisi RGBA renk modeline aşinadır. Her piksel dört bileşenle temsil edilir: Kırmızı, Yeşil, Mavi ve Alfa (şeffaflık). Veriler genellikle bellekte iç içe geçmiş (interleaved) olarak saklanır, yani tek bir piksel için R, G, B ve A değerleri ardışık olarak depolanır:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
Bu modelde, tüm görüntü tek bir, sürekli bellek bloğunda saklanır. Bunu tek bir veri "düzlemine" sahip olmak gibi düşünebiliriz.
YUV: Video Sıkıştırmanın Dili
Ancak video kodekleri nadiren doğrudan RGBA ile çalışır. YUV (veya daha doğrusu, Y'CbCr) renk uzaylarını tercih ederler. Bu model, görüntü bilgisini şu şekilde ayırır:
- Y (Luma): Parlaklık veya gri tonlama bilgisi. İnsan gözü en çok luma'daki değişikliklere duyarlıdır.
- U (Cb) ve V (Cr): Krominans veya renk farkı bilgisi. İnsan gözü, renk detaylarına parlaklık detaylarından daha az duyarlıdır.
Bu ayrım, verimli sıkıştırmanın anahtarıdır. U ve V bileşenlerinin çözünürlüğünü düşürerek—kroma alt örneklemesi (chroma subsampling) adı verilen bir teknikle—kalitede minimum algılanabilir kayıpla dosya boyutunu önemli ölçüde azaltabiliriz. Bu, Y, U ve V bileşenlerinin ayrı bellek bloklarında veya "düzlemlerde" saklandığı düzlemsel (planar) piksel formatlarına yol açar.
Yaygın bir format I420'dir (bir tür YUV 4:2:0), burada her 2x2 piksellik blok için dört Y örneği bulunurken, sadece bir U ve bir V örneği bulunur. Bu, U ve V düzlemlerinin Y düzleminin genişliğinin ve yüksekliğinin yarısına sahip olduğu anlamına gelir.
Bu ayrımı anlamak kritiktir çünkü WebCodecs size tam olarak kod çözücünün sağladığı şekliyle bu düzlemlere doğrudan erişim imkanı tanır.
VideoFrame Nesnesi: Piksel Verilerine Açılan Kapınız
Bu bulmacanın merkezi parçası VideoFrame nesnesidir. Tek bir video karesini temsil eder ve sadece piksel verilerini değil, aynı zamanda önemli meta verileri de içerir.
VideoFrame'in Temel Özellikleri
format: Piksel formatını belirten bir dize (ör. 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Kodek tarafından gereken herhangi bir dolgu dahil olmak üzere, bellekte saklandığı şekliyle karenin tam boyutları.displayWidth/displayHeight: Karenin görüntülenmesi için kullanılması gereken boyutlar.timestamp: Karenin mikrosaniye cinsinden sunum zaman damgası.duration: Karenin mikrosaniye cinsinden süresi.
Sihirli Metot: copyTo()
Ham piksel verilerine erişmek için birincil metot videoFrame.copyTo(destination, options)'dur. Bu asenkron metot, karenin düzlem verilerini sağladığınız bir tampona kopyalar.
destination: Veriyi tutacak kadar büyük birArrayBufferveya tipli bir dizi (Uint8Arraygibi).options: Hangi düzlemlerin kopyalanacağını ve bellek düzenlerini belirten bir nesne. Atlanırsa, tüm düzlemleri tek bir bitişik tampona kopyalar.
Metot, karenin her bir düzlemi için bir tane olmak üzere bir PlaneLayout nesneleri dizisiyle çözümlenen bir Promise döndürür. Her PlaneLayout nesnesi iki önemli bilgi içerir:
offset: Bu düzlemin verilerinin hedef tampon içinde başladığı bayt ofseti.stride: O düzlem için bir piksel satırının başlangıcı ile bir sonraki satırın başlangıcı arasındaki bayt sayısı.
Kritik Bir Kavram: Stride ve Genişlik
Bu, düşük seviyeli grafik programlamaya yeni başlayan geliştiriciler için en yaygın kafa karışıklığı kaynaklarından biridir. Her piksel veri satırının birbiri ardına sıkıca paketlendiğini varsayamazsınız.
- Genişlik (Width), görüntünün bir satırındaki piksel sayısıdır.
- Stride (adım veya satır adımı olarak da adlandırılır), bellekte bir satırın başlangıcından bir sonrakinin başlangıcına kadar olan bayt sayısıdır.
Genellikle stride, genişlik * piksel_başına_bayt değerinden daha büyük olacaktır. Bunun nedeni, belleğin CPU veya GPU tarafından daha hızlı işlenmesi için donanım sınırlarına (örneğin, 32 veya 64 baytlık sınırlar) hizalanacak şekilde doldurulmasıdır. Belirli bir satırdaki bir pikselin bellek adresini hesaplamak için her zaman stride kullanmalısınız.
Stride'ı göz ardı etmek, eğri veya bozuk görüntülere ve yanlış veri erişimine yol açacaktır.
Pratik Örnek 1: Gri Tonlamalı Bir Düzleme Erişme ve Görüntüleme
Basit ama güçlü bir örnekle başlayalım. Web'deki videoların çoğu I420 gibi bir YUV formatında kodlanmıştır. 'Y' düzlemi, görüntünün etkili bir şekilde tam bir gri tonlamalı temsilidir. Sadece bu düzlemi çıkarabilir ve bir tuvale işleyebiliriz.
async function displayGrayscale(videoFrame) {
// videoFrame'in 'I420' veya 'NV12' gibi bir YUV formatında olduğunu varsayıyoruz.
if (!videoFrame.format.startsWith('I4')) {
console.error('Bu örnek, YUV 4:2:0 düzlemsel bir format gerektirir.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y düzlemi her zaman birincidir.
// Sadece Y düzlemi verilerini tutmak için bir tampon oluşturun.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Y düzlemini tamponumuza kopyalayın.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Artık yPlaneData ham gri tonlamalı pikselleri içeriyor.
// Bunu işlememiz gerekiyor. Tuval için bir RGBA tamponu oluşturacağız.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Tuval pikselleri üzerinde yineleme yapın ve bunları Y düzlemi verilerinden doldurun.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Önemli: Doğru kaynak dizinini bulmak için stride kullanın!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// RGBA ImageData tamponundaki hedef dizinini hesaplayın.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Kırmızı
imageData.data[rgbaIndex + 1] = luma; // Yeşil
imageData.data[rgbaIndex + 2] = luma; // Mavi
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRİTİK: Belleğini serbest bırakmak için VideoFrame'i her zaman kapatın.
videoFrame.close();
}
Bu örnek, doğru düzlem düzenini belirleme, bir hedef tampon ayırma, verileri çıkarmak için copyTo kullanma ve yeni bir görüntü oluşturmak için stride kullanarak veriler üzerinde doğru şekilde yineleme yapma gibi birkaç önemli adımı vurgulamaktadır.
Pratik Örnek 2: Yerinde Manipülasyon (Sepya Filtresi)
Şimdi doğrudan bir veri manipülasyonu gerçekleştirelim. Sepya filtresi, uygulanması kolay klasik bir efekttir. Bu örnek için, bir tuvalden veya bir WebGL bağlamından alabileceğiniz bir RGBA karesiyle çalışmak daha kolaydır.
async function applySepiaFilter(videoFrame) {
// Bu örnek, giriş karesinin 'RGBA' veya 'BGRA' olduğunu varsayar.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Sepya filtresi örneği bir RGBA karesi gerektirir.');
videoFrame.close();
return null;
}
// Piksel verilerini tutmak için bir tampon ayırın.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA tek bir düzlemdir
// Şimdi, tampondaki verileri değiştirin.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // piksel başına 4 bayt (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) değişmeden kalır.
}
}
// Değiştirilmiş verilerle *yeni* bir VideoFrame oluşturun.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Orijinal kareyi kapatmayı unutmayın!
videoFrame.close();
return newFrame;
}
Bu, tam bir oku-değiştir-yaz döngüsünü gösterir: verileri dışarı kopyalayın, stride kullanarak üzerinde döngü yapın, her piksele matematiksel bir dönüşüm uygulayın ve sonuçta ortaya çıkan verilerle yeni bir VideoFrame oluşturun. Bu yeni kare daha sonra bir tuvale işlenebilir, bir VideoEncoder'a gönderilebilir veya başka bir işleme adımına aktarılabilir.
Performans Önemlidir: JavaScript ve WebAssembly (WASM) Karşılaştırması
Her kare için milyonlarca piksel üzerinde yineleme yapmak (1080p bir karede 2 milyondan fazla piksel veya RGBA'da 8 milyon veri noktası bulunur) JavaScript'te yavaş olabilir. Modern JS motorları inanılmaz derecede hızlı olsa da, yüksek çözünürlüklü videoların (HD, 4K) gerçek zamanlı işlenmesi için bu yaklaşım ana iş parçacığını kolayca bunaltabilir ve kesintili bir kullanıcı deneyimine yol açabilir.
İşte bu noktada WebAssembly (WASM) önemli bir araç haline gelir. WASM, C++, Rust veya Go gibi dillerde yazılmış kodu tarayıcı içinde neredeyse yerel hızda çalıştırmanıza olanak tanır. Video işleme için iş akışı şu şekilde olur:
- JavaScript'te: Ham piksel verilerini bir
ArrayBuffer'a almak içinvideoFrame.copyTo()kullanın. - WASM'e Aktarma: Bu tampona bir referansı derlenmiş WASM modülünüze geçirin. Bu, verileri kopyalamayı içermediği için çok hızlı bir işlemdir.
- WASM'de (C++/Rust): Yüksek düzeyde optimize edilmiş görüntü işleme algoritmalarınızı doğrudan bellek tamponu üzerinde çalıştırın. Bu, bir JavaScript döngüsünden kat kat daha hızlıdır.
- JavaScript'e Dönüş: WASM işini bitirdiğinde, kontrol JavaScript'e geri döner. Daha sonra değiştirilmiş tamponu kullanarak yeni bir
VideoFrameoluşturabilirsiniz.
Sanal arka planlar, nesne tespiti veya karmaşık filtreler gibi ciddi, gerçek zamanlı video manipülasyonu uygulamaları için WebAssembly'den yararlanmak sadece bir seçenek değil; bir zorunluluktur.
Farklı Piksel Formatlarıyla Çalışma (ör. I420, NV12)
RGBA basit olsa da, çoğunlukla bir VideoDecoder'dan düzlemsel YUV formatlarında kareler alırsınız. Şimdi I420 gibi tamamen düzlemsel bir formatla nasıl başa çıkılacağına bakalım.
I420 formatındaki bir VideoFrame, layout dizisinde üç düzen tanımlayıcısına sahip olacaktır:
layout[0]: Y düzlemi (luma). BoyutlarcodedWidthxcodedHeight.layout[1]: U düzlemi (kroma). BoyutlarcodedWidth/2xcodedHeight/2.layout[2]: V düzlemi (kroma). BoyutlarcodedWidth/2xcodedHeight/2.
İşte üç düzlemi de tek bir tampona nasıl kopyalayacağınız:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts, 3 PlaneLayout nesnesinden oluşan bir dizidir
console.log('Y Plane Layout:', layouts[0]); // { offset: 0, stride: ... }
console.log('U Plane Layout:', layouts[1]); // { offset: ..., stride: ... }
console.log('V Plane Layout:', layouts[2]); // { offset: ..., stride: ... }
// Artık `allPlanesData` tamponundaki her düzleme erişebilirsiniz
// kendi özel ofsetini ve stride'ını kullanarak.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Kroma boyutlarının yarıya indiğini unutmayın!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Accessed Y plane size:', yPlaneView.byteLength);
console.log('Accessed U plane size:', uPlaneView.byteLength);
videoFrame.close();
}
Başka bir yaygın format, yarı düzlemsel olan NV12'dir. İki düzlemi vardır: biri Y için ve U ile V değerlerinin iç içe geçtiği (ör. [U1, V1, U2, V2, ...]) ikinci bir düzlem. WebCodecs API'si bunu şeffaf bir şekilde yönetir; NV12 formatındaki bir VideoFrame, layout dizisinde basitçe iki düzene sahip olacaktır.
Zorluklar ve En İyi Uygulamalar
Bu kadar düşük seviyede çalışmak güçlüdür, ancak sorumlulukları da beraberinde getirir.
Bellek Yönetimi Her Şeyden Önemlidir
Bir VideoFrame, genellikle JavaScript çöp toplayıcısının yığınının dışında yönetilen önemli miktarda bellek tutar. Bu belleği açıkça serbest bırakmazsanız, tarayıcı sekmesini çökertebilecek bir bellek sızıntısına neden olursunuz.
Bir kareyle işiniz bittiğinde her zaman, ama her zaman videoFrame.close()'u çağırın.
Asenkron Doğa
Tüm veri erişimi asenkrondur. Uygulamanızın mimarisi, yarış koşullarından kaçınmak ve sorunsuz bir işleme hattı sağlamak için Promise'lerin ve async/await akışını doğru bir şekilde yönetmelidir.
Tarayıcı Uyumluluğu
WebCodecs modern bir API'dir. Tüm büyük tarayıcılarda desteklenmesine rağmen, her zaman kullanılabilirliğini kontrol edin ve satıcıya özgü uygulama ayrıntılarının veya sınırlamalarının farkında olun. API'yi kullanmaya çalışmadan önce özellik tespiti yapın.
Sonuç: Web Videosu İçin Yeni Bir Ufuk
WebCodecs API aracılığıyla bir VideoFrame'in ham düzlem verilerine doğrudan erişme ve bunları değiştirme yeteneği, web tabanlı medya uygulamaları için bir paradigma kaymasıdır. <video> elementinin kara kutusunu ortadan kaldırır ve geliştiricilere daha önce yerel uygulamalara ayrılmış olan ayrıntılı kontrolü verir.
Video bellek düzeninin temellerini—düzlemler, stride ve renk formatları—anlayarak ve performans açısından kritik işlemler için WebAssembly'nin gücünden yararlanarak, artık doğrudan tarayıcıda inanılmaz derecede sofistike video işleme araçları oluşturabilirsiniz. Gerçek zamanlı renk derecelendirme ve özel görsel efektlerden istemci tarafı makine öğrenimi ve video analizine kadar olasılıklar çok geniştir. Web'de yüksek performanslı, düşük seviyeli video dönemi gerçekten başlamıştır.