基于ffmpeg和SDL的音视频播放器

基于ffmpeg和SDL的音视频播放器

花了挺长时间给代码加了超级详细的注释
实现的功能:音视频同步播放

一.播放器的多线程模型

本文从播放器的多线程的模型开始分析播放器的源码

1.主线程

我们的设计思路是主线程不添加过多的业务逻辑,专注于SDL事件的响应,提高用户的响应速度

​ 首先要介绍一个超级重要的结构体VidoeState,这里面存放了视频文件的信息(上下文、解码器、各种参数等)和写代码过程中要使用的数据结构(队列、index、锁等),这个结构体要作为函数参数把各个线程串起来。

主线程首先做了一些初始化,然后创建了一个定时器,每隔40ms触发一次FF_REFRESH_EVENT事件,开启解复用线程,然后开始做SDL循环等待 (SDL_WaitEvent),FF_REFRESH_EVENT事件触发video_refresh_timer函数刷新视频帧,这个函数涉及到视频的同步很重要后面再说。

 int main(int argc, char *argv[]) 
    {
        SDL_Event event;
//创建全局状态对象
        VideoState *is;//important struct

        is = av_mallocz(sizeof(VideoState));//将分配的内存块所有字节置0  给VideoState结构体分配空间


        if (argc < 2) {
            fprintf(stderr, "Usage: test \n");
            exit(1);
        }
        //读写打开或建立一个二进制文件,允许读和写
        yuvfd = fopen("testout.yuv", "wb+");
        audiofd = fopen("testout.pcm", "wb+");
        // Register all formats and codecs
        av_register_all();

        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
            fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
            exit(1);
        }

//init mutex lock
        text_mutex = SDL_CreateMutex();//sdl lock?

        //Copy the string src to dst, but no more than size

        av_strlcpy(is->filename, argv[1], sizeof(is->filename));//复制视频文件路径名


        is->pictq_mutex = SDL_CreateMutex();//解码后图像帧队列
        is->pictq_cond = SDL_CreateCond();

        //start a timer
        //40ms push a refresh event
        schedule_refresh(is, 40);//40ms回调一次 触发FF_REFRESH_EVENT 渲染视频帧

        is->av_sync_type = DEFAULT_AV_SYNC_TYPE;//0  AV_SYNC_AUDIO_MASTER

        //creat demux thread 解复用线程
        is->parse_tid = SDL_CreateThread(demux_thread, "demux_thread", is);
        if (!is->parse_tid) {
            av_free(is);
            return -1;
        }

        //sdl event loop wait
        for (;;) {

            SDL_WaitEvent(&event);
            switch (event.type) {
                case FF_QUIT_EVENT:
                case SDL_QUIT://退出进程事件
                    is->quit = 1;//把quit标志置为1 线程退出机制
                    SDL_Quit();
                    return 0;
                    break;
                case FF_REFRESH_EVENT://视频显示刷新事件
                    //视频刷新定时器
                    video_refresh_timer(event.user.data1);//event.user.data1  is
                    break;
                default:
                    break;
            }
        }

        fclose(yuvfd);
        fclose(audiofd);
        return 0;

    }

2.解复用线程

解复用线程的目的就是把视频文件拆成音频和视频两路流 ,并打开解码器。然后创建了视频解码线程

具体步骤就是首先打开多媒体问价文件,然后把文件容器封装信息及码流参数传递给我们定义的VideoState结构体,然后遍历视频文件中的所有流,找到视频流和音频流的index,然后用到一个很重要的函数stream_component_open,这个函数主要功能是根据指定类型打开流,找到对应的解码器、创建对应的音频配置、保存关键信息到 VideoState、启动音频和视频解码线程

然后创建SDL窗口,创建渲染器,创建纹理,然后开始循环av_read_frame从多媒体文件中读取包,将视频包和音频包存放到不同的队列中,这里涉及到几个队列操作的函数,然后每次循环判断quit标志有没有被置为1,为1就延迟100ms后退出。

int demux_thread(void *arg) {

        int err_code;
        char errors[1024] = {0,};

        VideoState *is = (VideoState *) arg;//传递用户参数
        AVFormatContext *pFormatCtx;//保存文件容器封装信息及码流参数的结构体
        AVPacket pkt1, *packet = &pkt1;//在栈上创建临时数据包对象并关联指针

        int video_index = -1;//视频流类型标号初始化为-1
        int audio_index = -1;//音频流类型标号初始化为-1
        int i;//循环变量
        is->videoStream = -1;//视频流类型标号初始化为-1
        is->audioStream = -1;//音频流类型标号初始化为-1

        global_video_state = is;//传递全局状态参量结构体

        /* open input file, and allocate format context *///打开多媒体文件
        if ((err_code = avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)) < 0) {
            av_strerror(err_code, errors, 1024);
            fprintf(stderr, "Could not open source file %s, %d(%s)\n", is->filename, err_code, errors);
            return -1;
        }

        is->pFormatCtx = pFormatCtx;//传递文件容器封装信息及码流参数

        // Retrieve stream information  查找流相关的信息
        if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
            return -1; // Couldn't find stream information

// Dump information about file onto standard error,打印pFormatCtx中的码流信息
        av_dump_format(pFormatCtx, 0, is->filename, 0);

        // Find the first video stream and audio stream
        for (i = 0; i < pFormatCtx->nb_streams; i++) {//遍历文件中包含的所有流媒体类型(视频流、音频流、字幕流等)
            if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO &&
                video_index < 0)//若文件中包含有视频流
            {
                video_index = i;//用视频流类型的标号修改标识,使之不为-1
            }
            if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO &&
                audio_index < 0)//用视频流类型的标号修改标识,使之不为-1
            {
                audio_index = i;//用音频流类型的标号修改标识,使之不为-1
            }
        }
        if (audio_index >= 0) {//检查文件中是否存在音频流
            stream_component_open(is, audio_index);//根据指定类型打开音频流  设置音频相关参数
            /*
             * set code paramter  、init and open  decoder 、sws init and set opts、packet_queue_init、creat decoder thread
             */
        }
        if (video_index >= 0) {
            stream_component_open(is, video_index);//根据指定类型打开视频流  设置音频相关参数
        }

        if (is->videoStream < 0 || is->audioStream < 0) {//检查文件中是否存在音视频流
            fprintf(stderr, "%s: could not open codecs\n", is->filename);
            goto fail;//跳转至异常处理
        }

        //creat window from SDL  创建窗口
        win = SDL_CreateWindow("Media Player",
                               SDL_WINDOWPOS_UNDEFINED,
                               SDL_WINDOWPOS_UNDEFINED,
                               is->video_ctx->width, is->video_ctx->height,
                               SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
        if (!win) {
            fprintf(stderr, "SDL: could not set video mode - exiting\n");
            exit(1);
        }

        renderer = SDL_CreateRenderer(win, -1, 0);//创建渲染器

        //IYUV: Y + U + V  (3 planes)
        //YV12: Y + V + U  (3 planes)
        Uint32 pixformat = SDL_PIXELFORMAT_IYUV;

        //create texture for render
        texture = SDL_CreateTexture(renderer,//创建纹理
                                    pixformat,
                                    SDL_TEXTUREACCESS_STREAMING,
                                    is->video_ctx->width,
                                    is->video_ctx->height);


        // main decode loop

        for (;;) {
            if (is->quit)//检查退出进程标识
            {
                break;
            }
            // Seek stuff goes here,检查音视频编码数据包(未解码)队列长度是否溢出
            if (is->audioq.size > MAX_AUDIOQ_SIZE ||
                is->videoq.size > MAX_VIDEOQ_SIZE) {
                SDL_Delay(10);
                continue;
            }
/*-----------------------
		 * read in a packet and store it in the AVPacket struct
	 	 * ffmpeg allocates the internal data for us,which is pointed to by packet.data
	     * this is freed by the av_free_packet()
	 	 -----------------------*/

            if (av_read_frame(is->pFormatCtx, packet) < 0) {//从多媒体文件中读取一个包
                if (is->pFormatCtx->pb->error == 0) {
                    SDL_Delay(100); /* no error; wait for user input */
                    continue;
                } else {
                    break;
                }
            }
            //将视频包和音频包存放到不同的队列中
            // Is this a packet from the video stream?
            if (packet->stream_index == is->videoStream)//检查数据包是否为视频类型
            {
                packet_queue_put(&is->videoq, packet);//向队列中插入数据包
            } else if (packet->stream_index == is->audioStream)//检查数据包是否为音频类型
            {
                packet_queue_put(&is->audioq, packet);//向队列中插入数据包
            } else {//检查数据包是否为字幕类型
                av_free_packet(packet);//释放packet中保存的(字幕)编码数据
            }
        }
        /* all done - wait for it */
        while (!is->quit) {
            SDL_Delay(100);
        }

        fail:
        if (1) {
            SDL_Event event;//SDL事件对象
            event.type = FF_QUIT_EVENT;//指定退出事件类型
            event.user.data1 = is;//传递用户数据

            SDL_PushEvent(&event);//将该事件对象压入SDL后台事件队列
        }
        return 0;
    }

3.视频解码线程

视频解码线程的目的就是从视频流队列中提取数据包,然后解码后放入VideoPicture类型的数组缓冲区去

首先从队列中提取数据包到packet,并将提取的数据包出队列,然后调用avcodec_decode_video2解码packet得到一帧数据,然后av_frame_get_best_effort_timestamp得到当前帧的pts,调用synchronize_video更新视频帧的 PTS,然后调用queue_picture将解码成功的frame放进数据缓冲区。

//视频解码线程函数
    int decode_video_thread(void *arg)
    {
        VideoState *is = (VideoState *) arg;//传递用户数据
        AVPacket pkt1, *packet = &pkt1;//在栈上创建临时数据包对象并关联指针
        int frameFinished;//解码操作是否成功标识
        AVFrame *pFrame;
        double pts;

// Allocate video frame,为解码后的视频信息结构体分配空间并完成初始化操作(结构体中的图像缓存按照下面两步手动安装)
        pFrame = av_frame_alloc();

        for (;;) {//从队列中提取数据包到packet,并将提取的数据包出队列
            if (packet_queue_get(&is->videoq, packet, 1) < 0) {
                // means we quit getting packets
                break;
            }
            pts = 0;
/*-----------------------
	 	* Decode video frame,解码完整的一帧数据,并将frameFinished设置为true
		* 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行
	 	* avcodec_decode_video2()会在解码到完整的一帧时设置frameFinished为真
		* Technically a packet can contain partial frames or other bits of data
		* ffmpeg's parser ensures that the packets we get contain either complete or multiple frames
		* convert the packet to a frame for us and set frameFinisned for us when we have the next frame
	 	-----------------------*/

            // Decode video frame
            avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);
            //从packet中解码出frame后,如何得到frame的PTS
            //if we can't get right pts ,we set pts to 0;
            if ((pts = av_frame_get_best_effort_timestamp(pFrame)) != AV_NOPTS_VALUE) //AV_NOPTS_VALUE is pts  获得pts
            {

            } else {
                pts = 0;
            }
//转换成当前帧的pts
            pts *= av_q2d(is->video_st->time_base);//该函数负责把AVRational结构转换成double,通过这个函数可以计算出某一帧在视频中的时间位置

            // Did we get a video frame?
            if (frameFinished) {
                pts = synchronize_video(is, pFrame, pts);
                if (queue_picture(is, pFrame, pts) < 0) {//解码成功 放进解码后的图片队列
                    break;
                }
            }
            av_free_packet(packet);
        }
        av_frame_free(&pFrame);
        return 0;
    }

重要的函数及结构体分析

PacketQueue

typedef struct PacketQueue
{
  AVPacketList *first_pkt, *last_pkt;队列首尾节点指针
  int nb_packets;//packet numbers
  int size;//队列字节总数
  SDL_mutex *mutex;///用于维持PacketQueue的多线程安全  队列互斥量
  SDL_cond *cond;用于读、写线程相互通知(SDL_cond可以按pthread_cond_t理解)
} PacketQueue;k

VideoPicture

typedef struct VideoPicture
{
  AVPicture *bmp;SDL画布overlay
  int width, height; /* source height & width */
  int allocated;//是否分配内存空间,视频帧转换为SDL overlay标识
  double pts;
} VideoPicture;

VideoState

typedef struct VideoState
{

  //multi-media file
  char            filename[1024];//输入文件完整路径名
  AVFormatContext *pFormatCtx;//保存文件容器封装信息及码流参数的结构体
  int             videoStream, audioStream;//音视频流id

  //sync
  int             av_sync_type;// set is AV_SYNC_AUDIO_MASTER
  double          external_clock; /* external clock base */
  int64_t         external_clock_time;

  double          audio_diff_cum; /* used for AV difference average computation */
  double          audio_diff_avg_coef;
  double          audio_diff_threshold;
  int             audio_diff_avg_count;

  double          audio_clock;//音频正在播放的时间

  double          frame_timer;//下次要回调的timer
  double          frame_last_pts;//上一次播放视频帧的pts
  double          frame_last_delay;//上一次播放视频帧增加的delay

  double          video_clock; ///

video_refresh_timer

图片显示刷新函数

这里首先等待图像帧数据缓冲区中有数据,然后检查该帧pts的有效性,通过get_audio_clock获取音频的时钟,然后获得视频时间与音频时间的差值,根据阈值判断相对于音频是快了还是慢了,视频慢了就将delay设为0 慢了就将dealy延长两倍,然后计算出actual_delay,通过定时器延时后video_display显示,然后更新pictq_rindex

//显示刷新函数(FF_REFRESH_EVENT响应函数)
void video_refresh_timer(void *userdata) //play 同步
{

  VideoState *is = (VideoState *)userdata;//传递用户数据
    // vp is used in later tutorials for synchronization.
    VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st)
  {
    if(is->pictq_size == 0)//检查解码后图像帧队列是否有待显示图像
    {
      schedule_refresh(is, 1);
      //fprintf(stderr, "no picture in the queue!!!\n");
    }
    else//刷新图像
    {
        /*-------------------------
			 * Now, normally here goes a ton of code about timing, etc.
			 * we're just going to guess at a delay for now.
			 * You can increase and decrease this value and hard code the timing
			 * but I don't suggest that ;) We'll learn how to do it for real later..
			 ------------------------*/

      //fprintf(stderr, "get picture from queue!!!\n");

        //根据索引获取当前需要显示的VideoPicture
      vp = &is->pictq[is->pictq_rindex];
      
      is->video_current_pts = vp->pts;
      is->video_current_pts_time = av_gettime();

      /*我们在这里做了很多检查:首先,我们保证现在的时间戳和上一个时间戳之间的delay是有效的。
       * 如果无效的话,我们只能用猜测方式用上次的延迟。接着,我们有一个同步阈值,因为在同步的时候事情并不总是那么完美的。
       * 在ffplay中使用0.01作为它的值。我们也保证阈值不会比时间戳之间的间隔短。最后,我们把最小的刷新值设置为10毫秒。*/

      //  使用要播放的当前帧的PTS和上一帧的PTS差来估计播放下一帧的延迟时间      并根据video的播放速度来调整这个延迟时间,以实现视音频的同步播放。
        // 将当前帧的pts减去上一帧的pts,得到中间时间差
      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
        //检查差值是否在合理范围内,因为两个连续帧pts的时间差,不应该太大或太小
      if(delay <= 0 || delay >= 1.0)
      {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;//上一次用过的delay
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

        // 根据Audio clock来判断Video播放的快慢
       // 根据音频的时钟信号,重新计算了延时,从而达到了根据音频来调整视频的显示时间,从而实现音视频同步的效果。
      /* update delay to sync to audio if not master source */
      if(is->av_sync_type != AV_SYNC_VIDEO_MASTER)  //av_sync_type  is   AV_SYNC_AUDIO_MASTER
      {
          //获取当前视频帧播放的时间,与系统主时钟时间相减得到差值
            ref_clock = get_master_clock(is);//get_audio_clock
            diff = vp->pts - ref_clock;//获得视频时间与音频时间的差值
	
            /* Skip or repeat the frame. Take delay into account
               FFPlay still doesn't "know if this is the best guess." */
            sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;// delay 0.01 least 帧最短刷新时间10ms


            if(fabs(diff) < AV_NOSYNC_THRESHOLD) //非同步阈值  10s
            {
                //假如当前帧的播放时间,也就是pts,滞后于主时钟
              if(diff <= -sync_threshold)//慢了,delay设为0
              {
                delay = 0;
              }
   //假如当前帧的播放时间,也就是pts,超前于主时钟,那就需要加大延时
              else if(diff >= sync_threshold)
              {
                delay = 2 * delay;// 快了,加倍delay
              }
            }
      }
      //frame_timer保存着视频播放的延迟时间总和,这个值和当前时间点的差值就是播放下一帧的真正的延迟时间
      is->frame_timer += delay;//frame_timer为解复用时的系统时间
      /* computer the real delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);//av_gettime() : Get the current time in microseconds.
                                                                    av_gettime() / 1000000.0 获取当前系统时间
        //同步阈值
      if(actual_delay < 0.010)//小于10ms 就10ms刷新一次
      {
	/* Really it should skip the picture instead */
	    actual_delay = 0.010;
      }
      //换算成毫秒 加上微差值0.5
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));//设置显示下一帧图像的刷新时间,通过定时器timer方式触发
      
      /* show the picture! */
      video_display(is);//图像帧渲染
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE)//更新并检查图像帧队列读位置索引
      {
	        is->pictq_rindex = 0;//重置读位置索引  如果读到最后就从开始再读  (并且前面有判断数据缓存区是否为空)
      }
      SDL_LockMutex(is->pictq_mutex);//锁定互斥量,保护画布的像素数据
      is->pictq_size--;//更新图像帧队列长度
      SDL_CondSignal(is->pictq_cond);//发送队列就绪信号
      SDL_UnlockMutex(is->pictq_mutex);//释放互斥量
    }
  }
  else
  {
    schedule_refresh(is, 100);
  }
}

stream_component_open

这个函数主要是做一些解码前的准备工作,然后打开解码器,并初始化(packet_queue_init)音频流队列和视频流队列(未解码前的)。音频部分还需要设置音频参数以及回调函数audio_callback,然后打开音响设备,这里还需要进行重采样设置,准备将音频本身的采样参数转换为我们希望的。视频部分也需要设置视频裁剪上下文,

设置图像转换像素格式为AV_PIX_FMT_YUV420P,然后创建视频解码线程

//根据指定类型打开流,找到对应的解码器、创建对应的音频配置、保存关键信息到 VideoState、启动音频和视频解码线程
    int stream_component_open(VideoState *is, int stream_index)
    {

        AVFormatContext *pFormatCtx = is->pFormatCtx;//传递文件容器的封装信息及码流参数
        AVCodecContext *codecCtx = NULL;//解码器上下文对象,解码器依赖的相关环境、状态、资源以及参数集的接口指针
        AVCodec *codec = NULL;//保存编解码器信息的结构体,提供编码与解码的公共接口,可以看作是编码器与解码器的一个全局变量
        SDL_AudioSpec wanted_spec, spec;//SDL_AudioSpec a structure that contains the audio output format,创建 SDL_AudioSpec 结构体,设置音频播放数据


        //检查输入的流类型是否在合理范围内
        if (stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
            return -1;
        }

// Get a pointer to the codec context for the video stream.
        codecCtx = avcodec_alloc_context3(NULL);//取得解码器上下文


        int ret = avcodec_parameters_to_context(codecCtx, pFormatCtx->streams[stream_index]->codecpar);
        if (ret < 0)
            return -1;

        //* Find the decoder for the video stream,根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)
        codec = avcodec_find_decoder(codecCtx->codec_id);
        if (!codec) {
            fprintf(stderr, "Unsupported codec!\n");
            return -1;
        }


        if (codecCtx->codec_type == AVMEDIA_TYPE_AUDIO)检查解码器类型是否为音频解码器
        {

            // Set audio settings from codec info,SDL_AudioSpec a structure that contains the audio output format
            // 创建SDL_AudioSpec结构体,设置音频播放参数
            wanted_spec.freq = codecCtx->sample_rate;//采样频率 DSP frequency -- samples per second
            wanted_spec.format = AUDIO_S16SYS;//采样格式 Audio data format
            wanted_spec.channels = 2;//codecCtx->channels;//声道数 Number of channels: 1 mono, 2 stereo
            wanted_spec.silence = 0;//无输出时是否静音
            //默认每次读音频缓存的大小,推荐值为 512~8192,ffplay使用的是1024
            // specifies a unit of audio data refers to the size of the audio buffer in sample frames
            wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;//采样个数 每秒采样多少
            wanted_spec.callback = audio_callback;//回调函数
            wanted_spec.userdata = is;//传递用户数据

            fprintf(stderr, "wanted spec: channels:%d, sample_fmt:%d, sample_rate:%d \n",
                    2, AUDIO_S16SYS, codecCtx->sample_rate);
            /*---------------------------
                  * 以指定参数打开音频设备,并返回与指定参数最为接近的参数,该参数为设备实际支持的音频参数
               * Opens the audio device with the desired parameters(wanted_spec)
               * return another specs we actually be using
               * and not guaranteed to get what we asked for
                --------------------------*/
            //打开音响设备。
            if (SDL_OpenAudio(&wanted_spec, &spec) < 0) {
                fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
                return -1;
            }
            is->audio_hw_buf_size = spec.size;//audio buffer size in bytes
        }
//打开解码器
        if (avcodec_open2(codecCtx, codec, NULL) < 0) {
            fprintf(stderr, "Unsupported codec!\n");
            return -1;
        }

        //检查解码器类型
        switch (codecCtx->codec_type) {
            case AVMEDIA_TYPE_AUDIO://音频解码器
                is->audioStream = stream_index;//音频流类型标号初始化
                is->audio_st = pFormatCtx->streams[stream_index];//音频流 AVstream
                is->audio_ctx = codecCtx;//编解码器上下文
                is->audio_buf_size = 0;//解码后的多帧音频数据长度
                is->audio_buf_index = 0;//累计写入stream的长度  使用了多少

                memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));//音频包初始化

                packet_queue_init(&is->audioq);//音频数据包队列初始化

                //Out Audio Param
                uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO;

                //AAC:1024  MP3:
                //采样数
                int out_nb_samples = is->audio_ctx->frame_size;
                //AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;

                int out_sample_rate = is->audio_ctx->sample_rate;
                int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);
                //Out Buffer Size
                /*
                int out_buffer_size=av_samples_get_buffer_size(NULL,
                                                               out_channels,
                                                               out_nb_samples,
                                                               AV_SAMPLE_FMT_S16,
                                                               1);
                                                               */

                //uint8_t *out_buffer=(uint8_t *)av_malloc(MAX_AUDIO_FRAME_SIZE*2);
                int64_t in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels);

                struct SwrContext *audio_convert_ctx;
                audio_convert_ctx = swr_alloc();//音频重采样上下文
                swr_alloc_set_opts(audio_convert_ctx,//重采样设置
                                   out_channel_layout,
                                   AV_SAMPLE_FMT_S16,
                                   out_sample_rate,
                                   in_channel_layout,
                                   is->audio_ctx->sample_fmt,
                                   is->audio_ctx->sample_rate,
                                   0,
                                   NULL);
                fprintf(stderr,
                        "swr opts: out_channel_layout:%lld, out_sample_fmt:%d, out_sample_rate:%d, in_channel_layout:%lld, in_sample_fmt:%d, in_sample_rate:%d",
                        out_channel_layout, AV_SAMPLE_FMT_S16, out_sample_rate, in_channel_layout,
                        is->audio_ctx->sample_fmt, is->audio_ctx->sample_rate);
                swr_init(audio_convert_ctx);//音频重采样初始化

                is->audio_swr_ctx = audio_convert_ctx;

                //audio callback starts running again,开启音频设备,如果这时候没有获得数据那么它就静音
                SDL_PauseAudio(0);//开始播放
                break;
            case AVMEDIA_TYPE_VIDEO://视频解码器
                is->videoStream = stream_index;//视频流类型标号初始化
                is->video_st = pFormatCtx->streams[stream_index];
                is->video_ctx = codecCtx;

                is->frame_timer = (double) av_gettime() / 1000000.0;//或者当前系统时间 转换成秒
                is->frame_last_delay = 40e-3;
                is->video_current_pts_time = av_gettime();

                packet_queue_init(&is->videoq);//视频数据包队列初始化

                // Initialize SWS context for software scaling,设置图像转换像素格式为AV_PIX_FMT_YUV420P  视频裁剪上下文
                is->video_sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
                                                   is->video_ctx->pix_fmt, is->video_ctx->width,
                                                   is->video_ctx->height, AV_PIX_FMT_YUV420P,
                                                   SWS_BILINEAR, NULL, NULL, NULL
                );
                is->video_tid = SDL_CreateThread(decode_video_thread, "decode_video_thread", is);//创建视频解码线程
                break;
            default:
                break;
        }
    }

audio_callback

音频回调函数

audio_decode_frame填充audio_buf,声卡缓冲区从audio_buf中拿数据

检查是否需要调用audio_decode_frame执行解码操作,audio_buf没满时就一直往里面填充数据,声卡播放完声卡缓冲区的数据后SDL主动调用回调函数填充声卡缓冲区。

/*
-----Audio Callback-------
* 音频输出回调函数,sdl通过该回调函数将解码后的pcm数据送入声卡播放,
* sdl通常一次会准备一组缓存pcm数据,通过该回调送入声卡,声卡根据音频pts依次播放pcm数据
* 待送入缓存的pcm数据完成播放后,再载入一组新的pcm缓存数据(每次音频输出缓存为空时,sdl就调用此函数填充音频输出缓存,并送入声卡播放)
*/
void audio_callback(void *userdata, Uint8 *stream, int len)//stream 声卡驱动缓冲区  len:声卡缓冲区剩余长度
{

  VideoState *is = (VideoState *)userdata;//传递用户数据
  int len1, audio_size;//每次写入stream的数据长度,解码后的数据长度
  double pts;

  SDL_memset(stream, 0, len);
    /*   len是由SDL传入的SDL缓冲区的大小,如果这个缓冲未满,我们就一直往里填充数据 */
  while(len > 0)//检查音频缓存的剩余长度
  {
      /*  audio_buf_index 和 audio_buf_size 标示我们自己用来放置解码出来的数据的缓冲区,*/
              /*   这些数据待copy到SDL缓冲区, 当audio_buf_index >= audio_buf_size的时候意味着我*/
      	        /*   们的缓冲为空,没有数据可供copy,这时候需要调用audio_decode_frame来解码出更
10	        /*   多的桢数据 */
    if(is->audio_buf_index >= is->audio_buf_size)//检查是否需要执行解码操作
    {
      /* We have already sent all our data; get more
       * 缓存队列中提取数据包、解码,并返回解码后的数据长度,audio_buf缓存中可能包含多帧解码后的音频数据*/
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
        /* audio_data_size < 0 标示没能解码出数据,我们默认播放静音 */
      if(audio_size < 0)//检查解码操作是否成功
      {
            /* If error, output silence */
            is->audio_buf_size = 1024 * 2 * 2;
          //全零重置缓冲区
            memset(is->audio_buf, 0, is->audio_buf_size);
      }
      else
      {
          //return a new  audio buffer size
            audio_size = synchronize_audio(is, (int16_t *)is->audio_buf,
                               audio_size, pts);
            is->audio_buf_size = audio_size;//返回packet中包含的原始音频数据长度(多帧)
      }
      is->audio_buf_index = 0;//初始化累计写入缓存长度
    }
    len1 = is->audio_buf_size - is->audio_buf_index;//计算解码缓存剩余长度
    if(len1 > len)//检查每次写入缓存的数据长度是否超过指定长度(1024)  如果解码的数据长度大于声卡缓冲区 就先用声卡缓冲区的len
      len1 = len;//指定长度从解码的缓存中取数据
     // 对音乐数据进行混音。
      //每次从解码的缓存数据中以指定长度抽取数据并写入stream传递给声卡
    SDL_MixAudio(stream,(uint8_t *)is->audio_buf + is->audio_buf_index, len1, SDL_MIX_MAXVOLUME);


    //memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;//更新解码音频缓存的剩余长度
    stream += len1;//更新缓存写入位置
    is->audio_buf_index += len1;//更新累计写入缓存数据长度
  }
}

synchronize_audio

/* Add or subtract samples to get a better sync, return new
   audio buffer size */
int synchronize_audio(VideoState *is, short *samples,
		      int samples_size, double pts) {
  int n;
  double ref_clock;

  n = 2 * is->audio_ctx->channels;
  
  if(is->av_sync_type != AV_SYNC_AUDIO_MASTER)
  {
    double diff, avg_diff;
    int wanted_size, min_size, max_size /*, nb_samples */;
    
    ref_clock = get_master_clock(is);
    diff = get_audio_clock(is) - ref_clock;

    if(diff < AV_NOSYNC_THRESHOLD) {
      // accumulate the diffs
      is->audio_diff_cum = diff + is->audio_diff_avg_coef
	* is->audio_diff_cum;
      if(is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) {
	is->audio_diff_avg_count++;
      } else {
	avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef);
	if(fabs(avg_diff) >= is->audio_diff_threshold)
	{
	  wanted_size = samples_size + ((int)(diff * is->audio_ctx->sample_rate) * n);
	  min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100);
	  max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100);
	  if(wanted_size < min_size)
	  {
	    wanted_size = min_size;
	  } else if (wanted_size > max_size)
	  {
	    wanted_size = max_size;
	  }
	  if(wanted_size < samples_size) {
	    /* remove samples */
	    samples_size = wanted_size;
	  } else if(wanted_size > samples_size) {
	    uint8_t *samples_end, *q;
	    int nb;

	    /* add samples by copying final sample*/
	    nb = (samples_size - wanted_size);
	    samples_end = (uint8_t *)samples + samples_size - n;
	    q = samples_end + n;
	    while(nb > 0) {
	      memcpy(q, samples_end, n);
	      q += n;
	      nb -= n;
	    }
	    samples_size = wanted_size;
	  }
	}
      }
    } else {
      /* difference is TOO big; reset diff stuff */
      is->audio_diff_avg_count = 0;
      is->audio_diff_cum = 0;
    }
  }
  return samples_size;
}

synchronize_video

主要就是为了更新一下video_clock,然后考虑了重复帧的情况

//更新视频帧的 PTS
    double synchronize_video(VideoState *is, AVFrame *src_frame, double pts)
    {
        /*
 *  video_clock是视频播放到当前帧时的已播放的时间长度。
 *  在synchronize函数中,如果没有得到该帧的PTS就用当前的video_clock来近似,然后更新video_clock的值。
    到这里已经知道了video中frame的显示时间了(秒为单位)
 * */

        double frame_delay;
        //在synchronize函数中,如果没有得到该帧的PTS就用当前的video_clock来近似,然后更新video_clock的值。
        if (pts != 0)
        {
            // success Get pts,then set video clock to it
            is->video_clock = pts;
        } else
        {
            // Don't get pts,set it to video clock
            pts = is->video_clock;//如果pts是无效的 使用上一次的
        }

        //帧的显示时间戳 =  pts(占了多少个时间刻度)  * time_base(每个时间刻度是多少秒)。
        /* update the video clock */
        frame_delay = av_q2d(is->video_ctx->time_base);
        /* if we are repeating a frame, adjust clock accordingly */
        frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);//what mean     如果有重复的帧就放一半的时间?
        is->video_clock += frame_delay;video_clock是视频播放到当前帧时的已播放的时间长度   update video_clock
        return pts;
    }

queue_picture

1.首先检查队列是否有插入空间,若没有足够的空间插入图像则使当前线程休眠等待,这里如果数据缓冲数组中图像帧的满了或收到退出信号,等待video_refresh_timer显示刷新函数消费数据后,继续执行。.

2、初始化/重置YUV overlay,调用alloc_picture为图像帧分配内存空间

3、拷贝视频帧到YUV overlay,这里调用sws_scale将解码后的图像帧转换为AV_PIX_FMT_YUV420P格式后放入yuv420p图片数据缓存区,更新size,更新写入位置

/*---------------------------
 * queue_picture:图像帧插入队列等待渲染
 * @is:全局状态参数集
 * @pFrame:保存图像解码数据的结构体
 * 1、首先检查图像帧队列(数组)是否存在空间插入新的图像,若没有足够的空间插入图像则使当前线程休眠等待
 * 2、在初始化的条件下,队列(数组)中VideoPicture的bmp对象(YUV overlay)尚未分配空间,调用alloc_picture分配空间
 * 3、当队列(数组)中所有VideoPicture的bmp对象(YUV overlay)均已分配空间的情况下,直接跳过步骤2向bmp对象拷贝像素数据,像素数据在进行格式转换后执行拷贝操作
 ---------------------------*/

//把解码的帧放入到图片队列中
int queue_picture(VideoState *is, AVFrame *pFrame, double pts)
{
/*--------1、检查队列是否有插入空间-------*/
    VideoPicture *vp;

    /* wait until we have space for a new pic */
    SDL_LockMutex(is->pictq_mutex);//锁定互斥量,保护图像帧队列
    //检查队列当前长度
    while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&!is->quit)
    {

            SDL_CondWait(is->pictq_cond, is->pictq_mutex);//线程休眠等待
    }
        SDL_UnlockMutex(is->pictq_mutex);//释放互斥量

        if (is->quit)//检查进程退出标识
            return -1;
/*-------2、初始化/重置YUV overlay-------*/
        // windex is set to 0 initially
        vp = &is->pictq[is->pictq_windex];//从图像帧队列中抽取图像帧对象

        // Allocate or resize the buffer,检查YUV overlay是否已存在,否则初始化YUV overlay,分配像素缓存空间
        if (!vp->bmp || vp->width != is->video_ctx->width || vp->height != is->video_ctx->height)
        {
            vp->allocated = 0;//图像帧未分配空间
            alloc_picture(is);
            if (is->quit)
            {
                return -1;
            }
        }
        /*--------3、拷贝视频帧到YUV overlay-------*/
        /* We have a place to put our picture on the queue */
        if (vp->bmp)
        {

            vp->pts = pts;
            //将解码后的图像帧转换为AV_PIX_FMT_YUV420P格式,并拷贝到图像帧队列
            // Convert the image into YUV format that SDL uses
            sws_scale(is->video_sws_ctx, (uint8_t const *const *) pFrame->data,
                      pFrame->linesize, 0, is->video_ctx->height,
                      vp->bmp->data, vp->bmp->linesize);

            /* now we inform our display thread that we have a pic ready */
            if (++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {//更新并检查当前图像帧队列写入位置
                is->pictq_windex = 0;//重置图像帧队列写入位置
            }

            SDL_LockMutex(is->pictq_mutex);//锁定队列读写锁,保护队列数据
            is->pictq_size++;//更新图像帧队列长度
            SDL_UnlockMutex(is->pictq_mutex);//释放队列读写锁
        }
        return 0;
    }

alloc_picture

根据设定的图像类型 长宽等参数调用avpicture_alloc为图像帧分配内存

/创建/重置图像帧,为图像帧分配内存空间
void alloc_picture(void *userdata) {

  int ret;

  VideoState *is = (VideoState *)userdata;//传递用户数据
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];//从图像帧队列(数组)中提取图像帧结构对象

  if(vp->bmp)
  {//检查图像帧是否已存在

    // we already have one make another, bigger/smaller
    avpicture_free(vp->bmp);//释放当前overlay缓存
    free(vp->bmp);

    vp->bmp = NULL;
  }

  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(text_mutex);//锁定互斥量,保护画布的像素数据

//根据指定尺寸及像素格式重新创建像素缓存区
  vp->bmp = (AVPicture*)malloc(sizeof(AVPicture));
  ret = avpicture_alloc(vp->bmp, AV_PIX_FMT_YUV420P, is->video_ctx->width, is->video_ctx->height);
  if (ret < 0) {
      fprintf(stderr, "Could not allocate temporary picture: %s\n", av_err2str(ret));
  }

  SDL_UnlockMutex(text_mutex);//释放互斥锁

  vp->width = is->video_ctx->width;//设置图像帧宽度
  vp->height = is->video_ctx->height;//设置图像帧高度
  vp->allocated = 1;//图像帧像素缓冲区已分配内存

}

audio_decode_frame

//音频解码函数,从缓存队列中提取数据包、解码,并返回解码后的数据长度(对一个完整的packet解码,将解码数据写入audio_buf缓存,并返回多帧解码数据的总长度)
int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr)
{

  int len1, data_size = 0;//每次消耗的编码数据长度[input](len1),输出原始音频数据的缓存长度[output]
  AVPacket *pkt = &is->audio_pkt;//保存从队列中提取的数据包
  double pts;
  int n;


  for(;;)
  {
      //如果音频流队列中还有音频包
    while(is->audio_pkt_size > 0)检查缓存中剩余的编码数据长度(是否已完成一个完整的pakcet包的解码,一个数据包中可能包含多个音频编码帧)
    {
      int got_frame = 0;//解码操作成功标识,成功返回非零值

        //解码一帧音频数据,并返回消耗的编码数据长度
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) //检查是否执行了解码操作
      {
	/* if error, skip frame */
	is->audio_pkt_size = 0;//更新编码数据缓存长度
	break;
      }
      data_size = 0;
      if(got_frame) //检查解码操作是否成功
      {
        /*
	    data_size = av_samples_get_buffer_size(NULL,
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
        */
          //计算解码后音频数据长度[output]
        data_size = 2 * is->audio_frame.nb_samples * 2;//what mean?
        ///assert宏的原型定义在中,其作用是如果它的条件返回错误,则终止程序执行。
	assert(data_size <= buf_size);
//重采样
        swr_convert(is->audio_swr_ctx,
                        &audio_buf,
                        MAX_AUDIO_FRAME_SIZE*3/2,
                        (const uint8_t **)is->audio_frame.data,
                        is->audio_frame.nb_samples);

        //将解码数据复制到输出缓存
        fwrite(audio_buf, 1, data_size, audiofd);
	//memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;//更新编码数据缓存指针位置
      is->audio_pkt_size -= len1;//更新缓存中剩余的编码数据长度
      if(data_size <= 0) //检查输出解码数据缓存长度
      {
	/* No data yet, get more frames */
	continue;
      }
/*
 * 这里得到的audio_clock是不准确的 ,
 * 需要时间来把数据从音频包中移动到我们的输出缓冲区中。
 * 这意味着我们音频时钟中记录的时间比实际的要早太多。所以我们必须要检查一下我们还有多少没有写入
 * */
        // 每秒钟音频播放的字节数 sample_rate * channels * sample_format(一个sample占用的字节数)
      pts = is->audio_clock;
      *pts_ptr = pts;//pts_ptr是一个用来通知audio_callback函数当前声音包的时间戳的指针
      n = 2 * is->audio_ctx->channels;//乘以2是因为sample format是16位的无符号整型,占用2个字节。
      //
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_ctx->sample_rate);

      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt->data)//检查数据包是否已从队列中提取
      av_free_packet(pkt);//释放pkt中保存的编码数据

    if(is->quit) {//检查退出进程标识
      return -1;
    }
    /* next packet 从队列中提取数据包到pkt */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0)
    {
      return -1;
    }
    is->audio_pkt_data = pkt->data;//传递编码数据缓存指针
    is->audio_pkt_size = pkt->size;//传递编码数据缓存长度

    /*
     * Audio Clock,也就是Audio的播放时长,可以在Audio时更新Audio Clock。
     * 在函数audio_decode_frame中解码新的packet,这是可以设置Auddio clock为该packet的PTS
     * */
//我们得到新的包的时候:我们简单的设置声音时钟为这个包的时间戳
    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE)
    {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }
  }
}

get_audio_clock

最后一帧音频的pts不准确,还需要减去音频缓冲区中还没有消费的数据所需要的时间

//有了Audio clock后,在外面获取该值的时候却不能直接返回该值,因为audio缓冲区的可能还有未播放的数据,需要减去这部分的时间
double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  //用audio缓冲区中剩余的数据除以每秒播放的音频数据得到剩余数据的播放时间,从Audio clock中减去这部分的值就是当前的audio的播放时长
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_ctx->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_ctx->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

video_display

从图像帧队列(数组)中提取图像帧结构对象

解码后的视频帧存放到纹理中

SDL常规渲染操作

//视频(图像)帧渲染
void video_display(VideoState *is)
{

  SDL_Rect rect;//SDL矩形对象
  VideoPicture *vp;//图像帧结构体指针
  float aspect_ratio;//宽度/高度比
  int w, h, x, y;//窗口尺寸及起始位置
  int i;

  vp = &is->pictq[is->pictq_rindex];//从图像帧队列(数组)中提取图像帧结构对象
    //检查像素数据指针是否有效
  if(vp->bmp) {

    SDL_UpdateYUVTexture( texture, NULL, //解码后的视频帧存放到纹理中
                          vp->bmp->data[0], vp->bmp->linesize[0],
                          vp->bmp->data[1], vp->bmp->linesize[1],
                          vp->bmp->data[2], vp->bmp->linesize[2]);

      //设置矩形显示区域
    rect.x = 0;
    rect.y = 0;
    rect.w = is->video_ctx->width;
    rect.h = is->video_ctx->height;
    SDL_LockMutex(text_mutex);//锁定互斥量,保护画布的像素数据
    SDL_RenderClear( renderer );
    SDL_RenderCopy( renderer, texture, NULL, &rect);
    SDL_RenderPresent( renderer );
    SDL_UnlockMutex(text_mutex);//释放互斥锁

  }
}

packet_queue_init

//数据包队列初始化函数
void packet_queue_init(PacketQueue *q)
{
  memset(q, 0, sizeof(PacketQueue));//全零初始化队列结构体对象
  q->mutex = SDL_CreateMutex();//创建互斥量对象
  q->cond = SDL_CreateCond();//创建条件变量对象
}

packet_queue_put

常规队列操作 注意操作数据的时候要加锁

插入了新的数据后要要信号通知取数据线程可以拿数据了

/向队列中插入数据包
int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
/*-------准备队列(链表)节点对象------*/
  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));//在堆上创建链表节点对象
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;//将输入数据包赋值给新建链表节点对象中的数据包对象
  pkt1->next = NULL;//链表后继指针为空
    /*---------将新建节点插入队列-------*/
    SDL_LockMutex(q->mutex);队列互斥量加锁,保护队列数据
队列操作:如果last_pkt为空,说明队列是空的,新增节点为队头;否则,队列有数据,则让原队尾的next为新增节点。 最后将队尾指向新增节点
  if (!q->last_pkt)//检查队列尾节点是否存在(检查队列是否为空)
    q->first_pkt = pkt1;//若不存在(队列尾空),则将当前节点作队列为首节点
  else
    q->last_pkt->next = pkt1;//若已存在尾节点,则将当前节点挂到尾节点的后继指针上,并作为新的尾节点
  q->last_pkt = pkt1;//将当前节点作为新的尾节点
  //
  q->nb_packets++;//队列长度+1
  q->size += pkt1->pkt.size;//更新队列编码数据的缓存长度


    //发出信号,表明当前队列中有数据了,通知等待中的读线程可以取数据了
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);//释放互斥量
  return 0;
}

packet_queue_get

常规队列操作 注意操作数据的时候要加锁

这个有一个for循环,通过SDL_CondWait等待,暂时对互斥量解锁,该线程阻塞等待packet_queue_put线程给他

/* return < 0 if aborted, 0 if no packet and > 0 if packet.  */
//block: 调用者是否需要在没节点可取的情况下阻塞等待
//AVPacket: 输出参数,即MyAVPacketList.pkt

//从队列中提取数据包,并将提取的数据包出队列
int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;//临时链表节点对象指针
  int ret;//操作结果

  SDL_LockMutex(q->mutex);//队列互斥量加锁,保护队列数据
  
  for(;;)
  {
    //如果此时需要退出
    if(global_video_state->quit)
    {
      ret = -1;
      break;
    }
    //从队头拿数据
    pkt1 = q->first_pkt;
      //队列中有数据
    if (pkt1)
    {
      q->first_pkt = pkt1->next;队头移到第二个节点
      if (!q->first_pkt)//检查首节点的后继节点是否存在
      {
          q->last_pkt = NULL;//若不存在,则将尾节点指针置空
      }


      q->nb_packets--;//队列长度-1
      q->size -= pkt1->pkt.size;//更新队列编码数据的缓存长度
       // 返回AVPacket,这里发生一次AVPacket结构体拷贝,AVPacket的data只拷贝了指针
      *pkt = pkt1->pkt;//将队列首节点数据返回
      av_free(pkt1);//清空临时节点数据(清空首节点数据,首节点出队列)
      ret = 1;//操作成功
      break;
    }
    else if (!block)
    {//队列中没有数据,且非阻塞调用
      ret = 0;
      break;
    }
    else
    {//队列中没有数据,且阻塞调用
      //  此时通过SDL_CondWait函数等待qready就绪信号,并暂时对互斥量解锁
/*---------------------
			 * 等待队列就绪信号qready,并对互斥量暂时解锁
			 * 此时线程处于阻塞状态,并置于等待条件就绪的线程列表上
			 * 使得该线程只在临界区资源就绪后才被唤醒,而不至于线程被频繁切换
			 * 该函数返回时,互斥量再次被锁住,并执行后续操作
			 --------------------*/

        SDL_CondWait(q->cond, q->mutex);//暂时解锁互斥量并将自己阻塞,等待临界区资源就绪(等待SDL_CondSignal发出临界区资源就绪的信号)
    }
  }
  SDL_UnlockMutex(q->mutex);//释放互斥量
  return ret;
}

完整的代码和cmakelist文件及可执行文件如下
https://download.csdn.net/download/agentky/19243920?spm=1001.2014.3001.5501

你可能感兴趣的:(音视频)