• FFmpeg开发笔记(六):ffmpeg解码视频并使用SDL同步时间显示播放


    若该文为原创文章,未经允许不得转载
    原博主博客地址:https://blog.csdn.net/qq21497936
    原博主博客导航:https://blog.csdn.net/qq21497936/article/details/102478062
    本文章博客地址:https://blog.csdn.net/qq21497936/article/details/108648385
    各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究

     

    前言

      ffmpeg解码之后,显示需要同步,一是需要显示,本篇使用SDL进行显示,二是需要对时间戳进行同步。

     

    FFmpeg解码

     

    SDL显示

      SDL显示图片的基本流程请参照:
      《SD开发笔记(三):使用SDL渲染窗口颜色和图片

     

    ffmpeg同步

      ffmpeg同步包含音频、视频、字幕等等,此处描述的同步是时间与视频显示的同步。

    基本流程

      在这里插入图片描述

    同步关键点

      计算帧率,拿到信息后,用流上下文获取总时间、总帧数,这样计算出来的时间间隔是最准确的,也可以使用时间基数去计算,也准确只是复杂点。
      视频与时间的同步,比如25fps,那么40ms显示一帧,那么计算好时间间隔,在下一帧的时间节点之前先挂起直到到达该帧显示的时间节点再显示。
      基于消息的就更好处理,计算下一帧的时间间隔,直接定时为这个时间点后进行显示。
      此处还可能涉及到跳帧处理,如系统卡顿以下,解码时间间隔较大,如果按照上一帧往后加40ms计算下一帧,则会导致第一针为0ms,第二针卡顿为50ms,第三针为90ms,那么导致整体时长加长。
      综合以上,ffmpeg在做同步显示的时候,需要选取最开始播放的时间作为基准,用帧序号和帧间隔去计算下一帧的显示时间。

     

    ffmpeg同步相关结构体详解

    AVStream

      流信息的结构体,里面包含了AVCodecContext的实例指针

    struct AVStream {
        AVCodecContext *codec;     // 编码器相关的信息
        AVRational avg_frame_rate; // 根据该参数,可以把PTS转化为实际的时间(单位为秒s)
        int64_t duration;          // 视频时长,单位为10ms,不是ms,所以要除以10000
        int64_t nb_frames;         // 视频总帧数
    }
    

    AVCodecContext

      该结构体是编码上下文信息,对文件流进行探测后,就能得到文件流的具体相关信息了,关于编解码的相关信息就在此文件结构中。
      与同步视频显示相关的变量在此详解一下,其他的可以自行去看ffmpeg源码上对于结构体AVCodecContext的定义。

    struct AVCodecContext {
        AVMediaType codec_type;        // 编码器的类型,如视频、音频、字幕等等
        AVCodec *codec;               // 使用的编码器
        int bit_rata;                  // 平均比特率
        AVRational time_base:         // 根据该参数,可以把PTS转化为实际的时间(单位为秒s)
        int width, height:            // 如果是视频的话,代表宽和高
        enum AVPixelFormat pix_fmt;    // 代表视频像素的格式...
    } AVCodecContext;
    

      在这里插入图片描述

      在这里插入图片描述

     

    Demo源码

    void FFmpegManager::testDecodeSyncShow()
    {
    //    QString fileName = "test/1.avi";
        QString fileName = "test/1.mp4";
    
    
        // 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;             // ffmpag编码数据格式转换
    
        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";
            goto END;
        }
        // 步骤一:注册所有容器和编解码器(也可以只注册一类,如注册容器、注册编码器等)
        av_register_all();
    
        // 步骤二:打开文件(ffmpeg成功则返回0)
        LOG << "文件:" << fileName << ",是否存在:" << QFile::exists(fileName);
        ret = avformat_open_input(&pAVFormatContext, fileName.toUtf8().data(), 0, 0);
        if(ret)
        {
            LOG << "Failed";
            goto END;
        }
        // 步骤三:探测流媒体信息
        ret = avformat_find_stream_info(pAVFormatContext, 0);
        if(ret < 0)
        {
            LOG << "Failed to avformat_find_stream_info(pAVFormatContext, 0)";
            goto END;
        }
        // 步骤四:提取流信息,提取视频信息
        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";
            goto END;
        }
    
        // 步骤五:对找到的视频流寻解码器
        pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
        if(!pAVCodec)
        {
            LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
                << pAVCodecContext->codec_id;
            goto END;
        }
        // 步骤六:打开解码器
        ret = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
        if(ret)
        {
            LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
            goto END;
        }
    
        // 显示视频相关的参数信息(编码上下文)
        LOG << "比特率:" << pAVCodecContext->bit_rate;
    
        LOG << "宽高:" << pAVCodecContext->width << "x" << pAVCodecContext->height;
        LOG << "格式:" << pAVCodecContext->pix_fmt;
        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);
        LOG << "平均帧率:" << fps;
        interval = pAVStream->duration / 10.0 / pAVStream->nb_frames;
        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";
            goto END;
        }
        pSDLWindow = SDL_CreateWindow(fileName.toUtf8().data(),
                                      0,
                                      0,
                                      pAVCodecContext->width,
                                      pAVCodecContext->height,
                                      SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_OPENGL);
        if(!pSDLWindow)
        {
            LOG << "Failed";
            goto END;
        }
        pSDLRenderer = SDL_CreateRenderer(pSDLWindow, -1, 0);
        if(!pSDLRenderer)
        {
            LOG << "Failed";
            goto END;
        }
    
        startTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
        currentFrame = 0;
    
        // 步骤八:读取一帧数据的数据包
        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是可以正确加载的
                    // 暂时只能说这个地方标记下,可能有什么设置不对什么的
                    pSDLSurface = SDL_CreateRGBSurfaceFrom(outBuffer,
                                                           pAVCodecContext->width,
                                                           pAVCodecContext->height,
                                                           4 * 8,
                                                           pAVCodecContext->width * 4,
                                                           0x000000FF,
                                                           0x0000FF00,
                                                           0x00FF0000,
                                                           0xFF000000
                                                           );
                    pSDLTexture = SDL_CreateTextureFromSurface(pSDLRenderer, pSDLSurface);
    
                    SDL_FreeSurface(pSDLSurface);
    //                pSDLSurface = SDL_LoadBMP("testBMP/1.bmp");
    //                pSDLTexture = SDL_CreateTextureFromSurface(pSDLRenderer, pSDLSurface);
    
                    // 清除Renderer
                    SDL_RenderClear(pSDLRenderer);
                    // Texture复制到Renderer
                    SDL_RenderCopy(pSDLRenderer, pSDLTexture, 0, 0);
                    // 更新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);
            }
        }
    END:
        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.2.0

      对应工程模板v1.2.0:增加解码视频并使用SDL显示Demo。

     
     
  • 相关阅读:
    新概念第二册(1)--英语口语听力课1
    外企面试课程(一)---熟悉常见的缩略词
    公司 邮件 翻译 培训 长难句 结课
    workflow
    公司 邮件 翻译 培训 长难句 20
    公司 邮件 翻译 培训 长难句 19
    Engineering Management
    公司 邮件 翻译 培训 长难句 18
    公司 邮件 翻译 培训 长难句 17
    第14.5节 利用浏览器获取的http信息构造Python网页访问的http请求头
  • 原文地址:https://www.cnblogs.com/qq21497936/p/13689119.html
Copyright © 2020-2023  润新知