Kako radi video kodek? Dio 2. Što, zašto, kako

Prvi dio: Osnove rada s videom i slikom

Kako radi video kodek? Dio 2. Što, zašto, kako

Što je? Video kodek je dio softvera/hardvera koji sažima i/ili dekomprimira digitalni video.

Za što? Unatoč određenim ograničenjima i u pogledu propusnosti i
a što se tiče prostora za pohranu podataka, tržište zahtijeva sve veću kvalitetu videa. Sjećate li se kako smo u prošlom postu izračunali potreban minimum za 30 sličica u sekundi, 24 bita po pikselu, uz rezoluciju 480x240? Dobili smo 82,944 Mbit/s bez kompresije. Kompresija je trenutno jedini način općenitog prijenosa HD/FullHD/4K na televizijske zaslone i internet. Kako se to postiže? Pogledajmo sada ukratko glavne metode.

Kako radi video kodek? Dio 2. Što, zašto, kako

Prijevod je napravljen uz podršku EDISON Software.

Mi smo angažirani u integracija sustava video nadzoraI razvijamo mikrotomograf.

Kodek protiv spremnika

Česta pogreška početnika je brkanje digitalnog video kodeka i digitalnog video spremnika. Spremnik je određeni format. Omotač koji sadrži video (i moguće audio) metapodatke. Komprimirani video može se smatrati korisnim teretom spremnika.

Ekstenzija videodatoteke obično označava njenu vrstu spremnika. Na primjer, datoteka video.mp4 vjerojatno je spremnik MPEG-4, dio 14, a najvjerojatnije je datoteka pod nazivom video.mkv matrëška. Kako biste bili potpuno sigurni u format kodeka i spremnika, možete koristiti FFmpeg ili MediaInfo.

Malo povijesti

Prije nego što dođemo do Kako?, zaronimo malo u povijest kako bismo malo bolje razumjeli neke starije kodeke.

Video kodek H.261 pojavio se 1990. (tehnički - 1988.) i stvoren je za rad pri brzini prijenosa podataka od 64 Kbps. Već je koristio ideje kao što su poduzorkovanje boja, makroblokovi itd. Standard video kodeka objavljen je 1995 H.263, koji se razvijao do 2001. godine.

Prva verzija dovršena je 2003 H.264 / AVC. Iste godine TrueMotion je izdao svoj besplatni video kodek s gubicima tzv VP3. Google je tvrtku kupio 2008., puštajući u promet VP8 iste godine. U prosincu 2012. Google je objavio VP9, a podržan je na oko ¾ tržišta preglednika (uključujući mobilne uređaje).

AV1 je novi besplatni video kodek otvorenog koda koji je razvio Savez za otvorene medije (AOMedia), koja uključuje najpoznatije tvrtke, kao što su: Google, Mozilla, Microsoft, Amazon, Netflix, AMD, ARM, NVidia, Intel i Cisco. Prva verzija kodeka, 0.1.0, objavljena je 7. travnja 2016.

Rođenje AV1

Početkom 2015. Google je radio na VP10Xiph (koji je u vlasništvu Mozille) je radio na daala, a Cisco je napravio vlastiti besplatni video kodek tzv Thor.

tada MPEG LA prvi put objavljeni godišnji limiti za HEVC (H.265) i 8 puta veću naknadu nego za H.264, no ubrzo su opet promijenili pravila:

nema godišnjeg ograničenja,
naknada za sadržaj (0,5% prihoda) i
jedinična naknada je oko 10 puta veća od H.264.

Savez za otvorene medije kreirale su tvrtke iz različitih područja: proizvođači opreme (Intel, AMD, ARM, Nvidia, Cisco), pružatelji sadržaja (Google, Netflix, Amazon), kreatori preglednika (Google, Mozilla) i drugi.

Tvrtke su imale zajednički cilj - video kodek bez naknade. Zatim se pojavljuje AV1 s puno jednostavnijom patentnom licencom. Timothy B. Terryberry održao je zapanjujuću prezentaciju koja je postala ishodište trenutnog koncepta AV1 i njegovog modela licenciranja.

Iznenadit ćete se kada znate da možete analizirati AV1 kodek putem preglednika (zainteresirani mogu otići na aomanalyzer.org).

Kako radi video kodek? Dio 2. Što, zašto, kako

Univerzalni kodek

Pogledajmo glavne mehanizme na kojima se temelji univerzalni video kodek. Većina ovih koncepata je korisna i koristi se u modernim kodecima kao što su VP9, AV1 и HEVC. Upozoravam vas da će mnoge od objašnjenih stvari biti pojednostavljene. Ponekad će se primjeri iz stvarnog svijeta (kao kod H.264) koristiti za demonstraciju tehnologija.

1. korak - cijepanje slike

Prvi korak je podijeliti okvir u nekoliko odjeljaka, pododjeljaka i dalje.

Kako radi video kodek? Dio 2. Što, zašto, kako

Za što? Mnogo je razloga. Kada podijelimo sliku, možemo točnije predvidjeti vektor gibanja korištenjem malih dijelova za male pokretne dijelove. Dok se za statičnu pozadinu možete ograničiti na veće dijelove.

Kodeci obično organiziraju te odjeljke u odjeljke (ili dijelove), makroblokove (ili blokove stabla kodiranja) i više pododjeljaka. Maksimalna veličina ovih particija varira, HEVC je postavlja na 64x64 dok AVC koristi 16x16, a podparticije se mogu podijeliti do 4x4 veličine.

Sjećate li se tipova okvira iz prošlog članka?! Isto se može primijeniti i na blokove, tako da možemo imati I-fragment, B-blok, P-makroblok itd.

Za one koji žele vježbati, pogledajte kako je slika podijeljena na dijelove i pododjeljke. Da biste to učinili, možete koristiti onaj koji je već spomenut u prethodnom članku. Intel Video Pro Analyzer (ona koja se plaća, ali s besplatnom probnom verzijom koja je ograničena na prvih 10 sličica). Odjeljci analizirani ovdje VP9:

Kako radi video kodek? Dio 2. Što, zašto, kako

2. korak - predviđanje

Kad imamo odjeljke, za njih možemo napraviti astrološke prognoze. Za INTER prognoze mora se prenijeti vektori kretanja i ostatak, a za INTRA prognozu se prenosi prognozni smjer i ostatak.

3. korak - transformacija

Jednom kada imamo rezidualni blok (predviđeni presjek → pravi presjek), moguće ga je transformirati na takav način da znamo koji se pikseli mogu odbaciti uz zadržavanje ukupne kvalitete. Postoje neke transformacije koje osiguravaju točno ponašanje.

Iako postoje i druge metode, pogledajmo ih detaljnije. diskretna kosinusna transformacija (DCT - od diskretna kosinusna transformacija). Glavne funkcije DCT-a:

  • Pretvara blokove piksela u blokove frekvencijskih koeficijenata jednake veličine.
  • Kondenzira snagu kako bi se uklonila prostorna redundantnost.
  • Omogućuje reverzibilnost.

2. veljače 2017. Sintra R.J. (Cintra, RJ) i Bayer F.M. (Bayer FM) objavio je članak o DCT transformaciji za kompresiju slike koja zahtijeva samo 14 dodavanja.

Ne brinite ako ne razumijete prednosti svake točke. Upotrijebimo sada konkretne primjere da vidimo njihovu stvarnu vrijednost.

Uzmimo ovaj 8x8 blok piksela:

Kako radi video kodek? Dio 2. Što, zašto, kako

Ovaj blok se renderira u sljedeću sliku veličine 8 x 8 piksela:

Kako radi video kodek? Dio 2. Što, zašto, kako

Primijenite DCT na ovaj blok piksela i dobit ćete 8x8 blok koeficijenata:

Kako radi video kodek? Dio 2. Što, zašto, kako

I ako renderiramo ovaj blok koeficijenata, dobit ćemo sljedeću sliku:

Kako radi video kodek? Dio 2. Što, zašto, kako

Kao što vidite, ne izgleda kao izvorna slika. Možete vidjeti da se prvi koeficijent jako razlikuje od svih ostalih. Ovaj prvi koeficijent poznat je kao DC koeficijent, koji predstavlja sve uzorke u ulaznom nizu, nešto poput prosjeka.

Ovaj blok koeficijenata ima zanimljivo svojstvo: odvaja visokofrekventne komponente od niskofrekventnih.

Kako radi video kodek? Dio 2. Što, zašto, kako

Na slici je većina snage koncentrirana na nižim frekvencijama, pa ako pretvorite sliku u njezine frekvencijske komponente i odbacite koeficijente više frekvencije, možete smanjiti količinu podataka potrebnih za opisivanje slike bez žrtvovanja previše kvalitete slike.

Frekvencija se odnosi na to koliko se brzo signal mijenja.

Pokušajmo primijeniti znanje stečeno u testnom slučaju pretvaranjem izvorne slike u njezinu frekvenciju (blok koeficijenata) pomoću DCT-a i zatim odbacivanjem dijela najmanje važnih koeficijenata.

Prvo ga pretvaramo u frekvencijsku domenu.

Kako radi video kodek? Dio 2. Što, zašto, kako

Zatim odbacujemo dio (67%) koeficijenata, uglavnom donji desni dio.

Kako radi video kodek? Dio 2. Što, zašto, kako

Na kraju, rekonstruiramo sliku iz ovog odbačenog bloka koeficijenata (zapamtite, mora biti invertibilan) i uspoređujemo je s originalom.

Kako radi video kodek? Dio 2. Što, zašto, kako

Vidimo da podsjeća na originalnu sliku, ali postoje mnoge razlike od originala. Izbacili smo 67,1875% i još uvijek dobili nešto što podsjeća na original. Moglo se promišljenije odbaciti koeficijente kako bi se dobila slika još bolje kvalitete, ali to je već sljedeća tema.

Svaki koeficijent se generira korištenjem svih piksela

Važno: svaki koeficijent nije izravno mapiran na jedan piksel, već je ponderirani zbroj svih piksela. Ovaj nevjerojatni grafikon pokazuje kako se prvi i drugi koeficijent izračunavaju koristeći težine jedinstvene za svaki indeks.

Kako radi video kodek? Dio 2. Što, zašto, kako

Također možete pokušati vizualizirati DCT gledajući jednostavnu formaciju slike koja se temelji na njemu. Na primjer, ovdje je simbol A generiran korištenjem svake težine koeficijenta:

Kako radi video kodek? Dio 2. Što, zašto, kako

4. korak - kvantizacija

Nakon što smo u prethodnom koraku izbacili neke koeficijente, u zadnjem koraku (transformacija) izvodimo poseban oblik kvantizacije. U ovoj fazi prihvatljivo je izgubiti informacije. Ili, jednostavnije, kvantizirat ćemo koeficijente da postignemo kompresiju.

Kako možete kvantizirati blok koeficijenata? Jedna od najjednostavnijih metoda je uniformna kvantizacija, kada uzmemo blok, podijelimo ga s jednom vrijednošću (s 10) i zaokružimo rezultat.

Kako radi video kodek? Dio 2. Što, zašto, kako

Možemo li obrnuti ovaj blok koeficijenata? Da, možemo, množenje istom vrijednošću kojom smo podijelili.

Kako radi video kodek? Dio 2. Što, zašto, kako

Ovaj pristup nije najbolji jer ne uzima u obzir važnost svakog koeficijenta. Mogla bi se koristiti matrica kvantizatora umjesto jedne vrijednosti, a ova bi matrica mogla iskoristiti DCT svojstvo kvantiziranjem većine donjeg desnog i manjine gornjeg lijevog.

Korak 5 - entropijsko kodiranje

Nakon što smo kvantizirali podatke (blokove slika, fragmente, okvire), još uvijek ih možemo komprimirati bez gubitaka. Postoji mnogo algoritamskih načina komprimiranja podataka. Kratko ćemo pogledati neke od njih, a za dublje razumijevanje možete pročitati knjigu Understanding Compression: Data Compression for Modern Developers ("Razumijevanje kompresije: kompresija podataka za moderne programere").

Video kodiranje pomoću VLC-a

Recimo da imamo tok znakova: a, e, r и t. Vjerojatnost (u rasponu od 0 do 1) koliko se često svaki znak pojavljuje u toku prikazana je u ovoj tablici.

a e r t
Vjerojatnost 0,3 0,3 0,2 0,2

Najvjerojatnijima možemo dodijeliti jedinstvene binarne kodove (po mogućnosti male), a manje vjerojatnim veće kodove.

a e r t
Vjerojatnost 0,3 0,3 0,2 0,2
Binarni kod 0 10 110 1110

Komprimiramo tok, pretpostavljajući da ćemo na kraju potrošiti 8 bita za svaki znak. Bez kompresije, bilo bi potrebno 24 bita po znaku. Ako svaki znak zamijenite njegovim kodom, ostvarujete uštedu!

Prvi korak je kodiranje znaka e, što je jednako 10, a drugi znak je a, koji se dodaje (ne na matematički način): [10][0], i na kraju treći znak t, što naš konačni komprimirani bitstream čini jednakim [10][0][1110] ili 1001110, koji zahtijeva samo 7 bita (3,4 puta manje prostora od originala).

Imajte na umu da svaki kod mora biti jedinstveni kod s prefiksom. Huffmanov algoritam pomoći će vam pronaći ove brojeve. Iako ova metoda nije bez nedostataka, postoje video kodeci koji još uvijek nude ovu algoritamsku metodu kompresije.

I koder i dekoder moraju imati pristup tablici simbola sa svojim binarnim kodovima. Stoga je također potrebno poslati tablicu kao ulaz.

Aritmetičko kodiranje

Recimo da imamo tok znakova: a, e, r, s и t, a njihova je vjerojatnost prikazana u ovoj tablici.

a e r s t
Vjerojatnost 0,3 0,3 0,15 0,05 0,2

Pomoću ove tablice izgradit ćemo raspone koji će sadržavati sve moguće znakove, poredane prema najvećem broju.

Kako radi video kodek? Dio 2. Što, zašto, kako

Sada kodirajmo tok od tri znaka: jesti.

Prvo odaberite prvi znak e, koji je u podrasponu od 0,3 do 0,6 (ne uključujući). Uzimamo ovaj podraspon i ponovno ga dijelimo u istim omjerima kao prije, ali za ovaj novi raspon.

Kako radi video kodek? Dio 2. Što, zašto, kako

Nastavimo s kodiranjem našeg streama jesti. Sada uzmite drugi znak a, koji je u novom podrasponu od 0,3 do 0,39, a zatim uzmite naš posljednji znak t i ponavljajući isti proces ponovno, dobivamo konačni pod-raspon od 0,354 do 0,372.

Kako radi video kodek? Dio 2. Što, zašto, kako

Samo trebamo odabrati broj u zadnjem podrasponu od 0,354 do 0,372. Odaberimo 0,36 (ali možete odabrati bilo koji drugi broj u ovom podrasponu). Samo s ovim brojem moći ćemo vratiti izvorni stream. To je kao da povlačimo crtu unutar raspona da kodiramo naš tok.

Kako radi video kodek? Dio 2. Što, zašto, kako

Obrnuta operacija (tj. dekodiranje) je jednako jednostavan: s našim brojem 0,36 i našim početnim rasponom, možemo pokrenuti isti proces. Ali sada, koristeći ovaj broj, identificiramo tok kodiran pomoću ovog broja.

S prvim rasponom primjećujemo da naš broj odgovara rezu, stoga je ovo naš prvi znak. Sada ponovno dijelimo ovaj podraspon slijedeći isti postupak kao prije. Ovdje možete vidjeti da 0,36 odgovara simbolu a, a nakon ponavljanja procesa došli smo do posljednjeg znaka t (tvoreći naš originalni kodirani tok jesti).

I koder i dekoder moraju imati tablicu vjerojatnosti simbola, pa je potrebno i nju poslati u ulazne podatke.

Prilično elegantno, zar ne? Tko god da je smislio ovo rješenje, bio je vraški pametan. Neki video kodeci koriste ovu tehniku ​​(ili je barem nude kao opciju).

Ideja je komprimirati kvantizirani tok bitova bez gubitaka. Ovom članku zasigurno nedostaje mnoštvo detalja, razloga, kompromisa itd. Ali ako ste programer, trebali biste znati više. Novi kodeci pokušavaju koristiti različite algoritme entropijskog kodiranja kao što su ANS.

Korak 6 - bitstream format

Nakon što sve ovo učinite, preostaje samo raspakirati komprimirane okvire u kontekstu izvedenih koraka. Dekoder mora biti izričito obaviješten o odlukama koje je donio koder. Dekoderu se moraju osigurati sve potrebne informacije: dubina bita, prostor boja, razlučivost, informacije o predviđanju (vektori kretanja, usmjereno INTER predviđanje), profil, razina, brzina kadra, tip okvira, broj okvira i još mnogo toga.

Bacit ćemo brzi pogled na bitstream H.264. Naš prvi korak je stvoriti minimalni H.264 bitstream (FFmpeg prema zadanim postavkama dodaje sve opcije kodiranja kao što su SEI NAL — saznat ćemo o čemu se radi malo dalje). To možemo učiniti koristeći naše vlastito spremište i FFmpeg.

./s/ffmpeg -i /files/i/minimal.png -pix_fmt yuv420p /files/v/minimal_yuv420.h264

Ova naredba će generirati neobrađeni bitstream H.264 s jednim kadrom, rezolucija 64×64, s prostorom boja YUV420. U ovom slučaju, sljedeća slika se koristi kao okvir.

Kako radi video kodek? Dio 2. Što, zašto, kako

H.264 bitstream

Norma AVC (H.264) određuje da će se informacije slati u makrookvirima (u mrežnom smislu), tzv nal (ovo je razina mrežne apstrakcije). Glavni cilj NAL-a je pružiti video prezentaciju "web-friendly". Ovaj bi standard trebao raditi na televizorima (na temelju streama), internetu (na temelju paketa).

Kako radi video kodek? Dio 2. Što, zašto, kako

Postoji sinkronizacijski marker za definiranje granica NAL elemenata. Svaki token za sinkronizaciju sadrži vrijednost 0x00 0x00 0x01, osim prvog, koji je jednak 0x00 0x00 0x00 0x01. Ako lansiramo hexdump za generirani H.264 bitstream identificiramo najmanje tri NAL uzorka na početku datoteke.

Kako radi video kodek? Dio 2. Što, zašto, kako

Kao što je navedeno, dekoder mora znati ne samo slikovne podatke, već i detalje videa, okvira, boja, korištenih parametara i još mnogo toga. Prvi bajt svakog NAL-a definira njegovu kategoriju i tip.

NAL identifikator tipa Opis
0 Nepoznata vrsta
1 Kodirani fragment slike bez IDR-a
2 Odjeljak podataka kodiranog presjeka A
3 Odjeljak podataka kodiranog presjeka B
4 Odjeljak podataka kodiranog presjeka C
5 Kodirani IDR fragment IDR slike
6 Više informacija o proširenju SEI
7 Skup parametara SPS sekvence
8 Skup parametara PPS slike
9 Razdjelnik pristupa
10 Kraj niza
11 Kraj konca
... ...

Obično je prvi NAL toka bitova PLC. Ova vrsta NAL-a odgovorna je za informiranje o uobičajenim varijablama kodiranja kao što su profil, razina, razlučivost itd.

Ako preskočimo prvi marker sinkronizacije, možemo dekodirati prvi bajt da saznamo koji je NAL tip prvi.

Na primjer, prvi bajt nakon tokena sinkronizacije je 01100111, gdje je prvi bit (0) je u polju forbidden_zero_bit. Sljedeća 2 bita (11) govori nam polje nal_ref_idc, koji označava da li je ovaj NAL referentno polje ili ne. I preostalih 5 bitova (00111) govori nam polje tip_nal_jedinice, u ovom slučaju to je SPS blok (7) NAL.

Drugi bajt (binarni=01100100, šest=0x64, prosinca=100) u SPS NAL je polje profile_idc, koji pokazuje profil koji je koder koristio. U ovom slučaju korišten je ograničeni visoki profil (tj. visoki profil bez dvosmjerne potpore B-segmenta).

Kako radi video kodek? Dio 2. Što, zašto, kako

Ako pogledate specifikaciju bitstreama H.264 za SPS NAL, pronaći ćemo mnoge vrijednosti za naziv parametra, kategoriju i opis. Na primjer, pogledajmo polja pic_width_in_mbs_minus_1 и slika_visina_u_jedinicama_karte_minus_1.

Naziv parametra Kategorija Opis
pic_width_in_mbs_minus_1 0 ue(v)
slika_visina_u_jedinicama_karte_minus_1 0 ue(v)

Ako izvršimo neke matematičke operacije s vrijednostima ovih polja, dobit ćemo rezoluciju. Može se prikazati 1920 x 1080 pomoću pic_width_in_mbs_minus_1 s vrijednošću 119 ((119 + 1) * veličina_makrobloka = 120 * 16 = 1920). Opet, radi uštede prostora, umjesto kodiranja 1920, učinili smo to sa 119.

Ako nastavimo provjeravati naš stvoreni video u binarnom obliku (na primjer: xxd -b -c 11 v/minimalni_yuv420.h264), tada možete ići na posljednji NAL, koji je sam okvir.

Kako radi video kodek? Dio 2. Što, zašto, kako

Ovdje vidimo prvih 6 bajtnih vrijednosti: 01100101 10001000 10000100 00000000 00100001 11111111. Budući da je poznato da prvi bajt označava NAL tip, u ovom slučaju (00101) je IDR fragment (5), a zatim ga možete dalje istražiti:

Kako radi video kodek? Dio 2. Što, zašto, kako

Pomoću informacija o specifikaciji bit će moguće dekodirati vrstu fragmenta (vrsta_kriške) i broj okvira (frame_num) između ostalih važnih polja.

Da biste dobili vrijednosti nekih polja (ue(v), me(v), se(v) ili te(v)), moramo dekodirati fragment pomoću posebnog dekodera temeljenog na eksponencijalni Golombov kod. Ova metoda je vrlo učinkovita za kodiranje vrijednosti varijabli, posebno kada postoji mnogo zadanih vrijednosti.

značenje vrsta_kriške и frame_num ovog videa su 7 (I-fragment) i 0 (prvi kadar).

Bitstream se može smatrati protokolom. Ako želite saznati više o bitstreamu, trebali biste pogledati specifikaciju ITU H.264. Ovdje je makro dijagram koji pokazuje gdje su slikovni podaci (YUV u komprimiranom obliku).

Kako radi video kodek? Dio 2. Što, zašto, kako

Mogu se ispitati i drugi bitstreamovi, kao što je VP9, H.265 (HEVC) ili čak naš novi najbolji bitstream AV1. Jesu li svi slični? Ne, ali kada shvatite barem jedno, puno je lakše razumjeti ostatak.

Želiš vježbati? Istražite H.264 bitstream

Možete generirati videozapis s jednim okvirom i koristiti MediaInfo za ispitivanje bitstreama H.264. Zapravo, ništa vas ne sprječava da čak i pogledate izvorni kod koji analizira tok bitova H.264 (AVC).

Kako radi video kodek? Dio 2. Što, zašto, kako

Za vježbu možete koristiti Intel Video Pro Analyzer (jesam li već rekao da se program plaća, ali postoji besplatna probna verzija s ograničenjem od 10 okvira?).

Kako radi video kodek? Dio 2. Što, zašto, kako

Pregled

Imajte na umu da mnogi moderni kodeci koriste isti model koji smo upravo proučavali. Evo, pogledajmo blok dijagram video kodeka Thor. Sadrži sve korake kroz koje smo prošli. Cijela poanta ovog posta je da vam barem omogući bolje razumijevanje inovacija i dokumentacije u ovom području.

Kako radi video kodek? Dio 2. Što, zašto, kako

Prethodno je izračunato da će za pohranu video datoteke u trajanju od jednog sata u kvaliteti 139p i 720 fps biti potrebno 30 GB prostora na disku. Ako koristite metode o kojima se govori u ovom članku (interframe i interna predviđanja, transformacija, kvantizacija, entropijsko kodiranje, itd.), tada možete postići (na temelju činjenice da trošimo 0,031 bita po pikselu), videozapis prilično zadovoljavajuće kvalitete, zauzima samo 367,82 MB, a ne 139 GB memorije.

Kako H.265 postiže bolji omjer kompresije od H.264?

Sada kada znamo više o tome kako kodeci rade, lakše je razumjeti kako noviji kodeci mogu isporučiti više razlučivosti s manje bitova.

Ako usporedite AVC и HEVC, vrijedi zapamtiti da je to gotovo uvijek izbor između većeg opterećenja procesora i omjera kompresije.

HEVC ima više opcija odjeljka (i pododjeljka) od AVC, više unutarnjih smjerova predviđanja, poboljšano entropijsko kodiranje i više. Sva ova poboljšanja su napravljena H.265 sposoban kompresirati 50% više od H.264.

Kako radi video kodek? Dio 2. Što, zašto, kako

Prvi dio: Osnove rada s videom i slikom

Izvor: www.habr.com

Dodajte komentar