簡體   English   中英

Libav(ffmpeg)將解碼的視頻時間戳復制到編碼器

[英]Libav (ffmpeg) copying decoded video timestamps to encoder

我正在編寫一個應用程序,它從輸入文件(任何編解碼器,任何容器)解碼單個視頻流,進行一堆圖像處理,並將結果編碼為輸出文件(單個視頻流,Quicktime RLE,MOV)。 我正在使用ffmpeg的libav 3.1.5(目前是Windows版本,但應用程序將是跨平台的)。

輸入幀和輸出幀之間存在1:1的對應關系,我希望輸出中的幀時序與輸入相同。 真的很難完成這件事。 所以我的一般問題是: 我如何可靠地(在所有輸入情況下)將輸出幀時序設置為與輸入相同?

我花了很長時間來瀏覽API並且達到了我現在的目的。 我整理了一個最小的測試程序來處理:

#include <cstdio>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

using namespace std;


struct DecoderStuff {
    AVFormatContext *formatx;
    int nstream;
    AVCodec *codec;
    AVStream *stream;
    AVCodecContext *codecx;
    AVFrame *rawframe;
    AVFrame *rgbframe;
    SwsContext *swsx;
};


struct EncoderStuff {
    AVFormatContext *formatx;
    AVCodec *codec;
    AVStream *stream;
    AVCodecContext *codecx;
};


template <typename T>
static void dump_timebase (const char *what, const T *o) {
    if (o)
        printf("%s timebase: %d/%d\n", what, o->time_base.num, o->time_base.den);
    else
        printf("%s timebase: null object\n", what);
}


// reads next frame into d.rawframe and d.rgbframe. returns false on error/eof.
static bool read_frame (DecoderStuff &d) {

    AVPacket packet;
    int err = 0, haveframe = 0;

    // read
    while (!haveframe && err >= 0 && ((err = av_read_frame(d.formatx, &packet)) >= 0)) {
       if (packet.stream_index == d.nstream) {
           err = avcodec_decode_video2(d.codecx, d.rawframe, &haveframe, &packet);
       }
       av_packet_unref(&packet);
    }

    // error output
    if (!haveframe && err != AVERROR_EOF) {
        char buf[500];
        av_strerror(err, buf, sizeof(buf) - 1);
        buf[499] = 0;
        printf("read_frame: %s\n", buf);
    }

    // convert to rgb
    if (haveframe) {
        sws_scale(d.swsx, d.rawframe->data, d.rawframe->linesize, 0, d.rawframe->height,
                  d.rgbframe->data, d.rgbframe->linesize);
    }

    return haveframe;

}


// writes an output frame, returns false on error.
static bool write_frame (EncoderStuff &e, AVFrame *inframe) {

    // see note in so post about outframe here
    AVFrame *outframe = av_frame_alloc();
    outframe->format = inframe->format;
    outframe->width = inframe->width;
    outframe->height = inframe->height;
    av_image_alloc(outframe->data, outframe->linesize, outframe->width, outframe->height,
                   AV_PIX_FMT_RGB24, 1);
    //av_frame_copy(outframe, inframe);
    static int count = 0;
    for (int n = 0; n < outframe->width * outframe->height; ++ n) {
        outframe->data[0][n*3+0] = ((n+count) % 100) ? 0 : 255;
        outframe->data[0][n*3+1] = ((n+count) % 100) ? 0 : 255;
        outframe->data[0][n*3+2] = ((n+count) % 100) ? 0 : 255;
    }
    ++ count;

    AVPacket packet;
    av_init_packet(&packet);
    packet.size = 0;
    packet.data = NULL;

    int err, havepacket = 0;
    if ((err = avcodec_encode_video2(e.codecx, &packet, outframe, &havepacket)) >= 0 && havepacket) {
        packet.stream_index = e.stream->index;
        err = av_interleaved_write_frame(e.formatx, &packet);
    }

    if (err < 0) {
        char buf[500];
        av_strerror(err, buf, sizeof(buf) - 1);
        buf[499] = 0;
        printf("write_frame: %s\n", buf);
    }

    av_packet_unref(&packet);
    av_freep(&outframe->data[0]);
    av_frame_free(&outframe);

    return err >= 0;

}


int main (int argc, char *argv[]) {

    const char *infile = "wildlife.wmv";
    const char *outfile = "test.mov";
    DecoderStuff d = {};
    EncoderStuff e = {};

    av_register_all();

    // decoder
    avformat_open_input(&d.formatx, infile, NULL, NULL);
    avformat_find_stream_info(d.formatx, NULL);
    d.nstream = av_find_best_stream(d.formatx, AVMEDIA_TYPE_VIDEO, -1, -1, &d.codec, 0);
    d.stream = d.formatx->streams[d.nstream];
    d.codecx = avcodec_alloc_context3(d.codec);
    avcodec_parameters_to_context(d.codecx, d.stream->codecpar);
    avcodec_open2(d.codecx, NULL, NULL);
    d.rawframe = av_frame_alloc();
    d.rgbframe = av_frame_alloc();
    d.rgbframe->format = AV_PIX_FMT_RGB24;
    d.rgbframe->width = d.codecx->width;
    d.rgbframe->height = d.codecx->height;
    av_frame_get_buffer(d.rgbframe, 1);
    d.swsx = sws_getContext(d.codecx->width, d.codecx->height, d.codecx->pix_fmt,
                            d.codecx->width, d.codecx->height, AV_PIX_FMT_RGB24,
                            SWS_POINT, NULL, NULL, NULL);
    //av_dump_format(d.formatx, 0, infile, 0);
    dump_timebase("in stream", d.stream);
    dump_timebase("in stream:codec", d.stream->codec); // note: deprecated
    dump_timebase("in codec", d.codecx);

    // encoder
    avformat_alloc_output_context2(&e.formatx, NULL, NULL, outfile);
    e.codec = avcodec_find_encoder(AV_CODEC_ID_QTRLE);
    e.stream = avformat_new_stream(e.formatx, e.codec);
    e.codecx = avcodec_alloc_context3(e.codec);
    e.codecx->bit_rate = 4000000; // arbitrary for qtrle
    e.codecx->width = d.codecx->width;
    e.codecx->height = d.codecx->height;
    e.codecx->gop_size = 30; // 99% sure this is arbitrary for qtrle
    e.codecx->pix_fmt = AV_PIX_FMT_RGB24;
    e.codecx->time_base = d.stream->time_base; // ???
    e.codecx->flags |= (e.formatx->flags & AVFMT_GLOBALHEADER) ? AV_CODEC_FLAG_GLOBAL_HEADER : 0;
    avcodec_open2(e.codecx, NULL, NULL);
    avcodec_parameters_from_context(e.stream->codecpar, e.codecx); 
    //av_dump_format(e.formatx, 0, outfile, 1);
    dump_timebase("out stream", e.stream);
    dump_timebase("out stream:codec", e.stream->codec); // note: deprecated
    dump_timebase("out codec", e.codecx);

    // open file and write header
    avio_open(&e.formatx->pb, outfile, AVIO_FLAG_WRITE); 
    avformat_write_header(e.formatx, NULL);

    // frames
    while (read_frame(d) && write_frame(e, d.rgbframe))
        ;

    // write trailer and close file
    av_write_trailer(e.formatx);
    avio_closep(&e.formatx->pb); 

}

關於這一點的幾點說明:

  • 由於到目前為止我所有的幀定時嘗試都失敗了,所以我從這段代碼中刪除了幾乎所有與時序相關的東西,從一個干凈的平板開始。
  • 為簡潔起見,幾乎省略了所有錯誤檢查和清理。
  • 我分配一個新的輸出幀與一個新的緩沖區的原因write_frame ,而不是使用inframe直接,是因為這是比較有代表性的就是我真正的應用程序在做。 我的真實應用程序也在內部使用RGB24,因此這里的轉換。
  • 我在outframe生成奇怪模式的outframe ,而不是使用例如av_copy_frame ,是因為我只想要一個使用Quicktime RLE壓縮得很好的測試模式(否則我的測試輸入最終會產生1.7GB的輸出文件)。
  • 我正在使用的輸入視頻“wildlife.wmv”可以在這里找到。 我已對文件名進行了硬編碼。
  • 我知道avcodec_decode_video2avcodec_encode_video2已被棄用,但不在乎。 它們工作得很好,我已經很難解決最新版本的API問題,ffmpeg幾乎每個版本都會更改它們的API,而我現在真的不想處理avcodec_send_*avcodec_receive_*
  • 我想我應該通過將一個NULL幀傳遞給avcodec_encode_video2來刷新一些緩沖區或其他東西,但我對此有點困惑。 除非有人想解釋一下,讓我們現在忽略它,這是一個單獨的問題。 關於這一點,文檔是模糊的,因為它們是關於其他一切的。
  • 我的測試輸入文件的幀速率是29.97。

現在,至於我目前的嘗試。 以上時序相關字段存在於上述代碼中,詳細信息/混淆為粗體。 其中有很多,因為API令人難以置信的錯綜復雜:

  • main: d.stream->time_base :輸入視頻流時基。 對於我的測試輸入文件,這是1/1000。
  • main: d.stream->codec->time_base :不確定這是什么(當你總是使用自己的新上下文時,我永遠無法理解為什么AVStream有一個AVCodecContext字段)而且不推薦使用codec字段。 對於我的測試輸入文件,這是1/1000。
  • main: d.codecx->time_base :輸入編解碼器上下文時基。 對於我的測試輸入文件,這是0/1。 我應該設置它嗎?
  • main: e.stream->time_base :我創建的輸出流的時基。 我該怎么做呢?
  • main: e.stream->codec->time_base :我創建的輸出流的已棄用且神秘的編解碼器字段的時基。 我把它設置成什么?
  • main: e.codecx->time_base :我創建的編碼器上下文的時基。 我該怎么做呢?
  • read_frame: packet.dts :解碼數據包讀取的時間戳。
  • read_frame: packet.pts :數據包讀取的表示時間戳。
  • read_frame: packet.duration :數據包讀取的持續時間。
  • read_frame: d.rawframe->pts :解碼的原始幀的表示時間戳。 這總是0.解碼器為什么不讀取它?
  • read_frame: d.rgbframe->pts / write_frame: inframe->pts :轉換為RGB的解碼幀的顯示時間戳。 目前沒有設置任何內容。
  • read_frame: d.rawframe->pkt_* :從包中復制的字段,在閱讀本文后發現。 它們設置正確但我不知道它們是否有用。
  • write_frame: outframe->pts :正在編碼的幀的表示時間戳。 我應該把它設置成什么?
  • write_frame: outframe->pkt_* :來自數據包的定時字段。 我應該設置這些嗎? 它們似乎被編碼器忽略了。
  • write_frame: packet.dts :解碼正在編碼的數據包的時間戳。 我該怎么做呢?
  • write_frame: packet.pts :正在編碼的數據包的表示時間戳。 我該怎么做呢?
  • write_frame: packet.duration :正在編碼的數據包的持續時間。 我該怎么做呢?

我已經嘗試了以下結果。 需要注意的是inframed.rgbframe

    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.codecx->time_base
    • read_frame設置d.rgbframe->pts = packet.dts read_frame
    • 設置outframe->pts = inframe->ptswrite_frame
    • 結果:警告未設置編碼器時基(因為d.codecx->time_base was 0/1 ),seg fault。
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.stream->time_base
    • read_frame設置d.rgbframe->pts = packet.dts read_frame
    • 設置outframe->pts = inframe->ptswrite_frame
    • 結果:沒有警告,但VLC報告幀速率為480.048(不知道這個數字來自何處)和文件播放速度太快。 此外,編碼器將packet所有時序字段設置為0,這不是我所期望的。 (編輯:原來這是因為av_interleaved_write_frameav_write_frame不同,取得了數據包的所有權並將其與空白交換,我該調用之后打印了這些值。所以它們不會被忽略。)
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.stream->time_base
    • read_frame設置d.rgbframe->pts = packet.dts read_frame
    • write_frame中的packet中的任何pts / dts / duration write_frame為任何值。
    • 結果:未設置有關數據包時間戳的警告。 編碼器似乎將所有數據包時序字段重置為0,因此這些都沒有任何影響。
    • Init e.stream->time_base = d.stream->time_base
    • Init e.codecx->time_base = d.stream->time_base
    • 我在閱讀這篇文章后AVFrame找到了這些字段, pkt_ptspkt_dtspkt_duration ,所以我嘗試將這些字段pkt_pts復制到outframe
    • 結果:真的有了我的希望,但結果與嘗試3相同(數據包時間戳沒有設置警告,結果不正確)。

我嘗試了上面的各種其他手動波動的排列,沒有任何效果。 想要做的是創建一個輸出文件,以與輸入相同的時間和幀速率播放(在這種情況下為29.97恆定幀速率)。

那我該怎么做? 在這里有數以萬計的時序相關字段,我該怎么做才能使輸出與輸入相同? 我如何處理任意視頻輸入格式,可以在不同的地方存儲時間戳和時基? 我需要這個才能一直工作。


作為參考,這里是從我的測試輸入文件的視頻流中讀取的所有數據包和幀時間戳的表,以便了解我的測試文件的外觀。 沒有輸入數據包pts'被設置,與幀pts相同,並且由於某種原因,前108幀的持續時間為0. VLC正常播放文件並報告幀速率為29.9700089:

我認為你的問題是時間基礎,起初有點令人困惑。

  • d.stream->time_base: Input video stream time base 這是輸入容器中時間戳的分辨率。 av_read_frame返回的編碼幀將在此分辨率中具有其時間戳。
  • d.stream->codec->time_base: Not sure what this is 這是舊的API,用於API兼容性; 你正在使用編解碼器參數,所以忽略它。
  • d.codecx->time_base: Input codec context time-base. For my test input file this is 0/1. Am I supposed to set it? 這是編解碼器的時間戳的分辨率(與容器相對)。 編解碼器將假設其輸入編碼幀在此分辨率中具有其時間戳,並且還將在此分辨率中設置輸出解碼幀中的時間戳。
  • e.stream->time_base: Time base of the output stream I create 與解碼器相同
  • e.stream->codec->time_base 與demuxer相同 - 忽略這個。
  • e.codecx->time_base - 與demuxer相同

所以你需要做以下事情:

  • 打開demuxer。 那部分有效
  • 將解碼器時基設置為一些“理智”值,因為解碼器可能不會這樣做,而0/1是壞的 如果沒有設置任何組件的任何時基,事情將無法正常工作。 最簡單的方法是從分離器中復制時基
  • 打開解碼器。 它可能會改變它的時基,也可能不會。
  • 設置編碼器時基。 最簡單的是從(現在打開)解碼器復制時基,因為你沒有改變幀速率或任何東西。
  • 打開編碼器。 它可能會改變它的時基
  • 設置muxer時基。 同樣,最簡單的方法是從編碼器復制時基
  • 打開muxer。 它也可能會改變它的時基。

現在每幀:

  • 從分離器中讀取它
  • 將時間戳從解復用器轉換為解碼器時基。 av_packet_rescale_ts可以幫助您實現這一目標
  • 解碼數據包
  • 將幀時間戳( pts )設置為av_frame_get_best_effort_timestamp返回的值
  • 將幀時間戳從解碼器轉換為編碼器時基。 使用av_rescale_qav_rescale_q_rnd
  • 編碼包
  • 將時間戳從編碼器轉換為多路復用器時基。 再次使用av_packet_rescale_ts

這可能是一種矯枉過正,特別是編碼器可能不會在打開時更改其時基(在這種情況下,您不需要轉換原始幀的pts )。


關於刷新 - 傳遞給編碼器的幀不一定要編碼並立即輸出,所以是的,您應該調用avcodec_encode_video2其中NULL為幀,讓編碼器知道您已完成並輸出所有剩余數據(您需要的)像所有其他數據包一樣通過muxer)。 事實上,你應該反復這樣做,直到它停止噴出數據包。 有關一些示例,請參閱ffmpeg中doc/examples文件夾中的編碼示例之一。

所以,非常感謝Andrey Turkin非常清楚和有用的答案 ,我已經正常工作了,我想分享我所做的確切事情:

在初始化期間,要了解libav在某些時候可能會更改這些初始時基:

  • 在分配編解碼器上下文之后立即將解碼器編解碼器上下文時基初始化為合理的東西。 我去了亞毫秒分辨率:

     d.codecx->time_base = { 1, 10000 }; 
  • 在創建新流后立即初始化編碼器流時基(注意:在QtRLE情況下,如果我離開此{0,0},編寫標題后編碼器將設置為{0,90000},但是不知道其他情況是否合情合理,所以我在這里初始化)。 此時從輸入流中復制是安全的,雖然我注意到我也可以任意初始化它(例如{1,10000}),它仍然可以在以后工作:

     e.stream->time_base = d.stream->time_base; 
  • 分配后立即初始化編碼器編解碼器上下文時基。 與從解碼器復制時的流時基相同:

     e.codecx->time_base = d.codecx->time_base; 

我缺少的一件事是我可以設置這些時間戳,而libav將遵守。 沒有約束,這取決於我,無論我設置什么,解碼的時間戳都將在我選擇的時基中。 我沒有意識到這一點。

然后在解碼時:

  • 我所要做的就是手動填寫解碼幀pts。 pkt_*字段是可忽略的:

     d.rawframe->pts = av_frame_get_best_effort_timestamp(d.rawframe); 
  • 由於我正在轉換格式,我還將其復制到轉換后的幀:

     d.rgbframe->pts = d.rawframe->pts; 

然后,編碼:

  • 只需要設置框架的pts。 Libav將處理數據包。 所以就在編碼幀之前:

     outframe->pts = inframe->pts; 
  • 但是,我仍然需要手動轉換數據包時間戳,這看起來很奇怪,但所有這一切都很奇怪,所以我想這對於課程而言是相同的。 幀時間戳仍然在解碼器流時基中,因此在編碼幀之后但在寫入數據包之前:

     av_packet_rescale_ts(&packet, d.stream->time_base, e.stream->time_base); 

它的作用就像一個魅力,主要是:我注意到VLC報告輸入為29.97 FPS但輸出為30.03 FPS,我無法弄清楚。 但是,在我測試過的所有媒體播放器中,一切似乎都很好。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM