六、FFmpeg 4.0.2+SDL2 播放视频

[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);
}

然后点击启动


启动

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

未完持续。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容