背景
网上查了很多关于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有收到码流过来,但是报错
根据flv.js的出错提示,我猜测应该是缺失了AVCDecoderConfigurationRecord这个东东,为了验证,我把输出换成flv文件的方式和demo程序存成的flv对比(可以用flvanalyzer工具查看),确实是缺失了AVCDecoderConfigurationRecord。
因此查阅相关资料:
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播放。
下载
源码下载:https://download.csdn.net/download/qq_22633333/11464772