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 的东西。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。