FFmpeg进阶:生成视频的缩略图

news2024/10/6 22:32:28

文章目录

    • 1.读取对应位置的视频帧
    • 2.添加时间信息
    • 3.对图像进行拼接
    • 4.输出拼接图像
    • 5.显示效果

很多时候为了方便预览视频内容,我们会随机的抽取视频当中的一些帧组成一个图片作为视频的缩略图。这里介绍一下如何通过FFmpeg生成视频的缩略图。其实原理很简单,比如我们需要一个6X6的视频缩略图,也就是需要36帧。首先我们将视频按时长分成6X6+1份,这样视频中间就会出现6X6个时间节点,此时我们通过FFmpeg的seek指令跳转到对应的位置取出帧,然后将取出的帧按照顺序组合成一个图片进行输出就可以了。

1.读取对应位置的视频帧

根据视频的时长和选取帧的数量对视频进行分割,对应的实现如下:

//解码数据包
bool DecodeVideoPacket(AVPacket* pPacket, AVCodecContext* pCodecContext, AVFrame* pFrame)
{
	int ret = avcodec_send_packet(pCodecContext, pPacket);
	if (ret < 0)
	{
		return false;
	}

	ret = avcodec_receive_frame(pCodecContext, pFrame);
	if (ret != 0)
	{
		return false;
	}
	return true;
}


//缩略图的数量
int numFrames = 16;//4*4
//分割成多少份
const int numOfDivision = numFrames + 1;
int stream_count = input_format_ctx->nb_streams;

//视频的时长
int64_t timeStampLength = input_format_ctx->duration;

//每一段的时间间隔
int64_t timeStampStepSize = (double)timeStampLength / (double)numOfDivision;

//起始位置
int64_t timeStampIter = timeStampStepSize;

//跳转到对应帧的位置(注意时间单位的转换)
av_seek_frame(input_format_ctx, videoStreamIndex, (timeStampIter / 1000000) / av_q2d(pVideoStream->time_base), AVSEEK_FLAG_BACKWARD);

bool bDecodeResult = false;
int frameCounter = numFrames;

//读取数据帧
while (av_read_frame(input_format_ctx, pPacket) >= 0)
{
    static int64_t  last_pts = -1;
    if (pPacket->stream_index == videoStreamIndex)
    {
        bDecodeResult = DecodeVideoPacket(pPacket, pCodecContext, pFrame);
        if (bDecodeResult)
        {
            pFrame->pts = pFrame->best_effort_timestamp;
            //给每一帧添加时间水印
            av_buffersrc_add_frame_flags(buffersrc_ctx, pFrame, AV_BUFFERSRC_FLAG_KEEP_REF);
            //获取滤镜输出
            int ret = av_buffersink_get_frame(buffersink_ctx, pFilterFrame);
            //添加到缩略图中
            maker.addSubFrame(pFilterFrame,10,10);
            //编码之后输出
            av_frame_unref(pFilterFrame);
            if (--frameCounter <= 0) break;
        }

        //跳转到下一帧
        timeStampIter += timeStampStepSize;
        av_seek_frame(input_format_ctx, videoStreamIndex, (timeStampIter / 1000000) / av_q2d(pVideoStream->time_base), AVSEEK_FLAG_BACKWARD);
        
    }
    av_packet_unref(pPacket);
    av_frame_unref(pFrame);
}

2.添加时间信息

为了对应每一帧的时间信息,我们给每一帧图片打上时间水印方便区别。对应的实现如下所示:

//初始化滤镜
int InitFilter(AVCodecContext* codecContext,AVRational time_base)
{
	char args[512];
	int ret = 0;
	AVRational timebase = time_base;
	AVRational pixel_aspect{ 1, 1 };
	//缓存输入和缓存输出
	const AVFilter *buffersrc = avfilter_get_by_name("buffer");
	const AVFilter *buffersink = avfilter_get_by_name("buffersink");

	//创建输入输出参数
	AVFilterInOut *outputs = avfilter_inout_alloc();
	AVFilterInOut *inputs = avfilter_inout_alloc();

	//在固定的位置(100,100)绘制当前帧对应的时间
	//绘制的字体颜色为白色
	std::string  filters_descr = "drawtext=fontfile=.//msyh.ttc:fontsize=100:text='%{pts\\:gmtime\\:0\\:%H\\\\\\:%M\\\\\\:%S}':x=100:y=100:fontcolor=0xFFFFFF";
	enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P };

	//创建滤镜容器
	filter_graph = avfilter_graph_alloc();
	if (!outputs || !inputs || !filter_graph)
	{
		ret = AVERROR(ENOMEM);
		goto end;
	}

	
	//初始化数据帧的格式
	sprintf_s(args, sizeof(args),
		"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
		input_format_ctx->streams[0]->codecpar->width, codecContext->height, codecContext->pix_fmt,
		timebase.num, timebase.den,
		pixel_aspect.den, pixel_aspect.den);

	//输入数据缓存
	ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
		args, NULL, filter_graph);

	if (ret < 0) {
		goto end;
	}

	//输出数据缓存
	ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
		NULL, NULL, filter_graph);

	if (ret < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot create buffer sink\n");
		goto end;
	}

	//设置元素样式
	ret = av_opt_set_int_list(buffersink_ctx, "pix_fmts", pix_fmts,
		AV_PIX_FMT_YUV420P, AV_OPT_SEARCH_CHILDREN);
	if (ret < 0)
	{
		av_log(NULL, AV_LOG_ERROR, "Cannot set output pixel format\n");
		goto end;
	}

	//设置滤镜的端点
	outputs->name = av_strdup("in");
	outputs->filter_ctx = buffersrc_ctx;
	outputs->pad_idx = 0;
	outputs->next = NULL;

	inputs->name = av_strdup("out");
	inputs->filter_ctx = buffersink_ctx;
	inputs->pad_idx = 0;
	inputs->next = NULL;

	//初始化滤镜
	if ((ret = avfilter_graph_parse_ptr(filter_graph, filters_descr.c_str(),
		&inputs, &outputs, NULL)) < 0)
		goto end;

	//滤镜生效
	if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)
		goto end;
end:
	//释放对应的输入输出
	avfilter_inout_free(&inputs);
	avfilter_inout_free(&outputs);
	return ret;
}

3.对图像进行拼接

根据需要抽取的视频帧的数量,我们创建主图和子图对应的内存空间:

//设置每个子图宽和高
bool AllocSubFrameBuffer(const int width, const int height)
{
    if (m_sub_frame != nullptr)
    {
        return false;
    }

    int dstW = width;
    int dstH = height;

    //根据视频宽高来创建内存帧
    AVPixelFormat dstFormat = AVPixelFormat::AV_PIX_FMT_YUV420P;

    //需要指定内存对齐的大小
    int dstAlignment = 32;
    AVFrame* dstFrame = av_frame_alloc();
    if (dstFrame == nullptr)
    {
        return false;
    }
    int ret = av_image_alloc(dstFrame->data, dstFrame->linesize, dstW, dstH, dstFormat, dstAlignment);
    if (ret < 0)
    {
        return false;
    }
    dstFrame->width = dstW;
    dstFrame->height = dstH;
    dstFrame->format = dstFormat;
    m_sub_frame = dstFrame;
}

//设置主图的宽和高
bool AllocCompFrameBuffer(const int width, const int height)
{
    if (m_combine_frame != nullptr)
        return false;

    //创建主图的大小
    int compW = width;
    int compH = height;
    AVPixelFormat compFormat = AVPixelFormat::AV_PIX_FMT_YUV420P;
    int compAlignment = 32;

    AVFrame* compFrame = av_frame_alloc();
    av_image_alloc(compFrame->data, compFrame->linesize, compW, compH, compFormat, compAlignment);
    compFrame->width = compW;
    compFrame->height = compH;
    compFrame->format = compFormat;

    memset(compFrame->data[0], 0x00, compFrame->linesize[0] * compH); // Y
    memset(compFrame->data[1], 0x80, compFrame->linesize[1] * compH / 2); // U
    memset(compFrame->data[2], 0x80, compFrame->linesize[2] * compH / 2); // V

    m_combine_frame = compFrame;
}

由于从视频中获取到的视频帧和子图的大小不一致,我们需要对视频帧进行缩放:

//将传入的帧进行尺寸变换
void MapFrametoSubFrame(AVFrame* source, AVFrame* dest)
{
    int srcW = source->width;
    int srcH = source->height;
    AVPixelFormat srcFormat = (AVPixelFormat)source->format;

    int dstW = dest->width;
    int dstH = dest->height;
    AVPixelFormat dstFormat = (AVPixelFormat)dest->format;

    // sws filter operations.
    SwsContext* swsContext = sws_getContext(srcW, srcH, srcFormat, dstW, dstH, dstFormat, SWS_BICUBLIN, NULL, NULL, NULL);
    sws_scale(swsContext, source->data, source->linesize, 0, source->height, dest->data, dest->linesize);
    sws_freeContext(swsContext);
}

将缩放的视频帧组合到主图里面,这时候需要考虑到内存对齐和YUV视频帧的内存数据结构,对应的实现如下:

//将输入帧按顺序组合成合成帧
//@1输入子帧
//@2组合帧
//@3子帧在组合帧中行索引
//@4子帧在组合帧中的列索引
//@5总行数 @6总列数
void MapSubFrameToCombFrame(AVFrame* source, AVFrame* dest, int rowIndex, int colIndex, int numOfRows, int numOfCols)
{
    //假设输入输出都是YUV420P格式的数据
    //考虑内存对齐
    uint8_t* sourceY = source->data[0];
    uint8_t* sourceU = source->data[1];
    uint8_t* sourceV = source->data[2];

    uint8_t* destY = dest->data[0];
    uint8_t* destU = dest->data[1];
    uint8_t* destV = dest->data[2];

    //YUV数据中 YYYY + U + V
    //每一行中的水平方向偏移
    int offsetHorizontal = (dest->width / numOfCols) * colIndex;
    //从哪一行开始计算
    //dest->linesize[0]是经过内存对齐的每行的宽度
    int offsetVertical = ((dest->linesize[0] * dest->height) * rowIndex) / numOfRows;

    for (int i = 0; i < source->height; ++i) 
    {
        memcpy(destY + i * dest->linesize[0] + offsetHorizontal + offsetVertical, sourceY + i * source->linesize[0], source->width);
    }

    //UV
    offsetHorizontal = (dest->width / 2 * colIndex) / numOfCols;
    offsetVertical = ((dest->linesize[1] * (dest->height / 2)) * rowIndex) / numOfRows;
    for (int i = 0; i < source->height / 2; ++i) {
        memcpy(destU + i * dest->linesize[1] + offsetHorizontal + offsetVertical, sourceU + i * source->linesize[1], source->width / 2);
        memcpy(destV + i * dest->linesize[2] + offsetHorizontal + offsetVertical, sourceV + i * source->linesize[2], source->width / 2);
    }
}

实现了对应的功能模块之后,我们就可以把视频帧抽取出来组合成一个完整的图像了。

4.输出拼接图像

视频帧抽取组合完毕之后,将拼接的图像输出成一个图片,这里输出成jpg格式的图片,对应的实现如下:

//输出组合之后的图像
bool output_thumbnail_image(std::string output_path, AVFrame* frame)
{
	//创建输出上下文
	AVOutputFormat* outputFormat = av_guess_format("mjpeg", NULL, NULL);
	if (outputFormat == nullptr)
	{
		return false;
	}

	AVCodecParameters* parameters = avcodec_parameters_alloc();
	parameters->codec_id = outputFormat->video_codec;
	parameters->codec_type = AVMEDIA_TYPE_VIDEO;
	parameters->format = AV_PIX_FMT_YUVJ420P; //JPEG TYPE!
	parameters->width = frame->width;
	parameters->height = frame->height;

	AVCodec* codec = avcodec_find_encoder(parameters->codec_id);
	if (!codec)
	{
		return false;
	}

	AVCodecContext* codecContext = avcodec_alloc_context3(codec);
	if (!codecContext)
	{
		return false;
	}

	int ret = avcodec_parameters_to_context(codecContext, parameters);
	if (ret < 0) {
		return false;
	}
	codecContext->time_base = AVRational{ 1, 25 };

	codecContext->flags |= AV_CODEC_FLAG_QSCALE;
	codecContext->global_quality = FF_QP2LAMBDA * 9;

	ret = avcodec_open2(codecContext, codec, NULL);
	if (ret < 0)
	{
		return false;
	}

	ret = avcodec_send_frame(codecContext, frame);
	if (ret < 0)
	{
		return false;
	}

	AVPacket* pPacket = av_packet_alloc();
	if (!pPacket)
	{
		return false;
	}
	ret = avcodec_receive_packet(codecContext, pPacket);
	if (ret < 0)
	{
		return false;
	}

	AVFormatContext* pFormatContext = avformat_alloc_context();
	pFormatContext->oformat = outputFormat;

	ret = avio_open(&pFormatContext->pb, output_path.c_str(), AVIO_FLAG_READ_WRITE);
	if (ret < 0)
	{
		return false;
	}

	AVStream* pStream = avformat_new_stream(pFormatContext, NULL);
	*(pStream->codecpar) = *parameters;

	if (pStream == nullptr)
	{
		return false;
	}

	ret = avformat_write_header(pFormatContext, nullptr);
	if (ret < 0)
	{
		return false;
	}

	//写图片数据
	ret = av_write_frame(pFormatContext, pPacket);
	if (ret < 0)
	{
		return false;
	}
	
	//写文件尾
	av_write_trailer(pFormatContext);

	//释放资源
	avcodec_close(codecContext);
	avio_close(pFormatContext->pb);

	av_packet_unref(pPacket);
	avcodec_parameters_free(&parameters);
	avcodec_free_context(&codecContext);
	avformat_free_context(pFormatContext);
}

5.显示效果

这里以一个视频为例,分别输出了2X2,3X3,4X4的缩略图,对应的显示效果如下:
2X2缩略图

在这里插入图片描述

3X3缩略图

在这里插入图片描述

4X4缩略图
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/37215.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MyBatis association解决多对一和collection解决一对多的映射关系

多对一的映射关系 创建Emp和Dept类 1.处理多对一映射关系方式一&#xff1a;级联属性赋值 2.处理多对一映射关系方式二&#xff1a;association实现 association:处理多对一的映射关系 property:需要处理多对的映射关系的属性名 javaType:该属性的类型 3.处理多对一映射关…

Metabase学习教程:视图-6

表格视图几乎可以来做所有的事情 了解如何设置条件格式、小条形图、值格式等。 表格是数据的自然栖息地&#xff0c;对应关系数据库列和对应的行记录。它们可能不像条形图或者地图&#xff0c;但当你在很多领域工作时&#xff0c;它们往往是你所需要的。Metabase中的表可视化…

运动品牌推荐:2022年最值得入手的一些运动装备

运动是一个比较枯燥的过程&#xff0c;不断的身体重复&#xff0c;会让运动者的注意力过度的关注到自己身体的疲惫感并且放大&#xff0c;这个时候我们就可以通过外在的运动装备来消除这些疲劳感&#xff0c;提高自己的运动积极性。不过哪些运动装备好用并适合自己呢&#xff1…

服务器配置怎么查看

服务器配置怎么查看 在我们找服务器商买服务器时&#xff0c;一般都是根据自己需求来选择需要什么配置的服务器。 选服务器时主要看CPU、内存、硬盘、带宽、这几个主要配置今天艾西就教你怎么查看服务器配置 CPU、内存怎么查看&#xff1a; 方法一&#xff1a;我们远程进入服…

学术Paper写作技巧要点讲解

在国外图书馆阅读他人的学术文章的时候&#xff0c;是否发现他们英文与你的不一样&#xff1f;虽然他们的Paper与你的有相似的结构&#xff0c;即开头、正文、结论&#xff0c;但是你的写作与他们的比起来还是显得简单多了。就是类似于国内毕业Paper的写作&#xff0c;在国外学…

断点续传小解

断点续传的原理 HTTP 协议是互联网上应用最广泛网络传输协议之一&#xff0c;它基于 TCP/IP 通信协议来传递数据。断点续传的奥秘就隐藏在这 HTTP 协议中了。 我们知道HTTP请求会有一个Request header 和 Response header&#xff0c;在请求头里边有个和Range相关的参数 当下…

6种交互式内容创意帮助跨境电商卖家提高独立站商店知名度

关键词&#xff1a;跨境电商卖家、独立站商店 交互式内容是一种允许用户与之交互的内容。一些示例包括在线投票、问答环节、交互式视频和交互式计算器等交互式工具。此内容类型允许查看者通过单击或拖动项目来自定义显示方式和内容。内容还可以引导读者采取您想要的操作&#x…

【网络安全】——sql注入之云锁bypass

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

嵌入式分享合集110

一、功耗&#xff0c;成为芯片设计的头号问题 很明显&#xff0c;热量将成为半导体未来的限制因素。已经有很大一部分芯片在任何时候都是黑暗的&#xff0c;因为如果所有东西同时运行&#xff0c;所产生的热量将超过芯片和封装消散该能量的能力。如果我们现在开始考虑堆叠模具…

智能网卡的网络加速技术

2021年9月25日&#xff0c;由“科创中国”未来网络专业科技服务团指导&#xff0c;江苏省未来网络创新研究院、网络通信与安全紫金山实验室联合主办、SDNLAB社区承办的2021中国智能网卡研讨会中&#xff0c;多家机构谈到了智能网卡的网络加速实现&#xff0c;我们对此进行整理&…

金枪鱼群优化算法(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

跑步需要哪些运动装备?跑步装备选购指南

跑步是一项有氧运动&#xff0c;是富有韵律性的运动,在运动过程中&#xff0c;血液可以供给心肌足够的氧气&#xff1b;氧气能充分酵解体内的糖分&#xff0c;还可消耗体内脂肪&#xff0c;增强和改善心肺功能&#xff0c;预防骨质疏松。 而在跑步的时候选择好自己的运动装备&…

代码随想录训练营第32天|LeetCode 122.买卖股票的最佳时机II、55. 跳跃游戏、45.跳跃游戏II

参考 代码随想录 题目一&#xff1a;LeetCode 122.买卖股票的最佳时机II 按照自己的想法&#xff0c;无非就是在最低点买入&#xff0c;在最高点卖出&#xff0c;因此只需要找到成对的极小值和极大值&#xff0c;就可以计算利润了。代码实现如下&#xff1a; class Solutio…

使用姿势估计构建 姿势校正器

我们中的许多人大部分时间都在办公桌前弯腰驼背&#xff0c;身体前倾看着电脑屏幕&#xff0c;或者瘫坐在椅子上。如果你像我一样&#xff0c;只有当你的脖子或肩膀在数小时后受伤&#xff0c;或者你有偏头痛时&#xff0c;你才会想起你的不良姿势。如果有人可以提醒您坐直不是…

SpringBoot配置文件(学习笔记)

目录 一、配置文件概述 配置文件的作用 配置文件的格式 二、application.properties 配置文件 基本语法 读取配置文件 三、application.yml 配置文件 基本语法 读取yml中的配置 1、yml配置的简单读取 2、读取yml 配置中不同数据类型及 null​编辑 2、读取yml配置文…

Python基础知识入门(三)

Python基础知识入门&#xff08;一&#xff09; Python基础知识入门&#xff08;二&#xff09; 一、元组类型 元组是用英文小括号 () 把所有元素包裹起来&#xff0c;元组里面的每一个数据叫作元素。每个元素之间都要用 英文逗号 ( , ) 隔开。例如&#xff1a;(1,2,3)。 注意…

Head First设计模式(阅读笔记)-04.工厂模式

披萨订购 假设要完成披萨订购的功能&#xff0c;披萨的种类很多&#xff0c;比如 GreekPizz、CheesePizz 等&#xff0c;披萨店会根据用户需要的披萨种类制作披萨&#xff0c;制作的流程包括prepare->bake->cut->box 简单实现 下面代码的实现十分简单清晰&#xff0c;…

从0到0.1学习 lambda表达式(Java版)

编码几年时间&#xff0c;有一个东西似乎一直也逃不过去&#xff0c;那就是lambda表达式。 无论是c#&#xff0c;Python还是Java&#xff0c;lambda的思想都是共通的。但以下的语法和实例为java。 现在就来说说这个看似很难的lambda表达式 什么是lambda表达式&#xff1f; l…

【owt-server】m88分支和m59-server

OWT 单独有个webrtc的仓库,里面有m88的分支Merged Upgrade sdk to m88 for webrtc node #1026 提交记录 主干merge Merge pull request #1026 from starwarfan/mst-88webrtc-m88 目录 构建修改

【信管2.2】项目管理知识体系与组织结构

项目管理知识体系与组织结构上一次课中&#xff0c;我们已经学过了项目以及项目管理的概念&#xff0c;这些内容帮我们认识到了项目到底是个什么东西&#xff0c;有什么特点&#xff0c;和运营有什么区别等等。今天我们就继续沿着项目这件事说下去&#xff0c;我们将一起探讨一…