前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >FFmpeg4.0+SDL2.0笔记01:Making Screencaps

FFmpeg4.0+SDL2.0笔记01:Making Screencaps

原创
作者头像
非一
修改2021-04-14 10:08:04
5750
修改2021-04-14 10:08:04
举报
文章被收录于专栏:非一非一

环境

背景:在系统性学习FFmpeg时,发现官方推荐教程还是15年的,不少接口已经弃用,大版本也升了一级,所以在这里记录下FFmpeg4.0+SDL2.0的学习过程。

win10,VS2019,FFmpeg4.3.2,SDL2.0.14

原文地址:http://dranger.com/ffmpeg/tutorial01.html

概述

多媒体文件有一些基本组成知识。

1、多媒体文件本身被称为容器,容器类型决定了文件内部的存储形式,比如AVI和Quicktime就是两种不同的容器。

2、多媒体文件中有多串stream(数据流),通常是一串视频流加一串音频流(stream可以理解为按时间轴获取的连续数据元素),流中的数据元素被称为frame(帧),比如常见的视频流由一串连续的H264数据帧组成。不同的流通过不通的codec(编解码器)编码生成。codec定义了真实数据是如何编码/解码的(CODEC==COded+DECoded),

3、从流中直接读取出来的叫packet(数据包),每个packet里包含一个或多个frame。

音视频处理流程可以概括为这几步:

代码语言:javascript
复制
10 从 video.avi 中打开 videoStream
20 从 videoStream 中读取 packet 送给 ffmpeg 解码 
30 从 ffmpeg 获取拿解码完的 frame,如果frame还不完整,GOTO 20
40 用户根据自己的需求处理 frame,比如保存成文件,或者渲染成视频
50 GOTO 20

在这一章,我们将打开一个多媒体文件,读取里面的视频流,使用ffmpeg解码,然后把解码后的frame转换成RGB格式,最后保存到ppm文件里。

打开文件

在以前的版本,我们需要先使用av_register_all初始化ffmpeg,但4.0已弃用该接口,直接avformat_open_input打开文件即可。

代码语言:javascript
复制
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}

const string kMediaFile = "../../../assets/destiny.mp4";

int main(int argc, charg *argv[]) {
	//初始化不用啦,直接avformat_open_input即可
	//av_register_all();
	
	AVFormatContext* pFormatCtx = nullptr;
	//读取avformat
	if (avformat_open_input(&pFormatCtx, kMediaFile.c_str(), nullptr, nullptr) < 0) {
		cout << "avformat_open_input failed" << endl;
		return -1;
	}
}

avformat_open_input 读取文件头并把文件格式等信息保存在AVFormatContext中,后面两个参数分别为AVInputFormat指定视频格式,AVDictionary指定码流的各种参数,传空的话FFmpeg会自动检测。

接下来读取数据流信息:

代码语言:javascript
复制
    //读取流信息,该函数会填充pFormatCtx->streams
    if (avformat_find_stream_info(pFormatCtx, nullptr) < 0) {
        cout << "avformat_find_stream_info failed" << endl;
        return -1;
    }
    //dump格式信息
    av_dump_format(pFormatCtx, 0, kMediaFile.c_str(), 0);

avformat_find_stream_info读取流信息并保存到pFormatCtx->streams,可通过av_dump_format 查看具体内容。

pFormatCtx->streams是一个指针数组,数组里的指针分别指向不同的流信息,长度为pFormatCtx->nb_streams,我们遍历来找到视频流的相关参数。

代码语言:javascript
复制
    int iVideoStream = -1;
    AVCodecParameters* pVideoCodecPar = nullptr;
    //找到视频流编码信息
    for (unsigned i = 0; i < pFormatCtx->nb_streams; ++i) {
        if (iVideoStream == -1 && pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            iVideoStream = i;
            break;
        }
    }
    if (iVideoStream == -1) {
        cout << "couldn't find video stream" << endl;
        return -1;
    }
    //AVStream.codec 被替换为 AVStream.codecpar
    //详见https://lists.libav.org/pipermail/libav-commits/2016-February/018031.html
    pVideoCodecPar = pFormatCtx->streams[iVideoStream]->codecpar;

AVCodecParameters包含视频流使用的codec的相关参数(原先的AVStream.codec已被弃用)。有了我们就能找到具体的解码器并打开它。

代码语言:javascript
复制
        
	AVCodec* pVideoCodec = nullptr;
	AVCodecContext* pVideoCodecCtx = nullptr;
        //找到对应的decoder
        pVideoCodec = avcodec_find_decoder(pVideoCodecPar->codec_id);
        if (pVideoCodec == nullptr) {
            cout << "avcodec_find_decoder failed" << endl;
            return -1;
        }

        //avcodec_open2前必须使用avcodec_alloc_context3生成context
        pVideoCodecCtx = avcodec_alloc_context3(pVideoCodec);
        if (avcodec_parameters_to_context(pVideoCodecCtx, pVideoCodecPar) < 0) {
            cout << "avcodec_parameters_to_context failed" << endl;
            return -1;
        }

        //使用pVideoCodec初始化pVideoCodecCtx
        if (avcodec_open2(pVideoCodecCtx, pVideoCodec, nullptr) < 0) {
            cout << "avcodec_open2 failed" << endl;
            return -1;
        }

最后初始化AVCodecContext,即后续解码需要的上下文。

缓存数据

解码之前,我们需要一个载体来保存解码后的数据,在ffmpeg里,这个载体叫AVFrame,它必须由av_frame_alloc初始化。

代码语言:javascript
复制
    AVFrame* pFrame = nullptr;
    pFrame=av_frame_alloc();

拿到解码数据后,我们需要把它从YUV格式转换为RGB格式,转换需要用到<swscale>。相应的,我们要初始化转换上下文sws_context,以及分配保存RGB数据的buffer。

代码语言:javascript
复制
        SwsContext* pSwsCtx = nullptr;
        uint8_t* data[4] = { nullptr };
        int linesizes[4] = { 0 };
        
        //申请存储RGB的buffer
        //原先avpicture_get_size=>av_malloc=>avpicture_fill这套接口被弃用了
        //新接口 <libavutil/imgutils.h>::av_image_alloc
        if (av_image_alloc(data, linesizes, pVideoCodecCtx->width, pVideoCodecCtx->height, AV_PIX_FMT_RGB24, 16) < 0) {
            cout << "av_image_alloc failed" << endl;
            return -1;
        }
        
        //YUV2RGB前需要初始化sws_context
        pSwsCtx = sws_getContext(pVideoCodecCtx->width, pVideoCodecCtx->height, pVideoCodecCtx->pix_fmt,
            pVideoCodecCtx->width, pVideoCodecCtx->height, AV_PIX_FMT_RGB24,
            SWS_BILINEAR, nullptr, nullptr, nullptr);

读取数据

下面是读取packet,解码,转换,保存的代码:

代码语言:javascript
复制
    int count = 0;
    //读取视频包并解码,转换,保存文件
    while (av_read_frame(pFormatCtx, &packet) >= 0) {
        if (packet.stream_index == iVideoStream) {
            avcodec_send_packet(pVideoCodecCtx, &packet);
            if (avcodec_receive_frame(pVideoCodecCtx, pFrame) == 0) {

                //原先使用了pFrameRGB这个结构体作为data和linesizes的载体,这里弃用了
                sws_scale(pSwsCtx,
                    pFrame->data, pFrame->linesize, 0, pFrame->height,
                    data, linesizes);
                if (count++ % 100 == 0) {
                    saveFrame(data, linesizes, pVideoCodecCtx->width, pVideoCodecCtx->height, count);
                    cout << "save frame" << count << endl;
                }
            }
            //取消packet引用的内存,原先的av_free_packet弃用
            av_packet_unref(&packet);
        }
    }

整个流程很简单:

av_read_frame 读取packet。注意packet使用完后需要我们调用av_packet_unref来取消packet的数据引用。

avcodec_send_packet将packet送给ffmpeg解码。

avcodec_receive_frame从ffmpeg拿解码后的数据。不过不是每一个packet送过去都能拿到解码后的数据,需要根据返回值判断。

sws_scale将YUV转换为RGB。

saveFrame是我们自己实现的接口,它会将RGB保存为PPM文件。

代码语言:javascript
复制
const string kBaseDir = "../../../assets/";
void saveFrame(uint8_t** data, int* linesizes, int width, int height, int count) {
    ofstream ofs;
    string fileName;

    fileName = kBaseDir + to_string(count) + ".ppm";
    ofs.open(fileName, ios::binary);
    if (!ofs.is_open()) {
        cout << "can't open file " << fileName << endl;
        return;
    }

    //ppm header
    string header = "P6\n" + to_string(width) + " " + to_string(height) + "\n255\n";
    ofs.write(header.c_str(), header.size());
    //wiite rgb data by line
    for (int i = 0; i < height; ++i) {
        //对于RGB格式,linesize[0]即一行的字节数,有linesizes[0]==3*width
        ofs.write((const char*)data[0] + i * linesizes[0], linesizes[0]);
    }

    ofs.close();
}

完成!最后别忘了释放内存。

代码语言:javascript
复制
    av_freep(data);
    av_free(pFrame);

    avcodec_close(pVideoCodecCtx);

    avformat_close_input(&pFormatCtx);

以上就是第一章的全部内容。

代码:https://github.com/onlyandonly/ffmpeg_sdl_player

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 环境
  • 概述
  • 打开文件
  • 缓存数据
  • 读取数据
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档