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; }
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; }
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; }
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; }
因为进行了音视频同步,所以导致音频、视频队列中的数据包个数几乎保持不变,为12和37!!!