banner
jzman

jzman

Coding、思考、自觉。
github

IjkPlayer系列之數據讀取線程read_thread

PS: 控制新技術對你的影響而不是被控制。

本文分析下 IjkPlayer 的數據讀取線程 read_thread,目的是理清其基本流程以及關鍵函數的調用,主要內容如下:

  1. IjkPlayer 基本使用
  2. read_thread 創建
  3. avformat_alloc_context
  4. avformat_open_input
  5. avformat_find_stream_info
  6. avformat_seek_file
  7. av_dump_format
  8. av_find_best_stream
  9. stream_component_open
  10. read_thread 主循環

IjkPlayer 基本使用#

簡單回顧下 IjkPlayer 的基本使用方式如下:

// 創建IjkMediaPlayer
IjkMediaPlayer mMediaPlayer = new IjkMediaPlayer();
// 設置Log級別
mMediaPlayer.native_setLogLevel(IjkMediaPlayer.IJK_LOG_DEBUG);
// 設置Option
mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1);
// ...
// 設置事件監聽
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
mMediaPlayer.setOnCompletionListener(mCompletionListener);
mMediaPlayer.setOnErrorListener(mErrorListener);
mMediaPlayer.setOnInfoListener(mInfoListener);
// 設置Surface
mMediaPlayer.setSurface(surface)
// ...
// 設置url
mMediaPlayer.setDataSource(dataSource);
// 準備播放
mMediaPlayer.prepareAsync();

當調用 prepareAsync 之後收到 onPrepared 回調是調用 start 開始播放:

@Override
public void onPrepared(IMediaPlayer mp) {
    // 開始播放
    mMediaPlayer.start();
}

到此,一般情況下視頻就能正常播放了,這裡只關注調用流程。

read_thread 創建#

IjkMediaPlayer 的方法 prepareAsync 開始看,其調用流程如下:

Mermaid Loading...

可知 prepareAsync 最後調用的是函數 stream_open ,其定義如下:

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat){
    av_log(NULL, AV_LOG_INFO, "stream_open\n");
    assert(!ffp->is);
    // 初始化VideoState及部分參數。
    VideoState *is;
    is = av_mallocz(sizeof(VideoState));
    if (!is)
        return NULL;
    is->filename = av_strdup(filename);
    if (!is->filename)
        goto fail;
    // 這裡iformat還沒被賦值,後面通過探測找到最佳的AVInputFormat
    is->iformat = iformat;
    is->ytop    = 0;
    is->xleft   = 0;
#if defined(__ANDROID__)
    if (ffp->soundtouch_enable) {
        is->handle = ijk_soundtouch_create();
    }
#endif

    /* start video display */
    // 解碼後幀隊列初始化
    if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
        goto fail;
    if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
        goto fail;
    if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
        goto fail;

    // 未解碼數據隊列初始化
    if (packet_queue_init(&is->videoq) < 0 ||
        packet_queue_init(&is->audioq) < 0 ||
        packet_queue_init(&is->subtitleq) < 0)
        goto fail;

    // 條件變量(信號量)初始化,包括讀線程、視頻seek、音頻seek相關信號量
    if (!(is->continue_read_thread = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        goto fail;
    }

    if (!(is->video_accurate_seek_cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        ffp->enable_accurate_seek = 0;
    }

    if (!(is->audio_accurate_seek_cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        ffp->enable_accurate_seek = 0;
    }
    // 時鐘初始化
    init_clock(&is->vidclk, &is->videoq.serial);
    init_clock(&is->audclk, &is->audioq.serial);
    init_clock(&is->extclk, &is->extclk.serial);
    is->audio_clock_serial = -1;
    // 初始化音量範圍
    if (ffp->startup_volume < 0)
        av_log(NULL, AV_LOG_WARNING, "-volume=%d < 0, setting to 0\n", ffp->startup_volume);
    if (ffp->startup_volume > 100)
        av_log(NULL, AV_LOG_WARNING, "-volume=%d > 100, setting to 100\n", ffp->startup_volume);
    ffp->startup_volume = av_clip(ffp->startup_volume, 0, 100);
    ffp->startup_volume = av_clip(SDL_MIX_MAXVOLUME * ffp->startup_volume / 100, 0, SDL_MIX_MAXVOLUME);
    is->audio_volume = ffp->startup_volume;
    is->muted = 0;

    // 設置音視頻同步方式,默認AV_SYNC_AUDIO_MASTER
    is->av_sync_type = ffp->av_sync_type;

    // 播放互斥鎖
    is->play_mutex = SDL_CreateMutex();
    // 精準seek互斥鎖
    is->accurate_seek_mutex = SDL_CreateMutex();

    ffp->is = is;
    is->pause_req = !ffp->start_on_prepared;

    // 視頻渲染線程
    is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
    if (!is->video_refresh_tid) {
        av_freep(&ffp->is);
        return NULL;
    }

    is->initialized_decoder = 0;
    // 讀取線程
    is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
    if (!is->read_tid) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
        goto fail;
    }
    
    // 異步初始化解碼器,和硬解相關,默認未開啟
    if (ffp->async_init_decoder && !ffp->video_disable && ffp->video_mime_type && strlen(ffp->video_mime_type) > 0
                    && ffp->mediacodec_default_name && strlen(ffp->mediacodec_default_name) > 0) {
        // mediacodec
        if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2) {
            decoder_init(&is->viddec, NULL, &is->videoq, is->continue_read_thread);
            ffp->node_vdec = ffpipeline_init_video_decoder(ffp->pipeline, ffp);
        }
    }
    // 允許初始化解碼器
    is->initialized_decoder = 1;

    return is;
fail:
    is->initialized_decoder = 1;
    is->abort_request = true;
    if (is->video_refresh_tid)
        SDL_WaitThread(is->video_refresh_tid, NULL);
    stream_close(ffp);
    return NULL;
}

可知函數 stream_open 主要做了如下幾件事:

  1. 初始化 VideoState 及部分參數。
  2. 初始化幀隊列,包括初始化已解碼的視頻幀隊列 pictq、音頻幀隊列 sampq 和字幕幀隊列 subpq和未解碼的視頻數據隊列 videoq、音頻數據隊列 audioq 和字幕數據隊列 subtitleq
  3. 音視頻同步方式及時鐘初始化,默認 AV_SYNC_AUDIO_MASTER,也就是音頻時鐘作為主時鐘。
  4. 音量初始化範圍。
  5. 創建了線程名為 ff_vout 的視頻渲染線程 video_refresh_thread
  6. 創建了線程名為 ff_read 的視頻渲染線程 read_thread

到此開始本文主題數據讀取線程 read_thread 函數的分析, 函數 read_thread 關鍵部分簡化如下:

static int read_thread(void *arg){
    // ...
    
    // 1. 創建AVFormatContext,指定打開流、關閉流的默認函數等
    ic = avformat_alloc_context();
    if (!ic) {
        av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");
        ret = AVERROR(ENOMEM);
        goto fail;
    }
    // ...
    
    // 2. 打開碼流獲取header信息
    err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
    if (err < 0) {
        print_error(is->filename, err);
        ret = -1;
        goto fail;
    }
    ffp_notify_msg1(ffp, FFP_MSG_OPEN_INPUT);
    // ...
    
    // 3. 獲取碼流信息
    if (ffp->find_stream_info) {
        err = avformat_find_stream_info(ic, opts);
    } 
    ffp_notify_msg1(ffp, FFP_MSG_FIND_STREAM_INFO);
    // ...
    
    // 4. 如果有指定播放起始時間則seek到該播放位置
    if (ffp->start_time != AV_NOPTS_VALUE) {
        int64_t timestamp;
        timestamp = ffp->start_time;
        if (ic->start_time != AV_NOPTS_VALUE)
            timestamp += ic->start_time;
        ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
    }
    // ...
    
    // 5. 打印格式信息
    av_dump_format(ic, 0, is->filename, 0);
}

下面內容僅限於 read_thread 函數的主要流程。

avformat_alloc_context#

avformat_alloc_context 函數主要是為 AVFormatContext 分配內存、初始化 ic->internal 部分參數,如下:

AVFormatContext *avformat_alloc_context(void){
    // 為AVFormatContext分配內存
    AVFormatContext *ic;
    ic = av_malloc(sizeof(AVFormatContext));
    if (!ic) return ic;
    // 初始化打開流、關閉流的默認函數
    avformat_get_context_defaults(ic);
    // 為
    ic->internal = av_mallocz(sizeof(*ic->internal));
    if (!ic->internal) {
        avformat_free_context(ic);
        return NULL;
    }
    ic->internal->offset = AV_NOPTS_VALUE;
    ic->internal->raw_packet_buffer_remaining_size = RAW_PACKET_BUFFER_SIZE;
    ic->internal->shortest_end = AV_NOPTS_VALUE;
    return ic;
}

繼續查看 avformat_get_context_defaults 函數如下:

static void avformat_get_context_defaults(AVFormatContext *s){
    memset(s, 0, sizeof(AVFormatContext));
    s->av_class = &av_format_context_class;
    s->io_open  = io_open_default;
    s->io_close = io_close_default;
    av_opt_set_defaults(s);
}

這裡指定了打開流、關閉流的默認函數分別為 io_open_defaultio_close_default,這裡暫不關注後續流程。

avformat_open_input#

avformat_open_input 函數主要用於打開碼流獲取 header 信息,其定義簡化如下:

int avformat_open_input(AVFormatContext **ps, const char *filename,
                        AVInputFormat *fmt, AVDictionary **options){
    // ...

    // 打開碼流探測碼流輸入格式,返回最佳解復用器的得分
    av_log(NULL, AV_LOG_FATAL, "avformat_open_input > init_input before > nb_streams:%d\n",s->nb_streams);
    if ((ret = init_input(s, filename, &tmp)) < 0)
        goto fail;
    s->probe_score = ret;

    // 協議黑白名單檢查、碼流格式白名單檢查等
    // ...
    
    // 讀取媒體頭部
    // read_header主要是做某種格式的初始化工作,如填充自己的私有結構
    // 根據流的數量分配流結構並初始化,把文件指針指向數據區開始處等
    // 創建好AVStream,並等待在後續的流程中,可以取出或寫入音視頻流信息
    if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
        if ((ret = s->iformat->read_header(s)) < 0)
            goto fail;
    // ...
    
    // 處理音視頻中附加的圖片,比如專輯裡面的圖片
    if ((ret = avformat_queue_attached_pictures(s)) < 0)
        goto fail;
    // ...
    
    // 更新AVStream解碼器相關信息到AVCodecContext
    update_stream_avctx(s);
    // ...
}

可知 avformat_open_input 函數主要是打開碼流探測碼流輸入格式、協議黑白名單和碼流格式白名單檢查、讀取文件 header 信息等,最後使用函數 update_stream_avctx 更新 AVStream 解碼器相關信息到對應的 AVCodecContext 中,這個操作會在後續流程中經常看到。

最重要的是打開碼流探測碼流輸入格式和讀取文件 header 信息,分別調用的函數 init_inputread_header 函數,read_header 會在讀取 header 信息過程中完成 AVStream 的初始化。

init_input 函數主要是探測碼流格式並返回該碼流格式的得分,最終找到對應該碼流格式的最佳 AVInputFormat,這個結構體是初始化時註冊的解復用器,每個解復用器都對應一個 AVInputFormat 對象,同理復用器對應的是 AVOutputFormat 這裡暫且了解一下。

init_input 函數如果執行成功,則對應的碼流格式已經確定,此時就可以調用 read_header 函數了,其對應的是當前碼流格式 AVInputFormat 對應的解復用器 demuxer 中的 xxx_read_header 函數,如果是 hls 格式的碼流,則對應的則是 hls_read_header,其定義如下:

AVInputFormat ff_hls_demuxer = {
    .name           = "hls,applehttp",
    .read_header   = hls_read_header,
    // ...
};

// hls_read_header
static int hls_read_header(AVFormatContext *s, AVDictionary **options){
    // ...
}

avformat_find_stream_info#

avformat_find_stream_info 函數主要用來獲取碼流信息,用來探測沒有 header 的文件格式比較有用,可以通過該函數獲取視頻寬高、總時長、碼率、幀率、像素格式等等,其定義簡化如下:

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options){
    // ...
    
    // 1. 遍歷流
    for (i = 0; i < ic->nb_streams; i++) {
        // 初始化流中的解析器,具體是AVCodecParserContext和AVCodecParser初始化
        st->parser = av_parser_init(st->codecpar->codec_id);
        // ...
        
        // 根據AVStream中的解碼器參數探測對應的解碼器並返回
        codec = find_probe_decoder(ic, st, st->codecpar->codec_id);
        // ...
        
        // 如果解碼器參數不全,根據指定的AVCodec初始化AVCodecContext以及調用解碼器的init函數以初始化解碼器
        if (!has_codec_parameters(st, NULL) && st->request_probe <= 0) {
            if (codec && !avctx->codec)
                if (avcodec_open2(avctx, codec, options ? &options[i] :&thread_opt) < 0)
                    av_log(ic, AV_LOG_WARNING,
                           "Failed to open codec in %s\n",__FUNCTION__);
        }
    }
    
    // 2. 死循環獲取碼流信息
    for (;;) {
        // ...
        // 檢查是否有中斷請求,如果有則調用中斷函數
        if (ff_check_interrupt(&ic->interrupt_callback)) {
            break;
        }
        
        // 遍歷流,檢查是否還需要處理解碼器相關參數
        for (i = 0; i < ic->nb_streams; i++) {
            int fps_analyze_framecount = 20;
            st = ic->streams[i];
            // 檢查流中的的解碼器參數,如果完整則break,反之則繼續執行,以便進一步分析
            if (!has_codec_parameters(st, NULL))
                break;
            // ...
        }
        
        if (i == ic->nb_streams) {
            // 標識所有的流分析結束
            analyzed_all_streams = 1;
            // 如果當前AVFormatContext設置ctx_flags為AVFMTCTX_NOHEADER則表明當前碼流是沒有header信息的
            // 此時需要讀取一些數據包來獲取流信息,反之則直接break出去,死循環正常結束
            if (!(ic->ctx_flags & AVFMTCTX_NOHEADER)) {
                /* If we found the info for all the codecs, we can stop. */
                ret = count;
                av_log(ic, AV_LOG_DEBUG, "All info found\n");
                flush_codecs = 0;
                break;
            }
        }
         
        // 讀取的數據已經多餘允許探測的數據大小,但是還未得到所有的編解碼器信息
        if (read_size >= probesize) {
            break;
        }
         
        // 以下針對當前碼流是沒有header信息的處理
         
        // 讀取一幀壓縮編碼數據
        ret = read_frame_internal(ic, &pkt1);
        if (ret == AVERROR(EAGAIN)) continue;
        if (ret < 0) {
            /* EOF or error*/
            eof_reached = 1;
            break;
        }
        
        // 讀取的數據添加到緩存中,後續會先從緩存中讀這些數據
        ret = add_to_pktbuf(&ic->internal->packet_buffer, pkt,
                                &ic->internal->packet_buffer_end, 0);
        // 嘗試解碼一些壓縮編碼數據                       
        try_decode_frame(ic, st, pkt,(options && i < orig_nb_streams) ? &options[i] : NULL);
        
        // ...
    }
    
    // 3. 讀取數據到流的末尾處理
    if (eof_reached) {
         for (stream_index = 0; stream_index < ic->nb_streams; stream_index++) {
             if (!has_codec_parameters(st, NULL)) {
                const AVCodec *codec = find_probe_decoder(ic, st, st->codecpar->codec_id);
                if (avcodec_open2(avctx, codec, (options && stream_index < orig_nb_streams) ? &options[stream_index] : &opts) < 0)
                        av_log(ic, AV_LOG_WARNING,
         }
    }
    // 4. 解碼器執行flushing操作,避免緩存的數據未被取出
    if (flush_codecs) {
        AVPacket empty_pkt = { 0 };
        int err = 0;
        av_init_packet(&empty_pkt);
        for (i = 0; i < ic->nb_streams; i++) {
            st = ic->streams[i];
            /* flush the decoders */
            if (st->info->found_decoder == 1) {
                do {
                    // 
                    err = try_decode_frame(ic, st, &empty_pkt,
                                            (options && i < orig_nb_streams)
                                            ? &options[i] : NULL);
                } while (err > 0 && !has_codec_parameters(st, NULL));
        
                if (err < 0) {
                    av_log(ic, AV_LOG_INFO,
                        "decoding for stream %d failed\n", st->index);
                }
            }
        }
    }
    
    // 5. 後續就是一些碼流信息的計算,比如pix_fmt、橫縱比SAR、實際幀率、平均幀率等等
    // ...
    
    // 6. 從流的內部AVCodecContext(avctx)更新流對應的解碼器參數AVCodecParameters
    for (i = 0; i < ic->nb_streams; i++) {
        ret = avcodec_parameters_from_context(st->codecpar, st->internal->avctx);
        // ...
    }
    
    // ...
}

由於 avformat_find_stream_info 函數代碼量比較多,上述代碼省略了大部分細節,保留了比較關鍵的部分,這裡只看主要流程,從源碼可知,該函數中會頻繁的使用 has_codec_parameters 函數來檢查流內部解碼器上下問參數是否合理,如果不合理則要儘可能的採取措施保證流內部解碼器上下文參數合理,當 ret = 0; 標識 avformat_find_stream_info 執行成功,其主要流程如下:

  1. 遍歷流,根據流中的一些參數初始化 AVCodecParserAVCodecParserContext,通過函數 find_probe_decoder 來探測解碼器,使用函數 avcodec_open2 來初始化 AVCodecContext 並調用解碼器的 init 函數來初始化解碼器靜態數據。
  2. for (;;) 死循環主要是使用 ff_check_interrupt 函數進行中斷檢測、遍歷流使用 has_codec_parameters 函數檢測碼流內部的解碼器上下文參數是否合理及數據讀取的,如果合理且當前碼流有 header 信息,則標識 analyzed_all_streams = 1;flush_codecs = 0; 直接 break 退出該死循環,如果當前碼流有 header 信息,也就是 ic->ctx_flags 被設置為 AVFMTCTX_NOHEADER 的時候,此時需要調用 read_frame_internal 函數讀取一幀編解碼數據,並將其添加到緩存中,調用 try_decode_frame 函數解碼一幀數據進一步填充流中的 AVCodecContext
  3. eof_reached = 1 表示前面死循環使用 read_frame_internal 函數讀取到流的末尾了,遍歷流,再次使用 has_codec_parameters 函數來檢查流內部解碼器上下文參數是否合理,不合理則再重複上面 2 中的步驟來進行解碼器上下文相關參數的初始化。
  4. 解碼的過程就是一個不斷放數據和取數據的過程,分別對應的是 avcodec_send_packetavcodec_receive_frame 函數,為了避免解碼器數據殘留,這裡通過空的 AVPacket 來 flushing 解碼器,執行的條件是 flush_codecs = 1 的時候,也就是需要需要上面 2 中調用的 try_decode_frame 執行了解碼操作。
  5. 後續就是一些碼流信息的計算,比如 pix_fmt、橫縱比 SAR、實際幀率、平均幀率等等。
  6. 遍歷流,調用 avcodec_parameters_from_context 函數將之前填充的流的內部 AVCodecContext 中的解碼器參數填充到流的解碼器參數 st->codecpar中,對應結構體 AVCodecParameters,到此 avformat_find_stream_info 函數主要流程分析完畢。

avformat_seek_file#

avformat_seek_file 主要用來執行 seek 操作的,其定義簡化如下:

int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts,
                       int64_t ts, int64_t max_ts, int flags){
    // ...                   
    
    // 優先使用read_seek2
    if (s->iformat->read_seek2) {
        int ret;
        ff_read_frame_flush(s);
        ret = s->iformat->read_seek2(s, stream_index, min_ts, ts, max_ts, flags);
        if (ret >= 0)
            ret = avformat_queue_attached_pictures(s);
        return ret;
    }
    // ...
    
    // 如果不支持read_seek2則嘗試使用舊版API seek
    if (s->iformat->read_seek || 1) {
        // ...
        int ret = av_seek_frame(s, stream_index, ts, flags | dir);
        return ret;
    }
    return -1; //unreachable                           
}

可知 avformat_seek_file 函數執行時。如果當前解復用器 (AVInputFormat) 支持 read_seek2,則使用對應的 read_seek2 函數,否則調用舊版 API 裡面的 av_seek_frame 函數進行 seek,av_seek_frame 函數如下:

int av_seek_frame(AVFormatContext *s, int stream_index,int64_t timestamp, int flags){
    int ret;
    if (s->iformat->read_seek2 && !s->iformat->read_seek) {
        // ...
        return avformat_seek_file(s, stream_index, min_ts, timestamp, max_ts,
                                  flags & ~AVSEEK_FLAG_BACKWARD);
    }

    ret = seek_frame_internal(s, stream_index, timestamp, flags);

    // ...
    return ret;
}

可知如果當前 AVInputFormat 支持 read_seek2 且不支持 read_seek 則使用 avformat_seek_file 函數也就是 read_seek2 函數進行 seek,如果支持 read_seek 則優先調用內部 seek 函數 seek_frame_internal 進行 seek,seek_frame_internal 函數主要提供尋幀的幾種方式:

  1. seek_frame_byte:按照字節方式尋幀
  2. read_seek:按照當前指定格式的方式尋幀,具體由該格式對應的解復用器提供支持。
  3. ff_seek_frame_binary:按照二分查找的方式尋幀。
  4. seek_frame_generic:按照通用方式尋幀。

這也是 seek 操作的邏輯,比如 hls 格式的解復用器就不支持 read_seek2,僅支持 read_seek, ff_hls_demuxer 定義如下:

AVInputFormat ff_hls_demuxer = {
    // ...
    .read_seek      = hls_read_seek,
};

av_dump_format#

av_dump_format 函數用來根據當前 AVFormatContext 來打印碼流輸入格式的詳細信息,直接看 IjkPlayer 正常播放視頻打印信息如下:

IJKMEDIA: Input #0, hls,applehttp, from 'http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8':
IJKMEDIA:   Duration:
IJKMEDIA: 00:30:00.00
IJKMEDIA: , start:
IJKMEDIA: 19.888800
IJKMEDIA: , bitrate:
IJKMEDIA: 0 kb/s
IJKMEDIA:
IJKMEDIA:   Program 0
IJKMEDIA:     Metadata:
IJKMEDIA:       variant_bitrate :
IJKMEDIA: 0
IJKMEDIA:
IJKMEDIA:     Stream #0:0
IJKMEDIA: , 23, 1/90000
IJKMEDIA: : Video: h264, 1 reference frame ([27][0][0][0] / 0x001B), yuv420p(tv, smpte170m/smpte170m/bt709, topleft), 400x300 (400x304), 0/1
IJKMEDIA: ,
IJKMEDIA: 29.92 tbr,
IJKMEDIA: 90k tbn,
IJKMEDIA: 180k tbc
IJKMEDIA:
IJKMEDIA:     Metadata:
IJKMEDIA:       variant_bitrate :
IJKMEDIA: FFP_MSG_FIND_STREAM_INFO:
IJKMEDIA: 0
IJKMEDIA:
IJKMEDIA:     Stream #0:1
IJKMEDIA: , 9, 1/90000
IJKMEDIA: : Audio: aac ([15][0][0][0] / 0x000F), 22050 Hz, stereo, fltp
IJKMEDIA:
IJKMEDIA:     Metadata:
IJKMEDIA:       variant_bitrate :
IJKMEDIA: 0

av_find_best_stream#

av_find_best_stream 函數主要用來選擇最合適的音視頻流,其定義簡化如下:

int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type,
                        int wanted_stream_nb, int related_stream,
                        AVCodec **decoder_ret, int flags){
    // ...
    
    // 遍歷選擇合適的音視頻流
    for (i = 0; i < nb_streams; i++) {
        int real_stream_index = program ? program[i] : i;
        AVStream *st          = ic->streams[real_stream_index];
        AVCodecParameters *par = st->codecpar;
        if (par->codec_type != type)
            continue;
        if (wanted_stream_nb >= 0 && real_stream_index != wanted_stream_nb)
            continue;
        if (type == AVMEDIA_TYPE_AUDIO && !(par->channels && par->sample_rate))
            continue;
        if (decoder_ret) {
            decoder = find_decoder(ic, st, par->codec_id);
            if (!decoder) {
                if (ret < 0)
                    ret = AVERROR_DECODER_NOT_FOUND;
                continue;
            }
        }
        disposition = !(st->disposition & (AV_DISPOSITION_HEARING_IMPAIRED | AV_DISPOSITION_VISUAL_IMPAIRED));
        count = st->codec_info_nb_frames;
        bitrate = par->bit_rate;
        multiframe = FFMIN(5, count);
        if ((best_disposition >  disposition) ||
            (best_disposition == disposition && best_multiframe >  multiframe) ||
            (best_disposition == disposition && best_multiframe == multiframe && best_bitrate >  bitrate) ||
            (best_disposition == disposition && best_multiframe == multiframe && best_bitrate == bitrate && best_count >= count))
            continue;
        best_disposition = disposition;
        best_count   = count;
        best_bitrate = bitrate;
        best_multiframe = multiframe;
        ret          = real_stream_index;
        best_decoder = decoder;
        // ...
    }
    // ...
    return ret;
} 

可知 av_find_best_stream 函數主要從三個維度進行選擇,比較順序依次是 dispositionmultiframebitrate,在 disposition 相同的時候選擇已解碼幀數多的,對應 multiframe,最後選擇比特率高的,對應 bitrate

disposition 對應的是 AVStreamdisposition 成員,具體值是 AV_DISPOSITION_ 標識符,比如上面的 AV_DISPOSITION_HEARING_IMPAIRED 表示該流是面向聽障人群的,這個暫時了解一下。

對應 read_thread 函數中 av_find_best_stream 找到了最佳的音頻、視頻、字幕流,接下來就是解碼播放了。

stream_component_open#

stream_component_open 函數主要是創建音頻渲染線程,音頻、視頻、字幕解碼線程以及初始化 VideoState,其定義簡化如下:

static int stream_component_open(FFPlayer *ffp, int stream_index){
    // ...
    // 1. 初始化AVCodecContext
    avctx = avcodec_alloc_context3(NULL);
    if (!avctx)
        return AVERROR(ENOMEM);

    // 2. 使用流的解碼器參數更新當前AVCodecContext對應參數
    ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
    if (ret < 0)
        goto fail;
    av_codec_set_pkt_timebase(avctx, ic->streams[stream_index]->time_base);

    // 3. 根據解碼器ID查找解碼器
    codec = avcodec_find_decoder(avctx->codec_id);

    // ...

    // 4. 如果已經指定了解碼器名稱則使用解碼器的名字再查找一次解碼器
    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;
    }

    // ...
    
    // 5. 音頻渲染線程創建,音視、視頻、字幕解碼器初始化,音視、視頻、字幕開始解碼
    switch (avctx->codec_type) {
    case AVMEDIA_TYPE_AUDIO:
        // ...
        // 打開音頻輸出,創建音頻輸出線程ff_aout_android,對應的音頻線程函數aout_thread,
        // 最終調用AudioTrack的write方法寫音頻數據
        // ...
        // 音頻解碼器初始化
        decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
        if ((is->ic->iformat->flags & (AVFMT_NOBINSEARCH | AVFMT_NOGENSEARCH | AVFMT_NO_BYTE_SEEK)) && !is->ic->iformat->read_seek) {
            is->auddec.start_pts = is->audio_st->start_time;
            is->auddec.start_pts_tb = is->audio_st->time_base;
        }
        // 開始音頻解碼,這裡創建音頻解碼線程 ff_audio_dec,對應的音頻解碼線程函數為audio_thread
        if ((ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec")) < 0)
            goto out;
        SDL_AoutPauseAudio(ffp->aout, 0);
        break;
    case AVMEDIA_TYPE_VIDEO:
        is->video_stream = stream_index;
        is->video_st = ic->streams[stream_index];
        // 異步初始化解碼器,和使用MediaCodec有關
        if (ffp->async_init_decoder) {
            // ...
        } else {
            // 視頻解碼器初始化
            decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
            ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
            if (!ffp->node_vdec)
                goto fail;
        }
            // 開始視頻解碼,這裡創建音頻解碼線程 ff_video_dec,對應的音頻解碼線程函數為video_thread
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto out;

        // ...

        break;
    case AVMEDIA_TYPE_SUBTITLE:
        // ...
        // 字幕解碼器初始化
        decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread);
        // 開始視頻解碼,這裡創建音頻解碼線程 ff_subtitle_dec,對應的音頻解碼線程函數為subtitle_thread
        if ((ret = decoder_start(&is->subdec, subtitle_thread, ffp, "ff_subtitle_dec")) < 0)
            goto out;
        break;
    default:
        break;
    }
    goto out;

fail:
    avcodec_free_context(&avctx);
out:
    av_dict_free(&opts);

    return ret;
}

可知 stream_component_open 函數已經來是了創建了對應的解碼線程了,上述代碼註釋比較詳細,這裡不再贅述,在對應 read_thread 中,該函數之後填充了 IjkMediaMeta 的一些數據,此時 ffp->prepared = true; 並向應用層發送播放準備完成的事件消息 FFP_MSG_PREPARED,最終回調給 OnPreparedListener 中。

read_thread 主循環#

這裡的主循環是指 read_thread 中讀取數據的主循環,關鍵流程如下:

for (;;) {
    // 1. 流關閉或者應用層release的時候is->abort_request為1
    if (is->abort_request)
        break;
    // ...
    // 2. 處理seek操作
    if (is->seek_req) {
        // ...
        is->seek_req = 0;
        ffp_notify_msg3(ffp, FFP_MSG_SEEK_COMPLETE, (int)fftime_to_milliseconds(seek_target), ret);
        ffp_toggle_buffering(ffp, 1);
    }
    // 3. 處理attached_pic
    // 如果⼀個流中含有AV_DISPOSITION_ATTACHED_PIC說明這個流是*.mp3等 ⽂件中的⼀個Video Stream
    // 該流只有⼀個AVPacket就是attached_pic
    if (is->queue_attachments_req) {
        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;
    }
    // 4. 如果隊列滿了,暫時不能讀取更多數據
    // 如果是網絡流ffp->infinite_buffer為1
    /* if the queue are full, no need to read more */
    if (ffp->infinite_buffer<1 && !is->seek_req &&
        // ...
        SDL_LockMutex(wait_mutex);
        // 等待10ms讓解碼線程有時間消耗
        SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
        SDL_UnlockMutex(wait_mutex);
        continue;
    }
    // 5. 檢查碼流是否播放完成
    if ((!is->paused || completed) &&
        (!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
        (!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
        // 是否設置循環播放
        if (ffp->loop != 1 && (!ffp->loop || --ffp->loop)) {
            stream_seek(is, ffp->start_time != AV_NOPTS_VALUE ? ffp->start_time : 0, 0, 0);
        } else if (ffp->autoexit) {// 是否自動退出
            ret = AVERROR_EOF;
            goto fail;
        } else {
            // ...
            
            // 播放出錯...
            ffp_notify_msg1(ffp, FFP_MSG_ERROR);
            
            // 播放完成...
            ffp_notify_msg1(ffp, FFP_MSG_COMPLETED);
        }
    }
    pkt->flags = 0;
    // 6. 讀取數據包
    ret = av_read_frame(ic, pkt);
    // 7. 檢測數據讀取情況
    if (ret < 0) {
        // ...
        
        // 讀取到末尾處理...
        if (pb_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 (pb_error) {
            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;
            ffp->error = pb_error;
            av_log(ffp, AV_LOG_ERROR, "av_read_frame error: %s\n", ffp_get_error_string(ffp->error));
            // break;
        } else {
            ffp->error = 0;
        }
        if (is->eof) {
            ffp_toggle_buffering(ffp, 0);
            SDL_Delay(100);
        }
        SDL_LockMutex(wait_mutex);
        SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
        SDL_UnlockMutex(wait_mutex);
        ffp_statistic_l(ffp);
        continue;
    } else {
        is->eof = 0;
    }
    // ...
    // 8. 填充未解碼的幀隊列
    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);
    }
    // ...
}

可知 read_thread 中的死循環主要用來進行數據讀取的,每次讀取的一幀壓縮編碼數據都添加到未解碼的幀隊列中,具體如下:

  1. 如果打開流失敗等會調用 stream_close 函數或者應用層調用函數 release 釋放播放器直接 break
  2. 處理播放過程中的 seek 操作。
  3. 處理流中的 attached_pic,如果⼀個流中含有 AV_DISPOSITION_ATTACHED_PIC 說明這個流是 *.mp3 等⽂件中的⼀個 Video Stream,該流只有⼀個 AVPacket 就是 attached_pic
  4. 處理隊列滿了的情況,一是當未解碼的隊列,即未解碼的音頻、視頻、字幕對應的隊列大小之和超過 15M,二是使用 stream_has_enough_packets 判斷音頻、視頻、字幕流是否已經有了足夠的待解碼的 AVPacket,大於則相當於解復用器緩存滿了,延遲 10ms 供解碼器消耗數據。
  5. 檢查碼流是否播放完、是否設置循環播放、是否自動退出以及播放出錯的處理等。
  6. av_read_frame 可以說是 read_thread 線程的關鍵函數,其作用就是解復用,每次讀取的一幀壓縮編碼數據都添加到未解碼的幀隊列中供相應的解碼線程使用。
  7. 檢測數據讀取情況,主要是數據讀取到流末尾的處理和數據讀取出錯的處理。
  8. 如果 av_read_frame 數據讀取成功則將其添加到對應的未解碼的幀隊列中。

到此 IjkPlayer 的數據讀取線程 read_thread 線程基本梳理完畢,其實大多是還是 ffmpeg 的東西。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。