背景
最近調研做視頻秒開,使用B站開源的ijkplayer作為播放器。ijkplayer基于ffmpeg的播放器。
ijkplayer使用
- 創建IjkMediaPlayer對象
- 通過setDataSource設置播放路徑
- 調用prepareAsync讓播放器開始工作
JNI_OnLoad
當ijkplayer.so被加載時,會回調到IjkPlayer_jni.c
中的JNI_OnLoad中,最主要調用ijkmp_global_init
初始化
- 啊
ijkPlayer播放流程
- 在IjkMediaPlayer的構造函數中,
- 會調用
loadLibrariesOnce
加載libijkffmpeg
,ijksdl
,ijkplayer
這三個so - 調用
native_init
打印了一行日志 - 初始化當前線程Looper所使用的Handler
- 如果在有Looper的子線程初始化的話,則會在該子線程進行消息循環
- 如果沒有Looper的子線程,則使用主線程進行消息循環
- 初始化一個Native層的IjkPlayer的引用
- 會調用
private void initPlayer(IjkLibLoader libLoader) {
loadLibrariesOnce(libLoader);
initNativeOnce();
Looper looper;
if ((looper = Looper.myLooper()) != null) {
mEventHandler = new EventHandler(this, looper);
} else if ((looper = Looper.getMainLooper()) != null) {
mEventHandler = new EventHandler(this, looper);
} else {
mEventHandler = null;
}
/*
* Native setup requires a weak reference to our object. It's easier to
* create it here than in C++.
*/
native_setup(new WeakReference<IjkMediaPlayer>(this));
}
- 調用
__setDataSource
將視頻URL傳入Native層 - 調用
__prepareAsync
告知Native層開始加載解碼 -
Ijkplayer_jni.c
是IjkMediaPlayer對應的C文件,其中setup完成以下事情:- 初始化Native的IjkMediaPlayer對象,在
ijkmp_create
函數中通過ffp_create
初始化FFPlayer
對象,并且將message_loop
賦予該對象
- 初始化Native的IjkMediaPlayer對象,在
static void IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
MPTRACE("%s\n", __func__);
// 創建Native層的IjkMediaPlayer對象,并且將message_loop對象賦與mp->msg_loop
IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
JNI_CHECK_GOTO(mp, env, "java/lang/OutOfMemoryError", "mpjni: native_setup: ijkmp_create() failed", LABEL_RETURN);
// 通過JNI將上步創建的IjkMediaPlayer對象的指針地址保存到Java層的mNativeMediaPlayer屬性中
// 并且釋放舊的IjkMediaPlayer
jni_set_media_player(env, thiz, mp);
// 重新生成IjkMediaPlayer的弱引用賦值mp->weak_thiz;
ijkmp_set_weak_thiz(mp, (*env)->NewGlobalRef(env, weak_this));
// 設置ffp的inject_opaque為上述弱引用
ijkmp_set_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
ijkmp_set_ijkio_inject_opaque(mp, ijkmp_get_weak_thiz(mp));
ijkmp_android_set_mediacodec_select_callback(mp, mediacodec_select_callback, ijkmp_get_weak_thiz(mp));
LABEL_RETURN:
ijkmp_dec_ref_p(&mp);
}
-
setDataSource
最終會調用到ijkmp_set_data_source_l
- 將原來的
mp->data_source
指針釋放 - 重新將url生成char *賦值給
mp->data_source
- 將ijkmediaplayer對象的狀態修改成
MP_STATE_INITIALIZED
- 將原來的
static int ijkmp_set_data_source_l(IjkMediaPlayer *mp, const char *url)
{
...
freep((void**)&mp->data_source);
mp->data_source = strdup(url);
if (!mp->data_source)
return EIJK_OUT_OF_MEMORY;
ijkmp_change_state_l(mp, MP_STATE_INITIALIZED);
return 0;
}
-
prepareAsync
最終也會調用到ijkmp_prepare_async_l
中- 將ijkmediaplayer的狀態修改為
MP_STATE_ASYNC_PREPARING
- 初始化消息隊列
&mp->ffplayer->msg_queue
- 初始化消息處理線程,線程處理function為
ijk_msg_loop
- 調用
ffp_prepare_async_l
調用ffmpeg中的方法開始prepare
- 將ijkmediaplayer的狀態修改為
static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
...
ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
// 向&mp->ffplayer->msg_queue中丟入一個FFP_MSG_FLUSH消息
// 最終目的只是初始化Native的MessageQueue
msg_queue_start(&mp->ffplayer->msg_queue);
// released in msg_loop
ijkmp_inc_ref(mp);
// 創建處理消息隊列的線程,
// 向Java層IjkMediaPlayer的postEventFromNative回調
// 該回調會回調到EventHandler中,而隊列中只有FFP_MSG_FLUSH這個消息,這只是一個NOP的測試消息
mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_msg_loop, mp, "ff_msg_loop");
// msg_thread is detached inside msg_loop
// TODO: 9 release weak_thiz if pthread_create() failed;
int retval = ffp_prepare_async_l(mp->ffplayer, mp->data_source);
if (retval < 0) {
ijkmp_change_state_l(mp, MP_STATE_ERROR);
return retval;
}
return 0;
}
-
ffp_prepare_async_l
中真正調用ffmpeg開始準備播放- 判斷url協議是否為
rtmp
或者rtsp
,如果是則取消timeout參數 - 如果url長度大于1024,則加入
ijklongurl
參數 - 調用
stream_open
打開視頻流,使用FFPlayer播放
- 判斷url協議是否為
int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name)
{
...
if (av_stristart(file_name, "rtmp", NULL) ||
av_stristart(file_name, "rtsp", NULL)) {
// There is total different meaning for 'timeout' option in rtmp
av_log(ffp, AV_LOG_WARNING, "remove 'timeout' option for rtmp.\n");
av_dict_set(&ffp->format_opts, "timeout", NULL, 0);
}
/* there is a length limit in avformat */
if (strlen(file_name) + 1 > 1024) {
av_log(ffp, AV_LOG_ERROR, "%s too long url\n", __func__);
if (avio_find_protocol_name("ijklongurl:")) {
av_dict_set(&ffp->format_opts, "ijklongurl-url", file_name, 0);
file_name = "ijklongurl:";
}
}
...
av_opt_set_dict(ffp, &ffp->player_opts);
if (!ffp->aout) {
ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);
if (!ffp->aout)
return -1;
}
#if CONFIG_AVFILTER
if (ffp->vfilter0) {
GROW_ARRAY(ffp->vfilters_list, ffp->nb_vfilters);
ffp->vfilters_list[ffp->nb_vfilters - 1] = ffp->vfilter0;
}
#endif
// 打開視頻流
VideoState *is = stream_open(ffp, file_name, NULL);
if (!is) {
av_log(NULL, AV_LOG_WARNING, "ffp_prepare_async_l: stream_open failed OOM");
return EIJK_OUT_OF_MEMORY;
}
ffp->is = is;
ffp->input_filename = av_strdup(file_name);
return 0;
}
-
stream_open
開始打開視頻流,其中VideoState也就代表著視頻當前的狀態,包括幀,數據,解碼器等等- 初始化幀隊列:
&is->pictq
,&is->subpq
,&is->sampq
,Queue大小為16 - 初始化數據包隊列:
&is->videoq
,&is->audioq
,&is->subtitleq
- 初始化時針:
&is->vidclk
,&is->audclk
,&is->extclk
,代表當前視頻video,audio的時刻 - 創建視頻刷新的線程
is->video_refresh_tid
- 創建視頻讀取線程
is->read_tid
- 調用
decoder_init
初始化視頻解碼器
- 初始化幀隊列:
static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
VideoState *is; // 視頻內容以及狀態
...
/* 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;
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;
...
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) {
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;
}
FFMpeg模塊分布
encode/decode模塊
- 用于音視頻的編碼和解碼,存放在libavcodec子目錄中
muxer/demuxer模塊
- 用于音頻和視頻的合并與分離(也稱混合器模塊),存放在libavformat目錄中
內存等常用模塊
- 存放于libavutil目錄中
總結
- IjkPlayer在Java層初始化主線程/當前線程的EventHandler用于處理從Native層回調的消息
- 在Native層初始化IjkMediaPlayer對象
- 將
message_loop
函數指針賦值,以指定Native層的消息 - 通過
ffp_create
創建FFPlayer對象 - 初始化IjkMediaPlayer中的Mutex,以及
ref_count
自增 - 創建SDK_Vout用于圖形渲染
- 根據平臺特性創建各平臺的IJKFF_Pipeline,PipeLine中包括了視頻解碼、音頻輸出等功能
- 將
- 將NativeMediaPlayer的指針地址賦值給Java層的
mNativeMediaPlayer
- 當調用
prepareAsync
時,Native層會從Java層獲取之前保存的mNativeMediaPlayer
指針地址來獲取Native層的IjkMediaPlayer - 初始化
&mp->ffplayer->msg_queue
- 啟動消息線程
&mp->_msg_thread
以及消息循環函數ijkmp_msg_loop
參考資料
ijkplayer框架深入剖析
在線短視頻秒播優化之視頻文件格式之MP4文件Moov box的位置
FFmpeg學習1:視頻解碼
MP4文件格式的解析,以及MP4文件的分割算法
ijkplayer整理筆記(三)——AVFormatContext類圖詳解類圖詳解