Ota käyttöön edistynyt selaimessa tapahtuva videonkäsittely. Opi käyttämään ja manipuloimaan raakaa VideoFrame-tasodataa suoraan WebCodecs API:n avulla mukautettuja tehosteita ja analyysejä varten.
WebCodecs VideoFrame -tasojen käyttö: Syväsukellus raa'an videodatan manipulointiin
Vuosien ajan suorituskykyinen videonkäsittely selaimessa tuntui kaukaiselta unelmalta. Kehittäjät olivat usein rajoittuneita <video>-elementin ja 2D Canvas API:n asettamiin rajoituksiin, jotka, vaikka olivatkin tehokkaita, aiheuttivat suorituskyvyn pullonkauloja ja rajoittivat pääsyä alla olevaan raakaan videodataan. WebCodecs API:n saapuminen on muuttanut tämän tilanteen perusteellisesti tarjoamalla matalan tason pääsyn selaimen sisäänrakennettuihin mediakoodekkeihin. Yksi sen mullistavimmista ominaisuuksista on kyky suoraan käyttää ja manipuloida yksittäisten videokehysten raakadataa VideoFrame-olion kautta.
Tämä artikkeli on kattava opas kehittäjille, jotka haluavat siirtyä yksinkertaista videotoistoa pidemmälle. Tutustumme VideoFrame-tasojen käytön hienouksiin, selvennämme käsitteitä kuten väriavaruudet ja muistiasettelu, sekä tarjoamme käytännön esimerkkejä, joiden avulla voit rakentaa seuraavan sukupolven selainpohjaisia videosovelluksia reaaliaikaisista suodattimista kehittyneisiin konenäkötehtäviin.
Edellytykset
Saadaksesi tästä oppaasta parhaan hyödyn, sinulla tulisi olla vankka ymmärrys seuraavista asioista:
- Moderni JavaScript: Sisältäen asynkronisen ohjelmoinnin (
async/await, Promises). - Videon peruskäsitteet: Termien, kuten kehysten, resoluution ja koodekkien, tuntemus on hyödyllistä.
- Selain-API:t: Kokemus API:eista, kuten Canvas 2D tai WebGL, on eduksi, mutta ei ehdottoman välttämätöntä.
Videokehysten, väriavaruuksien ja tasojen ymmärtäminen
Ennen kuin sukellamme API:in, meidän on ensin luotava vankka mielikuva siitä, miltä videokehyksen data todellisuudessa näyttää. Digitaalinen video on sarja still-kuvia eli kehyksiä. Jokainen kehys on pikseliruudukko, ja jokaisella pikselillä on väri. Miten tämä väri tallennetaan, määritellään väriavaruuden ja pikselimuodon avulla.
RGBA: Webin äidinkieli
Useimmat web-kehittäjät tuntevat RGBA-värimallin. Jokainen pikseli esitetään neljällä komponentilla: punainen (Red), vihreä (Green), sininen (Blue) ja alfa (läpinäkyvyys, Alpha). Data tallennetaan tyypillisesti muistiin lomitettuna (interleaved), mikä tarkoittaa, että yhden pikselin R-, G-, B- ja A-arvot tallennetaan peräkkäin:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
Tässä mallissa koko kuva tallennetaan yhteen, yhtenäiseen muistilohkoon. Voimme ajatella tätä niin, että datalla on vain yksi "taso".
YUV: Videopakkauksen kieli
Videokoodekit työskentelevät kuitenkin harvoin suoraan RGBA:n kanssa. Ne suosivat YUV- (tai tarkemmin sanottuna Y'CbCr-) väriavaruuksia. Tämä malli erottaa kuvainformaation seuraaviin osiin:
- Y (Luma): Kirkkaus- tai harmaasävyinformaatio. Ihmissilmä on herkin luman muutoksille.
- U (Cb) ja V (Cr): Krominanssi- eli värieroinformaatio. Ihmissilmä on vähemmän herkkä värien yksityiskohdille kuin kirkkauden yksityiskohdille.
Tämä erottelu on avain tehokkaaseen pakkaukseen. Pienentämällä U- ja V-komponenttien resoluutiota – tekniikkaa kutsutaan kroma-alinäytteistykseksi (chroma subsampling) – voimme pienentää tiedostokokoa merkittävästi minimaalisella havaittavalla laadun heikkenemisellä. Tämä johtaa tasomaisiin (planar) pikselimuotoihin, joissa Y-, U- ja V-komponentit tallennetaan erillisiin muistilohkoihin eli "tasoihin".
Yleinen muoto on I420 (YUV 4:2:0 -tyyppi), jossa jokaista 2x2 pikselin lohkoa kohden on neljä Y-näytettä, mutta vain yksi U- ja yksi V-näyte. Tämä tarkoittaa, että U- ja V-tasojen leveys ja korkeus ovat puolet Y-tason leveydestä ja korkeudesta.
Tämän eron ymmärtäminen on kriittistä, koska WebCodecs antaa sinulle suoran pääsyn juuri näihin tasoihin, täsmälleen sellaisina kuin dekooderi ne tarjoaa.
VideoFrame-olio: Porttisi pikselidataan
Tämän palapelin keskeinen osa on VideoFrame-olio. Se edustaa yhtä videokehystä ja sisältää pikselidatan lisäksi myös tärkeää metadataa.
VideoFrame-olion keskeiset ominaisuudet
format: Merkkijono, joka ilmaisee pikselimuodon (esim. 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Kehyksen täydet mitat sellaisina kuin ne on tallennettu muistiin, mukaan lukien koodekin vaatima mahdollinen täyte (padding).displayWidth/displayHeight: Mitat, joita tulisi käyttää kehyksen näyttämiseen.timestamp: Kehyksen esitysaikaleima mikrosekunteina.duration: Kehyksen kesto mikrosekunteina.
Taianomainen metodi: copyTo()
Ensisijainen metodi raa'an pikselidatan käyttämiseen on videoFrame.copyTo(destination, options). Tämä asynkroninen metodi kopioi kehyksen tasodatan antamaasi puskuriin.
destination:ArrayBuffertai tyypitetty taulukko (kutenUint8Array), joka on riittävän suuri datan säilyttämiseen.options: Olio, joka määrittelee, mitkä tasot kopioidaan ja niiden muistiasettelun. Jos tämä jätetään pois, kaikki tasot kopioidaan yhteen yhtenäiseen puskuriin.
Metodi palauttaa Promisen, joka ratkeaa taulukolla PlaneLayout-olioita, yksi kutakin kehyksen tasoa kohti. Jokainen PlaneLayout-olio sisältää kaksi olennaista tietoa:
offset: Tavu-offset, josta tämän tason data alkaa kohdepuskurissa.stride: Tavujen määrä pikselirivin alusta seuraavan rivin alkuun kyseisellä tasolla.
Kriittinen käsite: Riviaskel (Stride) vs. Leveys (Width)
Tämä on yksi yleisimmistä sekaannuksen aiheista matalan tason grafiikkaohjelmointiin perehtyville kehittäjille. Et voi olettaa, että jokainen pikselidatarivi on pakattu tiiviisti peräkkäin.
- Leveys (Width) on pikselien määrä kuvan yhdessä rivissä.
- Riviaskel (Stride) (myös pitch tai line step) on tavujen määrä muistissa yhden rivin alusta seuraavan rivin alkuun.
Usein stride (riviaskel) on suurempi kuin leveys * tavut_per_pikseli. Tämä johtuu siitä, että muistiin lisätään usein täytettä laitteistorajojen (esim. 32 tai 64 tavun rajat) kanssa tasaamiseksi, mikä nopeuttaa suorittimen tai grafiikkaprosessorin käsittelyä. Sinun on aina käytettävä riviaskelta laskeaksesi pikselin muistiosoitteen tietyllä rivillä.
Riviaskeleen huomiotta jättäminen johtaa vääristyneisiin kuviin ja virheelliseen datan käyttöön.
Käytännön esimerkki 1: Harmaasävytason käyttäminen ja näyttäminen
Aloitetaan yksinkertaisella mutta tehokkaalla esimerkillä. Suurin osa web-videosta on koodattu YUV-muotoon, kuten I420. 'Y'-taso on käytännössä täydellinen harmaasävyesitys kuvasta. Voimme purkaa vain tämän tason ja renderöidä sen canvaalle.
async function displayGrayscale(videoFrame) {
// Oletamme, että videoFrame on YUV-muodossa, kuten 'I420' tai 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Tämä esimerkki vaatii YUV 4:2:0 -tasomuodon.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Y-taso on aina ensimmäinen.
// Luo puskuri vain Y-tason datalle.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Kopioi Y-taso puskuriimme.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Nyt yPlaneData sisältää raa'at harmaasävypikselit.
// Meidän täytyy renderöidä se. Luomme RGBA-puskurin canvaalle.
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);
// Käy läpi canvaan pikselit ja täytä ne Y-tason datalla.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Tärkeää: Käytä riviaskelta (stride) löytääksesi oikean lähdeindeksin!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Laske kohdeindeksi RGBA ImageData -puskurissa.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Punainen
imageData.data[rgbaIndex + 1] = luma; // Vihreä
imageData.data[rgbaIndex + 2] = luma; // Sininen
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRIITTISTÄ: Sulje aina VideoFrame vapauttaaksesi sen muistin.
videoFrame.close();
}
Tämä esimerkki korostaa useita keskeisiä vaiheita: oikean tason asettelun tunnistaminen, kohdepuskurin varaaminen, copyTo-metodin käyttö datan purkamiseen ja datan oikeaoppinen läpikäynti stride-arvoa käyttäen uuden kuvan rakentamiseksi.
Käytännön esimerkki 2: Paikallaan tapahtuva manipulointi (seepiasuodatin)
Tehdään nyt suoraa datan manipulointia. Seepiasuodatin on klassinen tehoste, joka on helppo toteuttaa. Tässä esimerkissä on helpompaa työskennellä RGBA-kehyksen kanssa, jonka saatat saada canvaalta tai WebGL-kontekstista.
async function applySepiaFilter(videoFrame) {
// Tämä esimerkki olettaa, että syötekehys on 'RGBA' tai 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Seepiasuodatin-esimerkki vaatii RGBA-kehyksen.');
videoFrame.close();
return null;
}
// Varaa puskuri pikselidatalle.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA on yksi taso
// Nyt manipuloidaan dataa puskurissa.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 tavua per pikseli (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]) pysyy muuttumattomana.
}
}
// Luo *uusi* VideoFrame muokatulla datalla.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Älä unohda sulkea alkuperäistä kehystä!
videoFrame.close();
return newFrame;
}
Tämä esittelee täydellisen luku-muokkaus-kirjoitus-syklin: kopioi data ulos, käy se läpi riviaskelta käyttäen, sovella matemaattinen muunnos jokaiseen pikseliin ja rakenna uusi VideoFrame tuloksena saadulla datalla. Tämä uusi kehys voidaan sitten renderöidä canvaalle, lähettää VideoEncoder-kooderille tai välittää toiseen käsittelyvaiheeseen.
Suorituskyvyllä on väliä: JavaScript vs. WebAssembly (WASM)
Miljoonien pikselien läpikäynti jokaisessa kehyksessä (1080p-kehyksessä on yli 2 miljoonaa pikseliä eli 8 miljoonaa datapistettä RGBA:ssa) JavaScriptillä voi olla hidasta. Vaikka modernit JS-moottorit ovat uskomattoman nopeita, korkearesoluutioisen videon (HD, 4K) reaaliaikaisessa käsittelyssä tämä lähestymistapa voi helposti ylikuormittaa pääsäikeen, mikä johtaa pätkivään käyttökokemukseen.
Tässä kohtaa WebAssembly (WASM) nousee välttämättömäksi työkaluksi. WASM mahdollistaa C++:n, Rustin tai Go:n kaltaisilla kielillä kirjoitetun koodin suorittamisen lähes natiivinopeudella selaimen sisällä. Videonkäsittelyn työnkulku muotoutuu seuraavanlaiseksi:
- JavaScriptissä: Käytä
videoFrame.copyTo()saadaksesi raa'an pikselidatanArrayBuffer-puskuriin. - Välitä WASM:iin: Välitä viittaus tähän puskuriin käännettyyn WASM-moduuliisi. Tämä on erittäin nopea toimenpide, koska se ei vaadi datan kopiointia.
- WASM:ssa (C++/Rust): Suorita korkeasti optimoidut kuvankäsittelyalgoritmisi suoraan muistipuskurille. Tämä on kertaluokkia nopeampaa kuin JavaScript-silmukka.
- Palaa JavaScriptiin: Kun WASM on valmis, kontrolli palaa JavaScriptille. Voit sitten käyttää muokattua puskuria uuden
VideoFrame-olion luomiseen.
Kaikissa vakavissa, reaaliaikaisissa videonmanipulointisovelluksissa – kuten virtuaalitaustoissa, kohteentunnistuksessa tai monimutkaisissa suodattimissa – WebAssemblyn hyödyntäminen ei ole vain vaihtoehto, se on välttämättömyys.
Eri pikselimuotojen käsittely (esim. I420, NV12)
Vaikka RGBA on yksinkertainen, saat useimmiten VideoDecoder-dekooderilta kehyksiä tasomaisissa YUV-muodoissa. Katsotaanpa, miten käsitellään täysin tasomaista muotoa, kuten I420.
I420-muotoisella VideoFrame-oliolla on kolme asettelukuvaajaa sen layout-taulukossa:
layout[0]: Y-taso (luma). Mitat ovatcodedWidthxcodedHeight.layout[1]: U-taso (kroma). Mitat ovatcodedWidth/2xcodedHeight/2.layout[2]: V-taso (kroma). Mitat ovatcodedWidth/2xcodedHeight/2.
Näin kopioisit kaikki kolme tasoa yhteen puskuriin:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts on taulukko, jossa on 3 PlaneLayout-oliota
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: ... }
// Voit nyt käyttää kutakin tasoa `allPlanesData`-puskurin sisällä
// käyttämällä sen omaa offsetia ja riviaskelta.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Huomaa, että kroman mitat ovat puolitettu!
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();
}
Toinen yleinen muoto on NV12, joka on puoli-tasomainen (semi-planar). Siinä on kaksi tasoa: yksi Y:lle ja toinen taso, jossa U- ja V-arvot ovat lomittain (esim. [U1, V1, U2, V2, ...]). WebCodecs API hoitaa tämän läpinäkyvästi; NV12-muotoisella VideoFrame-oliolla on yksinkertaisesti kaksi asettelua layout-taulukossaan.
Haasteet ja parhaat käytännöt
Tällä matalalla tasolla työskentely on tehokasta, mutta siihen liittyy vastuita.
Muistinhallinta on ensisijaisen tärkeää
VideoFrame pitää hallussaan merkittävän määrän muistia, jota hallitaan usein JavaScriptin roskienkerääjän keon (heap) ulkopuolella. Jos et vapauta tätä muistia nimenomaisesti, aiheutat muistivuodon, joka voi kaataa selainvälilehden.
Kutsu aina, aina videoFrame.close(), kun olet valmis kehyksen kanssa.
Asynkroninen luonne
Kaikki datan käyttö on asynkronista. Sovelluksesi arkkitehtuurin on käsiteltävä Promises-lupausten ja async/await-rakenteen virtaa oikein kilpailutilanteiden välttämiseksi ja sujuvan käsittelyputken varmistamiseksi.
Selainyhteensopivuus
WebCodecs on moderni API. Vaikka se on tuettu kaikissa suurimmissa selaimissa, tarkista aina sen saatavuus ja ole tietoinen mahdollisista toimittajakohtaisista toteutuksen yksityiskohdista tai rajoituksista. Käytä ominaisuuksien tunnistusta (feature detection) ennen kuin yrität käyttää API:a.
Johtopäätös: Uusi aikakausi web-videolle
Kyky suoraan käyttää ja manipuloida VideoFrame-olion raakaa tasodataa WebCodecs API:n kautta on paradigman muutos selainpohjaisille mediasovelluksille. Se poistaa <video>-elementin mustan laatikon ja antaa kehittäjille sen hienojakoisen hallinnan, joka oli aiemmin varattu vain natiivisovelluksille.
Ymmärtämällä videon muistiasettelun perusteet – tasot, riviaskeleen ja värimuodot – ja hyödyntämällä WebAssemblyn tehoa suorituskykykriittisissä operaatioissa, voit nyt rakentaa uskomattoman kehittyneitä videonkäsittelytyökaluja suoraan selaimessa. Mahdollisuudet ovat valtavat, reaaliaikaisesta värimäärittelystä ja mukautetuista visuaalisista tehosteista asiakaspuolen koneoppimiseen ja videoanalyysiin. Suorituskykyisen, matalan tason videonkäsittelyn aikakausi webissä on todella alkanut.