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();
// ログレベルの設定
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);
// サーフェスの設定
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

    /* ビデオ表示の開始 */
    // デコード後のフレームキューの初期化
    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 に対応するデマルチプレクサ demuxerxxx_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];
            /* 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 関数を使用してコーデックストリーム内部のデコーダーコンテキストパラメータが合理的かどうかをチェックします。合理的であれば、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 に充填します。これで 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の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 の内部シーク関数 seek_frame_internal を呼び出してシークを行います。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. ストリームを開くのに失敗した場合や、アプリケーション層が release 関数を呼び出した場合は、stream_close 関数を呼び出し、直接 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 に関連する内容です。

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