ijkplayer项目

news2024/11/25 22:45:10

ijkplayer项目

环境配置

NDK全称:Native Development Kit。

1、NDK是一系列工具的集合。NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。这些工具对开发者的帮助是巨大的。NDK集成了交叉编译器,并提供了相应的mk文件隔离平台、CPU、API等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。NDK可以自动地将so和Java应用一起打包,极大地减轻了开发人员的打包工作。

2、NDK提供了一份稳定、功能有限的API头文件声明。Google明确声明该API是稳定的,在后续所有版本中都稳定支持当前发布的API。从该版本的NDK中看出,这些API支持的功能非常有限,包含有:C标准库(libc)、标准数学库(libm)、压缩库(libz)、Log库(liblog)。

SDK:(software development kit)软件。

Gradle是一个基于JVM的构建工具,是一款通用灵活的构建工具,支持maven, Ivy仓库,支持传递性依赖管理,而不需要远程仓库或者是pom.xml和ivy.xml配置文件,基于Groovy,build脚本使用Groovy编写。

  • 【错误记录】编译 Android 版本的 ijkplayer 报错 ( You must define ANDROID_NDK before starting. | 下载指定版本 NDK )_韩曙亮的博客-CSDN博客

  • AndroidDevTools - Android开发工具 Android SDK下载 Android Studio下载 Gradle下载 SDK Tools下载

  • git大文件下载

    brew install git-lfs
    git lfs install
    git lfs pull
    

JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他编程语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的。例如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少要保证本地代码能工作在任何Java 虚拟机环境。

文件结构

ijkplayer在底层重写了ffplay.c文件,主要是去除ffplay中使用sdl音视频库播放音视频的部分;并且增加了对移动端硬件解码部分,视频渲染部分,以及音频播放部分的实现,这些部分在android和ios下有不同的实现,具体如下:

Platform硬件解码视频渲染音频播放
IOSVideoToolBoxOpenGL ESAudioQueue
AndroidMediaCodecOpenGL ES、ANativeWindowOpenSL ES、AudioTrack

从上面可以看出ijkplayer是暂时不支持音频硬件解码的,只支持软解。

主要目录结构:

目录解释
androidandroid平台上的上层接口封装以及平台相关方法
config存放编译ijkplayer所需的依赖源文件, 如ffmpeg、openssl等
ijkmedia核心代码
ijkj4aandroid平台下使用,用来实现c代码调用java层代码。这个文件夹是通过bilibili的另一个开源项目jni4android自动生成的。
ijkplayer播放器数据下载及解码相关
ijksdl音视频数据渲染相关
iosiOS平台上的上层接口封装以及平台相关方法
tool初始化项目工程脚本

源码解析

v4oWoq.png

v4ooSU.png

初始化

结构体:

  • SDL_Vout表示一个显示上下文,或者理解为一块画布,ANativeWindow,控制如何显示overlay。
  • SDL_VoutOverlay表示显示层,或者理解为一块图像数据,表达如何显示。

初始化:

  1. 创建IJKMediaPlayer对象, 通过ffp_create方法创建了FFPlayer对象,并设置消息处理函数;
  2. 创建图像渲染对象SDL_Vout;
  3. 创建平台相关的IJKFF_Pipeline对象,包括视频解码以及音频输出部分;

简单来说,就是创建播放器对象,完成音视频解码、渲染的准备工作。

当外部调用prepareToPlay启动播放后,ijkplayer内部最终会调用到ffplay.c中的方法int ffp_prepare_async_l(FFPlayer *ffp, const char *file_name),该方法是启动播放器的入口函数,在此会设置player选项,打开audio output,最重要的是调用stream_open方法。

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{  
    ......           
    /* start video display */
    if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 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 )
        goto fail;
 
    ......
    
    is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
    
    ......
    
    is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
    
    ......
}

从代码中可以看出,stream_open主要做了以下几件事情:

  1. 创建存放video/audio解码前数据的videoq/audioq;
  2. 创建存放video/audio解码后数据的pictq/sampq;
  3. 创建读数据线程read_thread;
  4. 创建视频渲染线程video_refresh_thread;

说明:subtitle是与video、audio平行的一个stream,ffplay中也支持对它的处理,即创建存放解码前后数据的两个queue,并且当文件中存在subtitle时,还会启动subtitle的解码线程。

数据读取

数据读取的整个过程都是由ffmpeg内部完成的,接收到网络过来的数据后,ffmpeg根据其封装格式,完成了解复用的动作,得到音视频分离开的解码前的数据,步骤如下:

  1. 创建上下文结构体,这个结构体是最上层的结构体,表示输入上下文
ic = avformat_alloc_context();
  1. 设置中断函数,如果出错或者退出,就可以立刻退出
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
  1. 打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
  1. 探测媒体类型,可得到当前文件的封装格式,音视频编码参数等信息
err = avformat_find_stream_info(ic, opts);
  1. 打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程。
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
  1. 读取媒体数据,得到的是音视频分离的解码前数据
ret = av_read_frame(ic, pkt);
  1. 将音视频数据分别送入相应的queue中
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 {
    av_packet_unref(pkt);
}

重复6、7两步,即可不断获取待播放的数据。

音视频解码

ijkplayer在视频解码上支持软解和硬解两种方式,可在起播前配置优先使用的解码方式,播放过程中不可切换。iOS平台上硬解使用VideoToolbox,Android平台上使用MediaCodec。ijkplayer中的音频解码只支持软解,暂不支持硬解。

  • 硬解,用自带播放器播放,android中的VideoView;
  • 软解,使用音视频解码库,比如FFmpeg;

视频解码方式选择

在打开解码器的方法中:

static int stream_component_open(FFPlayer *ffp, int stream_index)
{
    ......
    codec = avcodec_find_decoder(avctx->codec_id);
    ......
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }
    ......  
    case AVMEDIA_TYPE_VIDEO:
        ......
        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;
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto out;       
    ......
}

首先会打开ffmpeg的解码器,然后通过ffpipeline_open_video_decoder创建IJKFF_Pipenode。在创建IJKMediaPlayer对象时,通过ffpipeline_create_from_android创建了pipeline。该函数实现如下:

IJKFF_Pipenode* ffpipeline_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    return pipeline->func_open_video_decoder(pipeline, ffp);
}

func_open_video_decoder函数指针最后指向的是ffpipeline_android.c中的func_open_video_decoder,其定义如下:

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;
    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }
    return node;
}

首先通过ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2判断是否支持硬件解码,如果支持会优先去尝试打开硬件解码器,如果打开失败会自动切换使用ffmpeg软解码。

关于ffp->mediacodec_all_videos 、ffp->mediacodec_avc 、ffp->mediacodec_hevc 、ffp->mediacodec_mpeg2它们的值需要在起播前通过如下方法配置:

ijkmp_set_option_int(_mediaPlayer, IJKMP_OPT_CATEGORY_PLAYER,   "xxxxx", 1);

视频解码

video的解码线程为video_thread,audio的解码线程为audio_thread。

  • 视频解码线程
static int video_thread(void *arg)
{
    FFPlayer *ffp = (FFPlayer *)arg;
    int       ret = 0;
 
    if (ffp->node_vdec) {
        ret = ffpipenode_run_sync(ffp->node_vdec);
    }
    return ret;
}

ffpipenode_run_sync 中调用的是IJKFF_Pipenode对象中的 func_run_sync

int ffpipenode_run_sync(IJKFF_Pipenode *node)
{
    return node->func_run_sync(node);
}

func_run_sync 取决于播放前配置的软硬解,假设为硬解func_run_sync函数指针最后指向的是ffpipenode_android_mediacodec_vdec.c中的func_run_sync,其定义如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
    .......
    opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");
    if (!opaque->enqueue_thread) {
        ALOGE("%s: SDL_CreateThreadEx failed\n", __func__);
        ret = -1;
        goto fail;
    }
    while (!q->abort_request) {
        int64_t timeUs = opaque->acodec_first_dequeue_output_request ? 0 : AMC_OUTPUT_TIMEOUT_US;
        got_frame = 0;
        ret = drain_output_buffer(env, node, timeUs, &dequeue_count, frame, &got_frame);
        .......
        if (got_frame) {
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
            ......
        }
    }
}
  1. 首先该函数启动一个输入线程,线程的执行函数为enqueue_thread_func,函定义如下:
static int enqueue_thread_func(void *arg)
{
    ......
    while (!q->abort_request) {
        ret = feed_input_buffer(env, node, AMC_INPUT_TIMEOUT_US, &dequeue_count);
        if (ret != 0) {
            goto fail;
        }
    }
    ......
}

该函数在循环中通过feed_iput_buffer调用ffp_packet_queue_get_or_buffering一直不停的取数据,并将取得的数据交给硬件解码器。

  1. 创建完输入线程后,直接进入while循环,循环中调用drain_output_buffer去获取硬件解码后的数据,该函数最后一个参数用来标记是否接收到完整的一帧数据。

  2. got_frame为true时,将接收的帧通过ffp_queue_picture送入pictq队列里。

若为软解func_run_sync函数指针最后指向的是ffpipenode_ffplay_vdec.c中的func_run_sync,其定义如下:

static int func_run_sync(IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque *opaque = node->opaque;
    return ffp_video_thread(opaque->ffp);
}

static int ffplay_video_thread(void *arg) {
    AVFrame *frame = av_frame_alloc();
    for(;;){
        ret = get_video_frame(ffp, frame); // avcodec_receive_frame软解码获取一帧
        queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
    }
}

音频解码

ijkplayer的音频解码线程的入口函数是ff_ffplayer.c中的audio_thread()

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;
}
  1. 一开始就进入循环,然后调用decoder_decode_frame()进行解码,调用传进去的codec的codec->decode()方法解码,解码后的帧存放到frame中;

  2. 然后调用frame_queue_peek_writable()判断是否能把刚刚解码的frame写入is->sampq中。会判断sampq队列是否满了,如果已满,会调用pthread_cond_wait()方法阻塞队列;如果未满,就会返回frame应该放置的位置的地址。is->sampq是音频解码帧列表,播放线程从这里面读取数据,然后播放出来;

  3. 最后 av_frame_move_ref(af->frame, frame);把frame放入到sampq相应位置。由于前面af = frame_queue_peek_writable(&is->sampq),af为指向这一帧frame存放位置的指针,所以直接把值赋值给它的结构体里面的frame就行了。

  4. frame_queue_push(&is->sampq);里面是一个唤醒线程的操作,如果音频播放线程因为sampq队列为空而阻塞,这里可以唤醒它。

音视频渲染和同步

音频输出

ijkplayer中Android平台使用OpenSL ES或AudioTrack输出音频,iOS平台使用AudioQueue输出音频。

audio output节点,在ffp_prepare_async_l方法中被创建:

ffp->aout = ffpipeline_open_audio_output(ffp->pipeline, ffp);

ffpipeline_open_audio_output方法实际上调用的是IJKFF_Pipeline对象的函数指针func_open_audio_utput,该函数指针在初始化中的ijkmp_android_create方法中被赋值,最后指向的是ffpipeline_android.c中的函数func_open_audio_output

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;
}

该函数会根据ffp->opensles来决定是否使用openSLES来进行音频播放,后面的分析是基于openSLES方式处理音频的。

SDL_AoutAndroid_CreateForOpenSLES定义如下,主要完成的是创建SDL_Aout对象:

SDL_Aout *SDL_AoutAndroid_CreateForOpenSLES()
{
    SDLTRACE("%s\n", __func__);
    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();

    int ret = 0;
 
    SLObjectItf slObject = NULL;
    ret = slCreateEngine(&slObject, 0, NULL, 0, NULL, NULL);
    CHECK_OPENSL_ERROR(ret, "%s: slCreateEngine() failed", __func__);
    opaque->slObject = slObject;
 
    ret = (*slObject)->Realize(slObject, SL_BOOLEAN_FALSE);
    CHECK_OPENSL_ERROR(ret, "%s: slObject->Realize() failed", __func__);
 
    SLEngineItf slEngine = NULL;
    ret = (*slObject)->GetInterface(slObject, SL_IID_ENGINE, &slEngine);
    CHECK_OPENSL_ERROR(ret, "%s: slObject->GetInterface() failed", __func__);
    opaque->slEngine = slEngine;
 
    SLObjectItf slOutputMixObject = NULL;
    const SLInterfaceID ids1[] = {SL_IID_VOLUME};
    const SLboolean req1[] = {SL_BOOLEAN_FALSE};
    ret = (*slEngine)->CreateOutputMix(slEngine, &slOutputMixObject, 1, ids1, req1);
    CHECK_OPENSL_ERROR(ret, "%s: slEngine->CreateOutputMix() failed", __func__);
    opaque->slOutputMixObject = slOutputMixObject;
 
    ret = (*slOutputMixObject)->Realize(slOutputMixObject, SL_BOOLEAN_FALSE);
    CHECK_OPENSL_ERROR(ret, "%s: slOutputMixObject->Realize() failed", __func__);
 
    aout->free_l       = aout_free_l;
    aout->opaque_class = &g_opensles_class;
    aout->open_audio   = aout_open_audio;
    aout->pause_audio  = aout_pause_audio;
    aout->flush_audio  = aout_flush_audio;
    aout->close_audio  = aout_close_audio;
    aout->set_volume   = aout_set_volume;
    aout->func_get_latency_seconds = aout_get_latency_seconds;
 
    return aout;
fail:
    aout_free_l(aout);
    return NULL;
}

回到ffplay.c中,如果发现待播放的文件中含有音频,那么在调用 stream_component_open 打开解码器时,该方法里面也调用 audio_open 打开了audio output设备。

static int audio_open(FFPlayer *opaque, int64_t wanted_channel_layout, int wanted_nb_channels, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{
    FFPlayer *ffp = opaque;
    VideoState *is = ffp->is;
    SDL_AudioSpec wanted_spec, spec;
    ......
    wanted_nb_channels = av_get_channel_layout_nb_channels(wanted_channel_layout);
    wanted_spec.channels = wanted_nb_channels;
    wanted_spec.freq = wanted_sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.silence = 0;
    wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AoutGetAudioPerSecondCallBacks(ffp->aout)));
    wanted_spec.callback = sdl_audio_callback;
    wanted_spec.userdata = opaque;
    while (SDL_AoutOpenAudio(ffp->aout, &wanted_spec, &spec) < 0) {
        .....
    }
    ......
    return spec.size;
}

在 audio_open中配置了音频输出的相关参数 SDL_AudioSpec ,并通过

int SDL_AoutOpenAudio(SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained)
{
    if (aout && desired && aout->open_audio)
        return aout->open_audio(aout, desired, obtained);
    return -1;
}

设置给了Audio Output,android平台上即为OpenSLES。OpenSLES模块在工作过程中,通过不断的callback来获取pcm数据进行播放。

视频渲染

若Android平台上采用OpenGL渲染解码后的YUV图像,渲染线程为video_refresh_thread,最后渲染图像的方法为video_image_display2,定义如下:

static void video_image_display2(FFPlayer *ffp)
{
    VideoState *is = ffp->is;
    Frame *vp;
    Frame *sp = NULL;
 
    vp = frame_queue_peek_last(&is->pictq);
    ......
    
    SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);
    ......
}

从代码实现上可以看出,该线程的主要工作为:

  1. 调用frame_queue_peek_last从pictq中读取当前需要显示视频帧;
  2. 调用SDL_VoutDisplayYUVOverlay进行绘制;

display_overlay函数指针在函数SDL_VoutAndroid_CreateForANativeWindow()中被赋值为vout_display_overlay,该方法就是调用OpengGL绘制图像。

音视频同步

ijkplayer在默认情况下也是使用音频作为参考时钟源,处理同步的过程主要在视频渲染video_refresh_thread的线程中:

static int video_refresh_thread(void *arg)
{
    FFPlayer *ffp = arg;
    VideoState *is = ffp->is;
    double remaining_time = 0.0;
    while (!is->abort_request) {
        if (remaining_time > 0.0)
            av_usleep((int)(int64_t)(remaining_time * 1000000.0));
        remaining_time = REFRESH_RATE;
        if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
            video_refresh(ffp, &remaining_time);
    }
 
    return 0;
}

从上述实现可以看出,该方法中主要循环做两件事情:

  1. 休眠等待,remaining_time的计算在video_refresh中;
  2. 调用video_refresh方法,刷新视频帧;

可见同步的重点是在video_refresh中,下面着重分析该方法:

lastvp = frame_queue_peek_last(&is->pictq);
vp = frame_queue_peek(&is->pictq);
......
  /* compute nominal last_duration */
last_duration = vp_duration(is, lastvp, vp);
delay = compute_target_delay(ffp, last_duration, is);

lastvp是上一帧,vp是当前帧,last_duration则是根据当前帧和上一帧的pts,计算出来上一帧的显示时间,经过 compute_target_delay 方法,计算出显示当前帧需要等待的时间。

static double compute_target_delay(FFPlayer *ffp, double delay, VideoState *is)
{
    double sync_threshold, diff = 0;
 
    /* update delay to follow master synchronisation source */
    if (get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {
        /* if video is slave, we try to correct big delays by
           duplicating or deleting a frame */
        diff = get_clock(&is->vidclk) - get_master_clock(is);
 
        /* skip or repeat frame. We take into account the
           delay to compute the threshold. I still don't know
           if it is the best guess */
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD_MIN, FFMIN(AV_SYNC_THRESHOLD_MAX, delay));
        /* -- by bbcallen: replace is->max_frame_duration with AV_NOSYNC_THRESHOLD */
        if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD) {
            if (diff <= -sync_threshold)
                delay = FFMAX(0, delay + diff);
            else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
                delay = delay + diff;
            else if (diff >= sync_threshold)
                delay = 2 * delay;
        }
    }
 
    .....
 
    return delay;
}
  • 如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;
  • 如果视频帧超前,并且该帧的显示时间大于显示更新门槛,则显示下一帧的时间为超前的时间差加上上一帧的显示时间;
  • 如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略。

回到video_refresh中:

time= av_gettime_relative()/1000000.0;
if (isnan(is->frame_timer) || time < is->frame_timer)
  is->frame_timer = time;
if (time < is->frame_timer + delay) {
  *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
  goto display;
}

frame_timer实际上就是上一帧的播放时间,而frame_timer + delay实际上就是当前这一帧的播放时间,如果系统时间还没有到当前这一帧的播放时间,直接跳转至display,而此时is->force_refresh变量为0,不显示当前帧,进入video_refresh_thread中下一次循环,并睡眠等待。

is->frame_timer += delay;
  if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
      is->frame_timer = time;
 
  SDL_LockMutex(is->pictq.mutex);
  if (!isnan(vp->pts))
         update_video_pts(is, vp->pts, vp->pos, vp->serial);
  SDL_UnlockMutex(is->pictq.mutex);
 
  if (frame_queue_nb_remaining(&is->pictq) > 1) {
       Frame *nextvp = frame_queue_peek_next(&is->pictq);
       duration = vp_duration(is, vp, nextvp);
       if(!is->step && (ffp->framedrop > 0 || (ffp->framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration) {
           frame_queue_next(&is->pictq);
           goto retry;
       }
  }

如果当前这一帧的播放时间已经过了,并且其和当前系统时间的差值超过了AV_SYNC_THRESHOLD_MAX,则将当前这一帧的播放时间改为系统时间,并在后续判断是否需要丢帧,其目的是为后面帧的播放时间重新调整frame_timer,如果缓冲区中有更多的数据,并且当前的时间已经大于当前帧的持续显示时间,则丢弃当前帧,尝试显示下一帧。

{
   frame_queue_next(&is->pictq);
   is->force_refresh = 1;
 
   SDL_LockMutex(ffp->is->play_mutex);
   
    ......
    
display:
    /* display picture */
    if (!ffp->display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
        video_display2(ffp);

否则进入正常显示当前帧的流程,调用 video_display2 开始渲染。

事件处理

在播放过程中,某些行为的完成或者变化,如prepare完成,开始渲染等,需要以事件形式通知到外部,以便上层作出具体的业务处理。ijkplayer支持的事件比较多,具体定义在ijkplayer/ijkmedia/ijkplayer/ff_ffmsg.h中

#define FFP_MSG_FLUSH                       0
#define FFP_MSG_ERROR                       100     /* arg1 = error */
#define FFP_MSG_PREPARED                    200
#define FFP_MSG_COMPLETED                   300
#define FFP_MSG_VIDEO_SIZE_CHANGED          400     /* arg1 = width, arg2 = height */
#define FFP_MSG_SAR_CHANGED                 401     /* arg1 = sar.num, arg2 = sar.den */
#define FFP_MSG_VIDEO_RENDERING_START       402
#define FFP_MSG_AUDIO_RENDERING_START       403
#define FFP_MSG_VIDEO_ROTATION_CHANGED      404     /* arg1 = degree */
#define FFP_MSG_BUFFERING_START             500
#define FFP_MSG_BUFFERING_END               501
#define FFP_MSG_BUFFERING_UPDATE            502     /* arg1 = buffering head position in time, arg2 = minimum percent in time or bytes */
#define FFP_MSG_BUFFERING_BYTES_UPDATE      503     /* arg1 = cached data in bytes,            arg2 = high water mark */
#define FFP_MSG_BUFFERING_TIME_UPDATE       504     /* arg1 = cached duration in milliseconds, arg2 = high water mark */
#define FFP_MSG_SEEK_COMPLETE               600     /* arg1 = seek position,                   arg2 = error */
#define FFP_MSG_PLAYBACK_STATE_CHANGED      700
#define FFP_MSG_TIMED_TEXT                  800
#define FFP_MSG_VIDEO_DECODER_OPEN          10001

消息上报初始化

在IJKMediaPlayer的初始化方法中:

static void
IjkMediaPlayer_native_setup(JNIEnv *env, jobject thiz, jobject weak_this)
{
    MPTRACE("%s\n", __func__);
    IjkMediaPlayer *mp = ijkmp_android_create(message_loop);
    ......
}

可以看到在创建播放器时, message_loop 函数地址作为参数传入了 ijkmp_android_create ,继续跟踪代码,可以发现,该函数地址最终被赋值给了IjkMediaPlayer中的 msg_loop 函数指针:

IjkMediaPlayer *ijkmp_create(int (*msg_loop)(void*))
{
    ......
    mp->msg_loop = msg_loop;
    ......
}

开始播放时,会启动一个消息线程:

static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
    ......
    mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_msg_loop, mp, "ff_msg_loop");
    ......
}

ijkmp_msg_loop 方法中调用的即是 mp->msg_loop

消息上报处理

播放器底层上报事件时,实际上就是将待发送的消息放入消息队列,另外有一个线程会不断从队列中取出消息,上报给外部,其代码流程大致如下图所示:

vIMfoD.png

框架分析

ijkplayer(android)融合了mediacodec,实现了硬解支持。由于MediaCodec的使用与一般的解码API有所不同,其在应用中的使用层级更偏向于播放器层面。所以ijkplayer并没有将MediaCodec作为“解码器”扩展到ffmpeg中,而是另外定义了一个封装层,同时也统一了软解的接口,这个封装层即ffpipeline

ijkplayer中的音频是走的软解,后续提到的解码无特别说明都是指视频解码。

基础概念

解码封装层中有两个重要的结构体,ffpipelineffpipenode。ffpipeline表示解码器和音频输出的提供者,ffpipenode表示解码器。

ffpipeline定义如下:

struct IJKFF_Pipeline {
    SDL_Class             *opaque_class;
    IJKFF_Pipeline_Opaque *opaque;
    void            (*func_destroy)             (IJKFF_Pipeline *pipeline);
    IJKFF_Pipenode *(*func_open_video_decoder)  (IJKFF_Pipeline *pipeline, FFPlayer *ffp);
    SDL_Aout       *(*func_open_audio_output)   (IJKFF_Pipeline *pipeline, FFPlayer *ffp);
    IJKFF_Pipenode *(*func_init_video_decoder)  (IJKFF_Pipeline *pipeline, FFPlayer *ffp);
    int           (*func_config_video_decoder)  (IJKFF_Pipeline *pipeline, FFPlayer *ffp);
};

IJKFF_Pipeline主要定义了3组函数:

  • 获取音频输出:func_open_audio_output;
  • 同步方式获取视频解码器:func_open_video_decoder;
  • 异步方式获取视频解码器:func_init_video_decoder、func_config_video_decoder;

这里以IJKFF_Pipenode表示一个视频解码器,以SDL_Aout表示音频输出。

ijkplayer中的音频是软解码的,所以不需要像视频一样去作封装,而是直接基于ffplay修改的。

异步创建视频解码器的用意不是很明白,实际使用中也没需求需要用到,后面分析,先只关注同步创建解码器的部分,即func_open_video_decoder的使用。

ffpipenode定义如下:

struct IJKFF_Pipenode {
    SDL_mutex *mutex;
    void *opaque;
    void (*func_destroy) (IJKFF_Pipenode *node);
    int  (*func_run_sync)(IJKFF_Pipenode *node);
    int  (*func_flush)   (IJKFF_Pipenode *node); // optional
};

IJKFF_Pipenode中的主要函数是:

  • func_run_sync:表示解码主循环,从这个函数返回代表解码结束了;
  • func_flush:清空解码器,一般在seek时用到;

目录结构

解码封装层的源码文件及目录结构如下:

// +表示目录,目录下文件递进4个空格,-表示文件
+ijkmedia/ijkplayer
    -ff_ffpipenode.c/h                          //pipeline定义与封装
    -ff_ffpipeline.c/h                          //pipenode定义与封装
    +pipeline
        -ffpipeline_ffplay.c/h                  //ffplay的pipeline定义(未使用)
        -ffpipenode_ffplay_vdec.c/h             //ffplay的pipenode定义
    +android/pipeline
        -ffpipeline_android.c/h                 //android的ffpipeline定义
        -ffpipenode_android_mediacodec_vdec.c/h //android的(mediacodec)ffpipenode定义

上面提到的pipeline和pipenode的实现由两种(android上):

  • ffplay软解封装:在ijkmedia/ijkplayer/pipeline目录下,其中ffpipeline_ffplay并未用到;
  • mediacodec硬解封装:在ijkmedia/ijkplayer/android/pipeline目录下;

流程分析

解码器在播放器使用过程中的主要功能可以分为:创建、解码、帧入队、seek处理、销毁。解码器的调用主要在ff_ffplay。

创建

首先需要创建pipeline,pipeline的创建流程和SDL_Vout一样:

new IjkMediaPlayer() 
    -> initPlayer() 
        -> native_setup() 
            -> IjkMediaPlayer_native_setup() 
                -> ijkmp_android_create() 
                    -> SDL_VoutAndroid_CreateForAndroidSurface() //SDL_Vout在这里创建
                    -> ffpipeline_create_from_android() //android pipeline在这里创建

接着在ff_ffplay中创建解码器(pipenode):

static int stream_component_open(FFPlayer *ffp, int stream_index)
{
    //……
    switch (avctx->codec_type) {
        case AVMEDIA_TYPE_VIDEO:
            if (ffp->async_init_decoder) {
                //这里是异步创建解码器的代码
            }
            else{
                decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
              	// 默认情况,调用android pipeline的func_open_video_decoder,
              	// 其内部会根据所配置的选项和实际支持的编码类型,自动回退到软解,即创建ffplay pipenode
                ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
                if (!ffp->node_vdec)
                    goto fail;
            }
            if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto out;
            break;
            //……
    }
}

解码器的创建时在stream_component_open里调用ffpipeline_open_video_decoder完成的,创建好后的decoder保存在ffp->node_vdec中。接着调用decoder_start启动了video_thread

video_thread的实现很简单:

static int video_thread(void *arg)
{
    FFPlayer *ffp = (FFPlayer *)arg;
    int       ret = 0;

    if (ffp->node_vdec) {
        ret = ffpipenode_run_sync(ffp->node_vdec);
    }
    return ret;
}

直接调用循环的主体ffpipenode_run_sync,即pipenode的func_run_sync函数。

解码

解码工作是在video_thread中完成的,也就是由具体pipenode的实现决定。在ijkplayer中,这分为两种情况,一种是硬解,一种是软解。软解的实现基本是ffplay的改造,硬解的实现是调用的MediaCodec。

帧入队

在解码的过程中,需要将已经解码好的帧放入帧队列FrameQueue中。该工作由ff_ffplay中的ffp_queue_picture/queue_picture完成。

int ffp_queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    return queue_picture(ffp, src_frame, pts, duration, pos, serial);
}

static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    //……
}

ffp_queue_picture只是简单调用的queue_picture,这是为了改变它的可见性(去掉static),方便在其他文件调用该函数。

queue_picture的主要功能和结构与ffplay的类似,可以参考ffplay video显示线程分析。所不同的是ijk的显示流程和解码流程与ffplay不同,所以在queue_picture的时候调用SDL_VoutOverlayfunc_fill_frame把帧画面“绘制”到最终的显示图层上。

seek处理

seek时需要调用解码器的flush:

static int read_thread(void *arg)
{
    //……
    ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
    if (ret < 0) {
        //……
    }
    else {
        //……
        if (is->video_stream >= 0) {
            if (ffp->node_vdec) {
              	// 这里调用解码器的flush,即pipenode的func_flush函数
                ffpipenode_flush(ffp->node_vdec);
            }
            packet_queue_flush(&is->videoq);
            packet_queue_put(&is->videoq, &flush_pkt);
        }
        //……
    }
    //……
}

销毁

按预期pipenode的销毁应该出现在其创建函数stream_component_open对应的stream_component_close中,然而并没有。根据IJKFF_Pipenode的定义,其销毁应调用func_destroy,或者其封装函数ffpipenode_free/ffpipenode_free_p

正常流程只在整个播放器销毁时有调用到:

void ffp_destroy(FFPlayer *ffp)
{
    //……
    ffpipenode_free_p(&ffp->node_vdec);
    ffpipeline_free_p(&ffp->pipeline);
    //……
}

这就意味着,按代码分析的情况看,除第一次外,每次选择视频轨道都会造成一次内存泄漏!不过毕竟还未验证,待验证后再给结论。

音视频解码

硬解

在android中的ijkplayer是通过封装MediaCodec实现的硬解,以下将从解码器的创建、解码、帧入队三个方面介绍。

创建

硬解pipenode的创建是在stream_component_open中调用ffpipeline_open_video_decoder创建的。ffpipeline_open_video_decoder是pipeline的封装,在Android上调用的是ffpipeline_andriod.c中的func_open_video_decoder

static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;
    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }
    return node;
}

这里启用了硬解会调用ffpipenode_create_video_decoder_from_android_mediacodec

IJKFF_Pipenode *ffpipenode_create_video_decoder_from_android_mediacodec(FFPlayer *ffp, IJKFF_Pipeline *pipeline, SDL_Vout *vout)
{
    //……
    //1. 初始化node
    IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));
    node->func_destroy  = func_destroy;
    if (ffp->mediacodec_sync) {
        node->func_run_sync = func_run_sync_loop;
    } else {
        node->func_run_sync = func_run_sync;
    }
    node->func_flush    = func_flush;

    //2. 硬解选项检查
    switch (opaque->codecpar->codec_id) {
    case AV_CODEC_ID_H264:
        if (!ffp->mediacodec_avc && !ffp->mediacodec_all_videos) {
            ALOGE("%s: MediaCodec: AVC/H264 is disabled. codec_id:%d \n", __func__, opaque->codecpar->codec_id);
            goto fail;
        }
        opaque->mcc.profile = opaque->codecpar->profile;
        opaque->mcc.level   = opaque->codecpar->level;
    //……
    }

    //3. 创建MediaFormat
    ret = recreate_format_l(env, node);

    //4. 选择codec(选择最佳codec name)
    if (!ffpipeline_select_mediacodec_l(pipeline, &opaque->mcc) || !opaque->mcc.codec_name[0]) {
        ALOGE("amc: no suitable codec\n");
        goto fail;
    }

    //5. 配置codec(创建MediaCodec)
    ret = reconfigure_codec_l(env, node, jsurface);

    //一些特殊的解码器需要在MediaCodec解码后,增加一级帧队列,队列按pts排序,然后再送出到FrameQueue,源码分析中不考虑该特殊情况。
    if (opaque->n_buf_out) {
        int i;

        opaque->amc_buf_out = calloc(opaque->n_buf_out, sizeof(*opaque->amc_buf_out));
        assert(opaque->amc_buf_out != NULL);
        for (i = 0; i < opaque->n_buf_out; i++)
            opaque->amc_buf_out[i].pts = AV_NOPTS_VALUE;
    }
    //……
}

在pipenode的创建中,经历以下步骤:

  1. 初始化node;
  2. 硬解选项检查;
  3. 创建MediaFormat。在函数recreate_format_l中实现,主要设置mime type、width、height和csd-0;
  4. 选择codec(选择最佳codec name)。主要是调用ffpipeline_select_mediacodec_l函数进行选择,ffpipeline_select_mediacodec_l会回调到IjkMediaPlayeronSelectCodec。在onSelectCodec中会根据自己的一套规则取选择合适的codec name(与MediaCodecList.findDecoderForFormat的工作类似,不过更灵活);
  5. 配置codec(创建MediaCodec)。在函数reconfigure_codec_l中实现。

接下来看下reconfigure_codec_l

static int reconfigure_codec_l(JNIEnv *env, IJKFF_Pipenode *node, jobject new_surface)
{
    //……
    //acodec = new MediaCodec
    if (!opaque->acodec) {
        opaque->acodec = create_codec_l(env, node);
    }

    //MediaCodec.setSurface
    amc_ret = SDL_AMediaCodec_configure_surface(env, opaque->acodec, opaque->input_aformat, opaque->jsurface, NULL, 0);

    //MediaCodec.start
    amc_ret = SDL_AMediaCodec_start(opaque->acodec);
    //……
}

reconfigure的主要流程与java api的使用差不多。典型的new -> setSurface -> start。到这里,MediaCodec就创建好,准备接收数据了。

解码

解码调用过程:stream_component_open -> decoder_start -> video_thread -> fun_run_sync

在ffpipenode_android_mediacodec_vdec中有两个fun_run_sync的实现,可以通过mediacodec_sync选项进行切换:

//ffpipenode_create_video_decoder_from_android_mediacodec
    if (ffp->mediacodec_sync) {
        node->func_run_sync = func_run_sync_loop;
    } else {
        node->func_run_sync = func_run_sync;
    }

默认使用的是func_run_sync:

static int func_run_sync(IJKFF_Pipenode *node)
{
    //……
    //A. 创建enqueue_thread,喂原始数据
    opaque->enqueue_thread = SDL_CreateThreadEx(&opaque->_enqueue_thread, enqueue_thread_func, node, "amediacodec_input_thread");

    //B. 循环拉取解码数据
    while (!q->abort_request) {
        got_frame = 0;
        //1. drain_output_buffer获取frame
        ret = drain_output_buffer(env, node, timeUs, &dequeue_count, frame, &got_frame);
        //……
        if (ret != 0) {
            //拉取出错,release buffer false通知MediaCodec丢弃这一帧
        }
        if (got_frame) {
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            //2. 解码速度过慢,丢帧
            if (ffp->framedrop > 0 || (ffp->framedrop && ffp_get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
                //逻辑与软解丢帧类似,可以参考ffplay解码线程分析
            }
            //3. 帧入队
            ret = ffp_queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
            if (ret) {
                //入队出错,release buffer false通知MediaCodec丢弃这一帧
            }
            av_frame_unref(frame);
    }
    //……
}

func_run_sync的函数比较长,上述代码仅抽取了主干代码。主干代码分为两个部分:

  • 创建enqueue_thread,喂原始数据;
  • 循环拉取解码数据;

也就是ijkplayer中把MediaCodec的queueInputBufferdequeueOutputBuffer两个过程分离到了两个线程:func_run_sync负责dequeue,dequeue的主要实现在drain_output_buffer

drain_output_buffer调用后会取到一帧填充好的AVFrame,之后调用ffp_queue_picture将这一帧放入到FrameQueue中。

在分析drain_output_buffer前先看下enqueue_thread_func

static int enqueue_thread_func(void *arg)
{
    while (!q->abort_request && !opaque->abort) {
        ret = feed_input_buffer(env, node, AMC_INPUT_TIMEOUT_US, &dequeue_count);
        if (ret != 0) {
            goto fail;
        }
    }
    ret = 0;
fail:
    SDL_AMediaCodecFake_abort(opaque->acodec);
    ALOGI("MediaCodec: %s: exit: %d", __func__, ret);
    return ret;
}

enqueue_thread_func的实现比较简单,即循环调用feed_input_buffer

因此,在分析了上述两个线程后,我们找到的3个主要函数是:

  • feed_input_buffer:主要调用MediaCodec.queueInputBuffer给MediaCodec喂原始数据;
  • drain_output_buffer:主要调用MediaCodec.dequeueOutputBuffer从MediaCodec拉解码数据;
  • ffp_queue_picture:将解码后的AVFrame送入FrameQueue;

feed_input_buffer

static int feed_input_buffer(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *enqueue_count)
{
    //……
    //1. 读Packet,及不连续Packet的处理
    if (!d->packet_pending || d->queue->serial != d->pkt_serial) {
        do {
            ffp_packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished)if (ffp_is_flush_packet(&pkt) || opaque->acodec_flush_request) {
                SDL_AMediaCodec_flush()}
        }while (ffp_is_flush_packet(&pkt) || d->queue->serial != d->pkt_serial);
        av_packet_unref(&d->pkt);
        d->pkt_temp = d->pkt = pkt;
        d->packet_pending = 1;
    }

    //2. 喂数据
    if (d->pkt_temp.data) {
        //如果需要重新配置MediaCodec,则重新创建一个
        if (ffpipeline_is_surface_need_reconfigure_l(pipeline)) {
            ret = reconfigure_codec_l(env, node, new_surface);
        }
        input_buffer_index = SDL_AMediaCodec_dequeueInputBuffer(opaque->acodec, timeUs);
        copy_size = SDL_AMediaCodec_writeInputData(opaque->acodec, input_buffer_index, d->pkt_temp.data, d->pkt_temp.size);
        amc_ret = SDL_AMediaCodec_queueInputBuffer(opaque->acodec, input_buffer_index, 0, copy_size, time_stamp, queue_flags);
    }

    //3. pkt_temp更新
    if (copy_size < 0) {//无需分包
        d->packet_pending = 0;
    } else {
        d->pkt_temp.dts =
        d->pkt_temp.pts = AV_NOPTS_VALUE;
        if (d->pkt_temp.data) {//分包更新
            d->pkt_temp.data += copy_size;
            d->pkt_temp.size -= copy_size;
            if (d->pkt_temp.size <= 0)
                d->packet_pending = 0;
        } else {//解码结束
            d->packet_pending = 0;
            d->finished = d->pkt_serial;
        }
    }
}

这段代码有3个步骤:

  1. 读Packet,及不连续Packet的处理。主要是通过ffp_packet_queue_get_or_buffering读一个Packet,如果不连续,就丢Packet和Flush MediaCodec;
  2. 喂数据。MediaCodec的典型步骤:dequeueInputBuffer -> writeInputData -> queueInputBuffer
  3. pkt_temp更新;

导致这段代码不好理解的一个地方是分包发送的处理。因为在调用SDL_AMediaCodec_writeInputData发送Packet数据的时候,不一定能恰好完整发送,所以需要分多次发送。

分包发送代码中pkt_temp表示要发送的pkt,packet_pending表示pkt_temp中有未发送完的数据。每次发送后,根据已发送大小copy_size更新pkt_temp,直到pkt_temp.size小于0,置packet_pending为0。在读Packet前会先判断packet_pending是否为1,如果为1,则不拉取新的Packet,而是先消耗pkt_temp。这样就达到循环发送Packet的目的了。

上述代码是经过大量省略的,被省略的代码还有几个功能点:

  • reconfig的具体实现,以及如何与fun_run_sync线程同步
  • mediacodec_handle_resolution_change处理
  • H264/H265特殊处理
  • fake frame处理

drain_output_buffer

//drain_output_buffer = lock(opaque->acodec_mutex) + drain_output_buffer_l + unlock(opaque->acodec_mutex)
static int drain_output_buffer_l(JNIEnv *env, IJKFF_Pipenode *node, int64_t timeUs, int *dequeue_count, AVFrame *frame, int *got_frame)
{
    output_buffer_index = SDL_AMediaCodecFake_dequeueOutputBuffer(opaque->acodec, &bufferInfo, timeUs);
    if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED) {
        ALOGI("AMEDIACODEC__INFO_OUTPUT_BUFFERS_CHANGED\n");
    }
    else if (output_buffer_index == AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED) {
        ALOGI("AMEDIACODEC__INFO_OUTPUT_FORMAT_CHANGED\n");
    }
    else if (output_buffer_index == AMEDIACODEC__INFO_TRY_AGAIN_LATER) {
        AMCTRACE("AMEDIACODEC__INFO_TRY_AGAIN_LATER\n");
    }
    else if (output_buffer_index < 0) {
        goto done;
    }
    else if (output_buffer_index >= 0) {
        if (opaque->n_buf_out) {
            // 如果开启了缓冲区,则对缓冲区内的帧进行pts排序后输出。
          	// 看代码,目前只有codec是OMX.TI.DUCATI1.才启用。也就是默认MediaCodec的输出都是pts排序好的
        }else {
            ret = amc_fill_frame(node, frame, got_frame, output_buffer_index, SDL_AMediaCodec_getSerial(opaque->acodec), &bufferInfo);
        }
    }

done:
    if (opaque->decoder->queue->abort_request)
        ret = -1;
    else
        ret = 0;
fail:
    return ret;
}

drain_output_buffer相比feed_input_buffer来的简单,主要是调用SDL_AMediaCodecFake_dequeueOutputBuffer(即MediaCodec.dequeueOutputBuffer),根据返回值打印调试信息。如果是output_buffer_index >= 0,则调用amc_fill_frame把bufferinfo填充到AVFrame中。

amc_fill_frame主要是填充Frame的宽、高、pts等信息,把bufferinfo填入到Opaque中,并没有填充真正的图像数据。Frame显示的时候再利用这些信息通过MediaCodec的releasseOutputBuffer进行显示。

帧入队

在ijkplayer 解码实现分析——软解篇中我们分析了queue_picture(ffp_queue_picture封装的是queue_picture)的逻辑。与ffplay的queue_picture的差异在于增加了一些“绘图操作”,这些绘图操作是通过SDL_Vout_CreateOverlay -> SDL_VoutFillFrameYUVOverlay完成。

SDL_Vout_CreateOverlay会调用具体vout->create_overlaySDL_VoutFillFrameYUVOverlay会调用overlay->func_fill_frame

对于MediaCodec而言,vout->create_overlay会调用到ijksdl_vout_overlay_android_mediacodec.c中的SDL_VoutAMediaCodec_CreateOverlay,这个函数中关键的几行是:

SDL_VoutOverlay_Opaque *opaque = overlay->opaque;
opaque->buffer_proxy  = NULL;
overlay->opaque_class = &g_vout_overlay_amediacodec_class;
overlay->format       = SDL_FCC__AMC;

即:mediacodec的overlay的format指定为SDL_FCC__AMC,并且在opauqe中有一个buffer_proxy用于保存mediacodec解码后的buffer_index和bufferinfo。

SDL_FCC__AMC主要用于在显示函数(ijksdl_vout_android_nativewindow.c)func_display_overlay_l中判断是否应该调用SDL_VoutOverlayAMediaCodec_releaseFrame_l来显示。分析见ijkplayer video显示分析

对于overlay->func_fill_frame会调用到ijksdl_vout_overlay_android_mediacodec.c中的func_fill_frame,这个函数中关键的几行是:

opaque->buffer_proxy = (SDL_AMediaCodecBufferProxy *)frame->opaque;
overlay->opaque_class = &g_vout_overlay_amediacodec_class;
overlay->format     = SDL_FCC__AMC;
overlay->w = (int)frame->width;
overlay->h = (int)frame->height;

drain_output_buffer中分析过,amc_fill_frame会把bufferinfo填入到AVFrame的Opaque中,这里只是再复制到了overlay->opaque中,方便在显示时访问该变量。

软解

软解的pipenode定义在ffpipenode_ffplay_vdec.h/c中。通过函数ffpipenode_create_video_decoder_from_ffplay来创建一个软解码器。不过ijkplayer中ffplay pipenode并不是由ffplay pipeline创建,而是由android pipeline创建:

//ffpipeline_android.c
static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
{
    IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
    IJKFF_Pipenode        *node = NULL;

    //如果有启用任何一种硬解选项,则创建mediacodec video decoder
    if (ffp->mediacodec_all_videos || ffp->mediacodec_avc || ffp->mediacodec_hevc || ffp->mediacodec_mpeg2)
        node = ffpipenode_create_video_decoder_from_android_mediacodec(ffp, pipeline, opaque->weak_vout);

    //如果没有启用硬解,或者创建失败,则返回ffplay video decoder
    if (!node) {
        node = ffpipenode_create_video_decoder_from_ffplay(ffp);
    }
    return node;
}

ffpipenode_create_video_decoder_from_ffplay定义如下:

IJKFF_Pipenode *ffpipenode_create_video_decoder_from_ffplay(FFPlayer *ffp)
{
    IJKFF_Pipenode *node = ffpipenode_alloc(sizeof(IJKFF_Pipenode_Opaque));
    if (!node)
        return node;

    IJKFF_Pipenode_Opaque *opaque = node->opaque;
    opaque->ffp         = ffp;

    node->func_destroy  = func_destroy;
    node->func_run_sync = func_run_sync;

    ffp_set_video_codec_info(ffp, AVCODEC_MODULE_NAME, avcodec_get_name(ffp->is->viddec.avctx->codec_id));
    ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
    return node;
}

软解的关键实现在func_run_sync:

static int func_run_sync(IJKFF_Pipenode *node)
{
    IJKFF_Pipenode_Opaque *opaque = node->opaque;
    return ffp_video_thread(opaque->ffp);
}

int ffp_video_thread(FFPlayer *ffp)
{
    return ffplay_video_thread(ffp);
}

这里的ffplay_video_thread实现基本与ffplay的video_thread是一样的,其分析参考ffplay解码线程分析

queue_picture

软解与ffplay有所不同的地方在于queue_picture(将解码帧放入FrameQueue中):

static int queue_picture(FFPlayer *ffp, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    //……
    if (!(vp = frame_queue_peek_writable(&is->pictq)))
        return -1;

    //创建overlay
    if (!vp->bmp || !vp->allocated ||
        vp->width  != src_frame->width ||
        vp->height != src_frame->height ||
        vp->format != src_frame->format) {

        if (vp->width != src_frame->width || vp->height != src_frame->height)
            ffp_notify_msg3(ffp, FFP_MSG_VIDEO_SIZE_CHANGED, src_frame->width, src_frame->height);

        vp->allocated = 0;
        vp->width = src_frame->width;
        vp->height = src_frame->height;
        vp->format = src_frame->format;

        alloc_picture(ffp, src_frame->format);

        if (is->videoq.abort_request)
            return -1;
    }

    //填充overlay
    if (vp->bmp) {
        SDL_VoutLockYUVOverlay(vp->bmp);
        if (SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame) < 0) {
            av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
            exit(1);
        }
        SDL_VoutUnlockYUVOverlay(vp->bmp);

        //和ffplay类似,保存frame信息。不同的是,不保存frame数据
        vp->pts = pts;
        vp->duration = duration;
        vp->pos = pos;
        vp->serial = serial;
        vp->sar = src_frame->sample_aspect_ratio;
        vp->bmp->sar_num = vp->sar.num;
        vp->bmp->sar_den = vp->sar.den;

        frame_queue_push(&is->pictq);
        if (!is->viddec.first_frame_decoded) {
            ALOGD("Video: first frame decoded\n");
            ffp_notify_msg1(ffp, FFP_MSG_VIDEO_DECODED_START);
            is->viddec.first_frame_decoded_time = SDL_GetTickHR();
            is->viddec.first_frame_decoded = 1;
        }
    }

    return 0;
}

可以看到queue_picture中做了一些“绘图”相关的操作,即把AVFrame的数据绘制到vout overlay上。(这样就可以在显示的时候不关心具体的解码类型了)

创建overlay

正常情况下vp->bmp创建一次后可重复使用,不需要重新创建。只有格式变化后,才需要调用alloc_picture重新创建。

static void alloc_picture(FFPlayer *ffp, int frame_format)
{
    //……
    vp = &is->pictq.queue[is->pictq.windex];	//取当前要写入的帧,即peek_writabe的帧
    free_picture(vp);													//SDL_VoutFreeYUVOverlay(vp->bmp);

    SDL_VoutSetOverlayFormat(ffp->vout, ffp->overlay_format);

    vp->bmp = SDL_Vout_CreateOverlay(vp->width, vp->height,
                                   frame_format,
                                   ffp->vout);
    //……
    SDL_LockMutex(is->pictq.mutex);
    vp->allocated = 1;
    SDL_CondSignal(is->pictq.cond);
    SDL_UnlockMutex(is->pictq.mutex);
}

alloc_picture主要是通过调用SDL_Vout的接口,根据frame_format来创建一个Overlay。在ijkplayer video显示分析中分析过对于android默认通过SDL_VoutAndroid_CreateForAndroidSurface创建vout。该vout实现对应的overlay创建函数是:

//SDL_LockMutex(vout->mutex);
static SDL_VoutOverlay *func_create_overlay_l(int width, int height, int frame_format, SDL_Vout *vout)
{
    switch (frame_format) {
    case IJK_AV_PIX_FMT__ANDROID_MEDIACODEC:
        return SDL_VoutAMediaCodec_CreateOverlay(width, height, vout);
    default:
        return SDL_VoutFFmpeg_CreateOverlay(width, height, frame_format, vout);
    }
}
//SDL_UnlockMutex(vout->mutex);

如果配置的是硬解,这里的frame_format将是IJK_AV_PIX_FMT__ANDROID_MEDIACODEC,会创建mediacodec“显示”所需的overlay,否则,创建的ffmpeg overlay。

填充overlay

overlay的填充是调用的SDL_VoutFillFrameYUVOverlay,即overlay->func_fill_frame。对于软解调用的是ijksdl_vout_overlay_ffmpeg中的func_fill_frame

static int func_fill_frame(SDL_VoutOverlay *overlay, const AVFrame *frame)
{
    //……
    //1. 根据format决定后面的填充方法
    switch (overlay->format) {
        case SDL_FCC_YV12:
            need_swap_uv = 1;
            // no break;
        case SDL_FCC_I420:
            if (frame->format == AV_PIX_FMT_YUV420P || frame->format == AV_PIX_FMT_YUVJ420P) {
                // ALOGE("direct draw frame");
                use_linked_frame = 1;
                dst_format = frame->format;
            } else {
                // ALOGE("copy draw frame");
                dst_format = AV_PIX_FMT_YUV420P;
            }
            break;
        //……
    }

    //2. 准备内部frame用于填充;如果是use_linked_frame,则引用输入参数frame;否则开辟内存,存放到managed_frame
    if (use_linked_frame) {
        // linked frame
        av_frame_ref(opaque->linked_frame, frame);

        overlay_fill(overlay, opaque->linked_frame, opaque->planes);

        if (need_swap_uv)
            FFSWAP(Uint8*, overlay->pixels[1], overlay->pixels[2]);
    } else {
        // managed frame
        AVFrame* managed_frame = opaque_obtain_managed_frame_buffer(opaque);
        if (!managed_frame) {
            ALOGE("OOM in opaque_obtain_managed_frame_buffer");
            return -1;
        }

        overlay_fill(overlay, opaque->managed_frame, opaque->planes);

        // setup frame managed
        for (int i = 0; i < overlay->planes; ++i) {
            swscale_dst_pic.data[i] = overlay->pixels[i];
            swscale_dst_pic.linesize[i] = overlay->pitches[i];
        }

        if (need_swap_uv)
            FFSWAP(Uint8*, swscale_dst_pic.data[1], swscale_dst_pic.data[2]);
    }

    //3. 执行填充动作;对于use_linked_frame引用frame后即已填充好;否则需要用sws_scale转化到目标格式(可能是为了方便opengl绘图)
    if (use_linked_frame) {
        // do nothing
    } else if (ijk_image_convert(frame->width, frame->height,
                                 dst_format, swscale_dst_pic.data, swscale_dst_pic.linesize,
                                 frame->format, (const uint8_t**) frame->data, frame->linesize)) {
        opaque->img_convert_ctx = sws_getCachedContext(opaque->img_convert_ctx,
                                                       frame->width, frame->height, frame->format, frame->width, frame->height,
                                                       dst_format, opaque->sws_flags, NULL, NULL, NULL);
        if (opaque->img_convert_ctx == NULL) {
            ALOGE("sws_getCachedContext failed");
            return -1;
        }

        sws_scale(opaque->img_convert_ctx, (const uint8_t**) frame->data, frame->linesize,
                  0, frame->height, swscale_dst_pic.data, swscale_dst_pic.linesize);

        if (!opaque->no_neon_warned) {
            opaque->no_neon_warned = 1;
            ALOGE("non-neon image convert %s -> %s", av_get_pix_fmt_name(frame->format), av_get_pix_fmt_name(dst_format));
        }
    }
}

在queue_picture中将frame填充到opaque后,就可以调用SDL_VoutDisplayYUVOverlay去显示了。显示的逻辑可以参考ffplay video显示线程分析和ijkplayer video显示分析。

视频显示

ffplay基于sdl显示图像,ijkplayer在显示上摒弃了sdl,而是另辟蹊径封装了一套自己的显示接口。

基础概念

还是从显示函数开始看起(ff_ffplay.c):

stream_open -> SDL_CreateThreadEx video_refresh_thread
    ->video_refresh
        ->video_display2
            ->video_image_display2
                ->SDL_VoutDisplayYUVOverlay

整个调用链和ffplay保持一致,只是显示线程从主线程改变了,到了一个独立线程中。最后在显示一帧图像的时候调用的是SDL_VoutDisplayYUVOverlay

int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    if (vout && overlay && vout->display_overlay)
        return vout->display_overlay(vout, overlay);

    return -1;
}

SDL_VoutDisplayYUVOverlay的函数里,我们看到了两个新的概念:SDL_VoutSDL_VoutOverlay

这两个是ijk中才有的概念,是为了封装“显示上下文”和“显示层”准备的。

SDL_Vout

ijk中使用SDL_Vout表示一个显示上下文,或者理解为一块画布,比较接近于SDL中的Render。SDL_VoutOverlay表示显示层,或者理解为一块图像数据,比较接近于SDL中的Texture。

SDL_Vout的定义如下:

struct SDL_Vout {
    SDL_mutex *mutex;
    SDL_Class       *opaque_class;
    SDL_Vout_Opaque *opaque;
    SDL_VoutOverlay *(*create_overlay)(int width, int height, int frame_format, SDL_Vout *vout);
    void (*free_l)(SDL_Vout *vout);
    int (*display_overlay)(SDL_Vout *vout, SDL_VoutOverlay *overlay);

    Uint32 overlay_format;
};

定义了一个“类/接口”,支持的方法有create_overlayfree_ldisplay_overlay。其中,最重要的是display_overlay方法,即如何去呈现一个overlay。

既然是一个接口,就有对应的实现类:

  • dummy: 这是一个空实现,定义在ijk_sdl_vout_dummy.c;
  • android surface vout: 这是基于Android的surface实现的,定义在ijk_sdl_android_surface.c,ijk_vout_android_nativewindow.c;

SDL_VoutOverlay

SDL_VoutOverlay的定义如下:

struct SDL_VoutOverlay {
    int w; /**< Read-only */
    int h; /**< Read-only */
    Uint32 format; /**< Read-only */
    int planes; /**< Read-only */
    Uint16 *pitches; /**< in bytes, Read-only */
    Uint8 **pixels; /**< Read-write */

    int is_private;

    int sar_num;
    int sar_den;

    SDL_Class               *opaque_class;
    SDL_VoutOverlay_Opaque  *opaque;

    void    (*free_l)(SDL_VoutOverlay *overlay);
    int     (*lock)(SDL_VoutOverlay *overlay);
    int     (*unlock)(SDL_VoutOverlay *overlay);
    void    (*unref)(SDL_VoutOverlay *overlay);

    int     (*func_fill_frame)(SDL_VoutOverlay *overlay, const AVFrame *frame);
};

定义的方法有free_l/lock/unlock/unref/func_fill_frame,其中最重要的是func_fill_frame,也就是把AVFrame的图像“画”到overlay上。

它也有对应的几种实现:

  • ffmpeg overlay:用于软解绘图,主要是内存图像数据的格式转换。定义在ijksdl_vout_ffmpeg_overlay.c;
  • mediacodec overlay:用于mediacodec硬解绘图,主要用于MediaCodec的buffer index wrap和管理。定义在ijksdl_vout_overlay_android_mediacodec.c。(mediacodec可以直接绑定Surface解码,以提升效率,这种方式的解码是拿不到图像数据的,所以这里要wrap index);

目录结构

上面提到的一些结构体和函数,都在目录ijkmedia/ijksdl/下:

+ ijkmedia/ijksdl
    - ijk_sdl.h                             //包含其他sdl头文件
    - ijksdl_vout.h/c                       //封装层,提供SDL_VoutXXX的函数调用vout和overlay
    - ijksdl_vout_internal.h                //ijksdl目录内部使用的一些util函数
    + dummy                                 //dummy vout实现
    + ffmpeg
        - ijksdl_vout_overlay_ffmpeg.h/c    //ffmpeg overlay实现
    + android
        - ijksdl_vout_android_nativewindow.c//android vout的主要实现
        - ijksdl_vout_android_surface.h/c   //android vout与Java层Surface连接层
        - ijksdl_vout_overlay_android_mediacodec.h/c //mediacodec overlay实现

目录里还包括的timer/mutex/thread等的封装,另外音频相关的sdl封装也在这个目录里。

流程分析

Android上的SDL_Vout是通过ijksdl_vout_android_surface.c中的SDL_VoutAndroid_CreateForAndroidSurface函数创建的,对应的SDL_Vout的实现在ijksdl_vout_android_nativewindow.c。

SDL_VoutAndroid_CreateForAndroidSurface调用流程如下:

new IjkMediaPlayer() 
    -> initPlayer() 
        -> native_setup() 
            -> IjkMediaPlayer_native_setup() 
                -> ijkmp_android_create() 
                    -> SDL_VoutAndroid_CreateForAndroidSurface()

SDL_Vout创建后就可以用来显示SDL_VoutOverlay了,overlay的创建和填充是在解码线程中完成

前面分析了overlay的显示是在video_display2中调用SDL_VoutDisplayYUVOverlay显示的。SDL_VoutDisplayYUVOverlay只是封装了具体SDL_Vout实现类的display_overlay方法。对于Android,对应的是ijksdl_vout_android_nativewindow.c中的func_display_overlay

static int func_display_overlay(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    SDL_LockMutex(vout->mutex);
    int retval = func_display_overlay_l(vout, overlay);
    SDL_UnlockMutex(vout->mutex);
    return retval;
}

加锁调用func_display_overlay_l(精简了代码,直接看正常流程代码):

static int func_display_overlay_l(SDL_Vout *vout, SDL_VoutOverlay *overlay)
{
    switch(overlay->format) {
    case SDL_FCC__AMC: {
        // only ANativeWindow support
        IJK_EGL_terminate(opaque->egl);
        return SDL_VoutOverlayAMediaCodec_releaseFrame_l(overlay, NULL, true);
    }
    case SDL_FCC_RV24:
    case SDL_FCC_I420:
    case SDL_FCC_I444P10LE: {
        // only GLES support
        if (opaque->egl)
            return IJK_EGL_display(opaque->egl, native_window, overlay);
        break;
    }
    case SDL_FCC_YV12:
    case SDL_FCC_RV16:
    case SDL_FCC_RV32: {
        // both GLES & ANativeWindow support
        if (vout->overlay_format == SDL_FCC__GLES2 && opaque->egl)
            return IJK_EGL_display(opaque->egl, native_window, overlay);
        break;
    }
    }

    // fallback to ANativeWindow
    IJK_EGL_terminate(opaque->egl);
    return SDL_Android_NativeWindow_display_l(native_window, overlay); 
}

这里主要是根据传入overlay的format选择具体的显示方法。这里有3种显示方法:

  • 如果是SDL_FCC__AMC,则是MediaCodec的特定format,用SDL_VoutOverlayAMediaCodec_releaseFrame_l显示;
  • 如果是其他EGL支持的格式,则用IJK_EGL_display显示;
  • 最后,如果都无法显示,就Fallback到直接用ndk nativewindow的api显示;

其中第一种是针对MediaCodec的硬解显示方式,后两种都是软解的显示方式。

硬解显示

MediaCodec是Android硬解的统一API,方便了不同芯片厂商接入。MediaCodec解码时设置一个Surface以减少显示时的数据拷贝,可以提高效率。此时解码后拿到的是一个index,并非解码后的图像数据,ijk中将其封装为SDL_AMediaCodecBufferProxy,定义在jksdl_vout_android_nativewindow.c中:

struct SDL_AMediaCodecBufferProxy
{
    int buffer_id;
    int buffer_index;
    int acodec_serial;
    SDL_AMediaCodecBufferInfo buffer_info;
};

SDL_AMediaCodecBufferProxy的实例在android overlay的SDL_VoutOverlay_Opaque中定义:

typedef struct SDL_VoutOverlay_Opaque {
    SDL_mutex *mutex;

    SDL_Vout                   *vout;
    SDL_AMediaCodec            *acodec;
		
  	//这个是dequeueOutputBuffer的封装
    SDL_AMediaCodecBufferProxy *buffer_proxy;

    Uint16 pitches[AV_NUM_DATA_POINTERS];
    Uint8 *pixels[AV_NUM_DATA_POINTERS];
} SDL_VoutOverlay_Opaque;

MediaCodec要显示一帧,是通过调用releaseOutputBuffer通知MediaCodec该显示哪一帧的。这在ijk中是封装为SDL_AMediaCodec_releaseOutputBuffer

回到刚才的思路,看下SDL_VoutOverlayAMediaCodec_releaseFrame_l函数:

int  SDL_VoutOverlayAMediaCodec_releaseFrame_l(SDL_VoutOverlay *overlay, SDL_AMediaCodec *acodec, bool render)
{
    if (!check_object(overlay, __func__))
        return -1;

    SDL_VoutOverlay_Opaque *opaque = overlay->opaque;
    return SDL_VoutAndroid_releaseBufferProxyP_l(opaque->vout, &opaque->buffer_proxy, render);
}

SDL_VoutAndroid_releaseBufferProxyP_l调用了SDL_VoutAndroid_releaseBufferProxy_l

//这里省略了打印调试信息的代码
static int SDL_VoutAndroid_releaseBufferProxy_l(SDL_Vout *vout, SDL_AMediaCodecBufferProxy *proxy, bool render)
{
    SDL_Vout_Opaque *opaque = vout->opaque;

    //归还到SDL_AMediaCodecBufferProxy对象池
    ISDL_Array__push_back(&opaque->overlay_pool, proxy);

    //如果serial已经变化,说明该帧已经无效(比如是seek前的帧),不显示,丢弃
    if (!SDL_AMediaCodec_isSameSerial(opaque->acodec, proxy->acodec_serial)) {
        return 0;
    }

    //buffer_index即dequeueOutputBuffer的返回值,小于0说明不是一个图像帧(具体参考官方API),无需显示
    if (proxy->buffer_index < 0) {
        return 0;
    } 
    //FAKE_FRAME是一个特殊标志,表示当前帧只是占位,不应该显示(具体分析将在ijkplay硬解一文分析)
    else if (proxy->buffer_info.flags & AMEDIACODEC__BUFFER_FLAG_FAKE_FRAME) {
        proxy->buffer_index = -1;
        return 0;
    }

    //到这里,就可以显示了
    sdl_amedia_status_t amc_ret = SDL_AMediaCodec_releaseOutputBuffer(opaque->acodec, proxy->buffer_index, render);    
    if (amc_ret != SDL_AMEDIA_OK) {//显示失败,返回-1
        proxy->buffer_index = -1;
        return -1;
    }

    proxy->buffer_index = -1;
    return 0;
}

上面代码,主要做了3件事情:

  1. 归还proxy到SDL_AMediaCodecBufferProxy对象池。因为解码的调用很频繁,如果重复分配释放proxy对象,内存压力会比较大,所以这里引入了一个对象池进行优化;
  2. 做一些合法性检查。比如检查serial变化、检查index值、检查占位符等;
  3. 调用SDL_AMediaCodec_releaseOutputBuffer(也就是MediaCodec.releaseOutputBuffer)显示;

音频输出

ijkplayer在Android上的的音频输出支持opensles和AudioTrack。

概念

音频输出被抽象为SDL_Aout:

struct SDL_Aout {
//……
    SDL_Class       *opaque_class;
    SDL_Aout_Opaque *opaque;
    void (*free_l)(SDL_Aout *vout);
    int (*open_audio)(SDL_Aout *aout, const SDL_AudioSpec *desired, SDL_AudioSpec *obtained);
    void (*pause_audio)(SDL_Aout *aout, int pause_on);
    void (*flush_audio)(SDL_Aout *aout);
    void (*set_volume)(SDL_Aout *aout, float left, float right);
    void (*close_audio)(SDL_Aout *aout);
//……
};
// SDL_Aout由pipeline创建:
struct IJKFF_Pipeline {
    //……
    SDL_Aout       *(*func_open_audio_output)   (IJKFF_Pipeline *pipeline, FFPlayer *ffp);
    //……
}

android上ffpipeline_android.c根据选项opensles创建具体的SDL_Aout:

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;
}

和SDL_Aout相关的目录结构如下:

+ijkmedia/ijkplayer
    -ff_ffpipeline.h/c              //pipeline实现
    +android/pipeline
        -ffpipeline_android.h/c     //android pipeline实现

+ijksdl
    -ijksdl_aout.h/c                //SDL_Aout定义、封装
    +android
        -ijksdl_aout_android_audiotrack.h/c     //SDL_Aout的AudioTrack实现
        -ijksdl_aout_android_opensles.h/c       //SDL_Aout的opensles实现

对于SDL_Aout的使用流程基本和ffplay一样,不再展开分析。

AudioTrack实现分析

AudioTrack输出实现的SDL_Aout在文件ijksdl_aout_android_audiotrack.h/c中。AudioTrack aout的主要实现是一个循环线程aout_thread_n,该线程在aout_open_audio中创建。其他操作都是通过变量的改变来通知循环线程生效的,比如flush:

static void aout_flush_audio(SDL_Aout *aout)
{
    SDL_Aout_Opaque *opaque = aout->opaque;
    SDL_LockMutex(opaque->wakeup_mutex);
    SDLTRACE("aout_flush_audio()");
    opaque->need_flush = 1;
    SDL_CondSignal(opaque->wakeup_cond);
    SDL_UnlockMutex(opaque->wakeup_mutex);
}

先是置了一个need_flush标志,然后通过条件变量通知线程处理。

接下来就看下aout_thread_n的实现:

static int aout_thread_n(JNIEnv *env, SDL_Aout *aout)
{
    SDL_AudioCallback audio_cblk = opaque->spec.callback;//这就是ff_ffplay的sdl_audio_callback
    int copy_size = 256;//每次要求ff_ffplay给256个字节的音频数据

    SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);//音频输出线程设置为高优先级,保证音频流畅输出

    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);//AudioTrack.pause
            while (!opaque->abort_request && opaque->pause_on) {//循环超时等待信号
                SDL_CondWaitTimeout(opaque->wakeup_cond, opaque->wakeup_mutex, 1000);
            }
            //恢复播放
            if (!opaque->abort_request && !opaque->pause_on) {
                if (opaque->need_flush) {//如果有flush请求,处理flush
                    opaque->need_flush = 0;
                    SDL_Android_AudioTrack_flush(env, atrack);//AudioTrack.flush
                }
                SDL_Android_AudioTrack_play(env, atrack);//AudioTrack.play
            }
        }
        //如果有设置音量请求,设置音量
        if (opaque->need_set_volume) {
            opaque->need_set_volume = 0;
            SDL_Android_AudioTrack_set_volume(env, atrack, opaque->left_volume, opaque->right_volume);//AudioTrack.setVolume
        }
        //如果有变速请求,处理变速
        if (opaque->speed_changed) {
            opaque->speed_changed = 0;
            SDL_Android_AudioTrack_setSpeed(env, atrack, opaque->speed);//AudioTrack.setPlaybackRate
        }
        SDL_UnlockMutex(opaque->wakeup_mutex);

        //找ff_ffplay要解码后音频数据,一次256字节
        audio_cblk(userdata, buffer, copy_size);

        //AudioTrack.write写出
        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);
        }
    }

    SDL_Android_AudioTrack_free(env, atrack);//AudioTrack.release释放
}

源码中有不只一次处理flush请求,并调用flush。然而根据官方文档说明flush函数会“No-op if not stopped or paused”,即不在暂停或停止状态调用flush是无效的,所以为了理解方便,上面只保留了“有效”的一处flush。

aout_thread_n的代码逻辑比较清晰整体分3个部分:

  • 在加锁区,判断是否有未处理的请求,如果是,则处理该请求。这些请求是aout_flush_audio等SDL_Aout的函数发起的;
  • 调用audio_cblk(即sdl_audio_callback)要解码后的音频数据;
  • 通过AudioTrack.write写出;

MediaCodec

vHcOqs.png
  • dequeueinputBuffer(从input缓冲队列申请empty buffer,左边的上虚线);
  • inputbuffer(拷贝mp4文件的一帧到empty buffer);
  • queueInputBuffe(将inputbuffer放回codec);
  • dequeueOutputBuffer(从output缓冲区队列申请编解码后的buffer);
  • 编码后的数据渲染;
  • releaseOutputBuffer(放回到output缓冲区队列);

packetQueue和frameQueue

packetQueue采用两条链表,一个是保存数据的链表,一个是复用节点链表,保存没有数据的那些节点。数据链表从first_pktlast_pkt,插入数据接到last_pkt的后面,取数据从first_pkt拿。复用链表的开头是recycle_pkt,取完数据后的空节点,放到空链表recycle_pkt的头部,然后这个空节点成为新的recycle_pkt。存数据时,也从recycle_pkt复用一个节点。

链表的节点像是包装盒,装载数据的时候放到数据链表,数据取出后回归到复用链表。

frameQueue:数据使用一个简单的数组保存,可以把这个数据看成是环形的,然后也是其中一段有数据,另一段没有数据。rindex表示数据开头的index,也是读取数据的index,即read index;windex表示空数据开头的index,是写入数据的index,即write index。也是不断循环重用,然后size表示当前数据大小,max_size表示最大的槽位数,写入的时候如果size满了,就会阻塞等待;读取的时候size为空,也会阻塞等待。

读取的时候不是读的rindex位置的数据,而是rindex+rindex_shown。rindex_shown 表示 rindex 指向的节点是否已经显示,如果已经显示则为1,否则为0。

rindex 表示上一次播放的帧 lastvp,本次调用 video_refresh() 则 lastvp 会被删除,rindex 会加 1,即当调用 frame_queue_next 删除的是 lastvp,而不是当前的 vp,当前的 vp 转为 lastvp;rindex + rindex_shown 表示本次待播放的帧 vp,本次调用 video_refresh() 中,vp 会被读出播放;rindex + rindex_shown + 1 表示下一帧 nextvp。

注意,在启用 keep_last 机制后,rindex_shown 值总是为 1,rindex_shown 确保了最后播放的一帧总保留在队列中。

ijkplayerItem

AVPlayerItem:管理资源对象,提供播放数据源。

item_read_thread:

static int item_read_thread(void * context)
{
		......
		avf_inner = avformat_alloc_context();
		// 初始化
  	ret = avformat_open_input(&avf_inner, item->url, NULL, &format_opts);
  	avf = avformat_alloc_context();
    for (i = 0; i < avf_inner->nb_streams; i++) {
        AVStream *st ;
        st = avformat_new_stream(avf, NULL);
        ret = copy_stream_props(st, avf_inner->streams[i]);
        if (st->codecpar && st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
            has_audio = 1;
    }
		ret = av_read_frame(avf_inner, pkt);
		item_packet_queue_put(&item->queue, pkt);

参考

  • 视频播放之AVPlayer、AVPlayerItem、AVPlayerLayer - 时光清浅、 - 博客园 (cnblogs.com)
  • 播放器网络带宽预测方法_童话阿噗的博客-CSDN博客_带宽预测
  • ijkplayer-hook协议实现分析_一朵桃花压海棠的博客-CSDN博客

参考资料

  • Android视频播放软解与硬解的区别_Dawish_大D的博客-CSDN博客_android硬解码与软解码
  • 什么是JNI?为什么会有Native层?如何使用? - 简书 (jianshu.com)
  • 开源播放器 ijkplayer (一) :使用Ijkplayer播放直播视频 - 灰色飘零 - 博客园 (cnblogs.com)
  • Android ijkplayer详解使用教程 - 星辰之力 - 博客园 (cnblogs.com)
  • ijkplayer-android框架详解_Suk_39799839的博客-CSDN博客_ijkplayer
  • ijkplayer中遇到的问题汇总 - 知乎 (zhihu.com)
  • ijkplayer源码分析 整体概述_baiiu的博客-CSDN博客_ijkplayer源码解析
  • ijkplayer 源码分析 - 简书 (jianshu.com)
  • 带问题重读ijkPlayer - 简书 (jianshu.com)
  • Android NDK MediaCodec在ijkplayer中的实践 - 简书 (jianshu.com)
  • ijkplayer 解码框架分析 - 知乎 (zhihu.com)
  • ijkplayer 解码实现分析——硬解篇 - 知乎 (zhihu.com)
  • 初识MediaCodec - 知乎 (zhihu.com)
  • ijkplayer video显示分析 - 知乎 (zhihu.com)
  • ijkplayer audio输出分析 - 知乎 (zhihu.com)
  • 音视频技术 - 知乎 (zhihu.com)
  • MediaCodec编解码流程 - 简书 (jianshu.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/4448.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ Reference: Standard C++ Library reference: C Library: cwchar: wmemset

C官网参考链接&#xff1a;https://cplusplus.com/reference/cwchar/wmemset/ 函数 <cwchar> wmemset wchar_t* wmemset (wchar_t* ptr, wchar_t wc, size_t num); 填充宽字符数组 将由ptr指向的宽字符数组的第一个num个元素设置为wc指定的值。 这是memset&#xff08;&…

瑞吉外卖强化(一):缓存优化

瑞吉外卖强化&#xff08;一&#xff09;&#xff1a;缓存优化瑞吉外卖 缓存优化Redis基本操作短信验证码 缓存实现缓存菜品数据SpringCache常用注解瑞吉外卖 缓存优化 Redis基本操作 redisTemplate需要配置类 这里的 需要对其进行 序列化操作 reidsTeplate.opsForValue().s…

HummerRisk 快速入门教程

1、一键部署 1. 部署服务器要求 操作系统要求&#xff1a;任何支持 Docker 的 Linux x64CPU内存要求&#xff1a;最低要求 4C8G&#xff0c;推荐 8C16G部署目录空间&#xff08;默认/opt目录&#xff09;要求&#xff1a; 50G网络要求&#xff1a;可访问互联网&#xff08;如…

Recall:JS EventLoop

有时候一段代码没有达到你想要的效果&#xff0c;可能加上setTimeout就好了 之前对事件循环一知半解&#xff0c;今天重新深入理解一下&#x1f602; 宏任务 JS是单线程的&#xff0c;但是浏览器是多线程的&#xff0c;当 JS 需要执行异步任务时&#xff0c;浏览器会另外启…

企业架构概述及业务架构详解

编辑导语&#xff1a;企业架构可以辅助企业完成业务及IT战略规划&#xff0c;还是企业信息化规划的核心&#xff0c;也有助于个人职业的健康长远发展。本文作者对企业架构的全景以及业务架构设计进行了分析&#xff0c;感兴趣的小伙伴们一起来看一下吧。 1&#xff09;对公司而…

PyTorch 加载 Mask R-CNN 预训练模型并 fine-tuning

目录1 Mask R-CNN 原理(简单版)2 ROI Align3 PyTorch 加载预训练模型1 Mask R-CNN 原理(简单版) Mask R-CNN 是一个实例分割&#xff08;Instance segmentation&#xff09;算法&#xff0c;主要是在目标检测的基础上再进行分割。 Mask R-CNN 算法主要是 Faster R-CNN FCN&…

算法练习题(涉外黄成老师)

1.带锁的门在走廊上有n个带锁的门&#xff0c;从1到n依次编号。最初所有的门都是关着的。我们从门前经过n次&#xff0c;每一次都从1号门开始。在第i次经过时(i1,2,…,n)我们改变i的整数倍号锁的状态:如果门是关的&#xff0c;就打开它;如果门是打开的&#xff0c;就关上它。在…

CEC2015:(二)动态多目标野狗优化算法DMODOA求解DIMP2、dMOP2、dMOP2iso、dMOP2dec(提供Matlab代码)

一、cec2015中测试函数DIMP2、dMOP2、dMOP2iso、dMOP2dec详细信息 CEC2015&#xff1a;动态多目标测试函数之DIMP2、dMOP2、dMOP2iso、dMOP2dec详细信息 二、动态多目标野狗优化算法 多目标野狗优化算法&#xff08;Multi-Objective Dingo Optimization Algorithm&#xff0…

#入坑keychron#你还没一起入坑吗?

经济和科技飞速发展的今天&#xff0c;我们早已不在像从前那样有电脑玩就行&#xff0c;现在的我们追求的是更高的配置、更好的体验&#xff0c;就像从前一碗泡面就是最高的理想&#xff0c;而现在最少都得有根泡面搭档才能勉强接受&#xff0c;连泡面都有搭档&#xff0c;电脑…

web前端期末大作业:旅游网页设计与实现——个人旅游博客(4页)HTML+CSS

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

【后端】初识HTTP_2

我们学习的HTTP协议&#xff0c;是应用层里面最广泛使用的协议~ 我们主要是学习HTTP的请求响应的报文格式 我们可以借助抓包工具来学习&#xff0c;抓包抓到的是文本格式~~ 根据上节内容 我们大概了解了请求和响应的格式 请求有4部分&#xff1a; &#xff08;1&#xff…

leetcode 51. N皇后 回溯法求解(c++版本)

题目描述 简单来说就给一个N*N的棋盘 棋盘上的每一列每一行以及每一个对角不能出现两个皇后 因此明确以下几点 要找出所有可能的解法也是采用回溯法进行求解&#xff08;具体在下面进行详解&#xff09; 用下面一张示例图来说明回溯法的思路 说白了就是进行搜索&#xff0c;…

java项目-第102期基于ssm的校园二手交易平台-java毕业设计

java项目-第102期基于ssm的校园二手交易平台 【源码请到资源专栏下载】 1、项目简述 Hi&#xff0c;大家好&#xff0c;今天分享的源码是基于ssm的校园二手交易平台。 该交易平台分为两部分&#xff0c;前台和后台。用户在前台进行商品选购以及交易&#xff1b;管理员登录后台可…

python-(6-3-3)爬虫---requests入门(对参数封装)

文章目录一 需求二 分析三 代码四 补充说明一 需求 爬取豆瓣电影的“纪录片”的电影信息数据 二 分析 老规矩&#xff0c;先在网页的“检查”中提取我们需要的信息 如下图所示。在“纪录片”那一页面&#xff0c;选择"network"----“XHR”----“preview”。 我们…

【附源码】Python计算机毕业设计面向社区的购物平台系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

java计算机毕业设计ssm+vue网络考试信息网站

项目介绍 对网络考试系统进行了介绍&#xff0c;包括研究的现状&#xff0c;还有涉及的开发背景&#xff0c;然后还对系统的设计目标进行了论述&#xff0c;还有系统的需求&#xff0c;以及整个的设计方案&#xff0c;对系统的设计以及实现&#xff0c;也都论述的比较细致&…

五大模型看深度学习用于时序预测的最新进展

引言 在以往的时序预测中&#xff0c;大部分使用的是基于统计和机器学习的一些方法。然而&#xff0c;由于深度学习在时间序列的预测中表现并不是很好&#xff0c;且部分论文表述&#xff0c;在训练时间方面&#xff0c;用 Transformer、Informer 、Logtrace 等模型来做时间序…

ESP32 入门笔记06: FreeRTOS+《两只老虎》 (ESP32 for Arduino IDE)

ESP32FreeRTOS Esp32 模块中已经提供了 FreeRTOS&#xff08;实时操作系统&#xff09;固件。 FreeRTOS有助于提高系统性能和管理模块的资源。FreeRTOS允许用户处理多项任务&#xff0c;如测量传感器读数&#xff0c;发出网络请求&#xff0c;控制电机速度等&#xff0c;所有…

旅游推荐系统

摘要 随着社会的发展&#xff0c;人们生活水平的提高&#xff0c;旅游逐渐成为人们生活中的重要活动&#xff0c;2019年国内旅游人数超过60亿人次。并且&#xff0c;旅游业已经成为了我国经济发展的一个重要支柱&#xff0c;近年来我国旅游业对GDP贡献值呈上升趋势。2019年&am…

【附源码】计算机毕业设计java装修服务分析系统设计与实现

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…