天天看点

FFmpeg 内存H264流发布rtmp

背景

网上查了很多关于FFmpeg读取内存264直接发布成rtmp的资料,发现这方面的资料很少,最近做了这方面的功能,特此记录下。

问题描述

网上很多类似音视频转码的例子(无编解码过程,就是音视频格式重新封装),但是都是基于有输入文件,我的需求是,从内存读取一帧帧的264码流存成flv或发布成rtmp(rtmp本身的音视频格式就是flv)。

实施

1、demo程序验证

由于新版FFmpeg和旧版FFmpeg在接口上已经有发生了一些区别,所以我这边就拿最新版本的FFmpeg来做,首先用FFmpeg的remuxing.c例程出来做测试,主要是用来读取IPC出来的rtsp流存成FLV或者发布成rtmp,进过试验,存成的flv格式是正确的格式,rtmp也能正常在播放器上面播放,在flv.js上面播放都没问题(我的nginx是有集成nginx-http-flv-module的)。

下面贴出demo代码:

AVOutputFormat *ofmt = NULL;
	AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
	AVPacket pkt;
	const char *in_filename, *out_filename;
	int ret, i;
	int stream_index = 0;
	int *stream_mapping = NULL;
	int stream_mapping_size = 0;

	/*if (argc < 3) {
		printf("usage: %s input output\n"
			"API example program to remux a media file with libavformat and libavcodec.\n"
			"The output format is guessed according to the file extension.\n"
			"\n", argv[0]);
		return 1;
	}

	in_filename = argv[1];
	out_filename = argv[2];*/

	in_filename = "rtsp://admin:[email protected]:554/h264/ch1/main/av_stream?videoCodecType=H.264";
	out_filename = "rtmp://localhost:1985/live/mystream";
	//out_filename = "demo.flv";

	if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0) {
		fprintf(stderr, "Could not open input file '%s'", in_filename);
		goto end;
	}

	if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0) {
		fprintf(stderr, "Failed to retrieve input stream information");
		goto end;
	}

	av_dump_format(ifmt_ctx, 0, in_filename, 0);

	avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_filename);
	if (!ofmt_ctx) {
		fprintf(stderr, "Could not create output context\n");
		ret = AVERROR_UNKNOWN;
		goto end;
	}

	stream_mapping_size = ifmt_ctx->nb_streams;
	stream_mapping = (int*)av_mallocz_array(stream_mapping_size, sizeof(*stream_mapping));
	if (!stream_mapping) {
		ret = AVERROR(ENOMEM);
		goto end;
	}

	ofmt = ofmt_ctx->oformat;

	for (i = 0; i < ifmt_ctx->nb_streams; i++) {
		AVStream *out_stream;
		AVStream *in_stream = ifmt_ctx->streams[i];
		AVCodecParameters *in_codecpar = in_stream->codecpar;

		if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
			in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
			in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
			stream_mapping[i] = -1;
			continue;
		}

		stream_mapping[i] = stream_index++;

		out_stream = avformat_new_stream(ofmt_ctx, NULL);
		if (!out_stream) {
			fprintf(stderr, "Failed allocating output stream\n");
			ret = AVERROR_UNKNOWN;
			goto end;
		}

		ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
		if (ret < 0) {
			fprintf(stderr, "Failed to copy codec parameters\n");
			goto end;
		}
		out_stream->codecpar->codec_tag = 0;
	}
	av_dump_format(ofmt_ctx, 0, out_filename, 1);

	if (!(ofmt->flags & AVFMT_NOFILE)) {
		ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
		if (ret < 0) {
			fprintf(stderr, "Could not open output file '%s'", out_filename);
			goto end;
		}
	}

	ret = avformat_write_header(ofmt_ctx, NULL);
	if (ret < 0) {
		fprintf(stderr, "Error occurred when opening output file\n");
		goto end;
	}

	while (1) {
		AVStream *in_stream, *out_stream;

		ret = av_read_frame(ifmt_ctx, &pkt);
		if (ret < 0)
			break;

		in_stream = ifmt_ctx->streams[pkt.stream_index];
		if (pkt.stream_index >= stream_mapping_size ||
			stream_mapping[pkt.stream_index] < 0) {
			av_packet_unref(&pkt);
			continue;
		}

		pkt.stream_index = stream_mapping[pkt.stream_index];
		out_stream = ofmt_ctx->streams[pkt.stream_index];
		//log_packet(ifmt_ctx, &pkt, "in");

		/* copy packet */
		pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
		pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
		pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
		pkt.pos = -1;
		//log_packet(ofmt_ctx, &pkt, "out");

		ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
		if (ret < 0) {
			fprintf(stderr, "Error muxing packet\n");
			break;
		}
		av_packet_unref(&pkt);
	}

	av_write_trailer(ofmt_ctx);
end:

	avformat_close_input(&ifmt_ctx);

	/* close output */
	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_closep(&ofmt_ctx->pb);
	avformat_free_context(ofmt_ctx);

	av_freep(&stream_mapping);

	if (ret < 0 && ret != AVERROR_EOF) {
		fprintf(stderr, "Error occurred\n");
		return 1;
	}

	return 0;
           

2、内存H264发布rtmp

直接先贴出接口程序:

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

AVOutputFormat *ofmt = NULL;
AVFormatContext *ofmt_ctx = NULL;
//const char *out_filename = "new.flv";
const char *out_filename = "rtmp://localhost:1985/live/mystream";
int stream_index = 0;
int waitI = 0, rtmpisinit = 0;
int ptsInc = 0;

int GetSpsPpsFromH264(uint8_t* buf, int len)
{
	int i = 0;
	for (i = 0; i < len; i++) {
		if (buf[i+0] == 0x00 
			&& buf[i + 1] == 0x00
			&& buf[i + 2] == 0x00
			&& buf[i + 3] == 0x01
			&& buf[i + 4] == 0x06) {
			break;
		}
	}
	if (i == len) {
		printf("GetSpsPpsFromH264 error...");
		return 0;
	}

	printf("h264(i=%d):", i);
	for (int j = 0; j < i; j++) {
		printf("%x ", buf[j]);
	}
	return i;
}

static bool isIdrFrame2(uint8_t* buf, int len)
{
	switch (buf[0] & 0x1f) {
	case 7: // SPS
		return true;
	case 8: // PPS
		return true;
	case 5:
		return true;
	case 1:
		return false;

	default:
		return false;
		break;
	}
	return false;
}

static bool isIdrFrame1(uint8_t* buf, int size)
{
	int last = 0;
	for (int i = 2; i <= size; ++i) {
		if (i == size) {
			if (last) {
				bool ret = isIdrFrame2(buf + last, i - last);
				if (ret) {
					return true;
				}
			}
		}
		else if (buf[i - 2] == 0x00 && buf[i - 1] == 0x00 && buf[i] == 0x01) {
			if (last) {
				int size = i - last - 3;
				if (buf[i - 3]) ++size;
				bool ret = isIdrFrame2(buf + last, size);
				if (ret) {
					return true;
				}
			}
			last = i + 1;
		}
	}
	return false;
}

//初始化的时候必须把H264第一个关键帧的sps、pps数据放进来
static int RtmpInit(void* spspps_date, int spspps_datalen)
{
	int ret = 0;
	AVStream *out_stream;
	AVCodecParameters *out_codecpar;
	av_register_all();
	avformat_network_init();

	printf("rtmp init...\n");

	avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", NULL);// out_filename);
	if (!ofmt_ctx) {
		fprintf(stderr, "Could not create output context\n");
		ret = AVERROR_UNKNOWN;
		goto end;
	}

	ofmt = ofmt_ctx->oformat;

	out_stream = avformat_new_stream(ofmt_ctx, NULL);
	if (!out_stream) {
		fprintf(stderr, "Failed allocating output stream\n");
		ret = AVERROR_UNKNOWN;
		goto end;
	}
	stream_index = out_stream->index;

	//因为输入是内存读出来的一帧帧的H264数据,所以没有输入的codecpar信息,必须手动添加输出的codecpar
	out_codecpar = out_stream->codecpar;
	out_codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
	out_codecpar->codec_id = AV_CODEC_ID_H264;
	out_codecpar->bit_rate = 400000;
	out_codecpar->width = 1280;
	out_codecpar->height = 720;
	out_codecpar->codec_tag = 0;
	out_codecpar->format = AV_PIX_FMT_YUV420P;

	//必须添加extradata(H264第一帧的sps和pps数据),否则无法生成带有AVCDecoderConfigurationRecord信息的FLV
	//unsigned char sps_pps[26] = { 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f, 0x9d, 0xa8, 0x14, 0x01, 0x6e, 0x9b, 0x80, 0x80, 0x80, 0x81, 0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0x80 };
	out_codecpar->extradata_size = spspps_datalen;
	out_codecpar->extradata = (uint8_t*)av_malloc(spspps_datalen + AV_INPUT_BUFFER_PADDING_SIZE);
	if (out_codecpar->extradata == NULL)
	{ 
		printf("could not av_malloc the video params extradata!\n");			
		goto end;
	}
	memcpy(out_codecpar->extradata, spspps_date, spspps_datalen);
	
	av_dump_format(ofmt_ctx, 0, out_filename, 1);

	if (!(ofmt->flags & AVFMT_NOFILE)) {
		ret = avio_open(&ofmt_ctx->pb, out_filename, AVIO_FLAG_WRITE);
		if (ret < 0) {
			fprintf(stderr, "Could not open output file '%s'", out_filename);
			goto end;
		}
	}

	AVDictionary *opts = NULL;
	av_dict_set(&opts, "flvflags", "add_keyframe_index", 0);
	ret = avformat_write_header(ofmt_ctx, &opts);
	av_dict_free(&opts);
	if (ret < 0) {
		fprintf(stderr, "Error occurred when opening output file\n");
		goto end;
	}

	waitI = 1;
	return 0;

end:
	/* close output */
	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_closep(&ofmt_ctx->pb);
	if (ofmt_ctx) {
		avformat_free_context(ofmt_ctx);
		ofmt_ctx = NULL;
	}
	return -1;
}

static void VideoWrite(void* data, int datalen)
{
	int ret = 0, isI = 0;
	AVStream *out_stream;
	AVPacket pkt;
	
	out_stream = ofmt_ctx->streams[stream_index];

	av_init_packet(&pkt);
	isI = isIdrFrame1((uint8_t*)data, datalen);
	pkt.flags |= isI ? AV_PKT_FLAG_KEY : 0;
	pkt.stream_index = out_stream->index;
	pkt.data = (uint8_t*)data;
	pkt.size = datalen;
	//wait I frame
	if (waitI) {
		if (0 == (pkt.flags & AV_PKT_FLAG_KEY))
			return;
		else
			waitI = 0;	
	}
	
	AVRational time_base;
	time_base.den = 50;
	time_base.num = 1;
	pkt.pts = av_rescale_q((ptsInc++) * 2, time_base, out_stream->time_base);
	pkt.dts = av_rescale_q_rnd(pkt.dts, out_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
	pkt.duration = av_rescale_q(pkt.duration, out_stream->time_base, out_stream->time_base);
	pkt.pos = -1;

	/* copy packet (remuxing例子里面的)*/
	//pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
	//pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
	//pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
	//pkt.pos = -1;

	ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
	if (ret < 0) {
		fprintf(stderr, "Error muxing packet\n");
	}

	av_packet_unref(&pkt);
}

static void RtmpUnit(void)
{
	if (ofmt_ctx)
		av_write_trailer(ofmt_ctx);
	/* close output */
	if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
		avio_closep(&ofmt_ctx->pb);
	if (ofmt_ctx) {
		avformat_free_context(ofmt_ctx);
		ofmt_ctx = NULL;
	}
}

int main()
{
	//以下只是把个借口的调用方法简单罗列,具体调用要看实际情况
	
	//preturnps是输入的一帧帧264数据,iPsLength是一帧的长度
	//模拟初始化调用
	char *h264buffer = new char[iPsLength];
	memcpy(h264buffer, preturnps, iPsLength);
	printf("h264 len = %d\n", iPsLength);
	if (!rtmpisinit) {
		if (isIdrFrame1((uint8_t*)h264buffer, iPsLength)) {
				int spspps_len = GetSpsPpsFromH264((uint8_t*)h264buffer, iPsLength);
				if (spspps_len > 0) {
					char *spsbuffer = new char[spspps_len];
					memcpy(spsbuffer, h264buffer, spspps_len);
					rtmpisinit = 1;
					RtmpInit(spsbuffer, spspps_len);
					delete spsbuffer;
				}
		}
	}
	//开始推送视频数据
	if (rtmpisinit) {
		VideoWrite(h264buffer, iPsLength);
	}
	//去初始化
	RtmpUnit();
	return 0;
}
           

3、主要遇到的问题

这些接口是从remuxing.c演变而来的,但是因为我的输入是264码流,所以去掉了avformat_open_input这些FFmpeg对输入文件的一些操作,所以我输出的编解码信息就只能自己手动编辑,如果是输入文件的话,这些信息只要通过下面这个接口既可以获取

avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
           

现在我们没有输入文件,只能根据已知的信息按照如下编辑的编解码器信息

AVCodecParameters *out_codecpar = out_stream->codecpar;
	out_codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
	out_codecpar->codec_id = AV_CODEC_ID_H264;
	out_codecpar->bit_rate = 400000;
	out_codecpar->width = 1280;
	out_codecpar->height = 720;
	out_codecpar->codec_tag = 0;
	out_codecpar->format = AV_PIX_FMT_YUV420P;
           

然而,运行程序后发布出来的rtmp却没办法在flv.js上面播放,flv.js有收到码流过来,但是报错

FFmpeg 内存H264流发布rtmp

根据flv.js的出错提示,我猜测应该是缺失了AVCDecoderConfigurationRecord这个东东,为了验证,我把输出换成flv文件的方式和demo程序存成的flv对比(可以用flvanalyzer工具查看),确实是缺失了AVCDecoderConfigurationRecord。

FFmpeg 内存H264流发布rtmp

因此查阅相关资料:

RTMP推送的音视频流的封装形式和FLV格式相似,由此可知,推送H264和AAC直播流,需要首先发送"AVC sequence header"和"AAC sequence header",这两项数据包含的是重要的编码信息,没有它们,解码器将无法解码。

AVC sequence header就是AVCDecoderConfigurationRecord结构。

问题来了 ???

AVCDecoderConfigurationRecord这个东东包含的是什么内容呢?

正解:AVCDecoderConfigurationRecord.包含着是H.264解码相关比较重要的sps和pps信息,再给AVC解码器送数据流之前一定要把sps和pps信息送出,否则的话解码器不能正常解码。而且在解码器stop之后再次start之前,如seek、快进快退状态切换等,都需要重新送一遍sps和pps的信息.AVCDecoderConfigurationRecord在FLV文件中一般情况也是出现1次,也就是第一个video tag.

现在知道问题所在了,那么如何才能使flv包含AVCDecoderConfigurationRecord呢?

我查阅了AVCodecParameters这个结构体里面的各个参数,发现extradata这个数据好像得设置。这个参数的含义是:初始化解码器所需的额外二进制数据,依赖于编解码器。

/**
     * Extra binary data needed for initializing the decoder, codec-dependent.
     *
     * Must be allocated with av_malloc() and will be freed by
     * avcodec_parameters_free(). The allocated size of extradata must be at
     * least extradata_size + AV_INPUT_BUFFER_PADDING_SIZE, with the padding
     * bytes zeroed.
     */
    uint8_t *extradata;
           

得知extradata作为一个global headers,其实主要保存的是SPS、PPS等信息;

因此手动指定SPS/PPS内容,指定AVCodecParameters结构体中extradata的值;

但是,如何获取到sps和pps呢?

关于H264码流和关键帧的资料网上很多,就不详解,下面简单列出做说明:

h264编码出的NALU规律

第一帧 SPS【0 0 0 1 0x67】 PPS【0 0 0 1 0x68】 SEI【0 0 0 1 0x6】 IDR【0 0 0 1 0x65】

p帧 P【0 0 0 1 0x61】

I帧 SPS【0 0 0 1 0x67】 PPS【0 0 0 1 0x68】 IDR【0 0 0 1 0x65】

知道这个之后,我就可以从输入的H264数据帧中获取到SPS【0 0 0 1 0x67】 PPS【0 0 0 1 0x68】这部分的数据,然后送给extradata。

//因为输入是内存读出来的一帧帧的H264数据,所以没有输入的codecpar信息,必须手动添加输出的codecpar
	AVCodecParameters *out_codecpar = out_stream->codecpar;
	out_codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
	out_codecpar->codec_id = AV_CODEC_ID_H264;
	out_codecpar->bit_rate = 400000;
	out_codecpar->width = 1280;
	out_codecpar->height = 720;
	out_codecpar->codec_tag = 0;
	out_codecpar->format = AV_PIX_FMT_YUV420P;

	//必须添加extradata(H264第一帧的sps和pps数据),否则无法生成带有AVCDecoderConfigurationRecord信息的FLV
	//unsigned char sps_pps[26] = { 0x00, 0x00, 0x01, 0x67, 0x4d, 0x00, 0x1f, 0x9d, 0xa8, 0x14, 0x01, 0x6e, 0x9b, 0x80, 0x80, 0x80, 0x81, 0x00, 0x00, 0x00, 0x01, 0x68, 0xee, 0x3c, 0x80 };
	out_codecpar->extradata_size = spspps_datalen;
	out_codecpar->extradata = (uint8_t*)av_malloc(spspps_datalen + AV_INPUT_BUFFER_PADDING_SIZE);
	if (out_codecpar->extradata == NULL)
	{ 
		printf("could not av_malloc the video params extradata!\n");			
		goto end;
	}
	memcpy(out_codecpar->extradata, spspps_date, spspps_datalen);
           

经过添加这个步骤之后,内存H264存成的flv里面也有了AVCDecoderConfigurationRecord,发布出来的rtmp也可以顺利在flv.js播放。

FFmpeg 内存H264流发布rtmp

下载

源码下载:https://download.csdn.net/download/qq_22633333/11464772