Multimediální frameworky: stavíme vlastní přehrávač videa s pomocí FFmpeg

29. 1. 2025
Doba čtení: 9 minut

Sdílet

Autor: Depositphotos
V článku si vysvětlíme základní práci s kontejnerem a vytvoříme si jednoduchý přehrávač videa. Popíšeme si celý proces práce se snímky, abychom mohli přehrávat video. Použijeme k tomu API frameworku FFmpeg.

V tomto článku bude vysvětlena základní práce s kontejnerem a tvorba triviálního přehrávače videa. Dokumentaci k veškerému API lze nalézt na oficiálních stránkách frameworku FFmpeg.

Pro použití API frameworku je nutné překladač informovat, kde leží příslušné hlavičkové soubory a knihovny. V systémech unixového typu k tomu lze využít nástroj pkg-config. Volání překladače by mohlo vypadat zhruba následovně.

cc -I/usr/include/libavformat app.c -lavformat -lavutil -o app

Rozhraní knihoven FFmpeg je v čistém C. Pokud chceme volat funkce z C++, je třeba obalit příslušné#include konstrukcí extern "C" . K použití funkcí z jedné z knihoven frameworku FFmpeg, např. libavformat, je třeba vložit (includovat) příslušný hlavičkový soubor. Názvy hlavičkových souboru jsou shodné s názvy knihoven (bez prefixu „lib“), např. #include <avformat.h> .

Práce s kontejnerem

Následně se voláním funkce avformat_open_input otevře vstupní tok (kontejner, např. soubor AVI). Funkce vytvoří strukturu AVFormatContext, která zaobaluje informace o multimediálním kontejneru. Nyní je možné načíst z kontejneru základní informace (délka trvání, datový tok). To provede funkce avformat_find_stream_info. Zavoláním av_dump_format se na standardní chybový výstup vypíše několik základních informací o kontejneru a v něm obsažených datových tocích (stopách). Pro korektní uvolnění prostředků je nakonec nutno zavolat avformat_close_input. Celý příklad je možno vidět dále (jednoduchý program, který otevře vstupní soubor a vypíše o něm základní informace).

#include <avformat.h>

int main(int argc, const char *argv[])
{
    const char *filename = argc > 1 ? argv[1] : "clock.avi";

    av_log(NULL, AV_LOG_INFO, "Opening %s...\n", filename);

    AVFormatContext *pFormatCtx = NULL;

    if (avformat_open_input(&pFormatCtx, filename, NULL, NULL))
        abort();

    if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
        abort();

    av_dump_format(pFormatCtx, 0, filename, 0);

    avformat_close_input(&pFormatCtx);

    return 0;
}

Další text se věnuje pouze extrakci a zobrazení videosnímků. Pro přehrávání není volání avformat_find_stream_info a av_dump_format nezbytné. Po volání avformat_open_input jsou ve struktuře AVFormatContext k dispozici informace o datových tocích (stopách) uvnitř kontejneru. Jednotlivé stopy lze procházet pomocí pole streams této struktury. Každá stopa je zde typu AVStream. Uvnitř této struktury je důležitá především položka codecpar, která ukazuje na strukturu typu AVCodecParameters. AVCodecContext je obálka informací pro kodek, nikoli však vlastní kodek.

Z této struktury je nyní podstatná položka codec_type, pomocí které je možno rozlišit audio ( AVMEDIA_TYPE_AUDIO) a video ( AVMEDIA_TYPE_VIDEO) stopy. V předchozím příkladu je vstupní datový tok (kontejner) přístupný přes pFormatCtx. Videostopu je možné identifikovat postupným procházením všech stop uvnitř tohoto kontejneru. Namísto ručního procházení lze použít také funkci  av_find_best_stream.

Autor: David Bařina

Jádrem přehrávače je smyčka, ve které se dekódují jednotlivé snímky. Před jejich dekódováním je nutné alokovat prostředky a inicializovat kodek. Po skončení přehrávání je možné prostředky uvolnit.

Dekódování snímku

Nyní je třeba pro informace uvnitř struktury AVCodecContext nalézt odpovídající dekodér, to provede avcodec_find_decoder. V případě úspěchu je možno přistoupit k inicializaci dekodéru voláním avcodec_open2. Následně lze přistoupit k vlastnímu dekódování snímků. Snímek reprezentuje struktura AVFrame. Pro právě dekódovaný snímek je potřeba tuto strukturu alokovat, to provede av_frame_alloc. Dále je potřeba struktury AVPacket, do které budou z videostopy načtena surová (zkomprimovaná) data odpovídající jednomu snímku.

Vlastní dekódování bude probíhat ve smyčce (dokud bude co přehrávat). V každé iteraci smyčky se pomocí av_read_frame načte jeden paket ( AVPacket). Ten se vzápětí pomocí avcodec_send_packet a avcodec_receive_frame dekóduje do struktury AVFrame. Odtud je možné jej vykreslit na obrazovku (nebo jinak zpracovat). Kritická část kódu je v příkladu níže.

V kódu níže vidíme smyčku dekódující snímky u triviálního přehrávače video snímků. Po dekódování je možno provést zpracování (zobrazení) – naznačeno tečkami.

AVPacket pkt;

while (av_read_frame(pFormatCtx, &pkt) == 0 {
    if (pkt.stream_index == videoStream) {
        if (avcodec_send_packet(pCodecCtx, &pkt) < 0)
            abort();

        if (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
            // ...
        }
    }
    av_packet_unref(&pkt);
}

Protože dekódovaný snímek pravděpodobně nebude ve formátu, který je možno přímo odeslat do renderovacího zařízení, je nutné provést konverzi jeho formátu. Vhodným formátem může být snímek s pixely v BGR24, což udává v případě FFmpegu položka AV_PIX_FMT_BGR24 výčtu AVPixelFormat. Konverzi provedeme ihned po dekódování snímku voláním sws_scale z knihovny libswscale. Použití je v příkladu níže.

Příklad ukazuje převod snímku na RGB a zobrazení pomocí OpenCV. Dekódovaný snímek je uložen v pFrame, snímek v BGR24 bude uložen do pFrameRGB. OpenCV předpokládá pixely v pořadí BGR, s typem CV_8UC3 to bude přímo formát BGR24.

Mat img(pCodecCtx->height, pCodecCtx->width, CV_8UC3,
    (void *)pFrameRGB->data[0], pFrameRGB->linesize[0]);

struct SwsContext *c = NULL;

// ...

c = sws_getCachedContext(c, pCodecCtx->width, pCodecCtx->height,
    (enum AVPixelFormat)pFrame->format, pCodecCtx->width, pCodecCtx->height,
    AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);

sws_scale(c, (const uint8_t *const *)pFrame->data, pFrame->linesize,
    0, pFrame->height, pFrameRGB->data, pFrameRGB->linesize);

imshow("image", img);

Komprese videa

Pokud bude nutno dekomprimovaný snímek opět zkomprimovat a uložit do kontejneru, lze postupovat následovně. Nejprve je třeba pomocí volání avformat_alloc_output_context2 alokovat novou strukturu AVFormatContext, která bude reprezentovat výstupní kontejner (např. soubor AVI). Požadovaný videokodek se vybere z výčtu enum AVCodecID (např. AV_CODEC_ID_HUFFYUV). Pomocí volání avcodec_find_encoder se z něj vytvoří struktura AVCodec. Voláním avformat_new_stream se v kontejneru (alokovaná struktura AVFormatContext) vytvoří nová stopa komprimovaná vybraným kodekem (vytvořený AVCodec). Tímto krokem vznikne struktura AVStream, která mimo jiné obsahuje strukturu  .codecpar.

Pomocí volání avcodec_parameters_from_context převedeme tuto strukturu na AVCodecContext, která umožňuje nastavovat parametry komprese. Tato struktura se nejprve vyplní pro daný kodek výchozími hodnotami (voláním avcodec_alloc_context3). Poté lze některé parametry kodeku upravit (rozměry videa, formát pixelu). Po tomto kroku je již možné kodek otevřít a inicializovat jeho kontextem (vyplněná AVCodecContext) pomocí funkce avcodec_open2. Správnost popsaných operací lze ověřit výpisem pomocí av_dump_format. Výstup bude podobný následujícímu úseku.

Output #0, avi, to 'output.avi':
  Stream #0:0: Video: huffyuv, rgb24, 321x321, q=2-31, 200 kb/s, 1 tbn

Pokud je vše v pořádku, lze přistoupit ke skutečnému vytvoření kontejneru (otevření souboru AVI). K tomu slouží funkce avio_open, která jako parametr obdrží ukazatel na strukturu .pb struktury AVFormatContext a jméno výstupního souboru. Před uložením snímků je třeba zapsat hlavičku stopy pomocí avformat_write_header. Stejně tak je třeba zavolat po uložení všech potřebných snímků funkci av_write_trailer. Mezi těmito voláními lze ukládat jednotlivé komprimované snímky. K tomu slouží funkce av_write_frame, která jako parametr očekává již připravený paket ( AVPacket).

Ke kompresi snímku ( AVFrame) do paketu slouží funkce avcodec_send_frame a avcodec_receive_packet. Ta komprimuje dříve zvoleným kodekem a proto je předtím nutné snímek převést do formátu, kterému tento kodek rozumí. Pro kodek Huffyuv to bude např. formát AV_PIX_FMT_RGB24. Konverzi provede již v předchozí sekci zmiňovaná funkce  sws_scale.

Videokodek

Tato sekce popisuje napojení jednoduchého kodeku do frameworku FFmpeg. Často se setkáváme se situací, kdy kodek pracuje s obrazem, který je v paměti orientován „vzhůru nohama.“ Pro jeho obrácení bez skutečného kopírování dat může být užitečná následující funkce. Trik spočívá v nastavení ukazatele na obraz na jeho poslední řádek a udání záporné velikosti kroku mezi řádky.

static void flip(AVCodecContext *avctx, AVPicture *picture)
{
    picture->data[0] += picture->linesize[0] * (avctx->height - 1);
    picture->linesize[0] *= -1;
}

Přidání kodeku (zde pouze dekodéru) do frameworku FFmpeg znamená znovupřeložení všech změnou dotčených částí, což budou knihovny libavcodec a libavformat. Do knihovny libavcodec se přidá vlastní dekodér. Do knihovny libavformat se přidá pouze mapování nového FourCC kódu na nově přidaný kodek. V případě systémů unixového typu je k aktualizaci knihoven nainstalovaných v systému třeba administrátorských oprávnění. Nic ale uživateli nebrání v přeložení a instalaci frameworku FFmpeg do jeho domovského adresáře. Správnou funkci dekodéru lze rychle ověřit voláním nástroje ffplay na předpřipravené video.

Do podadresáře libavcodec se nejprve zkopírují zdrojové kódy kodeku, řekněmě rootcodec.h a rootcodec.c (podle Root.cz). Dále se v libavcodec  vytvoří soubor root.c, který bude sloužit jako obálka přidávaného kodeku pro FFmpeg. Tento soubor bude obsahovat pouze jednu datovou strukturu typu AVCodec, která se odkáže na lokální funkce obalující skutečná volání kodeku.

AVCodec ff_root_decoder =
{
    .name          = "root",
    .long_name     = NULL_IF_CONFIG_SMALL("Root.cz video codec"),
    .type          = AVMEDIA_TYPE_VIDEO,
    .id            = AV_CODEC_ID_ROOT,
    .capabilities  = AV_CODEC_CAP_DR1,
    .init          = root_decode_init,
    .decode        = root_decode_frame,
    .close         = root_decode_close,
};

V položce .type je nezbytné dát najevo, že se jedná o kodek videa. Položka .id je klíčová, udává totiž unikátní identifikátor kodeku uvnitř frameworku FFmpeg. Do .capabilities se nastaví relevantní AV_CODEC_CAP_*. Hodnota AV_CODEC_CAP_DR1 umožňuje kodeku použít funkci get_buffer. Do .init, .decode a .close se nastaví ukazatele na lokální funkce, které je třeba dále implementovat.

Ve stejném souboru se vytvoří podle prototypů v deklaraci AVCodec odkazované lokální funkce. Nejpodstatnější z nich je funkce pro dekódování snímku. Ta v podstatě pouze obaluje volání skutečné funkce, řekněmě decompress, přidávaného kodeku. Kodek na rozdíl od frameworku FFmpeg očekává obrácenou orientaci snímku (řádky odspodu nahoru), což je typické pro systém Microsoft Windows.

Tento problém lze obejít jednoduchým trikem, kdy se nastaví ukazatel na snímek až na jeho poslední řádek a udá se k němu záporný krok (stride). Alternativně lze volání decompress obalit voláním výše uvedené funkce flip. V úseku kódu níže je ještě vynechána alokace snímku, který by se měl nakonec z paměti také uvolnit. Funkce vrací počet skutečně použitých bajtů.

static int root_decode_frame(AVCodecContext *avctx,
    void *outdata, int *outdata_size, AVPacket *avpkt)
{
    AVFrame *picture = outdata;
    *outdata_size = sizeof(AVFrame);

    // ...

    return decompress(
        picture->data[0] + picture->linesize[0] * (avctx->height - 1),
        avpkt->data,
        avctx->width,
        avctx->height,
        picture->linesize[0] * -1
    );
}

Identifikátor AV_CODEC_ID_ROOT se přidá do výčtu AVCodecID v libavcodec/avcodec.h. Přitom je třeba přidat jej nakonec (jinak se rozbije binární kompatibilita knihoven) a s hodnotou, která jako řetězec ASCII znaků připomíná název kodeku (pro zamezení konfliktů s jinými nově přidanými kodeky).

AV_CODEC_ID_ROOT       = MKBETAG('R','O','O','T'),

Dále se upraví tabulka mapování FourCC kódů (ve formátu RIFF) na výše zavedené ID kodeku, tj. ff_codec_bmp_tags v libavformat/riff.c. Před konec se přidá následující řádek.

{ AV_CODEC_ID_ROOT,         MKTAG('R', 'O', 'O', 'T') },

Aby se kodek přeložil, je nutné modifikovat Makefile v adresáři libavcodec. Přidá se zde následující závislost.

OBJS-$(CONFIG_ROOT_DECODER)            += root.o rootcodec.o

Nyní je možné FFmpeg zkonfigurovat a přeložit. Jako parametry skriptu configure lze předat požadované volby (pro přehled --help), následně se zavolá  make.

Neutrální ikona do widgetu na odběr článků ze seriálů

Zajímá vás toto téma? Chcete se o něm dozvědět víc?

Objednejte si upozornění na nově vydané články do vašeho mailu. Žádný článek vám tak neuteče.


Autor článku

Autor vystudoval Fakultu informačních technologií VUT v Brně, kde nyní pracuje jako vědecký pracovník. Zajímá se o multimédia a na svých strojích používá výhradně Gentoo Linux.