美文网首页MobileMedia001ffmpeg
六、FFmpeg 4.0.2+SDL2 播放视频

六、FFmpeg 4.0.2+SDL2 播放视频

作者: Mirs | 来源:发表于2019-04-17 11:09 被阅读0次

    [TOC]

    开始前的BB

    之前我们都是拿ffplay播放视频,做为一个专业的开发人员,会用就够了么?


    image.png

    本章,我们就来进行(莞式)(分离-解码-显示)一条龙。
    这章的这里就得简单介绍一下SDL2了,

    SDL 是一个跨平台的媒体开发库 用C写的(pygame就是包装的它),主要功能包括,图像显示、音频播放、线程控制、事件处理、定时器、字节序无关(大小端)
    

    SDL2就是SDL1的升级版本,变了很多API(没有错,我解释的就是这么通俗)

    SDL2我们可以直接自己编译一下 下载地址
    选择

    image.png

    下载源码,解压之后通过终端进入,大概是这样


    image.png

    然后我们就开始输入命令编译

    ./configure --disable-libsamplerate --disable-libudev --disable-dbus --disable-ime --disable-ibus --disable-fcitx
    
    make -j8
    
    make install
    
    

    完事之后我们把include这个目录直接拷贝到我们项目的include/SDL2

    image.png

    /usr/local/lib/目录找到libSDL2-2.0.0.dylib,复制到librarys里

    image.png

    然后在Cmake文件中


    image.png

    把SDL2加进来,就准备开始愉快的玩耍了

    在src中新建chapter_06/sdl_video.h,撸码开始

    SDL2 播放解码后的视频

    整体先浏览一下调用方法以及顺序

    /** 1.初始化SDL2 **/
    void initSDL2();
    
    /** 2.初始化FFmpeg  **/
    void preparDecodec(const char *url);
    
    /** 3.解码播放 **/
    void decodecFrame();
    
    /** 4.释放资源 **/
    void freeContext();
    
    /** 3.1 绘制一帧数据 在 decodecFrame() 中调用 **/
    void drawFrame(AVFrame *frame);
    
    
    
    /** 播放视频 (外部调用的总方法)**/
    void playVideo(const char *url);
    

    初始化SDL2

    首先我们把SDL2初始化 新建方法initSDL2()

    #define WINDOW_WIDTH 1080
    #define WINDOW_HEIGHT 720
    
    /** ########## SDL2 相关 ############# **/
    SDL_Window *window;
    SDL_Renderer *render;
    SDL_Texture *texture;
    SDL_Rect rect;
    
    /**
     * 初始化SDL2
     */
    void initSDL2() {
        //初始化SDL2
        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER)) {
            cout << "[error] SDL Init error!" << endl;
            return;
        }
    
        //创建Window
        window = SDL_CreateWindow("LearnFFmpeg", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH,
                                  WINDOW_HEIGHT, SDL_WINDOW_OPENGL);
        if (!window) {
            cout << "[error] SDL CreateWindow error!" << endl;
            return;
        }
    
        //创建Render
        render = SDL_CreateRenderer(window, -1, 0);
        //创建Texture
        texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, WINDOW_WIDTH, WINDOW_HEIGHT);
    
        rect.x = 0;
        rect.y = 0;
        rect.w = WINDOW_WIDTH;
        rect.h = WINDOW_HEIGHT;
    }
    

    FFmpeg 解复用+解码

    初始好窗口之后,我们来初始化ffmpeg相关的变量以及参数

    /** ########### FFmpeg 相关 ############# **/
    AVFormatContext *formatContext;
    AVCodecContext *codecContext;
    AVCodec *codec;
    AVPacket *packet;
    AVFrame *frame;
    int videoIndex = -1;
    
    /** 初始化FFmpeg  **/
    void preparDecodec(const char *url) {
        int retcode;
        //初始化FormatContext
        formatContext = avformat_alloc_context();
        if (!formatContext) {
            cout << "[error] alloc format context error!" << endl;
            return;
        }
    
        //打开输入流
        retcode = avformat_open_input(&formatContext, url, nullptr, nullptr);
        if (retcode != 0) {
            cout << "[error] open input error!" << endl;
            return;
        }
    
        //读取媒体文件信息
        retcode = avformat_find_stream_info(formatContext, NULL);
        if (retcode != 0) {
            cout << "[error] find stream error!" << endl;
            return;
        }
    
        //分配codecContext
        codecContext = avcodec_alloc_context3(NULL);
        if (!codecContext) {
            cout << "[error] alloc codec context error!" << endl;
            return;
        }
    
        //寻找到视频流的下标
        videoIndex = av_find_best_stream(formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
        //将视频流的的编解码信息拷贝到codecContext中
        retcode = avcodec_parameters_to_context(codecContext, formatContext->streams[videoIndex]->codecpar);
        if (retcode != 0) {
            cout << "[error] parameters to context error!" << endl;
            return;
        }
    
        //查找解码器
        codec = avcodec_find_decoder(codecContext->codec_id);
        if (codec == nullptr) {
            cout << "[error] find decoder error!" << endl;
            return;
        }
    
        //打开解码器
        retcode = avcodec_open2(codecContext, codec, nullptr);
        if (retcode != 0) {
            cout << "[error] open decodec error!" << endl;
            return;
        }
    
        //初始化一个packet
        packet = av_packet_alloc();
        //初始化一个Frame
        frame = av_frame_alloc();
    }
    

    初始化好之后就可以进行解码

    /** 解码数据 **/
    void decodecFrame() {
        int sendcode = 0;
        //读取包
        while (av_read_frame(formatContext, packet) == 0) {
            if (packet->stream_index != videoIndex)continue;
            //接受解码后的帧数据
            while (avcodec_receive_frame(codecContext, frame) == 0) {
                //绘制图像
                drawFrame(frame);
            }
            //发送解码前的包数据
            sendcode = avcodec_send_packet(codecContext, packet);
            //根据发送的返回值判断状态
            if (sendcode == 0) {
                cout << "[debug] " << "SUCCESS" << endl;
            } else if (sendcode == AVERROR_EOF) {
                cout << "[debug] " << "EOF" << endl;
            } else if (sendcode == AVERROR(EAGAIN)) {
                cout << "[debug] " << "EAGAIN" << endl;
            } else {
                cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
            }
        }
    
    }
    

    这边我发现网上的教程都没有说avcodec_send_packetavcodec_receive_frame返回值是什么意思,这边我来解释一部分
    0 读取成功
    AVERROR_EOF 已经读取到最后 流结束的标志
    AVERROR(EAGAIN) 当前发送/接受队里已满/已空,需要调用对应的recive/send

    接受到AVFrame数据后调用drawFrame() 进行绘制

    SDL2显示一帧画面

    /** 绘制一帧数据 **/
    void drawFrame(AVFrame *frame) {
        if (frame == nullptr)return;
        //上传YUV到Texture
        SDL_UpdateYUVTexture(texture, &rect,
                             frame->data[0], frame->linesize[0],
                             frame->data[1], frame->linesize[1],
                             frame->data[2], frame->linesize[2]
        );
    
        SDL_RenderClear(render);
        SDL_RenderCopy(render, texture, NULL, &rect);
        SDL_RenderPresent(render);
    }
    

    最后记得释放资源

    /** 释放资源 **/
    void freeContext() {
        if (formatContext != nullptr) avformat_close_input(&formatContext);
        if (codecContext != nullptr) avcodec_free_context(&codecContext);
        if (packet != nullptr) av_packet_free(&packet);
        if (frame != nullptr) av_frame_free(&frame);
    }
    

    整合步骤

    我们来把这几个方法组装一下,方便外部调用

    /** 播放视频 **/
    void playVideo(const char *url) {
        initSDL2();
        preparDecodec(url);
        decodecFrame();
        freeContext();
    }
    

    我们在main方法中调用

    const char *url = "../video/test_video.mp4";
    playVideo(url);
    
    image.png

    喏,就显示出来了

    视频自同步

    是不是有些同学看的显示的非常快,没有错,因为他没有进行同步的操作,我们可以来个简单的同步操作

    • 根据视频的帧率进行同步

    我们都知道帧率是描述了视频图像连续出现在显示器上的频率,他的局限是有些帧之间的PTS差别较大/小的时候这种方式仍然会按照每个帧固定停留的时间进行显示,无法动态变化,通过下面的公式计算出平均每帧显示的时间(s)

    s = 1/fps
    

    所以我们可以新建一个变量double displayTimeUs = 0;,decodecFrame()可以改为

    /** 解码数据 **/
    void decodecFrame() {
        int sendcode = 0;
    
        //计算帧率
        double frameRate = av_q2d(formatContext->streams[videoIndex]->avg_frame_rate);
        //计算显示的时间
        displayTimeUs = 1*1000/frameRate;
    
        //读取包
        while (av_read_frame(formatContext, packet) == 0) {
            if (packet->stream_index != videoIndex)continue;
            //接受解码后的帧数据
            while (avcodec_receive_frame(codecContext, frame) == 0) {
                //绘制图像
                drawFrame(frame);
            }
            //发送解码前的包数据
            sendcode = avcodec_send_packet(codecContext, packet);
            //根据发送的返回值判断状态
            if (sendcode == 0) {
                cout << "[debug] " << "SUCCESS" << endl;
            } else if (sendcode == AVERROR_EOF) {
                cout << "[debug] " << "EOF" << endl;
            } else if (sendcode == AVERROR(EAGAIN)) {
                cout << "[debug] " << "EAGAIN" << endl;
            } else {
                cout << "[debug] " << av_err2str(AVERROR(sendcode)) << endl;
            }
        }
    
    }
    
    

    drawFrame()中新增一行代码SDL_Delay(displayTimeUs);

    /** 绘制一帧数据 **/
    void drawFrame(AVFrame *frame) {
        if (frame == nullptr)return;
        //上传YUV到Texture
        SDL_UpdateYUVTexture(texture, &rect,
                             frame->data[0], frame->linesize[0],
                             frame->data[1], frame->linesize[1],
                             frame->data[2], frame->linesize[2]
        );
    
        SDL_RenderClear(render);
        SDL_RenderCopy(render, texture, NULL, &rect);
        SDL_RenderPresent(render);
        SDL_Delay(displayTimeUs);
    }
    

    然后点击启动


    启动

    然后就会发现播放起来已经是正常了

    未完持续。。。

    相关文章

      网友评论

        本文标题:六、FFmpeg 4.0.2+SDL2 播放视频

        本文链接:https://www.haomeiwen.com/subject/plpgwqtx.html