六、FFmpeg 4.0.2+SDL2 播放视频

[TOC]

开始前的BB

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


六、FFmpeg 4.0.2+SDL2 播放视频_第1张图片
image.png

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

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

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

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

六、FFmpeg 4.0.2+SDL2 播放视频_第2张图片
image.png

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


六、FFmpeg 4.0.2+SDL2 播放视频_第3张图片
image.png

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

./configure --disable-libsamplerate --disable-libudev --disable-dbus --disable-ime --disable-ibus --disable-fcitx

make -j8

make install

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

六、FFmpeg 4.0.2+SDL2 播放视频_第4张图片
image.png

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

六、FFmpeg 4.0.2+SDL2 播放视频_第5张图片
image.png

然后在Cmake文件中


六、FFmpeg 4.0.2+SDL2 播放视频_第6张图片
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 播放视频_第7张图片
启动

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

未完持续。。。

你可能感兴趣的:(六、FFmpeg 4.0.2+SDL2 播放视频)