零基础读懂视频播放器控制原理——ffplay播放器源代码分析

版权声明:本文由张坤原创文章,转载请注明出处: 
文章原文连接:https://www.qcloud.com/community/article/535574001486630869架构

来源:腾云阁 https://www.qcloud.com/communityide

 

视频播放器原理其实大抵相同,都是对音视频帧序列的控制。只是一些播放器在音视频同步上可能作了更为复杂的帧预测技术,来保证音频和视频有更好的同步性。函数

ffplay是FFMpeg自带的播放器,使用了 ffmpeg解码库和用于视频渲染显示的sdl 库,也是业界播放器最初参考的设计标准。本文对ffplay源码进行分析,试图用更基础而系统的方法,来尝试解开播放器的音视频同步,以及播放/暂停、快进/后退的控制原理。编码

因为FFMpeg自己的跨平台特性,相比在移动端看音视频代码,在PC端利用VS查看和调试代码,分析播放器原理,要高效迅速不少。spa

因为FFMpeg官方提供的ffmplay在console中进行使用不够直观,本文直接分析CSDN上将ffplay移植到VC的代码(ffplay for MFC)进行分析。.net

文章目录:
1、初探mp4文件
2、以最简单播放器开始:FFmpeg解码 + SDL显示
3、先抛五个问题
4、ffplay代码整体结构
5、视频播放器的操做控制
5.1 ffplay所定义的关键结构体VideoState
5.2 补充基础知识——PTS和DTS
5.2 如何控制音视频同步
5.4 如何控制视频的播放和暂停?
5.5 逐帧播放是如何作的?
5.6 快进和后退
6、 此次分析ffplay代码的检讨总结线程

1、初探mp4文件

为了让你们对视频文件有一个初步认识,首先来看对一个MP4文件的简单分析,如图1。设计


图1 对MP4文件解参3d

从图一咱们知道,每一个视频文件都会有特定的封装格式、比特率、时长等信息。视频解复用以后,就划分为video_stream和audio_stream,分别对应视频流和音频流。指针

解复用以后的音视频有本身独立的参数,视频参数包括编码方式、采样率、画面大小等,音频参数包括采样率、编码方式和声道数等。

对解复用以后的音频和视频Packet进行解码以后,就变成原始的音频(PWM)和视频(YUV/RGB)数据,才能够在进行显示和播放。

其实这已经差很少涉及到了,视频解码播放的大部分流程,整个视频播放的流程如图2所示。


图2 视频播放流程(图摘自http://blog.csdn.net/leixiaohua1020/article/details/50534150)

2、以最简单播放器开始:FFmpeg解码 + SDL显示

为将问题简单化,先不考虑播放音频,只播放视频,代码流程图如图3所示:


图3 播放器流程图(图源见水印)

流程图说明以下:

1.FFmpeg初始化的代码比较固定,主要目的就是为了设置AVFormatContext实例中相关成员变量的值,调用av_register_all、avformat_open_input av_find_stream_info和avcodec_find_decoder等函数。

如图4所示,初始化以后的AVFormatContext实例里面具体的值,调用av_find_stream_info就是找到文件中的音视频流数据,对其中的streams(包含音频、视频流)变量进行初始化。


图4 AVFormatContext初始化实例

2.av_read_frame不断读取stream中的下一帧,对其进行解复用获得视频的AVPacket,随后调用avcodec_decode_video2是视频帧AVPacket进行解码,获得图像帧AVFrame。

3.获得AVFrame以后,接下来就是放到SDL中进行渲染显示了,也很简单,流程见下面代码注释:

SDL_Overlay *bmp;
//将解析获得的AVFrame的数据拷贝到SDL_Overlay实例当中
SDL_LockYUVOverlay(bmp);
bmp->pixels[0]=pFrameYUV->data[0];
bmp->pixels[2]=pFrameYUV->data[1];
bmp->pixels[1]=pFrameYUV->data[2];    
bmp->pitches[0]=pFrameYUV->linesize[0];
bmp->pitches[2]=pFrameYUV->linesize[1];  
bmp->pitches[1]=pFrameYUV->linesize[2];

SDL_UnlockYUVOverlay(bmp);
//设置SDL_Rect,由于涉及到起始点和显示大小,用rect进行表示。
SDL_Rect rect;
rect.x = 0;   
rect.y = 0;   
rect.w = pCodecCtx->width; 
rect.h = pCodecCtx->height;   
//将SDL_Overlay数据显示到SDL_Surface当中。
SDL_DisplayYUVOverlay(bmp, &rect);
//延时40ms,留足ffmpeg取到下一帧并解码该帧的时间,随后继续读取下一帧
SDL_Delay(40);

由上面的原理可知,从帧流中获取到AVPacket,而且解码获得AVFrame,渲染到SDL窗口中。


图5 视频播放状态图

对视频播放的流程总结一下就是:读取下一帧——>解码——>播放——>不断往复,状态图如图5所示。

3、先抛五个问题

本文仍是以问题抛问题的思路,以逐步对每一个问题进行原理性分析,加深对音视频解码和播放的认识。如下这些问题也是每个播放器所须要面对的基础问题和原理:

1.咱们在观看电影时发现,电影能够更换不一样字幕,甚至不一样音频,好比中英文字幕和配音,最后在同一个画面中进行显示,视频关于画面、字幕和声音是如何组合的? 
其实每个视频文件,读取出来以后发现,都会被区分不一样的流。为了让你们有更具体的理解,以FFMpeg中的代码为例,AVMediaType定义了具体的流类型:

enum AVMediaType {

    AVMEDIA_TYPE_VIDEO,  //视频流

    AVMEDIA_TYPE_AUDIO, //音频流

    AVMEDIA_TYPE_SUBTITLE, //字幕流

};

利用av_read_frame读取出音视频帧以后,随后就利用avcodec_decode_video2对视频捷星解码,或者调用avcodec_decode_audio4对音频进行解码,获得能够供渲染和显示的音视频原始数据。

图像和字幕都将会以Surface或者texture的形式,就像Android中的SurfaceFlinger,将画面不一样模块的显示进行组合,生成一幅新的图像,显示在视频画面中。

2.既然视频有帧率的概念,音频有采样率的概念,是否直接利用帧率就能够控制音视频的同步了呢? 
每个视频帧和音频帧在时域上都对应于一个时间点,按道理来讲只要控制每个音视频帧的播放时间,就能够实现同步。

但实际上,对每一帧显示的时间上的精确控制是很难的,更况且音频和视频的解码所需时间不一样,极容易引发音视频在时间上的不一样步。

因此,播放器具体是如何作音视频同步的呢?

3.视频的音频流、视频流和字幕流,他们在时间上是连续的仍是离散的?不一样流的帧数相同吗? 
因为计算机只能数字模拟离散的世界,因此在时间上确定是离散的。那既然是离散的,他们的帧数是否相同呢?

视频能够理解为诸多音频帧、视频帧和字幕帧在时间上的序列,他们在时间上的时长,跟视频总时长是相同的,可是因为每一个帧解码时间不一样,必然会致使他们在每帧的时间间隔不相同。

音频原始数据自己就是采样数据,因此是有固定时钟周期。可是视频假如想跟音频进行同步的话,可能会出现跳帧的状况,每一个视频帧播放时间差,都会起伏不定,不是恒定周期。

因此结论是,三者在视频总时长上播放的帧数确定是不同的。

4.视频播放就是一系列的连续帧不停渲染。对视频的控制操做包括:暂停和播放、快进和后退。那有没有想过,每次快进/后退的幅度,以时间为量度好,仍是以每次跳跃的帧数,就是每次快进是前进多长时间,仍是前进多少帧。 时间 VS 帧数? 
由上面问题分析,咱们知道,视频是以音频流、视频流和字幕流进行分流的,假如以帧数为基础,因为不一样流的帧数量不必定相同,以帧数为单位,很容易致使三个流播放的不一致。

所以以时间为量度,相对更好,直接搜寻mp4文件流,当前播放时间的前进或后退时长的seek时间点,随后从新对文件流进行分流解析,就能够达到快进和后退以后的音视频同步效果。

咱们能够看到绝大部分播放器,快进/倒退都是以时长为步进的,咱们能够看看ffplay是怎么样的,以及是如何实现的。

5.上一节中,实现的简单播放器,解码和播放都是在同一个线程中,解码速度直接影响播放速度,从而将直接形成播放不流畅的问题。那如何在解码可能出现速度不均匀的状况下,进行流畅的视频播放呢?

很容易想到,引入缓冲队列,将视频图像渲染显示和视频解码做为两个线程,视频解码线程往队列中写数据,视频渲染线程从队列中读取数据进行显示,这样就能够保证视频是能够流程播放的。

所以须要采用音频帧、视频帧和字幕帧的三个缓冲队列,那如何保证音视频播放的同步呢?

PTS是视频帧或者音频帧的显示时间戳,到底是如何利用起来的,从而控制视频帧、音频帧以及字幕帧的显示时刻呢?

那咱们就能够探寻ffplay,到底是如何去作缓冲队列控制的。

全部以上五个问题,咱们都将在对ffplay源代码的探寻中,逐步找到更具体的解答。

4、ffplay代码整体结构


图6 ffplay代码整体流程

网上有人作了ffplay的整体流程图,如图6。有了这幅图,代码看起来,就会轻松了不少。流程中具体包含的细节以下:

1.启动定时器Timer,计时器40ms刷新一次,利用SDL事件机制,触发从图像帧队列中读取数据,进行渲染显示;

2.stream_componet_open函数中,av_read_frame()读取到AVPacket,随后放入到音频、视频或字幕Packet队列中;

3.video_thread,从视频packet队列中获取AVPacket并进行解码,获得AVFrame图像帧,放到VideoPicture队列中。

4..audio_thread线程,同video_thread,对音频Packet进行解码;

5.subtitle_thread线程,同video_thread,对字幕Packet进行解码。

5、视频播放器的操做控制

视频播放器的操做包括播放/暂停、快进/倒退、逐帧播放等,这些操做的实现原理是什么呢,下面对其从代码层面逐个进行分析。

5.1 ffplay所定义的关键结构体VideoState

与FFmpeg解码相似,定义了一个AVFormatContext结构体,用于存储文件名、音视频流、解码器等字段,供全局进行访问。

ffplay也定义了一个结构体VideoState,经过对VideoState的分析,就能够大致知道播放器基本实现原理。

typedef struct VideoState {
       // Demux解复用线程,读视频文件stream线程,获得AVPacket,并对packet入栈
       SDL_Thread *read_tid;  
       //视频解码线程,读取AVPacket,decode 爬出能够成AVFrame并入队
       SDL_Thread *video_tid;
       //视频播放刷新线程,定时播放下一帧
       SDL_Thread *refresh_tid;
       int paused;  //控制视频暂停或播放标志位
       int seek_req;  //进度控制标志
       int seek_flags;

       AVStream *audio_st;   //音频流
       PacketQueue audioq;  //音频packet队列
       double audio_current_pts;  //当前音频帧显示时间

       AVStream *subtitle_st; //字幕流
       PacketQueue subtitleq;//字幕packet队列 

       AVStream *video_st; //视频流

       PacketQueue videoq;//视频packet队列
       double video_current_pts; ///当前视频帧pts
       double video_current_pts_drift;  

       VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];  //解码后的图像帧队列
}

从VideoState结构体中能够看出:

1.解复用、视频解码和视频刷新播放,分属三个线程中,并行控制;

2.音频流、视频流、字幕流,都有本身的缓冲队列,供不一样线程读写,而且有本身的当前帧的PTS;

3.解码后的图像帧单独放在pictq队列当中,SDL利用其进行显示。

其中PTS是什么呢,这在音视频中是一个很重要的概念,直接决定视频帧或音频帧的显示时间,下面具体介绍一下。

5.2 补充基础知识——PTS和DTS


图7 音视频解码分析

图7为输出的音频帧和视频帧序列,每一帧都有PTS和DTS标签,这两个标签到底是什么意思呢?
DTS(Decode Time Stamp)和PTS(Presentation Time Stamp)都是时间戳,前者是解码时间,后者是显示时间,都是为视频帧、音频帧打上的时间标签,以更有效地支持上层应用的同步机制。

也就是说,视频帧或者音频在解码时,会记录其解码时间,视频帧的播放时间依赖于PTS。

对于声音来讲 ,这两个时间标签是相同的;但对于某些视频编码格式,因为采用了双向预测技术,DTS会设置必定的超时或延时,保证音视频的同步,会形成DTS和PTS的不一致。

5.3 如何控制音视频同步

咱们已经知道,视频帧的播放时间其实依赖pts字段的,音频和视频都有本身单独的pts。但pts到底是如何生成的呢,假如音视频不一样步时,pts是否须要动态调整,以保证音视频的同步?

下面先来分析,如何控制视频帧的显示时间的:

static void video_refresh(void *opaque){ 

  //根据索引获取当前须要显示的VideoPicture
  VideoPicture *vp = &is->pictq[is->pictq_rindex];

  if (is->paused)
      goto display; //只有在paused的状况下,才播放图像

  // 将当前帧的pts减去上一帧的pts,获得中间时间差

  last_duration = vp->pts - is->frame_last_pts;

  //检查差值是否在合理范围内,由于两个连续帧pts的时间差,不该该太大或过小

  if (last_duration > 0 && last_duration < 10.0) {
    /* if duration of the last frame was sane, update last_duration in video state */
    is->frame_last_duration = last_duration;
  }

  //既然要音视频同步,确定要以视频或音频为参考标准,而后控制延时来保证音视频的同步,
  //这个函数就作这个事情了,下面会有分析,具体是如何作到的。
  delay = compute_target_delay(is->frame_last_duration, is);

  //获取当前时间
  time= av_gettime()/1000000.0;

   //假如当前时间小于frame_timer + delay,也就是这帧改显示的时间超前,还没到,就直接返回
  if (time < is->frame_timer + delay) 
      return;

  //根据音频时钟,只要须要延时,即delay大于0,就须要更新累加到frame_timer当中。
  if (delay > 0)
       /更新frame_timer,frame_time是delay的累加值
       is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay));

  SDL_LockMutex(is->pictq_mutex);

  //更新is当中当前帧的pts,好比video_current_pts、video_current_pos 等变量
  update_video_pts(is, vp->pts, vp->pos);

  SDL_UnlockMutex(is->pictq_mutex);

display:
  /* display picture */
  if (!display_disable)
    video_display(is);
}

函数compute_target_delay根据音频的时钟信号,从新计算了延时,从而达到了根据音频来调整视频的显示时间,从而实现音视频同步的效果。

static double compute_target_delay(double delay, VideoState *is)
{
    double sync_threshold, diff;
   //由于音频是采样数据,有固定的采用周期而且依赖于主系统时钟,要调整音频的延时播放较难控制。因此实际场合中视频同步音频相比音频同步视频实现起来更容易。
   if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) ||
     is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) {

       //获取当前视频帧播放的时间,与系统主时钟时间相减获得差值
       diff = get_video_clock(is) - get_master_clock(is);
       sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay);

      //假如当前帧的播放时间,也就是pts,滞后于主时钟
      if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
         if (diff <= -sync_threshold)
             delay = 0;
      //假如当前帧的播放时间,也就是pts,超前于主时钟,那就须要加大延时
      else if (diff >= sync_threshold)
        delay = 2 * delay;
      }

   }
   return delay;
}


图8 音视频帧显示序列

因此这里的流程就很简单了,图8简单画了一个音视频帧序列,想表达的意思是,音频帧数量和视频帧数量不必定对等,另外每一个音频帧的显示时间在时间上几乎对等,每一个视频帧的显示时间,会根据具体状况有延时显示,这个延时就是有上面的compute_target_delay函数计算出来的。

计算延迟后,更新pts的代码以下:

static void update_video_pts(VideoState *is, double pts, int64_t pos) {

    double time = av_gettime() / 1000000.0;
    /* update current video pts */
    is->video_current_pts = pts;
    is->video_current_pts_drift = is->video_current_pts - time;
    is->video_current_pos = pos;
    is->frame_last_pts = pts;
}

整个流程能够归纳为:
显示第一帧视频图像;
根据音频信号,计算出第二帧的delay时间,更新该帧的pts。
当pts到达后,显示第二帧视频图像。
重复以上步骤,到最后一帧
也许在这里仍然会让人很困惑,为何单单根据主时钟,就能够播放下一帧所须要的延时呢? 
其实视频是具有必定长度的播放流,具体能够分为音频流、视频流和字幕流,三者同时在一块儿播放造成了视频,固然他们总的播放时间是跟视频文件的播放时长是同样的。

因为音频流自己是pwm采样数据,以固定的频率播放,这个频率是跟主时钟相同或是它的分频,从时间的角度来看,每一个音频帧是天然均匀流逝。

因此音频的话,直接按照主时钟或其分频走就能够了。

视频,要根据本身的显示时间即pts,跟主时钟当前的时间进行对比,肯定是超前仍是滞后于系统时钟,从而肯定延时,随后进行准确的播放,这样就能够保证音视频的同步了。

那接下来,还有一个问题,计算出延时以后,难道须要sleep一下作延迟显示吗? 
其实并非如此,上面分析咱们知道delay会更新到当前须要更新视频帧的pts (video_current_pts),对当前AVFrame进行显示前,先检测其pts时间,假如还没到,就不进行显示了,直接return。直到下一次刷新,从新进行检测(ffplay采用的40ms定时刷新)。

代码以下,未到更新后的pts时间( is->frame_timer + dela),直接return:

if (av_gettime()/1000000.0 < is->frame_timer + delay)  
    return;

那接下来就是分析如何播放视频帧,就很简单了,只是这里多加了一个字幕流的处理:

static void video_image_display(VideoState *is)
{
    VideoPicture *vp;
   SubPicture *sp;
   AVPicture pict;
   SDL_Rect rect;
   int i;
   vp = &is->pictq[is->pictq_rindex];
   if (vp->bmp) {
       //字幕处理
       if (is->subtitle_st) {}                  
   }

   //计算图像的显示区域
   calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp);

   //显示图像
   SDL_DisplayYUVOverlay(vp->bmp, &rect);

   //将pic队列的指针向前移动一个位置
   pictq_next_picture(is);

}

VIDEO_PICTURE_QUEUE_SIZE 只设置为4,很快就会用完了。数据满了如何从新更新呢? 
一旦检测到超出队列大小限制,就处于等待状态,直到pictq被取出消费,从而避免开启播放器,就把整个文件所有解码完,这样会代码会很吃内存。

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts1, int64_t pos){

/* keep the last already displayed picture in the queue */
while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE - 2 &&
      !is->videoq.abort_request) {

    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
   }
   SDL_UnlockMutex(is->pictq_mutex);
}

5.4 如何控制视频的播放和暂停?

static void stream_toggle_pause(VideoState *is)
{

    if (is->paused) {
       //因为frame_timer记下来视频从开始播放到当前帧播放的时间,因此暂停后,必需要将暂停的时间( is->video_current_pts_drift - is->video_current_pts)一块儿累加起来,并加上drift时间。

     is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts;

     if (is->read_pause_return != AVERROR(ENOSYS)) {
     //并更新video_current_pts
        is->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0;

       }
    //drift其实就是当前帧的pts和当前时间的时间差
    is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0;
    }

    //paused取反,paused标志位也会控制到图像帧的展现,按一次空格键实现暂停,再按一次就实现播放了。
    is->paused = !is->paused;
}

特别说明:paused标志位控制着视频是否播放,当须要继续播放的时候,必定要从新更新当前所须要播放帧的pts时间,由于这里面要加上已经暂停的时间。

5.5 逐帧播放是如何作的?

在视频解码线程中,不断经过stream_toggle_paused,控制对视频的暂停和显示,从而实现逐帧播放:

static void step_to_next_frame(VideoState *is)
{
   //逐帧播放时,必定要先继续播放,而后再设置step变量,控制逐帧播放
   if (is->paused)
      stream_toggle_pause(is);//会不断将paused进行取反
   is->step = 1;
}

其原理就是不断的播放,而后暂停,从而实现逐帧播放:

static int video_thread(void *arg)
{
  if (is->step)
    stream_toggle_pause(is);
      ……………………
  if (is->paused)
    goto display;//显示视频
  }
}

5.6 快进和后退

关于快进/后退,首先抛出两个问题:

1. 快进以时间为维度仍是以帧数为维度来对播放进度进行控制呢? 
2.一旦进度发生了变化,那么当前帧,以及AVFrame队列是否须要清零,整个对stream的流是否须要从新来进行控制呢? 
ffplay中采用以时间为维度的控制方法。对于快进和后退的控制,都是经过设置VideoState的seek_req、seek_pos等变量进行控制

do_seek:
//其实是计算is->audio_current_pts_drift + av_gettime() / 1000000.0,肯定当前须要播放帧的时间值
pos = get_master_clock(cur_stream);
pos += incr; //incr为每次快进的步进值,相加便可获得快进后的时间点
stream_seek(cur_stream, (int64_t)(pos AV_TIME_BASE), (int64_t)(incr AV_TIME_BASE), 0);
关于stream_seek的代码以下,其实就是设置VideoState的相关变量,以控制read_tread中的快进或后退的流程:

/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{

  if (!is->seek_req) {
  is->seek_pos = pos;
  is->seek_rel = rel;
  is->seek_flags &= ~AVSEEK_FLAG_BYTE;
  if (seek_by_bytes)
    is->seek_flags |= AVSEEK_FLAG_BYTE;
  is->seek_req = 1;
}
}

stream_seek中设置了seek_req标志,就直接进入前进/后退控制流程了,其原理是调用avformat_seek_file函数,根据时间戳控制索引点,从而控制须要显示的下一帧:

static int read_thread(void *arg){
//当调整播放进度之后
if (is->seek_req) {
   int64_t seek_target = is->seek_pos;
   int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
   int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
  //根据时间抽查找索引点位置,定位到索引点以后,下一帧的读取直接从这里开始,就实现了快进/后退操做
  ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
  if (ret < 0) {
     fprintf(stderr, "s: error while seeking\n", is->ic->filename);
  } else {
  //查找成功以后,就须要清空当前的PAcket队列,包括音频、视频和字幕
     if (is->audio_stream >= 0) {
        packet_queue_flush(&is->audioq);
        packet_queue_put(&is->audioq, &flush_pkt);
     }
     if (is->subtitle_stream >= 0) {//处理字幕stream
        packet_queue_flush(&is->subtitleq);
        packet_queue_put(&is->subtitleq, &flush_pkt);
    }
    if (is->video_stream >= 0) {
       packet_queue_flush(&is->videoq);
       packet_queue_put(&is->videoq, &flush_pkt);
    }
  }
  is->seek_req = 0;
  eof = 0;
  }
}

另外从上面代码中发现,每次快进后退以后都会对audioq、videoq和subtitleq进行flush清零,也是至关于从新开始,保证缓冲队列中的数据的正确性。

对于音频,开始仍然有些困惑,由于在暂停的时候,没有看到对音频的控制,是如何控制的呢?

后来发现,其实暂停的时候设置了is->paused变量,解复用和音频解码和播放都依赖于is->paused变量,因此音频和视频播放都随之中止了。

6、 此次分析ffplay代码的检讨总结:

1.基础概念和原理积累,最开始接触FFmpeg,由于其涉及的概念不少,看起来有种无从下手的感受。这时候必须从基本模块入手,逐步理解更多,必定的量积累,就会产生一些质变,更好的理解视频编解码机制;

2.必定要首先看懂代码整体架构和流程,随后针对每一个细节点进行深刻分析,会极大提升看代码效率。会画一些框图是很是重要的,好比下面这张,因此简要的流程图要比注重细节的uml图要方便得多;


3.看FFmpeg代码,在PC端上调试,会快捷不少。假如要在Android上,调用jni来看代码,效率就会很低。

参考文章:

基于ffmpeg的跨平台播放器实现

https://www.qcloud.com/community/article/309889001486708756

雷神的文章(多媒体入门开发必看):

http://blog.csdn.net/leixiaohua1020/article/details/15811977

相关文章
相关标签/搜索