PS: 新技術の影響を制御し、制御されないようにしましょう。
この記事では、IjkPlayer のデータ読み取りスレッドread_thread
を分析し、その基本的な流れと重要な関数の呼び出しを明確にすることを目的としています。主な内容は以下の通りです:
- IjkPlayer の基本的な使用
- read_thread の作成
- avformat_alloc_context
- avformat_open_input
- avformat_find_stream_info
- avformat_seek_file
- av_dump_format
- av_find_best_stream
- stream_component_open
- 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 の作成#
IjkMediaPlayer
のprepareAsync
メソッドから見て、その呼び出しの流れは以下の通りです:
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
関数は主に以下のことを行います:
VideoState
および一部のパラメータを初期化します。- フレームキューを初期化します。デコード済みのビデオフレームキュー
pictq
、オーディオフレームキューsampq
、字幕フレームキューsubpq
、未デコードのビデオデータキューvideoq
、オーディオデータキューaudioq
、字幕データキューsubtitleq
を初期化します。 - 音声と映像の同期方式およびクロックを初期化します。デフォルトは
AV_SYNC_AUDIO_MASTER
で、音声クロックが主クロックとなります。 - 音量の初期範囲を設定します。
- スレッド名
ff_vout
のビデオレンダリングスレッドvideo_refresh_thread
を作成します。 - スレッド名
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_default
とio_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_input
とread_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
が成功したことを示します。主な流れは以下の通りです:
- ストリームを反復処理し、ストリーム内のいくつかのパラメータに基づいて
AVCodecParser
とAVCodecParserContext
を初期化します。find_probe_decoder
関数を使用してデコーダを探査し、avcodec_open2
関数を使用してAVCodecContext
を初期化し、デコーダのinit
関数を呼び出してデコーダの静的データを初期化します。 for (;;)
の無限ループは、ff_check_interrupt
関数を使用して中断を検出し、ストリームを反復処理してhas_codec_parameters
関数を使用してコーデックストリーム内部のデコーダコンテキストパラメータが合理的かどうかを確認します。合理的であり、現在のコーデックストリームにヘッダー情報がある場合、analyzed_all_streams = 1;
およびflush_codecs = 0;
を設定し、直接break
してこの無限ループを終了します。現在のコーデックストリームにヘッダー情報がない場合、すなわちic->ctx_flags
がAVFMTCTX_NOHEADER
に設定されている場合、read_frame_internal
関数を呼び出して 1 フレームの圧縮コーディングデータを読み取り、それをバッファに追加し、try_decode_frame
関数を呼び出して 1 フレームのデータをデコードし、ストリーム内のAVCodecContext
をさらに充填します。eof_reached = 1
は、前の無限ループでread_frame_internal
関数がストリームの末尾に達したことを示します。ストリームを反復処理し、再度has_codec_parameters
関数を使用してストリーム内部のデコーダコンテキストパラメータが合理的かどうかを確認します。合理的でない場合は、上記の 2 の手順を繰り返してデコーダコンテキスト関連パラメータの初期化を行います。- デコードのプロセスは、データを放送し、データを取得するプロセスの繰り返しです。これはそれぞれ
avcodec_send_packet
とavcodec_receive_frame
関数に対応します。デコーダデータの残留を避けるために、ここでは空のAVPacket
を使用してデコーダをフラッシュします。実行条件はflush_codecs = 1
のときです。これは、上記の 2 でtry_decode_frame
を呼び出してデコード操作を実行したときです。 - 後続の処理は、コーデックストリーム情報の計算を行います。例えば pix_fmt、アスペクト比 SAR、実際のフレームレート、平均フレームレートなど。
- ストリームを反復処理し、
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;
}
現在のAVInputFormat
がread_seek2
をサポートしており、read_seek
をサポートしていない場合は、avformat_seek_file
関数、すなわちread_seek2
関数を使用してシークします。read_seek
をサポートしている場合は、優先的に内部シーク関数seek_frame_internal
を呼び出してシークを行います。seek_frame_internal
関数は主にフレームをシークするいくつかの方法を提供します:
seek_frame_byte
:バイト方式でフレームをシークします。read_seek
:現在指定されたフォーマットの方式でフレームをシークします。具体的には、そのフォーマットに対応するデマルチプレクサがサポートします。ff_seek_frame_binary
:二分探索方式でフレームをシークします。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 つの次元から選択を行い、比較の順序はdisposition
、multiframe
、bitrate
です。disposition
が同じ場合は、デコード済みフレーム数の多い方を選択します。これに対応するのがmultiframe
で、最後にビットレートの高い方を選択します。これに対応するのがbitrate
です。
disposition
はAVStream
のdisposition
メンバーに対応し、具体的な値は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(©, &is->video_st->attached_pic)) < 0)
goto fail;
packet_queue_put(&is->videoq, ©);
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
します。 - 再生中のシーク操作を処理します。
- ストリーム内の
attached_pic
を処理します。ストリームにAV_DISPOSITION_ATTACHED_PIC
が含まれている場合、このストリームは *.mp3 などのファイル内の 1 つのVideo Stream
であり、このストリームには 1 つのAVPacket
だけがあり、それがattached_pic
です。 - キューが満杯の場合、未デコードのキュー、すなわち未デコードの音声、映像、字幕に対応するキューのサイズの合計が 15M を超えた場合、または
stream_has_enough_packets
を使用して音声、映像、字幕ストリームが十分な待機デコードAVPacket
を持っているかどうかを判断します。これが超えた場合、デマルチプレクサのバッファが満杯になったことを意味し、デコーダがデータを消費するために 10ms 遅延します。 - コーデックストリームが再生完了したかどうかを確認し、ループ再生が設定されているか、自動終了か、再生エラーの処理などを行います。
av_read_frame
はread_thread
スレッドの重要な関数であり、その役割はデマルチプレクシングです。毎回読み取った 1 フレームの圧縮コーディングデータは、対応するデコードスレッドで使用するために未デコードのフレームキューに追加されます。- データ読み取り状況を確認します。主にデータがストリームの末尾に達した場合の処理とデータ読み取りエラーの処理を行います。
av_read_frame
でデータ読み取りが成功した場合、そのデータを対応する未デコードのフレームキューに追加します。
これで IjkPlayer のデータ読み取りスレッドread_thread
スレッドの基本的な流れが整理されました。実際には、ほとんどが FFmpeg の内容です。