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\n",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\n",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!\n");
            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!\n");
            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...\n");
                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...\n");
            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-------------------\n");
                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...\n");
        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...\n");
            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!\n");
        return -1;
    }

    codecCtx = avcodec_alloc_context3(codec);
    if(!codecCtx){
        printf("Failed to alloc codec context\n");
        return -1;
    }

    if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){
        printf("Failed to copy codec context\n");
        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\n", SDL_GetError());
            return -1;
        }
    }

    if(avcodec_open2(codecCtx,codec,NULL) < 0){
        printf("Failed to open codec\n");
        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\n", is->filename);
        return -1;
    }

    is->pFormatCtx = pFormatCtx;

    if(avformat_find_stream_info(pFormatCtx,NULL)<0){
        printf("Failed to get detail stream infomation\n");
        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\n");
        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\n");
        goto fail;
    }
    //初始化视频流信息
    ret = stream_component_open(is,video_index);
    if(ret < 0){
        printf("Failed to initialize video data\n");
        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\n", is->videoq.nb_packets);
        }else if(ptk->stream_index == is->audioStream){    //音频包
            packet_queue_put(&is->audioq,ptk);
            printf("put audio queue, size:%d\n", 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>\n");
        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\n", 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\n", 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!\n");
            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!\n");
            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...\n");
                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...\n");
            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-------------------\n");
                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...\n");
        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...\n");
            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!\n");
        return -1;
    }

    codecCtx = avcodec_alloc_context3(codec);
    if(!codecCtx){
        printf("Failed to alloc codec context\n");
        return -1;
    }

    if(avcodec_copy_context(codecCtx,pFormatCtx->streams[stream_index]->codec) < 0){
        printf("Failed to copy codec context\n");
        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\n", SDL_GetError());
            return -1;
        }
    }

    if(avcodec_open2(codecCtx,codec,NULL) < 0){
        printf("Failed to open codec\n");
        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\n", is->filename);
        return -1;
    }

    is->pFormatCtx = pFormatCtx;

    if(avformat_find_stream_info(pFormatCtx,NULL)<0){
        printf("Failed to get detail stream infomation\n");
        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\n");
        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\n");
        goto fail;
    }
    //初始化视频流信息
    ret = stream_component_open(is,video_index);
    if(ret < 0){
        printf("Failed to initialize video data\n");
        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\n", is->videoq.nb_packets);
        }else if(ptk->stream_index == is->audioStream){    //音频包
            packet_queue_put(&is->audioq,ptk);
            printf("put audio queue, size:%d\n", 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>\n");
        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\n", 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\n", 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!!!

 

posted @ 2021-05-17 11:06  山上有风景  阅读(1150)  评论(0编辑  收藏  举报