1.目前我存在h264 + h265裸流+alaw音频(也可以说是s16 pcm)。
2.目前是私有协议可以简单理解为 多个音视频包组成了一个cms文件。
3.每个音视频包有信息可以描述 当前包时间戳(类似pts),大小
4.使用ffmpeg
5.参考了许多网上内容,感谢各位大佬
6.网上太多大同小异的原创代码,所以我这里不注重提供源代码,主要是展示针对我的难点及解决办法。
7.重大提示,我的大部分代码来源于ctrl+c。
1.需要将上述环境描述的音视频流封装为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)
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了。
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来计算,注意理解采样数的概念,然后兄弟恭喜你,你看完了。