• FFmpeg编程(四)SDL与FFmpeg的联合使用


    一:简单的播放器V1(只播放视频)

    (一)回顾

    FFmpeg编程(二)FFmpeg中级开发

    FFmpeg编程(三)SDL开发

    (二)FFmpeg与SDL的简单结合

    #include <stdio.h>
    #include <SDL.h>
    
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libswscale/swscale.h>
    
    int main(int argc,char* argv[]){
        char* in_file = NULL;
    
        int ret = -1;            //返回值
        int len;                //存放读取的数据大小
        int stream_idx;            //存放视频流的索引
        int frameFin;            //标志packet中数据帧是否读取完成
    
        AVFormatContext* fmt_cxt = NULL;    //格式上下文
        AVCodecContext* codec_cxt = NULL;    //编解码器上下文
        struct SwsContext* sws_cxt = NULL;    //图像处理上下文
    
        AVCodec* codec = NULL;    //编解码器
        AVFrame* frame = NULL;    //
        AVPicture* pict = NULL;    //YUV图像存放
    
        AVPacket packet;        //
    
        SDL_Window* win = NULL;    //SDL窗口指针
        SDL_Renderer* rend = NULL;    //SDL渲染器指针
        SDL_Texture* text = NULL;    //纹理
    
        SDL_Rect rect;    //用于显示数据的矩阵区域
    
        SDL_Event event;    //SDL事件
    
        int w_width = 640;    //设置的默认窗口大小,后面会进行调整
        int w_height = 480;
    
        if(argc<2){        //检查是否设置了播放的文件名称
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Usage: command <file>");
            return ret;
        }
        in_file = argv[1];
    
        if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to initialize SDL - %s!",SDL_GetError());
            return ret;
        }
        //------------------------配置所有的FFmpeg信息
        
        av_register_all();    //注册所有协议
    
        //获取格式上下文
        if(avformat_open_input(&fmt_cxt,in_file,NULL,NULL)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to open video!");
            goto __INIT;
        }
    
        //获取流的信息,这里换一种方式获取,不直接使用av_find_best_stream获取
        stream_idx = -1;
        for(int i=0;i<fmt_cxt->nb_streams;i++){
            if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
                stream_idx = i;
                break;
            }
        }
    
        if(stream_idx==-1){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream!");
            goto __FORMAT;
        }
    
        //打印输入视频流的详细信息
        if(avformat_find_stream_info(fmt_cxt,0)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream information!");
            goto __FORMAT;
        }
    
        av_dump_format(fmt_cxt,stream_idx,in_file,0);    //打印信息
    
        //开始获取解码器,对视频流进行解码获取YUV数据
        //根据输入流的参数获取对于的解码器
        codec = avcodec_find_decoder(fmt_cxt->streams[stream_idx]->codec->codec_id);
        if(codec==NULL){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder!");
            goto __FORMAT;
        }
    
        //获取对应上下文
        codec_cxt = avcodec_alloc_context3(codec);
        if(!codec_cxt){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder context!");
            goto __FORMAT;
        }
    
        //开始拷贝输入流参数到解码器中
        if(avcodec_copy_context(codec_cxt,fmt_cxt->streams[stream_idx]->codec)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy video decoder context!");
            goto __CODEC;
        }
    
        //打开编解码器
        if(avcodec_open2(codec_cxt,codec,NULL)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn video decoder!");
            goto __CODEC;
        }
    
        frame = av_frame_alloc();    //分配帧
        if(!frame){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video frame!");
            goto __CODEC;
        }
    
        //创建图片转换上下文(在上面参数拷贝后面)-----------------
        //源图像宽、高、像素格式;目标图像宽、高、像素格式;以及图像拉伸使用的算法
        sws_cxt = sws_getContext(codec_cxt->width,codec_cxt->height,codec_cxt->pix_fmt,
            codec_cxt->width,codec_cxt->height,AV_PIX_FMT_YUV420P,    //输出像素格式YUV
            SWS_BILINEAR,NULL,NULL,NULL);    //获取图像处理上下文
    
        pict = (AVPicture*)malloc(sizeof(AVPicture));
        if(!pict){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video picture frame!");
            goto __FRAME;
        }
        avpicture_alloc(pict,
            AV_PIX_FMT_YUV420P,
            codec_cxt->width,
            codec_cxt->height);    //为图像存放分配空间
    
        //------------------------开始设置SDL数据
        w_width = codec_cxt->width;
        w_height = codec_cxt->height;
    
        //创建窗口
        win = SDL_CreateWindow("Media Player",
            SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,    //不指定左上位置
            w_width,w_height,            //设置窗口宽高,与视频一般或者大些都可以
            SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);    //窗口可用于OpenGL上下文 窗口可以调整大小
        if(!win){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create window to SDL");
            goto __PICT;
        }
    
        //创建渲染器
        rend = SDL_CreateRenderer(win,-1,0);
        if(!rend){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create renderer to SDL");
            goto __WIN;
        }
    
        //创建纹理
        text = SDL_CreateTexture(rend,SDL_PIXELFORMAT_IYUV,
            SDL_TEXTUREACCESS_STREAMING,    //视频流,连续的
            w_width,w_height);
        if(!text){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create texture to SDL");
            goto __RENDER;
        }
        
        rect.x = 0;    //显示矩阵的左上点
        rect.y = 0;
        //------------------------开始读取数据,结合SDL与FFmpeg
        av_init_packet(&packet);    //初始化数据包
        while(av_read_frame(fmt_cxt,&packet)>=0){        
            if(packet.stream_index==stream_idx){    //属于视频流
                len = avcodec_decode_video2(codec_cxt,frame,&frameFin,&packet);    //作用是解码一帧视频数据。输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame。
                if(len<0){
                    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Fail to decode video frame");
                    goto __INIT;
                }
    
                if(frameFin){    //表示读取完成了一帧,开始进行转换,解码数据已经放入了frame中
                    sws_scale(sws_cxt,(uint8_t const * const *)frame->data,frame->linesize,    //当前处理区域的每个通道数据指针,每个通道行字节数
                        0,codec_cxt->height,    //参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。
                        pict->data,pict->linesize);     //定义输出图像信息(输出的每个通道数据指针,每个通道行字节数)
    
                    //可以渲染YUV数据
                    SDL_UpdateYUVTexture(text,NULL,
                        pict->data[0],pict->linesize[0],
                        pict->data[1],pict->linesize[1],
                        pict->data[2],pict->linesize[2]);
    
                    //更新矩阵的信息
                    rect.w = codec_cxt->width;
                    rect.h = codec_cxt->height;
    
                    SDL_RenderClear(rend);                   //清空渲染器缓冲区
    
                    SDL_RenderCopy(rend,text,NULL,&rect);    //拷贝纹理到显卡,选择渲染目标的一块矩形区域作为输出
                    SDL_RenderPresent(rend);                //将结果显示到窗口
                }
            }
    
            av_packet_unref(&packet);    //注意:减少引用不会释放空间,因为本函数还在引用这个packet结构体;
            //所以我们在解码函数中剩余的其他帧数据,还是保留在packet中的,后面根据av_read_frame继续向内部添加数据
    
            //设置事件轮询
            SDL_PollEvent(&event);
            switch(event.type){
                case SDL_QUIT:
                    goto __QUIT;
                    break;
                default:
                    break;
            }
    
        }
    
    __QUIT:
        ret = 0;
        av_packet_unref(&packet);    //减少引用,使得自己释放空间
        SDL_DestroyTexture(text);
    __RENDER:
        SDL_DestroyRenderer(rend);
    __WIN:
        SDL_DestroyWindow(win);
    __PICT:
        avpicture_free(pict);
        free(pict);
    __FRAME:
        av_frame_free(&frame);
    __CODEC:
        avcodec_close(codec_cxt);
    __FORMAT:
        avformat_close_input(&fmt_cxt);
    __INIT:
        SDL_Quit();
    
        return ret;
    }
    View Code
    gcc playerV1.c -o play_1 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil
    ./play_1 gfxm.mp4

    二:简单的播放器V2(播放视频和音频,未同步)

    (一)回顾锁与条件变量

    SDL中的互斥量和条件变量

    (二)基于队列实现音频数据的音视频播放器

    #include <stdio.h>
    #include <assert.h>
    #include <SDL.h>
    
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    
    #define SDL_AUDIO_BUFFER_SIZE 1024    //对于AAC,一帧数据中单通道获取 1024个 sample采样点
    #define    MAX_AUDIO_FRAME_SIZE 192000        //是指双通道下,采用48k采样率,位深为16位,采样时间为1s
    
    //定义队列
    typedef struct PacketQueue{
        AVPacketList* first_pkt,* last_pkt;
        int nb_packets;        //数据包个数
        int size;            //队列全部数据量大小
        SDL_mutex* mutex;    //队列中的锁
        SDL_cond* cond;        //条件变量
    }PacketQueue;
    
    struct SwrContext* adswr_cxt = NULL;    //全局音频重采样上下文
    PacketQueue audioq;        //定义全局音频数据包队列
    int quit = 0;            //标志音频数据是否全部读取完成
    
    //队列初始化数据成员
    void packet_queue_init(PacketQueue* q){
        memset(q,0,sizeof(PacketQueue));
        q->mutex = SDL_CreateMutex();    //创建互斥锁
        q->cond = SDL_CreateCond();        //创建条件变量
    }
    
    //向队列中添加数据包
    int packet_queue_put(PacketQueue* q,AVPacket* pkt){
        AVPacketList* pktl;
    
        //先对数据包加引用,使得不被提前释放
        /*
        原本数据采访在AVPacket成员---->AVBufferRef *buf中,由av_malloc申请的独立的buffer(unshared buffer);
        使用av_dup_packet后,拷贝了一份数据到data成员中,重新设置pkt->data终于有自己的独立内存了,不用共享别的AVPacket的内存
    
        https://blog.csdn.net/weixin_34416649/article/details/92075939
        https://blog.csdn.net/wangshilin/article/details/8186608
        当某个AVPacket结构的数据缓冲区不再被使用时,要需要通过调用 av_free_packet 释放。av_free_packet调用的是结构体本身的destruct函数
    
        安全起见,如果用户希望 自由地使用一个FFMPEG内部创建的AVPacket结构,最好调用av_dup_packet进行缓冲区的克隆,
        将其转化为缓冲区能够被释放的 AVPacket,以免对缓冲区的不当占用造成异常错误。
        av_dup_packet会为destruct指针为 av_destruct_packet_nofree的AVPacket新建一个缓冲区,然后将原缓冲区的数据拷贝至新缓冲区,置data的值为新缓冲区 的地址,
        同时设destruct指针为av_destruct_packet。
        */
        if(av_dup_packet(pkt)){    //使得更加安全,保证了数据不被别人共享,但是不能保证不被av_packet_unref删除,所以我们在主函数中处理音频数据包的时候,不要调用unref删除
            return -1;    //原本数据在buffer中,但是put操作将内容放入了data中,使得packet使用=赋值可以获取到我们想要的数据起始地址!!!!
        }
    
        pktl = av_malloc(sizeof(AVPacketList));
        if(!pktl){
            return -1;
        }
    
        pktl->pkt = *pkt;
        pktl->next = NULL;
    
        SDL_LockMutex(q->mutex);    //加锁,开始添加
        if(!q->last_pkt){
            q->first_pkt = pktl;
        }else{
            q->last_pkt->next = pktl;
        }
    
        q->last_pkt = pktl;
        q->nb_packets++;
        q->size += pktl->pkt.size;
    
        SDL_CondSignal(q->cond);    //重点:对于条件变量,发送信号前会进行解锁,发送后进行加锁------------
        SDL_UnlockMutex(q->mutex);
        return 0;
    }
    
    int packet_queue_get(PacketQueue* q,AVPacket* pkt,int block){
        AVPacketList* pktl;
        int ret;
    
        SDL_LockMutex(q->mutex);    //读取前加锁
        for(;;){    //如果没有数据则进行等待(当然不是一直加锁状态下死等,后面会使用条件变量,使得先解锁,等待信号量到达,再加锁处理)
            if(quit){    //音频数据全部读取完成,退出
                ret = -1;
                break;
            }
    
            pktl = q->first_pkt;
            if(pktl){    //队列中存在数据,直接读取
                q->first_pkt = pktl->next;
                if(!q->first_pkt)    //原本只有一个数据时候,尾指针也指向这个数据,所以也要处理,置为空
                    q->last_pkt = NULL;
                q->nb_packets--;
                q->size -= pktl->pkt.size;
                *pkt = pktl->pkt;
                av_free(pktl);    //只是释放了AVPakcetList,并没有释放内部的packet数据
                ret = 1;
                break;    //数据已经读取到pkt中了
            }else if(!block){    //队列中不存在数据,并且设置了block,不进行阻塞等待条件变量
                ret = 0;    //这时并没有返回如何数据,我们对应处理设置静默音即可
                break;
            }else{    //需要阻塞等待获取到数据,需要设置条件变量
                SDL_CondWait(q->cond,q->mutex);
            }
        }
        SDL_UnlockMutex(q->mutex);
        return ret;
    }
    
    //对数据包进行解码返回
    int audio_decode_frame(AVCodecContext* adcodec_cxt,uint8_t* buf,int size){
        //下面是静态存储区全局变量
        static AVPacket pkt;
        static uint8_t* audio_pkt_data = NULL;
        static int audio_pkt_size = 0;    //用来存放从队列中获取的packet大小
        static AVFrame frame;
    
        int len1,data_size=0;
    
        for(;;){
            while(audio_pkt_size>0){    //pakcet中还有数据没有处理完成
                int got_frame = 0;
                len1 = avcodec_decode_audio4(adcodec_cxt,&frame,&got_frame,&pkt);    //从pkt中解码packet数据为一帧一帧的frame数据,然后由swr进行重采样处理
                if(len1<0){    //解码出错,跳过该packet的数据
                    audio_pkt_size = 0;
                    break;
                }
    
                audio_pkt_data += len1;
                audio_pkt_size -= len1;
                data_size = 0;
                if(got_frame){    //获取了一帧数据
                    //dst_size = av_samples_get_buffer_size(NULL,out_channels,out_nb_samples,sample_fmt,1);    //这是每次采样的数据 = 通道数×每个通道采样(每帧)×格式
                    data_size = 2*2*frame.nb_samples;    //双通道,16位深
    
                    assert(data_size<=data_size);
                    swr_convert(adswr_cxt,&buf,
                        MAX_AUDIO_FRAME_SIZE*3/2,
                        (const uint8_t**)frame.data,
                        frame.nb_samples);    //将重采样结果放入buf中去
                }
                if(data_size<=0){    //==0,表示我们的packet中的剩余数据还没有达到获取一帧的数据大小,继续去获取    
                    continue;
                }
                return data_size;    //已经获取了一帧数据,返回
            }
    
            //不在上面while循环,说明需要去获取队列中数据
            if(pkt.data)    //先判断pkt是否清空
                av_free_packet(&pkt);    
    
            if(quit)
                return -1;
    
            if(packet_queue_get(&audioq,&pkt,1)<0){
                return -1;
            }
    
            audio_pkt_data = pkt.data;    //赋予初始值--------重点:原本数据在buffer中,但是前面的put操作将内容放入了data中
            audio_pkt_size = pkt.size;
        }
    
    }
    
    //设置实现音频声卡回调函数,用于声卡主动获取数据
    void audio_callback(void* userdata,uint8_t* stream,int len){    
        //先获取编解码上下文
        AVCodecContext* adcodec_cxt = (AVCodecContext*)userdata;
        int audio_size,len1;    //每次读取的数据包的大小,和,每次可以读取的数据大小
        //设置全局静态存储区变量
        static uint8_t audio_buf[MAX_AUDIO_FRAME_SIZE*3/2];
        static unsigned int audio_buf_size = 0;        //音频数据大小
        static unsigned int audio_buf_index = 0;    //音频数据起始位置,当当前数据处理完成,即index到达最后size,就开始解码下一个音频数据包
    
        while(len>0){    //声卡需要的数据长度
            if(audio_buf_index>=audio_buf_size){    //开始去获取下一个数据包
                audio_size = audio_decode_frame(adcodec_cxt,audio_buf,sizeof(audio_buf));
                if(audio_size<0){    //设置静默声音
                    audio_buf_size = 1024;
                    memset(audio_buf,0,audio_buf_size);
                }else{
                    audio_buf_size = audio_size;    //设置为获取的数据包大小
                }
                audio_buf_index = 0;    //设置起始位置为首部
            }
            //有数据了,开始读取数据到声卡
            len1 = audio_buf_size - audio_buf_index;
            if(len1 > len){    //可以直接读取
                len1 = len;
            }
            printf("index=%d,len1=%d,len=%d
    ",audio_buf_index,len1,len);
            memcpy(stream,(uint8_t*)audio_buf+audio_buf_index,len1);
            len -= len1;    //开始改变数据位置
            stream += len1;
            audio_buf_index += len1;
        }
    }
    
    
    int main(int argc,char* argv[]){
        char* in_file = NULL;
    
        int ret = -1;            //返回值
        int len;                //存放读取的数据大小
        int vdstream_idx;            //存放视频流的索引
        int adstream_idx;        //存放视频流的索引
        int frameFin;            //标志packet中数据帧是否读取完成
    
        AVFormatContext* fmt_cxt = NULL;    //格式上下文
        struct SwsContext* sws_cxt = NULL;    //图像处理上下文
    
        //----------视频编解码器----------
        AVCodec* vdcodec = NULL;    //编解码器
        AVCodecContext* vdcodec_cxt = NULL;    //编解码器上下文
    
        //----------音频编解码器----------
        AVCodec* adcodec = NULL;
        AVCodecContext* adcodec_cxt = NULL;    
    
        AVFrame* frame = NULL;    //
        AVPicture* pict = NULL;    //YUV图像存放
    
        AVPacket packet;        //
    
        SDL_Window* win = NULL;    //SDL窗口指针
        SDL_Renderer* rend = NULL;    //SDL渲染器指针
        SDL_Texture* text = NULL;    //纹理
    
        SDL_Rect rect;    //用于显示数据的矩阵区域
    
        int w_width = 640;    //设置的默认窗口大小,后面会进行调整
        int w_height = 480;
    
        SDL_Event event;    //SDL事件
    
        SDL_AudioSpec new_spec,old_spec;    //用来设置音频参数,分别表示原始数据和设置的新参数
        int64_t in_channel_layout;    //音频的布局
        int64_t out_channel_layout;
    
        if(argc<2){        //检查是否设置了播放的文件名称
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Usage: command <file>");
            return ret;
        }
        in_file = argv[1];
    
        if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to initialize SDL - %s!",SDL_GetError());
            return ret;
        }
        //------------------------配置所有的FFmpeg信息
        
        av_register_all();    //注册所有协议
    
        //获取格式上下文
        if(avformat_open_input(&fmt_cxt,in_file,NULL,NULL)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to open video!");
            goto __FAIL;
        }
    
        //获取流的信息,这里换一种方式获取,不直接使用av_find_best_stream获取
        vdstream_idx = -1;
        adstream_idx = -1;
        for(int i=0;i<fmt_cxt->nb_streams;i++){
            if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && vdstream_idx<0){
                vdstream_idx = i;
            }
            if(fmt_cxt->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && adstream_idx<0){
                adstream_idx = i;
            }
        }
    
        if(vdstream_idx==-1 || adstream_idx==-1){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream or audio stream!");
            goto __FAIL;
        }
    
        //打印输入视频流的详细信息
        if(avformat_find_stream_info(fmt_cxt,0)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to get video stream information!");
            goto __FAIL;
        }
    
        av_dump_format(fmt_cxt,vdstream_idx,in_file,0);    //打印信息
        av_dump_format(fmt_cxt,adstream_idx,in_file,0);    //打印信息
    
        //-----------------处理音频编解码器---------------
        adcodec = avcodec_find_decoder(fmt_cxt->streams[adstream_idx]->codec->codec_id);
        if(adcodec==NULL){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find audio decoder!");
            goto __FAIL;
        }
    
        adcodec_cxt = avcodec_alloc_context3(adcodec);
        if(!adcodec_cxt){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find adio decoder context!");
            goto __FAIL;
        }
    
        if(avcodec_copy_context(adcodec_cxt,fmt_cxt->streams[adstream_idx]->codec)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy audio decoder context!");
            goto __FAIL;
        }
    
        //打开编解码器
        if(avcodec_open2(adcodec_cxt,adcodec,NULL)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn audio decoder!");
            goto __FAIL;
        }
    
        //-----------------处理音频数据---------------
        new_spec.freq = adcodec_cxt->sample_rate;
        new_spec.format = AUDIO_S16SYS;
        new_spec.channels = adcodec_cxt->channels;
        new_spec.silence = 0;
        new_spec.samples = SDL_AUDIO_BUFFER_SIZE;    
        new_spec.callback = audio_callback;
        new_spec.userdata = adcodec_cxt;
    
        if(SDL_OpenAudio(&new_spec,&old_spec)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn audio device - %s!",SDL_GetError());
            goto __FAIL;
        }
    
        packet_queue_init(&audioq);    //初始化音频数据包队列
    
        in_channel_layout = av_get_default_channel_layout(adcodec_cxt->channels);    //获取输入流的通道布局
        out_channel_layout = in_channel_layout;    //输出流通道布局不变
        printf("in layout:%ld, out layout:%ld
    ",in_channel_layout,out_channel_layout);
    
        adswr_cxt = swr_alloc();    //分配音频重采样上下文
        if(!adswr_cxt){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc audio resample context!");
            goto __FAIL;
        }
    
        //----------创建重采样的上下文
        swr_alloc_set_opts(adswr_cxt,             //设置已经创建好的上下文,如果没有,则为NULL
                        out_channel_layout,       //设置输出目标的通道布局(双声道,立体声,...,方位增宽)
                        AV_SAMPLE_FMT_S16,        //设置输出目标的采样格式,与上面的AUDIO_S16SYS保持一致
                        adcodec_cxt->sample_rate, //设置输出目标的采样率
                        in_channel_layout,        //输入数据的通道布局,是双声道
                        adcodec_cxt->sample_fmt,  //输入数据的采样格式为s16le
                        adcodec_cxt->sample_rate, //输入的采样率
                        0,                        //日志级别
                        NULL);                    //日志上下文
    
        swr_init(adswr_cxt);
    
        SDL_PauseAudio(0);    //开始播放音频
    
        //-----------------处理视频编解码器---------------
        //开始获取解码器,对视频流进行解码获取YUV数据
        //根据输入流的参数获取对于的解码器
        vdcodec = avcodec_find_decoder(fmt_cxt->streams[vdstream_idx]->codec->codec_id);
        if(vdcodec==NULL){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder!");
            goto __FAIL;
        }
    
        //获取对应上下文
        vdcodec_cxt = avcodec_alloc_context3(vdcodec);
        if(!vdcodec_cxt){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to find video decoder context!");
            goto __FAIL;
        }
    
        //开始拷贝输入流参数到解码器中
        if(avcodec_copy_context(vdcodec_cxt,fmt_cxt->streams[vdstream_idx]->codec)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to copy video decoder context!");
            goto __FAIL;
        }
    
        //打开编解码器
        if(avcodec_open2(vdcodec_cxt,vdcodec,NULL)<0){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to opn video decoder!");
            goto __FAIL;
        }
    
        //-----------------处理视频数据---------------
    
        frame = av_frame_alloc();    //分配帧
        if(!frame){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video frame!");
            goto __FAIL;
        }
    
        //创建图片转换上下文(在上面参数拷贝后面)-----------------
        //源图像宽、高、像素格式;目标图像宽、高、像素格式;以及图像拉伸使用的算法
        sws_cxt = sws_getContext(vdcodec_cxt->width,vdcodec_cxt->height,vdcodec_cxt->pix_fmt,
            vdcodec_cxt->width,vdcodec_cxt->height,AV_PIX_FMT_YUV420P,    //输出像素格式YUV
            SWS_BILINEAR,NULL,NULL,NULL);    //获取图像处理上下文
    
        pict = (AVPicture*)malloc(sizeof(AVPicture));
        if(!pict){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to alloc video picture frame!");
            goto __FAIL;
        }
        avpicture_alloc(pict,
            AV_PIX_FMT_YUV420P,
            vdcodec_cxt->width,
            vdcodec_cxt->height);    //为图像存放分配空间
    
        //------------------------开始设置SDL数据
        w_width = vdcodec_cxt->width;
        w_height = vdcodec_cxt->height;
    
        //创建窗口
        win = SDL_CreateWindow("Media Player",
            SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,    //不指定左上位置
            w_width,w_height,            //设置窗口宽高,与视频一般或者大些都可以
            SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);    //窗口可用于OpenGL上下文 窗口可以调整大小
        if(!win){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create window to SDL");
            goto __FAIL;
        }
    
        //创建渲染器
        rend = SDL_CreateRenderer(win,-1,0);
        if(!rend){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create renderer to SDL");
            goto __FAIL;
        }
    
        //创建纹理
        text = SDL_CreateTexture(rend,SDL_PIXELFORMAT_IYUV,
            SDL_TEXTUREACCESS_STREAMING,    //视频流,连续的
            w_width,w_height);
        if(!text){
            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Failed to create texture to SDL");
            goto __FAIL;
        }
        
        rect.x = 0;    //显示矩阵的左上点
        rect.y = 0;
        //------------------------开始读取数据,结合SDL与FFmpeg
        av_init_packet(&packet);    //初始化数据包
        while(av_read_frame(fmt_cxt,&packet)>=0){        
            if(packet.stream_index == vdstream_idx){    //属于视频流
                len = avcodec_decode_video2(vdcodec_cxt,frame,&frameFin,&packet);    //作用是解码一帧视频数据。输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame。
                if(len<0){
                    SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,"Fail to decode video frame");
                    goto __FAIL;
                }
    
                if(frameFin){    //表示读取完成了一帧,开始进行转换,解码数据已经放入了frame中
                    sws_scale(sws_cxt,(uint8_t const * const *)frame->data,frame->linesize,    //当前处理区域的每个通道数据指针,每个通道行字节数
                        0,vdcodec_cxt->height,    //参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。
                        pict->data,pict->linesize);     //定义输出图像信息(输出的每个通道数据指针,每个通道行字节数)
    
                    //可以渲染YUV数据
                    SDL_UpdateYUVTexture(text,NULL,
                        pict->data[0],pict->linesize[0],
                        pict->data[1],pict->linesize[1],
                        pict->data[2],pict->linesize[2]);
    
                    //更新矩阵的信息
                    rect.w = vdcodec_cxt->width;
                    rect.h = vdcodec_cxt->height;
    
                    SDL_RenderClear(rend);                   //清空渲染器缓冲区
    
                    SDL_RenderCopy(rend,text,NULL,&rect);    //拷贝纹理到显卡,选择渲染目标的一块矩形区域作为输出
                    SDL_RenderPresent(rend);                //将结果显示到窗口
                }
                av_packet_unref(&packet);    //注意:减少引用不会释放空间,因为本函数还在引用这个packet结构体;
                //所以我们在解码函数中剩余的其他帧数据,还是保留在packet中的,后面根据av_read_frame继续向内部添加数据
            }else if(packet.stream_index == adstream_idx){    //处理音频流
                packet_queue_put(&audioq,&packet);    //重点:对于音频数据包我们是暂存放在队列中,所以不能马上删除!!!!
            }else{
                av_packet_unref(&packet);    //注意:减少引用不会释放空间,因为本函数还在引用这个packet结构体;
                //所以我们在解码函数中剩余的其他帧数据,还是保留在packet中的,后面根据av_read_frame继续向内部添加数据
            }
    
    
            //设置事件轮询
            SDL_PollEvent(&event);
            switch(event.type){
                case SDL_QUIT:
                    goto __QUIT;
                    break;
                default:
                    break;
            }
    
        }
    
    __QUIT:
        ret = 0;
    __FAIL:
        av_packet_unref(&packet);    //减少引用,使得自己释放空间
        
        if(frame){
            av_frame_free(&frame);
        }
    
        if(text){
            SDL_DestroyTexture(text);
        }
    
        if(rend){
            SDL_DestroyRenderer(rend);
        }
    
        if(win){
            SDL_DestroyWindow(win);
        }
    
        if(pict){
            avpicture_free(pict);
            free(pict);
        }
    
        if(adcodec_cxt){
            avcodec_close(adcodec_cxt);
        }
    
        if(vdcodec_cxt){
            avcodec_close(vdcodec_cxt);
        }
    
        if(fmt_cxt){
            avformat_close_input(&fmt_cxt);
        }
        
        SDL_Quit();
        return ret;
    }
    View Code
    gcc playerV2.c -o play_2 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil -lswresample

    三:简单的播放器V3(线程实现播放视频和音频,未同步)

    (一)线程模型

    1.主线程:事件处理、视频渲染等简单事件,不做过多事情,使得响应更快。将其它复杂的逻辑放入子线程当中

    2.解复用线程:对文件数据进行解复用,将视频流放入视频流队列,音频流放入音频流队列当中,然后创建子线程进行处理(解码处理占CPU)

    3.视频解码线程:解码视频流队列中的数据,将结果放入解码后的视频队列中去

    4.SDL创建音频渲染线程,去音频流队列中取数据,进行解码,输出到声卡

    5.视频渲染,由主线程实现,通过从解码后的视频流队列中取得数据,进行渲染

    以上使得音视频的渲染都是通过回调的方式处理,使得更好的进行音视频同步处理!!!

    (二)基于多线程实现音频数据的音视频播放器 

    #include <assert.h>
    #include <stdio.h>
    #include <SDL.h>
    
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    
    #define SDL_AUDIO_BUFFER_SIZE 1024        //对于AAC,一帧数据中单通道获取 1024个 sample采样点
    #define    MAX_AUDIO_FRAME_SIZE 192000        //是指双通道下,采用48k采样率,位深为16位,采样时间为1s
    
    #define MAX_AUDIOQ_SIZE (5*6*1024)        //音频队列中数据字节数    界限,在MAX_AUDIO_FRAME_SIZE内部,即可。 采用最小8k采样率,2通道,16位深 = 32k,这里使用(5*6*1024)小于这个最小值即可
    #define MAX_VEDIOQ_SIZE (5*256*1024)    //视频队列中数据字节数    界限,需要处理1280x1024的图像,这里使用这个数值作为界限,实际上对于yuv420p数据,可以在×1.5,但是没必要的。
    
    #define FF_REFRESH_EVENT (SDL_USEREVENT)    //使用自定义事件
    #define FF_QUIT_EVENT (SDL_USEREVENT + 1)    //使用自定义事件
    
    #define VIDEO_PICTURE_QUEUE_SIZE 1        //定义视频图像裁剪后帧的最大个数
    
    typedef struct PacketQueue {
        AVPacketList* first_pkt,* last_pkt;
        int nb_packets;
        int size;
        SDL_mutex* mutex;
        SDL_cond* cond;
    }PacketQueue;
    
    typedef struct VideoPicture {
        AVPicture* pict;    //存放yuv数据
        int width,height;
        int allocated;        //用来标记图像是否分配了空间
    }VideoPicture;
    
    typedef struct VideoState {
        /*------------多媒体文件的基础信息-----------*/
        char                     filename[1024];                //文件名
        AVFormatContext*         pFormatCtx;                    //文件格式上下文
        int                        videoStream,audioStream;    //视频流、音频流索引
    
        /*----------------音频信息----------------*/
        AVStream                 *audio_st;                    //
        AVCodecContext             *audio_ctx;                    //解码上下文
        struct SwrContext*         audio_swr_cxt;                //音频重采样上下文
    
    
        uint8_t                    audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];    //重采样后的数据最后存放位置
        unsigned int             audio_buf_size;                //写入的数据大小
        unsigned int             audio_buf_index;            //读取到的数据的索引
    
        PacketQueue             audioq;                        //音频队列
    
        AVFrame                 audio_frame;                //音频帧(解码后的)--- 从队列中取出来的packet,进行解码出来的一帧数据
        AVPacket                 audio_pkt;                    //音频包(队列中读取的),这里是提前初始化了一个包,避免了后面每次获取数据时都设置一个,类似于全局变量了。或者我们像前一个版本那样使用静态局部变量也可以的
        uint8_t                    *audio_pkt_data;            //指向packet中的data数据要读取的位置
        int                        audio_pkt_size;                //读取的packet中的数据大小
    
        /*----------------视频信息----------------*/
        AVStream                *video_st;                    //
        AVCodecContext            *video_ctx;                    //图像解码上下文
        struct SwsContext        *sws_ctx;                    //图像裁剪上下文
    
        PacketQueue                videoq;                        //队列中保存着所有读取的关于视频的packet
    
        VideoPicture             pictq[VIDEO_PICTURE_QUEUE_SIZE];    //最后要存放的数据位置---队列中取出来的一个packet进行裁剪处理后的数据
        int                        pictq_size;                    //pictq_size表示pictq数组的大小,
        int                     pictq_rindex;                //索引pictq_rindex表示要访问的数据在pictq数组中的位置
        int                     pictq_windex;                //索引pictq_windex表示要插入的数据在pictq数组中的位置
    
        /*----------------线程信息----------------*/            //只有视频是我们要处理的数据,所以包含独立的锁,音频是被声卡主动读取的
        SDL_mutex                *pictq_mutex;                //视频数据的锁,因为数据可以同时读取和写入
        SDL_cond                *pictq_cond;                //条件变量
    
        SDL_Thread                *parse_tid;                    //解复用线程id
        SDL_Thread                *video_tid;                    //视频解码线程id
    
        /*--------------全局退出信号---------------*/
        int                        quit;                        //0正常,1退出
    }VideoState;
    
    
    /*-------------------SDL相关-----------------*/
    SDL_mutex                *texture_mutex;                    //是对渲染器的锁,因为主线程和视频解码线程都会用到
    
    SDL_Window                *win;                            //窗口指针
    SDL_Renderer            *renderer;                        //渲染器指针
    SDL_Texture                *texture;                        //纹理指针
    
    /*--------------只有一个解复用线程,只在这里线程中被修改!!!---------------*/
    VideoState                *global_video_state;            
    
    /*-----------------队列操作方法---------------*/
    void packet_queue_init(PacketQueue *q){
        memset(q,0,sizeof(PacketQueue));
        q->mutex = SDL_CreateMutex();
        q->cond = SDL_CreateCond();
    }
    
    int packet_queue_get(PacketQueue *q,AVPacket *pkt,int block){
        AVPacketList *pktl;
        int ret;
    
        SDL_LockMutex(q->mutex);
        for(;;){
            if(global_video_state->quit){    //对于每个死循环,我们都要进行退出信号判断
                fprintf(stderr,"quit from queue_get!
    ");
                ret = -1;
                break;
            }
    
            pktl = q->first_pkt;
            if(pktl){
                q->first_pkt = pktl->next;
                if(!q->first_pkt)
                    q->last_pkt = NULL;
                q->nb_packets--;
                q->size -= pktl->pkt.size;
                *pkt = pktl->pkt;
                av_free(pktl);
                ret = 1;
                break;
            }else if(!block){
                ret = 0;    //表示没有获取到数据,接着进入轮询
                break;
            }else{    //类似阻塞等待
                fprintf(stderr,"queue is empty, so wait a moment and wait a cond signal!
    ");
                SDL_CondWait(q->cond,q->mutex);
            }
        }
        SDL_UnlockMutex(q->mutex);
        return ret;
    }
    
    int packet_queue_put(PacketQueue *q,AVPacket *pkt){
        AVPacketList* pktl;
    
        if(av_dup_packet(pkt) < 0){
            return -1;
        }
    
        pktl = av_malloc(sizeof(AVPacketList));
        if(!pktl){
            return -1;
        }
    
        pktl->pkt = *pkt;    //data赋值引用即可
        pktl->next = NULL;
    
        SDL_LockMutex(q->mutex);
        if(!q->last_pkt)
            q->first_pkt = pktl;
        else
            q->last_pkt->next = pktl;
        q->last_pkt = pktl;
        q->nb_packets++;
        q->size += pktl->pkt.size;
    
        SDL_CondSignal(q->cond);    //发送信号给get消费者
    
        SDL_UnlockMutex(q->mutex);
        return 0;
    }
    
    /*-----------------音频操作方法---------------*/
    int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size){
        int len1, data_size = 0;
        AVPacket *pkt = &is->audio_pkt;
    
        for(;;){
            while(is->audio_pkt_size > 0){    //还有数据在packet中
                int got_frame = 0;
                len1 = avcodec_decode_audio4(is->audio_ctx,&is->audio_frame,&got_frame,pkt);    //读取pkt中的数据到frame
                if(len1 < 0){
                    printf("Failed to decode audio...
    ");
                    is->audio_pkt_size = 0;
                    break;
                }
    
                data_size = 0;
                if(got_frame){    //解码帧成功
                    data_size = 2*2*is->audio_frame.nb_samples;    //双通道,16位深
                    assert(data_size <= buf_size);
    
                    //开始进行重采样
                    swr_convert(is->audio_swr_cxt,
                        &audio_buf,    //输出空间
                        MAX_AUDIO_FRAME_SIZE*3/2,    //空间大小
                        (const uint8_t **)is->audio_frame.data,    //帧数据位置
                        is->audio_frame.nb_samples);    //帧通道采样率
    
                    //处理数据
                    is->audio_pkt_size -= len1;
                    is->audio_pkt_data += len1;
                }
                if(data_size == 0)    //没有读取到数据
                    continue;
                return data_size;    //读取到了数据
            }
    
            //如果没有进入上面的while循环,则表示开始没有数据存在pkt中,我们需要去队列中获取
            if(pkt->data)
                av_free_packet(pkt);
    
            if(is->quit){    //对于循环,一定退出判断
                printf("Will quit program...
    ");
                return -1;
            }
    
            if(packet_queue_get(&is->audioq,pkt,0) <= 0){
                return -1;
            }
    
            is->audio_pkt_data = pkt->data;
            is->audio_pkt_size = pkt->size;
        }
    }
    
    //声卡回调读取数据
    void audio_callback(void *userdata,uint8_t *stream,int len){
        VideoState *is = (VideoState *)userdata;
        int len1,audio_size;
    
        SDL_memset(stream,0,len);    //初始化缓冲区
    
        while(len > 0){    //这个循环不用担心,因为会产生静默声音,最后跳出while
            if(is->audio_buf_index >= is->audio_buf_size){    //原有的数据已经读完了,下面开始获取新的数据
                audio_size = audio_decode_frame(is,is->audio_buf,sizeof(is->audio_buf));
                if(audio_size < 0){    //读取出错 设置静默声音,当其中audio_decode_frame去队列取数据block为0时,会进入
                    is->audio_buf_size = 1024*2*2;
                    printf("--------------no audio packet-------------------
    ");
                    memset(is->audio_buf,0,is->audio_buf_size);
                }else{
                    is->audio_buf_size = audio_size;
                }
                is->audio_buf_index = 0;    //新读取的数据的起始索引位置
            }
    
            len1 = is->audio_buf_size - is->audio_buf_index;    //数据有效大小
            if(len1 > len)
                len1 = len;
            //memcpy(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1);    //使用这个可以代替SDL_memset和
            SDL_MixAudio(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1,SDL_MIX_MAXVOLUME);
            len -= len1;
            stream += len1;
            is->audio_buf_index += len1;
        }
    }
    
    /*-----------------视频操作方法---------------*/
    //设置定时器
    static uint32_t sdl_refresh_timer_cb(uint32_t interval, void *dat){
        SDL_Event event;
    
        event.type = FF_REFRESH_EVENT;
        event.user.data1 = dat;
        SDL_PushEvent(&event);
        return 0;    //0意味着停止计时
    }
    
    //定时刷新
    static void schedule_refresh(VideoState *is,int delay){
        SDL_AddTimer(delay,sdl_refresh_timer_cb,is);
    }
    
    //显示图像
    void video_display(VideoState *is){
        SDL_Rect rect;
        VideoPicture *vp;
    
        vp = &is->pictq[is->pictq_rindex];    //获取要显示的数据
        if(vp->pict){    //有数据,开始显示
            SDL_UpdateYUVTexture(texture,NULL,
                vp->pict->data[0],vp->pict->linesize[0],
                vp->pict->data[1],vp->pict->linesize[1],
                vp->pict->data[2],vp->pict->linesize[2]);
    
            rect.x = 0;
            rect.y = 0;
            rect.w = is->video_ctx->width;
            rect.h = is->video_ctx->height;
    
            SDL_LockMutex(texture_mutex);    //分配数据和渲染数据加锁处理
            SDL_RenderClear(renderer);
            SDL_RenderCopy(renderer,texture,NULL,&rect);
            SDL_RenderPresent(renderer);
            SDL_UnlockMutex(texture_mutex);
        }
    }
    
    //开始根据事件处理显示图像
    void video_refresh_timer(void *userdata){
        VideoState *is = (VideoState*)userdata;
        VideoPicture *vp;
    
        if(is->video_st){    //如果视频流存在,则进行处理
            if(is->pictq_size == 0){    //队列中没有数据,则反复去轮询是否有数据
                schedule_refresh(is,1);    //轮询时间为1
            }else{
                vp = &is->pictq[is->pictq_rindex];    //获取要显示的数据
    
                schedule_refresh(is,40);    //显示图像之间间隔40ms
                video_display(is);            //显示图像
                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);    //通知queue_picture去分配空间
                SDL_UnlockMutex(is->pictq_mutex);
            }
        }else{
            schedule_refresh(is,100);    //如果不存在视频流,就再延长等待一段时间
        }
    }
    
    //为pictq数组中新插入的数据分配空间
    void alloc_picture(void *userdata){
        VideoState *is = (VideoState *)userdata;
        VideoPicture *vp = &is->pictq[is->pictq_windex];
    
        if(vp->pict){
            avpicture_free(vp->pict);    //释放yuv空间
            free(vp->pict);    //释放pict
        }
    
        SDL_LockMutex(texture_mutex);    //因为分配和渲染都要使用到这个数据,所以要进行加锁
        vp->pict = (AVPicture*)malloc(sizeof(AVPicture));    //分配空间
        if(vp->pict){
            avpicture_alloc(vp->pict,
                AV_PIX_FMT_YUV420P,
                is->video_ctx->width,
                is->video_ctx->height);
        }
        SDL_UnlockMutex(texture_mutex);
    
        vp->width = is->video_ctx->width;
        vp->height = is->video_ctx->height;
        vp->allocated = 1;
    }
    
    //分配空间,将视频帧放入pictq数组中去
    int queue_picture(VideoState *is,AVFrame *pFrame){
        VideoPicture *vp;
        int dst_pix_fmt;
        AVPicture pict;    //yuv数据
    
        SDL_LockMutex(is->pictq_mutex);        //------------??应该是if
        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){
            printf("quit from queue picture...
    ");
            return -1;
        }
    
        vp = &is->pictq[is->pictq_windex];    //这个是要插入的位置数据
        if(!vp->pict || 
            vp->width != is->video_ctx->width ||
            vp->height != is->video_ctx->height){    //开始分配空间
            vp->allocated = 0;        //还没有开始分配空间
            alloc_picture(is);        //开始分配空间,内部会修改标识符
            if(is->quit){
                printf("quit from queue picture 2...
    ");
                return -1;
            }
        }
    
        if(vp->pict){    //分配成功,开始进行裁剪处理,处理后的数据放入这里面
            sws_scale(is->sws_ctx,(uint8_t const* const*)pFrame->data,
                pFrame->linesize,0,is->video_ctx->height,
                vp->pict->data,vp->pict->linesize);
    
            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;
    }
    
    //解码视频帧
    int video_thread(void *arg){
        VideoState *is = (VideoState *)arg;
        AVPacket packet,*pkt = &packet;
        int frameFinished;
        AVFrame *pFrame;
    
        pFrame = av_frame_alloc();
    
        for(;;){
            if(packet_queue_get(&is->videoq,pkt,1) < 0){
                break;    //没有数据,退出
            }
    
            avcodec_decode_video2(is->video_ctx,pFrame,&frameFinished,pkt);    //解码数据帧
            if(frameFinished){    //去裁剪处理
                if(queue_picture(is,pFrame) < 0){
                    break;
                }
            }
    
            av_free_packet(pkt);
        }
        av_frame_free(&pFrame);
        return 0;
    }
    
    /*-----------------解复用线程方法,主要用于初始化---------------*/
    
    //初始化音视频流的相关数据,(只被音频、视频流各自调用一次)
    int stream_component_open(VideoState *is, int stream_index){
        int64_t in_channel_layout,out_channel_layout;
        AVFormatContext *pFormatCtx = is->pFormatCtx;
        AVCodecContext *codecCtx = NULL;
        AVCodec *codec = NULL;
        SDL_AudioSpec new_spec,old_spec;
    
        codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);    //获取解码器信息
        if(!codec){
            printf("UnSupport codec!
    ");
            return -1;
        }
    
        codecCtx = avcodec_alloc_context3(codec);
        if(!codecCtx){
            printf("Failed to alloc codec context
    ");
            return -1;
        }
    
        if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){
            printf("Failed to copy codec context
    ");
            return -1;
        }
    
        if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO){    //设置音频信息
            new_spec.freq = codecCtx->sample_rate;
            new_spec.format = AUDIO_S16SYS;
            new_spec.channels = codecCtx->channels;
            new_spec.silence = 0;
            new_spec.samples = SDL_AUDIO_BUFFER_SIZE;    //AAC单通道采样率就是1024
            new_spec.callback = audio_callback;
            new_spec.userdata = is;
    
            if(SDL_OpenAudio(&new_spec,&old_spec)<0){
                printf("SDL_OpenAudio: %s
    ", SDL_GetError());
                return -1;
            }
        }
    
        if(avcodec_open2(codecCtx,codec,NULL) < 0){
            printf("Failed to open codec
    ");
            return -1;
        }
    
        switch(codecCtx->codec_type){
            case AVMEDIA_TYPE_AUDIO:    //处理音频
                is->audioStream = stream_index;
                is->audio_st = pFormatCtx->streams[stream_index];
                is->audio_ctx = codecCtx;
                is->audio_buf_size = 0;
                is->audio_buf_index = 0;
    
                memset(&is->audio_pkt,0,sizeof(is->audio_pkt));    //取代了前一个版本的静态局部变量而已,这里不使用memset也可以
                packet_queue_init(&is->audioq);
    
                SDL_PauseAudio(0);
    
                in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels);
                out_channel_layout = in_channel_layout;
    
                is->audio_swr_cxt = swr_alloc();    //开始分配重采样的数据空间
                swr_alloc_set_opts(is->audio_swr_cxt,
                    out_channel_layout,
                    AV_SAMPLE_FMT_S16,
                    is->audio_ctx->sample_rate,
                    in_channel_layout,
                    is->audio_ctx->sample_fmt,
                    is->audio_ctx->sample_rate,
                    0,
                    NULL);
    
                swr_init(is->audio_swr_cxt);
                break;
            case AVMEDIA_TYPE_VIDEO:    //处理视频
                is->videoStream = stream_index;
                is->video_st = pFormatCtx->streams[stream_index];
                is->video_ctx = codecCtx;
                packet_queue_init(&is->videoq);
                is->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(video_thread,"video_thread",is);    //开始视频解码线程
                break;
            default:
                break;
        }
        return 0;
    }
    
    //解复用,会在后面一直读取视频数据包
    int decode_thread(void *arg){
        VideoState *is = (VideoState *)arg;
        AVFormatContext *pFormatCtx = NULL;
        AVPacket packet, *ptk = &packet;
        SDL_Event event;
    
        int i,ret=0;
        int video_index = -1;
        int audio_index = -1;
    
        //-------初始化操作
        is->videoStream = -1;
        is->audioStream = -1;
    
        global_video_state = is;
    
        //ffmpeg操作,获取文件信息
        if(avformat_open_input(&pFormatCtx,is->filename,NULL,NULL)<0){
            printf("Failed to open file[%s] context
    ", is->filename);
            return -1;
        }
    
        is->pFormatCtx = pFormatCtx;
    
        if(avformat_find_stream_info(pFormatCtx,NULL)<0){
            printf("Failed to get detail stream infomation
    ");
            return -1;
        }
    
        for(i=0;i<pFormatCtx->nb_streams;i++){
            if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0)
                video_index = i;
            if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0)
                audio_index = i;
        }
    
        if(video_index < 0 || audio_index < 0){
            printf("Failed to find stream index for audio/video
    ");
            return -1;
        }
    
        av_dump_format(pFormatCtx,audio_index,is->filename,0);    //打印信息
        av_dump_format(pFormatCtx,video_index,is->filename,0);    //打印信息
    
        //初始化音频流信息
        ret = stream_component_open(is,audio_index);
        if(ret < 0){
            printf("Failed to initialize audio data
    ");
            goto fail;
        }
        //初始化视频流信息
        ret = stream_component_open(is,video_index);
        if(ret < 0){
            printf("Failed to initialize video data
    ");
            goto fail;
        }
    
        //初始化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
            );
    
        renderer = SDL_CreateRenderer(win,-1,0);
    
        texture = SDL_CreateTexture(renderer,
            SDL_PIXELFORMAT_IYUV,
            SDL_TEXTUREACCESS_STREAMING,
            is->video_ctx->width,
            is->video_ctx->height);
    
        //开始获取包
        for(;;){
            if(is->quit){    //对于死循环,需要一直去判断退出信号
                SDL_CondSignal(is->videoq.cond);    //既然要退出了,就要让所有在循环中的程序退出,发送所有可能阻塞的信号量,防止因为缺少信号量而导致的阻塞
                SDL_CondSignal(is->audioq.cond);
                break;
            }
    
            if(is->audioq.size > MAX_AUDIOQ_SIZE ||
                is->videoq.size > MAX_VEDIOQ_SIZE){    //对于数据量大于界限值的时候,我们直接去获取这些值,不去获取下一个packet
                SDL_Delay(10);    //等待10ms
                continue;
            }
    
            if(av_read_frame(is->pFormatCtx,ptk) < 0){
                //context对象中有一个pb,这是内部用到的IO上下文,通过pb->error,就能获取到IO上发生的错误。
                if(is->pFormatCtx->pb->error == 0){
                    SDL_Delay(100);    //IO错误,等待一段时间用户输入
                    continue;
                }else{    //读取完毕,退出
                    break;
                }
            }
    
            //开始处理读取的数据包
            if(ptk->stream_index == is->videoStream){    //视频包
                packet_queue_put(&is->videoq,ptk);        //当我们读取的视频也是25fps的话,播放声音不会卡顿,当是30fps,音频会出现卡顿,因为队列中数据可能不存在
                printf("put video queue, size:%d
    ", is->videoq.nb_packets);
            }else if(ptk->stream_index == is->audioStream){    //音频包
                packet_queue_put(&is->audioq,ptk);
                printf("put audio queue, size:%d
    ", is->audioq.nb_packets);
            }else{
                av_free_packet(ptk);
            }
        }
    
        while(!is->quit){    //出现数据包读取完毕,放入了队列中,但是还没有从队列中读取完成去解码播放,所以我们需要等待一段时间
            SDL_Delay(100);    //播放完毕以后,等待用户主动关闭
        }
    
    fail:
        event.type = FF_QUIT_EVENT;
        event.user.data1 = is;
        SDL_PushEvent(&event);
    
        return 0;
    }
    
    /*-----------------主(函数)线程方法---------------*/
    int main(int argc,char *argv[]){
        int ret = -1;
    
        SDL_Event event;    //主函数线程主要处理事件和视频渲染问题
        VideoState *is;
    
        if(argc<2){
            fprintf(stderr,"Usage: test <file>
    ");
            return ret;
        }
    
        //------进行初始化操作(简单的初始化,初始化锁、条件变量等信息),开启解复用线程
        is = av_malloc(sizeof(VideoState));
    
        av_register_all();
        if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
            printf("Failed to initialize SDL - %s
    ", SDL_GetError());
            return ret;
        }
        texture_mutex = SDL_CreateMutex();
    
        av_strlcpy(is->filename,argv[1],sizeof(is->filename));
    
        is->pictq_mutex = SDL_CreateMutex();
        is->pictq_cond = SDL_CreateCond();
    
        schedule_refresh(is,40);    //设置40ms发送刷新信号,这个间隔内去实现解复用,获取数据。如果这个时间不够,后面获取数据为空,则还会再延迟的
    
        //开启解复用线程,内部也实现了很多初始化操作
        is->parse_tid = SDL_CreateThread(decode_thread,"decode_thread",is);
        if(!is->parse_tid){
            av_free(is);    //解复用线程开启失败
            goto __FAIL;
        }
    
        for(;;){
            SDL_WaitEvent(&event);    //阻塞式等待信号
            switch(event.type){
                case FF_QUIT_EVENT:    //出错导致出现这个事件
                case SDL_QUIT:        //用户点击了关闭
                    printf("receive a QUIT event:%d
    ", event.type);
                    is->quit = 1;    //其他线程推出
                    goto __QUIT;
                    break;
                case FF_REFRESH_EVENT:
                    video_refresh_timer(event.user.data1);    //传递的数据就是VideoState
                    break;
                default:
                    break;
            }
        }
    
    __QUIT:
        ret = 0;
    
    __FAIL:
        SDL_Quit();
        return ret;
    }
    View Code
    gcc playerV3.c -o play_3 -I /usr/local/include/ -I /usr/local/include/SDL2/ -L /usr/local/lib/ -lSDL2 -lavformat -lavcodec -lswscale -lavutil -lswresample

    因为音频的频率是25fps,视频这里是30fps;所以是不同步的。而我们代码中实现的是40ms每张图片(25fps显示),会导致视频队列中的数据包个数不断累加!!!

    四:简单的播放器V4(线程实现播放视频和音频,实现音视频同步)

    (一)时间同步

    回顾:FFmpeg学习(四)视频基础

    补充1:PTS的获取

    一般使用解码后的AVFrame的pts,但是pts出错的话,可以通过函数获取进行推算!!!

    补充2:如何获取下一帧的PTS

    video_clock = 上一帧的video_clock + frame_delay帧间隔时间

    音视频同步就是这两种clock相互追赶实现的!!

    补充3:音视频同步方式

    1.视频同步到音频:视频展示的时候进行延迟即可
    2.音频同步到视频:丢帧或者添加静默声
    3.都同步到系统时钟

    补充4:视频播放的基本思路

    (二)基于时间同步的音视频播放器 

    以audio为参考时钟,video同步到音频的示例代码:

    1.获取当前要显示的video PTS,减去上一帧视频PTS,则得出上一帧视频应该显示的时长delay;
    2.当前video PTS与参考时钟当前audio PTS比较,得出音视频差距diff;
    3.获取同步阈值sync_threshold,为一帧视频差距,范围为10ms-100ms;
    4.diff小于sync_threshold,则认为不需要同步;否则delay+diff值,则是正确纠正delay;
    5.如果超过sync_threshold,且视频落后于音频,那么需要减小delay(FFMAX(0, delay + diff)),让当前帧尽快显示。
    6.如果视频落后超过1秒,且之前10次都快速输出视频帧,那么需要反馈给音频源减慢,同时反馈视频源进行丢帧处理,让视频尽快追上音频。因为这很可能是视频解码跟不上了,再怎么调整delay也没用。
    7.如果超过sync_threshold,且视频快于音频,那么需要加大delay,让当前帧延迟显示。
    8.将delay*2慢慢调整差距,这是为了平缓调整差距,因为直接delay+diff,会让画面画面迟滞。
    9.如果视频前一帧本身显示时间很长,那么直接delay+diff一步调整到位,因为这种情况再慢慢调整也没太大意义。
    10.考虑到渲染的耗时,还需进行调整。frame_timer为一帧显示的系统时间,frame_timer+delay- curr_time,则得出正在需要延迟显示当前帧的时间。
    #include <assert.h>
    #include <stdio.h>
    #include <math.h>
    #include <SDL.h>
    
    #include <libavutil/log.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    
    #define SDL_AUDIO_BUFFER_SIZE 1024        //对于AAC,一帧数据中单通道获取 1024个 sample采样点
    #define    MAX_AUDIO_FRAME_SIZE 192000        //是指双通道下,采用48k采样率,位深为16位,采样时间为1s
    
    #define AV_SYNC_THRESHOLD 0.01            //10ms --- 同步阈值
    #define AV_NOSYNC_THRESHOLD 10.0        //10s  --- 非同步阈值
    
    #define MAX_AUDIOQ_SIZE (5*6*1024)        //音频队列中数据字节数    界限,在MAX_AUDIO_FRAME_SIZE内部,即可。 采用最小8k采样率,2通道,16位深 = 32k,这里使用(5*6*1024)小于这个最小值即可
    #define MAX_VEDIOQ_SIZE (5*256*1024)    //视频队列中数据字节数    界限,需要处理1280x1024的图像,这里使用这个数值作为界限,实际上对于yuv420p数据,可以在×1.5,但是没必要的。
    
    #define FF_REFRESH_EVENT (SDL_USEREVENT)    //使用自定义事件
    #define FF_QUIT_EVENT (SDL_USEREVENT + 1)    //使用自定义事件
    
    #define VIDEO_PICTURE_QUEUE_SIZE 1        //定义视频图像裁剪后帧的最大个数
    
    typedef struct PacketQueue {
        AVPacketList* first_pkt,* last_pkt;
        int nb_packets;
        int size;
        SDL_mutex* mutex;
        SDL_cond* cond;
    }PacketQueue;
    
    typedef struct VideoPicture {
        AVPicture* pict;    //存放yuv数据
        int width,height;
        int allocated;        //用来标记图像是否分配了空间
        double pts;
    }VideoPicture;
    
    typedef struct VideoState {
        /*------------多媒体文件的基础信息-----------*/
        char                     filename[1024];                //文件名
        AVFormatContext*         pFormatCtx;                    //文件格式上下文
        int                        videoStream,audioStream;    //视频流、音频流索引
    
        /*----------------音频信息----------------*/
        AVStream                 *audio_st;                    //
        AVCodecContext             *audio_ctx;                    //解码上下文
        struct SwrContext*         audio_swr_cxt;                //音频重采样上下文
    
    
        uint8_t                    audio_buf[(MAX_AUDIO_FRAME_SIZE*3)/2];    //重采样后的数据最后存放位置
        unsigned int             audio_buf_size;                //写入的数据大小
        unsigned int             audio_buf_index;            //读取到的数据的索引
    
        PacketQueue             audioq;                        //音频队列
    
        AVFrame                 audio_frame;                //音频帧(解码后的)--- 从队列中取出来的packet,进行解码出来的一帧数据
        AVPacket                 audio_pkt;                    //音频包(队列中读取的),这里是提前初始化了一个包,避免了后面每次获取数据时都设置一个,类似于全局变量了。或者我们像前一个版本那样使用静态局部变量也可以的
        uint8_t                    *audio_pkt_data;            //指向packet中的data数据要读取的位置
        int                        audio_pkt_size;                //读取的packet中的数据大小
    
        /*----------------视频信息----------------*/
        AVStream                *video_st;                    //
        AVCodecContext            *video_ctx;                    //图像解码上下文
        struct SwsContext        *sws_ctx;                    //图像裁剪上下文
    
        PacketQueue                videoq;                        //队列中保存着所有读取的关于视频的packet
    
        VideoPicture             pictq[VIDEO_PICTURE_QUEUE_SIZE];    //最后要存放的数据位置---队列中取出来的一个packet进行裁剪处理后的数据
        int                        pictq_size;                    //pictq_size表示pictq数组的大小,
        int                     pictq_rindex;                //索引pictq_rindex表示要访问的数据在pictq数组中的位置
        int                     pictq_windex;                //索引pictq_windex表示要插入的数据在pictq数组中的位置
    
        /*----------------线程信息----------------*/            //只有视频是我们要处理的数据,所以包含独立的锁,音频是被声卡主动读取的
        SDL_mutex                *pictq_mutex;                //视频数据的锁,因为数据可以同时读取和写入
        SDL_cond                *pictq_cond;                //条件变量
    
        SDL_Thread                *parse_tid;                    //解复用线程id
        SDL_Thread                *video_tid;                    //视频解码线程id
    
        /*--------------音视频同步信息-------------*/
        double                    audio_clock;                //音频(参考)时钟当前audio PTS,时间戳
        double                    video_clock;                //视频当前要显示的video PTS
    
        double                    frame_timer;                //记录下次回调的时间(是av_gettime获取的系统时间,不是时间间隔)
        double                    frame_last_pts;                //上一次播放视频帧的pts
        double                    frame_last_delay;            //上一次播放视频帧的delay(也可以认为是上一个图片显示的时间)
    
    
        /*--------------全局退出信号---------------*/
        int                        quit;                        //0正常,1退出
    }VideoState;
    
    
    /*-------------------SDL相关-----------------*/
    SDL_mutex                *texture_mutex;                    //是对渲染器的锁,因为主线程和视频解码线程都会用到
    
    SDL_Window                *win;                            //窗口指针
    SDL_Renderer            *renderer;                        //渲染器指针
    SDL_Texture                *texture;                        //纹理指针
    
    /*--------------只有一个解复用线程,只在这里线程中被修改!!!---------------*/
    VideoState                *global_video_state;            
    
    /*-----------------队列操作方法---------------*/
    void packet_queue_init(PacketQueue *q){
        memset(q,0,sizeof(PacketQueue));
        q->mutex = SDL_CreateMutex();
        q->cond = SDL_CreateCond();
    }
    
    int packet_queue_get(PacketQueue *q,AVPacket *pkt,int block){
        AVPacketList *pktl;
        int ret;
    
        SDL_LockMutex(q->mutex);
        for(;;){
            if(global_video_state->quit){    //对于每个死循环,我们都要进行退出信号判断
                fprintf(stderr,"quit from queue_get!
    ");
                ret = -1;
                break;
            }
    
            pktl = q->first_pkt;
            if(pktl){
                q->first_pkt = pktl->next;
                if(!q->first_pkt)
                    q->last_pkt = NULL;
                q->nb_packets--;
                q->size -= pktl->pkt.size;
                *pkt = pktl->pkt;
                av_free(pktl);
                ret = 1;
                break;
            }else if(!block){
                ret = 0;    //表示没有获取到数据,接着进入轮询
                break;
            }else{    //类似阻塞等待
                fprintf(stderr,"queue is empty, so wait a moment and wait a cond signal!
    ");
                SDL_CondWait(q->cond,q->mutex);
            }
        }
        SDL_UnlockMutex(q->mutex);
        return ret;
    }
    
    int packet_queue_put(PacketQueue *q,AVPacket *pkt){
        AVPacketList* pktl;
    
        if(av_dup_packet(pkt) < 0){
            return -1;
        }
    
        pktl = av_malloc(sizeof(AVPacketList));
        if(!pktl){
            return -1;
        }
    
        pktl->pkt = *pkt;    //data赋值引用即可
        pktl->next = NULL;
    
        SDL_LockMutex(q->mutex);
        if(!q->last_pkt)
            q->first_pkt = pktl;
        else
            q->last_pkt->next = pktl;
        q->last_pkt = pktl;
        q->nb_packets++;
        q->size += pktl->pkt.size;
    
        SDL_CondSignal(q->cond);    //发送信号给get消费者
    
        SDL_UnlockMutex(q->mutex);
        return 0;
    }
    
    /*-----------------音频操作方法---------------*/
    //获取音频的时间戳---获取方法和函数audio_decode_frame中的pts获取相对
    double get_audio_clock(VideoState *is){
        double pts;
        int hw_buf_size,bytes_per_sec,n;
    
        pts = is->audio_clock;        //获取预测的下一个音频包开始播放时间(或者当前音频包的结束时间)
        hw_buf_size = is->audio_buf_size - is->audio_buf_index;    //这个就是当前播放的音频大小
        bytes_per_sec = 0;
        n = is->audio_ctx->channels*2;    //通道数×2字节(16位深)
        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;
    }
    
    
    //音频解码
    int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr){
        int len1, data_size = 0,n;
        AVPacket *pkt = &is->audio_pkt;
        double pts;
    
        for(;;){
            while(is->audio_pkt_size > 0){    //还有数据在packet中
                int got_frame = 0;
                len1 = avcodec_decode_audio4(is->audio_ctx,&is->audio_frame,&got_frame,pkt);    //读取pkt中的数据到frame
                if(len1 < 0){
                    printf("Failed to decode audio...
    ");
                    is->audio_pkt_size = 0;
                    break;
                }
    
                data_size = 0;
                if(got_frame){    //解码帧成功
                    data_size = 2*2*is->audio_frame.nb_samples;    //双通道,16位深
                    assert(data_size <= buf_size);
    
                    //开始进行重采样
                    swr_convert(is->audio_swr_cxt,
                        &audio_buf,    //输出空间
                        MAX_AUDIO_FRAME_SIZE*3/2,    //空间大小
                        (const uint8_t **)is->audio_frame.data,    //帧数据位置
                        is->audio_frame.nb_samples);    //帧通道采样率
    
                    //处理数据
                    is->audio_pkt_size -= len1;
                    is->audio_pkt_data += len1;
                }
                if(data_size == 0)    //没有读取到数据
                    continue;
                //---------------------------------------重点:更新音频包的时间戳!!------------------------------------
                pts = is->audio_clock;    //获取时间戳
                *pts_ptr = pts;            //赋值
                n = 2*is->audio_ctx->channels;    //通道×2字节(重采样后的16位深) 
    
                //(n*is->audio_ctx->sample_rate)是1s内采样的数据量,所以
                //(double)data_size / (double)(n*is->audio_ctx->sample_rate)就是这些数据处理(播放)会花的时间
                is->audio_clock += (double)data_size / (double)(n*is->audio_ctx->sample_rate);    //就相当于下一次的音频pts,因为部分packet不会包含pts,这样推断更好
                //这个is->audio_clock是预测的下一次时间(或者本次音频包播放完毕的时间)
    
                return data_size;    //读取到了数据
            }
    
            //如果没有进入上面的while循环,则表示开始没有数据存在pkt中,我们需要去队列中获取
            if(pkt->data)
                av_free_packet(pkt);
    
            if(is->quit){    //对于循环,一定退出判断
                printf("Will quit program...
    ");
                return -1;
            }
    
            if(packet_queue_get(&is->audioq,pkt,0) <= 0){
                return -1;
            }
    
            is->audio_pkt_data = pkt->data;
            is->audio_pkt_size = pkt->size;
    
            //---------------------------------------重点:获取音频包的时间戳!!------------------------------------
            if(pkt->pts != AV_NOPTS_VALUE){    //如果这个数据包有PTS值,就开始更新
                is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;    //获取时间戳!! timestamp(秒) = pts * av_q2d(time_base)
            }
        }
    }
    
    //声卡回调读取数据
    void audio_callback(void *userdata,uint8_t *stream,int len){
        VideoState *is = (VideoState *)userdata;
        int len1,audio_size;
        double pts;            //返回的当前的音频帧的pts!!!
    
        SDL_memset(stream,0,len);    //初始化缓冲区
    
        while(len > 0){    //这个循环不用担心,因为会产生静默声音,最后跳出while
            if(is->audio_buf_index >= is->audio_buf_size){    //原有的数据已经读完了,下面开始获取新的数据
                audio_size = audio_decode_frame(is,is->audio_buf,sizeof(is->audio_buf),&pts);
                if(audio_size < 0){    //读取出错 设置静默声音,当其中audio_decode_frame去队列取数据block为0时,会进入
                    is->audio_buf_size = 1024*2*2;
                    printf("--------------no audio packet-------------------
    ");
                    memset(is->audio_buf,0,is->audio_buf_size);
                }else{
                    is->audio_buf_size = audio_size;
                }
                is->audio_buf_index = 0;    //新读取的数据的起始索引位置
            }
    
            len1 = is->audio_buf_size - is->audio_buf_index;    //数据有效大小
            if(len1 > len)
                len1 = len;
            //memcpy(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1);    //使用这个可以代替SDL_memset和
            SDL_MixAudio(stream,(uint8_t*)is->audio_buf+is->audio_buf_index,len1,SDL_MIX_MAXVOLUME);
            len -= len1;
            stream += len1;
            is->audio_buf_index += len1;
        }
    }
    
    /*-----------------视频操作方法---------------*/
    //设置定时器
    static uint32_t sdl_refresh_timer_cb(uint32_t interval, void *dat){
        SDL_Event event;
    
        event.type = FF_REFRESH_EVENT;
        event.user.data1 = dat;
        SDL_PushEvent(&event);
        return 0;    //0意味着停止计时
    }
    
    //定时刷新
    static void schedule_refresh(VideoState *is,int delay){
        SDL_AddTimer(delay,sdl_refresh_timer_cb,is);
    }
    
    //显示图像
    void video_display(VideoState *is){
        SDL_Rect rect;
        VideoPicture *vp;
    
        vp = &is->pictq[is->pictq_rindex];    //获取要显示的数据
        if(vp->pict){    //有数据,开始显示
            SDL_UpdateYUVTexture(texture,NULL,
                vp->pict->data[0],vp->pict->linesize[0],
                vp->pict->data[1],vp->pict->linesize[1],
                vp->pict->data[2],vp->pict->linesize[2]);
    
            rect.x = 0;
            rect.y = 0;
            rect.w = is->video_ctx->width;
            rect.h = is->video_ctx->height;
    
            SDL_LockMutex(texture_mutex);    //分配数据和渲染数据加锁处理
            SDL_RenderClear(renderer);
            SDL_RenderCopy(renderer,texture,NULL,&rect);
            SDL_RenderPresent(renderer);
            SDL_UnlockMutex(texture_mutex);
        }
    }
    
    //开始根据事件处理显示图像
    void video_refresh_timer(void *userdata){
        VideoState *is = (VideoState*)userdata;
        VideoPicture *vp;
        /*
        actual_delay:    实际时延间隔
        delay:             上一次的时间
        sync_threshold  同步阈值
        ref_clock:        参考时间(音频的时间)
        diff:            差值=当前视频帧pts - 音频的参考pts
        */
        double actual_delay,delay,sync_threshold,ref_clock,diff;
    
        if(is->video_st){                                //如果视频流存在,则进行处理
            if(is->pictq_size == 0){                    //队列中没有数据,则反复去轮询是否有数据
                schedule_refresh(is,1);                    //轮询时间为1
            }else{
                vp = &is->pictq[is->pictq_rindex];        //获取要显示的数据
    
                delay = vp->pts - is->frame_last_pts;    //当前视频帧的pts - 上一帧的pts
                if(delay <= 0 || delay >= 1.0){            //不合理时间 0或者1s
                    delay = is->frame_last_delay;        //上一次的时延(展示时间)
                }
    
                is->frame_last_delay = delay;            //更新本次时延
                is->frame_last_pts = vp->pts;            //上一帧的pts = 当前的pts
    
                ref_clock = get_audio_clock(is);        //获取参考帧的数据
                diff = vp->pts - ref_clock;
    
                sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;    //delay时间是否超过阈值,进行更新(取大)
                if(fabs(diff) < AV_NOSYNC_THRESHOLD){    //非同步阈值10s
                    if(diff <= -sync_threshold){        //视频比音频慢(diff为负号),并且超过允许的时延阈值(<=-sync_threshold),加快----
                        delay = 0;    //快点播放
                    }else if(diff >= sync_threshold){    //视频比音频快(diff为正号),并且超过允许的时延阈值
                        delay *= 2;                        //视频延迟展示
                    }
                }
    
                is->frame_timer += delay;                //下次回调时间(时间戳)
                actual_delay = is->frame_timer - (av_gettime()/1000000.0);    //去计算时延间隔(delay)---秒
    
                if(actual_delay < AV_SYNC_THRESHOLD){    //在允许的阈值范围内
                    actual_delay = AV_SYNC_THRESHOLD;    //至少10ms刷新一次
                }
    
                schedule_refresh(is,(int)(actual_delay*1000+0.5));    //下一次帧渲染时间,将上面获取的s转换为ms,+0.5的差值
    
                video_display(is);            //显示图像
                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);    //通知queue_picture去分配空间
                SDL_UnlockMutex(is->pictq_mutex);
            }
        }else{
            schedule_refresh(is,100);    //如果不存在视频流,就再延长等待一段时间
        }
    }
    
    //为pictq数组中新插入的数据分配空间
    void alloc_picture(void *userdata){
        VideoState *is = (VideoState *)userdata;
        VideoPicture *vp = &is->pictq[is->pictq_windex];
    
        if(vp->pict){
            avpicture_free(vp->pict);    //释放yuv空间
            free(vp->pict);    //释放pict
        }
    
        SDL_LockMutex(texture_mutex);    //因为分配和渲染都要使用到这个数据,所以要进行加锁
        vp->pict = (AVPicture*)malloc(sizeof(AVPicture));    //分配空间
        if(vp->pict){
            avpicture_alloc(vp->pict,
                AV_PIX_FMT_YUV420P,
                is->video_ctx->width,
                is->video_ctx->height);
        }
        SDL_UnlockMutex(texture_mutex);
    
        vp->width = is->video_ctx->width;
        vp->height = is->video_ctx->height;
        vp->allocated = 1;
    }
    
    //分配空间,将视频帧放入pictq数组中去
    int queue_picture(VideoState *is,AVFrame *pFrame,double pts){
        VideoPicture *vp;
        int dst_pix_fmt;
        AVPicture pict;    //yuv数据
    
        SDL_LockMutex(is->pictq_mutex);        //------------??应该是if
        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){
            printf("quit from queue picture...
    ");
            return -1;
        }
    
        vp = &is->pictq[is->pictq_windex];    //这个是要插入的位置数据
        if(!vp->pict || 
            vp->width != is->video_ctx->width ||
            vp->height != is->video_ctx->height){    //开始分配空间
            vp->allocated = 0;        //还没有开始分配空间
            alloc_picture(is);        //开始分配空间,内部会修改标识符
            if(is->quit){
                printf("quit from queue picture 2...
    ");
                return -1;
            }
        }
    
        if(vp->pict){    //分配成功,开始进行裁剪处理,处理后的数据放入这里面
            //---------------------修改pts---------------------
            vp->pts = pts;    //更新pts
    
            sws_scale(is->sws_ctx,(uint8_t const* const*)pFrame->data,
                pFrame->linesize,0,is->video_ctx->height,
                vp->pict->data,vp->pict->linesize);
    
            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;
    }
    
    double synchronize_video(VideoState* is,AVFrame *src_frame,double pts){
        double frame_delay;
    
        if(pts != 0){
            is->video_clock = pts;    //直接更新video_clock
        }else{
            pts = is->video_clock;    //否则使用上一次的video_clock作为pts值
        }
    
        frame_delay = av_q2d(is->video_ctx->time_base);    //通过时间基获取帧间隔
    
        //纠正play (播放时间)的方法 repeat_pict / (2 * fps) 是ffmpeg注释里教的
        frame_delay += src_frame->repeat_pict * (frame_delay*0.5);    //这个帧有一个字段repeat_pict,是要这个帧重复的播放
        is->video_clock += frame_delay;    //保存了下一帧的pts
    
        return pts;
    }
    
    //解码视频帧
    int video_thread(void *arg){
        VideoState *is = (VideoState *)arg;
        AVPacket packet,*pkt = &packet;
        int frameFinished;
        AVFrame *pFrame;
        double pts;
    
        pFrame = av_frame_alloc();
    
        for(;;){
            if(packet_queue_get(&is->videoq,pkt,1) < 0){
                break;    //没有数据,退出
            }
    
            avcodec_decode_video2(is->video_ctx,pFrame,&frameFinished,pkt);    //解码数据帧
    
            //------------------------获取视频帧的pts信息------------------------
            if((pts = av_frame_get_best_effort_timestamp(pFrame)) == AV_NOPTS_VALUE){    //av_frame_get_best_effort_timestamp计算最合适的pts时间
                pts = 0;
            }
            pts *= av_q2d(is->video_st->time_base);        //获取时间戳信息---转换为秒
    
            if(frameFinished){    //去裁剪处理
                pts = synchronize_video(is,pFrame,pts);    //计算pts,防止前面的pts为0,然后顺便获取下一帧的pts
                if(queue_picture(is,pFrame,pts) < 0){    //内部修改视频的pts信息
                    break;
                }
            }
    
            av_free_packet(pkt);
        }
        av_frame_free(&pFrame);
        return 0;
    }
    
    /*-----------------解复用线程方法,主要用于初始化---------------*/
    
    //初始化音视频流的相关数据,(只被音频、视频流各自调用一次)
    int stream_component_open(VideoState *is, int stream_index){
        int64_t in_channel_layout,out_channel_layout;
        AVFormatContext *pFormatCtx = is->pFormatCtx;
        AVCodecContext *codecCtx = NULL;
        AVCodec *codec = NULL;
        SDL_AudioSpec new_spec,old_spec;
    
        codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);    //获取解码器信息
        if(!codec){
            printf("UnSupport codec!
    ");
            return -1;
        }
    
        codecCtx = avcodec_alloc_context3(codec);
        if(!codecCtx){
            printf("Failed to alloc codec context
    ");
            return -1;
        }
    
        if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){
            printf("Failed to copy codec context
    ");
            return -1;
        }
    
        if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO){    //设置音频信息
            new_spec.freq = codecCtx->sample_rate;
            new_spec.format = AUDIO_S16SYS;
            new_spec.channels = codecCtx->channels;
            new_spec.silence = 0;
            new_spec.samples = SDL_AUDIO_BUFFER_SIZE;    //AAC单通道采样率就是1024
            new_spec.callback = audio_callback;
            new_spec.userdata = is;
    
            if(SDL_OpenAudio(&new_spec,&old_spec)<0){
                printf("SDL_OpenAudio: %s
    ", SDL_GetError());
                return -1;
            }
        }
    
        if(avcodec_open2(codecCtx,codec,NULL) < 0){
            printf("Failed to open codec
    ");
            return -1;
        }
    
        switch(codecCtx->codec_type){
            case AVMEDIA_TYPE_AUDIO:    //处理音频
                is->audioStream = stream_index;
                is->audio_st = pFormatCtx->streams[stream_index];
                is->audio_ctx = codecCtx;
                is->audio_buf_size = 0;
                is->audio_buf_index = 0;
    
                memset(&is->audio_pkt,0,sizeof(is->audio_pkt));    //取代了前一个版本的静态局部变量而已,这里不使用memset也可以
                packet_queue_init(&is->audioq);
    
                SDL_PauseAudio(0);
    
                in_channel_layout = av_get_default_channel_layout(is->audio_ctx->channels);
                out_channel_layout = in_channel_layout;
    
                is->audio_swr_cxt = swr_alloc();    //开始分配重采样的数据空间
                swr_alloc_set_opts(is->audio_swr_cxt,
                    out_channel_layout,
                    AV_SAMPLE_FMT_S16,
                    is->audio_ctx->sample_rate,
                    in_channel_layout,
                    is->audio_ctx->sample_fmt,
                    is->audio_ctx->sample_rate,
                    0,
                    NULL);
    
                swr_init(is->audio_swr_cxt);
                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;
    
                packet_queue_init(&is->videoq);
                is->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(video_thread,"video_thread",is);    //开始视频解码线程
                break;
            default:
                break;
        }
        return 0;
    }
    
    //解复用,会在后面一直读取视频数据包
    int decode_thread(void *arg){
        VideoState *is = (VideoState *)arg;
        AVFormatContext *pFormatCtx = NULL;
        AVPacket packet, *ptk = &packet;
        SDL_Event event;
    
        int i,ret=0;
        int video_index = -1;
        int audio_index = -1;
    
        //-------初始化操作
        is->videoStream = -1;
        is->audioStream = -1;
    
        global_video_state = is;
    
        //ffmpeg操作,获取文件信息
        if(avformat_open_input(&pFormatCtx,is->filename,NULL,NULL)<0){
            printf("Failed to open file[%s] context
    ", is->filename);
            return -1;
        }
    
        is->pFormatCtx = pFormatCtx;
    
        if(avformat_find_stream_info(pFormatCtx,NULL)<0){
            printf("Failed to get detail stream infomation
    ");
            return -1;
        }
    
        for(i=0;i<pFormatCtx->nb_streams;i++){
            if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO && video_index < 0)
                video_index = i;
            if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO && audio_index < 0)
                audio_index = i;
        }
    
        if(video_index < 0 || audio_index < 0){
            printf("Failed to find stream index for audio/video
    ");
            return -1;
        }
    
        av_dump_format(pFormatCtx,audio_index,is->filename,0);    //打印信息
        av_dump_format(pFormatCtx,video_index,is->filename,0);    //打印信息
    
        //初始化音频流信息
        ret = stream_component_open(is,audio_index);
        if(ret < 0){
            printf("Failed to initialize audio data
    ");
            goto fail;
        }
        //初始化视频流信息
        ret = stream_component_open(is,video_index);
        if(ret < 0){
            printf("Failed to initialize video data
    ");
            goto fail;
        }
    
        //初始化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
            );
    
        renderer = SDL_CreateRenderer(win,-1,0);
    
        texture = SDL_CreateTexture(renderer,
            SDL_PIXELFORMAT_IYUV,
            SDL_TEXTUREACCESS_STREAMING,
            is->video_ctx->width,
            is->video_ctx->height);
    
        //开始获取包
        for(;;){
            if(is->quit){    //对于死循环,需要一直去判断退出信号
                SDL_CondSignal(is->videoq.cond);    //既然要退出了,就要让所有在循环中的程序退出,发送所有可能阻塞的信号量,防止因为缺少信号量而导致的阻塞
                SDL_CondSignal(is->audioq.cond);
                break;
            }
    
            if(is->audioq.size > MAX_AUDIOQ_SIZE ||
                is->videoq.size > MAX_VEDIOQ_SIZE){    //对于数据量大于界限值的时候,我们直接去获取这些值,不去获取下一个packet
                SDL_Delay(10);    //等待10ms
                continue;
            }
    
            if(av_read_frame(is->pFormatCtx,ptk) < 0){
                //context对象中有一个pb,这是内部用到的IO上下文,通过pb->error,就能获取到IO上发生的错误。
                if(is->pFormatCtx->pb->error == 0){
                    SDL_Delay(100);    //IO错误,等待一段时间用户输入
                    continue;
                }else{    //读取完毕,退出
                    break;
                }
            }
    
            //开始处理读取的数据包
            if(ptk->stream_index == is->videoStream){    //视频包
                packet_queue_put(&is->videoq,ptk);        //当我们读取的视频也是25fps的话,播放声音不会卡顿,当是30fps,音频会出现卡顿,因为队列中数据可能不存在
                printf("put video queue, size:%d
    ", is->videoq.nb_packets);
            }else if(ptk->stream_index == is->audioStream){    //音频包
                packet_queue_put(&is->audioq,ptk);
                printf("put audio queue, size:%d
    ", is->audioq.nb_packets);
            }else{
                av_free_packet(ptk);
            }
        }
    
        while(!is->quit){    //出现数据包读取完毕,放入了队列中,但是还没有从队列中读取完成去解码播放,所以我们需要等待一段时间
            SDL_Delay(100);    //播放完毕以后,等待用户主动关闭
        }
    
    fail:
        event.type = FF_QUIT_EVENT;
        event.user.data1 = is;
        SDL_PushEvent(&event);
    
        return 0;
    }
    
    /*-----------------主(函数)线程方法---------------*/
    int main(int argc,char *argv[]){
        int ret = -1;
    
        SDL_Event event;    //主函数线程主要处理事件和视频渲染问题
        VideoState *is;
    
        if(argc<2){
            fprintf(stderr,"Usage: test <file>
    ");
            return ret;
        }
    
        //------进行初始化操作(简单的初始化,初始化锁、条件变量等信息),开启解复用线程
        is = av_malloc(sizeof(VideoState));
    
        av_register_all();
        if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
            printf("Failed to initialize SDL - %s
    ", SDL_GetError());
            return ret;
        }
        texture_mutex = SDL_CreateMutex();
    
        av_strlcpy(is->filename,argv[1],sizeof(is->filename));
    
        is->pictq_mutex = SDL_CreateMutex();
        is->pictq_cond = SDL_CreateCond();
    
        schedule_refresh(is,40);    //设置40ms发送刷新信号,这个间隔内去实现解复用,获取数据。如果这个时间不够,后面获取数据为空,则还会再延迟的
    
        //开启解复用线程,内部也实现了很多初始化操作
        is->parse_tid = SDL_CreateThread(decode_thread,"decode_thread",is);
        if(!is->parse_tid){
            av_free(is);    //解复用线程开启失败
            goto __FAIL;
        }
    
        for(;;){
            SDL_WaitEvent(&event);    //阻塞式等待信号
            switch(event.type){
                case FF_QUIT_EVENT:    //出错导致出现这个事件
                case SDL_QUIT:        //用户点击了关闭
                    printf("receive a QUIT event:%d
    ", event.type);
                    is->quit = 1;    //其他线程推出
                    goto __QUIT;
                    break;
                case FF_REFRESH_EVENT:
                    video_refresh_timer(event.user.data1);    //传递的数据就是VideoState
                    break;
                default:
                    break;
            }
        }
    
    __QUIT:
        ret = 0;
    
    __FAIL:
        SDL_Quit();
        return ret;
    }
    View Code

    因为进行了音视频同步,所以导致音频、视频队列中的数据包个数几乎保持不变,为12和37!!!

  • 相关阅读:
    使用Delphi调用条形码控件BarTender打印标签
    我看过的书
    语法规则
    智能家居
    HAL库ADC的DMA采集
    HAL库串口中断接收
    触动心灵的一句话
    摄影技巧
    中国茶道
    单片机延时函数
  • 原文地址:https://www.cnblogs.com/ssyfj/p/14763842.html
Copyright © 2020-2023  润新知