天天看點

ijkplayer 代碼走讀之 read_thread 線程中 av_read_frame() 資料流讀取過程詳解回顧read_thread() 線程讀取資料流

回顧

ijkplayer 開機過程:

  1. 使用者在 Android 程式中,調用封裝接口 IjkLibLoader 方法,裝載 ijkffmpeg、ijksdl和ijkplayer三個庫檔案到安卓系統;
  2. 初始化播放器,調用的JNI接口程式 native_setup() 函數,此函數建立播放器消息隊列和播放其相關參數;
  3. 使用者在 Android 程式中,調用 createPlayer() 和 prepareAsync() 封裝接口函數建立播放器,并讓播放器進入待播放狀态;
  4. 啟動播放器。

前面分析過 prepareAsync() 函數相關内容,其中比較重要函數是 VideoState *is = stream_open(ffp, file_name, NULL);

在函數中:

  1. 建立 3 個隊列,視訊、音頻和副标題隊列,is中存放三個流 AVStream *audio_st、*subtitle_st、*video_st。
  2. 建立 2 個線程,read_thread() 和 video_refresh() 線程;
  3. 初始化解碼器相關參數,函數退出。

    播放器就具備播放的能力,這個過程涵蓋 ijkplayer 源碼絕大部分内容; 進入播放過程中,程式邏輯架構已經建立起來,在運作期間能夠

    處理一些使用者交換功能。

再下來回顧 read_thread() 線程功能,總結如下:

  1. 調用 avformat_open_input() 函數,此函數根據資料源選擇網絡協定、解封裝器類别,通過使用者 URL位址關鍵字區分,

    如: “tcpext://192.168.1.31:1717/v-out.h264”, ijkplayer播放就解析出協定是tcpext、解封裝器是h264方式。

  2. 調用 avformat_find_stream_info(ic, opts) 函數, 此函數通過資料流資料識别編碼格式,得出該資料流應該配置

    什麼樣的解碼器。在進入此函數時,該 AVFormatContext 中流的數量和種類就确定下來了,是在什麼時候确認的呢?存疑.

  3. 調用 stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO])函數, 根據資料流資訊為該資料流建構解碼器,

    分别針對視訊、音頻和副标題流類型,配置解碼器。

  4. 進入線程的循環體,av_read_frame(ic, pkt) -> packet_queue_put(&is->videoq, &copy),讀取-> 入隊過程反複循環。

籠統描述程式主體邏輯,大緻就這麼個邏輯。

本篇主要分析資料流讀取過程,清晰目标,開啟代碼走讀模式。

read_thread() 線程

我們先看看 read_thread() 線程中此部分相關簡化邏輯代碼,如下:

void read_thread(void *arg)
{
    FFPlayer *ffp = arg;                                            ///> 此參數是Android使用者空間傳遞相關參數
    VideoState *is = ffp->is;
    AVFormatContext *ic = NULL;
    int err, i, ret __unused;
    int st_index[AVMEDIA_TYPE_NB];
    AVPacket pkt1, *pkt = &pkt1;

    ///> 資料流編碼格式識别部分代碼 1 部分
    if (ffp->find_stream_info) {
        AVDictionary **opts = setup_find_stream_info_opts(ic, ffp->codec_opts);  ///> 擷取解碼器參數字典指針
        int orig_nb_streams = ic->nb_streams;

        do {
            if (av_stristart(is->filename, "data:", NULL) && orig_nb_streams > 0) {
                for (i = 0; i < orig_nb_streams; i++) {
                    if (!ic->streams[i] || !ic->streams[i]->codecpar || ic->streams[i]->codecpar->profile == FF_PROFILE_UNKNOWN) {
                        break;
                    }
                }

                if (i == orig_nb_streams) {
                    break;
                }
            }
            err = avformat_find_stream_info(ic, opts);                          ///> 進入比對查找過程,在解碼器 options 字典中 flag 辨別流的種類
        } while(0); 
        ffp_notify_msg1(ffp, FFP_MSG_FIND_STREAM_INFO);
    }

    is->realtime = is_realtime(ic);
    av_dump_format(ic, 0, is->filename, 0);

    ///> 資料流編碼格式識别部分代碼 2 部分
    int video_stream_count = 0;
    int h264_stream_count = 0;
    int first_h264_stream = -1;
    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 && ffp->wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, ffp->wanted_stream_spec[type]) > 0)
                st_index[type] = i;

        // choose first h264

        if (type == AVMEDIA_TYPE_VIDEO) {
            enum AVCodecID codec_id = st->codecpar->codec_id;
            video_stream_count++;
            if (codec_id == AV_CODEC_ID_H264) {
                h264_stream_count++;
                if (first_h264_stream < 0)
                    first_h264_stream = i;
            }
        }
        av_log(NULL, AV_LOG_INFO, "DEBUG %s, LINE:%d ,CODEC_ID:%d\n",__FILE__, __LINE__, (uint32_t)st->codecpar->codec_id);
    }

    ///> 如果是多流模式
    if (video_stream_count > 1 && st_index[AVMEDIA_TYPE_VIDEO] < 0) {
        st_index[AVMEDIA_TYPE_VIDEO] = first_h264_stream;
        av_log(NULL, AV_LOG_WARNING, "multiple video stream found, prefer first h264 stream: %d\n", first_h264_stream);
    }

    ///> 逐一比對解碼器
    if (!ffp->video_disable)
        st_index[AVMEDIA_TYPE_VIDEO] =
            av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                                st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
    if (!ffp->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 (!ffp->video_disable && !ffp->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 = ffp->show_mode;
    ///* open the streams,打開流格式 */
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
    } else {
        ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
        is->av_sync_type  = ffp->av_sync_type;
    }

    ret = -1;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(ffp, 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(ffp, st_index[AVMEDIA_TYPE_SUBTITLE]);
    }
    ffp_notify_msg1(ffp, FFP_MSG_COMPONENT_OPEN);

    ///> 通知 android 空間程式
    if (!ffp->ijkmeta_delay_init) {
        ijkmeta_set_avformat_context_l(ffp->meta, ic);
    }

    ///> 設定字典項的狀态
    ffp->stat.bit_rate = ic->bit_rate;
    if (st_index[AVMEDIA_TYPE_VIDEO] >= 0)
        ijkmeta_set_int64_l(ffp->meta, IJKM_KEY_VIDEO_STREAM, st_index[AVMEDIA_TYPE_VIDEO]);
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0)
        ijkmeta_set_int64_l(ffp->meta, IJKM_KEY_AUDIO_STREAM, st_index[AVMEDIA_TYPE_AUDIO]);
    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0)
        ijkmeta_set_int64_l(ffp->meta, IJKM_KEY_TIMEDTEXT_STREAM, st_index[AVMEDIA_TYPE_SUBTITLE]);

    ///> 播放器狀态調節
    ffp->prepared = true;
    ffp_notify_msg1(ffp, FFP_MSG_PREPARED);
    if (ffp->auto_resume) {
        ffp_notify_msg1(ffp, FFP_REQ_START);
        ffp->auto_resume = 0;
    }
    /* offset should be seeked*/
    if (ffp->seek_at_start > 0) {
        ffp_seek_to_l(ffp, (long)(ffp->seek_at_start));
    }

    ///> 進入循環播放狀态,線程循環體
    for (;;){

        ///>
        if (is->queue_attachments_req) {  ///> 在打開流的時候配置此辨別 = 1
            if (is->video_st && (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
                AVPacket copy = { 0 };
                if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)
                    goto fail;
                packet_queue_put(&is->videoq, &copy);
                packet_queue_put_nullpacket(&is->videoq, is->video_stream);
            }
            is->queue_attachments_req = 0;
        }
        ///> 
        pkt->flags = 0;
        ret = av_read_frame(ic, pkt);
        ///>
        if (pkt->flags & AV_PKT_FLAG_DISCONTINUITY) {
            if (is->audio_stream >= 0) {
                packet_queue_put(&is->audioq, &flush_pkt);
            }
            if (is->subtitle_stream >= 0) {
                packet_queue_put(&is->subtitleq, &flush_pkt);
            }
            if (is->video_stream >= 0) {
                packet_queue_put(&is->videoq, &flush_pkt);
            }
        }
        ///>
        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 && (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);
        }
        ///>
        ffp_statistic_l(ffp);
        av_log(NULL, AV_LOG_INFO, " %s / %s , LINE:%d \n",__FILE__, __func__, __LINE__);
    }
}
           

此程式是簡化版結構内容,各節點有标注資訊。

讀取資料流

在 read_thread() 線程執行 av_read_frame(ic, pkt) 函數循環讀取資料流内容函數,進行跟蹤梳理函數調用關系如下。

av_read_frame(ic, pkt);                             ///> 入口參數: AVFormatContext *ic
    -> read_frame_internal(s, pkt);
        -> ff_read_packet(s, &cur_pkt);             ///> 入口參數: AVPacket cur_pkt;
            -> av_init_packet(pkt);
            -> s->iformat->read_packet(s, pkt);     ///> 此處 read_packet 調用是 ff_raw_read_partial_packet(AVFormatContext *s, AVPacket *pkt) 函數,在 libavformat/rawdec.c 檔案中
                -> av_new_packet(pkt, size)
                -> avio_read_partial(s->pb, pkt->data, size); ///> 入口參數: AVIOContext s->pb, 函數在 libavformat/aviobuf.c 檔案中
                    -> s->read_packet(s->opaque, buf, size);  ///> 此 read_packet 調用是 io_read_packet() 函數,此函數最終調用 tcp_read() 函數,見下面分析。
                    -> memcpy(buf, s->buf_ptr, len);
                    -> s->buf_ptr += len;
                    -> return len;
                -> av_shrink_packet(pkt, ret);
        -> av_parser_init(st->codecpar->codec_id)
        -> avcodec_get_name(st->codecpar->codec_id)
        -> compute_pkt_fields(s, st, NULL, pkt, AV_NOPTS_VALUE, AV_NOPTS_VALUE)
        -> read_from_packet_buffer(&s->internal->parse_queue, &s->internal->parse_queue_end, pkt)
        -> update_stream_avctx(s);
    -> add_to_pktbuf(&s->internal->packet_buffer, pkt,&s->internal->packet_buffer_end, 1);
           

下面分析入口參數之間關系, 函數調用關系 io_read_packet() -> ffurl_read() -> tcp_read() 導圖。

先從函數入口參數梳理,如下。

函數入口第一個參數 AVFormatContext -> AVIOContext -> opaque 入口參數來源關系導圖,在 AVIOContext 結構體定義中為 void * opaque 類型。

///> 函數入口參數是 s->opaque 内容
static int io_read_packet(void *opaque, uint8_t *buf, int buf_size)
{
    AVIOInternal *internal = opaque;                    ///> 此處直接指派轉換成 AVIOInternal 指針,入口傳遞過來的 AVIOContext s->pb,
    return ffurl_read(internal->h, buf, buf_size);      ///> ffurl_read 調用是 tcp_read() 增加的私有協定中的函數。
}

//> 結構體 AVIOInternal 定義如下
typedef struct AVIOInternal {
    URLContext *h;
} AVIOInternal;

//> 結構體 URLContext 定義如下
typedef struct URLContext {
    const AVClass *av_class;    /**< information for av_log(). Set by url_open(). */
    const struct URLProtocol *prot;
    void *priv_data;
    char *filename;             /**< specified URL */
    int flags;
    int max_packet_size;        /**< if non zero, the stream is packetized with this max packet size */
    int is_streamed;            /**< true if streamed (no seek possible), default = false */
    int is_connected;
    AVIOInterruptCB interrupt_callback;
    int64_t rw_timeout;         /**< maximum time to wait for (network) read/write operation completion, in mcs */
    const char *protocol_whitelist;
    const char *protocol_blacklist;
    int min_packet_size;        /**< if non zero, the stream is packetized with this min packet size */
    int64_t pts;                ///< 增加 pts 變量
} URLContext;

///> 此函數的入口 URLContext *h 指針内容類型如上圖。
static int tcp_read(URLContext *h, uint8_t *buf, int size)
{
    uint8_t header[HEADER_SIZE];
    TCPEXTContext *s = h->priv_data;
    int ret;

    if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
        ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
        if (ret)
            return ret;
    }

    ret = recv(s->fd, header, HEADER_SIZE, MSG_WAITALL);
    if(ret < HEADER_SIZE){
        av_log(NULL, AV_LOG_INFO, "%s/%s(), LINE:%d ,READ_HEADER_AIL length:%d \n",__FILE__, __func__, __LINE__, ret);
        return 0;
    }
    uint32_t msb = header[0] << 24 | header[1] << 16 | header[2] << 8 | header[3];
    uint32_t lsb = header[4] << 24 | header[5] << 16 | header[6] << 8 | header[7];
    uint32_t len = header[8] << 24 | header[9] << 16 | header[10] << 8 | header[11];
    uint64_t pts = msb << 32 | lsb ;
    av_log(NULL, AV_LOG_INFO, "READ HEADER msb:%08x, lsb:%08x, len:%08x \n", msb, lsb, len);    
    assert( pts == NO_PTS || (pts & 0x8000000000000000) == 0);
    assert(len);

    ret = recv(s->fd, buf, len, MSG_WAITALL);
    if (ret > 0){
        av_application_did_io_tcp_read(s->app_ctx, (void*)h, ret);
        uint32_t hsb = buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3];
        msb = buf[4] << 24 | buf[5] << 16 | buf[6] << 8 | buf[7];
        lsb = buf[8] << 24 | buf[9] << 16 | buf[10] << 8 | buf[11];
        av_log(NULL, AV_LOG_INFO, "H264 HEADER hsb:%08x, msb:%08x, lsb:%08x \n", hsb, msb, lsb);
    }
    av_log(NULL, AV_LOG_INFO, "%s/%s(), LINE:%d ,recv length:%d \n",__FILE__, __func__, __LINE__, ret);
    return ret < 0 ? ff_neterrno() : ret;
}
           

總結:

  • 1>. 線程 read_thread() 定義 AVFormatContext ic,AVPacket pkt1,全局變量, 函數 av_read_frame(ic, pkt);入口參數

    都是全局變量,tcp_read函數入口 h = (URLContext)ic->pb->opaque, buf = pkt->data 參數。

  • 2>. 隻能在 URLContext *h 中存放 pts 資料,在增加的私有解封裝器 ff_raw_read_partial_packet() 函數中,把 pts 值轉寫

    到 pkt->pts 中。

  • 3>. 在 ijkplayer的sdk中,增加私有通訊協定和私有解封裝器、程式處理思路基本類似,此子產品可供參考。

此處擷取的 packet_buffer 對象中有 pts 值,也就是說在讀取資料時可以把目前的 pts 内容放進去,