音视频转码FFmpeg
前言
音视频转码主要指这样的概念:
- 容器格式的转换,比如MP4转换为MOV
- 容器中音视频数据编码方式转换,比如H264编码转换成MPEG4编码,MP3换为AAC
- 音视频码率的转换,比如4M的视频码率降为2M
- 视频分辨率的转换,比如1080P视频变为720P,音频重采样等等
转码技术点分析
转码流程图
流拷贝是指源文件音/视频编码方式也被目标文件支持,那么此情况下音/视频数据拷贝就可以直接拷贝到目标文件下。
转码类型分析
容器格式的转换
有两种情况,第一种情况源容器格式的音/视频编码方式在目标容器格式也支持,这样只需要进行流拷贝;第二种情况源容器格式的音/视频编码昂视在目标容器不被支持,那么就需要先解码再编码
1、流拷贝方式的
流程图:
实现代码
1、解封装实现相关函数
AVFormatContext
:代表解封装相关上下文
avformat_open_input()
:对源文件进行解封装
avformat_find_streaminfo()
:查找流信息
av_frame_read()
:从源文件中读取音频或者视频AVPacket
2、封装实现相关函数
AVFormatContext
:代表封装相关上下文
avformat_alloc_output_context2()
:创建基于目标容器格式的上下文对象
avformat_new_stream()
:向封装上下文添加音/视频流
avio_open2()
:打开输出流,用于封装时写入数据
avformat_write_header()
:封装时写入头文件
av_write_frame()
:写入音/视数据
av_write_trailer()
:封装时写入尾部信息
3、完整代码
/** 实现MP4转换成MOV,FLV,TS,AVI文件,不改变编码方式*/
void Transcode::doExtensionTranscode()
{string curFile(__FILE__);unsigned long pos = curFile.find("2-video_audio_advanced");if (pos == string::npos) {LOGD("not find file");return;}string srcDic = curFile.substr(0,pos)+"filesources/";string srcPath = srcDic + "test_1280x720_3.mp4";string dstPath = srcDic + "2-test_1280x720_3.mov"; // 这里后缀可以改成flv,ts,avi mov则生成对应的文件AVFormatContext *in_fmtCtx = NULL,*ou_fmtCtx = NULL;int video_in_stream_index = -1,audio_in_stream_index = -1;int video_ou_stream_index = -1,audio_ou_stream_index = -1;int ret = 0;if ((ret = avformat_open_input(&in_fmtCtx,srcPath.c_str(),NULL,NULL)) < 0) {LOGD("avformat_open_input fail %s",av_err2str(ret));return;}if (avformat_find_stream_info(in_fmtCtx,NULL) < 0) {LOGD("avformat_find_stream_info fail %s",av_err2str(ret));return;}av_dump_format(in_fmtCtx, 0, srcPath.c_str(), 0);if ((ret = avformat_alloc_output_context2(&ou_fmtCtx,NULL,NULL,dstPath.c_str())) < 0) {LOGD("avformat_alloc_output_context2 fail %s",av_err2str(ret));return;}for (int i=0; i<in_fmtCtx->nb_streams; i++) {AVStream *stream = in_fmtCtx->streams[i];if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video_in_stream_index == -1) {video_in_stream_index = i;// 添加流信息AVStream *newStream = avformat_new_stream(ou_fmtCtx,NULL);video_ou_stream_index = newStream->index;// 将编码信息拷贝过来LOGD("fourcc %s",av_fourcc2str(newStream->codecpar->codec_tag));if ((ret = avcodec_parameters_copy(newStream->codecpar, stream->codecpar)) <0) {LOGD("avcodec_parameters_copy fail %s",av_err2str(ret));return;}/** 遇到问题:mp4转换为avi时,播放提示“reference count 1 overflow,chroma_log2_weight_denom 1696 is out of range”* 分析原因:之前的写法是没有加下面的判断条件直接为newStream->codecpar->codec_tag = 0;这样avi将选择默认的h264作为码流格式(源mp4中为avc1),导致两边不一样* 解决方案:加如下判断*/uint32_t src_codec_tag = stream->codecpar->codec_tag;if (av_codec_get_id(ou_fmtCtx->oformat->codec_tag, src_codec_tag) != newStream->codecpar->codec_id) {newStream->codecpar->codec_tag = 0;}}if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_in_stream_index == -1) {audio_in_stream_index = i;AVStream *newStream = avformat_new_stream(ou_fmtCtx,NULL);audio_ou_stream_index = newStream->index;if ((ret = avcodec_parameters_copy(newStream->codecpar, stream->codecpar)) <0) {LOGD("avcodec_parameters_copy2 fail %s",av_err2str(ret));return;}/** 遇到问题:avformat_write_header()函数提示"Tag mp4a incompatible with output codec id '86018'"* 分析原因:code_tag代表了音视频数据采用的码流格式。拿aac举例,AVI和MP4都支持aac编码的音频数据存储,MP4支持MKTAG('m', 'p', '4',* 'a')码流格式的aac流,avi支持(0x00ff,0x1600,0x706d等),显然两者是不一样的,上面avcodec_parameters_copy()就相当于让封装和解封装的* code_tag标签一模一样,所以造成了不一致的问题* 解决方案:将codecpar->codec_tag=0,系统会默认选择第一个匹配编码方式的codec_tag值*/uint32_t src_codec_tag = stream->codecpar->codec_tag;if (av_codec_get_id(ou_fmtCtx->oformat->codec_tag, src_codec_tag) != newStream->codecpar->codec_id) {newStream->codecpar->codec_tag = 0;}}}// 当flags没有AVFMT_NOFILE标记的时候才能调用avio_open2()函数进行初始化if (!(ou_fmtCtx->flags & AVFMT_NOFILE)) {if ((ret = avio_open2(&ou_fmtCtx->pb,dstPath.c_str(),AVIO_FLAG_WRITE,NULL,NULL)) < 0) {LOGD("avio_open2 fail %d",ret);return;}}// 写入文件头信息if ((ret = avformat_write_header(ou_fmtCtx, NULL)) < 0) {LOGD("avformat_write_header fail %s",av_err2str(ret));return;}av_dump_format(ou_fmtCtx, 0, dstPath.c_str(), 1);AVPacket *in_packet = av_packet_alloc();while ((av_read_frame(in_fmtCtx, in_packet)) == 0) {if (in_packet->stream_index != video_in_stream_index && in_packet->stream_index != audio_in_stream_index) {continue;}
// LOGD("write packet index %d size %d",in_packet->stream_index,in_packet->size);AVStream *in_stream = in_fmtCtx->streams[in_packet->stream_index];AVStream *ou_stream = NULL;if (in_stream->index == video_in_stream_index) {ou_stream = ou_fmtCtx->streams[video_ou_stream_index];} else {ou_stream = ou_fmtCtx->streams[audio_ou_stream_index];}/** 每个packet中pts,dts,duration 转换成浮点数时间的公式(以pts为例):pts * timebase.num/timebase.den*/in_packet->pts = av_rescale_q_rnd(in_packet->pts,in_stream->time_base,ou_stream->time_base,AV_ROUND_UP);in_packet->dts = av_rescale_q_rnd(in_packet->dts,in_stream->time_base,ou_stream->time_base,AV_ROUND_UP);in_packet->duration = av_rescale_q_rnd(in_packet->duration, in_stream->time_base, ou_stream->time_base, AV_ROUND_UP);in_packet->stream_index = ou_stream->index;/** 遇到问题:提示"H.264 bitstream malformed, no startcode found, use the video bitstream filter 'h264_mp4toannexb' to fix it ('-bsf:v* h264_mp4toannexb' option with ffmpeg)"* 分析原因:MP4中h264编码的视频码流格式为avcc(即每个NALU的前面都加了四个大端序的字节,表示每个NALU的长度),而avi中h264* 编码的视频码流格式为annexb(即每个NALU的前面是0001或者001开头的开始码),两者不一致。* 解决方案:将开头的四个字节替换掉*/
// if (in_packet->stream_index == video_ou_stream_index) {// uint8_t *orgData = in_packet->data;
// orgData[0] = 0;
// orgData[1] = 0;
// orgData[2] = 0;
// orgData[3] = 1;
// }
// printBuffertoHex(in_packet->data,in_packet->size);av_write_frame(ou_fmtCtx, in_packet);av_packet_unref(in_packet);}av_write_trailer(ou_fmtCtx);avformat_close_input(&in_fmtCtx);avformat_free_context(ou_fmtCtx);
}
函数详解
enum AVCodecID av_codec_get_id(const struct AVCodecTag * const *tags, unsigned int tag);
用于获取编码器支持的tags对应tag的codeid
结构体:
struct AVCodecTag;
该结构体唯一代表了一种编码方式的码流格式,由编码方式(AVCodecId)和码流标签共同决定。比如h264,它有avcc和annexb两种码流格式,对于avi,它采用annexb格式存储h264的视频数据,而对于mp4,它则采用avcc格式存储h264视频数据,所以就算是流拷贝,再进行不同容器格式的封装时也要考虑码流格式是否一致
遇到问题
1、mp4转换为avi时,播放提示“reference count 1 overflow,chroma_log2_weight_denom 1696 is out of range”
分析原因:
之前的写法是没有加下面的判断条件直接为newStream->codecpar->codec_tag = 0;这样avi将选择默认的h264作为码流格式(源mp4中为avc1),导致两边不一样
解决方案:
如上代码添加判断,详情上面代码
2、提示"H.264 bitstream malformed, no startcode found, use the video bitstream filter
'h264_mp4toannexb' to fix it ('-bsf:v
* h264_mp4toannexb' option with ffmpeg)"
分析原因:
MP4中h264编码的视频码流格式为avcc(即每个NALU的前面都加了四个大端序的字节,表示每个NALU的长度),而avi中h264* 编码的视频码流格式为annexb(即每个NALU的前面是0001或者001开头的开始码),两者不一致。
解决方案:
将开头的四个字节替换掉
2、重新编解码方式
采用重新编解码方式意味着解封装之后得先对源音视频数据进行解码,然后将得到的未压缩音视频数据按照目标编码方式再次编码,最后将得到的压缩数据按照指定的容器格式进行封装
流程图
技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、大厂面试题、和热门技术教学视频资料共享在里面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等等.),有需要的可以自行添加哦!~
实现代码
/** 做转码(包括编码方式,码率,分辨率,采样率,采样格式等等的转换)*/
void Transcode::doTranscode()
{string curFile(__FILE__);unsigned long pos = curFile.find("2-video_audio_advanced");if (pos == string::npos) {LOGD("cannot find file");return;}int ret = 0;string srcDic = curFile.substr(0,pos) + "filesources/";srcPath = srcDic + "test_1280x720_3.mp4";dstPath = srcDic + "1-test_1280x720_3.mov";// 做编码方式的转码,目标封装格式要支持 第一个参数代表是否引用源文件的参数,如果为true则忽略第二个参数,否则使用第二个参数// 视频dst_video_id = Par_CodeId(false,AV_CODEC_ID_MPEG4);dst_width = Par_Int32t(false,1280);dst_height = Par_Int32t(false,720);dst_video_bit_rate = Par_Int64t(true,9000000); // 0.9Mb/sdst_pix_fmt = Par_PixFmt(false,AV_PIX_FMT_NV12);dst_fps = Par_Int32t(true,50); //fpsvideo_need_transcode = true;// 音频dst_audio_id = Par_CodeId(false,AV_CODEC_ID_MP3);dst_audio_bit_rate = Par_Int64t(true,128*1000);dst_sample_rate = Par_Int32t(true,44100); // 44.1khzdst_sample_fmt = Par_SmpFmt(false,AV_SAMPLE_FMT_FLTP);dst_channel_layout = Par_Int64t(false,AV_CH_LAYOUT_STEREO); // 双声道audio_need_transcode = true;// 打开输入文件及输出文件的上下文if (!openFile()) {LOGD("openFile()");return;}// 为输出文件添加视频流if (video_in_stream_index != -1 && video_need_transcode) {if (!add_video_stream()) {LOGD("add_video_stream fai");return;}}// 为输出文件添加音频流if (audio_in_stream_index != -1 && audio_need_transcode) {if (!add_audio_stream()) {LOGD("add_audio_stream()");return;}}av_dump_format(ouFmtCtx, 0, dstPath.c_str(), 1);// 打开解封装的上下文if (!(ouFmtCtx->oformat->flags & AVFMT_NOFILE)) {if (avio_open(&ouFmtCtx->pb, dstPath.c_str(), AVIO_FLAG_WRITE) < 0) {LOGD("avio_open fail");releaseSources();return;}}// 写入头信息/** 遇到问题:avformat_write_header()崩溃* 分析原因:没有调用avio_open()函数* 解决方案:写成 !(ouFmtCtx->flags & AVFMT_NOFILE)即可*/if ((ret = avformat_write_header(ouFmtCtx, NULL)) < 0) {LOGD("avformat_write_header fail %d",ret);releaseSources();return;}// 读取源文件中的音视频数据进行解码AVPacket *inPacket = av_packet_alloc();while (av_read_frame(inFmtCtx, inPacket) == 0) {// 说明读取的视频数据if (inPacket->stream_index == video_in_stream_index && video_need_transcode) {doDecodeVideo(inPacket);}// 说明读取的音频数据if (inPacket->stream_index == audio_in_stream_index && audio_need_transcode) {doDecodeAudio(inPacket);}// 因为每一次读取的AVpacket的数据大小不一样,所以用完之后要释放av_packet_unref(inPacket);}LOGD("文件读取完毕");// 刷新解码缓冲区,获取缓冲区中数据if (video_in_stream_index != -1 && video_need_transcode) {doDecodeVideo(NULL);}if (audio_in_stream_index != -1 && audio_need_transcode) {doDecodeAudio(NULL);}// 写入尾部信息if (ouFmtCtx) {av_write_trailer(ouFmtCtx);}LOGD("结束");// 释放资源releaseSources();
}bool Transcode::openFile()
{if (!srcPath.length()) {return false;}int ret = 0;// 打开输入文件if ((ret = avformat_open_input(&inFmtCtx, srcPath.c_str(), NULL, NULL)) < 0) {LOGD("avformat_open_input() fail");releaseSources();return false;}if ((ret = avformat_find_stream_info(inFmtCtx, NULL)) < 0) {LOGD("avformat_find_stream_info fail %d",ret);releaseSources();return false;}// 输出输入文件信息av_dump_format(inFmtCtx,0,srcPath.c_str(),0);for (int i=0; i<inFmtCtx->nb_streams; i++) {AVCodecParameters *codecpar = inFmtCtx->streams[i]->codecpar;if(codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video_in_stream_index == -1) {src_video_id = codecpar->codec_id;video_in_stream_index = i;}if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_in_stream_index == -1) {src_audio_id = codecpar->codec_id;audio_in_stream_index = i;}}// 打开输出流if ((ret = avformat_alloc_output_context2(&ouFmtCtx, NULL, NULL, dstPath.c_str())) < 0) {LOGD("avformat_alloc_context fail");releaseSources();return false;}// 检查目标编码方式是否被支持if (video_in_stream_index != -1 && av_codec_get_tag(ouFmtCtx->oformat->codec_tag, dst_video_id.par_val.codecId) == 0) {LOGD("video tag not found");releaseSources();return false;}if (audio_in_stream_index != -1 && av_codec_get_tag(ouFmtCtx->oformat->codec_tag,dst_audio_id.par_val.codecId) == 0) {LOGD("audio tag not found");releaseSources();return false;}return true;
}static int select_sample_rate(AVCodec *codec,int rate)
{int best_rate = 0;int deft_rate = 44100;bool surport = false;const int* p = codec->supported_samplerates;while (*p) {best_rate = *p;if (*p == rate) {surport = true;break;}p++;}if (best_rate != rate && best_rate != 0 && best_rate != deft_rate) {return deft_rate;}return best_rate;
}static enum AVSampleFormat select_sample_format(AVCodec *codec,enum AVSampleFormat fmt)
{enum AVSampleFormat retfmt = AV_SAMPLE_FMT_NONE;enum AVSampleFormat deffmt = AV_SAMPLE_FMT_FLTP;const enum AVSampleFormat * fmts = codec->sample_fmts;while (*fmts != AV_SAMPLE_FMT_NONE) {retfmt = *fmts;if (retfmt == fmt) {break;}fmts++;}if (retfmt != fmt && retfmt != AV_SAMPLE_FMT_NONE && retfmt != deffmt) {return deffmt;}return retfmt;
}static int64_t select_channel_layout(AVCodec *codec,int64_t ch_layout)
{int64_t retch = 0;int64_t defch = AV_CH_LAYOUT_STEREO;const uint64_t * chs = codec->channel_layouts;while (*chs) {retch = *chs;if (retch == ch_layout) {break;}chs++;}if (retch != ch_layout && retch != AV_SAMPLE_FMT_NONE && retch != defch) {return defch;}return retch;
}static enum AVPixelFormat select_pixel_format(AVCodec *codec,enum AVPixelFormat fmt) {enum AVPixelFormat retpixfmt = AV_PIX_FMT_NONE;enum AVPixelFormat defaltfmt = AV_PIX_FMT_YUV420P;const enum AVPixelFormat *fmts = codec->pix_fmts;while (*fmts != AV_PIX_FMT_NONE) {retpixfmt = *fmts;if (retpixfmt == fmt) {break;}fmts++;}if (retpixfmt != fmt && retpixfmt != AV_PIX_FMT_NONE && retpixfmt != defaltfmt) {return defaltfmt;}return retpixfmt;
}bool Transcode::add_video_stream()
{AVCodec *codec = avcodec_find_encoder(dst_video_id.par_val.codecId);if (codec == NULL) {LOGD("video code find fail");releaseSources();return false;}AVStream *stream = avformat_new_stream(ouFmtCtx, NULL);if (!stream) {LOGD("avformat_new_stream fail");return false;}video_ou_stream_index = stream->index;video_en_ctx = avcodec_alloc_context3(codec);if (video_en_ctx == NULL) {releaseSources();LOGD("video codec not found");return false;}AVCodecParameters *inpar = inFmtCtx->streams[video_in_stream_index]->codecpar;// 设置编码相关参数video_en_ctx->codec_id = codec->id;// 设置码率video_en_ctx->bit_rate = dst_video_bit_rate.copy?inpar->bit_rate:dst_video_bit_rate.par_val.i64_val;// 设置视频宽video_en_ctx->width = dst_width.copy?inpar->width:dst_width.par_val.i32_val;// 设置视频高video_en_ctx->height = dst_height.copy?inpar->height:dst_height.par_val.i32_val;// 设置帧率int fps = dst_fps.copy?inFmtCtx->streams[video_in_stream_index]->r_frame_rate.num:dst_fps.par_val.i32_val;video_en_ctx->framerate = (AVRational){fps,1};// 设置时间基;stream->time_base = (AVRational){1,video_en_ctx->framerate.num};video_en_ctx->time_base = stream->time_base;// I帧间隔,决定了压缩率video_en_ctx->gop_size = 12;// 设置视频像素格式enum AVPixelFormat want_pix_fmt = dst_pix_fmt.copy?(AVPixelFormat)inpar->format:dst_pix_fmt.par_val.pix_fmt;enum AVPixelFormat result_pix_fmt = select_pixel_format(codec, want_pix_fmt);if (result_pix_fmt == AV_PIX_FMT_NONE) {LOGD("can not surport pix fmt");releaseSources();return false;}video_en_ctx->pix_fmt = result_pix_fmt;// 每组I帧间B帧的最大个数if (codec->id == AV_CODEC_ID_MPEG2VIDEO) {video_en_ctx->max_b_frames = 2;}/* Needed to avoid using macroblocks in which some coeffs overflow.* This does not happen with normal video, it just happens here as* the motion of the chroma plane does not match the luma plane. */if (codec->id == AV_CODEC_ID_MPEG1VIDEO) {video_en_ctx->mb_decision = 2;}// 判断是否需要进行格式转换if (result_pix_fmt != (enum AVPixelFormat)inpar->format || video_en_ctx->width != inpar->width || video_en_ctx->height != inpar->height) {video_need_convert = true;}// 对应一些封装器需要添加这个标记/** 遇到问题:生成的mp4或者mov文件没有显示用于预览的小图片* 分析原因:之前代编码器器flags标记设置的为AVFMT_GLOBALHEADER(错了),正确的值应该是AV_CODEC_FLAG_GLOBAL_HEADER* 解决思路:使用如下代码设置AV_CODEC_FLAG_GLOBAL_HEADER*/if (ouFmtCtx->oformat->flags & AVFMT_GLOBALHEADER) {// video_en_ctx->flags |= AVFMT_GLOBALHEADER;video_en_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;}// x264编码特有参数if (video_en_ctx->codec_id == AV_CODEC_ID_H264) {av_opt_set(video_en_ctx->priv_data,"preset","slow",0);video_en_ctx->flags |= AV_CODEC_FLAG2_LOCAL_HEADER;}int ret = 0;if ((ret = avcodec_open2(video_en_ctx, video_en_ctx->codec, NULL)) < 0) {LOGD("video encode open2() fail %d",ret);releaseSources();return false;}if((ret = avcodec_parameters_from_context(stream->codecpar, video_en_ctx)) < 0) {releaseSources();LOGD("video avcodec_parameters_from_context fail");return false;}return true;
}bool Transcode::add_audio_stream()
{if (ouFmtCtx == NULL) {LOGD("audio outformat NULL");releaseSources();return false;}// 添加一个音频流AVStream *stream = avformat_new_stream(ouFmtCtx, NULL);if (!stream) {LOGD("avformat_new_stream fail");return false;}audio_ou_stream_index = stream->index;AVCodecParameters *incodecpar = inFmtCtx->streams[audio_in_stream_index]->codecpar;AVCodec *codec = avcodec_find_encoder(dst_audio_id.par_val.codecId);AVCodecContext *ctx = avcodec_alloc_context3(codec);if (ctx == NULL) {LOGD("audio codec ctx NULL");releaseSources();return false;}// 设置音频编码参数// 采样率int want_sample_rate = dst_sample_rate.copy?incodecpar->sample_rate:dst_sample_rate.par_val.i32_val;int relt_sample_rate = select_sample_rate(codec, want_sample_rate);if (relt_sample_rate == 0) {LOGD("cannot surpot sample_rate");releaseSources();return false;}ctx->sample_rate = relt_sample_rate;// 采样格式enum AVSampleFormat want_sample_fmt = dst_sample_fmt.copy?(enum AVSampleFormat)incodecpar->format:dst_sample_fmt.par_val.smp_fmt;enum AVSampleFormat relt_sample_fmt = select_sample_format(codec, want_sample_fmt);if (relt_sample_fmt == AV_SAMPLE_FMT_NONE) {LOGD("cannot surpot sample_fmt");releaseSources();return false;}ctx->sample_fmt = relt_sample_fmt;// 声道格式int64_t want_ch = dst_channel_layout.copy?incodecpar->channel_layout:dst_channel_layout.par_val.i64_val;int64_t relt_ch = select_channel_layout(codec, want_ch);if (!relt_ch) {LOGD("cannot surpot channel_layout");releaseSources();return false;}ctx->channel_layout = relt_ch;// 声道数ctx->channels = av_get_channel_layout_nb_channels(ctx->channel_layout);// 编码后的码率ctx->bit_rate = dst_audio_bit_rate.copy?incodecpar->bit_rate:dst_audio_bit_rate.par_val.i32_val;// frame_size 这里不用设置,调用avcodec_open2()函数后会根据编码类型自动设置
// ctx->frame_size = incodecpar->frame_size;// 设置时间基ctx->time_base = (AVRational){1,ctx->sample_rate};stream->time_base = ctx->time_base;// 保存audio_en_ctx = ctx;// 设置编码的相关标记,这样进行封装的时候不会漏掉一些元信息if (ouFmtCtx->oformat->flags & AVFMT_GLOBALHEADER) {audio_en_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;}// 初始化编码器if (avcodec_open2(ctx, codec, NULL) < 0) {LOGD("audio avcodec_open2() fail");releaseSources();return false;}if (audio_en_ctx->frame_size != incodecpar->frame_size || relt_sample_fmt != incodecpar->format || relt_ch != incodecpar->channel_layout || relt_sample_rate != incodecpar->sample_rate) {audio_need_convert = true;}int ret = 0;// 将编码信息设置到音频流中if ((ret = avcodec_parameters_from_context(stream->codecpar, ctx)) < 0) {LOGD("audio copy audio stream fail");releaseSources();return false;}// 初始化重采样if (audio_need_convert) {swr_ctx = swr_alloc_set_opts(NULL, ctx->channel_layout, ctx->sample_fmt, ctx->sample_rate, incodecpar->channel_layout, (enum AVSampleFormat)incodecpar->format, incodecpar->sample_rate, 0, NULL);if ((ret = swr_init(swr_ctx)) < 0) {LOGD("swr_alloc_set_opts() fail %d",ret);releaseSources();return false;}}return true;
}void Transcode::doDecodeVideo(AVPacket *inpacket)
{if (src_video_id == AV_CODEC_ID_NONE) {LOGD("src has not video");return;}int ret = 0;if (video_de_ctx == NULL) {AVCodec *codec = avcodec_find_decoder(src_video_id);video_de_ctx = avcodec_alloc_context3(codec);if (video_de_ctx == NULL) {LOGD("video decodec context create fail");releaseSources();return;}// 设置解码参数;这里是直接从AVCodecParameters中拷贝AVCodecParameters *codecpar = inFmtCtx->streams[video_in_stream_index]->codecpar;if ((ret = avcodec_parameters_to_context(video_de_ctx, codecpar)) < 0) {LOGD("avcodec_parameters_to_context fail %d",ret);releaseSources();return;}// 初始化解码器if (avcodec_open2(video_de_ctx, codec, NULL) < 0) {releaseSources();LOGD("video avcodec_open2 fail");return;}}if (video_de_frame == NULL) {video_de_frame = av_frame_alloc();}if ((ret = avcodec_send_packet(video_de_ctx,inpacket)) < 0) {LOGD("video avcodec_send_packet fail %s",av_err2str(ret));releaseSources();return;}while (true) {// 从解码缓冲区接收解码后的数据if((ret = avcodec_receive_frame(video_de_ctx, video_de_frame)) < 0) {if (ret == AVERROR_EOF) {// 解码缓冲区结束了,那么也要flush编码缓冲区doEncodeVideo(NULL);}break;}// 如果需要格式转换 则在这里进行格式转换if (video_en_frame == NULL) {video_en_frame = get_video_frame(video_en_ctx->pix_fmt, video_en_ctx->width, video_en_ctx->height);if (video_en_frame == NULL) {LOGD("cannot create vdioe fram");releaseSources();return;}}if (video_need_convert) {// 分配像素转换上下文if (sws_ctx == NULL) {AVCodecParameters *inpar = inFmtCtx->streams[video_in_stream_index]->codecpar;int dst_width = video_en_ctx->width;int dst_height = video_en_ctx->height;enum AVPixelFormat dst_pix_fmt = video_en_ctx->pix_fmt;sws_ctx = sws_getContext(inpar->width, inpar->height, (enum AVPixelFormat)inpar->format,dst_width, dst_height, dst_pix_fmt, SWS_BICUBIC, NULL, NULL, NULL);if (!sws_ctx) {LOGD("sws_getContext fail");releaseSources();return;}}// 进行转换;返回目标heightret = sws_scale(sws_ctx, video_de_frame->data, video_de_frame->linesize, 0, video_de_frame->height, video_en_frame->data, video_en_frame->linesize);if (ret < 0) {LOGD("sws_scale fail");releaseSources();return;}} else {// 直接拷贝即可av_frame_copy(video_en_frame, video_de_frame);}// 数据拷贝到用于编码的/** 遇到问题:warning, too many B-frames in a row* 分析原因:之前下面doEncodeVideo()函数传递的参数为video_de_frame,这个是解码之后直接得到的AVFrame,它本身就包含了* 相关和编码不符合的参数* 解决方案:重新创建一个AVFrame,将解码后得到的AVFrame的data数据拷贝过去。然后用这个作为编码的AVFrame*/video_en_frame->pts = video_pts;video_pts++;doEncodeVideo(video_en_frame);}
}/** 遇到问题:生成的MOV和MP4文件可以播放,但是看不到小的预览图*/
void Transcode::doEncodeVideo(AVFrame *frame)
{int ret = 0;// 获得了解码后的数据,然后进行重新编码if ((ret = avcodec_send_frame(video_en_ctx, frame)) < 0) {LOGD("video encode avcodec_send_frame fail");return;}while (true) {AVPacket *pkt = av_packet_alloc();if((ret = avcodec_receive_packet(video_en_ctx, pkt)) < 0) {break;}// 将编码后的数据写入文件/** 遇到问题:生成的文件帧率不对* 分析原因:当调用avformat_write_header()后,内部会改变输出流的time_base参数,而这里没有重新调整packet的pts,dts,duration的值* 解决方案:使用av_packet_rescale_ts重新调整packet的pts,dts,duration的值*/AVStream *stream = ouFmtCtx->streams[video_ou_stream_index];av_packet_rescale_ts(pkt, video_en_ctx->time_base, stream->time_base);pkt->stream_index = video_ou_stream_index;doWrite(pkt, true);}
}void Transcode::doDecodeAudio(AVPacket *packet)
{int ret = 0;if (audio_de_ctx == NULL) {AVCodec *codec = avcodec_find_decoder(src_audio_id);if (!codec) {LOGD("audio decoder not found");releaseSources();return;}audio_de_ctx = avcodec_alloc_context3(codec);if (!audio_de_ctx) {LOGD("audio decodec_ctx fail");releaseSources();return;}// 设置音频解码上下文参数;这里来自于源文件的拷贝if (avcodec_parameters_to_context(audio_de_ctx, inFmtCtx->streams[audio_in_stream_index]->codecpar) < 0) {LOGD("audio set decodec ctx fail");releaseSources();return;}if ((avcodec_open2(audio_de_ctx, codec, NULL)) < 0) {LOGD("audio avcodec_open2 fail");releaseSources();return;}}// 创建解码用的AVFrameif (!audio_de_frame) {audio_de_frame = av_frame_alloc();}if ((ret = avcodec_send_packet(audio_de_ctx, packet)) < 0) {LOGD("audio avcodec_send_packet fail %d",ret);releaseSources();return;}while (true) {if ((ret = avcodec_receive_frame(audio_de_ctx, audio_de_frame)) < 0) {if (ret == AVERROR_EOF) {LOGD("audio decode finish");// 解码缓冲区结束了,那么也要flush编码缓冲区doEncodeAudio(NULL);}break;}// 创建编码器用的AVFrameif (audio_en_frame == NULL) {audio_en_frame = get_audio_frame(audio_en_ctx->sample_fmt, audio_en_ctx->channel_layout, audio_en_ctx->sample_rate, audio_en_ctx->frame_size);if (audio_en_frame == NULL) {LOGD("can not create audio frame ");releaseSources();return;}}// 解码成功,然后再重新进行编码;// 为了避免数据污染,所以这里只需要要将解码后得到的AVFrame中data数据拷贝到编码用的audio_en_frame中,解码后的其它数据则丢弃int pts_num = 0;if (audio_need_convert) {int dst_nb_samples = (int)av_rescale_rnd(swr_get_delay(swr_ctx, audio_de_frame->sample_rate)+audio_de_frame->nb_samples, audio_en_ctx->sample_rate, audio_de_frame->sample_rate, AV_ROUND_UP);if (dst_nb_samples != audio_en_frame->nb_samples) {av_frame_free(&audio_en_frame);audio_en_frame = get_audio_frame(audio_en_ctx->sample_fmt, audio_en_ctx->channel_layout, audio_en_ctx->sample_rate, dst_nb_samples);if (audio_en_frame == NULL) {LOGD("can not create audio frame2 ");releaseSources();return;}}/** 遇到问题:当音频编码方式不一致时转码后无声音* 分析原因:因为每个编码方式对应的AVFrame中的nb_samples不一样,所以再进行编码前要进行AVFrame的转换* 解决方案:进行编码前先转换*/// 进行转换ret = swr_convert(swr_ctx, audio_en_frame->data, dst_nb_samples, (const uint8_t**)audio_de_frame->data, audio_de_frame->nb_samples);if (ret < 0) {LOGD("swr_convert() fail %d",ret);releaseSources();doEncodeAudio(NULL);break;}pts_num = ret;} else {av_frame_copy(audio_en_frame, audio_de_frame);pts_num = audio_en_frame->nb_samples;}/** 遇到问题:得到的文件播放时音画不同步* 分析原因:由于音频的AVFrame的pts没有设置对;pts是基于AVCodecContext的时间基的时间,所以pts的设置公式:* 音频:pts = (timebase.den/sample_rate)*nb_samples*index; index为当前第几个音频AVFrame(索引从0开始),nb_samples为每个AVFrame中的采样数* 视频:pts = (timebase.den/fps)*index;* 解决方案:按照如下代码方式设置AVFrame的pts*/
// audio_en_frame->pts = audio_pts++; // 造成了音画不同步的问题audio_en_frame->pts = av_rescale_q(audio_pts, (AVRational){1,audio_en_frame->sample_rate}, audio_de_ctx->time_base);audio_pts += pts_num;doEncodeAudio(audio_en_frame);}
}void Transcode::doEncodeAudio(AVFrame *frame)
{int ret = 0;if ((ret = avcodec_send_frame(audio_en_ctx, frame)) < 0) {LOGD("audio avcodec_send_frame fail %d",ret);releaseSources();}while (true) {AVPacket *pkt = av_packet_alloc();if ((ret = avcodec_receive_packet(audio_en_ctx, pkt)) < 0) {break;}// 说明编码得到了一个完整的帧// 因为调用avformat_write_header()函数后内部会改变ouFmtCtx->streams[audio_ou_stream_index]->time_base的值,而// pkt是按照audio_en_ctx->time_base来进行编码的,所以这里写入文件之前要进行时间的转换AVStream *stream = ouFmtCtx->streams[audio_ou_stream_index];av_packet_rescale_ts(pkt, audio_en_ctx->time_base, stream->time_base);pkt->stream_index = audio_ou_stream_index;doWrite(pkt, false);}
}void Transcode::doWrite(AVPacket *packet,bool isVideo)
{if (!packet) {LOGD("packet is null");return;}AVPacket *a_pkt = NULL;AVPacket *v_pkt = NULL;AVPacket *w_pkt = NULL;if (isVideo) {v_pkt = packet;} else {a_pkt = packet;}// 为了精确的比较时间,先缓存一点点数据if (last_video_pts == 0 && isVideo && videoCache.size() == 0) {videoCache.push_back(packet);last_video_pts = packet->pts;LOGD(" 还没有 %s 数据",audio_pts==0?"audio":"video");return;}if (last_audio_pts == 0 && !isVideo && audioCache.size() == 0) {audioCache.push_back(packet);last_audio_pts = packet->pts;LOGD(" 还没有 %s 数据",video_pts==0?"video":"audio");return;}if (videoCache.size() > 0) { // 里面有缓存了数据v_pkt = videoCache.front();if (isVideo) {videoCache.push_back(packet);}}if (audioCache.size() > 0) { // 里面有缓存了数据a_pkt = audioCache.front();if (!isVideo) {audioCache.push_back(packet);}}if (v_pkt) {last_video_pts = v_pkt->pts;}if (a_pkt) {last_audio_pts = a_pkt->pts;}if (a_pkt && v_pkt) { // 两个都有 则进行时间的比较AVStream *a_stream = ouFmtCtx->streams[audio_ou_stream_index];AVStream *v_stream = ouFmtCtx->streams[video_ou_stream_index];if (av_compare_ts(last_audio_pts,a_stream->time_base,last_video_pts,v_stream->time_base) <= 0) { // 视频在后w_pkt = a_pkt;if (audioCache.size() > 0) {vector<AVPacket*>::iterator begin = audioCache.begin();audioCache.erase(begin);}} else {w_pkt = v_pkt;if (videoCache.size() > 0) {vector<AVPacket*>::iterator begin = videoCache.begin();videoCache.erase(begin);}}} else if (a_pkt) {w_pkt = a_pkt;if (audioCache.size() > 0) {vector<AVPacket*>::iterator begin = audioCache.begin();audioCache.erase(begin);}} else if (v_pkt) {w_pkt = v_pkt;if (videoCache.size() > 0) {vector<AVPacket*>::iterator begin = videoCache.begin();videoCache.erase(begin);}}static int sum = 0;if (!isVideo) {sum++;}LOGD("index %d pts %d dts %d du %d a_size %d v_size %d sum %d",w_pkt->stream_index,w_pkt->pts,w_pkt->dts,w_pkt->duration,audioCache.size(),videoCache.size(),sum);if ((av_interleaved_write_frame(ouFmtCtx, w_pkt)) < 0) {LOGD("av_write_frame fail");}av_packet_unref(w_pkt);
}AVFrame* Transcode::get_audio_frame(enum AVSampleFormat smpfmt,int64_t ch_layout,int sample_rate,int nb_samples)
{AVFrame * audio_en_frame = av_frame_alloc();// 根据采样格式,采样率,声道类型以及采样数分配一个AVFrameaudio_en_frame->sample_rate = sample_rate;audio_en_frame->format = smpfmt;audio_en_frame->channel_layout = ch_layout;audio_en_frame->nb_samples = nb_samples;int ret = 0;if ((ret = av_frame_get_buffer(audio_en_frame, 0)) < 0) {LOGD("audio get frame buffer fail %d",ret);return NULL;}if ((ret = av_frame_make_writable(audio_en_frame)) < 0) {LOGD("audio av_frame_make_writable fail %d",ret);return NULL;}return audio_en_frame;
}AVFrame* Transcode::get_video_frame(enum AVPixelFormat pixfmt,int width,int height)
{AVFrame *video_en_frame = av_frame_alloc();video_en_frame->format = pixfmt;video_en_frame->width = width;video_en_frame->height = height;int ret = 0;if ((ret = av_frame_get_buffer(video_en_frame, 0)) < 0) {LOGD("video get frame buffer fail %d",ret);return NULL;}if ((ret = av_frame_make_writable(video_en_frame)) < 0) {LOGD("video av_frame_make_writable fail %d",ret);return NULL;}return video_en_frame;
}void Transcode::releaseSources()
{if (inFmtCtx) {avformat_close_input(&inFmtCtx);inFmtCtx = NULL;}if (ouFmtCtx) {avformat_free_context(ouFmtCtx);ouFmtCtx = NULL;}if (video_en_ctx) {avcodec_free_context(&video_en_ctx);video_en_ctx = NULL;}if (audio_en_ctx) {avcodec_free_context(&audio_en_ctx);audio_en_ctx = NULL;}if (video_de_ctx) {avcodec_free_context(&video_de_ctx);video_de_ctx = NULL;}if (audio_de_ctx) {avcodec_free_context(&audio_de_ctx);audio_de_ctx = NULL;}if (video_de_frame) {av_frame_free(&video_de_frame);video_de_frame = NULL;}if (audio_de_frame) {av_frame_free(&audio_de_frame);}if (video_en_frame) {av_frame_free(&video_en_frame);video_en_frame = NULL;}if (audio_en_frame) {av_frame_free(&audio_en_frame);audio_en_frame = NULL;}
}
如下条件的转码会使用重新编解码方式:
- 源文件和目标文件采用不同编码方式
- 源文件和目标文件视频的分辨率不同
- 源文件和目标文件视频流或者音频流的码率不一样
- 源文件和目标文件音频流的采样率,采用格式,声道类型等不一样。
重新编解码方式实现转码还是遇到了不少坑的,如下都是遇到坑的记录。
遇到问题
1、遇到问题:avformat_write_header()
崩溃
分析原因:没有调用avio_open()
函数
解决方案:写成 !(ouFmtCtx->flags & AVFMT_NOFILE)
即可
2、遇到问题:生成的mp4或者mov文件没有显示用于预览的小图片
分析原因:之前代编码器器flags标记设置的为AVFMT_GLOBALHEADER
(错了),正确的值应该是AV_CODEC_FLAG_GLOBAL_HEADER
解决思路:使用如下代码设置AV_CODEC_FLAG_GLOBAL_HEADER
3、遇到问题:warning, too many B-frames in a row
分析原因:之前下面doEncodeVideo()
函数传递的参数为video_de_frame
,这个是解码之后直接得到的AVFrame
,它本身就包含了相关和编码不符合的参数
解决方案:重新创建一个AVFrame
,将解码后得到的AVFrame
的data
数据拷贝过去。然后用这个作为编码的AVFrame
4、遇到问题:生成的文件帧率不对
分析原因:当调用avformat_write_header()
后,内部会改变输出流的time_base
参数,而这里没有重新调整packet
的pts
,dts
,duration
的值
解决方案:使用av_packet_rescale_ts
重新调整packet
的pts
,dts
,duration
的值
5、遇到问题:当音频编码方式不一致时转码后无声音
分析原因:因为每个编码方式对应的AVFrame
中的nb_samples
不一样,所以再进行编码前要进行AVFrame
的转换
解决方案:进行编码前先转换
6、遇到问题:得到的文件播放时音画不同步
分析原因:由于音频的AVFrame
的pts
没有设置对;pts
是基于AVCodecContext
的时间基的时间,所以pts
的设置公式:
音频:pts = (timebase.den/sample_rate)nb_samplesindex
; index
为当前第几个音频AVFrame
(索引从0开始),nb_samples
为每个AVFrame
中的采样数
视频:pts = (timebase.den/fps)*index
;
解决方案:按照如上代码方式设置AVFrame
的pts
项目地址
https://github.com/nldzsz/ffmpeg-demo
实现代码位于cppsrc目录下文件
transcode.hpp
transcode.cpp
void doExtensionTranscode();对应于与流拷贝方式
void doTranscode();对应于重新编解码方式
项目下示例可运行于iOS/android/mac平台,工程分别位于demo-ios/demo-android/demo-mac三个目录下,可根据需要选择不同平台。
音视频转码FFmpeg相关推荐
- 基于ffmpeg实现音视频转码
一.背景 偶然的机会接触了ffmpeg,当时是从B站下载的视频转移到笔记本上看.使用b站手机客户端下载的视频格式为m4s的两个文件(video.m4s和audio.m4s),需要转成普通播放器支持的m ...
- 基于ffmpeg的音视频转码、压制、录屏、裁切、合并、提取
ffmpeg转码.压制.录屏.裁切.合并.提取 1.ffmpeg介绍 2.转换格式 3.音频转码 4.视频转码 5.码率控制模式 6.合并.提取音视频 7.截取.连接音视频 8.截图.水印.动图 9. ...
- JAVA调用FFmpeg实现音视频转码加水印功能
目录 目录 写在前面 MAVEN引用 获取音视频基本信息 音频转码成Mp3格式 视频转码成Mp4格式 视频转码成Mp4并添加文字水印 视频转码成Mp4并添加图片水印 测试代码 写在前面 如今各大云厂商 ...
- ffmpeg学习(13)音视频转码(2)使用filter
ffmpeg学习(10)音视频文件muxer(1)封装格式转换 中介绍了媒体文件的封装格式转换,ffmpeg学习(11)音视频文件muxer(2)多输入混流 中介绍了音视频的混流,本文介绍基于ffmp ...
- android flv 编码器,Android 音视频深入 十七 FFmpeg 获取 RTMP 流保存为 flv (附源码下载)...
Android 音视频深入 十七 FFmpeg 获取 RTMP 流保存为 flv (附源码下载) 项目地址 https://github.com/979451341/RtmpSave 这个项目主要代码 ...
- ffmpeg转码_音视频处理神器FFmpeg
下面是文字版,方便进行复制测试使用. 一 FFmpeg支持的平台 下载官方链接:http://ffmpeg.org/download.html FFmpeg的使用门槛,就是部分机器可能存在一些依赖版 ...
- FFmpeg环境安装及使用命令实现音视频转码
官方地址:http://www.ffmpeg.org/ FFmpeg是领先的多媒体框架,能够解码,编码,转码,复用,解复用,流式传输,过滤和播放人类和机器创建的任何内容. 它支持最晦涩难懂的古代格式, ...
- 音视频学习之ffmpeg常用基础命令整理
基于windows环境安装好必要的ffmpeg后,对ffmpeg基础命令进行一些了解: 1:ffmpeg查看版本: ffmpeg -version 2:ffmpeg查询命令: 基本信息:ffmpeg ...
- Serverless 音视频转码 —— 芒果 TV 落地实践(下)
在 <Serverless 音视频转码--芒果 TV 落地实践(上)>中,我们回顾了芒果 TV 吴坚强老师在 techo 大会的精彩分享,芒果TV 音视频编解码业务团队通过使用腾讯云 Se ...
最新文章
- python之路——模块和包
- C++ Primer 学习笔记(第四章:表达式)
- C++实现树的建立,查找,遍历输出
- 解决GitLab中使用SSH的git clone总是提示输入密码且任何密码都不对
- 光伏电站清扫机器人_轻型光伏电站清扫机器人的制作方法
- 2017.9.4 栅栏 失败总结
- Flexbox弹性布局,更优雅的布局
- NFC卡模拟之模拟卡ID
- FlashFXP v5.3.0.3932中文版
- 华为机试(Python)真题Od【A卷+B卷】
- 虚拟化服务器端口用万兆,虚拟化升级,千兆变万兆!
- 电脑桌面计算机文件打不开怎么办,教大家电脑桌面上的文件都打不开怎么办
- MyEclipse中怎么修改项目访问路径
- AI高考的信息检索策略
- 上传身份证--uc手机浏览器拍照覆盖问题
- C++探索之旅 | 第一部分第二课:C++编程的必要软件
- 简述计算机软件系统的功能及分类,第二章 管理信息系统技术基础
- LaTeX代码: 输出LaTeX的LOGO字样
- Metabase学习教程:提问-3
- Unity _ASE暴露正反显示模式到材质球的设置方法