百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

ijkplayer框架深入剖析

toyiye 2024-06-21 12:20 9 浏览 0 评论

随着互联网技术的飞速发展,移动端播放视频的需求如日中天,由此也催生了一批开源/闭源的播放器,但是无论这个播放器功能是否强大、兼容性是否优秀,它的基本模块通常都是由以下部分组成:事务处理、数据的接收和解复用、音视频解码以及渲染,其基本框架如下图所示:

播放器基本框图

针对各种铺天盖地的播放器项目,我们选取了比较出众的ijkplayer进行源码剖析。它是一个基于FFPlay的轻量级Android/iOS视频播放器,实现了跨平台的功能,API易于集成;编译配置可裁剪,方便控制安装包大小。

本文基于k0.7.6版本的ijkplayer,重点分析其C语言实现的核心代码,涉及到不同平台下的封装接口或处理方式时,均以iOS平台为例,Android平台大同小异,请大家自行查阅研究。

一、总体说明

打开ijkplayer,可看到其主要目录结构如下:tool - 初始化项目工程脚本

config - 编译ffmpeg使用的配置文件

extra - 存放编译ijkplayer所需的依赖源文件, 如ffmpeg、openssl等

ijkmedia - 核心代码

ijkplayer - 播放器数据下载及解码相关

ijksdl - 音视频数据渲染相关

ios - iOS平台上的上层接口封装以及平台相关方法

android - android平台上的上层接口封装以及平台相关方法

在功能的具体实现上,iOS和Android平台的差异主要表现在视频硬件解码以及音视频渲染方面,两者实现的载体区别如下表所示:

C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

二、初始化流程

初始化完成的主要工作就是创建播放器对象,打开ijkplayer/ios/IJKMediaDemo/IJKMediaDemo.xcodeproj工程,可看到IJKMoviePlayerViewController类中viewDidLoad方法中创建了IJKFFMoviePlayerController对象,即iOS平台上的播放器对象。

- (void)viewDidLoad
{
    ......
    self.player = [[IJKFFMoviePlayerController alloc] initWithContentURL:self.url withOptions:options];
    ......
}

查看ijkplayer/ios/IJKMediaPlayer/IJKMediaPlayer/IJKFFMoviePlayerController.m文件,其初始化方法具体实现如下:

- (id)initWithContentURL:(NSURL *)aUrl
             withOptions:(IJKFFOptions *)options
{
    if (aUrl == nil)
        return nil;

    // Detect if URL is file path and return proper string for it
    NSString *aUrlString = [aUrl isFileURL] ? [aUrl path] : [aUrl absoluteString];

    return [self initWithContentURLString:aUrlString
                              withOptions:options];
}


- (id)initWithContentURLString:(NSString *)aUrlString
                   withOptions:(IJKFFOptions *)options
{
    if (aUrlString == nil)
        return nil;

    self = [super init];
    if (self) {
        ......
        // init player
        _mediaPlayer = ijkmp_ios_create(media_player_msg_loop);
        ......
    }
    return self;
}

可发现在此创建了IjkMediaPlayer结构体实例_mediaPlayer:

IjkMediaPlayer *ijkmp_ios_create(int (*msg_loop)(void*))
{
    IjkMediaPlayer *mp = ijkmp_create(msg_loop);
    if (!mp)
        goto fail;

    mp->ffplayer->vout = SDL_VoutIos_CreateForGLES2();
    if (!mp->ffplayer->vout)
        goto fail;

    mp->ffplayer->pipeline = ffpipeline_create_from_ios(mp->ffplayer);
    if (!mp->ffplayer->pipeline)
        goto fail;

    return mp;

fail:
    ijkmp_dec_ref_p(&mp);
    return NULL;
}

在该方法中主要完成了三个动作:

  1. 创建IJKMediaPlayer对象
IjkMediaPlayer *ijkmp_create(int (*msg_loop)(void*))
{
    IjkMediaPlayer *mp = (IjkMediaPlayer *) mallocz(sizeof(IjkMediaPlayer));
    ......
    mp->ffplayer = ffp_create();
    ......
    mp->msg_loop = msg_loop;
    ......
    return mp;
}
  • 通过ffp_create方法创建了FFPlayer对象,并设置消息处理函数。
  • 创建图像渲染对象SDL_Vout
  • SDL_Vout *SDL_VoutIos_CreateForGLES2()
    {
        SDL_Vout *vout = SDL_Vout_CreateInternal(sizeof(SDL_Vout_Opaque));
        if (!vout)
            return NULL;
    
        SDL_Vout_Opaque *opaque = vout->opaque;
        opaque->gl_view = nil;
        vout->create_overlay = vout_create_overlay;
        vout->free_l = vout_free_l;
        vout->display_overlay = vout_display_overlay;
    
        return vout;
    }

    创建平台相关的IJKFF_Pipeline对象,包括视频解码以及音频输出部分

    IJKFF_Pipeline *ffpipeline_create_from_ios(FFPlayer *ffp)
    {
        IJKFF_Pipeline *pipeline = ffpipeline_alloc(&g_pipeline_class, sizeof(IJKFF_Pipeline_Opaque));
        if (!pipeline)
            return pipeline;
    
        IJKFF_Pipeline_Opaque *opaque     = pipeline->opaque;
        opaque->ffp                       = ffp;
        pipeline->func_destroy            = func_destroy;
        pipeline->func_open_video_decoder = func_open_video_decoder;
        pipeline->func_open_audio_output  = func_open_audio_output;
    
        return pipeline;
    }
    

    至此已经完成了ijkplayer播放器初始化的相关流程,简单来说,就是创建播放器对象,完成音视频解码、渲染的准备工作。在下一章节中,会重点介绍播放的核心代码。

    C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

    三、核心代码剖析

    ijkplayer实际上是基于ffplay.c实现的,本章节将以该文件为主线,从数据接收、音视频解码、音视频渲染及同步这三大方面进行讲解,要求读者有基本的ffmpeg知识。

    ffplay.c中主要的代码调用流程如下图所示:

    ffplay代码调用流程图

    当外部调用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主要做了以下几件事情:

    • 创建存放video/audio解码前数据的videoq/audioq
    • 创建存放video/audio解码后数据的pictq/sampq
    • 创建读数据线程read_thread
    • 创建视频渲染线程video_refresh_thread

    说明:subtitle是与video、audio平行的一个stream,ffplay中也支持对它的处理,即创建存放解码前后数据的两个queue,并且当文件中存在subtitle时,还会启动subtitle的解码线程,由于篇幅有限,本文暂时忽略对它的相关介绍。

    3.1 数据读取

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

    1. 创建上下文结构体,这个结构体是最上层的结构体,表示输入上下文
    ic = avformat_alloc_context();

    2.设置中断函数,如果出错或者退出,就可以立刻退出

    ic->interrupt_callback.callback = decode_interrupt_cb;
    ic->interrupt_callback.opaque = is;

    3.打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等

    err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);

    4.探测媒体类型,可得到当前文件的封装格式,音视频编码参数等信息

    err = avformat_find_stream_info(ic, opts);

    5.打开视频、音频解码器。在此会打开相应解码器,并创建相应的解码线程。

    stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);

    6.读取媒体数据,得到的是音视频分离的解码前数据

    ret = av_read_frame(ic, pkt);

    7.将音视频数据分别送入相应的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步,即可不断获取待播放的数据。

    3.2 音视频解码

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

    3.2.1 视频解码方式选择

    在打开解码器的方法中:

    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_ios创建了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_ios.c中的func_open_video_decoder,其定义如下:

    static IJKFF_Pipenode *func_open_video_decoder(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
    {
        IJKFF_Pipenode* node = NULL;
        IJKFF_Pipeline_Opaque *opaque = pipeline->opaque;
        if (ffp->videotoolbox) {
           node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);
           if (!node)
              ALOGE("vtb fail!!! switch to ffmpeg decode!!!! \n");
         }
         if (node == NULL) {
            node = ffpipenode_create_video_decoder_from_ffplay(ffp);
            ffp->stat.vdec_type = FFP_PROPV_DECODER_AVCODEC;
            opaque->is_videotoolbox_open = false;
         } else {
            ffp->stat.vdec_type = FFP_PROPV_DECODER_VIDEOTOOLBOX;
            opaque->is_videotoolbox_open = true;
         }
         ffp_notify_msg2(ffp, FFP_MSG_VIDEO_DECODER_OPEN, opaque->is_videotoolbox_open);
         return node;
    }
    

    如果配置了ffp->videotoolbox,会优先去尝试打开硬件解码器,

    node = ffpipenode_create_video_decoder_from_ios_videotoolbox(ffp);

    如果硬件解码器打开失败,则会自动切换至软解

    node = ffpipenode_create_video_decoder_from_ffplay(ffp);

    ffp->videotoolbox需要在起播前通过如下方法配置:

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

    3.2.2 音视频解码

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

    不管视频解码还是音频解码,其基本流程都是从解码前的数据缓冲区中取出一帧数据进行解码,完成后放入相应的解码后的数据缓冲区,如下图所示:

    音视频解码示意图.

    本文以video的软解流程为例进行分析,audio的流程可对照研究。

    视频解码线程

    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取决于播放前配置的软硬解,假设为软解,则调用

    static int ffplay_video_thread(void *arg)
    {
        FFPlayer *ffp = arg;
        
        ......
    
        for (;;) {
            ret = get_video_frame(ffp, frame);
            ......
            ret = queue_picture(ffp, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial);
        }
        return 0;
    }
    

    get_video_frame中调用了decoder_decode_frame,其定义如下:

    static int decoder_decode_frame(FFPlayer *ffp, Decoder *d, AVFrame *frame, AVSubtitle *sub) {
        int got_frame = 0;
    
        do {
            int ret = -1;
            ......
            if (!d->packet_pending || d->queue->serial != d->pkt_serial){
                AVPacket pkt;
                do {
                    ......
                    if (packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0)
                        return -1;
                    ......
                } while (pkt.data == flush_pkt.data || d->queue->serial != d->pkt_serial);
                ......
            }
    
            switch (d->avctx->codec_type) {
                case AVMEDIA_TYPE_VIDEO: {
                    ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp);
                    ......
                   }
                    break;
            }
            ......
        } while (!got_frame && !d->finished);
    
        return got_frame;
    }
    

    该方法中从解码前的video queue中取出一帧数据,送入decoder进行解码,解码后的数据在ffplay_video_thread中送入pictq。

    3.3 音视频渲染及同步

    3.3.1 音频输出

    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_ios_create方法中被赋值,最后指向的是func_open_audio_output

    static SDL_Aout *func_open_audio_output(IJKFF_Pipeline *pipeline, FFPlayer *ffp)
    {
        return SDL_AoutIos_CreateForAudioUnit();
    }

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

    SDL_Aout *SDL_AoutIos_CreateForAudioUnit()
    {
        SDL_Aout *aout = SDL_Aout_CreateInternal(sizeof(SDL_Aout_Opaque));
        if (!aout)
            return NULL;
    
        // SDL_Aout_Opaque *opaque = aout->opaque;
    
        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->close_audio = aout_close_audio;
    
        aout->func_set_playback_rate = aout_set_playback_rate;
        aout->func_set_playback_volume = aout_set_playback_volume;
        aout->func_get_latency_seconds = auout_get_latency_seconds;
        aout->func_get_audio_persecond_callbacks = aout_get_persecond_callbacks;
        return aout;
    }
    

    回到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, iOS平台上即为AudioQueue。

    AudioQueue模块在工作过程中,通过不断的callback来获取pcm数据进行播放。

    有关AudioQueue的具体内容此处不再介绍。

    3.3.2 视频渲染

    iOS平台上采用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进行绘制
    int SDL_VoutDisplayYUVOverlay(SDL_Vout *vout, SDL_VoutOverlay   *overlay)
    {
        if (vout && overlay && vout->display_overlay)
            return vout->display_overlay(vout, overlay);
    
        return -1;
    }

    display_overlay函数指针在前面初始化流程有介绍过,它在

    SDL_Vout *SDL_VoutIos_CreateForGLES2()
    1. 方法中被赋值为vout_display_overlay,该方法就是调用OpengGL绘制图像。

    3.4.3 音视频同步

    对于播放器来说,音视频同步是一个关键点,同时也是一个难点,同步效果的好坏,直接决定着播放器的质量。通常音视频同步的解决方案就是选择一个参考时钟,播放时读取音视频帧上的时间戳,同时参考当前时钟参考时钟上的时间来安排播放。如下图所示:

    音视频同步示意图.

    如果音视频帧的播放时间大于当前参考时钟上的时间,则不急于播放该帧,直到参考时钟达到该帧的时间戳;如果音视频帧的时间戳小于当前参考时钟上的时间,则需要“尽快”播放该帧或丢弃,以便播放进度追上参考时钟。

    参考时钟的选择也有多种方式:

    • 选取视频时间戳作为参考时钟源
    • 选取音频时间戳作为参考时钟源
    • 选取外部时间作为参考时钟源

    考虑人对视频、和音频的敏感度,在存在音频的情况下,优先选择音频作为主时钟源。

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

    在compute_target_delay方法中,如果发现当前主时钟源不是video,则计算当前视频时钟与主时钟的差值:

    • 如果当前视频帧落后于主时钟源,则需要减小下一帧画面的等待时间;
    • 如果视频帧超前,并且该帧的显示时间大于显示更新门槛,则显示下一帧的时间为超前的时间差加上上一帧的显示时间
    • 如果视频帧超前,并且上一帧的显示时间小于显示更新门槛,则采取加倍延时的策略。

    回到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开始渲染。

    C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)

    四、事件处理

    在播放过程中,某些行为的完成或者变化,如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
    

    4.1 消息上报初始化

    在IJKFFMoviePlayerController的初始化方法中:

    - (id)initWithContentURLString:(NSString *)aUrlString
                       withOptions:(IJKFFOptions *)options
    {
        ......  
        // init player
        _mediaPlayer = ijkmp_ios_create(media_player_msg_loop);   
        ......          
    }
    

    可以看到在创建播放器时,media_player_msg_loop函数地址作为参数传入了ijkmp_ios_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。

    至此已经完成了播放消息发送的准备工作。

    4.2 消息上报处理

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

    消息上报代码调用流程图.

    我们以prepare完成事件为例,看看代码中事件上报的具体流程。

    ffplay.c中上报PREPARED完成时调用:

    ffp_notify_msg1(ffp, FFP_MSG_PREPARED);

    ffp_notify_msg1方法实现如下:

    inline static void ffp_notify_msg1(FFPlayer *ffp, int what) {
        msg_queue_put_simple3(&ffp->msg_queue, what, 0, 0);
    }

    msg_queue_put_simple3中将事件及其参数封装成了AVMessge对象,

    inline static void msg_queue_put_simple3(MessageQueue *q, int what, int arg1, int arg2)
    {
        AVMessage msg;
        msg_init_msg(&msg);
        msg.what = what;
        msg.arg1 = arg1;
        msg.arg2 = arg2;
        msg_queue_put(q, &msg);
    }
    

    继续跟踪代码,可以发现最后在

    inline static int msg_queue_put_private(MessageQueue *q, AVMessage *msg)

    方法中,消息对象被放在了消息队列里。但是哪里读取的队列里的消息呢?在4.1节中,我们有提到在创建播放器时,会传入media_player_msg_loop函数地址,最后作为一个单独的线程运行,现在来看一下media_player_msg_loop方法的实现:

    int media_player_msg_loop(void* arg)
    {
        @autoreleasepool {
            IjkMediaPlayer *mp = (IjkMediaPlayer*)arg;
            __weak IJKFFMoviePlayerController *ffpController = ffplayerRetain(ijkmp_set_weak_thiz(mp, NULL));
            while (ffpController) {
                @autoreleasepool {
                    IJKFFMoviePlayerMessage *msg = [ffpController obtainMessage];
                    if (!msg)
                        break;
    
                    int retval = ijkmp_get_msg(mp, &msg->_msg, 1);
                    if (retval < 0)
                        break;
    
                    // block-get should never return 0
                    assert(retval > 0);
                    [ffpController performSelectorOnMainThread:@selector(postEvent:) withObject:msg waitUntilDone:NO];
                }
            }
    
            // retained in prepare_async, before SDL_CreateThreadEx
            ijkmp_dec_ref_p(&mp);
            return 0;
        }
    }
    

    由此可以看出,最后是在该方法中读取消息,并采用notification通知到APP上层。

    五、结束语

    本文只是粗略的分析了ijkplayer的关键代码部分,平台相关的解码、渲染以及用户事务处理部分,都没有具体分析到,大家可以参考代码自行分析

    相关推荐

    为何越来越多的编程语言使用JSON(为什么编程)

    JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

    何时在数据库中使用 JSON(数据库用json格式存储)

    在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

    MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

    前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

    JSON对象花样进阶(json格式对象)

    一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

    深入理解 JSON 和 Form-data(json和formdata提交区别)

    在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

    JSON 语法(json 语法 priority)

    JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

    JSON语法详解(json的语法规则)

    JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

    MySQL JSON数据类型操作(mysql的json)

    概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

    JSON的数据模式(json数据格式示例)

    像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

    前端学习——JSON格式详解(后端json格式)

    JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

    什么是 JSON:详解 JSON 及其优势(什么叫json)

    现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

    PostgreSQL JSON 类型:处理结构化数据

    PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

    JavaScript:JSON、三种包装类(javascript 包)

    JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

    Python数据分析 只要1分钟 教你玩转JSON 全程干货

    Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

    比较一下JSON与XML两种数据格式?(json和xml哪个好)

    JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

    取消回复欢迎 发表评论:

    请填写验证码