Tarayıcıda yüksek kaliteli video akışının kilidini açın. WebCodecs API ve VideoFrame manipülasyonu kullanarak gürültü azaltma için gelişmiş zamansal filtrelemeyi uygulamayı öğrenin.
WebCodecs'te Uzmanlaşmak: Zamansal Gürültü Azaltma ile Video Kalitesini İyileştirme
Web tabanlı video iletişimi, akış ve gerçek zamanlı uygulamalar dünyasında kalite her şeyden önemlidir. Dünyanın dört bir yanındaki kullanıcılar, ister bir iş toplantısında olsunlar, ister canlı bir etkinliği izlesinler veya uzak bir hizmetle etkileşimde bulunsunlar, canlı ve net bir video beklerler. Ancak, video akışları genellikle kalıcı ve dikkat dağıtıcı bir yapaylıktan muzdariptir: gürültü. Genellikle grenli veya statik bir doku olarak görülen bu dijital gürültü, izleme deneyimini düşürebilir ve şaşırtıcı bir şekilde bant genişliği tüketimini artırabilir. Neyse ki, WebCodecs adlı güçlü bir tarayıcı API'si, geliştiricilere bu sorunun üstesinden gelmek için benzeri görülmemiş bir alt düzey kontrol sağlar.
Bu kapsamlı rehber, sizi WebCodecs'i belirli ve yüksek etkili bir video işleme tekniği için kullanma konusunda derinlemesine bir yolculuğa çıkaracak: zamansal gürültü azaltma. Video gürültüsünün ne olduğunu, neden zararlı olduğunu ve doğrudan tarayıcıda bir filtreleme hattı oluşturmak için VideoFrame
nesnesinden nasıl yararlanabileceğinizi keşfedeceğiz. Temel teoriden pratik bir JavaScript uygulamasına, WebAssembly ile performans değerlendirmelerine ve profesyonel düzeyde sonuçlar elde etmek için gelişmiş kavramlara kadar her şeyi ele alacağız.
Video Gürültüsü Nedir ve Neden Önemlidir?
Bir sorunu çözmeden önce, onu anlamalıyız. Dijital videoda gürültü, video sinyalindeki parlaklık veya renk bilgilerindeki rastgele varyasyonları ifade eder. Görüntü yakalama ve iletim sürecinin istenmeyen bir yan ürünüdür.
Gürültünün Kaynakları ve Türleri
- Sensör Gürültüsü: Ana suçlu. Düşük ışık koşullarında, kamera sensörleri yeterince parlak bir görüntü oluşturmak için gelen sinyali yükseltir. Bu yükseltme süreci aynı zamanda rastgele elektronik dalgalanmaları da artırarak görünür grenlere neden olur.
- Termal Gürültü: Kameranın elektroniği tarafından üretilen ısı, elektronların rastgele hareket etmesine neden olarak ışık seviyesinden bağımsız bir gürültü yaratabilir.
- Kuantizasyon Gürültüsü: Analogdan dijitale dönüştürme ve sıkıştırma işlemleri sırasında, sürekli değerlerin sınırlı bir dizi ayrık seviyeye eşlendiği yerde ortaya çıkar.
Bu gürültü tipik olarak, her pikselin yoğunluğunun gerçek değerinin etrafında rastgele değiştiği ve tüm kare boyunca ince, titrek bir gren oluşturduğu Gauss gürültüsü olarak ortaya çıkar.
Gürültünün İki Yönlü Etkisi
Video gürültüsü kozmetik bir sorundan daha fazlasıdır; önemli teknik ve algısal sonuçları vardır:
- Kötüleşen Kullanıcı Deneyimi: En bariz etki görsel kalite üzerindedir. Gürültülü bir video profesyonelce görünmez, dikkat dağıtıcıdır ve önemli ayrıntıları ayırt etmeyi zorlaştırabilir. Telekonferans gibi uygulamalarda, katılımcıların grenli ve belirsiz görünmesine neden olarak mevcudiyet hissini azaltabilir.
- Azalan Sıkıştırma Verimliliği: Bu daha az sezgisel ama aynı derecede kritik bir sorundur. Modern video kodekleri (H.264, VP9, AV1 gibi) artıklıklardan yararlanarak yüksek sıkıştırma oranları elde eder. Kareler arasındaki benzerliklere (zamansal artıklık) ve tek bir kare içindeki benzerliklere (mekansal artıklık) bakarlar. Gürültü, doğası gereği rastgele ve öngörülemezdir. Bu artıklık kalıplarını bozar. Kodlayıcı, rastgele gürültüyü korunması gereken yüksek frekanslı bir ayrıntı olarak görür ve onu gerçek içerik yerine gürültüyü kodlamak için daha fazla bit ayırmaya zorlar. Bu, ya aynı algılanan kalite için daha büyük bir dosya boyutuna ya da aynı bit hızında daha düşük kaliteye neden olur.
Gürültüyü kodlamadan önce kaldırarak, video sinyalini daha tahmin edilebilir hale getirebilir ve kodlayıcının daha verimli çalışmasını sağlayabiliriz. Bu, daha iyi görsel kaliteye, daha düşük bant genişliği kullanımına ve her yerdeki kullanıcılar için daha akıcı bir akış deneyimine yol açar.
WebCodecs Sahneye Çıkıyor: Alt Düzey Video Kontrolünün Gücü
Yıllarca, tarayıcıda doğrudan video manipülasyonu sınırlıydı. Geliştiriciler büyük ölçüde <video>
öğesinin ve genellikle GPU'dan performans düşürücü geri okumaları içeren Canvas API'sinin yetenekleriyle sınırlıydı. WebCodecs oyunu tamamen değiştiriyor.
WebCodecs, tarayıcının yerleşik medya kodlayıcılarına ve kod çözücülerine doğrudan erişim sağlayan alt düzey bir API'dir. Video düzenleyiciler, bulut oyun platformları ve gelişmiş gerçek zamanlı iletişim istemcileri gibi medya işleme üzerinde hassas kontrol gerektiren uygulamalar için tasarlanmıştır.
Odaklanacağımız temel bileşen VideoFrame
nesnesidir. Bir VideoFrame
, tek bir video karesini bir görüntü olarak temsil eder, ancak basit bir bit eşlemden çok daha fazlasıdır. Çeşitli piksel formatlarında (RGBA, I420, NV12 gibi) video verilerini tutabilen ve aşağıdakiler gibi önemli meta verileri taşıyan son derece verimli, aktarılabilir bir nesnedir:
timestamp
: Karenin mikrosaniye cinsinden sunum zamanı.duration
: Karenin mikrosaniye cinsinden süresi.codedWidth
vecodedHeight
: Karenin piksel cinsinden boyutları.format
: Verinin piksel formatı (ör. 'I420', 'RGBA').
En önemlisi, VideoFrame
, ham, sıkıştırılmamış piksel verilerini bir ArrayBuffer
'a kopyalamamıza olanak tanıyan copyTo()
adlı bir yöntem sağlar. Bu, analiz ve manipülasyon için giriş noktamızdır. Ham baytları aldıktan sonra, gürültü azaltma algoritmamızı uygulayabilir ve ardından değiştirilmiş verilerden yeni bir VideoFrame
oluşturarak işleme hattının daha ilerisine (örneğin, bir video kodlayıcıya veya bir tuvale) aktarabiliriz.
Zamansal Filtrelemeyi Anlamak
Gürültü azaltma teknikleri genel olarak iki türe ayrılabilir: mekansal ve zamansal.
- Mekansal Filtreleme: Bu teknik tek bir kare üzerinde tek başına çalışır. Gürültüyü tanımlamak ve yumuşatmak için komşu pikseller arasındaki ilişkileri analiz eder. Basit bir örnek, bir bulanıklık filtresidir. Gürültüyü azaltmada etkili olsa da, mekansal filtreler önemli ayrıntıları ve kenarları da yumuşatarak daha az keskin bir görüntüye yol açabilir.
- Zamansal Filtreleme: Bu, odaklandığımız daha sofistike bir yaklaşımdır. Zaman içinde birden çok kare üzerinde çalışır. Temel ilke, gerçek sahne içeriğinin bir kareden diğerine ilişkili olma olasılığının yüksek olması, gürültünün ise rastgele ve ilişkisiz olmasıdır. Bir pikselin değerini belirli bir konumda birkaç kare boyunca karşılaştırarak, tutarlı sinyali (gerçek görüntü) rastgele dalgalanmalardan (gürültü) ayırt edebiliriz.
Zamansal filtrelemenin en basit şekli zamansal ortalamadır. Mevcut karenin ve önceki karenin olduğunu hayal edin. Herhangi bir piksel için, 'gerçek' değeri muhtemelen mevcut karedeki değeri ile bir önceki karedeki değeri arasında bir yerdedir. Bunları harmanlayarak, rastgele gürültünün ortalamasını alabiliriz. Yeni piksel değeri basit bir ağırlıklı ortalama ile hesaplanabilir:
yeni_piksel = (alfa * mevcut_piksel) + ((1 - alfa) * önceki_piksel)
Burada, alfa
0 ile 1 arasında bir harmanlama faktörüdür. Daha yüksek bir alfa
, mevcut kareye daha fazla güvendiğimiz anlamına gelir, bu da daha az gürültü azaltma ancak daha az hareket artefaktı ile sonuçlanır. Daha düşük bir alfa
, daha güçlü gürültü azaltma sağlar ancak hareketli alanlarda 'hayaletlenme' veya izlere neden olabilir. Doğru dengeyi bulmak anahtardır.
Basit bir Zamansal Ortalama Filtresi Uygulamak
Şimdi bu konseptin pratik bir uygulamasını WebCodecs kullanarak oluşturalım. Hattımız üç ana adımdan oluşacaktır:
VideoFrame
nesnelerinden oluşan bir akış alın (örneğin, bir web kamerasından).- Her kare için, önceki karenin verilerini kullanarak zamansal filtremizi uygulayın.
- Yeni, temizlenmiş bir
VideoFrame
oluşturun.
Adım 1: Kare Akışını Ayarlama
Canlı bir VideoFrame
nesneleri akışı elde etmenin en kolay yolu, bir MediaStreamTrack
'i (getUserMedia
'dan gelen gibi) tüketen ve karelerini okunabilir bir akış olarak sunan MediaStreamTrackProcessor
kullanmaktır.
Kavramsal JavaScript Kurulumu:
async function setupVideoStream() {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor({ track });
const reader = trackProcessor.readable.getReader();
let previousFrameBuffer = null;
let previousFrameTimestamp = -1;
while (true) {
const { value: frame, done } = await reader.read();
if (done) break;
// Her bir 'frame'i işleyeceğimiz yer burası
const processedFrame = await applyTemporalFilter(frame, previousFrameBuffer);
// Bir sonraki yineleme için, *orijinal* mevcut karenin verilerini saklamamız gerekiyor
// Orijinal karenin verilerini kapatmadan önce 'previousFrameBuffer'a kopyalardınız.
// Belleği serbest bırakmak için kareleri kapatmayı unutmayın!
frame.close();
// İşlenmiş kare ile bir şeyler yapın (örneğin, tuvale çizdirin, kodlayın)
// ... ve sonra onu da kapatın!
processedFrame.close();
}
}
Adım 2: Filtreleme Algoritması - Piksel Verileriyle Çalışma
Bu, çalışmamızın özüdür. applyTemporalFilter
fonksiyonumuzun içinde, gelen karenin piksel verilerine erişmemiz gerekiyor. Basitlik açısından, karelerimizin 'RGBA' formatında olduğunu varsayalım. Her piksel 4 bayt ile temsil edilir: Kırmızı, Yeşil, Mavi ve Alfa (şeffaflık).
async function applyTemporalFilter(currentFrame, previousFrameBuffer) {
// Harmanlama faktörümüzü tanımlayın. 0.8, yeni karenin %80'i ve eskisinin %20'si anlamına gelir.
const alpha = 0.8;
// Boyutları alın
const width = currentFrame.codedWidth;
const height = currentFrame.codedHeight;
// Mevcut karenin piksel verilerini tutmak için bir ArrayBuffer ayırın.
const currentFrameSize = width * height * 4; // RGBA için piksel başına 4 bayt
const currentFrameBuffer = new Uint8Array(currentFrameSize);
await currentFrame.copyTo(currentFrameBuffer);
// Eğer bu ilk kare ise, harmanlanacak önceki bir kare yoktur.
// Olduğu gibi döndürün, ancak bir sonraki yineleme için arabelleğini saklayın.
if (!previousFrameBuffer) {
const newFrameBuffer = new Uint8Array(currentFrameBuffer);
// Global 'previousFrameBuffer'ımızı bu fonksiyonun dışında bununla güncelleyeceğiz.
return { buffer: newFrameBuffer, frame: currentFrame };
}
// Çıktı karemiz için yeni bir arabellek oluşturun.
const outputFrameBuffer = new Uint8Array(currentFrameSize);
// Ana işleme döngüsü.
for (let i = 0; i < currentFrameSize; i++) {
const currentPixelValue = currentFrameBuffer[i];
const previousPixelValue = previousFrameBuffer[i];
// Her renk kanalı için zamansal ortalama formülünü uygulayın.
// Alfa kanalını atlıyoruz (her 4. bayt).
if ((i + 1) % 4 !== 0) {
outputFrameBuffer[i] = Math.round(alpha * currentPixelValue + (1 - alpha) * previousPixelValue);
} else {
// Alfa kanalını olduğu gibi bırakın.
outputFrameBuffer[i] = currentPixelValue;
}
}
return { buffer: outputFrameBuffer, frame: currentFrame };
}
YUV formatları (I420, NV12) üzerine bir not: RGBA'nın anlaşılması kolay olsa da, çoğu video verimlilik için doğal olarak YUV renk uzaylarında işlenir. YUV ile çalışmak daha karmaşıktır çünkü renk (U, V) ve parlaklık (Y) bilgileri ayrı ayrı ('düzlemler' halinde) saklanır. Filtreleme mantığı aynı kalır, ancak her düzlemi (Y, U ve V) ayrı ayrı yinelemeniz, ilgili boyutlarına dikkat etmeniz gerekir (renk düzlemleri genellikle daha düşük çözünürlüklüdür, bu tekniğe kroma alt örneklemesi denir).
Adım 3: Yeni Filtrelenmiş `VideoFrame` Oluşturma
Döngümüz bittikten sonra, outputFrameBuffer
yeni, daha temiz karemizin piksel verilerini içerir. Şimdi bunu yeni bir VideoFrame
nesnesine sarmamız ve meta verileri orijinal kareden kopyaladığımızdan emin olmamız gerekiyor.
// Ana döngünüzün içinde, applyTemporalFilter çağrısından sonra...
const { buffer: processedBuffer, frame: originalFrame } = await applyTemporalFilter(frame, previousFrameBuffer);
// İşlenmiş arabelleğimizden yeni bir VideoFrame oluşturun.
const newFrame = new VideoFrame(processedBuffer, {
format: 'RGBA',
codedWidth: originalFrame.codedWidth,
codedHeight: originalFrame.codedHeight,
timestamp: originalFrame.timestamp,
duration: originalFrame.duration
});
// ÖNEMLİ: Sonraki yineleme için önceki kare arabelleğini güncelleyin.
// Filtrelenmiş verileri değil, *orijinal* karenin verilerini kopyalamamız gerekiyor.
// Filtrelemeden önce ayrı bir kopya yapılmalıdır.
previousFrameBuffer = new Uint8Array(originalFrameData);
// Şimdi 'newFrame'i kullanabilirsiniz. Çizdirin, kodlayın, vb.
// renderer.draw(newFrame);
// Ve en önemlisi, bellek sızıntılarını önlemek için işiniz bittiğinde kapatın.
newFrame.close();
Bellek Yönetimi Kritik Öneme Sahiptir: VideoFrame
nesneleri büyük miktarda sıkıştırılmamış video verisi tutabilir ve JavaScript yığınının dışındaki bellek tarafından desteklenebilir. İşiniz biten her kare için frame.close()
yöntemini çağırmanız gerekir. Bunu yapmamak, hızla bellek tükenmesine ve sekmenin çökmesine yol açacaktır.
Performans Değerlendirmeleri: JavaScript ve WebAssembly Karşılaştırması
Yukarıdaki saf JavaScript uygulaması öğrenme ve gösterim için mükemmeldir. Ancak, 30 FPS, 1080p (1920x1080) bir video için, döngümüzün saniyede 248 milyondan fazla hesaplama yapması gerekir! (1920 * 1080 * 4 bayt * 30 fps). Modern JavaScript motorları inanılmaz derecede hızlı olsa da, bu piksel başına işleme, daha performans odaklı bir teknoloji için mükemmel bir kullanım durumudur: WebAssembly (Wasm).
WebAssembly Yaklaşımı
WebAssembly, C++, Rust veya Go gibi dillerde yazılmış kodu tarayıcıda neredeyse yerel hızda çalıştırmanıza olanak tanır. Zamansal filtremizin mantığı bu dillerde uygulanması basittir. Girdi ve çıktı arabelleklerine işaretçiler alan ve aynı yinelemeli harmanlama işlemini gerçekleştiren bir fonksiyon yazardınız.
Wasm için kavramsal C++ fonksiyonu:
extern "C" {
void apply_temporal_filter(unsigned char* current_frame, unsigned char* previous_frame, unsigned char* output_frame, int buffer_size, float alpha) {
for (int i = 0; i < buffer_size; ++i) {
if ((i + 1) % 4 != 0) { // Alfa kanalını atla
output_frame[i] = (unsigned char)(alpha * current_frame[i] + (1.0 - alpha) * previous_frame[i]);
} else {
output_frame[i] = current_frame[i];
}
}
}
}
JavaScript tarafından, bu derlenmiş Wasm modülünü yüklersiniz. Temel performans avantajı, belleği paylaşmaktan gelir. JavaScript'te Wasm modülünün doğrusal belleği tarafından desteklenen ArrayBuffer
'lar oluşturabilirsiniz. Bu, kare verilerini pahalı bir kopyalama olmadan Wasm'a geçirmenizi sağlar. Tüm piksel işleme döngüsü daha sonra tek, yüksek düzeyde optimize edilmiş bir Wasm fonksiyon çağrısı olarak çalışır, bu da bir JavaScript `for` döngüsünden önemli ölçüde daha hızlıdır.
Gelişmiş Zamansal Filtreleme Teknikleri
Basit zamansal ortalama harika bir başlangıç noktasıdır, ancak önemli bir dezavantajı vardır: hareket bulanıklığı veya 'hayaletlenme' yaratır. Bir nesne hareket ettiğinde, mevcut karedeki pikselleri, önceki kareden gelen arka plan pikselleriyle harmanlanır ve bir iz oluşturur. Gerçekten profesyonel düzeyde bir filtre oluşturmak için hareketi hesaba katmamız gerekir.
Harekete Duyarlı Zamansal Filtreleme (MCTF)
Zamansal gürültü azaltma için altın standart, Harekete Duyarlı Zamansal Filtrelemedir. Bir pikseli önceki karedeki aynı (x, y) koordinatındaki pikselle körü körüne harmanlamak yerine, MCTF önce o pikselin nereden geldiğini bulmaya çalışır.
Süreç şunları içerir:
- Hareket Tahmini: Algoritma mevcut kareyi bloklara böler (örneğin, 16x16 piksel). Her blok için, önceki karede en benzer olan (örneğin, en düşük Mutlak Farklar Toplamına sahip olan) bloğu arar. Bu iki blok arasındaki yer değiştirmeye 'hareket vektörü' denir.
- Hareket Telafisi: Daha sonra, blokları hareket vektörlerine göre kaydırarak önceki karenin 'harekete duyarlı' bir versiyonunu oluşturur.
- Filtreleme: Son olarak, mevcut kare ile bu yeni, harekete duyarlı önceki kare arasında zamansal ortalama işlemini gerçekleştirir.
Bu şekilde, hareket eden bir nesne, yeni ortaya çıkardığı arka planla değil, önceki kareden kendisiyle harmanlanır. Bu, hayaletlenme artefaktlarını büyük ölçüde azaltır. Hareket tahmini uygulamak, hesaplama açısından yoğun ve karmaşıktır, genellikle gelişmiş algoritmalar gerektirir ve neredeyse yalnızca WebAssembly veya hatta WebGPU hesaplama gölgelendiricileri için bir görevdir.
Uyarlanabilir Filtreleme
Başka bir geliştirme, filtreyi uyarlanabilir hale getirmektir. Tüm kare için sabit bir alfa
değeri kullanmak yerine, bunu yerel koşullara göre değiştirebilirsiniz.
- Hareket Uyarlanabilirliği: Yüksek hareket algılanan alanlarda, neredeyse tamamen mevcut kareye dayanmak için
alfa
'yı artırabilir (örneğin, 0.95 veya 1.0'a), böylece herhangi bir hareket bulanıklığını önleyebilirsiniz. Statik alanlarda (arka plandaki bir duvar gibi), çok daha güçlü gürültü azaltma içinalfa
'yı azaltabilirsiniz (örneğin, 0.5'e). - Parlaklık Uyarlanabilirliği: Gürültü genellikle bir görüntünün daha karanlık alanlarında daha belirgindir. Filtre, ayrıntıyı korumak için gölgelerde daha agresif ve parlak alanlarda daha az agresif hale getirilebilir.
Pratik Kullanım Alanları ve Uygulamalar
Tarayıcıda yüksek kaliteli gürültü azaltma yapabilme yeteneği, sayısız olasılığın kilidini açar:
- Gerçek Zamanlı İletişim (WebRTC): Bir kullanıcının web kamerası beslemesini video kodlayıcıya gönderilmeden önce ön işleme tabi tutun. Bu, düşük ışıklı ortamlardaki video görüşmeleri için büyük bir kazançtır, görsel kaliteyi artırır ve gereken bant genişliğini azaltır.
- Web Tabanlı Video Düzenleme: Tarayıcı içi bir video düzenleyicide 'Gürültü Azaltma' filtresini bir özellik olarak sunarak, kullanıcıların yükledikleri görüntüleri sunucu tarafında işleme olmadan temizlemelerine olanak tanıyın.
- Bulut Oyun ve Uzak Masaüstü: Sıkıştırma artefaktlarını azaltmak ve daha net, daha kararlı bir resim sağlamak için gelen video akışlarını temizleyin.
- Bilgisayarla Görme Ön İşlemesi: Web tabanlı AI/ML uygulamaları (nesne takibi veya yüz tanıma gibi) için, girdi videosundaki gürültüyü azaltmak veriyi stabilize edebilir ve daha doğru ve güvenilir sonuçlara yol açabilir.
Zorluklar ve Gelecek Yönelimler
Güçlü olmasına rağmen, bu yaklaşım zorluklardan yoksun değildir. Geliştiricilerin şunlara dikkat etmesi gerekir:
- Performans: HD veya 4K video için gerçek zamanlı işleme zorludur. Verimli uygulama, genellikle WebAssembly ile, bir zorunluluktur.
- Bellek: Bir veya daha fazla önceki kareyi sıkıştırılmamış arabellekler olarak saklamak, önemli miktarda RAM tüketir. Dikkatli yönetim esastır.
- Gecikme: Her işleme adımı gecikme ekler. Gerçek zamanlı iletişim için, bu hattın fark edilebilir gecikmeleri önlemek için yüksek düzeyde optimize edilmesi gerekir.
- WebGPU ile Gelecek: Gelişmekte olan WebGPU API'si, bu tür işler için yeni bir sınır sağlayacaktır. Bu piksel başına algoritmaların, sistemin GPU'sunda yüksek düzeyde paralel hesaplama gölgelendiricileri olarak çalıştırılmasına izin verecek ve CPU'daki WebAssembly'nin bile ötesinde başka bir büyük performans sıçraması sunacaktır.
Sonuç
WebCodecs API'si, web'de gelişmiş medya işleme için yeni bir çağ başlatıyor. Geleneksel kara kutu <video>
öğesinin engellerini yıkıyor ve geliştiricilere gerçekten profesyonel video uygulamaları oluşturmak için gereken ince ayarlı kontrolü veriyor. Zamansal gürültü azaltma, gücünün mükemmel bir örneğidir: hem kullanıcı tarafından algılanan kaliteyi hem de altta yatan teknik verimliliği doğrudan ele alan sofistike bir teknik.
Bireysel VideoFrame
nesnelerini yakalayarak gürültüyü azaltmak, sıkıştırılabilirliği iyileştirmek ve üstün bir video deneyimi sunmak için güçlü filtreleme mantığı uygulayabildiğimizi gördük. Basit bir JavaScript uygulaması harika bir başlangıç noktası olsa da, üretime hazır, gerçek zamanlı bir çözüme giden yol, WebAssembly'nin performansından ve gelecekte WebGPU'nun paralel işlem gücünden geçmektedir.
Bir dahaki sefere bir web uygulamasında grenli bir video gördüğünüzde, bunu düzeltmek için gereken araçların artık ilk kez doğrudan web geliştiricilerinin elinde olduğunu unutmayın. Web'de video ile bir şeyler inşa etmek için heyecan verici bir zaman.