DirectShow


U ovom dokumentu opisani su osnovni početci programiranja DirectShow aplikacija. Za programiranje DirectShow aplikacija potrebno je imati Microsoft DirectX SDK. U ovom dokumentu kao programski alat koristi se C++ / Visual C++,  iako je programiranje DirectShow aplikacija moguće i u Visual Basicu.

 

1. Uvod u DirectShow

Rad s multimedijom predstavlja niz velikih izazova:

Microsoft DirectShow je arhitektura namijenjena za prikazivanje multimedijskih sadržaja na Microsoft Windows platformi. Dizajniran je da efikasno rješava navedeni niz izazova. Podržava više formata uključujući Advanced System Format (ASF), Digital Video (DV), Motion Picture Experts Group (MPEG), Audio-Video Interleaved (AVI) , MPEG Audio Layer 3 (MP3), WAV zvučne datoteke te mnoge druge. Podržava snimanje (capture) sa TV kartice putem Windows Driver Model (WDM) ili starijih Video for Windows (VfW) drivera. DirectShow je integriran zajedno sa ostalim DirectX tehnologijama te automatski pronalazi i koristi video i audio akceleraciju (DirectDraw i DirectSound) ukoliko je moguće, ali podržava i sustave koji nemaju hardwaresku akceleraciju. Koristi vremenski označene uzorke te tako rješava problem sinkronizacije. DirectShow je vrlo moćan alat koji znatno pojednostavljuje pisanje aplikacija kao što su DVD playeri, programi za obradu video zapisa, MP3 playeri, programi za snimanje sa TV kartica. DirectShow je zasnovan na Component Object Modelu (COM-u). Za pisanje DirectShow aplikacija potrebno je osnovno znanje COM objektnog programiranja.

 

2. Component Object Model (COM)

Component Object Model (COM) je model objektno-orijentiranog programiranja korišten pri izradi svih DirectX aplikacija pa ga tako koriste i DirectShow programeri. DirectShow programeri uglavnom ne moraju znati kako napraviti svoje COM objekte što može biti izrazito kompleksno već moraju imati osnove korištenja već postojećih COM objekata što je znatno jednostavnije. Za potrebe DirectShow programiranja, u ovom poglavlju biti će opisano što je COM objekt, kako ga stvoriti te kako dohvatiti njegova sučelja.

COM objekti su jednostavno rečeno crne kutije koje programi učitavaju i koriste. Uglavnom su implementirani kao DLL datoteke. Kao i normalni objekti COM objekti izlažu metode koje se pozivaju i obavljaju zadatke. Ono što bitno razlikuje COM objekte od normalnih je njihovo stvaranje te to da se DLL datoteke ne moraju eksplicitno učitavati već se COM brine o tome i automatski učitava potreban DLL. Svaki objekt izlaže određen broj sučelja (interfaces). Sučelje je zapravo grupa metoda (funkcijskih članova objekta) koje obavljaju razne zadatke. Dakle, ukoliko se želi obaviti neka operacija nad COM objektom potrebno je prvo zatražiti određeno sučelje, a nakon toga pozvati metodu preko tog sučelja.

Svaki objekt mora imati GUID (Globally Unique Identifier) kako bi se objekti razlikovali. GUID je 128-bitna struktura koja je stvorena na takav način da ne postoji još jedna ista takva struktura. COM koristi GUID-e radi jedinstvenog označavanja objekata (CLSID) i radi jedinstvenog označavanja sučelja (IID).

Svaka COM metoda vraća 32-bitnu strukturu nazvanu HRESULT. Kod većine metoda HRESULT donosi dvije informacije; da li je metoda uspješno obavila svoj zadatak te malo detaljniji opis o ishodu operacija unutar metode. Prema dogovoru, ukoliko je metoda uspješno obavila operaciju prefiks HRESULT vrijednosi je S_, a ukoliko je došlo do greške prefiks je E_. Npr. dva najviše korištena koda su S_OK i E_FAIL. Potrebno je biti oprezan prilikom provjere vraćenih vrijednosti što može ilustrirati slijedeći primjer:

if(hr == E_FAIL)
{
    //U slučaju greške
}

else
{
    //U slučaju da je sve OK
}

Moglo se dogoditi da je metoda vratila npr. vrijednost E_INVALIDARG (poslani krivi argumenti), a uz ovakvo provjeravanje ispalo bi da je sve ok te bi vrlo vjerojatno došlo do rušenja programa. Zbog toga se koriste makro funkcije SUCCEDED i FAILED. Makro SUCCEDED vraća TRUE za sve vrijednosti s prefiksom S_, dok makro FAILED vraća TRUE za sve vrijednosti s prefiksom E_. Ispravno napisano provjeravanje bilo bi:

if(FAILED(hr))
{
    //U slučaju greške
}

else
{
    //U slučaju da je sve OK
}

Dva najviše korištena načina za stvaranje COM objekta kod DirectShow programiranja su:

Prije nego se bilo koji objekt može stvoriti potrebno je inicijalizirati COM pozivanjem funkcije CoInitialize. Na kraju programa kada se više neće koristiti COM programiranje potrebno je pozvati CoUninitialize. Ovdje će biti navedeno stvaranje objekta direktno budući da se indirektno stvaranje objekta koristi rjeđe u DirectShow-u.

Da bi stvorili objekt putem CoCreateInstance funkcije potrebno je znati CLSID objekta koji želimo stvoriti. Funkcija prima pet parametara:

Slijedeći primjer pokazuje kako stvoriti Filter Graph Manager objekt te dohvatiti njegovo IGraphBuilder sučelje putem CoCreateInstance funkcije.

IGraphBuilder* pGraph;
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, 
	CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph);

Ponekad su nam potrebna još neka sučelja koje objekt može izložiti. Svaki COM objekt ima IUnknown sučelje koje sadrži metodu QueryInterface. Zbog toga nije potrebno stvarati novi objekt kako bi zatražili drugo sučelje već je moguće na postojećem objektu zatražiti dodatno sučelje koje trebamo putem QueryInterface metode. Naravno, potrebno je znati IDD sučelja koje želimo. Primjer:

IMediaControl *pControl;
IMediaEvent   *pEvent;
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

Navedeni primjer dohvaća pokazivače na IMediaControl i IMediaEvent sučelja koja izlaže Filter Graph Manager.

 

3. DirectShow arhitektura

U ovom poglavlju navedena je osnovna terminologija i koncept DirectShow programiranja. Osnovna jedinica DirectShow-a naziva se filtar. Filtar je softwareska komponenta koja obavlja neku radnju na multimedijalnim podacima. DirectShow filtri mogu:

Filtri dobivaju ulazne podatke i stvaraju izlazne. Npr. ako filtar dekodira MPEG video, njegov ulaz je MPEG komprimiran signal, a izlaz je niz nekomprimiranih video frameova. U DirectShow-u aplikacija obavlja zadatak povezivanjem više filtara u lance tako da je izlaz jednog filtra ulaz drugome. Ovako povezani filtri čine graf filtara (filter graph). Aplikacija ne mora voditi računa o toku podataka kroz graf već je za to zadužena komponenta Filter Graph Manager. Aplikacija poziva API funkcije kao što su "Run" (puštanje streama podataka kroz graf) i "Stop" (zaustavljanje streama kroz graf). Ukoliko je potrebno više kontrole, nad pojedinim filtrima moguće je zatražiti njihova dodatna sučelja te preko njih pristupati direktno filtrima. Filter Graph Manager pruža metode koje aplikacija poziva radi stvaranja grafa povezivanjem filtara u lance. Dakle, programer piše DirectShow aplikaciju tako što pomoću Filter Graph Managera gradi graf, tj. povezuje filtre. Strogo gledajući postoje tri osnovna koraka koje aplikacija mora napraviti. Koraci su ilustrirani slikom:

  1. Aplikacija mora stvoriti objekt Filter Graph Manager

  2. Aplikacija koristi Filter Graph Manager da sagradi graf

  3. Aplikacija koristi Filter Graph Manager radi kontroliranja grafa te protoka podataka kroz graf. Aplikacija također reagira na događaje koje joj šalje Filter Graph Manager.

DirectShow filtri sadrže COM objekte koji se zovu pinovi (pins). Pinovi su spojna mjesta filtara te ih filtri koriste za prebacivanje podataka iz jednog filtra u drugi. Filtri se mogu svrstati u nekoliko osnovnih kategorija:

Već je prije rečeno da se graf stvara pomoću objekta zvanog Filter Graph Manager. Filter Graph Manager podržava sljedeće metode za stvaranje grafa. Metode su navedene tako da je napisano sučelje preko kojeg se pristupa određenoj metodi.

Ove metode nude tri osnovna pristupa stvaranju grafa:

  1. Filter Graph Manager stvara cijeli graf

    Ovaj način stvaranja grafa koristi se pozivom RenderFile metode ukoliko programer želi producirati npr. avi, mpeg, wav ili mp3 datoteku. RenderFile metoda automatski dodaje potrebne filtre u graf te ih spaja.
     

  2. Filter Graph Manager stvara dio grafa

    Ukoliko želimo da naša aplikacija radi bilo što drugo osim samo produciranja datoteke, potrebno je da aplikacija stvori barem dio grafa. Npr. ukoliko radimo aplikaciju za prikaz TV signala sa TV kartice potrebno je "ručno" dodati source filtar, a nakon toga prepustiti Filter Graph Manageru da dovrši posao pozivanjem Render metode.
     

  3. Aplikacija stvara cijeli graf

    Ponekad je potrebno da aplikacija dodaje u graf svaki filtar posebno pomoću AddFilter metode. Nakon toga potrebno je dohvatiti pokazivače na svaki pin pojedinog filtra te ih međusobno povezati pomoću Connect ili ConnectDirect metoda. Iako je ovakvo stvaranje najkompliciranije, omogućuje nam najveću moguću specifikaciju prilikom stvaranja grafa, a ponekad je i jedino moguće.

Prilikom povezivanja grafa pomoću metoda Render, RenderFile i Connect koristi se tzv. inteligentno spajanje (Intelligent Connect). Inteligentno spajanje sastoji se od nekoliko algoritama pomoću kojih Filter Graph Manager određuje koje filtre će ubaciti u graf radi mogućeg spajanja. Ukoliko npr. želimo spojiti izlaz filtra koji podržava RGB24 tip video podataka sa filtrom kojem ulaz zahtjeva tip RGB32, spajanje nećemo moći obaviti metodom ConnectDirect budući da ova metoda ne podržava inteligentno spajanje već pokušava direktno spojiti navedene filtre. Ukoliko ova dva filtra spajamo metodom Connect, ova metoda će pokušati naći odgovarajući filtar koji će ubaciti između ova dva filtra radi prilagođavanja tipa podataka. Tako, za navedeni primjer, metoda Connect će ubaciti filtar zvan Color Space Converter koji će ulazni tip RGB24 pretvoriti u tip RGB32 na svom izlazu, te će se filtri uspješno spojiti. Inteligentno spajanje omogućuje nam, dakle, da ne moramo točno znati koji filtar je moguće spojiti s kojim već prepuštamo DirectShow-u da se pobrine o tome.

 

4. GraphEdit

GraphEdit je pomoćni alat za stvaranje i simuliranje grafa koji dolazi zajedno sa Microsoft DirectX SDK. Pomoću GraphEdit-a možemo stvoriti graf, provjeriti njegovu funkcionalnost, eksperimentirati prije nego što uopće počnemo pisati aplikaciju.

Pomoću GraphEdit-a, moguće je:

Također je moguće učitati graf koji se izrađuje u aplikaciji te vidjeti da li aplikacija stvara točan graf. Ukoliko proizvedete svoj filtar moguće unutar GraphEdit-a moguće ga je testirati. Moguće je i stvoriti graf, spremiti ga u datoteku i kasnije učitati u aplikaciji i koristiti kao da je u aplikaciji i sastavljen. Međutim, ovakav način pisanja aplikacije ipak se ne preporučuje.

 

5. Primjer

Da bi se vidjelo kako se sve ovo napisano može iskoristiti, naveden je jedan primjer. Primjer predstavlja jednostavno napisan player koji između ostalog može reproducirati wav, mp3, mpeg i avi. Primjer nema veliku funkcionalnost, te je naveden radi pokazivanja kako koristiti navedene objekte i sučelja.

Program počinje pozivanjem CoInitialize funkcije potrebne za inicijalizaciju COM programiranja.

HRESULT hr = CoInitialize(NULL);
if (FAILED(hr))
{
    // Ispisati poruku ukoliko je došlo do greške
}

Radi jednostavnosti u daljnjem kodu neće biti provjeravane greške, ali u pravom programu uvijek bi trebalo provjeravati povratne vrijednosti funkcija.

Koristeći CoCreateInstance funkciju stvaramo objekt Filter Graph Manager:

IGraphBuilder *pGraph;
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, 
    CLSCTX_INPROC_SERVER, IID_IGraphBuilder, (void **)&pGraph);

Vidljivo je da je CLSID Filter Graph Managera CLSID_FilterGraph. Pozivom funkcije CoCreateInstance stvoren je objekt, te je vraćeno sučelje IGraphBuilder potrebno za gradnju grafa. Također će nam u programu zatrebati još dva sučelja IMediaControl i IMediaEvent.

Preko IMediaControl sučelja kontrolira se graf. U ovom primjeru služi za pokretanje grafa.
Preko IMediaEvent sučelja dobivaju se događaji koje šalje aplikacija ili Filter Graph Manager. U ovom primjeru koristi se radi čekanja da reprodukcija završi.

Sučelja dohvaćamo pozivom QueryInterface metode preko IGraphBuilder sučelja.

IMediaControl *pControl;
IMediaEvent   *pEvent;
hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

Sada gradimo graf. Ovdje korstimo prvi od tri navedena načina stvaranja grafa. Pozivom RenderFile metode, Filter Graph Manager automatski dodaje potrebne filtre za prikaz datoteke te ih spaja. Kao prvi parametar navodimo ime datoteke koju želimo pustiti, dok je drugi parametar rezerviran i mora biti NULL.

hr = pGraph->RenderFile(L"C:\\Primjer.avi", NULL);

Ova metoda neće se uspješno izvršiti ukoliko navedena datoteka ne postoji ili format datoteke nije podržan. (Ne može se sagraditi graf). Svakako ovdje upisati stvarno ime postojeće datoteke na disku. U pravom programu ovo bi svakako trebalo provjeriti.

Graf se pokreće pozivom Run metode preko IMediaControl sučelja.

hr = pControl->Run();

Nakon poziva ove metode podaci kreću kroz graf, dolaze do audio i video renderer filtara i prikazuju se. Međutim, protok podataka se odvija u posebnoj niti, a program se izvršava dalje. Zbog toga je potrebno pozivom funkcije WaitForCompletion preko IMediaEvent sučelja blokirati program i pričekati kraj prikazivanja.

long evCode = 0;
pEvent->WaitForCompletion(INFINITE, &evCode);

Ova metoda blokira dok nije došao kraj datoteke ili dok se, u prvom parametru, navedeni vremenski trenutak ne dostigne (izrazen u milisekundama). Kako je ovdje prvi parametar INFINITE, metoda blokira sve dok se ne dostigne kraj datoteke.

Kada je prikazivanje završilo, potrebno je otpustiti pokazivače na sučelja te zatvoriti COM.

pControl->Release();
pEvent->Release();
pGraph->Release();
CoUninitialize();

Ovdje je naveden cijeli kod programa. Prilikom prevođenja potrebno je dodati Strmiids.lib datoteku unutar link opcije.

include <dshow.h>
void main(void)
{
    IGraphBuilder *pGraph = NULL;
    IMediaControl *pControl = NULL;
    IMediaEvent   *pEvent = NULL;

    // Inicijalizacija COM-a
    HRESULT hr = CoInitialize(NULL);
    if (FAILED(hr))
    {
        printf("Greska, nemoguce inicijalizirati COM.\n");
        return;
    }

    // Stvaranje Filter Graph Managera i trazenje dodatnih sucelja
    hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, 
                        IID_IGraphBuilder, (void **)&pGraph);
    if (FAILED(hr))
    {
        printf("Greska, nemoguce stvoriti Filter Graph Manager.\n");
        return;
    }

    hr = pGraph->QueryInterface(IID_IMediaControl, (void **)&pControl);
    hr = pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

    // Gradjenje grafa. VAZNO: Podesiti prvi parametar na postojecu datoteku.
    hr = pGraph->RenderFile(L"C:\\Example.avi", NULL);
    if (SUCCEEDED(hr))
    {
        // Pokrece graf
        hr = pControl->Run();
        if (SUCCEEDED(hr))
        {
            // Cekanje kraja prikazivanja.
            long evCode;
            pEvent->WaitForCompletion(INFINITE, &evCode);

            // Ne koristiti INFINITE u pravom programu jer moze blokirati beskonacno dugo
        }
    } else {
        printf("Greska, ne mogu pronaci datoteku ili format nije podrzan.\n");
	return;
    }
    // Otpustiti sucelja
    pControl->Release();
    pEvent->Release();
    pGraph->Release();
    CoUninitialize();
}

Izvorni kod napisan kao Visual C++ projekt: Primjer.zip

 

6. Literatura

1. Microsoft DirectX 9.0 SDK C++ Documentation

2. Mark D. Pesce, Programming Microsoft DirectShow for Digital Video and Television