前言
ffmpeg播放rtsp网络流和摄像头流。
Demo
使用ffmpeg播放局域网rtsp1080p海康摄像头,调整摄像头码流后:延迟0.2s,不存在马赛克
使用ffmpeg播放局域网rtsp1080p海康摄像头:延迟0.2s,存在马赛克
使用ffmpeg播放网络rtsp文件流:偶尔卡顿,延迟看不出
使用vlc软件播放局域网rtsp1080p海康摄像头:演示2s,不存在马赛克
使用vlc软件播放网络rtsp文件流:不卡顿,延迟看不出
FFmpeg基本播放流程
ffmpeg解码流程
ffmpeg新增API的解码执行流程。
新api解码基本流程如下:
步骤一:注册:
使用ffmpeg对应的库,都需要进行注册,可以注册子项也可以注册全部。
步骤二:打开文件:
打开文件,根据文件名信息获取对应的ffmpeg全局上下文。
步骤三:探测流信息:
一定要探测流信息,拿到流编码的编码格式,不探测流信息则其流编码器拿到的编码类型可能为空,后续进行数据转换的时候就无法知晓原始格式,导致错误。
步骤四:查找对应的解码器
依据流的格式查找解码器,软解码还是硬解码是在此处决定的,但是特别注意是否支持硬件,需要自己查找本地的硬件解码器对应的标识,并查询其是否支持。普遍操作是,枚举支持文件后缀解码的所有解码器进行查找,查找到了就是可以硬解了(此处,不做过多的讨论,对应硬解码后续会有文章进行进一步研究)。
(注意:解码时查找解码器,编码时查找编码器,两者函数不同,不要弄错了,否则后续能打开但是数据是错的)
步骤五:打开解码器
开打解码器的时候,播放的是rtsp流,需要设置一些参数,在ffmpeg中参数的设置是通过AVDictionary来设置的。
使用以上设置的参数,传入并打开获取到的解码器。
AVDictionary *pAVDictionary = 0 // 设置缓存大小 1024000byte av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0); // 设置超时时间 20s av_dict_set(&pAVDictionary, "stimeout", "20000000", 0); // 设置最大延时 3s av_dict_set(&pAVDictionary, "max_delay", "30000000", 0); // 设置打开方式 tcp/udp av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0); ret = avcodec_open2(pAVCodecContext, pAVCodec, &pAVDictionary); if(ret) { LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)"; return; }
步骤六:申请缩放数据格式转换结构体
此处特别注意,基本上解码的数据都是yuv系列格式,但是我们显示的数据是rgb等相关颜色空间的数据,所以此处转换结构体就是进行转换前到转换后的描述,给后续转换函数提供转码依据,是很关键并且非常常用的结构体。
步骤七:申请缓存区
申请一个缓存区outBuffer,fill到我们目标帧数据的data上,比如rgb数据,QAVFrame的data上存是有指定格式的数据,且存储有规则,而fill到outBuffer(自己申请的目标格式一帧缓存区),则是我们需要的数据格式存储顺序。
举个例子,解码转换后的数据为rgb888,实际直接用data数据是错误的,但是用outBuffer就是对的,所以此处应该是ffmpeg的fill函数做了一些转换。
进入循环解码:
步骤八:分组数据包送往解码器(此处由一个步骤变为了步骤八和步骤九)
拿取封装的一个packet,判断packet数据的类型进行送往解码器解码。
步骤九:从解码器缓存中获取解码后的数据
一个包可能存在多组数据,老的api获取的是第一个,新的api分开后,可以循环获取,直至获取不到跳转“步骤十二”。
步骤十一:自行处理
拿到了原始数据自行处理。
不断循环,直到拿取pakcet函数成功,但是无法got一帧数据,则代表文件解码已经完成。
帧率需要自己控制循环,此处只是循环拿取,可加延迟等。
步骤十二:释放QAVPacket
此处要单独列出是因为,其实很多网上和开发者的代码:
在进入循环解码前进行了av_new_packet,循环中未av_free_packet,造成内存溢出;
在进入循环解码前进行了av_new_packet,循环中进行av_free_pakcet,那么一次new对应无数次free,在编码器上是不符合前后一一对应规范的。
查看源代码,其实可以发现av_read_frame时,自动进行了av_new_packet(),那么其实对于packet,只需要进行一次av_packet_alloc()即可,解码完后av_free_packet。
执行完后,返回执行“步骤八:获取一帧packet”,一次循环结束。
步骤十三:释放转换结构体
全部解码完成后,安装申请顺序,进行对应资源的释放。
步骤十四:关闭解码/编码器
关闭之前打开的解码/编码器。
步骤十五:关闭上下文
关闭文件上下文后,要对之前申请的变量按照申请的顺序,依次释放。
补充
ffmpeg打开rtsp出现严重的马赛克和部分卡顿,需要修改文件udp.c的缓存区大小,修改后需要重新编译。
实测更改后的马赛克会好一些,相比较软件来说有一些差距的,这部分需要继续优化。
编译请参照《FFmpeg开发笔记(三):ffmpeg介绍、windows编译以及开发环境搭建》
Demo源码
void FFmpegManager::testDecodeRtspSyncShow() { QString rtspUrl = "http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear2/prog_index.m3u8"; // QString rtspUrl = "rtsp://admin:Admin123@192.168.1.65:554/h264/ch1/main/av_stream"; // SDL相关变量预先定义 SDL_Window *pSDLWindow = 0; SDL_Renderer *pSDLRenderer = 0; SDL_Surface *pSDLSurface = 0; SDL_Texture *pSDLTexture = 0; SDL_Event event; qint64 startTime = 0; // 记录播放开始 int currentFrame = 0; // 当前帧序号 double fps = 0; // 帧率 double interval = 0; // 帧间隔 // ffmpeg相关变量预先定义与分配 AVFormatContext *pAVFormatContext = 0; // ffmpeg的全局上下文,所有ffmpeg操作都需要 AVStream *pAVStream = 0; // ffmpeg流信息 AVCodecContext *pAVCodecContext = 0; // ffmpeg编码上下文 AVCodec *pAVCodec = 0; // ffmpeg编码器 AVPacket *pAVPacket = 0; // ffmpag单帧数据包 AVFrame *pAVFrame = 0; // ffmpeg单帧缓存 AVFrame *pAVFrameRGB32 = 0; // ffmpeg单帧缓存转换颜色空间后的缓存 struct SwsContext *pSwsContext = 0; // ffmpeg编码数据格式转换 AVDictionary *pAVDictionary = 0; // ffmpeg数据字典,用于配置一些编码器属性等 int ret = 0; // 函数执行结果 int videoIndex = -1; // 音频流所在的序号 int numBytes = 0; // 解码后的数据长度 uchar *outBuffer = 0; // 解码后的数据存放缓存区 pAVFormatContext = avformat_alloc_context(); // 分配 pAVPacket = av_packet_alloc(); // 分配 pAVFrame = av_frame_alloc(); // 分配 pAVFrameRGB32 = av_frame_alloc(); // 分配 if(!pAVFormatContext || !pAVPacket || !pAVFrame || !pAVFrameRGB32) { LOG << "Failed to alloc"; return; } // 步骤一:注册所有容器和编解码器(也可以只注册一类,如注册容器、注册编码器等) av_register_all(); avformat_network_init(); // 步骤二:打开文件(ffmpeg成功则返回0) LOG << "打开:" << rtspUrl; ret = avformat_open_input(&pAVFormatContext, rtspUrl.toUtf8().data(), 0, 0); if(ret) { LOG << "Failed"; return; } // 步骤三:探测流媒体信息 ret = avformat_find_stream_info(pAVFormatContext, 0); if(ret < 0) { LOG << "Failed to avformat_find_stream_info(pAVFormatContext, 0)"; return; } // 步骤四:提取流信息,提取视频信息 for(int index = 0; index < pAVFormatContext->nb_streams; index++) { pAVCodecContext = pAVFormatContext->streams[index]->codec; pAVStream = pAVFormatContext->streams[index]; switch (pAVCodecContext->codec_type) { case AVMEDIA_TYPE_UNKNOWN: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_UNKNOWN"; break; case AVMEDIA_TYPE_VIDEO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_VIDEO"; videoIndex = index; LOG; break; case AVMEDIA_TYPE_AUDIO: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_AUDIO"; break; case AVMEDIA_TYPE_DATA: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_DATA"; break; case AVMEDIA_TYPE_SUBTITLE: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_SUBTITLE"; break; case AVMEDIA_TYPE_ATTACHMENT: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_ATTACHMENT"; break; case AVMEDIA_TYPE_NB: LOG << "流序号:" << index << "类型为:" << "AVMEDIA_TYPE_NB"; break; default: break; } // 已经找打视频品流 if(videoIndex != -1) { break; } } if(videoIndex == -1 || !pAVCodecContext) { LOG << "Failed to find video stream"; return; } // 步骤五:对找到的视频流寻解码器 pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id); if(!pAVCodec) { LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):" << pAVCodecContext->codec_id; return; } // 步骤六:打开解码器 // 设置缓存大小 1024000byte av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0); // 设置超时时间 20s av_dict_set(&pAVDictionary, "stimeout", "20000000", 0); // 设置最大延时 3s av_dict_set(&pAVDictionary, "max_delay", "30000000", 0); // 设置打开方式 tcp/udp av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0); ret = avcodec_open2(pAVCodecContext, pAVCodec, &pAVDictionary); if(ret) { LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)"; return; } // 显示视频相关的参数信息(编码上下文) LOG << "比特率:" << pAVCodecContext->bit_rate; LOG << "宽高:" << pAVCodecContext->width << "x" << pAVCodecContext->height; LOG << "格式:" << pAVCodecContext->pix_fmt; // AV_PIX_FMT_YUV420P 0 LOG << "帧率分母:" << pAVCodecContext->time_base.den; LOG << "帧率分子:" << pAVCodecContext->time_base.num; LOG << "帧率分母:" << pAVStream->avg_frame_rate.den; LOG << "帧率分子:" << pAVStream->avg_frame_rate.num; LOG << "总时长:" << pAVStream->duration / 10000.0 << "s"; LOG << "总帧数:" << pAVStream->nb_frames; // 有总时长的时候计算帧率(较为准确) // fps = pAVStream->nb_frames / (pAVStream->duration / 10000.0); // interval = pAVStream->duration / 10.0 / pAVStream->nb_frames; // 没有总时长的时候,使用分子和分母计算 fps = pAVStream->avg_frame_rate.num * 1.0f / pAVStream->avg_frame_rate.den; interval = 1 * 1000 / fps; LOG << "平均帧率:" << fps; LOG << "帧间隔:" << interval << "ms"; // 步骤七:对拿到的原始数据格式进行缩放转换为指定的格式高宽大小 pSwsContext = sws_getContext(pAVCodecContext->width, pAVCodecContext->height, pAVCodecContext->pix_fmt, pAVCodecContext->width, pAVCodecContext->height, AV_PIX_FMT_RGBA, SWS_FAST_BILINEAR, 0, 0, 0); numBytes = avpicture_get_size(AV_PIX_FMT_RGBA, pAVCodecContext->width, pAVCodecContext->height); outBuffer = (uchar *)av_malloc(numBytes); // pAVFrame32的data指针指向了outBuffer avpicture_fill((AVPicture *)pAVFrameRGB32, outBuffer, AV_PIX_FMT_RGBA, pAVCodecContext->width, pAVCodecContext->height); ret = SDL_Init(SDL_INIT_VIDEO); if(ret) { LOG << "Failed"; return; } pSDLWindow = SDL_CreateWindow(rtspUrl.toUtf8().data(), 0, 0, pAVCodecContext->width, pAVCodecContext->height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if(!pSDLWindow) { LOG << "Failed"; return; } pSDLRenderer = SDL_CreateRenderer(pSDLWindow, -1, 0); if(!pSDLRenderer) { LOG << "Failed"; return; } startTime = QDateTime::currentDateTime().toMSecsSinceEpoch(); currentFrame = 0; pSDLTexture = SDL_CreateTexture(pSDLRenderer, // SDL_PIXELFORMAT_IYUV, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, pAVCodecContext->width, pAVCodecContext->height); if(!pSDLTexture) { LOG << "Failed"; return; } // 步骤八:读取一帧数据的数据包 while(av_read_frame(pAVFormatContext, pAVPacket) >= 0) { if(pAVPacket->stream_index == videoIndex) { // 步骤八:对读取的数据包进行解码 ret = avcodec_send_packet(pAVCodecContext, pAVPacket); if(ret) { LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret; break; } while(!avcodec_receive_frame(pAVCodecContext, pAVFrame)) { sws_scale(pSwsContext, (const uint8_t * const *)pAVFrame->data, pAVFrame->linesize, 0, pAVCodecContext->height, pAVFrameRGB32->data, pAVFrameRGB32->linesize); // 格式为RGBA=8:8:8:8” // rmask 应为 0xFF000000 但是颜色不对 改为 0x000000FF 对了 // gmask 0x00FF0000 0x0000FF00 // bmask 0x0000FF00 0x00FF0000 // amask 0x000000FF 0xFF000000 // 测试了ARGB,也是相反的,而QImage是可以正确加载的 // 暂时只能说这个地方标记下,可能有什么设置不对什么的 qDebug() << __FILE__ << __LINE__ << pSDLTexture; SDL_UpdateYUVTexture(pSDLTexture, NULL, pAVFrame->data[0], pAVFrame->linesize[0], pAVFrame->data[1], pAVFrame->linesize[1], pAVFrame->data[2], pAVFrame->linesize[2]); qDebug() << __FILE__ << __LINE__ << pSDLTexture; SDL_RenderClear(pSDLRenderer); // Texture复制到Renderer SDL_Rect sdlRect; sdlRect.x = 0; sdlRect.y = 0; sdlRect.w = pAVFrame->width; sdlRect.h = pAVFrame->height; qDebug() << __FILE__ << __LINE__ << SDL_RenderCopy(pSDLRenderer, pSDLTexture, 0, &sdlRect) << pSDLTexture; // 更新Renderer显示 SDL_RenderPresent(pSDLRenderer); // 事件处理 SDL_PollEvent(&event); } // 下一帧 currentFrame++; while(QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime < currentFrame * interval) { SDL_Delay(1); } LOG << "current:" << currentFrame <<"," << time << (QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime); } } LOG << "释放回收资源"; if(outBuffer) { av_free(outBuffer); outBuffer = 0; } if(pSwsContext) { sws_freeContext(pSwsContext); pSwsContext = 0; LOG << "sws_freeContext(pSwsContext)"; } if(pAVFrameRGB32) { av_frame_free(&pAVFrameRGB32); pAVFrame = 0; LOG << "av_frame_free(pAVFrameRGB888)"; } if(pAVFrame) { av_frame_free(&pAVFrame); pAVFrame = 0; LOG << "av_frame_free(pAVFrame)"; } if(pAVPacket) { av_free_packet(pAVPacket); pAVPacket = 0; LOG << "av_free_packet(pAVPacket)"; } if(pAVCodecContext) { avcodec_close(pAVCodecContext); pAVCodecContext = 0; LOG << "avcodec_close(pAVCodecContext);"; } if(pAVFormatContext) { avformat_close_input(&pAVFormatContext); avformat_free_context(pAVFormatContext); pAVFormatContext = 0; LOG << "avformat_free_context(pAVFormatContext)"; } // 步骤五:销毁渲染器 SDL_DestroyRenderer(pSDLRenderer); // 步骤六:销毁窗口 SDL_DestroyWindow(pSDLWindow); // 步骤七:退出SDL SDL_Quit(); }
工程模板v1.5.0
对应工程模板v1.5.0:增加播放rtsp使用SDL播放Demo