写在前面
前几篇文章大概分析到了数据的读取,接下来就该解码和播放了。那么ijkplayer解码和播放又是怎么做的呢?
解码线程
从上一篇文章我们可以看到,ijkplayer的音频解码线程的入口函数是audio_thread()
,那么我们就跟踪到audio_thread()/ff_ffplayer.c
函数里面:
static int audio_thread(void *arg)
{
//...
do {
ffp_audio_statistic_l(ffp);
if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)
goto the_end;
//...
while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0) {
//...
if (!(af = frame_queue_peek_writable(&is->sampq)))
goto the_end;
//...
av_frame_move_ref(af->frame, frame);
frame_queue_push(&is->sampq);
//...
}
} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
the_end:
//...
av_frame_free(&frame);
return ret;
}
从上面代码可以看出,一开始就进入循环,然后调用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应该放置的位置的地址。
解码线程大概就结束了。
播放流程
之前在初始化的时候,有个地方还没分析到,那就是在ijkmp_android_create()/ijkplayer_android.c
里面:
IjkMediaPlayer *ijkmp_android_create(int(*msg_loop)(void*))
{
IjkMediaPlayer *mp = ijkmp_create(msg_loop);
mp->ffplayer->vout = SDL_VoutAndroid_CreateForAndroidSurface();
if (!mp->ffplayer->vout)
goto fail;
mp->ffplayer->pipeline = ffpipeline_create_from_android(mp->ffplayer);
ffpipeline_set_vout(mp->ffplayer->pipeline, mp->ffplayer->vout);
return mp;
}
在ffpipeline_create_from_android()
里面有一句
pipeline->func_open_audio_output = func_open_audio_output;
接着我们看看func_open_audio_output()/ffpipeline_android.c
:
static SDL_Aout *func_open_audio_output(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
SDL_Aout *aout = NULL;
if (ffp->opensles) {
aout = SDL_AoutAndroid_CreateForOpenSLES();
} else {
aout = SDL_AoutAndroid_CreateForAudioTrack();
}
if (aout)
SDL_AoutSetStereoVolume(aout, pipeline->opaque->left_volume, pipeline->opaque->right_volume);
return aout;
}
从上面可以看出,音频播放也分为:opensles,audiotrack
,然后我们就来看看audiotrack
吧:
SDL_Aout *SDL_AoutAndroid_CreateForAudioTrack()
{
SDL_Aout *aout = SDL_Aout_CreateInternal(sizeof(SDL_Aout_Opaque));
if (!aout)
return NULL;
SDL_Aout_Opaque *opaque = aout->opaque;
opaque->wakeup_cond = SDL_CreateCond();
opaque->wakeup_mutex = SDL_CreateMutex();
opaque->speed = 1.0f;
aout->opaque_class = &g_audiotrack_class;
aout->free_l = aout_free_l;
aout->open_audio = aout_open_audio;
aout->pause_audio = aout_pause_audio;
aout->flush_audio = aout_flush_audio;
aout->set_volume = aout_set_volume;
aout->close_audio = aout_close_audio;
aout->func_get_audio_session_id = aout_get_audio_session_id;
aout->func_set_playback_rate = func_set_playback_rate;
return aout;
}
我们要分析音频播放对吧?在前面数据读取线程我们分析到了在stream_component_open()
里面会调用aout->open_audio(aout, pause_on);
,相信大家还记得吧?我们现在看看在初始化的时候:
aout->open_audio = aout_open_audio;
所以在前面stream_component_open()
里面相当于直接调用了aout_open_audio()/ijksdl_aout_android_audiotrack.c
。当然要是我们在播放器里面用的opensles
,程序流程是差不多的,大家有兴趣的话可以下去看看。
上面接着会调用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
:
static int aout_thread_n(JNIEnv *env, SDL_Aout *aout)
{
SDL_Aout_Opaque *opaque = aout->opaque;
SDL_Android_AudioTrack *atrack = opaque->atrack;
SDL_AudioCallback audio_cblk = opaque->spec.callback;
void *userdata = opaque->spec.userdata;
uint8_t *buffer = opaque->buffer;
//...
if (!opaque->abort_request && !opaque->pause_on)
SDL_Android_AudioTrack_play(env, atrack);
while (!opaque->abort_request) {
SDL_LockMutex(opaque->wakeup_mutex);
if (!opaque->abort_request && opaque->pause_on) {
SDL_Android_AudioTrack_pause(env, atrack);
while (!opaque->abort_request && opaque->pause_on) {
SDL_CondWaitTimeout(opaque->wakeup_cond, opaque->wakeup_mutex, 1000);
}
if (!opaque->abort_request && !opaque->pause_on)
SDL_Android_AudioTrack_play(env, atrack);
}
if (opaque->need_flush) {
opaque->need_flush = 0;
SDL_Android_AudioTrack_flush(env, atrack);
}
if (opaque->need_set_volume) {
opaque->need_set_volume = 0;
SDL_Android_AudioTrack_set_volume(env, atrack, opaque->left_volume, opaque->right_volume);
}
if (opaque->speed_changed) {
opaque->speed_changed = 0;
if (J4A_GetSystemAndroidApiLevel(env) >= 23) {
SDL_Android_AudioTrack_setSpeed(env, atrack, opaque->speed);
}
}
SDL_UnlockMutex(opaque->wakeup_mutex);
audio_cblk(userdata, buffer, copy_size);
if (opaque->need_flush) {
SDL_Android_AudioTrack_flush(env, atrack);
opaque->need_flush = false;
}
if (opaque->need_flush) {
opaque->need_flush = 0;
SDL_Android_AudioTrack_flush(env, atrack);
} else {
int written = SDL_Android_AudioTrack_write(env, atrack, buffer, copy_size);
if (written != copy_size) {
ALOGW("AudioTrack: not all data copied %d/%d", (int)written, (int)copy_size);
}
}
// TODO: 1 if callback return -1 or 0
}
这个函数的开始有很多SDL_Android_AudioTrack_set_xxx(),主要是设置播放器相关的配置,比如播放速度,声音大小等。
接着就是audio_cblk()
,不知道大家还记得在上一节我说过在stream_component_open()
的里面调用的audio_open()
,会有这么一句代码:
wanted_spec.callback = sdl_audio_callback;
现在这里排上用场了。
在上面aout_thread_n()
里调用的audio_cblk()
,实际上就是调用的opaque->spec.callback
,其实就是调用到sdl_audio_callback()
这个函数来了。
然后继续分析:
static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
//...
while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {
audio_size = audio_decode_frame(is);
if (audio_size < 0) {
/* 发生错误,就输出silence */
//...
} else {
if (is->show_mode != SHOW_MODE_VIDEO)
update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
is->audio_buf_size = audio_size;
}
is->audio_buf_index = 0;
}
len1 = is->audio_buf_size - is->audio_buf_index;
if (len1 > len)
len1 = len;
if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
else {
//...
}
len -= len1;
stream += len1;
is->audio_buf_index += len1;
}
is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
/* Let's assume the audio driver that is used by SDL has two periods. */
//...
}
保留了部分相对重要代码,其中重要代码有:
audio_size = audio_decode_frame(ffp);
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
我们继续看audio_decode_frame()
,还好作者有注释,根据注释提取重要代码:
/**
* Decode one audio frame and return its uncompressed size.
*
* The processed audio frame is decoded, converted if required, and
* stored in is->audio_buf, with size in bytes given by the return
* value.
*/
static int audio_decode_frame(FFPlayer *ffp){
af = frame_queue_peek_readable(&is->sampq)
is->audio_buf = af->frame->data[0];
}
从上面代码看,这里主要是判断解码后的is->sampq
是否为空,其中和解码的时候一样,如果为空(解码时放入is->sampq
判断是否满),如果为空,就阻塞(还记得解码的时候,每向is->sampq
放入一frame,就唤醒线程么?),否则返回队列的第一个frame。然后赋值给ffp->is->audio_buf
。
接着返回到上面sdl_audio_callback()
中,接着再把刚刚赋值的ffp->is->audio_buf
copy到stream中,stream从命名来看是一个流,流的另外一头在哪里呢?
再返回到aout_thread_n()
中:
SDL_Android_AudioTrack_write(env, atrack, buffer, copy_size);
这里的buffer就是刚刚的stream,该函数继续调用:
(*env)->SetByteArrayRegion(env, atrack->byte_buffer, 0, (int)size_in_byte, (jbyte*) data);
J4AC_AudioTrack__write(env, atrack->thiz, atrack->byte_buffer, 0, (int)size_in_byte);
这里先是把data拷贝到数组中,为什么呢?因为后面会把这个数组,也就是音频帧传递给java,而SetByteArrayRegion()
就是这里的一次转换。
J4AC_AudioTrack__write()
中继续跟踪会发现:
jint J4AC_android_media_AudioTrack__write(JNIEnv *env, jobject thiz, jbyteArray audioData, jint offsetInBytes, jint sizeInBytes)
{
return (*env)->CallIntMethod(env, thiz, class_J4AC_android_media_AudioTrack.method_write, audioData, offsetInBytes, sizeInBytes);
}
这就尴尬了,又调用到java里面去了,这里调用了java层的AudioTrack.java
中的write()函数。
其实这里又用到了bilibili另外一个开源项目:jni4android。这个项目可以直接在c里面生成一个java的装饰类。这里用到的java就是AudioTrack.java
,生成的文件就是AudioTrack.h
和AudioTrack.c
。后面基本就不用分析了吧。在java里面基本都会用androidTrack吧?网上教程也很多。
到了这里基本就结束了。音频播放完成了,接下来会分析视频播放流程,其实视频播放流程和音频潦差不多,不过比音频麻烦点。
** 如果大家还想了解ijkplayer的工作流程的话,可以关注下android下的ijkplayer。**