利用FFMPEG实现YUV裸数据的编码和封装

有个工业相机编码推流的需求,于是又把之前写的一份FFMPEG编码YUV裸数据的接口翻出来,去掉了分模块的接口封装,恢复成如下的纯过程代码,测试了一下,功能是OK的。满足输入YUV裸数据,按照需要的分辨率、帧率、比特率输出指定的编码封装文件需求。简单起见,代码里直接用了H264的编码和mpegts的封装,也可以改成其他的编码和封装格式。

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <iostream>

#ifdef __cplusplus
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include "libswscale/swscale.h"
#include <libavutil/opt.h>

}
#endif

using namespace std;

static int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index)
{
    int ret;
    int got_frame;
	AVPacket enc_pkt;
	
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & AV_CODEC_CAP_DELAY))
        return 0;

    while (1) 

	{
		enc_pkt.data = NULL;
		enc_pkt.size = 0;
		av_init_packet(&enc_pkt);
		ret = avcodec_encode_video2(fmt_ctx->streams[stream_index]->codec, &enc_pkt, NULL, &got_frame);

		av_frame_free(NULL);
			
        if (ret < 0)
        {
        	break;
        }
        if (!got_frame)
        {
        	ret = 0;
			break;
        }

		enc_pkt.stream_index = stream_index;
		av_packet_rescale_ts(&enc_pkt, fmt_ctx->streams[stream_index]->codec->time_base, 
			fmt_ctx->streams[stream_index]->time_base);
		
		ret = av_interleaved_write_frame(fmt_ctx, &enc_pkt);
		if(ret < 0)
		{
			break;
		}
    }
	
    return ret;
}


int main(int argc, char * argv [ ])
{
	const char *in_filename = "littlegirl.yuv";
	//const char *out_filename = "udp://233.233.233.223:5555"; 
	const char *out_filename = "littlegirl.ts"; 
	
	int in_width = 960;   // 输入裸图像宽度
	int in_height = 544;  // 输出裸图像高度
	int out_width = in_width / 2;   //输出视频图像宽度
	int out_height = in_height / 2;  //输出视频图像高度
	int frame_rate = 20;     //输出视频帧率
	int bitrate = 3000000;   //输出视频比特率

	AVOutputFormat *ofmt = NULL;
	AVFormatContext *ofmt_ctx = NULL;
	AVStream *video_st = NULL;
	AVCodecContext *pCodecCtx = NULL;
	AVCodec *pCodec = NULL;
	struct SwsContext *sws_ctx = NULL;

	AVFrame *picture = NULL;    // 输入图像结构体
	uint8_t *picture_buf = NULL;  // yuv420 data,输入图像buffer
	AVFrame *out_picture = NULL;  // 输出图像结构体,作为编码器的输入
	uint8_t *out_picture_buf = NULL;  //输出图像buffer
	int picture_size = 0;    // 输入图像数据大小,yuv420格式下,size = width*height*3/2
	int out_picture_size = 0;  // 输出图像数据大小,与格式有关
	
	AVPacket enc_pkt;   // 编码数据包
	int64_t frame_pts = 0;  
	int ret;
	size_t read_size;
	bool isrgb = false;  //指示输入是否是RGB裸数据,true为RGB数据,false为YUV数据

	/* 打开输入YUV格式文件 */	
	FILE *in_file = fopen(in_filename, "rb+");
	if(!in_file)
	{
		cout << "Open input file failed! " << endl;
		return -1;
	}

	/* 一系列初始化操作 */
	avcodec_register_all();  
    av_register_all();  
	//avformat_network_init();   //推流需要该功能

	/* 分配输出上下文 */
	avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", out_filename);
	if(!ofmt_ctx)
	{
		cout << "Could not create output context!" << endl;
		goto end;
	}

	// 输出格式
	ofmt = ofmt_ctx->oformat;   

	/* 打开输出文件 */
	if(avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_READ_WRITE) < 0)
	{
		cout << "Open output file failed!" << endl;
		goto end;
	}

	//为输出文件创建一个新的stream
	video_st = avformat_new_stream(ofmt_ctx, NULL);
	if(!video_st)
	{
		cout << "Allocate output stream failed" << endl;
		goto end;
	}

	// Set frame rate
	video_st->time_base.num = 1;
	video_st->time_base.den = frame_rate;

	/* 设置编码参数 */
    pCodecCtx = video_st->codec;
	pCodecCtx->codec_id = AV_CODEC_ID_H264;
	pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
	pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
	pCodecCtx->width = out_width;
	pCodecCtx->height = out_height;

	pCodecCtx->time_base.num = 1;
	pCodecCtx->time_base.den = frame_rate;
	pCodecCtx->bit_rate = bitrate;
	pCodecCtx->gop_size = frame_rate;			// 1 second 1 gop
	
	if(pCodecCtx->codec_id == AV_CODEC_ID_H264)
	{
		pCodecCtx->qmin = 10;
		pCodecCtx->qmax = 51;
		pCodecCtx->qcompress = 0.6;

		pCodecCtx->thread_count = 6;
		pCodecCtx->thread_type = FF_THREAD_FRAME;
		pCodecCtx->profile = FF_PROFILE_H264_BASELINE;
		av_opt_set(pCodecCtx->priv_data, "preset", "superfast", 0);
		av_opt_set(pCodecCtx->priv_data, "tune", "zerolatency", 0);
	}

	/* 根据编码器ID查找注册过的编码器 */
	pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
	if(!pCodec)
	{
		cout << "Can not find encoder!" << endl;
		goto end;
	}

	/* 利用已知的AVCodec初始化AVCodecContext */
	if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
	{
		cout << "Open encoder failed!" << endl;
		goto end;
	}

	// 打印输出信息
	av_dump_format(ofmt_ctx, 0, out_filename, 1);

	/* 设置输入图像格式 */
	picture = av_frame_alloc();
	picture->width = in_width;
	picture->height = in_height;
	picture->format = AV_PIX_FMT_YUV420P;  
	picture_size = avpicture_get_size((enum AVPixelFormat)picture->format, picture->width, picture->height);
	picture_buf = (uint8_t *)av_malloc(picture_size);
	avpicture_fill((AVPicture *)picture, (const uint8_t *)picture_buf, AV_PIX_FMT_YUV420P, in_width, in_height);

	/* 设置输出图像(送入编码器的图像)格式 */
	out_picture = av_frame_alloc();
	out_picture->width = pCodecCtx->width;
	out_picture->height = pCodecCtx->height;
	out_picture->format = pCodecCtx->pix_fmt;
	out_picture_size = avpicture_get_size((enum AVPixelFormat)out_picture->format, out_picture->width, out_picture->height);
	out_picture_buf = (uint8_t *)av_malloc(out_picture_size);
	avpicture_fill((AVPicture *)out_picture, (const uint8_t *)out_picture_buf, AV_PIX_FMT_YUV420P, out_width, out_height);



	/* 图像rescale,包括转换图像格式,若输入为RGB格式,该功能很重要 */
	sws_ctx = sws_getContext(in_width, in_height, AV_PIX_FMT_YUV420P,
						out_width, out_height, AV_PIX_FMT_YUV420P,
						SWS_BILINEAR, NULL, NULL, NULL);
	if(!sws_ctx)
	{
		cout << "Impossible to create scale context for format conversion!" << endl;
		return -1;
	
	}

	/* 写输出文件头 */
	avformat_write_header(ofmt_ctx, NULL);

	av_new_packet(&enc_pkt, out_picture_size);

	while(1)
	{
		read_size = fread(picture_buf, 1, picture_size, in_file);

		if(feof(in_file))
		{
			break;
		}

		/* 将读取到的buffer数据填充到picture结构体中 */
		avpicture_fill((AVPicture *)picture, (const uint8_t *)picture_buf, AV_PIX_FMT_YUV420P, in_width, in_height);
		
		/* 根据编码输出要求,将输入图像格式转换成编码器需要的图像格式 */
		sws_scale(sws_ctx, picture->data, picture->linesize, 0, in_height, out_picture->data, out_picture->linesize);

		/* 设置当前帧的时间戳 */
		out_picture->pts = frame_pts++;
		int got_pkt = 0;

		// 编码一帧数据
		ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, out_picture, &got_pkt);
		if(ret < 0)
		{
			cout << "Encode failed!" << endl;
			return -1;
		}

		/* 将编码后的一包数据打上时间戳,然后写到输出文件中 */
		if(got_pkt)
		{
			enc_pkt.stream_index = video_st->index;
			av_packet_rescale_ts(&enc_pkt, pCodecCtx->time_base, video_st->time_base);  //根据timebase计算输出流的时间戳
			enc_pkt.pos = -1;
			ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);  // Write the encoded packet to the output file
			av_free_packet(&enc_pkt);
		}
	}

	/* 编码结束时,将编码器中缓存的图像数据输出到输出文件 */
	ret = flush_encoder(ofmt_ctx, 0);
	if(ret < 0)
	{
		cout << "Flushing encoder failed." << endl;
		return -1;
	}

	// Write the output file trailer
	av_write_trailer(ofmt_ctx);

end:
	if(ofmt_ctx)
	{
		avio_close(ofmt_ctx->pb);
		avformat_free_context(ofmt_ctx);
	}

	ret = fclose(in_file);
	if(!ret)
	{
		cout << "Input file closed OK. " << endl;
	}
	else
	{
		cout << "Input file closed failed." << endl;
	}
		

	if(video_st)
	{
		avcodec_close(video_st->codec);
		video_st = NULL;
	}

	if(picture)
	{
		av_frame_free(&picture);
		picture = NULL;
	}

	if(picture_buf)
	{
		av_freep(&picture_buf);
	}

	if(out_picture_buf)
	{
		av_freep(&out_picture_buf);
	}

	if(sws_ctx)
	{
		sws_freeContext(sws_ctx);
	}
	
}


回到出发点,工业相机一般输出的是裸的RGB数据,可以在调用编码接口之前,将RGB数据转换成YUV数据,通过如下语句实现:

/* 计算格式转换上下文 */
sws_ctx = sws_getContext(in_width, in_height, AV_PIX_FMT_BGR24,
							out_width, out_height, AV_PIX_FMT_YUV420P,
							SWS_BILINEAR, NULL, NULL, NULL);

/* 图像填充,获取AVPicture结构体的相应参数,picture_buf中为RGB数据,注意输入格式为RGB24 */    
avpicture_fill((AVPicture *)picture, (const uint8_t *)picture_buf, AV_PIX_FMT_BGR24, in_width, in_height);

/* RGB图像向YUV图像的转换 */
sws_scale(sws_ctx, picture->data, picture->linesize, 0, in_height, out_picture->data, out_picture->linesize);
	

当然,有些编码器支持RGB输入,有兴趣的朋友可以研究下。这里是按照FFMPEG默认的YUV420数据做编码输入的。

参考资料:

[1] FFMPEG 4.0官方example代码;

[2] 雷神的博客,因为写这份原始代码的时间有点久远,具体哪篇忘记了。https://me.csdn.net/leixiaohua1020

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
YUV和PCM数据编码成视频和音频文件需要使用FFmpeg库,具体的实现步骤如下: 1. 初始化FFmpeg库 在使用FFmpeg库之前,需要先进行初始化。使用av_register_all函数可以注册FFmpeg库中的所有编解码器、格式器和协议等。 ``` av_register_all(); ``` 2. 打开输出文件 使用avformat_alloc_output_context2和avio_open2函数打开输出文件,创建AVFormatContext结构体并分配内存,将输出文件与该结构体关联。 ``` AVFormatContext *out_ctx = NULL; int ret = avformat_alloc_output_context2(&out_ctx, NULL, NULL, output_file); if (ret < 0) { // 创建AVFormatContext失败 return; } if (!(out_ctx->oformat->flags & AVFMT_NOFILE)) { ret = avio_open2(&out_ctx->pb, output_file, AVIO_FLAG_WRITE, NULL, NULL); if (ret < 0) { // 打开输出文件失败 return; } } ``` 3. 创建音视频流 使用avformat_new_stream函数创建音视频流,并设置音视频流的相关参数,如编码器、帧率、码率、采样率等。 ``` AVStream *video_stream = avformat_new_stream(out_ctx, NULL); if (video_stream == NULL) { // 创建视频流失败 return; } AVCodecParameters *codecpar = video_stream->codecpar; codecpar->codec_type = AVMEDIA_TYPE_VIDEO; codecpar->width = width; codecpar->height = height; codecpar->format = AV_PIX_FMT_YUV420P; codecpar->codec_id = AV_CODEC_ID_H264; codecpar->bit_rate = bit_rate; codecpar->framerate = {fps, 1}; AVStream *audio_stream = avformat_new_stream(out_ctx, NULL); if (audio_stream == NULL) { // 创建音频流失败 return; } codecpar = audio_stream->codecpar; codecpar->codec_type = AVMEDIA_TYPE_AUDIO; codecpar->sample_rate = sample_rate; codecpar->format = AV_SAMPLE_FMT_S16; codecpar->channels = channels; codecpar->channel_layout = av_get_default_channel_layout(channels); codecpar->codec_id = AV_CODEC_ID_AAC; codecpar->bit_rate = bit_rate; ``` 4. 打开视频和音频编码器 使用avcodec_find_encoder函数查找视频和音频编码器,并使用avcodec_open2打开编码器。 ``` AVCodec *video_codec = avcodec_find_encoder(video_stream->codecpar->codec_id); if (video_codec == NULL) { // 查找视频编码器失败 return; } AVCodecContext *video_cctx = avcodec_alloc_context3(video_codec); if (video_cctx == NULL) { // 创建视频编码器上下文失败 return; } ret = avcodec_open2(video_cctx, video_codec, NULL); if (ret < 0) { // 打开视频编码器失败 return; } AVCodec *audio_codec = avcodec_find_encoder(audio_stream->codecpar->codec_id); if (audio_codec == NULL) { // 查找音频编码器失败 return; } AVCodecContext *audio_cctx = avcodec_alloc_context3(audio_codec); if (audio_cctx == NULL) { // 创建音频编码器上下文失败 return; } ret = avcodec_open2(audio_cctx, audio_codec, NULL); if (ret < 0) { // 打开音频编码器失败 return; } ``` 5. 写入视频和音频数据 使用av_frame_alloc函数创建AVFrame结构体,填充YUV和PCM数据,并使用avcodec_send_frame和avcodec_receive_packet函数将数据编码成视频和音频包,最后使用av_write_frame函数将包写入输出文件。 ``` AVFrame *video_frame = av_frame_alloc(); // 填充YUV数据到video_frame中 AVPacket *video_packet = av_packet_alloc(); ret = avcodec_send_frame(video_cctx, video_frame); if (ret < 0) { // 向视频编码器发送数据失败 return; } while (ret >= 0) { ret = avcodec_receive_packet(video_cctx, video_packet); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } if (ret < 0) { // 从视频编码器接收数据失败 return; } av_packet_rescale_ts(video_packet, video_cctx->time_base, video_stream->time_base); video_packet->stream_index = video_stream->index; ret = av_write_frame(out_ctx, video_packet); if (ret < 0) { // 写入视频数据失败 return; } } AVFrame *audio_frame = av_frame_alloc(); // 填充PCM数据到audio_frame中 AVPacket *audio_packet = av_packet_alloc(); ret = avcodec_send_frame(audio_cctx, audio_frame); if (ret < 0) { // 向音频编码器发送数据失败 return; } while (ret >= 0) { ret = avcodec_receive_packet(audio_cctx, audio_packet); if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { break; } if (ret < 0) { // 从音频编码器接收数据失败 return; } av_packet_rescale_ts(audio_packet, audio_cctx->time_base, audio_stream->time_base); audio_packet->stream_index = audio_stream->index; ret = av_write_frame(out_ctx, audio_packet); if (ret < 0) { // 写入音频数据失败 return; } } ``` 6. 关闭编码器和输出文件 使用av_write_trailer、avcodec_free_context和avformat_free_context函数释放资源并关闭编码器和输出文件。 ``` av_write_trailer(out_ctx); avcodec_free_context(&video_cctx); avcodec_free_context(&audio_cctx); avformat_free_context(out_ctx); ``` 以上是将YUV和PCM数据编码成视频和音频文件的基本流程,需要注意的是各项参数的设置和数据的填充。如果需要进行更详细的配置和处理,可以参考FFmpeg库的官方文档。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值