zoukankan      html  css  js  c++  java
  • FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    前言

      ffmpeg播放rtsp网络流和摄像头流。

     

    Demo

      使用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

     
  • 相关阅读:
    动态规划----查找一个数组中存在的数学数列
    java数据结构和算法------第八章
    java数据结构和算法-----第七章
    04002_HTML表单
    雷林鹏分享:PHP 高级过滤器
    雷林鹏分享:Lua 函数
    雷林鹏分享:Lua 流程控制
    雷林鹏分享:Lua 循环
    雷林鹏分享:Lua 变量
    雷林鹏分享:Lua 数据类型
  • 原文地址:https://www.cnblogs.com/qq21497936/p/13955653.html
Copyright © 2011-2022 走看看