大致流程
将dshow设备作为数据输入源
- ffmpeg中可将dshow设备作为数据输入源,其操作与操作文件作为数据源大同小异,以视频输入设备举例:
/*打开视频设备伪代码*/
//因为字符标准的问题, vs平台下,设备有中文名需要转换为utf8才能被ffmpeg识别到
char *dup_wchar_to_utf8(wchar_t *w)
{
char *s = NULL;
int l = WideCharToMultiByte(CP_UTF8, 0, w, -1, 0, 0, 0, 0);
s = (char *) av_malloc(l);
if (s)
WideCharToMultiByte(CP_UTF8, 0, w, -1, s, l, 0, 0);
return s;
}
......
av->video_device_name = dup_wchar_to_utf8(L"video=摄像头");
/* Open an input stream and read the header -- a specific input format */
av->pAVInFmt = av_find_input_format("dshow");
/*open input device of video stream*/
AVDictionary *param1 = NULL;
av_dict_set(¶m1, "video_size", "640x480", 0); //宽高
av_dict_set(¶m1, "framerate", "30", 0); //帧率
av_dict_set(¶m1, "rtbufsize", "30000000", 0); //缓冲区大小
av->v_ret = avformat_open_input(&av->pVFmtCtx, av->video_device_name, av->pAVInFmt, ¶m1);
.......
视频编码器参数
- codec_id编码器ID,如AV_CODEC_ID_H264;
- 编码器输入数据的像素格式pix_fmt,如AV_PIX_FMT_YUV420P;
- 编码器输入数据的宽高,帧率;
- bit_rate码率,在一定范围内,码率越大,视频质量越好,文件大小也越大;
- gop_size,一组数据中的帧数量;
- max_b_frames,非B帧间B帧的最大数量;
- qmin,qmax,图像质量的量化参数,范围为0~51;
- 若是H264编码器,还可以设置相关参数,如
- preset参数:调节编码速度和质量的平衡,有veryfast、fast、medium、slow等这10个选项,从快到慢,相对应于图像质量从差到好;
- tune参数:主要配合视频类型和视觉优化的参数,比如
- zerolatency:零延迟,用在需要非常低的延迟的情况下
- film: 电影类型;
- animation: 动画类型;
- Profile参数:表示画质级别,比如
- Baseline Profile 提供I/P帧,仅支持progressive(逐行扫描)和CAVLC;
- Main Profile 提供I/P/B帧,支持progressive(逐行扫描)和interlaced(隔行扫描),提供CAVLC或CABAC;
音频编码器参数
- codec_id编码器ID,如AV_CODEC_ID_AAC;
- sample_fmt输入数据的采样格式,如AV_SAMPLE_FMT_S16;
- sample_rate输入数据的采样率,如44100,48000;
- channels和channel_layout,输入数据的通道数和通道布局,如双通道,AV_CH_LAYOUT_STEREO布局;
- bit_rate码率,在一定范围内,码率越大,视频质量越好,文件大小也越大;
数据包pts设置
- 编码前原始数据AVFrame的pts,可以通过av_gettime() 来得到,即开始捕获视频帧/音频帧时获取一个基准时间,随后基于这个基准时间获取当前帧的pts;
- 需要注意的是,从麦克风获取的音频数据包是由几帧数据组成,因此对于每一帧的pts,还需要根据获取到数据包的总采样数和每帧采样数,基于基准时间去获取当前帧的pts;
- 因为当前得到的pts不是基于输出文件数据流的时间基,所以需要通过av_rescale_q_rnd()来转换时间基,包括编码器输出数据包的dts和duration;
/*视频帧pts获取的伪代码*/
av->time_base = av_gettime();
while (!av->vexit) {
......
av->pVFrameYUV->pts = av_gettime() - av->time_base;
ret = avcodec_encode_video2(enc->pCodecCtx, &enc->pkt, av->pVFrameYUV, &enc->flag);
......
enc->pkt.pts = av_rescale_q_rnd(enc->pkt.pts, enc->stream->time_base, enc->pCodecCtx->time_base,
(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
enc->pkt.dts = av_rescale_q_rnd(enc->pkt.dts, enc->stream->time_base, enc->pCodecCtx->time_base,
(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
enc->pkt.duration = av_rescale_q_rnd(enc->pkt.duration, enc->stream->time_base, enc->pCodecCtx->time_base,
(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
av_interleaved_write_frame(enc->pFmtCtx, &enc->pkt);
.......
}
/*音频帧pts获取的伪代码*/
av->time_base = av_gettime();
while (!av->vexit) {
......
int nb_sample = av->pAFramePCM->nb_samples; //one time received_frame nb_samples
int frame_nbs = enc->pCodecCtx->frame_size; //the frame nb_samples
int64_t incra_pts = (int64_t)(1000000 * (int64_t)enc->pCodecCtx->frame_size) / av->pAFramePCM->nb_samples;
int64_t pts_basic = av_gettime() - av->time_base;
int64_t last_pf_pts = 0;
......
do {
......
if (pf->pts < last_pf_pts) {
pf->pts = last_pf_pts + incra_pts;
}
last_pf_pts = pf->pts;
......
ret = avcodec_encode_audio2(enc->pCodecCtx, &enc->pkt, pf, &enc->flag);
......
av->acnt ++;
enc->pkt.pts = av_rescale_q_rnd(enc->pkt.pts, enc->stream->time_base, enc->pCodecCtx->time_base,
(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
enc->pkt.dts = av_rescale_q_rnd(enc->pkt.dts, enc->stream->time_base, enc->pCodecCtx->time_base,
(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
enc->pkt.duration = av_rescale_q_rnd(enc->pkt.duration, enc->stream->time_base, enc->pCodecCtx->time_base,
(enum AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
av_interleaved_write_frame(enc->pFmtCtx, &enc->pkt);
......
} while (nb_sample > 0);
}
注意的地方
- 音频编码和视频编码的速度不一致,导致结束录制时留存在缓冲区的音频帧和视频帧数量不一致,就会导致音画不一致,此时有两种做法
- 将缓冲区的数据全部取出来,进行编码打包;
- 以较少时间的数据流为基准,裁剪另一数据流;
相关API函数 — 与之前文章重复的函数未罗列
- av_find_input_format:根据输入格式的短名称查找 AVInputFormat。
AVInputFormat *av_find_input_format(const char *short_name);
- av_dict_set:在AVDictionary中设置给定条目指定的值,覆盖现有条目的值。
int av_dict_set(AVDictionary **pm, const char *key,
const char *value, int flags);
- pm:指向字典结构指针的指针。如果 *pm 为 NULL,则分配一个字典结构并放入 *pm。
- key:要添加到 *pm 的入口键。
- value:要设置的入口键的值,若为NULL,则删除原值。
- 返回值:≥0成功,否则失败。
- avcodec_encode_video2:对视频帧进行编码,从AVFrame中获取输入的原始视频数据,并将下一个输出数据包写入到AVPacket中。
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
const AVFrame *frame, int *got_packet_ptr);
- avctx:编解码器上下文。
- avpkt:编码输出的AVPacket。
- frame:包含要编码原始视频数据的AVFrame。
- got_packet_ptr:如果输出数据包为非空,则libavcodec将此字段设置为1,如果为空,则将其设置为0。如果函数返回错误,则可以假设数据包无效,并且got_packet_ptr值为未定义,因此不使用该数据包。
- 返回值:0表示成功,负数表示失败。
- avcodec_encode_audio2:对音频帧进行编码,从AVFrame中获取输入的原始视频数据,并将下一个输出数据包写入到AVPacket中。
int avcodec_encode_audio2(AVCodecContext *avctx, AVPacket *avpkt,
const AVFrame *frame, int *got_packet_ptr);
- avctx:编解码器上下文。
- avpkt:编码输出的AVPacket。
- frame:包含要编码原始视频数据的AVFrame。
- got_packet_ptr:如果输出数据包为非空,则libavcodec将此字段设置为1,如果为空,则将其设置为0。如果函数返回错误,则可以假设数据包无效,并且got_packet_ptr值为未定义,因此不使用该数据包。
- 返回值:0表示成功,负数表示失败。
- av_write_frame:将数据包写入输出媒体文件,与av_interleaved_write_frame类似,区别在于前者只能用于单一数据流,后者可以用于单一或多个数据流。
int av_write_frame(AVFormatContext *s, AVPacket *pkt);