ffmpeg使用滤镜叠加png图片logo到视频(C++代码实现)

近期学习了ffmpeg的滤镜,并使用ffmpeg滤镜完成了一系列相关的功能,现写博客总结下。

第一个功能便是将png图片作为logo叠加到ffmpeg解码后的视频画面上。效果图如下:

 

左上角便是叠加的logo图片,位置可根据传入的参数来设置。

 

首先对ffmpeg滤镜AVFilter做个简单介绍。

滤镜

在多媒体处理中,术语 滤镜(filter) 指的是修改未编码的原始音视频数据帧的一种软件工具。滤镜分为音频滤镜和视频滤镜。FFmpeg 提供了很多内置滤镜,可以用很多方式将这些滤镜组合使用。通过一些复杂指令,可以将解码后的帧从一个滤镜引向另一个滤镜。这简化了媒体处理,因为有损编解码器对媒体流进行多次解码和编码操作会降低总体质量,而引入滤镜后,不需要多次解码编码操作,相关处理可以使用多个滤镜完成,而滤镜处理的是原始数据,不会造成数据损伤。

滤镜的使用

FFmpeg 的 libavfilter 库提供了滤镜 API,支持多路输入和多路输出。

滤镜(filter)的语法为:

[input_link_lable1][input_link_lable2]... filter_name=parameters [output_link_lable1][output_link_lable12]...

上述语法中,输入输出都有连接标号(link lable),连接符号是可选项,输入连接标号表示滤镜的输入,输出连接标号表示滤镜的输出。连接标号通常用在滤镜图中,通常前一个滤镜的输出标号会作为后一个滤镜的输入标号,通过同名的标号将滤镜及滤镜链连接起来。连接标号的用法参考 4.3.2 节示例。

示例1:

ffplay -f lavfi -i testsrc -vf transpose=1

“-vf”(同“-filter:v”)选项表示使用视频滤镜,“transpose=1” 是滤镜名称及参数,此行命令表示使用 transpose 视频滤镜产生一个顺时针旋转 90 度的测试图案

示例2:

ffmpeg -i input.mp3 -af atempo=0.8 output.mp3

“-af”(同“-filter:a”)选项表示使用音频滤镜,“atempo=0.8” 是滤镜名称及参数,此行命令表示使用 atempo 音频滤镜将输入音频速率降低到 80% 后写入输出文件

注意:有些滤镜只会修改帧属性而不会修改帧内容。例如,fps 滤镜,setpts 滤镜等。 滤镜链的使用

滤镜链(filterchain) 是以逗号分隔的滤镜(filter)序列,语法如下:

filter1,fiter2,filter3,...,filterN-2,filterN-1,filterN

滤镜链中如果有空格,需要将滤镜链用双引号括起来,因为命令行中空格用于分隔参数。

示例1:

ffmpeg -i input.mpg -vf hqdn3d,pad=2*iw output.mp4

“hqdn3d,pad=2*iw” 是 filterchain,此 filterchain 中第一个 filter 是 “hqdn3d”(降噪);第二个 filter 是 “pad=2*iw”(将图像宽度填充到输入宽度的 2 倍)。此行命令表示,将输入视频经降噪处理后,再填充视频宽度为输入宽度的 2 倍。

滤镜图的使用

滤镜图(filtergraph) 通常是以分号分隔的滤镜链(filterchain)序列。滤镜图分为简单滤镜图和复杂滤镜图。

滤镜图(filtergraph)的语法如下:

filter1;fiter2;filter3;...;filterN-2;filterN-1;filterN

简单滤镜图

简单滤镜图只能处理单路输入流和单路输出流,而且要求输入和输出具有相同的流类型。

简单滤镜图由 “-filter” 选项指定。简单滤镜图示意图如下:

 

 input  --->  simple filter graph  --->  output 

 

 

复杂滤镜图

复杂滤镜图用于简单滤镜图处理不了的场合。比如,多路输入流和(或)多路输出流,或者输出流与输入流类型不同。

有些特殊的滤镜(filter)本身就属于复杂滤镜图,用 “-filter_complex” 选项或 “-lavfi” 选项指定,如 overlay 滤镜和 amix 滤镜就是复杂滤镜图。overlay 滤镜有两个视频输入和一个视频输出,将两个输入视频混合在一起。而 amix 滤镜则是将两个输入音频混合在一起。

复杂滤镜图示意图如下:

input 0 ----------| complex |----------output0
                         |                |
intput1---------- | filter        | 
                         |                |
intput2---------- | graph     |-----------output1

■ 示例1:

ffmpeg -i INPUT -vf "split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2" OUTPUT

上例中 “split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2” 是复杂滤镜图,由三个滤镜链构成(分号分隔),第二个滤镜链 “[tmp] crop=iw:ih/2:0:0, vflip [flip]” 由两个滤镜构成(逗号分隔)。第一个滤镜链中:滤镜 split 产生两个输出 [main] 和 [tmp];第二个滤镜链中:[tmp] 作为 crop 滤镜的输入,[flip] 作为 vflip 滤镜的输出,crop 滤镜输出连接到 vflip 滤镜的输入;第三个滤镜链中:[main] 和 [flip] 作为 overlay 滤镜的输入。整行命令实现的功能是:将输入分隔为两路,其中一路经过裁剪和垂直翻转后,再与另一路混合,生成输出文件。示意图如下所示:

[main] input --> split ---------------------> overlay --> output | ^ |[tmp] [flip]| +-----> crop --> vflip -------+

滤镜图中的连接标号

在滤镜图中可以使用连接标号(link lable),连接标号表示特定滤镜/滤镜链的输入或输出,参 考上节。

例如,我们想要把一个经过降噪处理后的输出文件与输入原文件进行比较,如果不使用带连接标号的滤镜图,我们需要至少两条命令: ffmpeg -i input.mpg -vf hqdn3d,pad=2*iw output.mp4` ffmpeg -i output.mp4 -i input.mpg -filter_complex overlay=w compare.mp4

如果使用带有连接标号的滤镜图,则一条命令就可以了:

ffplay -i i.mpg -vf split[a][b];[a]pad=2*iw[A];[b]hqdn3d[B];[A][B]overlay=w

滤镜使用总结

滤镜(广义)通常以滤镜链(filterchain, 以逗号分隔的滤镜序列)和滤镜图(filtergraph, 以分号分隔的滤镜序列)的形式使用。滤镜链由滤镜构成,滤镜图由滤镜链构成,这样可以提供复杂多样的组合方式以应对不同的应用场景。

滤镜(狭义)是滤镜链的简单特例,滤镜链是滤镜图的简单特例。比如,一个滤镜图可以只包含一个滤镜链,而一个滤镜链也可以只包含一个滤镜,这种特例情况下,一个滤镜图仅由单个滤镜构成。

FFmpeg 的命令行中,滤镜(广义)的出现形式有滤镜(狭义)、滤镜链、滤镜图三种形式,但滤镜(狭义)和滤镜链可以看作是特殊的滤镜图,因此,为了简便,FFmpeg 的命令行中滤镜相关选项,只针对滤镜图(filtergraph)概念,分为如下两类:

  • 针对简单滤镜图的选项:“-vf” 等同 “-filter:v”,“-af” 等同 “-filter:a”
  • 针对复杂滤镜图的选项:“-lavfi” 等价于 “-filter_complex”

 

关于滤镜的概念及说明网络也亦有不少教程,笔者参考的博客如下:

https://www.cnblogs.com/leisure_chn/p/10297002.html

感谢这位大神的无私分享。

 

另外,我发现网络上关于AVFilter的连接和使用,多数用的是滤镜字符串的构造和解析 ,我认为这样的方式比起使用接口来连接,灵活性稍微较差。如果滤镜链字符串过于复杂,则很容易写错,且排查问题所花时间有时会很长。故我在写了很多个demo验证后,发现还是使用AVFilter自带的接口来连接和配置滤镜更为方便。

 

使用AVFilter的步骤如下

创建、连接和配置滤镜:

1、使用avfilter_get_by_name()接口获取相关的滤镜;

2、使用avfilter_graph_alloc()分配一个AVFilterGraph结构;

3、使用avfilter_graph_create_filter()创建buffer滤镜,该滤镜是滤镜链的入,即音视频帧的输入;

4、使用avfilter_graph_create_filter()创建buffersink滤镜,该滤镜是滤镜链的出,即音视频帧的输出;

5、创建其它所需功能的滤镜;

6、最关键的一步,使用avfilter_link()连接各个滤镜,从输入滤镜---其它滤镜---输出滤镜进行连接。这一步是和使用字符串方式的滤镜的最大区别;

7、使用avfilter_graph_config()接口配置滤镜。

以上所有步骤接口调用成功后方可使用滤镜。

使用滤镜:

1、使用av_buffersrc_add_frame()将视频帧或音频帧(如果滤镜需要的话)加入到滤镜图中;

2、使用av_buffersink_get_frame()接口获取从滤镜图中过滤后的帧。此时得到的视频帧或音频帧即是所需要的帧了。

 

通过以上的简单介绍,学习了滤镜的概念和使用步骤。下面以一个例子和代码来详细说明滤镜的使用。

例子的功能为解码一个mp4视频,将一个png图片作为一个logo叠加到视频的左上角,如开头的图所示。

代码的功能步骤为:

1、打开MP4文件,获取媒体信息,找到解码器;

2、创建、连接和配置movie和叠加相关滤镜;

3、创建SDL渲染窗口;

4、解码,获取到原始视频帧;

5、将视频帧加入到滤镜,并从滤镜中获取叠加后的帧;

6、将叠加logo后的帧进行渲染,在窗口上看到视频。

 

从PNG图片或别的视频中获取流的滤镜为movie滤镜,ffmpeg对movie滤镜的介绍如下:

Read audio and/or video stream(s) from a movie container.

 

It accepts the following parameters:

 

filename

The name of the resource to read (not necessarily a file; it can also be a device or a stream accessed through some protocol).

 

format_name, f

Specifies the format assumed for the movie to read, and can be either the name of a container or an input device. If not specified, the format is guessed from movie_name or by probing.

 

seek_point, sp

Specifies the seek point in seconds. The frames will be output starting from this seek point. The parameter is evaluated with av_strtod, so the numerical value may be suffixed by an IS postfix. The default value is "0".

 

streams, s

Specifies the streams to read. Several streams can be specified, separated by "+". The source will then have as many outputs, in the same order. The syntax is explained in the (ffmpeg)"Stream specifiers" section in the ffmpeg manual. Two special names, "dv" and "da" specify respectively the default (best suited) video and audio stream. Default is "dv", or "da" if the filter is called as "amovie".

 

stream_index, si

Specifies the index of the video stream to read. If the value is -1, the most suitable video stream will be automatically selected. The default value is "-1". Deprecated. If the filter is called "amovie", it will select audio instead of video.

 

loop

Specifies how many times to read the stream in sequence. If the value is 0, the stream will be looped infinitely. Default value is "1".

 

Note that when the movie is looped the source timestamps are not changed, so it will generate non monotonically increasing timestamps.

 

discontinuity

Specifies the time difference between frames above which the point is considered a timestamp discontinuity which is removed by adjusting the later timestamps.

 

It allows overlaying a second video on top of the main input of a filtergraph, as shown in this graph:

 

input -----------> deltapts0 --> overlay --> output

^

|

movie --> scale--> deltapts1 -------+

 

ffmpeg对于滤镜介绍的文档链接为:

http://ffmpeg.org/ffmpeg-filters.html 从这里可以查找到所有滤镜的说明和使用。

 

该记录的都记录完毕,以下是样例的代码:

#include <windows.h>
#include <iostream>
#include <string>
#include <thread>

extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/buffersink.h"
#include "libavfilter/buffersrc.h"
#include "libavutil/opt.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"

#include "SDL.h"
}


#define SYSPAUSE system("pause")

#undef main 

// 视频解码器相关
AVFormatContext*		format_ctx = nullptr;
AVCodecContext*			video_codec_ctx = nullptr;
int						video_codec_index = 0;

// 滤镜相关
AVFilterContext*        filter_buffersrc_ctx = nullptr;		// buffersrc 滤镜上下文
AVFilterContext*        filter_buffersink_ctx = nullptr;	// buffersink 滤镜上下文
AVFilterContext*		filter_movie_ctx = nullptr;			// move 滤镜上下文
AVFilterContext*        filter_overlay_ctx = nullptr;		// overlay 滤镜上下文
AVFilterGraph*          filter_graph = nullptr;				// 滤镜图

int						n_width = 0;						// 视频宽
int						n_height = 0;						// 视频高

// SDL相关
SDL_Window*				SDLWindow = nullptr;
SDL_Renderer*           SDLRender = nullptr;
SDL_Texture*            SDLTexture = nullptr;
SDL_Rect                SDLRect;
SDL_Thread*             SDLThread = nullptr;
SDL_Event               SDLEvent;

int						n_fps = 0;						// 视频帧率

#define SFM_REFRESH_EVENT  (SDL_USEREVENT + 1) 
#define SFM_BREAK_EVENT  (SDL_USEREVENT + 2) 
int						n_thread_exit = 0;				// 线程结束标志
int						n_thread_pause = 0;				// 线程暂停标志

static int open_media_file(const char* file_name);	// 打开视频媒体文件
static int init_filters(const std::string& logo_file);// 初始化滤镜,创建、配置并连接滤镜
static int init_SDL();			// 初始化SDL,创建渲染窗口
static int sfp_refresh_thread(void* opaque);	// SDL线程函数


int open_media_file(const char* file_name)
{
	if (!file_name || !file_name[0])
		return -1;

	int ret = -2;
	ret = avformat_open_input(&format_ctx, file_name, nullptr, nullptr);
	if (ret < 0)
		return ret;

	ret = avformat_find_stream_info(format_ctx, nullptr);
	if (ret < 0)
	{
		avformat_free_context(format_ctx);
		return ret;
	}

	video_codec_ctx = avcodec_alloc_context3(nullptr);
	bool bSus = true;
	for (int i = 0; i < format_ctx->nb_streams; i++)
	{
		AVStream *stream = format_ctx->streams[i];
		AVCodecParameters *codecParam = stream->codecpar;

		if (codecParam->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			AVCodec* codec = avcodec_find_decoder(codecParam->codec_id);
			if (!codec)
				continue;
			video_codec_index = i;
			avcodec_parameters_to_context(video_codec_ctx, stream->codecpar);
			ret = avcodec_open2(video_codec_ctx, codec, nullptr);
			if (ret < 0)
			{
				std::cout << "打开视频解码器失败" << std::endl;
				bSus = false;
				break;
			}
		}
	}

	if (!bSus)
	{
		avformat_free_context(format_ctx);
		return -2;
	}

	n_width = video_codec_ctx->width;
	n_height = video_codec_ctx->height;

	return 0;
}

int init_filters(const std::string& logo_file)
{
	if (logo_file.empty())
		return -1;

	const AVFilter* buffer_src = avfilter_get_by_name("buffer");
	const AVFilter* buffer_sink = avfilter_get_by_name("buffersink");
	const AVFilter* movie = avfilter_get_by_name("movie");
	const AVFilter* overlay = avfilter_get_by_name("overlay");

	int ret = -1;
	filter_graph = avfilter_graph_alloc();		// 首先分配一个AVFilterGraph
	if (!filter_graph)
	{
		ret = AVERROR(ENOMEM);
		goto FILTER_END;
	}

	AVRational video_time_base = format_ctx->streams[video_codec_index]->time_base;
	char video_args[512] = { 0 };
	snprintf(video_args, sizeof(video_args),
		"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
		video_codec_ctx->width, 
		video_codec_ctx->height, 
		video_codec_ctx->pix_fmt,
		video_time_base.num, video_time_base.den,
		video_codec_ctx->sample_aspect_ratio.num, 
		video_codec_ctx->sample_aspect_ratio.den);

	// 创建滤镜
	// 创建buffersrc滤镜上下文
	ret = avfilter_graph_create_filter(&filter_buffersrc_ctx, buffer_src, "in",
		video_args, nullptr, filter_graph);
	if (ret < 0)
	{
		goto FILTER_END;
	}

	enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_YUV420P, AV_PIX_FMT_NONE };
	AVBufferSinkParams* sink_params = av_buffersink_params_alloc();
	sink_params->pixel_fmts = pix_fmts;			// 输出画面指定为 YUV420P
	ret = avfilter_graph_create_filter(&filter_buffersink_ctx, buffer_sink, "out",
		nullptr, sink_params, filter_graph);
	if (ret < 0) 
	{
		goto FILTER_END;
	}

	ret = avfilter_graph_create_filter(&filter_movie_ctx, movie, "movie",
		logo_file.c_str(), nullptr, filter_graph);
	if (ret < 0)
	{
		goto FILTER_END;
	}

	ret = avfilter_graph_create_filter(&filter_overlay_ctx, overlay, "overlay",
		"x=0:y=0", nullptr, filter_graph);	// x=0,y=0出叠加图片logo
	if (ret < 0)
	{
		goto FILTER_END;
	}

	// 连接滤镜
	// 将输入滤镜连接到overlay滤镜,最后一个参数dstpad传入0
	ret = avfilter_link(filter_buffersrc_ctx, 0, filter_overlay_ctx, 0);
	if (ret < 0)
	{
		goto FILTER_END;
	}
	// 将logo的滤镜连接到overlay滤镜,最后一个参数dstpad传入1
	ret = avfilter_link(filter_movie_ctx, 0, filter_overlay_ctx, 1);
	if (ret < 0)
	{
		goto FILTER_END;
	}
	// 将叠加滤镜画面连接到输出
	ret = avfilter_link(filter_overlay_ctx, 0, filter_buffersink_ctx, 0);
	if (ret < 0)
	{
		goto FILTER_END;
	}

	// 配置滤镜,成功则完成了滤镜配置
	ret = avfilter_graph_config(filter_graph, nullptr);
	if (ret < 0)
	{
		goto FILTER_END;
	}
	return ret;

FILTER_END:
	if (filter_graph != nullptr)
	{
		avfilter_graph_free(&filter_graph);
		filter_graph = nullptr;
	}

	return ret;
}

int init_SDL()
{
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER))
	{
		std::cout << "初始化SDL失败" << std::endl;
		return -1;
	}


	int n_screen_width = 1280;
	int n_screen_height = 720;

	SDLWindow = SDL_CreateWindow("Logo Overlay Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		n_screen_width, n_screen_height, SDL_WINDOW_OPENGL);
	if (SDLWindow == nullptr)
	{
		std::cout << "初始化SDL窗口失败" << std::endl;
		goto SDL_END;
	}

	SDLRender = SDL_CreateRenderer(SDLWindow, -1, 0);
	if (SDLRender == nullptr)
	{
		goto SDL_END;
	}
	SDLTexture = SDL_CreateTexture(SDLRender, 
		SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING,
		n_width, n_height);
	SDLRect.x = SDLRect.y = 0;
	SDLRect.w = n_width;
	SDLRect.h = n_height;

	if (video_codec_ctx->framerate.den != 0)
		n_fps = video_codec_ctx->framerate.num / video_codec_ctx->framerate.den;
	else
		n_fps = 25;

	n_fps = n_fps == 0 ? 25 : n_fps;
	SDLThread = SDL_CreateThread(sfp_refresh_thread, nullptr, nullptr);

	return 0;

SDL_END:
	if (SDLTexture)
	{
		SDL_DestroyTexture(SDLTexture);
		SDLTexture = nullptr;
	}
	if (SDLRender)
	{
		SDL_DestroyRenderer(SDLRender);
		SDLRender = nullptr;
	}
	if (SDLWindow)
	{
		SDL_DestroyWindow(SDLWindow);
		SDLWindow = nullptr;
	}
	return -1;
}

int sfp_refresh_thread(void* opaque)
{
	n_thread_exit = 0;
	n_thread_pause = 0;
	while (!n_thread_exit)
	{
		if (!n_thread_pause)
		{
			SDL_Event event;
			event.type = SFM_REFRESH_EVENT;
			SDL_PushEvent(&event);
		}
		SDL_Delay(n_fps);
	}
	n_thread_exit = 0;
	n_thread_pause = 0;
	//Break	
	SDL_Event event;
	event.type = SFM_BREAK_EVENT;
	SDL_PushEvent(&event);
	return 0;
}

int main(int argc, char** argv)
{
	char* szMediaFile = "test.mp4";
	int ret = 0;

	ret = open_media_file(szMediaFile);
	if (ret < 0)
	{
		std::cout << "打开文件失败" << std::endl;
		goto END;
	}

	ret = init_filters("logo.png");
	if (ret < 0)
	{
		std::cout << "配置滤镜失败" << std::endl;
		goto END;
	}

	AVPacket avPacket;
	AVFrame* videoFrame = nullptr;
	AVFrame* filterFrame = nullptr;

	videoFrame = av_frame_alloc();  // 分配解码后的视频帧结构
	filterFrame = av_frame_alloc(); // 从滤镜中获取的帧结构

	if (init_SDL() < 0)
	{
		std::cout << "创建SDL实例失败" << std::endl;
		goto END;
	}

	while (true)
	{
		SDL_WaitEvent(&SDLEvent);
		if (SDLEvent.type == SFM_REFRESH_EVENT)
		{
			bool bVideoCodec = true;
			while (1)
			{
				if ((ret = av_read_frame(format_ctx, &avPacket)) < 0)
				{
					n_thread_exit = 1;
				}
				if (avPacket.stream_index == video_codec_index)
				{
					bVideoCodec = true;
					break;
				}
			}

			if (bVideoCodec)
			{
				// 解码,发送读取到的包数据
				ret = avcodec_send_packet(video_codec_ctx, &avPacket);
				if (ret < 0)
				{
					printf("send packet failed.");
					return -1;
				}

				// 接收帧
				ret = avcodec_receive_frame(video_codec_ctx, videoFrame);
				if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
					continue;
				}
				else if (ret < 0) 
				{
					av_log(nullptr, AV_LOG_ERROR, "Error while receiving a frame from the decoder\n");
					return -2;
				}

				videoFrame->pts = videoFrame->best_effort_timestamp;

			}

			// 将原始视频帧加入到滤镜
			if (av_buffersrc_add_frame(filter_buffersrc_ctx, videoFrame) < 0) 
			{
				av_log(nullptr, AV_LOG_ERROR, "Error while feeding the filtergraph\n");
				break;
			}
			
			while (1)
			{
				// 从滤镜中获取最终的结果帧
				ret = av_buffersink_get_frame(filter_buffersink_ctx, filterFrame);
				if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
					break;

				SDL_UpdateYUVTexture(SDLTexture, &SDLRect,
					filterFrame->data[0], filterFrame->linesize[0],
					filterFrame->data[1], filterFrame->linesize[1],
					filterFrame->data[2], filterFrame->linesize[2]);

				SDL_RenderClear(SDLRender);
				SDL_RenderCopy(SDLRender, SDLTexture, nullptr, nullptr);
				SDL_RenderPresent(SDLRender);

				break;
			}

			std::this_thread::sleep_for(std::chrono::milliseconds(1000 / n_fps));

			av_frame_unref(filterFrame);
			av_frame_unref(videoFrame);
			av_packet_unref(&avPacket);
		}
		else if (SDLEvent.type == SDL_KEYDOWN)
		{
			if (SDLEvent.key.keysym.sym == SDLK_SPACE)
				n_thread_pause = !n_thread_pause;
		}
		else if (SDLEvent.type == SDL_QUIT)
		{
			n_thread_exit = 1;
		}
		else if (SDLEvent.type == SFM_BREAK_EVENT)
		{
			break;
		}
	}

END:
	if (filter_graph != nullptr)
	{
		avfilter_graph_free(&filter_graph);
		filter_graph = nullptr;
	}
	if (video_codec_ctx != nullptr)
	{
		avcodec_free_context(&video_codec_ctx);
		video_codec_ctx = nullptr;
	}
	if (format_ctx != nullptr)
	{
		avformat_close_input(&format_ctx);
		avformat_free_context(format_ctx);
		format_ctx = nullptr;
	}

	if (videoFrame != nullptr)
	{
		av_frame_free(&videoFrame);
		videoFrame = nullptr;
	}
	if (filterFrame != nullptr)
	{
		av_frame_free(&filterFrame);
		filterFrame = nullptr;
	}

	system("pause");
	return 0;
}

代码资源链接:https://download.csdn.net/download/explorer114/12740253

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值