ffmpeg实战教程(十三)iJKPlayer源码简析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/King1425/article/details/74030026

要使用封装优化ijk就必须先了解ffmpeg,然后看ijk对ffmpeg的C层封装!
这是我看ijk源码时候的笔记,比较散乱。不喜勿喷~

ijk源码简析:

1.ijkplayer_jni.c 封装的播放器JNI层 API,对应java层调用。

2.ijkplayer.c 封装的播放器API,对应给JNI层调用

3.ff_ffplay_options.c 参数设置options

4.ijkplayer_android.c 创建播放器,对应Android系统的音视频

5..ff_ffplay.c ffmpeg主要封装类 含整个解码流程

首先入口是 JNI_OnLoad()加载.so库,各种初始化。RegisterNatives注册g_methods中的native方法,注册后,相当于java层声明为native方法的函数与c层有一个映射关系;举个栗子,当我们java里面调用_start()方法的时候,其实调用的是c层中的IjkMediaPlayer_start()方法;对应地,作者在java层声明了一个修饰java方法的annotation,这些函数也会在这里注册,实现后面在C层也能调用java层的函数。

接着还调用了几个init方法:
ffp_global_init() –>主要是ffmpeg的初始化工作, 前面说过ijkplayer是基于ffmpeg的。
FFmpegApi_global_init(env)–>初始化FFmpegApi#av_base64_encode函数。

后面调用的IjkMediaPlayer.native_profileBegin(“libijkplayer.so”);发现并没有什么作用的样子,注释了还是能成功运行。未解?

IjkMediaPlayer初始化过程:调用了native方法native_setup(new WeakReference(this));对应C语言的IjkMediaPlayer_native_setup()方法,也在ijkplayer_jni.c文件里面。

然后就是ijkmp_android_create(message_loop);/ijkplayer_android.c : 很重要
创建一个IJKMediaPlayer类型的player。
接下来就是对ijkplayer结构体中的ffplayer进行设置,刚刚通过SDL_VoutAndroid_CreateForAndroidSurface()创建了输出设备vout,后面在ffpipeline_set_vout()里面:mp->ffplayer->opaque->weak_vout = vout。
然后再jni_set_media_player(),这里基本就结束了。
SDL_VoutAndroid_CreateForAndroidSurface()创建视频输出设备的时候,跟踪函数跳转发现,硬解用的mediacidec,软解用的ffmpeg。还有渲染视频设备用的ANativeWindow,对接surface等。

prepareAsync()流程:IjkMediaPlayer_prepareAsync/ijkplayer_jni.c———->ijkmp_prepare_async_l()—–>ffp_prepare_async_l()—–>stream_open—->
video_refresh_thread为入口的视频显示线程——->read_thread为入口的网络数据或者本地文件的数据读取线程.
其实在read_thread里面还向msg_queue发送了一个消息,那就是ffp_notify_msg1(ffp, FFP_MSG_PREPARED);,消息循环线程收到这个消息后,会调用java层的函数,向handler发送个MEDIA_PREPARED消息,然后在handler的handleMessage函数里面会处理这个消息:

    case MEDIA_PREPARED:
                player.notifyOnPrepared();
                return;
//最终会调用:
videoView.setOnPreparedListener(new IMediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(IMediaPlayer mp) {
                videoView.start();
            }
        });

ijkplayer_jni.c message_loop_n消息循环线程
然后 post_event发送到ijkj4a文件夹中IjkMediaPlayer.c
在ijkj4a文件夹中IjkMediaPlayer.c中实现postEventFromNative 然后调用ijkj4a文件夹Java层postEventFromNative。然后ijkplayer-java的IjkMediaPlayer.java具体实现了postEventFromNative。
C到Java ——>(CallStaticVoidMethod)

IJK音频解码源码:

audio_thread()/ff_ffplayer.c
一开始就进入循环,然后调用decoder_decode_frame()进行解码,解码后的帧存放到frame中,然后调用frame_queue_peek_writable()判断是否能把刚刚解码的frame写入is->sampq中,因为is->sampq是音频解码帧列表,然而播放线程直接从这里面读取数据,然后播放出来。最后 av_frame_move_ref(af->frame, frame);把frame放入到sampq相应位置。由于前面af = frame_queue_peek_writable(&is->sampq),af就是指向这一帧frame应该放的位置的指针,所以直接把值赋值给它的结构体里面的frame就行了。
然后frame_queue_push(&is->sampq);里面是一个唤醒线程的操作,如查音频播放线程因为sampq队列为空而阻塞,这里可以唤醒它。
在decoder_decode_frame()里面是调用传进去的codec的codec->decode()方法解码。
在frame_queue_peek_writable()里面会判断sampq队列是否满了,如果没位置放我们的frame的话,会调用pthread_cond_wait()方法阻塞队列。如果有位置放frame的话,就会返回frame应该放置的位置的地址。

IJK音频播放源码:

ijkmp_android_create()/ijkplayer_android.c
在ffpipeline_create_from_android()里面有一句
pipeline->func_open_audio_output = func_open_audio_output;
接着我们看看func_open_audio_output()/ffpipeline_android.c:
可以看出,音频播放也分为:opensles,audiotrack

看看audiotrack吧.
aout_open_audio()/ijksdl_aout_android_audiotrack.c。
接着会调用aout_open_audio_n()/ijksdl_aout_android_audiotrack.c
SDL_CreateThreadEx(&opaque->_audio_tid, aout_thread, aout, “ff_aout_android”);这里创建的线程就是播放线程。
接着我们看看入口函数aout_thread,在这个函数内部会调用aout_thread_n()/ijksdl_aout_android_audiotrack.c:
这个函数的开始有很多SDL_Android_AudioTrack_set_xxx(),主要是设置播放器相关的配置,比如播放速度,声音大小等。

IJK视频解码源码:

SDL_CreateThreadEx(&is->_video_tid, video_thread, ffp, “ff_video_dec”);频解码线程的创建
然后在创建之后,有一个死循环:

for (;;) {
        if (is->abort_request)
            break;
        //ignore audio part
        ret = av_read_frame(ic, pkt);
        packet_queue_put(&is->videoq, pkt);   //将获取到的视频包推入videoq队列中
    }

这里循环把解析到的frame放入is->videoq中,那么我们在后面解码的时候,肯定是从这里面读取帧,然后再解码。
现在我们来看看解码线程:
ffp->node_vdec->func_run_sync(node);
是直接运行的ffplayer结构体里面的函数,这里肯定和音频播放一样,在之前初始化的时候给这个函数指针赋值过,那么我们回过头看看到底在哪里为它赋值的:
ijkmp_android_create()调用ffpipeline_create_from_android(),然后里面有一句:pipeline->func_open_video_decoder = func_open_video_decoder;
然后我们再返回看到在stream_component_open()/ff_ffplayer.c里面有一句:
decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
点进去ffpipeline_open_video_decoder(ffp->pipeline, ffp);:
IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
return pipeline->func_open_video_decoder(pipeline, ffp);
}
在stream_component_open()/ff_ffplayer.c里面其实运行了func_open_video_decoder(),然后把返回值赋值给ffp->node_vdec;继续跟着流程到func_open_video_decoder(),最终ffp->node_vdec =node。
前面调用的ffp->node_vdec->func_run_sync(node);现在看来就是调用的func_run_sync()函数
然后在视频解码流程里面又创建了一个线程
opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, “amediacodec_input_thread”);
点进去feed_input_buffer()/ffpipenode_android_mediacodec_vdec.c:
这个enqueue_thread_func也就只是把每一帧数据送到解码器去解码而已。

再看到之前解码线程,发现真正的解码是重新开一个线程解码,那么这个线程是干嘛的呢? 从硬解码器中获得解码后的数据。

IJK视频播放源码:

显示的线程非常简单,就是从is->pictq读取数据,然后直接推送给硬件设备,完成渲染。
入口函数video_refresh_thread()/ff_ffplayer.c开始
video_refresh_thread(void *arg)
|(调用)
video_refresh(ffp, &remaining_time);
|(调用)
video_display2(ffp);
|(调用)
video_image_display2(ffp);
SDL_VoutDisplayYUVOverlay()从pictq列表中获取视频frame数据,然后再写入nativewindows的视频缓冲中进行渲染。

猜你喜欢

转载自blog.csdn.net/King1425/article/details/74030026