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);
// オプションを設定
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 の作成#

IjkMediaPlayerprepareAsyncメソッドから見て、その呼び出しの流れは以下の通りです:

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

    /* ビデオ表示を開始 */
    // デコード後のフレームキューを初期化
    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;

    // 条件変数(セマフォ)を初期化します。読み取りスレッド、ビデオシーク、オーディオシークに関連するセマフォを含みます。
    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();
    // 精密シーク用ミューテックス
    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. コーデックストリームを開いてヘッダー情報を取得します。
    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. 指定された再生開始時間がある場合は、その位置にシークします。
    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関数は主にコーデックストリームを開いてヘッダー情報を取得するために使用され、その定義は以下のように簡略化されます:

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関数は主にコーデックストリームを開いてストリーム入力フォーマットを探査し、プロトコルのホワイトリストとブラックリストのチェック、ファイルヘッダー情報の読み取りなどを行います。最後にupdate_stream_avctx関数を使用して、AVStreamのデコーダに関連する情報を対応するAVCodecContextに更新します。この操作は後続の流れで頻繁に見られます。

最も重要なのは、コーデックストリームを開いてストリーム入力フォーマットを探査し、ファイルヘッダー情報を読み取ることです。それぞれinit_inputread_header関数が呼び出され、read_headerはヘッダー情報を読み取る過程でAVStreamの初期化を完了します。

init_input関数は主にコーデックストリームフォーマットを探査し、そのコーデックストリームフォーマットのスコアを返します。最終的にそのコーデックストリームフォーマットに対応する最適なAVInputFormatを見つけます。この構造体は初期化時に登録されたデマルチプレクサに対応しており、各デマルチプレクサはAVInputFormatオブジェクトに対応しています。同様に、マルチプレクサはAVOutputFormatに対応しています。ここでは一応理解しておきます。

init_input関数が成功すると、対応するコーデックストリームフォーマットが決定され、この時点でread_header関数を呼び出すことができます。これは現在のコーデックストリームフォーマットAVInputFormatに対応するデマルチプレクサの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関数は主にコーデックストリーム情報を取得するために使用され、ヘッダーのないファイルフォーマットを探査するのに非常に役立ちます。この関数を使用して、ビデオの幅、高さ、総時間、ビットレート、フレームレート、ピクセルフォーマットなどを取得できます。その定義は以下のように簡略化されます:

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に設定している場合、現在のコーデックストリームにはヘッダー情報がないことを示します。
            // この場合、いくつかのデータパケットを読み取ってストリーム情報を取得する必要があります。逆に、直接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;
        }
         
        // 以下は、現在のコーデックストリームにヘッダー情報がない場合の処理です。
         
        // 1フレームの圧縮コーディングデータを読み取ります。
        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. デコーダがフラッシング操作を実行し、バッファに残っているデータが取り出されないようにします。
    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];
            /* デコーダをフラッシュします */
            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関数を使用してコーデックストリーム内部のデコーダコンテキストパラメータが合理的かどうかを確認します。合理的であり、現在のコーデックストリームにヘッダー情報がある場合、analyzed_all_streams = 1;およびflush_codecs = 0;を設定し、直接breakしてこの無限ループを終了します。現在のコーデックストリームにヘッダー情報がない場合、すなわちic->ctx_flagsAVFMTCTX_NOHEADERに設定されている場合、read_frame_internal関数を呼び出して 1 フレームの圧縮コーディングデータを読み取り、それをバッファに追加し、try_decode_frame関数を呼び出して 1 フレームのデータをデコードし、ストリーム内のAVCodecContextをさらに充填します。
  3. eof_reached = 1は、前の無限ループでread_frame_internal関数がストリームの末尾に達したことを示します。ストリームを反復処理し、再度has_codec_parameters関数を使用してストリーム内部のデコーダコンテキストパラメータが合理的かどうかを確認します。合理的でない場合は、上記の 2 の手順を繰り返してデコーダコンテキスト関連パラメータの初期化を行います。
  4. デコードのプロセスは、データを放送し、データを取得するプロセスの繰り返しです。これはそれぞれavcodec_send_packetavcodec_receive_frame関数に対応します。デコーダデータの残留を避けるために、ここでは空のAVPacketを使用してデコーダをフラッシュします。実行条件は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は主にシーク操作を実行するために使用され、その定義は以下のように簡略化されます:

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のシークを試みます。
    if (s->iformat->read_seek || 1) {
        // ...
        int ret = av_seek_frame(s, stream_index, ts, flags | dir);
        return ret;
    }
    return -1; //到達不能                           
}

avformat_seek_file関数が実行されると、現在のデマルチプレクサ (AVInputFormat) がread_seek2をサポートしている場合は、対応するread_seek2関数を使用します。そうでない場合は、古い API のav_seek_frame関数を呼び出してシークを行います。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;
}

現在のAVInputFormatread_seek2をサポートしており、read_seekをサポートしていない場合は、avformat_seek_file関数、すなわちread_seek2関数を使用してシークします。read_seekをサポートしている場合は、優先的に内部シーク関数seek_frame_internalを呼び出してシークを行います。seek_frame_internal関数は主にフレームをシークするいくつかの方法を提供します:

  1. seek_frame_byte:バイト方式でフレームをシークします。
  2. read_seek:現在指定されたフォーマットの方式でフレームをシークします。具体的には、そのフォーマットに対応するデマルチプレクサがサポートします。
  3. ff_seek_frame_binary:二分探索方式でフレームをシークします。
  4. seek_frame_generic:一般的な方式でフレームをシークします。

これがシーク操作のロジックです。例えば、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関数は主に 3 つの次元から選択を行い、比較の順序はdispositionmultiframebitrateです。dispositionが同じ場合は、デコード済みフレーム数の多い方を選択します。これに対応するのがmultiframeで、最後にビットレートの高い方を選択します。これに対応するのがbitrateです。

dispositionAVStreamdispositionメンバーに対応し、具体的な値は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;
    }

    // ...
    
    // 音声レンダリングスレッドを作成し、音声、映像、字幕デコーダを初期化し、音声、映像、字幕のデコードを開始します。
    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. シーク操作を処理します。
    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などのファイル内の1つのVideo Streamです。
    // このストリームには1つの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 フレームの圧縮コーディングデータは、未デコードのフレームキューに追加されます。具体的には以下の通りです:

  1. ストリームを開くのに失敗した場合など、stream_close関数を呼び出すか、アプリケーション層がrelease関数を呼び出してプレーヤーを解放すると、直接breakします。
  2. 再生中のシーク操作を処理します。
  3. ストリーム内のattached_picを処理します。ストリームにAV_DISPOSITION_ATTACHED_PICが含まれている場合、このストリームは *.mp3 などのファイル内の 1 つのVideo Streamであり、このストリームには 1 つのAVPacketだけがあり、それがattached_picです。
  4. キューが満杯の場合、未デコードのキュー、すなわち未デコードの音声、映像、字幕に対応するキューのサイズの合計が 15M を超えた場合、またはstream_has_enough_packetsを使用して音声、映像、字幕ストリームが十分な待機デコードAVPacketを持っているかどうかを判断します。これが超えた場合、デマルチプレクサのバッファが満杯になったことを意味し、デコーダがデータを消費するために 10ms 遅延します。
  5. コーデックストリームが再生完了したかどうかを確認し、ループ再生が設定されているか、自動終了か、再生エラーの処理などを行います。
  6. av_read_frameread_threadスレッドの重要な関数であり、その役割はデマルチプレクシングです。毎回読み取った 1 フレームの圧縮コーディングデータは、対応するデコードスレッドで使用するために未デコードのフレームキューに追加されます。
  7. データ読み取り状況を確認します。主にデータがストリームの末尾に達した場合の処理とデータ読み取りエラーの処理を行います。
  8. av_read_frameでデータ読み取りが成功した場合、そのデータを対応する未デコードのフレームキューに追加します。

これで IjkPlayer のデータ読み取りスレッドread_threadスレッドの基本的な流れが整理されました。実際には、ほとんどが FFmpeg の内容です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。