FFmepg入门:最简单的音视频转码工具

FFmepg入门:最简单的音视频转码工具

最近工作真的巨忙,上周给我忙晕了,好久没来更新FFmpeg的相关学习了,最近简单写一个视频的重编码工具;可以根据输入的宽高以及编码器输出到指定的文件中进行预览。接下来就进入正文吧。

0402更新音频重编码

补充:音频重编码步骤

大家好呀,好久没有更新了,最近研究了一下MAC的程序开发,发现真的剧难,想了想咱们这种菜鸡还是用SDL吧哈哈,swift真的不是我等人用的来的。

今天把音频重编码的步骤补上,代码融合在了视频重编码的代码中,可以看最后的源码部分。话不多说,我们直接开始。

1. 输入流准备

重要的一些实体,AVCodecContext. AVCodec, audio_stream

audio_stream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, &acodec, 0);

acodeCtx = avcodec_alloc_context3(acodec);
	avcodec_parameters_to_context(acodeCtx, formatCtx->streams[audio_stream]->codecpar);

acodec = avcodec_find_decoder(acodeCtx->codec_id);

if (avcodec_open2(acodeCtx, acodec, NULL) < 0) {
	printf("打开解码器失败");
}

2. 输出流准备工作

编码器encoder

aoutput_codec = avcodec_find_encoder_by_name(acodec_name);
if (!aoutput_codec) {
	printf("输入的编码器有误");
}
int open = avcodec_open2(aoutput_codeCtx, aoutput_codec, NULL);
if (open < 0){
	printf("打开编码器有误: %s\n", av_err2str(open));
}

输出AVCodecContext

必须参数

  1. sample_rate 采样率
  2. sample_fmt 采样格式
  3. ch_layout 通道
  4. bit_rate 码率
aoutput_codeCtx = avcodec_alloc_context3(aoutput_codec);
	
aoutput_codeCtx->sample_rate = sample_rate;
aoutput_codeCtx->sample_fmt = aoutput_codec->sample_fmts[0];
av_channel_layout_copy(&aoutput_codeCtx->ch_layout, &acodeCtx->ch_layout);
aoutput_codeCtx->bit_rate = 192000;

输出AVStream

aoutput_stream = avformat_new_stream(output_formatCtx, aoutput_codec);
	
if (avcodec_parameters_from_context(aoutput_stream->codecpar, aoutput_codeCtx)) {
	printf("输出codec上下文拷贝失败");
}

转化工具SwrContext

// 音频上下文格式转换
swr_alloc_set_opts2(&swr_ctx,
					&aoutput_codeCtx->ch_layout,			// 输出layout
					aoutput_codeCtx->sample_fmt,			// 输出格式
					aoutput_codeCtx->sample_rate,			// 输出采样率
					&acodeCtx->ch_layout,					// 输入layout
					acodeCtx->sample_fmt,					// 输入格式
					acodeCtx->sample_rate,					// 输入采样率
					0, NULL);
swr_init(swr_ctx);

输出AVFrame

AVFrame* aoutput_frame = av_frame_alloc();
aoutput_frame->format = aoutput_codeCtx->sample_fmt;
aoutput_frame->ch_layout = aoutput_codeCtx->ch_layout;
aoutput_frame->sample_rate = aoutput_codeCtx->sample_rate;
aoutput_frame->nb_samples = aoutput_codeCtx->frame_size;

av_frame_get_buffer(aoutput_frame, 0);

3. 逐帧编码

if (packet.stream_index == audio_stream) {
	/**
	 音频编解码操作
	 */
	int ret = avcodec_send_packet(acodeCtx, &packet);
	if (ret < 0) {
		printf("1packet resolve error!");
		break;
	}
	while (!avcodec_receive_frame(acodeCtx, frame)) {
		ret = swr_convert(swr_ctx,
					aoutput_frame->data,
					aoutput_frame->nb_samples,
					(const uint8_t **)frame->data,
					frame->nb_samples);
		if (ret<0) {
			printf("frame resolve error: %s\n", av_err2str(ret));
		}
		/**
		 送入编码器
		 */
		ret = avcodec_send_frame(aoutput_codeCtx, aoutput_frame);
		if (ret < 0) {
			printf("frame resolve error: %s\n", av_err2str(ret));
			break;
		}
		
		while (!avcodec_receive_packet(aoutput_codeCtx, &packet)) {
			packet.stream_index = aoutput_stream->index;
			av_interleaved_write_frame(output_formatCtx, &packet);
			av_packet_unref(&packet);
		}
		
	}
}

0402更新音频重编码

整体思路

  1. 解析输入的参数,-i 后面是输入的文件,-vc 指定视频编码器, -w 指定视频宽,-h 指定视频高,-o 输出文件名
  2. 对输入的文件进行解析,参考流程图,读出format上下文,codec上下文,编码器,以及帧率等基本参数
  3. 根据输入参数更新 输出编码器,输出帧大小
  4. 逐帧读取 解码后进行编码;
  5. 写入文件
    在这里插入图片描述
    我们将上述过程拆为三个部分进行讲解,然后看看每个部分都做了哪些工作;

1. 输入流准备

第一步就是准备输入流,我们结合流程图和代码讲解。
首先指定需要使用的参数

char* 	input_file = "/Users/chenhuaiyi/workspace/ffmpeg/files/恋爱.mp4";
char* 	codec_name = "libx264";
int		width = 896;
int 	height = 414;
char*	output_file = "/Users/chenhuaiyi/workspace/ffmpeg/files/恋爱_重编码.mp4";

接下来初始化一些上下文参数(这里我就不赘述了,学过前面的视频播放的同学对固定的哪些实体都很熟系了吧)

avFormatContext

// 1. 打开视频文件,获取格式上下文
	if (avformat_open_input(&formatCtx, input_file, NULL, NULL) < 0) {
		printf("打开文件%s失败", input_file);
	}

video_stream

// 3. 找到对应的视频流
	video_stream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
	if (video_stream == -1) {
		printf("该文件没有视频流");
	}

avCodecContext

// 4. 将视频流编码参数写入上下文
	codeCtx = avcodec_alloc_context3(codec);
	avcodec_parameters_to_context(codeCtx, formatCtx->streams[video_stream]->codecpar);

avcodec

// 5. 查找流的解码器
	codec = avcodec_find_decoder(codeCtx->codec_id);
	
	// 6. 打开流的解码器
	if (avcodec_open2(codeCtx, codec, NULL) < 0) {
		printf("打开解码器失败");
	}

2. 输出流准备

输出流可能相比较比较陌生,实际上和输入流准备是一个相反的过程。
输入formatContext ---- 输出formatContext
输入avcodecContext ---- 输出avcodecContext
输入stream ---- 输出stream
输入解码器avcodec — 输出编码器codec

另外,还要加一个设置AVIOContext的过程。

avcodec输出编码器

output_codec = avcodec_find_encoder_by_name(codec_name);
	if (!output_codec) {
		printf("输入的编码器有误");
	}

avcodecContext输出上下文
这里我说一下,这下面的参数一开始的时候我也不清楚需要设置哪些,也是看资料和尝试试出来的。

这下面几个应该是必须设置的

width和height:不必多说,这个是视频的大小
pix_fmt:帧格式
time_base:时间基,这个非常重要,关系到后续pts和dts
bit_rate:码率,没有码率就不知道怎么传输
framerate:帧率,帧率就是和视频播放速度挂钩,本质上和time_base就是一个东西

// 2. 输出codec上下文
	output_codeCtx = avcodec_alloc_context3(output_codec);
	
	// 3. 输出参数设置(宽,高,格式,帧率)
	output_codeCtx->width 		= 	width;
	output_codeCtx->height 		= 	height;
	output_codeCtx->pix_fmt 	= 	AV_PIX_FMT_YUV420P;
	output_codeCtx->time_base 	= 	av_inv_q(formatCtx->streams[video_stream]->avg_frame_rate);
	output_codeCtx->bit_rate	=  	400000;
	output_codeCtx->framerate	=   formatCtx->streams[video_stream]->r_frame_rate;;
	// output_codeCtx->max_b_frames=	1;
	// output_codeCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

打开编码器

// 4. 打开编码器
	if (avcodec_open2(output_codeCtx, output_codec, NULL) < 0){
		printf("打开编码器有误");
	}

输出 output avformatContext

// 5. 设置输出formatCtx
	avformat_alloc_output_context2(&output_formatCtx, NULL, NULL, output_file);

输出stream

// 5. 添加输出流到format codec上下文
	output_stream = avformat_new_stream(output_formatCtx, output_codec);
	
// 6. 设置流信息
	if (avcodec_parameters_from_context(output_stream->codecpar, output_codeCtx)) {
		printf("输出codec上下文拷贝失败");
	}

AVIOContext
这个实体有点陌生
看原码注解:大概就是在复用过程中,要执行一下,确保正确写入文件头

muxing: set by the user before avformat_write_header().
The caller must take care of closing / freeing the IO context.

	// 7. 设置IO
	if (!(output_formatCtx->oformat->flags & AVFMT_NOFILE)) {
		if (avio_open(&output_formatCtx->pb, output_file, AVIO_FLAG_WRITE) < 0) {
			fprintf(stderr, "Could not open output file '%s'\n", output_file);
		}
	}

写入文件头

// 8. 写入文件头
	if (avformat_write_header(output_formatCtx, NULL) < 0) {
		fprintf(stderr, "Error occurred when opening output file\n");
	}

3. 编解码转化

这个流程实际上非常简单。先明确两个事情

  • 编码后、解码前的数据为packet
  • 编码前、解码后的数据为frame

那么编解码转化就可以从下面五步组成了(详细见代码详情)

  1. 从input中取出packet: av_read_frame
  2. 将packet送入解码器: avcodec_send_packet
  3. 从解码器中获得解码后的frame: avcodec_receive_frame
  4. 格式转化:sws_scale
  5. 将frame送入编码器:avcodec_send_frame
  6. 从编码器中获取编码后的packet:avcodec_receive_packet
  7. 将packet写入output:av_interleaved_write_frame

4. 收尾工作

写入文件:

/**
	 写入文件
	 */
	av_write_trailer(output_formatCtx);

释放空间

源代码

//
//  main.c
//  decoder
//  音视频的编解码知识学习
//
//  Created by chenhuaiyi on 2025/3/13.
//

#include <stdio.h>
// ffmpeg
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/time.h"
#include "libavutil/channel_layout.h"
// SDL
#include "SDL.h"
#include "SDL_thread.h"


int main(int argc, const char * argv[]) {
	
	/**
	 功能:输入一个视频,并指定视频的编码的参数,以及输出的文件,最终生产新的文件
	 1. 解析输入的参数,-i 后面是输入的文件,-vc 指定视频编码器, -w 指定视频宽,-h 指定视频高,-o 输出文件名
	 2. 对输入的文件进行解析,参考流程图,读出format上下文,codec上下文,编码器,以及帧率等基本参数
	 3. 根据输入参数更新 输出编码器,输出帧大小
	 4. 逐帧读取 解码后进行编码;
	 5. 写入文件
	 */
	
	char* 	input_file = "/Users/chenhuaiyi/workspace/ffmpeg/files/恋爱.mp4";
	char*	output_file = "/Users/chenhuaiyi/workspace/ffmpeg/files/恋爱_重编码.mp4";
	AVFormatContext*	formatCtx = avformat_alloc_context();
	AVFormatContext*	output_formatCtx;
	
	// 视频编码参数
	char* 	codec_name = "libx264";
	int		width = 896;
	int 	height = 414;
	
	// 音频编码参数
	char*   acodec_name = "aac";
	int		sample_rate = 44100;
	
	// 视频输入实体
	AVCodecContext*		codeCtx;
	const AVCodec*		codec;
	int					video_stream = -1;
	// 视频输出实体
	const AVCodec*		output_codec;
	AVCodecContext*		output_codeCtx;
	AVStream*			output_stream;
	struct SwsContext*	sws_ctx;
	
	// 音频输入实体
	AVCodecContext*		acodeCtx;
	const AVCodec*		acodec;
	int					audio_stream = -1;
	
	// 音频输出实体
	const AVCodec*		aoutput_codec;
	AVCodecContext*		aoutput_codeCtx;
	AVStream*			aoutput_stream;
	struct SwrContext*	swr_ctx;
	
	
	/**  ------------------------   输入流准备工作      ---------------------- **/
	// 1. 打开视频文件,获取格式上下文
	if (avformat_open_input(&formatCtx, input_file, NULL, NULL) < 0) {
		printf("打开文件%s失败", input_file);
	}
	
	// 2. 对文件探测流信息
	if (avformat_find_stream_info(formatCtx, NULL) < 0) {
		printf("文件探测流信息失败");
	}
	
	// 3. 找到对应的视频流
	video_stream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
	audio_stream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, &acodec, 0);
	if (video_stream == -1 || audio_stream == -1) {
		printf("该文件没有视频流");
	}
	
	// 4. 将视频&音频流编码参数写入上下文
	codeCtx = avcodec_alloc_context3(codec);
	avcodec_parameters_to_context(codeCtx, formatCtx->streams[video_stream]->codecpar);
	acodeCtx = avcodec_alloc_context3(acodec);
	avcodec_parameters_to_context(acodeCtx, formatCtx->streams[audio_stream]->codecpar);

	// 5. 查找流的解码器
	codec = avcodec_find_decoder(codeCtx->codec_id);
	acodec = avcodec_find_decoder(acodeCtx->codec_id);
	
	// 6. 打开流的解码器
	if (avcodec_open2(codeCtx, codec, NULL) < 0) {
		printf("打开解码器失败");
	}
	if (avcodec_open2(acodeCtx, acodec, NULL) < 0) {
		printf("打开解码器失败");
	}
	
	av_dump_format(formatCtx, 0, input_file, 0);
	
	/**  ------------------------   输出流准备工作      ---------------------- **/
	avformat_alloc_output_context2(&output_formatCtx, NULL, NULL, output_file);
	
	// 1. 找到编码器
	output_codec = avcodec_find_encoder_by_name(codec_name);
	if (!output_codec) {
		printf("输入的编码器有误");
	}
	
	// 2. 输出codec上下文
	output_codeCtx = avcodec_alloc_context3(output_codec);
	
	// 3. 输出参数设置(宽,高,格式,帧率)
	output_codeCtx->width 		= 	width;
	output_codeCtx->height 		= 	height;
	output_codeCtx->pix_fmt 	= 	AV_PIX_FMT_YUV420P;
	output_codeCtx->time_base 	= 	av_inv_q(formatCtx->streams[video_stream]->avg_frame_rate);
	output_codeCtx->bit_rate	=  	400000;
	output_codeCtx->framerate	=   formatCtx->streams[video_stream]->r_frame_rate;;
	
	// 4. 打开编码器
	if (avcodec_open2(output_codeCtx, output_codec, NULL) < 0){
		printf("打开编码器有误");
	}
	
	// 5. 添加输出流到format codec上下文
	output_stream = avformat_new_stream(output_formatCtx, output_codec);
	
	// 6. 设置流信息
	if (avcodec_parameters_from_context(output_stream->codecpar, output_codeCtx)) {
		printf("输出codec上下文拷贝失败");
	}
	
	// 7. 音频输出工作
	aoutput_codec = avcodec_find_encoder_by_name(acodec_name);
	if (!aoutput_codec) {
		printf("输入的编码器有误");
	}
	
	aoutput_codeCtx = avcodec_alloc_context3(aoutput_codec);
	
	aoutput_codeCtx->sample_rate = sample_rate;
	aoutput_codeCtx->sample_fmt = aoutput_codec->sample_fmts[0];
	av_channel_layout_copy(&aoutput_codeCtx->ch_layout, &acodeCtx->ch_layout);
	aoutput_codeCtx->bit_rate = 192000;
	
	int open = avcodec_open2(aoutput_codeCtx, aoutput_codec, NULL);
	if (open < 0){
		printf("打开编码器有误: %s\n", av_err2str(open));
	}
	
	aoutput_stream = avformat_new_stream(output_formatCtx, aoutput_codec);
	
	if (avcodec_parameters_from_context(aoutput_stream->codecpar, aoutput_codeCtx)) {
		printf("输出codec上下文拷贝失败");
	}
	
	
	// 8. 设置IO
	if (!(output_formatCtx->oformat->flags & AVFMT_NOFILE)) {
		if (avio_open(&output_formatCtx->pb, output_file, AVIO_FLAG_WRITE) < 0) {
			fprintf(stderr, "Could not open output file '%s'\n", output_file);
		}
	}
	
	// 9. 写入文件头
	if (avformat_write_header(output_formatCtx, NULL) < 0) {
		fprintf(stderr, "Error occurred when opening output file\n");
	}
	
	av_dump_format(output_formatCtx, 0, output_file, 1);
	
	
	
	/**  ------------------------   编解码转化     ---------------------- **/
	AVPacket packet;
	AVFrame* frame = av_frame_alloc();
	AVFrame* output_frame = av_frame_alloc();
	output_frame->width	= width;
	output_frame->height = height;
	output_frame->format = AV_PIX_FMT_YUV420P;
	av_image_alloc(output_frame->data, output_frame->linesize, width, height, AV_PIX_FMT_YUV420P, 1);
	
	sws_ctx = sws_getContext(codeCtx->width,
							 codeCtx->height,
							 codeCtx->pix_fmt,
							 width, height,
							 AV_PIX_FMT_YUV420P,
							 SWS_BILINEAR,
							 NULL, NULL, NULL);
	
	AVFrame* aoutput_frame = av_frame_alloc();
	aoutput_frame->format = aoutput_codeCtx->sample_fmt;
	aoutput_frame->ch_layout = aoutput_codeCtx->ch_layout;
	aoutput_frame->sample_rate = aoutput_codeCtx->sample_rate;
	aoutput_frame->nb_samples = aoutput_codeCtx->frame_size;
	
	av_frame_get_buffer(aoutput_frame, 0);
	
	// 音频上下文格式转换
	swr_alloc_set_opts2(&swr_ctx,
						&aoutput_codeCtx->ch_layout,			// 输出layout
						aoutput_codeCtx->sample_fmt,			// 输出格式
						aoutput_codeCtx->sample_rate,			// 输出采样率
						&acodeCtx->ch_layout,					// 输入layout
						acodeCtx->sample_fmt,					// 输入格式
						acodeCtx->sample_rate,					// 输入采样率
						0, NULL);
	swr_init(swr_ctx);
	
	// 开始工作
	while (av_read_frame(formatCtx, &packet) >= 0) {
		
		if (packet.stream_index == audio_stream) {
			/**
			 音频编解码操作
			 */
			int ret = avcodec_send_packet(acodeCtx, &packet);
			if (ret < 0) {
				printf("1packet resolve error!");
				break;
			}
			while (!avcodec_receive_frame(acodeCtx, frame)) {
				ret = swr_convert(swr_ctx,
							aoutput_frame->data,
							aoutput_frame->nb_samples,
							(const uint8_t **)frame->data,
							frame->nb_samples);
				if (ret<0) {
					printf("frame resolve error: %s\n", av_err2str(ret));
				}
				/**
				 送入编码器
				 */
				ret = avcodec_send_frame(aoutput_codeCtx, aoutput_frame);
				if (ret < 0) {
					printf("frame resolve error: %s\n", av_err2str(ret));
					break;
				}
				
				while (!avcodec_receive_packet(aoutput_codeCtx, &packet)) {
					packet.stream_index = aoutput_stream->index;
					av_interleaved_write_frame(output_formatCtx, &packet);
					av_packet_unref(&packet);
				}
				
			}
		}
		
		else if (packet.stream_index == video_stream) {
			/**
			 送入解码器, 解码
			 packet->frame
			 */
			int ret = avcodec_send_packet(codeCtx, &packet);
			if (ret < 0) {
				printf("2packet resolve error!:%s\n", av_err2str(ret));
				break;
			}
			
			while (!avcodec_receive_frame(codeCtx, frame)) {
				// 上下文转化
				sws_scale(sws_ctx,
						  (uint8_t const * const *)frame->data,
						  frame->linesize,
						  0,
						  frame->height,
						  output_frame->data,
						  output_frame->linesize);
				
				output_frame->pts = av_rescale_q(frame->pts, av_inv_q(formatCtx->streams[video_stream]->avg_frame_rate), output_codeCtx->time_base);
				
				/**
				 送入编码器,编码
				 frame->packet
				 */
				ret = avcodec_send_frame(output_codeCtx, output_frame);
				if (ret < 0) {
					printf("frame resolve error: %s\n", av_err2str(ret));
					break;
				}
				
				while (!avcodec_receive_packet(output_codeCtx, &packet)) {
					packet.stream_index = output_stream->index;
					av_interleaved_write_frame(output_formatCtx, &packet);
					av_packet_unref(&packet);
				}
				
			}
		}
		av_packet_unref(&packet);
	}
	
	/**
	 写入文件
	 */
	av_write_trailer(output_formatCtx);
	
	sws_freeContext(sws_ctx);
	av_frame_free(&frame);
	av_frame_free(&output_frame);
	avcodec_free_context(&codeCtx);
	avcodec_free_context(&acodeCtx);
	avcodec_free_context(&output_codeCtx);
	avcodec_free_context(&aoutput_codeCtx);
	avformat_close_input(&formatCtx);
	avio_closep(&output_formatCtx->pb);
	avformat_free_context(output_formatCtx);
	
	
	printf("Hello, World!\n");
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值