ffplay.c 源碼分析- 視頻部分

FFmpeg 代碼 version 3.3:

ffplay中的線程模型

ffplay線程模型-視頻爲例.png

概述

ffplay.c 中線程模型簡單命令。主要是有如下幾個線程: 1. 渲染的線程-主線程 簡單的理解,來說就是main方法運行所在的線程。 實際上是SDL_CreateWindow 調用所在的線程。以Android爲例(筆者比較熟悉),創建的是OpenGLSurface。也就是EGLContext所在的線程了。

在EventLoop中,將會負責下面兩個功能

  • 負責將解碼後的數據送顯 從解碼後的隊列中,取得數據。經過同步時間鐘的同步,睡眠後(需要同步的話),然後通過SDL_UpdateTexture/SDL_RenderCopy/SDL_RenderPresent ,更新紋理數據,送顯。
  • 對按鍵進行相應 還會按鈕事件的相應。

2. 讀取線程-read_thread 在main方法中會啓動的讀取的線程。

  • 循環讀取 這個線程中,會進行讀取的循環。不斷的通過av_read_frame方法,讀取解碼前的數據packet。
  • 送入隊列 最後將得到的數據,送入對應的流的packet隊列(視頻/音頻/字幕都對應視頻流自己的隊列) 3. 對應流的解碼線程-decode thread 在讀取線程中,對AVFormatContext進行初始化,獲取AVStream信息後,對應不同的碼流會開啓對應的解碼線程Decode Thread。 ffplay中這裏包括了3種流。視頻流。音頻流和字幕流。
以視頻流爲例子。
  • 循環讀取 會從對應流的packet隊列中,得到數據。 然後送入解碼器通過avcodec_decode_video2(舊的API)進行解碼。
  • 送入隊列 解碼之後,得到解碼前的數據AVFrame,並確定對應的pts。 最後然後其再次送入隊列當中。

整體的流程就是這樣簡單。


ffplay初始化(main_thread)

1. 對FFmpeg的初始化

調用av_register_allavformat_network_init。 如果有AVDeviceAVFilter也會對其進行初始化。

    /* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
    avdevice_register_all();
#endif
#if CONFIG_AVFILTER
    avfilter_register_all();
#endif
    av_register_all();
    avformat_network_init();
2. 對傳遞的參數進行初始化

parse_options(NULL, argc, argv, options, opt_input_file); 這個方法就是去解析我們傳入的參數的。具體的方法寫在cmdutils.h 中。 ffplay支持的參數類型,可以在定義的地方看到。

static const OptionDef options[]  //這個數組中,有許多選項,不是重點,暫時不做詳細的介紹了。
3. SDL的初始化

SDL會先初始化SDL_init 進行初始化。

flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
    if (audio_disable)
        flags &= ~SDL_INIT_AUDIO;
    else {
        /* Try to work around an occasional ALSA buffer underflow issue when the
         * period size is NPOT due to ALSA resampling by forcing the buffer size. */
        if (!SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"))
            SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE","1", 1);
    }
    if (display_disable)
        flags &= ~SDL_INIT_VIDEO;
    if (SDL_Init (flags)) {
        av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError());
        av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
        exit(1);
    }

後續的創建SDL_Window/SDL_Renderer/SDL_texture 的部分,會在後續初始化。

4. 通過stream_open函數,開啓read_thread讀取線程
 is = stream_open(input_filename, file_iformat);

通過 stream_open函數,會對VideoState進行初始化,包括解碼數據隊列(PacketQueue)和解碼後數據隊列(FrameQueue)和時間鍾(Clock)等的初始化,並且開啓read_thread

  • VideoState 結構體 這個結構體相當於一個全局的變量都囊括在一起了。 從源碼中可以看到。 包括幾個方面的參數。
  1. 視頻操作類的暫停,運行和seek
  2. 三種同步的時間鍾
  3. 三種碼流對應的FrameQueue/Decoder/ AVStream/PacketQueue,以及對應碼流各自的信息。
  4. 用來Render的一些變量。如SDL_Texture,窗口的位置參數等
  5. read_threadSDL_cond *continue_read_thread條件鎖
  6. 還有些中間變量
// 視頻狀態結構
typedef struct VideoState {
    
    SDL_Thread *read_tid;                   // 讀取線程
    AVInputFormat *iformat;             // 輸入格式
    int abort_request;                          // 請求取消
    int force_refresh;                          // 強制刷新
    int paused;                                 // 停止
    int last_paused;                                // 最後停止
    int queue_attachments_req;          // 隊列附件請求
    int seek_req;                                   // 查找請求
    int seek_flags;                             // 查找標誌
    int64_t seek_pos;                           // 查找位置
    int64_t seek_rel;                           // 
    int read_pause_return;                  // 讀停止返回
    AVFormatContext *ic;                    // 解碼格式上下文
    int realtime;                                   // 是否實時碼流

    Clock audclk;                               // 音頻時鐘
    Clock vidclk;                                   // 視頻時鐘
    Clock extclk;                                   // 外部時鐘

    FrameQueue pictq;                       // 視頻隊列
    FrameQueue subpq;                       // 字幕隊列
    FrameQueue sampq;                       // 音頻隊列

    Decoder auddec;                         // 音頻解碼器
    Decoder viddec;                         // 視頻解碼器
    Decoder subdec;                         // 字幕解碼器

    int audio_stream;                           // 音頻碼流Id

    int av_sync_type;                           // 同步類型

    double audio_clock;                     // 音頻時鐘
    int audio_clock_serial;                 // 音頻時鐘序列
    double audio_diff_cum;                  // 用於音頻差分計算 /* used for AV difference average computation */
    double audio_diff_avg_coef;         //  
    double audio_diff_threshold;            // 音頻差分閾值
    int audio_diff_avg_count;               // 平均差分數量
    AVStream *audio_st;                     // 音頻碼流
    PacketQueue audioq;                 // 音頻包隊列
    int audio_hw_buf_size;                  // 硬件緩衝大小
    uint8_t *audio_buf;                     // 音頻緩衝區
    uint8_t *audio_buf1;                        // 音頻緩衝區1
    unsigned int audio_buf_size;            // 音頻緩衝大小 /* in bytes */
    unsigned int audio_buf1_size;       // 音頻緩衝大小1
    int audio_buf_index;                        // 音頻緩衝索引 /* in bytes */
    int audio_write_buf_size;               // 音頻寫入緩衝大小
    int audio_volume;                           // 音量
    int muted;                                      // 是否靜音
    struct AudioParams audio_src;       // 音頻參數
#if CONFIG_AVFILTER                         
    struct AudioParams audio_filter_src; // 音頻過濾器
#endif
    struct AudioParams audio_tgt;       // 音頻參數
    struct SwrContext *swr_ctx;         // 音頻轉碼上下文
    int frame_drops_early;                  // 
    int frame_drops_late;                       // 

    enum ShowMode {                     // 顯示類型
        SHOW_MODE_NONE = -1,        // 無顯示
        SHOW_MODE_VIDEO = 0,            // 顯示視頻
        SHOW_MODE_WAVES,                // 顯示波浪,音頻
        SHOW_MODE_RDFT,                 // 自適應濾波器
        SHOW_MODE_NB                        // 
    } show_mode;
    int16_t sample_array[SAMPLE_ARRAY_SIZE]; // 採樣數組
    int sample_array_index;                 // 採樣索引
    int last_i_start;                               // 上一開始
    RDFTContext *rdft;                      // 自適應濾波器上下文
    int rdft_bits;                                  // 自使用比特率
    FFTSample *rdft_data;                   // 快速傅里葉採樣
    int xpos;                                       // 
    double last_vis_time;                       // 
    SDL_Texture *vis_texture;               // 音頻Texture
    SDL_Texture *sub_texture;               // 字幕Texture
    SDL_Texture *vid_texture;               // 視頻Texture

    int subtitle_stream;                        // 字幕碼流Id
    AVStream *subtitle_st;                  // 字幕碼流
    PacketQueue subtitleq;                  // 字幕包隊列

    double frame_timer;                     // 幀計時器
    double frame_last_returned_time;    // 上一次返回時間
    double frame_last_filter_delay;     // 上一個過濾器延時
    int video_stream;                           // 視頻碼流Id
    AVStream *video_st;                     // 視頻碼流
    PacketQueue videoq;                 // 視頻包隊列
    double max_frame_duration;          // 最大幀顯示時間 // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
    struct SwsContext *img_convert_ctx; // 視頻轉碼上下文
    struct SwsContext *sub_convert_ctx; // 字幕轉碼上下文
    int eof;                                            // 結束標誌

    char *filename;                             // 文件名
    int width, height, xleft, ytop;         // 寬高,其實座標
    int step;                                       // 步進

#if CONFIG_AVFILTER
    int vfilter_idx;                                // 過濾器索引
    AVFilterContext *in_video_filter;   // 第一個視頻濾鏡 // the first filter in the video chain
    AVFilterContext *out_video_filter;  // 最後一個視頻濾鏡 // the last filter in the video chain
    AVFilterContext *in_audio_filter;   // 第一個音頻過濾器 // the first filter in the audio chain
    AVFilterContext *out_audio_filter;  // 最後一個音頻過濾器 // the last filter in the audio chain
    AVFilterGraph *agraph;                  // 音頻過濾器 // audio filter graph
#endif

    // 上一個視頻碼流Id、上一個音頻碼流Id、上一個字幕碼流Id
    int last_video_stream, last_audio_stream, last_subtitle_stream;

    SDL_cond *continue_read_thread; // 連續讀線程
} VideoState;
5. 開啓EventLoop
refresh_loop_wait_event(cur_stream, &event);

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
    double remaining_time = 0.0;
  //取出事件
    SDL_PumpEvents();
    //  如果事件不是要處理的,就會進行渲染的判斷,然後繼續取出事件的循環
    while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
      //是否顯示鼠標
        if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
            SDL_ShowCursor(0);
            cursor_hidden = 1;
        }
        //使用av_usleep來控制視頻刷新的幀率
        if (remaining_time > 0.0)
            av_usleep((int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        //通知刷新的話。is->force_refresh==1.
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            //進入繪製
            video_refresh(is, &remaining_time);
        //繼續下一個事件循環
        SDL_PumpEvents();
    }
}

通過這個方法,不斷的來取出SDL的事件。同時通過這樣的事件循環,不斷video_refresh送顯繪製。

讀取線程read_thread

如上部分所示,通過stream_open方法,開啓read_thread

read_thread 初始化參數

read_thread作爲讀取線程。

  1. 需要初始化AVformatContextAVStream
  2. 同時,在得到對應的AVStream之後,它還負責初始化好對應的AVCodecAVCodecContext,然後開啓對應的解碼線程。
  3. 最後,通過不斷的av_read_frame,將數據讀取出,送入隊列。等待解碼。
需要初始化AVformatContextAVStream

1. 顯示先創建 AVformatContext

  ic = avformat_alloc_context();

** 2. 設置解碼中斷回調方法** 這個方法,會在網絡中斷的時候,發生調用

    ic->interrupt_callback.callback = decode_interrupt_cb;
    // 設置中斷回調參數
    ic->interrupt_callback.opaque = is;

decode_interrupt_cb 方法

static int decode_interrupt_cb(void *ctx)
{
    VideoState *is = ctx;
    return is->abort_request;
}

3. 打開avformat_open_input

  err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
  //同時,將其注入成全局的一個變量。但是具體如何使用呢??
 av_format_inject_global_side_data(ic);

4. 查找AVStream 找到不同的AVStream,並將其放入對應的數組中。等待後續使用。 找到視頻流之後,還會再次同步顯示的比例和視頻的長寬

    for (i = 0; i < ic->nb_streams; i++) {
        AVStream *st = ic->streams[i];
        enum AVMediaType type = st->codecpar->codec_type;
        st->discard = AVDISCARD_ALL;
        if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
                st_index[type] = i;
    }
    for (i = 0; i < AVMEDIA_TYPE_NB; i++) {
        if (wanted_stream_spec[i] && st_index[i] == -1) {
            av_log(NULL, AV_LOG_ERROR, "Stream specifier %s does not match any %s stream\n", wanted_stream_spec[i], av_get_media_type_string(i));
            st_index[i] = INT_MAX;
        }
    }

    if (!video_disable)
        st_index[AVMEDIA_TYPE_VIDEO] =
            av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                                st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
    if (!audio_disable)
        st_index[AVMEDIA_TYPE_AUDIO] =
            av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
                                st_index[AVMEDIA_TYPE_AUDIO],
                                st_index[AVMEDIA_TYPE_VIDEO],
                                NULL, 0);
    if (!video_disable && !subtitle_disable)
        st_index[AVMEDIA_TYPE_SUBTITLE] =
            av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
                                st_index[AVMEDIA_TYPE_SUBTITLE],
                                (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
                                 st_index[AVMEDIA_TYPE_AUDIO] :
                                 st_index[AVMEDIA_TYPE_VIDEO]),
                                NULL, 0);

    is->show_mode = show_mode;
    //找到視頻流之後,還會再次同步顯示的比例和視頻的長寬
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
        AVCodecParameters *codecpar = st->codecpar;
        AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
        if (codecpar->width)
            set_default_window_size(codecpar->width, codecpar->height, sar);
    }
開啓對應的解碼線程

打開stream_component_open對應的AVStream。打開解碼線程。 ffplay中對應三種碼流。(視頻、音頻和字幕,對應打開自己的解碼線程)

   /* open the streams */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
    }
    if (is->show_mode == SHOW_MODE_NONE)
        is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;

    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
        stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }

開啓對應的線程之前,會初始化好每個碼流的AVCodecAVCodecContext

  • 初始化AVCodecAVCodecContext
    //創建AVCodecContext
   avctx = avcodec_alloc_context3(NULL);
    if (!avctx)
        return AVERROR(ENOMEM);
    //從找到對應的流中的codecpar,codecpar其實是avcodec_parameters,
    // 然後將它完全複製到創建的AVCodecContext
    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
    if (ret < 0)
        goto fail;
    avctx->pkt_timebase = ic->streams[stream_index]->time_base;
    
    //根據codec_id找出最合適的codec
    codec = avcodec_find_decoder(avctx->codec_id);

    switch(avctx->codec_type){
        case AVMEDIA_TYPE_AUDIO   : is->last_audio_stream    = stream_index; forced_codec_name =    audio_codec_name; break;
        case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index; forced_codec_name = subtitle_codec_name; break;
        case AVMEDIA_TYPE_VIDEO   : is->last_video_stream    = stream_index; forced_codec_name =    video_codec_name; break;
    }
    //強制通過解碼器的名字,來打開對應的解碼器
    if (forced_codec_name)
        codec = avcodec_find_decoder_by_name(forced_codec_name);
    if (!codec) {
        if (forced_codec_name) av_log(NULL, AV_LOG_WARNING,
                                      "No codec could be found with name '%s'\n", forced_codec_name);
        else                   av_log(NULL, AV_LOG_WARNING,
                                      "No codec could be found with id %d\n", avctx->codec_id);
        ret = AVERROR(EINVAL);
        goto fail;
    }
    avctx->codec_id = codec->id;

    // 下面是設置屬性,也不大懂。。這些屬性的意思
    if (stream_lowres > codec->max_lowres) {
        av_log(avctx, AV_LOG_WARNING, "The maximum value for lowres supported by the decoder is %d\n",
                codec->max_lowres);
        stream_lowres = codec->max_lowres;
    }
    avctx->lowres = stream_lowres;

    if (fast)
        avctx->flags2 |= AV_CODEC_FLAG2_FAST;

    opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec);
   //av_dict_set方法,傳入參數的效果,等同於我們用命令行時 - 的傳遞方式
    if (!av_dict_get(opts, "threads", NULL, 0))
        av_dict_set(&opts, "threads", "auto", 0);
    if (stream_lowres)
        av_dict_set_int(&opts, "lowres", stream_lowres, 0);
    if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO)
        av_dict_set(&opts, "refcounted_frames", "1", 0);
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }
    if ((t = av_dict_get(opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
        av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
        ret =  AVERROR_OPTION_NOT_FOUND;
        goto fail;
    }
  • 初始化對應的解碼線程 有了AVCodecContext 和AVCodec 之後,就可以初始化解碼線程了 對解碼器的參數再次配置(音頻需要), 然後decoder_init方法初始化Decoder結構體
static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
    memset(d, 0, sizeof(Decoder));
    d->avctx = avctx;
    d->queue = queue;
    d->empty_queue_cond = empty_queue_cond;
    d->start_pts = AV_NOPTS_VALUE;
    d->pkt_serial = -1;
}

decoder_start 正式開啓線程。

static int decoder_start(Decoder *d, int (*fn)(void *), void *arg)
{
    packet_queue_start(d->queue);
    d->decoder_tid = SDL_CreateThread(fn, "decoder", arg);
    if (!d->decoder_tid) {
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    return 0;
}

packet_queue_start 開啓線程之前,將flush_pkt送入PacketQueue隊列當中。

static void packet_queue_start(PacketQueue *q)
{
    SDL_LockMutex(q->mutex);
    q->abort_request = 0;
    packet_queue_put_private(q, &flush_pkt);
    SDL_UnlockMutex(q->mutex);
}
不斷的av_read_frame,將數據讀取出,送入隊列

ps:快進的邏輯後面分析

讀取av_read_frame

 ret = av_read_frame(ic, pkt);

讀取失敗,各個流中送入空包,同時鎖住continue_read_thread,等待10ms

 if (ret < 0) {
 // 讀取結束或失敗。會想各個流,送入一個空的packet.爲什麼要送入空的packet??
            if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
                if (is->video_stream >= 0)
                    packet_queue_put_nullpacket(&is->videoq, is->video_stream);
                if (is->audio_stream >= 0)
                    packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
                if (is->subtitle_stream >= 0)
                    packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
                is->eof = 1;
            }
            if (ic->pb && ic->pb->error)
                break;
            //讀取失敗的話,讀取失敗的原因有很多,其他地方可能會重新Signal這個鎖condition。如果沒有singal這個condition的話,就會等待10ms之後,再釋放,重新循環讀取. 那這個continue_read_thread 到底是鎖了哪呢?
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        } else {
            is->eof = 0;
        }

todo:continue_read_thread 等同於隊列中的empty_queue_cond???

讀取成功,送入隊列 變量pkt中就是我們讀取到的數據。

/* check if packet is in play range specified by user, then queue, otherwise discard */
        //記錄stream_start_time
        stream_start_time = ic->streams[pkt->stream_index]->start_time;
        //如果沒有pts, 就用dts
        pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
        //判斷是否在範圍內。如果duration還沒被定義的話,通過
         //或者在定義的duration內纔可以,用當前的pts-start_time .
        //duration 會在解碼器打開之後,纔會被初始化
        pkt_in_play_range = duration == AV_NOPTS_VALUE ||
                (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
                av_q2d(ic->streams[pkt->stream_index]->time_base) -
                (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
                <= ((double)duration / 1000000);
        // 將解複用得到的數據包添加到對應的待解碼隊列中
        if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
            packet_queue_put(&is->audioq, pkt);
        } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                   && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
            packet_queue_put(&is->videoq, pkt);
        } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
            packet_queue_put(&is->subtitleq, pkt);
        } else {
            av_packet_unref(pkt);
        }

最後退出時,需要關閉對應資源 這個線程關閉的時候。會清空AVFormatContext 和鎖資源

if (ic && !is->ic)
        avformat_close_input(&ic);

    if (ret != 0) {
        SDL_Event event;

        event.type = FF_QUIT_EVENT;
        event.user.data1 = is;
        SDL_PushEvent(&event);
    }
    SDL_DestroyMutex(wait_mutex);
    return 0;
入列的操作

整體的流程如上,讓我們在特別關注一下,具體的入列的操作

MyAVPacketList結構體 PacketList中存儲的單元是自己定義的MyAVPacketList結構體

typedef struct MyAVPacketList {
    AVPacket pkt;
    struct MyAVPacketList *next;
    int serial;
} MyAVPacketList;

結構體中主要是保存了AVPacket數據和隊列的下一個的指針。 同時還保留了一個serial變量。它可以理解成操作數。在初始化和快進的時候,會增加操作數。小於的操作數,在下一次顯示的時候,會直接被拋棄。

隊列操作packet_queue_put

static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    int ret;
    //鎖住隊列中的互斥鎖。這個鎖是每個隊列自己的
    SDL_LockMutex(q->mutex);
    ret = packet_queue_put_private(q, pkt);
    SDL_UnlockMutex(q->mutex);

    // 如果不是flush類型的包,並且沒有成功入隊,則銷燬當前的包
    if (pkt != &flush_pkt && ret < 0)
        av_packet_unref(pkt);

    return ret;
}

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;

    // 如果隊列本身處於捨棄狀態,則直接返回-1
    if (q->abort_request)
       return -1;

    // 創建一個包
    pkt1 = av_malloc(sizeof(MyAVPacketList));
    if (!pkt1)
        return -1;
    pkt1->pkt = *pkt;
    pkt1->next = NULL;

    // 判斷包是否數據flush類型,調整包序列
    // 在創建decoder thread時,會刷入一個flush_pkt,這個時候會提高serial
    if (pkt == &flush_pkt)
        q->serial++;
    pkt1->serial = q->serial;

    // 調整指針。存入隊尾,若沒有隊尾,就放在開頭。
    if (!q->last_pkt)
        q->first_pkt = pkt1;
    else
        q->last_pkt->next = pkt1;
    q->last_pkt = pkt1;
    q->nb_packets++;
    q->size += pkt1->pkt.size + sizeof(*pkt1);
    q->duration += pkt1->pkt.duration;
    /* XXX: should duplicate packet data in DV case */
    // 條件信號
   //通知讀的部分,有數據了,可以繼續讀取了
    //q->cond 在讀取的時候,如果沒有數據就會鎖住,這樣,生產了數據,就會通知讀取。相等於一個讀鎖
    SDL_CondSignal(q->cond);
    return 0;
}

隊列操作packet_queue_put_nullpacket 在讀取錯誤時,也會丟入一個空白。

static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
{
    // 創建一個空數據的包
    AVPacket pkt1, *pkt = &pkt1;
    av_init_packet(pkt);
    pkt->data = NULL;
    pkt->size = 0;
    pkt->stream_index = stream_index;
    return packet_queue_put(q, pkt);
}
其他

弄清楚q->mutex 是什麼鎖 主線程初始化中,進行初始化的,是每個PacketQueue 都有一個自己的互斥鎖和條件鎖的。

static int packet_queue_init(PacketQueue *q)
{
    // 爲一個包隊列分配內存
    memset(q, 0, sizeof(PacketQueue));
    // 創建互斥鎖
    q->mutex = SDL_CreateMutex();
    if (!q->mutex) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    // 創建條件鎖
    q->cond = SDL_CreateCond();
    if (!q->cond) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    // 默認情況捨棄入隊的數據
    q->abort_request = 1;
    return 0;
}

條件鎖q->cond在讀取的時候,如果沒有數據就會鎖住,這樣,生產了數據,就會通知讀取。相等於一個讀鎖

視頻解碼線程video_thread

read_thread的中對應視頻流時,初始化好了AVCodecAVCodecContext。通過decoder_start方法,開啓了video_thread。 在video_thread中需要創建AVFrame來接受解碼後的數據,確定視頻的幀率。 然後開啓解碼循環。 不斷的從隊列中獲取解碼前的數據,然後送入解碼器解碼。 再得到解碼後的數據,在送入對應的隊列當中。

初始化參數

創建AVFrame和得到大致的視頻幀率

    //創建AVFrame
    AVFrame *frame = av_frame_alloc();
    //設置好time_base和frame_rate
    AVRational tb = is->video_st->time_base;
    // 猜測視頻幀率
    AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);

開始循環解碼

從隊列中取得解碼器的數據packet_queue_get 取到要解碼的數據,放到 &d->pkt_temp上

do {
        int ret = -1;
        // 如果處於捨棄狀態,直接返回
        if (d->queue->abort_request)
            return -1;
        // 如果當前沒有包在等待,或者隊列的序列不相同時,取出下一幀
        if (!d->packet_pending || d->queue->serial != d->pkt_serial) {
            AVPacket pkt;
            do {
                // 隊列爲空
                if (d->queue->nb_packets == 0)
                    //當隊列爲空的時候,就會通知釋放這個條件鎖。之前在讀取線程,讀取失敗的時候,也有鎖住這個鎖進行等待。
                    SDL_CondSignal(d->empty_queue_cond);
                // 從隊列中取數據
                if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)
                    return -1;
                // 如果是第一個數據,則會把數據清空一遍。然後開始獲取
                if (pkt.data == flush_pkt.data) {
                    //重置解碼器的狀態,因爲第一次開始解碼或者快進的時候,會先存入一個flush_data,當取到這個的時候,就需要去/重置解碼器的狀態
                //Reset the internal decoder state / flush internal buffers. Should be called
                    avcodec_flush_buffers(d->avctx);
                    d->finished = 0;
                    d->next_pts = d->start_pts;
                    d->next_pts_tb = d->start_pts_tb;
                }
            } while (pkt.data == flush_pkt.data || d->queue->serial != d->pkt_serial);
            // 釋放包
            av_packet_unref(&d->pkt);
            // 更新包
            d->pkt_temp = d->pkt = pkt;
            // 包等待
            d->packet_pending = 1;
        }

得到pkt_temp之後,送入解碼器進行解碼 解碼成功後,還需要得去對應的pts。

// 根據解碼器類型判斷是音頻還是視頻還是字幕
        switch (d->avctx->codec_type) {
            // 視頻解碼
            case AVMEDIA_TYPE_VIDEO:
                // 解碼視頻
                ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp);
                // 解碼成功,更新時間戳
                if (got_frame) {
                    if (decoder_reorder_pts == -1) {
                        frame->pts = av_frame_get_best_effort_timestamp(frame);
                    } else if (!decoder_reorder_pts) {      // 如果不重新排列時間戳,則需要更新幀的pts
                        frame->pts = frame->pkt_dts;
                    }
                }
                break;

            // 音頻解碼
            case AVMEDIA_TYPE_AUDIO:
                // 音頻解碼
                ret = avcodec_decode_audio4(d->avctx, frame, &got_frame, &d->pkt_temp);
                // 音頻解碼完成,更新時間戳
                if (got_frame) {
                    AVRational tb = (AVRational){1, frame->sample_rate};
                    // 更新幀的時間戳
                    if (frame->pts != AV_NOPTS_VALUE)
                        frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb);
                    else if (d->next_pts != AV_NOPTS_VALUE)
                        frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);
                    // 更新下一時間戳
                    if (frame->pts != AV_NOPTS_VALUE) {
                        d->next_pts = frame->pts + frame->nb_samples;
                        d->next_pts_tb = tb;
                    }
                }
                break;
            // 字幕解碼
            case AVMEDIA_TYPE_SUBTITLE:
                ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &d->pkt_temp);
                break;
        }

        // 判斷是否解碼成功
       //  如果解碼失敗的話,包繼續等待
        if (ret < 0) {
            d->packet_pending = 0;
        } else {
            d->pkt_temp.dts =
            d->pkt_temp.pts = AV_NOPTS_VALUE;
            //下面這個操作不明白???
            if (d->pkt_temp.data) {
                if (d->avctx->codec_type != AVMEDIA_TYPE_AUDIO)
                    ret = d->pkt_temp.size;

                d->pkt_temp.data += ret;
                d->pkt_temp.size -= ret;
                
                if (d->pkt_temp.size <= 0)
                    d->packet_pending = 0;
            } else {
                if (!got_frame) {
                    d->packet_pending = 0;
                    d->finished = d->pkt_serial;
                }
            }
        }

得到pkt_temp之後,還需要判斷,是否需要拋棄

static int get_video_frame(VideoState *is, AVFrame *frame)
{
    int got_picture;
    // 解碼視頻幀
    if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
        return -1;
    // 判斷是否解碼成功
    if (got_picture) {
        double dpts = NAN;

        if (frame->pts != AV_NOPTS_VALUE)
            //通過 pts*av_q2d(timebase)可以得到準確的時間
            dpts = av_q2d(is->video_st->time_base) * frame->pts;
        
        //重新得到視頻的比例
        frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
        // 判斷是否需要捨棄該幀。
        if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            if (frame->pts != AV_NOPTS_VALUE) {
                //得到的是當前的時間和時間鍾之間的差值。
                double diff = dpts - get_master_clock(is);
                if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
                    diff - is->frame_last_filter_delay < 0 &&
                    is->viddec.pkt_serial == is->vidclk.serial &&
                    is->videoq.nb_packets) {
                    is->frame_drops_early++;
                    av_frame_unref(frame);
                    got_picture = 0;
                }
            }
        }
    }

    return got_picture;
}

計算時間和入列

// 計算幀的pts、duration等
            //每一幀的時長,應該就是等於幀率的倒數
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            // 放入到已解碼隊列
            ret = queue_picture(is, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
            av_frame_unref(frame);

將已經解碼的數據放到隊列當中queue_picture

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    Frame *vp;

#if defined(DEBUG_SYNC)
    printf("frame_type=%c pts=%0.3f\n",
           av_get_picture_type_char(src_frame->pict_type), pts);
#endif
    
  //先從隊列中取。因爲要queue的大小有限,所以先判斷是否可以繼續寫入
    if (!(vp = frame_queue_peek_writable(&is->pictq)))
        return -1;

    vp->sar = src_frame->sample_aspect_ratio;
    vp->uploaded = 0;

    vp->width = src_frame->width;
    vp->height = src_frame->height;
    vp->format = src_frame->format;

    vp->pts = pts;
    vp->duration = duration;
    vp->pos = pos;
    vp->serial = serial;

    set_default_window_size(vp->width, vp->height, vp->sar);
    
    //將src中的數據送入vp當中,並且重置src
    av_frame_move_ref(vp->frame, src_frame);
    //重新推入
    frame_queue_push(&is->pictq);
    return 0;
}

/**
 * 查找可寫幀
 * @param  f [description]
 * @return   [description]
 */
static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    SDL_LockMutex(f->mutex);
    // 如果幀隊列大於最大
    while (f->size >= f->max_size &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;
    
    //writable index 是 windex
    return &f->queue[f->windex];
}

static void frame_queue_push(FrameQueue *f)
{
    //會將可寫的index偏移
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

已放入隊列,最後 av_frame_unref(frame) 原來的創建的frame就可以了釋放了。

其他

attached_pic入列 attached_pic的意思是有附帶的圖片。比如說一些MP3,AAC音頻文件附帶的專輯封面。所以,就是如果有,就去顯示吧。

is->queue_attachments_req = 1;

主線程視頻顯示部分

在主線程的EventLoop開啓之後,會進行繪製。先會根據主的時間鍾進行同步,然後進行顯示。這裏我們因爲只分析視頻部分。所以就不關注時間鐘的同步了。

視頻的顯示

同步後時間,取到具體的frame時,就送入顯示。

/* display the current picture, if any */
static void video_display(VideoState *is)
{
     //當window還沒創建的時候,就是第一次,會去初始化
    if (!window)
        video_open(is);
  
    //SDL常規操作兩則
    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);
    if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
        video_audio_display(is);
    else if (is->video_st)
        //video走這裏
        video_image_display(is);
    //送顯
    SDL_RenderPresent(renderer);
}
  • 第一次會進入video_open。 先去創建SDL_WindowSDL_Renderer。 舉Android的例子來說,在Android中SDL使用的是OpenGL。 SDL_CreateWindow就是通過ANativeWindow 來創建一個GL Surface。同時創建GLContext。 SDL_CreateRenderer 就是glmakeCurrent, 同時對Renderer的各個方法進行初始化,以供後面調用。
  • 確認打開做好SDL_WindowSDL_Renderer之後,就會調用 video_image_display進行顯示。
static void video_image_display(VideoState *is)
{
    Frame *vp;
    Frame *sp = NULL;
    SDL_Rect rect;

    vp = frame_queue_peek_last(&is->pictq);
    
    //省略了關於字幕的部分...
    
    //計算SDL_Rect  就是當前顯示的範圍
    calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp->width, vp->height, vp->sar);
    
    //如果這一幀還沒顯示過
    if (!vp->uploaded) {
        int sdl_pix_fmt = vp->frame->format == AV_PIX_FMT_YUV420P ? SDL_PIXELFORMAT_YV12 : SDL_PIXELFORMAT_ARGB8888;
        //如果需要重新創建紋理的話
        if (realloc_texture(&is->vid_texture, sdl_pix_fmt, vp->frame->width, vp->frame->height, SDL_BLENDMODE_NONE, 0) < 0)
            return;
        //刷新紋理
        if (upload_texture(is->vid_texture, vp->frame, &is->img_convert_ctx) < 0)
            return;
        vp->uploaded = 1;
        vp->flip_v = vp->frame->linesize[0] < 0;
    }

    SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0);
    if (sp) {
#if USE_ONEPASS_SUBTITLE_RENDER
        SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
#else
        int i;
        double xratio = (double)rect.w / (double)sp->width;
        double yratio = (double)rect.h / (double)sp->height;
        for (i = 0; i < sp->sub.num_rects; i++) {
            SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i];
            SDL_Rect target = {.x = rect.x + sub_rect->x * xratio,
                               .y = rect.y + sub_rect->y * yratio,
                               .w = sub_rect->w * xratio,
                               .h = sub_rect->h * yratio};
            SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target);
        }
#endif
    }
}

通過calculate_display_rectton這裏的就是計算需要顯示的區域。 然後如果還沒創建紋理的話,在realloc_texture內調用SDL_CreateTexture來創建紋理。 在upload_texture內調用SDL_UpdateTexture來更新紋理。 最後是進行SDL_RendererCopy.或者SDL_RendererCopyEx(SDL_RendererCopy的加強版,可以通知顯示區域和翻轉)。 然後送顯SDL_CreateTexture。 這裏可以看到依舊是SDL視頻送顯的經典套路。

SDL視頻送顯的經典套路.png

處理按鍵的事件

這部分暫時掠過...

總結

整體的流程.jpg

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章