Přehrávání a střih videozáznamů v jazyce JAVA

Dostupné prostředky

V jazyce JAVA existuje pouze jedna jediná možnost, jak zpracovávat a přehrávat video. Tou je balík JMF neboli Java Media Framework. Tento balík je API pro začlenění časově závislých (time-based) mediálních dat (zvuk, video) do Java aplikací a appletů. Aktuální verze JMF je 2.1.1e, která je dostupná ve třech verzích, a to platformě nezávislá, pro Linux a pro Windows na adrese http://java.sun.com/products/java-media/jmf/index.jsp. Každá z verzí má jinou podporu pro různé formáty. Podporované formáty jsou uvedeny zde. Uvedené podporované formáty jsou platné pro verzi 2.1.1.

Prostředky pro přehrávání videa v JMF

Přímo pro přehrávání časově závislých medií je v jazyce JAVA určeno rozhraní Player, které má metody splňující naše kladené požadavky na přehrávání. Těmito nezbytnými metodami jsou:

Nezbytné pro pochopení fungování všech instancí implementující rozhraní je nutné nastínit, že rozhraní Player, ať už je implementováno jakkoliv, má několik možných stavů. Stavy a přechody mezi nimi ukazuje následující diagram.


V každém stavu nabízí jiné možnosti manipulace. Metody „realized“ a „prefetched“ jsou zděděny z rozhraní Controller. Přechody do stabilních stavů jsou iniciovány metodami k tomu určenými. Tyto metody jsou neblokující a proto po zavolání například metody „realize“ po jejím skončení se nemusí přehrávač nacházet ve stavu Realized nýbrž pouze ve stavu Realizing. Pro zjištění aktuálního stavu přehrávače slouží metoda „getState“, která je také z rozhraní Controller. Při přechodu z jednoho stavu do druhého je zaregistrovanému posluchači typu ControllerListener zaslána informace o příslušné změně stavu.

Rozhraní Player samozřejmě vytvořit instanci sama sebe neumí, proto je nutné použít třídu Manager, která má již prostředky, jak vrátit instanci rozhraní Player. Třída Manager má všechny metody statické a je chápána jako singleton, proto není možné založit její instanci, která stejně není zapotřebí. Pro pouhé přehrávání bez aplikování různých filtrů a podobných úprav nám bude stačit její přetížená metoda „createRealizedPlayer“, která je připravena v těchto třech verzích:

Metoda „createRealizedPlayer“ je blokující a po jejím skončení vrací přehrávač, který je ve stavu Realized. Během zpracování požadavku může metoda propagovat několik výjimek, které podávají přesné informace, v čem je problém. Výjimky mohou nastat tyto tři:



Praktické zkušenosti s přehrávačem

Bohužel praktická zkušenost s těmito výjimkami není dobrá. K vyvolání výjimky občas nedochází a tedy není dost dobře možné na vzniklé situace reagovat. Ani po bližším seznámením s problémem se mi nepodařilo zjistit, proč dochází k výpisu chybové hlášky na standardní chybový výstup a výjimka není vyvolána. Testy jsem prováděl jak na nepodporovaných formátech, tak i na neexistujících souborech. Naštěstí se během implementace můžeme spolehnout na to, že pokud došlo k chybě, tak se nevytvoří žádný přehrávač a do proměnné bude uložena hodnota null. Tomuto neduhu jsem musel upravit i návrh třídy, která se bude starat o přehrávání video záznamu. Je hned několik věcí, které se musí bohužel ošetřovat nepřímo. Proto doporučuji otestovat libovolným způsobem, zda soubor, který se má přehrát, opravdu existuje. A po skončení metody „createRealizedPlayer“ provést test, jestli opravdu vytvořila kýžený přehrávač.

Pokud vše proběhlo bez problémů a kýžený přehrávač je vytvořen, je již velice snadné začít přehrávat data ze zvoleného zdroje. Stačí zavolat metodu „start“, která postupně inicializuje přechody ze stavu Realized do stavu Started. Dalším problematickým faktorem je, že předem není možné určit, kdy se opravdu zvolená data začnou prezentovat.

Nachází-li se přehrávač ve stavu Started je bez problémů možné volání metod „setRate“ a „setMediaTime“, ty se už postarají o potřebné akce, které je nutné provést. U metody „setMediaTime“ nelze předem určit, jak dlouho jí bude trvat než provede potřebné akce k přesunu v toku dat. Samozřejmě je tato metoda také neblokující. Metoda „setRate“ má další specifikum. Tím specifikem je, že se pouze pokusí nastavit požadovanou rychlost přehrávání a zároveň jako návratovou hodnotu vrátí skutečně nastavenou rychlost přehrávání. Tyto dvě rychlosti se vůbec nemusí shodovat.

Bohužel i zde vyvstala chyba. I když se metoda tváří, že nastavila vyšší rychlost přehrávání, opak je pravdou. Testováním bylo zjištěno, že při rychlosti vyšší než 2 se zrychlí akorát rychlost posouvání zobrazovaného ovládacího prvku, který určuje pozici ve videozáznamu. Video se ovšem pořád přehrává blíže neurčenou rychlostí (nejspíše rychlostí 1), která rozhodně neodpovídá rychlosti zvolené. Směrem ke zpomalení je situace lepší, testované zpomalení 0,1 vypadalo vcelku v pořádku.

Prostředky pro editaci ( i střih) videa v JMF

Pro editaci časově závislých medií je v JMF určeno rozhraní Processor, které je odvozeno z rozhraní Player. Rozhraní Processor přidává k rozhraní Player několik důležitých funkcí, které umožňují lepší kontrolu nad zpracovávanými daty. Rozhraní Processor se ale neliší jen v počtu poskytovaných funkcí, ale i počtem stavů. Do rozhraní Processor jsou přidány dva další stavy, které byly přidány kvůli kýžené konfigurovatelnosti. Stavový diagram tedy vypadá takto:


Než Processor dosáhne stavu Configured, nelze u něj provádět žádná nastavení. Do stavu Configured se Processor uvede zavoláním metody „configure“. Metoda je také neblokující, proto je nutné ošetřit čekání. Během stavu Configuring se může Processor pokusit přistoupit a číst ze vstupního souboru či jiného zdroje dat. Jako vstupní zdroj dat mu může sloužit mikrofon, webová kamera nebo jiné podobné zařízení. Jakmile Processor dosáhne stavu Configured, je zaregistrovanému posluchači zaslána zpráva ConfigureCompleteEvent.

Metody rozhraní Processor

Processor stehně jako Player, nemůže založit svoji vlastní instanci, protože se jedná o rozhraní. K vytvoření Processor je tedy nutné použít třídu Manager. Třída Manager pro vytvoření Processoru nabízí tyto metody:

V praxi lze řadit několik Processorů za sebou, čímž je možné aplikovat několik různých efektů. Processor lze použít i na převod na jiný formát. Řazené Processory za sebou spolupracují metodou producent-konzument.

Střih videa krátký úvod

Pro sestříhání časově závislých dat je klíčové dodržet přesnou délku vybraného úseku. Aby bylo možné dosáhnout požadované přesnosti, je nutné zvolený formát zvukového záznamu převést do nekomprimované podoby (jedná se o zvukovou stopu ve video záznamu). Dalším důležitým krokem je zvolit formát video stopy. Požadavky na zvolený video formát nejsou zrovna malé, co se prostředí JMF týče. Po video formátu, který chceme použít, požadujeme možnost jej uložit do výsledného souboru. K tomu by bylo dobré mít kontrolu nad kvalitou kvůli výsledné velikosti souboru.

Pro změny formátu jednotlivých stop použijeme rozhraní TrackControl. Její instance poskytuje rozhraní Processor metodou „getTrackControl“, která je popsána výše. Rozhraní TrackControl k tomuto účelu poskytuje metodu zděděnou z rozhraní FormatControl.

        Public Format setFormat(Format format)

Pro změnu formátu video stopy a audio stopy se samozřejmě používá jiný formát. Třída, která určuje formáty video stopy, je potomkem třídy Format a jmenuje VideoFormat. Pro audio stopy existuje analogicky potomek třídy Format, který se jmenuje AudioFormat. Pro zvolení formátu obou stop není nic jednoduššího, než ho změnit. Kód může vypadat třeba takto:

        TrackControl tc[] = processor.getTrackControl();
        tc[0].setFormat(new VideoFormat(VideoFormat.JPEG));
        tc[1].setFormat(new AudioFormat(AudioFormat.LINEAR));

Zvolené formáty jsou JPEG a LINEAR. Tím je docíleno změny formátu zvuku, který umožní správně určit délku přehrávaného (stříhaného záznamu). Dalším nezbytným krokem je nastavení výstupu Processoru. Jak je výše uvedeno, k tomu je určena metoda „setContentDescriptor“. Protože není vhodné v této fázi vynucovat jiný výstup než prostý výstup nikterak upravovaný pro formáty souborů jako jsou AVI, MOV apod, bude nastavení vypadat takto:

        processor.setContentDescriptor( new FileTypeDescriptor.RAW);

Toto nastavení ale není nezbytně nutné, protože je Processorem implicitně předpokládáno. V této fázi se musí Processor převést do stavu Realized. Pokud toto vyvolá výjimku, zvolené formáty nejsou podporovány. Také je možné provést předem kontrolu za pomoci metody „getSupportedContentDescriptors“.

Bohužel další kroky již tak jasné nejsou. Přístup selským rozumem zde asi není vhodný. Cílem je vystřihnout požadovaný úsek video záznamu. Takže nejjednodušší přístup, který se zde nabízí, je nastavit u Processoru metodou „setMediaTime“ počátek zvoleného úseku a metodou „setStopTime“ nastavit konec zvoleného úseku a  výstup Processoru přesměrovat do výstupního souboru. Bohužel tento postup nelze realizovat, protože při pokusu nastavit „setMediaTime“ jiný než 0 (tedy počátek) nastane chyba v dekódování, která ovšem nevyvolá výjimku, nýbrž jen chybové hlášení na výstup. Tedy je nutné se od této koncepce odklonit.

Další možnou alternativou by mohlo být vytvořit další Processor, který by byl přilepený na výstup předešlého Processoru (rozdělení bude „výstupní processor“ a „vstupní processor“). Navázání vypadá takto:

        DataSource ds =  vstupniProcessor.getDataOutput();
        Processor vystupniProcessor  =  Manager.createProcessor(ds);

Takto vytvořený Processor je nutné uvést do stavu Configured. V této fázi se provede nastavení výstupního kódování audio signálu a video signálu. Také je nutné nastavit formát souboru, do kterého má být video záznam posléze uložen. Konfigurace vypadá takto:

        ContentDescriptor cdout = new FileTypeDescriptor(FileTypeDescriptor.MSVIDEO);
        vystupniProcessor.setContentDescriptor(cdout);
        TrackControl tc[] = vystupniProcessor.getTrackControl();
        tc[0].setFormat(new VideoFormat(VideoFormat.MJPG));
        tc[1].setFormat(new AudioFormat(AudioFormat.ULAW,8000,8,1));

Konstanta FileTypeDescriptor.MSVIDEO udává, že výstupní soubor bude ve formátu AVI. Video formát je MJPG ( zkratka od Motion JPEG), audio stopa bude ve formátu ULAW se vzorkovací frekvencí 8000Hz, rozlišením 8bitů a pouze mono. Tyto nastavení mají veliký vliv na velikost výstupního souboru, proto je nutné si uvědomit, jak moc kvalitní zvuk je požadován. Bohužel ne každé nastavení je kompatibilní a někdy se stává, že výsledný zvuk je špatně převzorkován. Proto je nutné si s tímto nastavením trochu pohrát.

Nyní je nutné převést „Výstupní processor“ do stavu Realized.

V tuto chvíli je již vše připraveno na uložení do souboru. K tomuto účelu slouží rozhraní DataSink. Toto rozhraní je velice jednoduché použít i bez jeho podrobnější znalosti. Vytvořit jej umí zase jen třída Manager, která k tomuto účelu nabízí jednu jedinou metodu:

static DataSink createDataSink (DataSource datasource, MediaLocator destLocator)

Metoda vytvoří požadovaný DataSink a její volání vypadá takto:

        DataSink fileWriter = Manager.createDatasink(vystupniProcessor.getDataOutput(), vystupniMediaLocator);

V této chvíli je nutné zaregistrovat příslušné posluchače, kteří by zareagovali na konec souboru (obecně na konec dat). Posluchač pro „vstupní processor“ i „výstupnímu processor“ implementuje rozhraní ControllerListener a může vypadat třeba takto:

class EOMListener implements ControllerListener {
        Processor p;
        public EOMListener(Processor p) {
            this.p = p;
        }
        public void controllerUpdate(ControllerEvent ce) {
                if (ce instanceof StopEvent){
                                p.close();
                        }
                        System.out.println(ce.getClass().toString());
                }
    }

Další posluchač, musí být zaregistrován instanci DataSink „fileWriter“. Třída, musí implementovat rozhraní DataSinkListener. Implementace konkrétní metody:

public synchronized void dataSinkUpdate(DataSinkEvent event) {
                if (event instanceof EndOfStreamEvent) {
                    filewriter.close();
                }
}

Nyní už je vše připraveno na spuštění a testování teorie. Cílem je vystřižení kýžené části video záznamu včasným nastartováním „Výstupního processoru“. Při pokusu spustit „výstupní processor“ později narazíme na další úskalí. Tímto úskalím je zatuhnutí vláken obou processorů. Zatuhnutí je způsoben řešením producent-konzument, protože námi vytvořený konzument „výstupní processor“ neodebírá data od producenta, producentův buffer se zaplní a to způsobí uváznutí. Druhý přístup, že by producent začal přehrávat od zvoleného času, vyvolá „chybu při dekódování“.

Proto je nutné problém vyřešit úplně jinak. Současná vzniklá struktura, kterou jsme vytvořili, vypadá takto:


Protože ani jedna z předpokládaných metod nevedla k úspěchu, je nutné přijít na jiné řešení. Řešením je vložit námi řízený DataSource mezi „vstupní“ a „výstupní“ processor. Po tomto datovém zdroji budeme chtít, aby si sám počítal čas a nepotřebná data z bufferu vstupního Processoru zahazoval. Bohužel toto řešení bude časově náročné. K implementaci této části bude zapotřebí vytvořit dvě třídy. Jedna třída bude potomek třídy DataSource konkrétně třídy PushBufferDataSource. Tato vzniklá třída bude spravovat instance třídy, která bude implementovat rozhraní PushBufferStreamBufferTransferHandler. Takto vzniklá třída bude spravovat přenos dat, tedy vybírat data od „vstupního processoru“ a ze zvukových dat bude provádět výpočty času. Z výpočtu času bude určovat, zda daná data zahodit či je předat „výstupnímu processoru“ ke zpracování.

Použitá metoda má hned několik nevýhod.

První dva neduhy není možné nijak ovlivnit, protože architektura JMF to neumožňuje. Snad v další verzi JMF bude tento problém možné řešit jiným způsobem.

Problémy knihoven, o kterých se nepíše

Během různých pokusů jsem narazil na jeden překvapivý problém. Při pokusu o střih formátu MPEG-1, který nebyl zcela v pořádku (jediný program, který hlásil chybu byl VirtualDub), došlo k výjimce v odpovědné dynamické knihovně, která je součástí instalace JMF pro windows. Tato výjimka nebyla výše propagována a střih video záznamu skončil. Naštěstí k tomuto jevu docházelo jen u poškozených souborů. Takto poškozený soubor není problém přehrát.

Zdrojové kódy, které implementují střih, jsou ke stažení zde. Zdrojové kódy jsou odvozeny z příkladu Cutting Sections from an Input.