Dekodowanie wideo z ffmpegiem

 
W tym wpisie przedstawię podstawy użycia bibliotek, na których opiera się słynny program ffmpeg. Nie będę tu prezentował przykładów użycia ffmpega jako aplikacji. Przedstawię za to pewien minimalny kod, umożliwiający wydobycie i zdekodowanie do bitmapy  każdej klatki dowolnego wspieranego przez ffmpega pliku zawierającego wideo.

1. ffmpeg

Pod tą nazwą kryje się przede wszystkim pojedynczy program, obsługiwany z linii poleceń. W Internecie można znaleźć kilka określeń, m.in. „FFmpeg – the swiss army knife of Internet Streaming” albo oficjalne hasełko „A complete, cross-platform solution to record, convert and stream audio and video”. Można śmiało powiedzieć, że powyższe slogany są prawdziwe. Jeśli chodzi o operacje na cyfrowych multimediach, a przede wszystkim procesy dekodowania i kodowania wideo to ffmpeg jest najpotężniejszym z dostępnych narzędzi. Jest również niezwykle kłopotliwy, po pierwsze z powodu dość skomplikowanej składni poleceń, pod drugie z ubogiej dokumentacji. Do tego wymaga dość solidnej wiedzy z zakresu multimediów cyfrowych – form reprezentacji danych wideo i audio, kodowań, formatów, protokołów, a także rozmaitych niuansów z dziedziny przetwarzania audio/wideo.

Sam program ffmpeg, jeśli spojrzeć na rozmiar kodu źródłowego, jest dość kompaktowy. Cały jego ogromny mechanizm został umieszczony w pakiecie bibliotek:

avformat
avcodec
avutil
avdevice
avfilter
swscale
swresample
postproc

avformat obsługuje – jak sama nazwa wskazuje – formaty multimedialne, zwane też kontenerami, czyli na ogół pliki takie jak .avi .mov .mp4 .mkv itd. Implementuje rozmaite skanery i parsery które wyciągają z plików metadane i umożliwiają dostęp do kolejnych zakodowanych klatek/ramek wideo/audio.
avcodec – to obsługa kodeków – zarówno koderów jak i dekoderów wideo i audio, ale także napisów. Baza obsługiwanych przez avcodec kodeków jest ogromna i można śmiało stwierdzić, że avcodec obsługuje dekodowanie wszystkich współczesnych, „przemysłowych” kodowań.
swscale i swresample – pierwsza służy do obsługi skalowania wideo i konwersji między formatami piksela (w tym konwersje przestrzeni barw, np YUV –> RGB i odwrotnie), druga służy do operacji na zdekodowanym audio, głównie do zmian częstości próbkowania i liczby kanałów.

Na podstawie tych bibliotek można dość niewielkim wysiłkiem stworzyć prosty program playera, obsługujący olbrzymią liczbę kodeków i formatów. Jedyne czego by tutaj brakowało, to obsługa GUI. To daje pewne pojęcie o tym, jak potężnym narzędziem jest ffmpeg wraz z jego bibliotekami. W tym wpisie przedstawię pewien minimalny kod, obsługujący dekodowanie wideo z plików multimedialnych do bitmap RGB.

 

2. Kontener multimedialny

Warto przy okazji bardzo ogólnie opisać z czego składa się typowy plik multimedialny, zwany czasem kontenerem lub formatem. Jest to wprawdzie temat na osobny wpis, który być może niebawem powstanie.

 

kontener

Zatem plik multimedialny może zawierać metadane, które opisują jego zawartość, jednak nie musi tak być. Wystarczy przypomnieć sobie pliki mp3, które mogły zawierać tylko dźwięk, ale format dopuszczał załączanie tagów ID3, w których można było zawrzeć tytuł, autora, album i szereg innych informacji charakterystycznych dla utworu muzycznego.
Plik multimedialny może zawierać kilka strumieni. Z reguły mamy do czynienia z jednym audio, jednym wideo. Czasem ścieżek dźwiękowych jest kilka i wówczas najczęściej w metadanych znajduje się wzmianka o języku danej ścieżki dźwiękowej. Mogą się również tutaj znaleźć napisy, które przez ffmpega traktowane są jako oddzielne strumienie.

Jeszcze rzut oka na to co z reguły zawierają metadane kontenera multimedialnego.

metadane

Musimy przy tym pamiętać, że większość formatów (kontenerów) multimedialnych wspiera tylko określone kodeki. Przykładem może być MPEG2 Transport Stream, który może jedynie przenosić strumienie wideo zakodowane w którymś z kodowań MPEG.

W dzisiejszym wpisie skupimy się wyłącznie na pojedynczym strumieniu wideo.

3. Program

Biblioteki ffmpega napisane są w C, nagłówki do nich nie zawierają typowego zabezpieczenia w postaci #ifdef __cplusplus dlatego pracując w C++ każdy #include z API ffmpega musi być zawarty w magicznym bloku extern „C” {} wymuszającym traktowanie zawartych funkcji jako stricte C, inaczej kompilator „zmanglowałby” nazwy funkcji zgodnie ze standardem C++, co kończy się błędem z serii „unidentified symbol„.

extern "C" {
    #include "libswscale/swscale.h"
    #include "libavformat/avformat.h"
    #include "libavcodec/avcodec.h"
}

Potem w main():

    av_register_all();

To wywołanie aktywuje wszystkie muksery, demuksery, parsery, kodery i dekodery, z jakimi zbudowany był nasz build ffmpega.

    std::string filename = "/tmp/big_buck_bunny_1080p_h264.mov";

    AVFormatContext* fmtCtx = NULL;

    int avErr = 0;
    if ((avErr = avformat_open_input(&fmtCtx, filename.c_str(), NULL, NULL)) != 0) {
        printf("Nie udalo sie otworzyc pliku, AVERR %d\n", avErr);
        return 1;
    }

avformat_open_input otwiera plik multimedialny. W przedostatnim parametrze możemy określić dokładnie, za pomocą jakiego demuksera plik ma być otwarty. Tutaj jest to NULL, zatem avformat będzie musiał odgadnąć o jaki format chodzi, co raczej nie sprawi bibliotece problemu, nawet, gdy usuniemy rozszerzenie .mov .
AVFormatContext jest obiektem przechowującym stan parsera i demuksera pliku. Powstaje wskutek wywołania avformat_open_input. Nie jest to jednak tzw nieprzejrzysty wskaźnik (opaque pointer). Jest to rozbudowana struktura, w której zawarte są inne struktury itd. Większość pól kontekstu ma swoje konkretne znaczenie w procesie muksowania (kodowania) i demuksowania (dekodowania). Większość popularnych formatów posiada wydzielone miejsce w pliku, gdzie przechowywane są najistotniejsze metadane, na przykad liczba strumieni, ich konfiguracje, czas trwania i tym podobne. W takich przypadkach kontekst wypełni się wszystkimi potrzebnymi informacjami wskutek wywołania avformat_open_input. W przypadku formatów, w których nie ma oddzielnego „nagłówka” z metadanymi, jak na przykład MPEG2 TS, przydatne może się okazać wywołanie avformat_find_stream_info.

    avErr = avformat_find_stream_info(fmtCtx, NULL);
    if (avErr < 0) {
        printf("Problem z przetwarzaniem pliku, AVERR %d\n", avErr);
        avformat_free_context(fmtCtx);
        return 1;
    }

    printf("Liczba strumieni: %d\n", fmtCtx->nb_streams);
    printf("Czas trwania: %lld us\n", fmtCtx->duration);

Funkcja ta przespaceruje się potreści pakietów kontenera poszukując informacji o liczbie strumieni i ich konfiguracji. Akurat przypadku pliku .mov nie jest to niezbędny krok.
W tym przypadku okazało się, że w pliku są 3 strumienie #0 – wideo, #1 – dane (timecode), #2 – audio. Do badania kontenerów multimedialnych polecam skorzystać z narzędzia ffprobe w pakiecie ffmpega, bardzo ładnie wypisuje wszystkie informacje zawarte w AVFormatContext.
Pora wyodrębnić pierwszy napotkany strumień wideo:

    int vidStream = -1;
    for (unsigned ii = 0; ii < fmtCtx->nb_streams; ++ii) {
        if (fmtCtx->streams[ii]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            vidStream = ii;
            break;
        }
    }
    if (vidStream < 0) {
        printf("Nie udalo sie znalzec strumienia wideo\n");
        return 1;
    } else {
        printf("Index strumienia wideo: %d\n", vidStream);
    }

avformat w trakcie otwierania pliku stara się ustalić z jakim kodekiem mamy do czynienia w każdym ze strumieni i jaka jest konfiguracja kodeka wymagana do odkodowania zawartego w strumieniu medium.

printf("ID kodeka wideo: %d\n", fmtCtx->streams[vidStream]->codec->codec_id);

Wypisze:
ID kodeka wideo: 28
Niewiele to mówi, jest to jedynie enum wewnątrz avcodec.h, jednak możemy za pomocą tego ID wydobyć z avcodec’a obiekt dekodera odpowiadający temu ID.

    AVCodec* deCodec = avcodec_find_decoder(fmtCtx->streams[vidStream]->codec->codec_id);

    if (deCodec == NULL) {
        printf("Nasz ffmpeg (avcodec) nie wspiera danego kodeka\n");
        return 1;
    } else {
        printf("Kodek wideo: %s\n", deCodec->long_name);
    }

AVCodec jest obiektem kodera-dekodera. Jest to w zasadzie struktura tylko do odczytu, która powstaje podczas wołania av_register_all . Na podstawie ID możemy wydobyć dekoder odpowiadający kodekowi naszego strumienia o ile został wkompilowany w nasz build podczas budowania bibliotek. Powyższy kod wypisał:
Kodek wideo: H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
Teraz pora na uruchomienie dekodera. O ile obiekt AVCodec jest swego rodzaju singletonem, zawierającym jedynie wskaźniki do funkcji realizującej dany kodek, obiekt AVCodecContext jest już tym, co przechowuje cały stan kodowania bądź dekodowania.
AVCodec jest jeden na cały proces, natomiast AVCodecContext alokujemy za każdym razem, gdy chcemy coś zakodować/zdekodować. Otwarcie dekodera powoduje związanie kontekstu z kodekiem.
Podczas otwierania pliku avformat stworzył również po jednej strukturze AVCodecContext na każdy strumień. W przypadku audio i wideo struktury te zostały wypełnione konfigurcjami strumieni i kodeków. Mając AVCodecContext i AVCodec możemy już uruchomić dekoder:

avcodec_open2(fmtCtx->streams[vidStream]->codec, deCodec, NULL);

Warto nadmienić, że z biblioteki avcodec można korzystać oddzielnie od avformat. Wówczas twórca aplikacji sam musi zaalokować AVCodecContext i najczęściej wypełnić go konfiguracją umożliwiającą zdekodowanie strumienia.

    int64_t seekTimestamp = 0;
    //int64_t seekTimestamp = 50000;
    //int64_t seekTimestamp = 150000;

    av_seek_frame(fmtCtx, vidStream, seekTimestamp, AVSEEK_FLAG_BACKWARD);

Ten fragment kodu w zasadzie nie robi nic. Gdyby seekTimestamp miał wartość taką jak w zakomentowanych liniach to nastąpiłoby „przewinięcie” strumienia do danego czasu. W tym wypadku podstawą czasu strumienia wideo jest milisekunda.
av_seek_frame powoduje skok demuksera do czasu podanego w argumencie (seekTimestamp). Jednak w większości współczesnych kodeków wideo poszczególne klatki strumienia zą od siebie zależne i większości klatek nie można zdekodować zanim nie zostaną zdekodowane poprzednie. Dlatego avformat w takich przypadkach skacze do najbliższej klatki kluczowej, która nie zależy od innych klatek i może być zdekodowana natychmiast. Flaga AVSEEK_FLAG_BACKWARD oznacza tyle, że seek zaprowadzi nas do najbliższej klatki kluczowej, której timestamp jest niewiększy od podanego do funkcji. Bez tej flagi byłby to timestamp niemniejszy od podanego. Teraz czas na demuksowanie, czyli wyciąganie zakodowanych klatek z kontenera.

    AVPacket packet;
    av_init_packet(&packet);
    while (av_read_frame(fmtCtx, &packet) == 0)
    {
        if (packet.stream_index == vidStream)
            break;
        av_free_packet(&packet);
    }

av_read_frame czyta z kontenera kolejne pakiety audio/wideo (a także pozostałych typów strumieni). Prosty filtr eliminuje te, które nie należą do pierwszego strumienia wideo.
av_init_packet inicjalizuje pola AVPacket do wartości zerowych, w końcu packet został zaalokowany na stosie i jego poszczególne pola są niezainicjalizowane.

    AVFrame* frm = av_frame_alloc();
    int got = 0;
    if (avcodec_decode_video2(fmtCtx->streams[vidStream]->codec, frm, &got, &packet) < 0) {
        printf("Błąd dekodowania\n");
        return 1;
    }
    av_free_packet(&packet);

AVFrame reprezentuje tutaj klatkę zdekodowaną, do frm dekoder zapisze obraz powstały ze zdekodowania klatki packet. Za pomocą zmiennej got sygnalizowane jest, że w tym wywołaniu do frm avcodec zapisał zdekodowaną ramkę. W przypadku niektórych kodeków (np audio) aby powstała ramka wyjściowa, na wejście musi zostać podanych kilka ramek wejściowych. Poza tym w przypadku niektórych dekoderów ramka wyjściowa pojawia się dopiero po włożeniu dwóch ramek na wejściu (codec delay). Po dotarciu do końca pliku w dekoderze mogą znajdować się zbuforowane ramki, wówczas „wypłukuje” się je (flush) za pomocą pustych AVPacket (packet.data = NULL).
Ponieważ z większości dekoderów wideo otrzymamy bitmapy w przestrzeni barw YUV i dodatkowo podpróbkowane (np YUV 4:2:0), konieczna będzie konwersja do RGB. Tutaj sięgamy po swscale.

    SwsContext* scalingCtx = sws_getContext(frm->width, frm->height, fmtCtx->streams[vidStream]->codec->pix_fmt, frm->width, frm->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

    if (scalingCtx == NULL) {
        printf("Błąd tworzenia skalera\n");
        return 1;
    }

    unsigned char* rgb24 = new unsigned char[frm->width * frm->height * 3];
    int rgbLinesize = frm->width * 3;

    sws_scale(scalingCtx, frm->data, frm->linesize, 0, frm->height, &rgb24, &rgbLinesize);

Pole pix_fmt w AVCodecContext mówi jaki jest format piksela, czyli z jaką przestrzenią barw mamy do czynienia i ile bitów przypada na  każdą składową piksela. Jak widać, nie przeprowadzamy tutaj skalowania, a jedynie konwersję przestrzeni barw. Po tej operacji, w buforze rgb24 znajdzie się zdekodowana bitmapa w formacie RGB i głębi 8 bitów na kolor.

A oto wynik działania dla filmu Big Buck Bunny i timestampów:

int64_t seekTimestamp = 0;

frame

int64_t seekTimestamp = 50000;

frame

int64_t seekTimestamp = 150000;

frame

 

Poniżej pełen kod programu:

(kliknij aby rozwinąć)

#include <cstdio>
#include <string>

extern "C" {
    #include "libswscale/swscale.h"
    #include "libavformat/avformat.h"
    #include "libavcodec/avcodec.h"
}

#define CALC_PADDING(width) (((width) * 3) % 4 ? 4 - (((width) * 3) % 4) : 0)

int saveBmp(std::string filename, unsigned char* rgbData, unsigned width, unsigned height)
{

    FILE* fp = fopen(filename.c_str(), "wb");
    if (fp == NULL)
        return -1;

    const unsigned paddingBytes = CALC_PADDING(width);
    const unsigned rowsize =  paddingBytes + width * 3;
    const unsigned pixArraySize = height * rowsize;
    static const unsigned fileHeaderSize = 14;
    static const unsigned bmpInfoHeaderSize = 40;
    static const char bmTag[] = "BM";

    const uint32_t fileSize = fileHeaderSize + bmpInfoHeaderSize + pixArraySize;
    const uint32_t pixArrayOffset = fileHeaderSize + bmpInfoHeaderSize;

    // -- Write file header --

    // Put the "BM" start tag
    fputs(bmTag, fp);
    // Put file size
    fputc(fileSize & 0xff, fp);
    fputc((fileSize >> 8) & 0xff, fp);
    fputc((fileSize >> 16) & 0xff, fp);
    fputc((fileSize >> 24) & 0xff, fp);
    // Put 4 reserved zero bytes
    for (unsigned ii = 0; ii < 4; ++ii)
        fputc(0, fp);
    // Put offset to pixel array
    fputc(pixArrayOffset & 0xff, fp);
    fputc((pixArrayOffset >> 8) & 0xff, fp);
    fputc((pixArrayOffset >> 16) & 0xff, fp);
    fputc((pixArrayOffset >> 24) & 0xff, fp);

    // -- Write bitmap info header --

    // Header size
    fputc(40, fp);
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);
    // Picture width
    fputc(width & 0xff, fp);
    fputc((width >> 8) & 0xff, fp);
    fputc((width >> 16) & 0xff, fp);
    fputc((width >> 24) & 0xff, fp);
    // Picture height
    fputc(height & 0xff, fp);
    fputc((height >> 8) & 0xff, fp);
    fputc((height >> 16) & 0xff, fp);
    fputc((height >> 24) & 0xff, fp);
    // Color planes ( = 1)
    fputc(1, fp);
    fputc(0, fp);
    // Bits per pixel ( = 24)
    fputc(24, fp);
    fputc(0, fp);
    // Compression method (none - all zeros)
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);
    // pix array size
    fputc(pixArraySize & 0xff, fp);
    fputc((pixArraySize >> 8) & 0xff, fp);
    fputc((pixArraySize >> 16) & 0xff, fp);
    fputc((pixArraySize >> 24) & 0xff, fp);
    // pixel per meter - horizontal (fudge!)
    fputc(0x13, fp);
    fputc(0x0b, fp);
    fputc(0, fp);
    fputc(0, fp);
    // pixel per meter - vertical (fudge!)
    fputc(0x13, fp);
    fputc(0x0b, fp);
    fputc(0, fp);
    fputc(0, fp);
    // palette size ( = 0 )
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);
    // Important colors ( = 0)
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);
    fputc(0, fp);

    // -- the pixel array --


    for (int yy = height - 1; yy >= 0; --yy)
    {
        unsigned char* ptr = rgbData + yy * width * 3;
        for (unsigned xx = 0; xx < width; ++xx) {
            fputc(*(ptr + 2), fp); // B
            fputc(*(ptr + 1), fp); // G
            fputc(*ptr, fp); // R
            ptr += 3;
        }
        for (unsigned ii = 0; ii < paddingBytes; ++ii)
            fputc(0, fp);
    }

    fclose(fp);
    return 0;
}

using namespace std;

int main()
{
    av_register_all();

    std::string filename = "/tmp/big_buck_bunny_1080p_h264.mov";

    AVFormatContext* fmtCtx = NULL;

    int avErr = 0;
    if ((avErr = avformat_open_input(&fmtCtx, filename.c_str(), NULL, NULL)) != 0) {
        printf("Nie udalo sie otworzyc pliku, AVERR %d\n", avErr);
        return 1;
    }

    // Find info about streams and their codecs
    avErr = avformat_find_stream_info(fmtCtx, NULL);
    if (avErr < 0) {
        printf("Problem z przetwarzaniem pliku, AVERR %d\n", avErr);
        avformat_free_context(fmtCtx);
        return 1;
    }

    printf("Liczba strumieni: %d\n", fmtCtx->nb_streams);
    printf("Czas trwania: %lld us\n", fmtCtx->duration);

    int vidStream = -1;
    for (unsigned ii = 0; ii < fmtCtx->nb_streams; ++ii) {
        if (fmtCtx->streams[ii]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            vidStream = ii;
            break;
        }
    }
    if (vidStream < 0) {
        printf("Nie udalo sie znalzec strumienia wideo\n");
        return 1;
    } else {
        printf("Index strumienia wideo: %d\n", vidStream);
    }

    printf("ID kodeka wideo: %d\n", fmtCtx->streams[vidStream]->codec->codec_id);

    AVCodec* deCodec = avcodec_find_decoder(fmtCtx->streams[vidStream]->codec->codec_id);

    if (deCodec == NULL) {
        printf("Nasz ffmpeg (avcodec) nie wspiera danego kodeka\n");
        return 1;
    } else {
        printf("Kodek wideo: %s\n", deCodec->long_name);
    }

    avcodec_open2(fmtCtx->streams[vidStream]->codec, deCodec, NULL);

    int64_t seekTimestamp = 0;
    //int64_t seekTimestamp = 50000;
    //int64_t seekTimestamp = 150000;

    av_seek_frame(fmtCtx, vidStream, seekTimestamp, AVSEEK_FLAG_BACKWARD);

    AVFrame* frm = av_frame_alloc();
    int got = 0;

    AVPacket packet;
    av_init_packet(&packet);

    while (!got) {

        while (av_read_frame(fmtCtx, &packet) == 0)
        {
            if (packet.stream_index == vidStream)
                break;
            av_free_packet(&packet);
        }

        if (packet.data == NULL) {
            printf("Nie udalo sie odczytac wideo z kontenera");
            return 1;
        }

        if (avcodec_decode_video2(fmtCtx->streams[vidStream]->codec, frm, &got, &packet) < 0) {
            printf("Błąd dekodowania\n");
            return 1;
        }
        av_free_packet(&packet);
    }

    SwsContext* scalingCtx = sws_getContext(frm->width, frm->height, fmtCtx->streams[vidStream]->codec->pix_fmt, frm->width, frm->height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);

    if (scalingCtx == NULL) {
        printf("Błąd tworzenia skalera\n");
        return 1;
    }

    unsigned char* rgb24 = new unsigned char[frm->width * frm->height * 3];
    int rgbLinesize = frm->width * 3;

    sws_scale(scalingCtx, frm->data, frm->linesize, 0, frm->height, &rgb24, &rgbLinesize);

    saveBmp("frame.bmp", rgb24, frm->width, frm->height);

    return 0;
}

 

2 komentarze do “Dekodowanie wideo z ffmpegiem

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *