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

若该文为原创文章,未经容许不得转载
原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:http://www.noobyard.com/article/p-gebpnuxb-mo.html
本文章博客地址:http://www.noobyard.com/article/p-gbpmjxxb-mo.html
各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么本身研究web

红胖子(红模仿)的博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中…(点击传送门)api

FFmpeg和SDL开发专栏(点击传送门)

上一篇:《FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)
下一篇:《FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放缓存


前言

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


FFmpeg解码

  FFmpeg解码的基本流程请参照:
  《FFmpeg开发笔记(四):ffmpeg解码的基本流程详解
  《FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)svg


SDL显示

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


ffmpeg同步

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

基本流程

  在这里插入图片描述

同步关键点

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


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的定义。.net

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。


上一篇:《FFmpeg开发笔记(五):ffmpeg解码的基本流程详解(ffmpeg3新解码api)
下一篇:《FFmpeg开发笔记(七):ffmpeg解码音频保存为PCM并使用软件播放


原博主博客地址:https://blog.csdn.net/qq21497936
原博主博客导航:http://www.noobyard.com/article/p-gebpnuxb-mo.html
本文章博客地址:http://www.noobyard.com/article/p-gbpmjxxb-mo.html