ijkplayer系列(四) —— ijkplayer音频解码与播放

写在前面

前几篇文章大概分析到了数据的读取,接下来就该解码和播放了。那么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.hAudioTrack.c。后面基本就不用分析了吧。在java里面基本都会用androidTrack吧?网上教程也很多。

到了这里基本就结束了。音频播放完成了,接下来会分析视频播放流程,其实视频播放流程和音频潦差不多,不过比音频麻烦点。

** 如果大家还想了解ijkplayer的工作流程的话,可以关注下android下的ijkplayer。**

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容