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

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

前言

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


FFmpeg解码

  FFmpeg解码的基本流程请参照:

  《FFmpeg开发笔记(四):ffmpeg解码的基本流程详解

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


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。


相关文章
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
1248 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
343 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
Ubuntu 应用服务中间件 nginx
Ubuntu安装笔记(三):ffmpeg(3.2.16)源码编译opencv(3.4.0)
本文是关于Ubuntu系统中使用ffmpeg 3.2.16源码编译OpenCV 3.4.0的安装笔记,包括安装ffmpeg、编译OpenCV、卸载OpenCV以及常见报错处理。
828 2
Ubuntu安装笔记(三):ffmpeg(3.2.16)源码编译opencv(3.4.0)
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
429 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
XML Java Android开发
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
GSYVideoPlayer是一款国产移动端视频播放器,支持弹幕、滤镜、广告等功能,采用IJKPlayer、Media3(EXOPlayer)、MediaPlayer及AliPlayer多种内核。截至2024年8月,其GitHub星标数达2万。集成时需使用新版Android Studio,并按特定步骤配置依赖与权限。提供了NormalGSYVideoPlayer、GSYADVideoPlayer及ListGSYVideoPlayer三种控件,支持HLS、RTMP等多种直播链接。
460 18
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
914 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
228 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
Linux 开发工具 Android开发
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
EasyPusher是一款国产RTSP直播录制推流客户端工具,支持Windows、Linux、Android及iOS等系统。尽管其GitHub仓库(安卓版:https://github.com/EasyDarwin/EasyPusher-Android)已多年未更新,但通过一系列改造,如升级SDK版本、迁移到AndroidX、指定本地NDK版本及更新Gradle版本等,仍可在最新Android Studio上运行。以下是针对Android Studio Dolphin版本的具体改造步骤。
217 3
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
|
编解码 移动开发 安全
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生
自互联网普及以来,流媒体技术特别是视频直播技术不断进步,出现了多种传输协议。早期的MMS由微软主导,但随WMV格式衰落而减少使用。RTSP由网景和RealNetworks联合提出,支持多种格式,但在某些现代应用中不再受支持。RTMP由Adobe开发,曾广泛用于网络直播,但因HTML5不支持Flash而受影响。HLS由苹果开发,基于HTTP,适用于点播。SRT和RIST均为较新协议,强调安全与可靠性,尤其SRT在电视直播中应用增多。尽管RTMP仍占一定市场,但SRT等新协议正逐渐兴起。
387 8
FFmpeg开发笔记(五十)聊聊几种流媒体传输技术的前世今生