h264.265裸流和音频(ALAW或PCM)封装为mp4

环境描述:

1.目前我存在h264 + h265裸流+alaw音频(也可以说是s16 pcm)。

2.目前是私有协议可以简单理解为 多个音视频包组成了一个cms文件。

3.每个音视频包有信息可以描述 当前包时间戳(类似pts),大小

4.使用ffmpeg

5.参考了许多网上内容,感谢各位大佬

6.网上太多大同小异的原创代码,所以我这里不注重提供源代码,主要是展示针对我的难点及解决办法。

7.重大提示,我的大部分代码来源于ctrl+c。

目的:

1.需要将上述环境描述的音视频流封装为MP4

拆分步骤:

一.将h264封装为MP4,已完成,但是我不理解的部分挺多

步骤实现:

一、将h264封装为MP4

1.解析私有协议获取流信息,

2.根据流信息创建MP4的封装格式,可以算是写头,代码中的命名应该可以理解含义,我就不描述了

void AVTransform::writeHeader()
{
	switch (m_sCmsFileInfo.cms_video_info.codec)
	{
	case E_H264:
		m_nCodecID = AV_CODEC_ID_H264;
		break;
	case E_H265:
		m_nCodecID = AV_CODEC_ID_H265;
		break;
	default:
		m_nCodecID = AV_CODEC_ID_H264;
		break;
	}
	int ret = avformat_alloc_output_context2(&m_pOutContext, NULL, NULL, m_sOutFileName.toStdString().c_str());

	/*
	* since all input files are supposed to be identical (framerate, dimension, color format, ...)
	* we can safely set output codec values from first input file
	*/
	AVDictionary* opt = NULL;
	av_dict_set_int(&opt, "video_track_timescale", 15, 0);
	o_video_stream = avformat_new_stream(m_pOutContext, NULL);
	{
		AVCodecContext *c;
		c = o_video_stream->codec;
		c->bit_rate = m_nDataTotalNum*8*1000 / (unsigned long long)m_sCmsFileInfo.duration;
		c->codec_id = m_nCodecID;
		c->codec_type = AVMEDIA_TYPE_VIDEO;
		c->time_base.num = 1;
		c->time_base.den = 100000;
		//c->framerate.num = 1;
		//c->framerate.den = 100000;
		//c->time_base = av_inv_q({ 1 , 10000});
		fprintf(stderr, "time_base.num = %d time_base.den = %d\n", c->time_base.num, c->time_base.den);
		c->width = m_sCmsFileInfo.cms_video_info.width;
		c->height = m_sCmsFileInfo.cms_video_info.height;
		c->pix_fmt = AV_PIX_FMT_YUVJ420P;
		//printf("%d %d %d", c->width, c->height, c->pix_fmt);
		//c->flags = i_video_stream->codec->flags;
		c->flags |= CODEC_FLAG_GLOBAL_HEADER;
		
		//c->me_range = i_video_stream->codec->me_range;
		//c->max_qdiff = i_video_stream->codec->max_qdiff;
		//c->qmin = i_video_stream->codec->qmin;
		//c->qmax = i_video_stream->codec->qmax;
		//c->qcompress = i_video_stream->codec->qcompress;
	}
	ret = avio_open(&m_pOutContext->pb, m_sOutFileName.toStdString().c_str(), AVIO_FLAG_WRITE);

	ret = avformat_write_header(m_pOutContext, NULL);
}

3.新建线程将私有协议的音视频流中找出视频包,实际为i或者p帧。然后写入文件,即为写数据,我的裸流每帧都有时间戳所以你会看见我的pts计算方法是这样

void AVTransform::writeData(CMS_PACKAGE p)
{
    av_init_packet(&i_pkt);
    i_pkt.size = p.len;
    i_pkt.data = p.data;
    i_pkt.pts = p.ts*100;
    i_pkt.dts = p.ts*100;
    //i_pkt.duration = 1000 / m_sCmsFileInfo.cms_video_info.rate;
    //if (av_read_frame(i_fmt_ctx, &i_pkt) < 0)
    //    break;
    /*
    * pts and dts should increase monotonically
    * pts should be >= dts
    */
    i_pkt.flags |= AV_PKT_FLAG_KEY;
    //pts = i_pkt.pts;
    //i_pkt.pts += last_pts;
    //dts = i_pkt.dts;
    //i_pkt.dts += last_dts;
    i_pkt.stream_index = 0;

    //printf("%lld %lld\n", i_pkt.pts, i_pkt.dts); 
    //static int num = 1;
    //printf("frame %d\n", num++);
    int ret = av_interleaved_write_frame(m_pOutContext, &i_pkt);
    //av_free_packet(&i_pkt); 
    //av_init_packet(&i_pkt); 
    //last_dts += dts;
    //last_pts += pts;
    delete i_pkt.data;
}

4.上述步骤中重点理解 timebase pts dts,其实我还是有疑问。timebase的设置den好像不能小于100000,否则它会自动帮你乘以10直到达到某个值,我在某个博客上看到类似的描述,暂时我没有去深究,所以你可以看到此时我的pts=100*p.ts 因为我的数据包中ts是相对时间戳(相对于首帧)(ms)

二、h265封装为MP4

1.我按照上述同样流程操作,但是迅雷影音播放报错0x80040265,我正在找问题解决

=======================我是阔爱的分割线===============================

因为上诉的问题我放弃了自己建立codec,应该是h265比h264需要更多的参数配置。通过从流中(读取文件)获取codec。

如果你在代码中看到了未申明的参数,那应该是我的成员函数,我就不想贴出来了,其实从流中获取各种数据网上应该有各种博客完整的代码等实例,这里我只是起一个更多参考的作用。

	AVOutputFormat *ofmt = NULL;

	int ret, i;
	//Input
	if ((ret = avformat_open_input(&ifmt_ctx_v, m_sFileName.toStdString().c_str(), 0, 0)) < 0) {
		printf("Could not open input file.");
		return ret;
	}
	if ((ret = avformat_find_stream_info(ifmt_ctx_v, 0)) < 0) {
		printf("Failed to retrieve input stream information");
		return ret;
	}

	printf("===========Input Information==========\n");
	av_dump_format(ifmt_ctx_v, 0, m_sFileName.toStdString().c_str(), 0);
	printf("======================================\n");
	//Output
	avformat_alloc_output_context2(&m_pOutContext, NULL, NULL, m_sOutFileName.toStdString().c_str());
	if (!m_pOutContext) {
		printf("Could not create output context\n");
		ret = AVERROR_UNKNOWN;
		return ret;
	}
	ofmt = m_pOutContext->oformat;
	//Open output file
	if (!(ofmt->flags & AVFMT_NOFILE)) {
		ret = avio_open(&m_pOutContext->pb, m_sOutFileName.toStdString().c_str(), AVIO_FLAG_WRITE);
		if (ret < 0) {
			printf("Could not open output file '%s'", m_sOutFileName.toStdString().c_str());
			return ret;
		}
	}

	for (i = 0; i < ifmt_ctx_v->nb_streams; i++) {
		//Create output AVStream according to input AVStream
		if (ifmt_ctx_v->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
			AVStream *in_stream = ifmt_ctx_v->streams[i];
			AVStream *out_stream = avformat_new_stream(m_pOutContext, in_stream->codec->codec);
			if (!out_stream) {
				printf("Failed allocating output stream\n");
				ret = AVERROR_UNKNOWN;
				return ret;
			}
			videoindex_out = out_stream->index;
			//Copy the settings of AVCodecContext
			if ((ret = avcodec_copy_context(out_stream->codec, in_stream->codec)) < 0) {
				printf("Failed to copy context from input to output stream codec context\n");
				return ret;
			}
			out_stream->codec->codec_tag = 0;
			if (m_pOutContext->oformat->flags & AVFMT_GLOBALHEADER)
				out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
			break;
		}
	}

至此H264 265 封装为MP4的代码应该完成了,总结一下:注重解码器参数的正确性,注重timebase pts dts的理解,其实我的数据里只有I P 帧,所以我这里dts=pts,还有就是AVPacket中stream_index的正确性,比如这时我们只new 了一个avformat_new_stream(m_pOutContext, in_stream->codec->codec);视频流,所以它应该=0,如果我们此时新new了音频流,那音频流的AVPacket就是1了。

三、将ALAW(S16LE PCM)封装为MP4

1.这里我输入文件里的音频流其实是alaw格式,但是实际是将alaw先转换为PCM原始数据然后在封装进MP4,

源数据:ALAW 8000 8位 单通道-> PCM 8000 16位 单通道 -> ACC 8000 FLTP(flot 4字节 32位)单通道。所以下面的代码中国你你会看到我的参数配置会是那样

2.S16LE封装进MP4,其实网上有大量的类似讲解,但是都大同小异,感觉都起始于同一个爹。他们的操作都类似于视频,从一个音频文件中获取流,这时候他们不用关心codec的参数是什么,但是我的音视频文件存在于我们的私有格式文件中,可以理解为单纯的H264 265+PCM数据组成,

3.ffmpeg中封装进MP4只支持ACC格式音频,而ACC的数据格式是AV_SAMPLE_FMT_FLTP,举个两通道的例子也就是data1-LLLLLLLL data2-RRRRRRR,然后我的源数据应该是AV_SAMPLE_FMT_S16 如data1-LRLRLRLR。。。,所以首先我们需要使用swr_convert()进行重采样,网上的通用代码套路如下(这里我将codec的建立过程一起贴出来):

	//音频解码context创建 和 流创建
	if(m_sCmsFileInfo.cms_audio_info.codec == E_ALAW){
		m_pAudioQueue = new AQueue(AUDIO_BUFFER_LEN);
		AVStream *out_stream_a = avformat_new_stream(m_pOutContext, NULL);
		if (!out_stream_a) {
			ret = -1;
			return ret;
		}
		//pInputAudioContext = avcodec_alloc_context3(pInputAudioCodec);
		pInputAudioContext = out_stream_a->codec;
		pInputAudioContext->codec_id = ofmt->audio_codec;
		pInputAudioContext->bit_rate = 64000;
		pInputAudioContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
		pInputAudioContext->sample_rate = 8000;
		pInputAudioContext->channel_layout = AV_CH_LAYOUT_MONO;
		pInputAudioContext->channels = av_get_channel_layout_nb_channels(pInputAudioContext->channel_layout);
		pInputAudioContext->codec_type = AVMEDIA_TYPE_AUDIO;
		pInputAudioContext->time_base.num = 1;
		pInputAudioContext->time_base.den = pInputAudioContext->sample_rate;
		audioindex_out = out_stream_a->index;

		pInputAudioCodec = avcodec_find_encoder(ofmt->audio_codec);
		if (!pInputAudioCodec) {
			fprintf(stderr, "Codec not found\n");
			ret = -1;
			return ret;
		}
		//if (ofmt->flags & AVFMT_GLOBALHEADER)
		pInputAudioContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
		ret = avcodec_open2(pInputAudioContext, pInputAudioCodec, NULL);
		if (ret < 0) {
			printf("Failed to open encoder!\n");
			return ret;
		}
		pAudioFrame = av_frame_alloc();
		pAudioFrame->nb_samples = pInputAudioContext->frame_size;
		pAudioFrame->format = pInputAudioContext->sample_fmt;
		pAudioFrame->channels = 1;
		//ret = av_frame_make_writable(pAudioFrame);

		m_nAudioFramSize = av_samples_get_buffer_size(NULL, pInputAudioContext->channels, pInputAudioContext->frame_size, pInputAudioContext->sample_fmt, 1);
		m_pAudioFrameBuf = (uint8_t *)av_malloc(m_nAudioFramSize);
		avcodec_fill_audio_frame(pAudioFrame, pInputAudioContext->channels, pInputAudioContext->sample_fmt, (const uint8_t*)m_pAudioFrameBuf, m_nAudioFramSize, 1);
		av_new_packet(&pkt_a, m_nAudioFramSize);

		swr = swr_alloc();
		av_opt_set_int(swr, "in_channel_layout", AV_CH_LAYOUT_MONO, 0);
		av_opt_set_int(swr, "out_channel_layout", AV_CH_LAYOUT_MONO, 0);
		av_opt_set_int(swr, "in_sample_rate", 8000, 0);
		av_opt_set_int(swr, "out_sample_rate", 8000, 0);
		av_opt_set_sample_fmt(swr, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
		av_opt_set_sample_fmt(swr, "out_sample_fmt", AV_SAMPLE_FMT_FLTP, 0);
		swr_init(swr);
		outs[0] =  new uint8_t [m_nAudioFramSize];

接下来是数据封装进去,这里困扰了我很久,这里我提醒你们首先仔细理解重采样函数的参数含义,基本你就能正确使用,但是说实话初次接触很难理解。这里我同时贴出音视频数据一起写入的代码

	if (mediaType == E_VIDEO) {
		av_init_packet(&i_pkt);
		i_pkt.size = p.len;
		i_pkt.data = p.data;
		if (isKey) {
			i_pkt.flags |= AV_PKT_FLAG_KEY;
		}
		i_pkt.pts = p.ts * 90;
		i_pkt.dts = i_pkt.pts;
		i_pkt.stream_index = videoindex_out;
		int ret = av_interleaved_write_frame(m_pOutContext, &i_pkt);
		av_free_packet(&i_pkt);
	}
	else {
		char* buf = new char[p.len * 4];
		int len = G711Decode(buf, (char*)p.data, p.len, G711ALAW);
		m_pAudioQueue->write(buf, len);
		delete buf;
	}
	{
		if (m_pAudioQueue->getUsed() >= 2048) {
			char* buf = new char[2048];
			int len = m_pAudioQueue->read(buf, 2048);
			memcpy(m_pAudioFrameBuf, buf, len);
			delete buf;
			int count = swr_convert(swr, outs, 1024, (const uint8_t**)&m_pAudioFrameBuf, 1024);
			pAudioFrame->data[0] = (uint8_t*)outs[0];
			int got_frame = 0;
			int ret = avcodec_encode_audio2(pInputAudioContext, &pkt_a, pAudioFrame, &got_frame);
			if (ret >= 0 && got_frame == 1) {
				pkt_a.pts = last_ts * 8;
				pkt_a.dts = pkt_a.pts;
				pkt_a.stream_index = audioindex_out;
				ret = av_interleaved_write_frame(m_pOutContext, &pkt_a);
				last_ts += 128;
			}
		}
	}

重点及我的难点,我的数据是480字节一包的ALAW数据,我decode为PCM之后为960字节的PCM数据,然而ACC一帧每通道需要的采样数是1024,注意我说的是采样数,不是字节数,一帧每通道需要的字节数的计算:    m_nAudioFramSize = av_samples_get_buffer_size(NULL, pInputAudioContext->channels, pInputAudioContext->frame_size, pInputAudioContext->sample_fmt, 1);你可以仔细理解这个函数 通道数*每帧采样数*数据格式 ,结合我ACC的格式配置 它=1*1024*4,所以我这里ACC一帧每通道的数据字节数是4096,而我的源数据是S16 PCM 所以我需要给swr_convert()提供2048字节,这里我要墙裂推荐一个博客https://blog.csdn.net/bixinwei22/article/details/86545497,这可以加强你对以上描述的理解,然后你再仔细理解重采样函数的参数注释,所以综上我需要一个缓冲区,以至于我能够缓冲足够的2048字节然后再提供给重采样函数,然后就是这个pts的计算,为什么我这里是pkt_a.pts = last_ts * 8; last_ts += 128;,我的音频timebase是(1,8000),我2048个字节的数据是0.125ms*1024=128ms,你猜为什么这里是1024来计算,注意理解采样数的概念,然后兄弟恭喜你,你看完了。

你可能感兴趣的:(音视频编解码)