Atklājiet augstas kvalitātes video straumēšanu pārlūkprogrammā. Uzziniet, kā ieviest modernu temporālo filtrēšanu trokšņu samazināšanai, izmantojot WebCodecs API un VideoFrame manipulācijas.
WebCodecs apgūšana: video kvalitātes uzlabošana ar temporālo trokšņu samazināšanu
Tīmekļa video komunikācijas, straumēšanas un reāllaika lietojumprogrammu pasaulē kvalitāte ir vissvarīgākā. Lietotāji visā pasaulē sagaida skaidru un asu video, neatkarīgi no tā, vai viņi piedalās biznesa sapulcē, skatās tiešraides pasākumu vai mijiedarbojas ar attālinātu pakalpojumu. Tomēr video straumes bieži vien nomoka pastāvīgs un traucējošs artefakts: troksnis. Šis digitālais troksnis, kas bieži redzams kā graudaina vai statiska tekstūra, var pasliktināt skatīšanās pieredzi un, pārsteidzošā kārtā, palielināt joslas platuma patēriņu. Par laimi, jaudīgs pārlūka API, WebCodecs, sniedz izstrādātājiem bezprecedenta zema līmeņa kontroli, lai risinātu šo problēmu tieši.
Šis visaptverošais ceļvedis jūs aizvedīs dziļā izpētē par WebCodecs izmantošanu konkrētai, augstas ietekmes video apstrādes tehnikai: temporālajai trokšņu samazināšanai. Mēs izpētīsim, kas ir video troksnis, kāpēc tas ir kaitīgs un kā jūs varat izmantot VideoFrame
objektu, lai izveidotu filtrēšanas konveijeru tieši pārlūkprogrammā. Mēs apskatīsim visu, sākot no pamata teorijas līdz praktiskai JavaScript implementācijai, veiktspējas apsvērumiem ar WebAssembly un progresīviem konceptiem profesionāla līmeņa rezultātu sasniegšanai.
Kas ir video troksnis un kāpēc tas ir svarīgs?
Pirms mēs varam atrisināt problēmu, mums tā vispirms ir jāsaprot. Digitālajā video troksnis attiecas uz nejaušām spilgtuma vai krāsu informācijas variācijām video signālā. Tas ir nevēlams attēlu uzņemšanas un pārraides procesa blakusprodukts.
Trokšņu avoti un veidi
- Sensora troksnis: Galvenais vaininieks. Vāja apgaismojuma apstākļos kameru sensori pastiprina ienākošo signālu, lai radītu pietiekami spilgtu attēlu. Šis pastiprināšanas process arī pastiprina nejaušas elektroniskās svārstības, radot redzamu graudainību.
- Termiskais troksnis: Kameras elektronikas radītais siltums var izraisīt nejaušu elektronu kustību, radot troksni, kas nav atkarīgs no gaismas līmeņa.
- Kvantēšanas troksnis: Tiek ieviests analogciparu pārveidošanas un kompresijas procesos, kur nepārtrauktas vērtības tiek kartētas uz ierobežotu diskrētu līmeņu kopu.
Šis troksnis parasti izpaužas kā Gausa troksnis, kur katra pikseļa intensitāte nejauši mainās ap tā patieso vērtību, radot smalku, mirgojošu graudainību visā kadrā.
Trokšņa divkāršā ietekme
Video troksnis ir vairāk nekā tikai kosmētisks jautājums; tam ir būtiskas tehniskas un uztveres sekas:
- Pasliktināta lietotāja pieredze: Visredzamākā ietekme ir uz vizuālo kvalitāti. Trokšņains video izskatās neprofesionāli, ir traucējošs un var apgrūtināt svarīgu detaļu saskatīšanu. Lietojumprogrammās, piemēram, telekonferencēs, tas var likt dalībniekiem izskatīties graudainiem un neskaidriem, mazinot klātbūtnes sajūtu.
- Samazināta kompresijas efektivitāte: Šī ir mazāk intuitīva, bet tikpat kritiska problēma. Mūsdienu video kodeki (piemēram, H.264, VP9, AV1) sasniedz augstus kompresijas rādītājus, izmantojot redundanci. Tie meklē līdzības starp kadriem (temporālā redundance) un viena kadra ietvaros (telpiskā redundance). Troksnis pēc savas būtības ir nejaušs un neparedzams. Tas lauž šos redundances modeļus. Kodētājs uztver nejaušo troksni kā augstfrekvences detaļu, kas jāsaglabā, liekot tam piešķirt vairāk bitu trokšņa kodēšanai, nevis faktiskajam saturam. Tas noved pie lielāka faila izmēra ar tādu pašu uztverto kvalitāti vai zemākas kvalitātes ar tādu pašu bitu pārraides ātrumu.
Noņemot troksni pirms kodēšanas, mēs varam padarīt video signālu paredzamāku, ļaujot kodētājam strādāt efektīvāk. Tas nodrošina labāku vizuālo kvalitāti, mazāku joslas platuma patēriņu un vienmērīgāku straumēšanas pieredzi lietotājiem visur.
Iepazīstinām ar WebCodecs: zemā līmeņa video kontroles spēks
Gadiem ilgi tieša video manipulācija pārlūkprogrammā bija ierobežota. Izstrādātāji lielā mērā bija spiesti izmantot <video>
elementa un Canvas API iespējas, kas bieži vien ietvēra veiktspēju nogalinošu datu nolasīšanu no GPU. WebCodecs pilnībā maina spēles noteikumus.
WebCodecs ir zema līmeņa API, kas nodrošina tiešu piekļuvi pārlūkprogrammas iebūvētajiem multivides kodētājiem un dekoderiem. Tas ir paredzēts lietojumprogrammām, kurām nepieciešama precīza kontrole pār multivides apstrādi, piemēram, video redaktoriem, mākoņspēļu platformām un progresīviem reāllaika komunikācijas klientiem.
Galvenā komponente, uz kuru mēs koncentrēsimies, ir VideoFrame
objekts. VideoFrame
attēlo vienu video kadru kā attēlu, bet tas ir daudz vairāk nekā vienkārša bitkarte. Tas ir ļoti efektīvs, pārnesams objekts, kas var saturēt video datus dažādos pikseļu formātos (piemēram, RGBA, I420, NV12) un ietver svarīgus metadatus, piemēram:
timestamp
: Kadra prezentācijas laiks mikrosekundēs.duration
: Kadra ilgums mikrosekundēs.codedWidth
uncodedHeight
: Kadra izmēri pikseļos.format
: Datu pikseļu formāts (piemēram, 'I420', 'RGBA').
Būtiski, ka VideoFrame
nodrošina metodi ar nosaukumu copyTo()
, kas ļauj mums kopēt neapstrādātus, nesaspiestus pikseļu datus ArrayBuffer
. Šis ir mūsu sākumpunkts analīzei un manipulācijai. Kad mums ir neapstrādāti baiti, mēs varam pielietot savu trokšņu samazināšanas algoritmu un pēc tam izveidot jaunu VideoFrame
no modificētajiem datiem, lai to nodotu tālāk pa apstrādes konveijeru (piemēram, video kodētājam vai uz canvas).
Temporālās filtrēšanas izpratne
Trokšņu samazināšanas tehnikas var plaši iedalīt divos veidos: telpiskajā un temporālajā.
- Telpiskā filtrēšana: Šī tehnika darbojas ar vienu kadru atsevišķi. Tā analizē attiecības starp blakus esošajiem pikseļiem, lai identificētu un izlīdzinātu troksni. Vienkāršs piemērs ir izplūdināšanas (blur) filtrs. Lai gan efektīvi samazina troksni, telpiskie filtri var arī mīkstināt svarīgas detaļas un malas, radot mazāk asu attēlu.
- Temporālā filtrēšana: Šī ir sarežģītākā pieeja, uz kuru mēs koncentrējamies. Tā darbojas ar vairākiem kadriem laika gaitā. Pamatprincips ir tāds, ka faktiskais ainas saturs, visticamāk, būs korelēts no viena kadra uz nākamo, kamēr troksnis ir nejaušs un nekorelēts. Salīdzinot pikseļa vērtību noteiktā vietā vairākos kadros, mēs varam atšķirt konsekvento signālu (reālo attēlu) no nejaušajām svārstībām (trokšņa).
Vienkāršākā temporālās filtrēšanas forma ir temporālā vidējošana. Iedomājieties, ka jums ir pašreizējais kadrs un iepriekšējais kadrs. Jebkuram dotajam pikselim tā 'patiesā' vērtība, visticamāk, ir kaut kur starp tā vērtību pašreizējā kadrā un tā vērtību iepriekšējā kadrā. Sajaucot tos, mēs varam izlīdzināt nejaušo troksni. Jauno pikseļa vērtību var aprēķināt ar vienkāršu svērto vidējo:
new_pixel = (alpha * current_pixel) + ((1 - alpha) * previous_pixel)
Šeit alpha
ir sajaukšanas faktors no 0 līdz 1. Augstāks alpha
nozīmē, ka mēs vairāk uzticamies pašreizējam kadram, kas rezultējas ar mazāku trokšņu samazināšanu, bet mazāk kustības artefaktiem. Zemāks alpha
nodrošina spēcīgāku trokšņu samazināšanu, bet var izraisīt 'spokainu' attēlu (ghosting) vai sliedes vietās ar kustību. Pareizā līdzsvara atrašana ir galvenais.
Vienkārša temporālās vidējošanas filtra ieviešana
Izveidosim praktisku šī koncepta implementāciju, izmantojot WebCodecs. Mūsu konveijers sastāvēs no trīs galvenajiem soļiem:
- Iegūt
VideoFrame
objektu straumi (piemēram, no tīmekļa kameras). - Katram kadram piemērot mūsu temporālo filtru, izmantojot iepriekšējā kadra datus.
- Izveidot jaunu, tīrāku
VideoFrame
.
1. solis: Kadru straumes iestatīšana
Vieglākais veids, kā iegūt tiešraides VideoFrame
objektu straumi, ir izmantot MediaStreamTrackProcessor
, kas patērē MediaStreamTrack
(piemēram, no getUserMedia
) un atklāj tā kadrus kā lasāmu straumi.
Konceptuāls JavaScript iestatījums:
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;
// Šeit mēs apstrādāsim katru 'frame'
const processedFrame = await applyTemporalFilter(frame, previousFrameBuffer);
// Nākamajai iterācijai mums ir jāsaglabā *oriģinālā* pašreizējā kadra dati
// Šeit jūs nokopētu oriģinālā kadra datus uz 'previousFrameBuffer', pirms to aizvērt.
// Neaizmirstiet aizvērt kadrus, lai atbrīvotu atmiņu!
frame.close();
// Dariet kaut ko ar processedFrame (piem., renderējiet uz canvas, kodējiet)
// ... un pēc tam aizveriet arī to!
processedFrame.close();
}
}
2. solis: Filtrēšanas algoritms - darbs ar pikseļu datiem
Šī ir mūsu darba kodols. Mūsu applyTemporalFilter
funkcijas iekšienē mums ir jāpiekļūst ienākošā kadra pikseļu datiem. Vienkāršības labad pieņemsim, ka mūsu kadri ir 'RGBA' formātā. Katrs pikselis tiek attēlots ar 4 baitiem: sarkans, zaļš, zils un alfa (caurspīdīgums).
async function applyTemporalFilter(currentFrame, previousFrameBuffer) {
// Definējam mūsu sajaukšanas faktoru. 0.8 nozīmē 80% no jaunā kadra un 20% no vecā.
const alpha = 0.8;
// Iegūstam izmērus
const width = currentFrame.codedWidth;
const height = currentFrame.codedHeight;
// Alocējam ArrayBuffer, lai turētu pašreizējā kadra pikseļu datus.
const currentFrameSize = width * height * 4; // 4 baiti uz pikseli RGBA
const currentFrameBuffer = new Uint8Array(currentFrameSize);
await currentFrame.copyTo(currentFrameBuffer);
// Ja šis ir pirmais kadrs, nav iepriekšējā kadra, ar ko sajaukt.
// Vienkārši atgrieziet to tādu, kāds tas ir, bet saglabājiet tā buferi nākamajai iterācijai.
if (!previousFrameBuffer) {
const newFrameBuffer = new Uint8Array(currentFrameBuffer);
// Mēs atjaunināsim mūsu globālo 'previousFrameBuffer' ar šo ārpus šīs funkcijas.
return { buffer: newFrameBuffer, frame: currentFrame };
}
// Izveidojam jaunu buferi mūsu izvades kadram.
const outputFrameBuffer = new Uint8Array(currentFrameSize);
// Galvenais apstrādes cikls.
for (let i = 0; i < currentFrameSize; i++) {
const currentPixelValue = currentFrameBuffer[i];
const previousPixelValue = previousFrameBuffer[i];
// Piemērojam temporālās vidējošanas formulu katram krāsu kanālam.
// Mēs izlaižam alfa kanālu (katru 4. baitu).
if ((i + 1) % 4 !== 0) {
outputFrameBuffer[i] = Math.round(alpha * currentPixelValue + (1 - alpha) * previousPixelValue);
} else {
// Saglabājam alfa kanālu nemainīgu.
outputFrameBuffer[i] = currentPixelValue;
}
}
return { buffer: outputFrameBuffer, frame: currentFrame };
}
Piezīme par YUV formātiem (I420, NV12): Lai gan RGBA ir viegli saprotams, lielākā daļa video dabiski tiek apstrādāti YUV krāsu telpās efektivitātes dēļ. Darbs ar YUV ir sarežģītāks, jo krāsu (U, V) un spilgtuma (Y) informācija tiek glabāta atsevišķi ('plaknēs'). Filtrēšanas loģika paliek tāda pati, bet jums būtu jāiterē cauri katrai plaknei (Y, U un V) atsevišķi, ņemot vērā to attiecīgos izmērus (krāsu plaknes bieži ir ar zemāku izšķirtspēju, tehniku sauc par hromas apakšdiskretizāciju).
3. solis: Jaunā filtrētā VideoFrame
izveide
Pēc mūsu cikla pabeigšanas outputFrameBuffer
satur pikseļu datus mūsu jaunajam, tīrākajam kadram. Tagad mums tas ir jāiepako jaunā VideoFrame
objektā, pārliecinoties, ka nokopējam metadatus no oriģinālā kadra.
// Jūsu galvenajā ciklā pēc applyTemporalFilter izsaukšanas...
const { buffer: processedBuffer, frame: originalFrame } = await applyTemporalFilter(frame, previousFrameBuffer);
// Izveidojam jaunu VideoFrame no mūsu apstrādātā bufera.
const newFrame = new VideoFrame(processedBuffer, {
format: 'RGBA',
codedWidth: originalFrame.codedWidth,
codedHeight: originalFrame.codedHeight,
timestamp: originalFrame.timestamp,
duration: originalFrame.duration
});
// SVARĪGI: Atjauniniet iepriekšējā kadra buferi nākamajai iterācijai.
// Mums ir jākopē *oriģinālā* kadra dati, nevis filtrētie dati.
// Atsevišķa kopija jāizveido pirms filtrēšanas.
previousFrameBuffer = new Uint8Array(originalFrameData);
// Tagad jūs varat izmantot 'newFrame'. Renderējiet to, kodējiet to utt.
// renderer.draw(newFrame);
// Un kritiski svarīgi, aizveriet to, kad esat pabeidzis, lai novērstu atmiņas noplūdes.
newFrame.close();
Atmiņas pārvaldība ir kritiski svarīga: VideoFrame
objekti var saturēt lielu daudzumu nesaspiestu video datu un var būt nodrošināti ar atmiņu ārpus JavaScript kaudzes. Jums obligāti jāizsauc frame.close()
katram kadram, ar kuru esat pabeidzis darbu. Ja to nedarīsiet, tas ātri novedīs pie atmiņas izsmelšanas un cilnes avārijas.
Veiktspējas apsvērumi: JavaScript pret WebAssembly
Iepriekš minētā tīrā JavaScript implementācija ir lieliska mācībām un demonstrācijai. Tomēr 30 FPS, 1080p (1920x1080) video gadījumā mūsu ciklam ir jāveic vairāk nekā 248 miljoni aprēķinu sekundē! (1920 * 1080 * 4 baiti * 30 fps). Lai gan mūsdienu JavaScript dzinēji ir neticami ātri, šī pikseļu līmeņa apstrāde ir ideāls pielietojuma gadījums veiktspējīgākai tehnoloģijai: WebAssembly (Wasm).
WebAssembly pieeja
WebAssembly ļauj jums palaist kodu, kas rakstīts tādās valodās kā C++, Rust vai Go, pārlūkprogrammā gandrīz ar natīvo ātrumu. Mūsu temporālā filtra loģika ir vienkārši implementējama šajās valodās. Jūs uzrakstītu funkciju, kas pieņem rādītājus uz ievades un izvades buferiem un veic to pašu iteratīvo sajaukšanas operāciju.
Konceptuāla C++ funkcija priekš Wasm:
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) { // Izlaist alfa kanālu
output_frame[i] = (unsigned char)(alpha * current_frame[i] + (1.0 - alpha) * previous_frame[i]);
} else {
output_frame[i] = current_frame[i];
}
}
}
}
No JavaScript puses jūs ielādētu šo kompilēto Wasm moduli. Galvenā veiktspējas priekšrocība nāk no atmiņas koplietošanas. Jūs varat izveidot ArrayBuffer
s JavaScript, kas ir nodrošināti ar Wasm moduļa lineāro atmiņu. Tas ļauj nodot kadru datus uz Wasm bez dārgas kopēšanas. Viss pikseļu apstrādes cikls tad darbojas kā viens, augsti optimizēts Wasm funkcijas izsaukums, kas ir ievērojami ātrāks nekā JavaScript `for` cikls.
Progresīvas temporālās filtrēšanas tehnikas
Vienkārša temporālā vidējošana ir lielisks sākumpunkts, bet tai ir būtisks trūkums: tā rada kustības izplūšanu vai 'spokainu' attēlu (ghosting). Kad objekts kustas, tā pikseļi pašreizējā kadrā tiek sajaukti ar fona pikseļiem no iepriekšējā kadra, radot sliedi. Lai izveidotu patiesi profesionāla līmeņa filtru, mums ir jāņem vērā kustība.
Kustību kompensējošā temporālā filtrēšana (MCTF)
Temporālās trokšņu samazināšanas zelta standarts ir Kustību kompensējošā temporālā filtrēšana. Tā vietā, lai akli sajauktu pikseli ar to, kas atrodas tajās pašās (x, y) koordinātās iepriekšējā kadrā, MCTF vispirms mēģina noskaidrot, no kurienes šis pikselis ir nācis.
Process ietver:
- Kustības novērtēšana: Algoritms sadala pašreizējo kadru blokos (piemēram, 16x16 pikseļi). Katram blokam tas meklē iepriekšējā kadrā vislīdzīgāko bloku (piemēram, kam ir zemākā Absolūto atšķirību summa). Pārvietojums starp šiem diviem blokiem tiek saukts par 'kustības vektoru'.
- Kustības kompensācija: Pēc tam tas izveido 'kustību kompensētu' iepriekšējā kadra versiju, pārvietojot blokus atbilstoši to kustības vektoriem.
- Filtrēšana: Visbeidzot, tas veic temporālo vidējošanu starp pašreizējo kadru un šo jauno, kustību kompensēto iepriekšējo kadru.
Šādā veidā kustīgs objekts tiek sajaukts ar sevi pašu no iepriekšējā kadra, nevis ar fonu, ko tas tikko atsedzis. Tas krasi samazina 'spokainā' attēla artefaktus. Kustības novērtēšanas ieviešana ir skaitļošanas ziņā intensīva un sarežģīta, bieži vien prasa progresīvus algoritmus un gandrīz vienmēr ir uzdevums WebAssembly vai pat WebGPU skaitļošanas ēnotājiem.
Adaptīvā filtrēšana
Vēl viens uzlabojums ir padarīt filtru adaptīvu. Tā vietā, lai izmantotu fiksētu alpha
vērtību visam kadram, jūs to varat mainīt atkarībā no vietējiem apstākļiem.
- Kustības adaptācija: Vietās ar augstu konstatētu kustību jūs varat palielināt
alpha
(piemēram, līdz 0.95 vai 1.0), lai gandrīz pilnībā paļautos uz pašreizējo kadru, novēršot jebkādu kustības izplūšanu. Statiskās vietās (piemēram, siena fonā) jūs varat samazinātalpha
(piemēram, līdz 0.5), lai panāktu daudz spēcīgāku trokšņu samazināšanu. - Luminances adaptācija: Troksnis bieži vien ir redzamāks tumšākās attēla daļās. Filtru varētu padarīt agresīvāku ēnās un mazāk agresīvu spilgtās vietās, lai saglabātu detaļas.
Praktiski pielietojuma gadījumi un lietojumprogrammas
Spēja veikt augstas kvalitātes trokšņu samazināšanu pārlūkprogrammā atver daudzas iespējas:
- Reāllaika komunikācija (WebRTC): Iepriekš apstrādāt lietotāja tīmekļa kameras plūsmu, pirms tā tiek nosūtīta video kodētājam. Tas ir milzīgs ieguvums video zvaniem vāja apgaismojuma apstākļos, uzlabojot vizuālo kvalitāti un samazinot nepieciešamo joslas platumu.
- Tīmekļa video rediģēšana: Piedāvāt 'Denoise' filtru kā funkciju pārlūkprogrammas video redaktorā, ļaujot lietotājiem attīrīt savus augšupielādētos materiālus bez servera puses apstrādes.
- Mākoņspēles un attālinātā darbvirsma: Attīrīt ienākošās video straumes, lai samazinātu kompresijas artefaktus un nodrošinātu skaidrāku, stabilāku attēlu.
- Datorredzes priekšapstrāde: Tīmekļa AI/ML lietojumprogrammām (piemēram, objektu izsekošanai vai sejas atpazīšanai) ievades video attīrīšana no trokšņa var stabilizēt datus un novest pie precīzākiem un uzticamākiem rezultātiem.
Izaicinājumi un nākotnes virzieni
Lai gan šī pieeja ir jaudīga, tā nav bez izaicinājumiem. Izstrādātājiem jābūt uzmanīgiem attiecībā uz:
- Veiktspēja: Reāllaika apstrāde HD vai 4K video ir prasīga. Efektīva implementācija, parasti ar WebAssembly, ir obligāta.
- Atmiņa: Viena vai vairāku iepriekšējo kadru glabāšana nesaspiestos buferos patērē ievērojamu daudzumu RAM. Rūpīga pārvaldība ir būtiska.
- Latentums: Katrs apstrādes solis pievieno latentumu. Reāllaika komunikācijai šim konveijeram jābūt augsti optimizētam, lai izvairītos no pamanāmas kavēšanās.
- Nākotne ar WebGPU: Jaunais WebGPU API nodrošinās jaunu robežu šāda veida darbam. Tas ļaus šos pikseļu līmeņa algoritmus palaist kā augsti paralēlus skaitļošanas ēnotājus sistēmas GPU, piedāvājot vēl vienu milzīgu veiktspējas lēcienu pat salīdzinājumā ar WebAssembly uz CPU.
Noslēgums
WebCodecs API iezīmē jaunu ēru progresīvai multivides apstrādei tīmeklī. Tas nojauc tradicionālā melnās kastes <video>
elementa barjeras un dod izstrādātājiem smalku kontroli, kas nepieciešama, lai veidotu patiesi profesionālas video lietojumprogrammas. Temporālā trokšņu samazināšana ir ideāls piemērs tās spēkam: sarežģīta tehnika, kas tieši risina gan lietotāja uztverto kvalitāti, gan pamatā esošo tehnisko efektivitāti.
Mēs esam redzējuši, ka, pārtverot atsevišķus VideoFrame
objektus, mēs varam ieviest spēcīgu filtrēšanas loģiku, lai samazinātu troksni, uzlabotu saspiežamību un nodrošinātu izcilu video pieredzi. Lai gan vienkārša JavaScript implementācija ir lielisks sākumpunkts, ceļš uz ražošanai gatavu, reāllaika risinājumu ved caur WebAssembly veiktspēju un nākotnē - WebGPU paralēlās apstrādes jaudu.
Nākamreiz, kad redzēsiet graudainu video tīmekļa lietotnē, atcerieties, ka rīki tā labošanai tagad, pirmo reizi, ir tieši tīmekļa izstrādātāju rokās. Šis ir aizraujošs laiks, lai veidotu ar video tīmeklī.