Otključajte naprednu obradu videa u pregledniku. Naučite izravno pristupati i manipulirati sirovim podacima ravnina VideoFramea pomoću WebCodecs API-ja za efekte i analizu.
Pristup ravninama WebCodecs VideoFramea: Dubinski uvid u manipulaciju sirovim video podacima
Godinama se visokoučinkovita obrada videa u web pregledniku činila kao daleki san. Programeri su često bili ograničeni mogućnostima <video> elementa i 2D Canvas API-ja, koji su, iako moćni, uvodili uska grla u performansama i ograničavali pristup temeljnim sirovim video podacima. Dolazak WebCodecs API-ja iz temelja je promijenio taj krajolik, pružajući niskorazinski pristup ugrađenim medijskim kodecima preglednika. Jedna od njegovih najrevolucionarnijih značajki je mogućnost izravnog pristupa i manipulacije sirovim podacima pojedinačnih video okvira putem VideoFrame objekta.
Ovaj članak je sveobuhvatan vodič za programere koji žele nadići jednostavnu reprodukciju videa. Istražit ćemo zamršenosti pristupa ravninama VideoFramea, demistificirati koncepte poput prostora boja i rasporeda memorije te pružiti praktične primjere kako bismo vas osnažili za izgradnju sljedeće generacije video aplikacija u pregledniku, od filtara u stvarnom vremenu do sofisticiranih zadataka računalnog vida.
Preduvjeti
Da biste maksimalno iskoristili ovaj vodič, trebali biste imati solidno razumijevanje:
- Moderni JavaScript: Uključujući asinkrono programiranje (
async/await, Promises). - Osnovni koncepti videa: Poznavanje pojmova kao što su okviri, rezolucija i kodeci je korisno.
- API-ji preglednika: Iskustvo s API-jima poput Canvas 2D ili WebGL-a bit će korisno, ali nije strogo nužno.
Razumijevanje video okvira, prostora boja i ravnina
Prije nego što zaronimo u API, moramo prvo izgraditi čvrst mentalni model o tome kako podaci video okvira zapravo izgledaju. Digitalni video je slijed statičnih slika, odnosno okvira. Svaki okvir je mreža piksela, a svaki piksel ima boju. Način na koji se ta boja pohranjuje definiran je prostorom boja i formatom piksela.
RGBA: Izvorni jezik weba
Većina web programera upoznata je s RGBA modelom boja. Svaki piksel predstavljen je s četiri komponente: crvena (Red), zelena (Green), plava (Blue) i alfa (prozirnost). Podaci se obično pohranjuju isprepleteno (interleaved) u memoriji, što znači da se R, G, B i A vrijednosti za jedan piksel pohranjuju jedna za drugom:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
U ovom modelu, cijela slika pohranjena je u jednom, kontinuiranom bloku memorije. Možemo to zamisliti kao da imamo jednu "ravninu" podataka.
YUV: Jezik video kompresije
Video kodeci, međutim, rijetko rade izravno s RGBA. Oni preferiraju YUV (ili točnije, Y'CbCr) prostore boja. Ovaj model razdvaja informacije o slici na:
- Y (Luma): Informacije o svjetlini ili sivim tonovima. Ljudsko oko je najosjetljivije na promjene u lumi.
- U (Cb) i V (Cr): Informacije o krominanciji ili razlici u boji. Ljudsko oko je manje osjetljivo na detalje u boji nego na detalje u svjetlini.
Ovo razdvajanje ključno je za učinkovitu kompresiju. Smanjenjem rezolucije U i V komponenata – tehnika koja se naziva poduzorkovanje krome (chroma subsampling) – možemo značajno smanjiti veličinu datoteke s minimalnim primjetnim gubitkom kvalitete. To dovodi do planarnih formata piksela, gdje se Y, U i V komponente pohranjuju u odvojenim memorijskim blokovima, ili "ravninama".
Uobičajeni format je I420 (vrsta YUV 4:2:0), gdje za svaki blok piksela 2x2 postoje četiri Y uzorka, ali samo jedan U i jedan V uzorak. To znači da U i V ravnine imaju pola širine i pola visine Y ravnine.
Razumijevanje ove razlike je ključno jer vam WebCodecs daje izravan pristup upravo tim ravninama, točno onako kako ih dekoder pruža.
VideoFrame objekt: Vaš ulaz u podatke piksela
Središnji dio ove slagalice je VideoFrame objekt. On predstavlja jedan okvir videa i ne sadrži samo podatke o pikselima, već i važne metapodatke.
Ključna svojstva VideoFramea
format: Niz znakova koji označava format piksela (npr. 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Pune dimenzije okvira kako su pohranjene u memoriji, uključujući bilo kakvo dopunjavanje (padding) koje zahtijeva kodek.displayWidth/displayHeight: Dimenzije koje bi se trebale koristiti za prikazivanje okvira.timestamp: Prezentacijska vremenska oznaka okvira u mikrosekundama.duration: Trajanje okvira u mikrosekundama.
Čarobna metoda: copyTo()
Primarna metoda za pristup sirovim podacima piksela je videoFrame.copyTo(destination, options). Ova asinkrona metoda kopira podatke ravnina okvira u međuspremnik (buffer) koji vi pružite.
destination:ArrayBufferili tipizirani niz (poputUint8Array) dovoljno velik da primi podatke.options: Objekt koji specificira koje ravnine treba kopirati i njihov raspored u memoriji. Ako se izostavi, kopira sve ravnine u jedan kontinuirani međuspremnik.
Metoda vraća Promise koji se razrješava s nizom PlaneLayout objekata, po jedan za svaku ravninu u okviru. Svaki PlaneLayout objekt sadrži dva ključna podatka:
offset: Pomak u bajtovima gdje počinju podaci ove ravnine unutar odredišnog međuspremnika.stride: Broj bajtova između početka jednog reda piksela i početka sljedećeg reda za tu ravninu.
Ključan koncept: Stride naspram širine
Ovo je jedan od najčešćih izvora zabune za programere koji su novi u niskorazinskom grafičkom programiranju. Ne možete pretpostaviti da je svaki red podataka piksela čvrsto zbijen jedan za drugim.
- Širina (Width) je broj piksela u jednom redu slike.
- Stride (također zvan pitch ili line step) je broj bajtova u memoriji od početka jednog reda do početka sljedećeg.
Često će stride biti veći od širina * bajtova_po_pikselu. To je zato što se memorija često dopunjava (padding) kako bi se poravnala s hardverskim granicama (npr. granice od 32 ili 64 bajta) radi brže obrade od strane CPU-a ili GPU-a. Uvijek morate koristiti stride za izračunavanje memorijske adrese piksela u određenom redu.
Ignoriranje stridea dovest će do iskošenih ili iskrivljenih slika i neispravnog pristupa podacima.
Praktični primjer 1: Pristup i prikaz ravnine sivih tonova
Počnimo s jednostavnim, ali moćnim primjerom. Većina videa na webu kodirana je u YUV formatu poput I420. 'Y' ravnina je zapravo potpuni prikaz slike u sivim tonovima. Možemo izdvojiti samo tu ravninu i iscrtati je na canvas.
async function displayGrayscale(videoFrame) {
// Pretpostavljamo da je videoFrame u YUV formatu poput 'I420' ili 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Ovaj primjer zahtijeva YUV 4:2:0 planarni format.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y ravnina je uvijek prva.
// Stvorite međuspremnik koji će sadržavati samo podatke Y ravnine.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Kopirajte Y ravninu u naš međuspremnik.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Sada, yPlaneData sadrži sirove piksele sivih tonova.
// Moramo to iscrtati. Stvorit ćemo RGBA međuspremnik za canvas.
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);
// Iterirajte preko piksela canvasa i popunite ih podacima iz Y ravnine.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Važno: Koristite stride za pronalaženje ispravnog izvornog indeksa!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Izračunajte odredišni indeks u RGBA ImageData međuspremniku.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Crvena
imageData.data[rgbaIndex + 1] = luma; // Zelena
imageData.data[rgbaIndex + 2] = luma; // Plava
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRITIČNO: Uvijek zatvorite VideoFrame kako biste oslobodili njegovu memoriju.
videoFrame.close();
}
Ovaj primjer ističe nekoliko ključnih koraka: identificiranje ispravnog rasporeda ravnine, alociranje odredišnog međuspremnika, korištenje copyTo za izdvajanje podataka i ispravno iteriranje preko podataka pomoću stridea za konstruiranje nove slike.
Praktični primjer 2: Manipulacija na licu mjesta (Sepia filtar)
Sada izvršimo izravnu manipulaciju podacima. Sepia filtar je klasičan efekt koji je lako implementirati. Za ovaj primjer, lakše je raditi s RGBA okvirom, koji možete dobiti s canvasa ili WebGL konteksta.
async function applySepiaFilter(videoFrame) {
// Ovaj primjer pretpostavlja da je ulazni okvir 'RGBA' ili 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Primjer sa sepia filtrom zahtijeva RGBA okvir.');
videoFrame.close();
return null;
}
// Alocirajte međuspremnik za pohranu podataka o pikselima.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA je jedna ravnina
// Sada, manipulirajte podacima u međuspremniku.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 bajta po pikselu (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]) ostaje nepromijenjena.
}
}
// Stvorite *novi* VideoFrame s izmijenjenim podacima.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Ne zaboravite zatvoriti izvorni okvir!
videoFrame.close();
return newFrame;
}
Ovo demonstrira potpuni ciklus čitanja-mijenjanja-pisanja: kopirajte podatke, prođite kroz njih petljom koristeći stride, primijenite matematičku transformaciju na svaki piksel i konstruirajte novi VideoFrame s rezultirajućim podacima. Ovaj novi okvir se zatim može iscrtati na canvas, poslati u VideoEncoder ili proslijediti na drugi korak obrade.
Performanse su važne: JavaScript naspram WebAssemblyja (WASM)
Iteriranje preko milijuna piksela za svaki okvir (1080p okvir ima preko 2 milijuna piksela, ili 8 milijuna točaka podataka u RGBA) u JavaScriptu može biti sporo. Iako su moderni JS enginei nevjerojatno brzi, za obradu videa visoke rezolucije (HD, 4K) u stvarnom vremenu, ovaj pristup može lako preopteretiti glavnu nit (main thread), što dovodi do isprekidanog korisničkog iskustva.
Ovdje WebAssembly (WASM) postaje neophodan alat. WASM vam omogućuje pokretanje koda napisanog u jezicima poput C++, Rusta ili Goa brzinom bliskom nativnoj unutar preglednika. Tijek rada za obradu videa postaje:
- U JavaScriptu: Koristite
videoFrame.copyTo()da biste dobili sirove podatke piksela uArrayBuffer. - Proslijedite u WASM: Proslijedite referencu na ovaj međuspremnik u vaš kompajlirani WASM modul. Ovo je vrlo brza operacija jer ne uključuje kopiranje podataka.
- U WASM-u (C++/Rust): Izvršite svoje visoko optimizirane algoritme za obradu slike izravno na memorijskom međuspremniku. Ovo je redovima veličine brže od JavaScript petlje.
- Povratak u JavaScript: Kada WASM završi, kontrola se vraća JavaScriptu. Tada možete koristiti izmijenjeni međuspremnik za stvaranje novog
VideoFramea.
Za bilo koju ozbiljnu aplikaciju za manipulaciju videom u stvarnom vremenu – kao što su virtualne pozadine, detekcija objekata ili složeni filtri – korištenje WebAssemblyja nije samo opcija; to je nužnost.
Rukovanje različitim formatima piksela (npr. I420, NV12)
Iako je RGBA jednostavan, najčešće ćete primati okvire u planarnim YUV formatima iz VideoDecodera. Pogledajmo kako rukovati potpuno planarnim formatom kao što je I420.
VideoFrame u I420 formatu imat će tri deskriptora rasporeda u svom layout nizu:
layout[0]: Y ravnina (luma). Dimenzije sucodedWidthxcodedHeight.layout[1]: U ravnina (kroma). Dimenzije sucodedWidth/2xcodedHeight/2.layout[2]: V ravnina (kroma). Dimenzije sucodedWidth/2xcodedHeight/2.
Evo kako biste kopirali sve tri ravnine u jedan međuspremnik:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts je niz od 3 PlaneLayout objekta
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: ... }
// Sada možete pristupiti svakoj ravnini unutar `allPlanesData` međuspremnika
// koristeći njezin specifični pomak i stride.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Imajte na umu da su dimenzije krome prepolovljene!
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();
}
Drugi uobičajeni format je NV12, koji je polu-planaran. Ima dvije ravnine: jednu za Y, i drugu ravninu gdje su U i V vrijednosti isprepletene (npr. [U1, V1, U2, V2, ...]). WebCodecs API to rješava transparentno; VideoFrame u NV12 formatu jednostavno će imati dva rasporeda u svom layout nizu.
Izazovi i najbolje prakse
Rad na ovoj niskoj razini je moćan, ali dolazi s odgovornostima.
Upravljanje memorijom je od najveće važnosti
VideoFrame zauzima značajnu količinu memorije, kojom se često upravlja izvan hrpe (heap) JavaScript sakupljača smeća (garbage collector). Ako eksplicitno ne oslobodite tu memoriju, prouzročit ćete curenje memorije koje može srušiti karticu preglednika.
Uvijek, ali uvijek pozovite videoFrame.close() kada završite s okvirom.
Asinkrona priroda
Sav pristup podacima je asinkron. Arhitektura vaše aplikacije mora ispravno rukovati tijekom Promisea i async/await kako bi se izbjegla stanja utrke (race conditions) i osigurao gladak procesorski cjevovod.
Kompatibilnost s preglednicima
WebCodecs je moderan API. Iako je podržan u svim većim preglednicima, uvijek provjerite njegovu dostupnost i budite svjesni bilo kakvih specifičnih detalja implementacije ili ograničenja pojedinih proizvođača. Koristite detekciju značajki prije pokušaja korištenja API-ja.
Zaključak: Nova granica za web video
Mogućnost izravnog pristupa i manipulacije sirovim podacima ravnina VideoFramea putem WebCodecs API-ja predstavlja promjenu paradigme za web-bazirane medijske aplikacije. Uklanja "crnu kutiju" <video> elementa i daje programerima granuliranu kontrolu prethodno rezerviranu za nativne aplikacije.
Razumijevanjem osnova rasporeda video memorije – ravnina, stridea i formata boja – te korištenjem snage WebAssemblyja za operacije kritične za performanse, sada možete graditi nevjerojatno sofisticirane alate za obradu videa izravno u pregledniku. Od gradacije boja u stvarnom vremenu i prilagođenih vizualnih efekata do strojnog učenja na strani klijenta i video analize, mogućnosti su ogromne. Era visokoučinkovitog, niskorazinskog videa na webu je uistinu započela.