FFmpeg进阶: 给视频添加自定义封面


很多时候为了区分不同的视频,同时也是为了方便大家了解视频内容,我们会给视频添加一个自定义的封面。FFmpeg支持给视频添加自定义封面。但是研究了一下网上的案例,几乎所有的添加封面都是用的FFmpeg的工具实现的,这样使用起来虽然方便一些,但是集成起来很不方便,所以这里自己写了一个通过代码实现的给视频添加封面的功能。给视频添加封面的流程图如下图所示:

在这里插入图片描述

视频解封装

通过解封装,我们可以获取视频文件中的原始视频流和音频流

//打开视频文件,获取流信息
//@1文件地址 @2媒体上下文  @3音频解码器上下文  @4视频解码器上下文  @5音频流 
//@6视频流
int openVideoFile(const char *file, AVFormatContext *&formatContext, AVCodecContext *&audioContext,
	AVCodecContext *&videoContext, AVStream *&audioStream, AVStream *&videoStream) {
    
    
	int ret = 0;
	ret = avformat_open_input(&formatContext, file, nullptr, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}
	ret = avformat_find_stream_info(formatContext, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}

	for (int j = 0; j < formatContext->nb_streams; ++j) {
    
    
		if (formatContext->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    
    
			videoStream = formatContext->streams[j];
			AVCodec *codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
			videoContext = avcodec_alloc_context3(codec);
			avcodec_parameters_to_context(videoContext, videoStream->codecpar);
			avcodec_open2(videoContext, codec, nullptr);
		}
		else if (formatContext->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
    
    
			audioStream = formatContext->streams[j];
			AVCodec *codec = avcodec_find_decoder(audioStream->codecpar->codec_id);
			audioContext = avcodec_alloc_context3(codec);
			avcodec_parameters_to_context(audioContext, audioStream->codecpar);
			avcodec_open2(audioContext, codec, nullptr);
		}
		if (videoStream && audioStream) break;
	}

	if (!videoStream) 
	{
    
    
		return -1;
	}
	if (!audioContext) 
	{
    
    
		return -1;
	}

	return 0;
}

图片解封装

解析图片获取对应的图片流,作为新的视频文件里面的封面数据流

//打开图片文件,获取流信息
//@1文件地址  @2媒体上下文  @3视频解码器上下文  @4视频流
int openImageFile(const char *file, AVFormatContext *&formatContext, AVCodecContext *&videoContext,
	AVStream *&videoStream) {
    
    
	int ret = 0;
	ret = avformat_open_input(&formatContext, file, nullptr, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}
	ret = avformat_find_stream_info(formatContext, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}

	for (int j = 0; j < formatContext->nb_streams; ++j) {
    
    
		if (formatContext->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    
    
			videoStream = formatContext->streams[j];
			AVCodec *codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
			videoContext = avcodec_alloc_context3(codec);
			avcodec_parameters_to_context(videoContext, videoStream->codecpar);
			avcodec_open2(videoContext, codec, nullptr);
		}
	}
	if (!videoStream)
	{
    
    
		return -1;
	}
	return 0;
}

封装新格式

获得音视频流和封面数据流之后,我们就可以通过封装生成带封面的视频文件了,对应的实现如下:


//给视频添加自定义封面
//@1输出视频地址  @2输入视频地址 @3图片地址
int add_cover_to_video(const char *output_filename, const char *input_filename, const char *image_filename)
{
    
    
	int ret = 0;
	//各种解码器的上下文
	AVFormatContext *outFmtContext = nullptr;
	AVFormatContext *inFmtContext = nullptr;
	AVFormatContext *imageFmtContext = nullptr;
	AVCodecContext *inAudioContext = nullptr;
	AVCodecContext *inVideoContext = nullptr;
	AVCodecContext *outAudioContext = nullptr;
	AVCodecContext *imageVideoContext = nullptr;

	//音视频流信息
	AVStream *inAudioStream = nullptr;
	AVStream *inVideoStream = nullptr;
	AVStream *outAudioStream = nullptr;
	AVStream *outVideoStream = nullptr;
	AVStream *imageVideoStream = nullptr;

	AVCodec *audioCodec = nullptr;

	//打开视频文件获取上下文
	ret = openVideoFile(input_filename, inFmtContext, inAudioContext, inVideoContext, inAudioStream,
		inVideoStream);
	if (ret < 0) return ret;

	//打开图片文件获取上下文
	ret = openImageFile(image_filename, imageFmtContext, imageVideoContext, imageVideoStream);
	if (ret < 0) return ret;

	//创建输出的上下文
	ret = avformat_alloc_output_context2(&outFmtContext, nullptr, nullptr, output_filename);


	//输出视频流
	outVideoStream = avformat_new_stream(outFmtContext, nullptr);
	if (!outVideoStream) 
	{
    
    
		return -1;
	}
	outVideoStream->id = outFmtContext->nb_streams - 1;
	ret = avcodec_parameters_copy(outVideoStream->codecpar, inVideoStream->codecpar);
	if (ret < 0) 
	{
    
    
		return -1;
	}
	outVideoStream->codecpar->codec_tag = 0;

	//输出音频流
	audioCodec = avcodec_find_encoder(inAudioStream->codecpar->codec_id);
	outAudioStream = avformat_new_stream(outFmtContext, audioCodec);
	if (!outAudioStream) 
	{
    
    
		return -1;
	}
	ret = avcodec_parameters_from_context(outAudioStream->codecpar, inAudioContext);
	if (ret < 0)
	{
    
    
		return -1;
	}

	//视频封面对应的流
	AVStream* cover_stream = avformat_new_stream(outFmtContext, nullptr);
	outVideoStream->id = outFmtContext->nb_streams - 1;
	ret = avcodec_parameters_copy(cover_stream->codecpar, imageVideoStream->codecpar);
	cover_stream->disposition = AV_DISPOSITION_ATTACHED_PIC;
	av_read_frame(imageFmtContext, &cover_stream->attached_pic);
	cover_stream->attached_pic.stream_index = outFmtContext->nb_streams - 1;
	cover_stream->attached_pic.flags |= AV_PKT_FLAG_KEY;


	//拷贝原始数据
	av_dict_copy(&outFmtContext->metadata, inFmtContext->metadata, 0);
	av_dict_copy(&outVideoStream->metadata, inVideoStream->metadata, 0);
	av_dict_copy(&outAudioStream->metadata, inAudioStream->metadata, 0);
	av_dict_copy(&cover_stream->metadata, imageVideoStream->metadata, 0);

	if (!(outFmtContext->oformat->flags & AVFMT_NOFILE)) 
	{
    
    
		ret = avio_open(&outFmtContext->pb, output_filename, AVIO_FLAG_WRITE);
		if (ret < 0) 
		{
    
    
			return -1;
		}
	}


	//写文件头
	ret = avformat_write_header(outFmtContext, nullptr);
	if (ret < 0)
	{
    
    
		return -1;
	}

	//写封面对应的数据包
	ret = av_interleaved_write_frame(outFmtContext, &cover_stream->attached_pic);
	AVFrame *inputFrame = av_frame_alloc();
	do 
	{
    
    
		AVPacket packet{
    
     nullptr };
		av_init_packet(&packet);

		ret = av_read_frame(inFmtContext, &packet);
		if (ret == AVERROR_EOF)
		{
    
    
			break;
		}
		else if (ret < 0)
		{
    
    
			break;
		}

		if (packet.flags & AV_PKT_FLAG_DISCARD) continue;
		//写视频流和音频流
		if (packet.stream_index == inVideoStream->index) 
		{
    
    
			packet.stream_index = outVideoStream->index;
			av_packet_rescale_ts(&packet, inVideoStream->time_base, outVideoStream->time_base);
			packet.duration = av_rescale_q(packet.duration, inVideoStream->time_base, outVideoStream->time_base);
			packet.pos = -1;
			ret = av_interleaved_write_frame(outFmtContext, &packet);
		}
		else if (packet.stream_index == inAudioStream->index)
		{
    
    
			packet.stream_index = outAudioStream->index;
			av_packet_rescale_ts(&packet, inAudioStream->time_base, outAudioStream->time_base);
			ret = av_interleaved_write_frame(outFmtContext, &packet);
		}

	} while (true);

	av_write_trailer(outFmtContext);

	if (!(outFmtContext->oformat->flags & AVFMT_NOFILE)) 
	{
    
    
		avio_closep(&outFmtContext->pb);
	}

	//清理分配之后的数据
	av_frame_free(&inputFrame);
	avformat_free_context(outFmtContext);
	avformat_free_context(inFmtContext);
	avformat_free_context(imageFmtContext);
	avcodec_free_context(&inAudioContext);
	avcodec_free_context(&inVideoContext);
	avcodec_free_context(&imageVideoContext);

	return 0;
}

示例效果图

需要注意的是,有的视频封装格式可能只支持一个视频流和一个音频流,没有办法添加封面流,这时候这种通过流的方式添加封面的方法可能就不起作用了。

自定义封面如下所示:
在这里插入图片描述

原始视频和添加封面的视频如下所示:

在这里插入图片描述

工程代码

extern "C" 
{
    
    
#include <libavformat/avformat.h>
#include <libavutil/timestamp.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
}

#include <string>

//打开图片文件,获取流信息
//@1文件地址  @2媒体上下文  @3视频解码器上下文  @4视频流
int openImageFile(const char *file, AVFormatContext *&formatContext, AVCodecContext *&videoContext,
	AVStream *&videoStream) {
    
    
	int ret = 0;
	ret = avformat_open_input(&formatContext, file, nullptr, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}
	ret = avformat_find_stream_info(formatContext, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}

	for (int j = 0; j < formatContext->nb_streams; ++j) {
    
    
		if (formatContext->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    
    
			videoStream = formatContext->streams[j];
			AVCodec *codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
			videoContext = avcodec_alloc_context3(codec);
			avcodec_parameters_to_context(videoContext, videoStream->codecpar);
			avcodec_open2(videoContext, codec, nullptr);
		}
	}
	if (!videoStream)
	{
    
    
		return -1;
	}
	return 0;
}

//打开视频文件,获取流信息
//@1文件地址 @2媒体上下文  @3音频解码器上下文  @4视频解码器上下文  @5音频流 
//@6视频流
int openVideoFile(const char *file, AVFormatContext *&formatContext, AVCodecContext *&audioContext,
	AVCodecContext *&videoContext, AVStream *&audioStream, AVStream *&videoStream) {
    
    
	int ret = 0;
	ret = avformat_open_input(&formatContext, file, nullptr, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}
	ret = avformat_find_stream_info(formatContext, nullptr);
	if (ret < 0) 
	{
    
    
		return -1;
	}

	for (int j = 0; j < formatContext->nb_streams; ++j) {
    
    
		if (formatContext->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
    
    
			videoStream = formatContext->streams[j];
			AVCodec *codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
			videoContext = avcodec_alloc_context3(codec);
			avcodec_parameters_to_context(videoContext, videoStream->codecpar);
			avcodec_open2(videoContext, codec, nullptr);
		}
		else if (formatContext->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
    
    
			audioStream = formatContext->streams[j];
			AVCodec *codec = avcodec_find_decoder(audioStream->codecpar->codec_id);
			audioContext = avcodec_alloc_context3(codec);
			avcodec_parameters_to_context(audioContext, audioStream->codecpar);
			avcodec_open2(audioContext, codec, nullptr);
		}
		if (videoStream && audioStream) break;
	}

	if (!videoStream) 
	{
    
    
		return -1;
	}
	if (!audioContext) 
	{
    
    
		return -1;
	}

	return 0;
}

//给视频添加自定义封面
//@1输出视频地址  @2输入视频地址 @3图片地址
int add_cover_to_video(const char *output_filename, const char *input_filename, const char *image_filename)
{
    
    
	int ret = 0;
	//各种解码器的上下文
	AVFormatContext *outFmtContext = nullptr;
	AVFormatContext *inFmtContext = nullptr;
	AVFormatContext *imageFmtContext = nullptr;
	AVCodecContext *inAudioContext = nullptr;
	AVCodecContext *inVideoContext = nullptr;
	AVCodecContext *outAudioContext = nullptr;
	AVCodecContext *imageVideoContext = nullptr;

	//音视频流信息
	AVStream *inAudioStream = nullptr;
	AVStream *inVideoStream = nullptr;
	AVStream *outAudioStream = nullptr;
	AVStream *outVideoStream = nullptr;
	AVStream *imageVideoStream = nullptr;

	AVCodec *audioCodec = nullptr;

	//打开视频文件获取上下文
	ret = openVideoFile(input_filename, inFmtContext, inAudioContext, inVideoContext, inAudioStream,
		inVideoStream);
	if (ret < 0) return ret;

	//打开图片文件获取上下文
	ret = openImageFile(image_filename, imageFmtContext, imageVideoContext, imageVideoStream);
	if (ret < 0) return ret;

	//创建输出的上下文
	ret = avformat_alloc_output_context2(&outFmtContext, nullptr, nullptr, output_filename);


	//输出视频流
	outVideoStream = avformat_new_stream(outFmtContext, nullptr);
	if (!outVideoStream) 
	{
    
    
		return -1;
	}
	outVideoStream->id = outFmtContext->nb_streams - 1;
	ret = avcodec_parameters_copy(outVideoStream->codecpar, inVideoStream->codecpar);
	if (ret < 0) 
	{
    
    
		return -1;
	}
	outVideoStream->codecpar->codec_tag = 0;

	//输出音频流
	audioCodec = avcodec_find_encoder(inAudioStream->codecpar->codec_id);
	outAudioStream = avformat_new_stream(outFmtContext, audioCodec);
	if (!outAudioStream) 
	{
    
    
		return -1;
	}
	ret = avcodec_parameters_from_context(outAudioStream->codecpar, inAudioContext);
	if (ret < 0)
	{
    
    
		return -1;
	}

	//视频封面对应的流
	AVStream* cover_stream = avformat_new_stream(outFmtContext, nullptr);
	outVideoStream->id = outFmtContext->nb_streams - 1;
	ret = avcodec_parameters_copy(cover_stream->codecpar, imageVideoStream->codecpar);
	cover_stream->disposition = AV_DISPOSITION_ATTACHED_PIC;
	av_read_frame(imageFmtContext, &cover_stream->attached_pic);
	cover_stream->attached_pic.stream_index = outFmtContext->nb_streams - 1;
	cover_stream->attached_pic.flags |= AV_PKT_FLAG_KEY;


	//拷贝原始数据
	av_dict_copy(&outFmtContext->metadata, inFmtContext->metadata, 0);
	av_dict_copy(&outVideoStream->metadata, inVideoStream->metadata, 0);
	av_dict_copy(&outAudioStream->metadata, inAudioStream->metadata, 0);
	av_dict_copy(&cover_stream->metadata, imageVideoStream->metadata, 0);

	if (!(outFmtContext->oformat->flags & AVFMT_NOFILE)) 
	{
    
    
		ret = avio_open(&outFmtContext->pb, output_filename, AVIO_FLAG_WRITE);
		if (ret < 0) 
		{
    
    
			return -1;
		}
	}


	//写文件头
	ret = avformat_write_header(outFmtContext, nullptr);
	if (ret < 0)
	{
    
    
		return -1;
	}

	//写封面对应的数据包
	ret = av_interleaved_write_frame(outFmtContext, &cover_stream->attached_pic);
	AVFrame *inputFrame = av_frame_alloc();
	do 
	{
    
    
		AVPacket packet{
    
     nullptr };
		av_init_packet(&packet);

		ret = av_read_frame(inFmtContext, &packet);
		if (ret == AVERROR_EOF)
		{
    
    
			break;
		}
		else if (ret < 0)
		{
    
    
			break;
		}

		if (packet.flags & AV_PKT_FLAG_DISCARD) continue;
		//写视频流和音频流
		if (packet.stream_index == inVideoStream->index) 
		{
    
    
			packet.stream_index = outVideoStream->index;
			av_packet_rescale_ts(&packet, inVideoStream->time_base, outVideoStream->time_base);
			packet.duration = av_rescale_q(packet.duration, inVideoStream->time_base, outVideoStream->time_base);
			packet.pos = -1;
			ret = av_interleaved_write_frame(outFmtContext, &packet);
		}
		else if (packet.stream_index == inAudioStream->index)
		{
    
    
			packet.stream_index = outAudioStream->index;
			av_packet_rescale_ts(&packet, inAudioStream->time_base, outAudioStream->time_base);
			ret = av_interleaved_write_frame(outFmtContext, &packet);
		}

	} while (true);

	av_write_trailer(outFmtContext);

	if (!(outFmtContext->oformat->flags & AVFMT_NOFILE)) 
	{
    
    
		avio_closep(&outFmtContext->pb);
	}

	//清理分配之后的数据
	av_frame_free(&inputFrame);
	avformat_free_context(outFmtContext);
	avformat_free_context(inFmtContext);
	avformat_free_context(imageFmtContext);
	avcodec_free_context(&inAudioContext);
	avcodec_free_context(&inVideoContext);
	avcodec_free_context(&imageVideoContext);

	return 0;
}

int main(int argc, char* argv[])
{
    
    
	if (argc != 4)
	{
    
    
		return -1;
	}
	std::string video_input = std::string(argv[1]);    //视频输入地址
	std::string image_input = std::string(argv[2]);    //图片地址
	std::string video_output = std::string(argv[3]);   //视频输出

	add_cover_to_video(video_output.c_str(), video_input.c_str(), image_input.c_str());
}

命令行调用

如果有的时候应急,我们也可以直接通过FFmpeg的工具实现添加封面的功能,对应的实现如下:

ffmpeg -i 封面.png -i 原始视频.mp4 -map 1:0 -map 1:1 -map 0:0 -c copy -disposition:2 attached_pic 添加封面视频.mp4

猜你喜欢

转载自blog.csdn.net/yang1fei2/article/details/128158749