在前一篇中,我们讲解了GID抓屏的整体流程,此时已经拿到了图像的原始数据BGRA,其他抓屏手段不管是Mirror Driver还是DX HOOK,得到的原始数据大都是此。
在压缩之前不妨先了解一下H264的编码基础知识。
以上两篇可能不会那么详细,具体请自行搜索相关资料。在这里要提的一点是,一些基础知识如音视频的编解码、音视频的同步原理等等最好能花时间了解一番,这些基础理论还是很有必要掌握的。
言归正传,在市面中的各类264编码器中,是无法直接对RGBA进行编码的,首先需要对齐进行转码,即转换为YUV。同样我们仍然使用ffmpeg进行转换。
初始化SwsContext
struct SwsContext *_ctx = sws_getContext(src_width,src_height,src_fmt,dst_width,dst_height,dst_fmt,SWS_BICUBIC,NULL, NULL, NULL);
其中src_fmt为桌面数据格式,在这里我们是AV_PIX_FMT_BGRA,dst_width,dst_height,dst_fmt则为转换后的图像数据宽高以及格式,在这里可以通过设置dst_width,dst_height对图像进行缩放。
计算转换后的数据大小
int _buffer_size = avpicture_get_size(dst_fmt, dst_width, dst_height);
创建一个AVFrame和buffer用以存储转换后的图像数据
uint8_t *_buffer = new uint8_t[_buffer_size];
AVFrame *_frame = av_frame_alloc();
将buffer映射到AVFrame
avpicture_fill((AVPicture*)_frame, _buffer, dst_fmt, dst_width, dst_height);
至此完成了RGBA->YUV的转换初始化,可通过如下函数进行转换
sws_scale(_ctx,(const uint8_t *const *)frame->data,frame->linesize,0, frame->height,_frame->data, _frame->linesize);
其中frame为包含桌面原始图像数据的AVframe结构
开始创建H264编码器,首先为编码器预设一些参数
AVDictionary *options = 0;
av_dict_set(&options, "preset", "superfast", 0);
av_dict_set(&options, "tune", "zerolatency", 0);
查找编码器
AVCodec *_encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
为找到的编码器申请内存
AVCodecContext *_encoder_ctx = avcodec_alloc_context3(_encoder);
配置编码器各种压缩参数
_encoder_ctx->codec_id = AV_CODEC_ID_H264;
_encoder_ctx->codec_type = AVMEDIA_TYPE_VIDEO;
_encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
_encoder_ctx->width = pic_width;
_encoder_ctx->height = pic_height;
_encoder_ctx->time_base.num = 1;
_encoder_ctx->time_base.den = frame_rate; //时间基
_encoder_ctx->framerate = { frame_rate,1 };//帧率
_encoder_ctx->bit_rate = bit_rate;//重要,最终视频的码率
_encoder_ctx->gop_size = gop_size;//关键帧间隔
_encoder_ctx->qmin = 30;//最小压缩质量
_encoder_ctx->qmax = 35;//最大压缩质量,与最小质量一起控制压缩后的图像质量
_encoder_ctx->max_b_frames = 0;//不输出B帧,输出B帧后的时间戳控制较为繁琐
_encoder_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;//设置后通过extradata获取sps、pps
打开编码器,并在接口调用时传入一些配置参数
avcodec_open2(_encoder_ctx, _encoder, &options)
申请一个AVFrame和buffer用以接收YUV数据
AVFrame _frame = av_frame_alloc();
_frame->format = _encoder_ctx->pix_fmt;//AV_PIX_FMT_YUV420P
_frame->width = _encoder_ctx->width;
_frame->height = _encoder_ctx->height;
int _buff_size = avpicture_get_size(_encoder_ctx->pix_fmt, _encoder_ctx->width, _encoder_ctx->height);
uint8_t *_buff = (uint8_t*)av_malloc(_buff_size);
将buff映射至AVFrame
avpicture_fill((AVPicture*)_frame, _buff, _encoder_ctx->pix_fmt, _encoder_ctx->width, _encoder_ctx->height);
至此我们的H264编码器初始化工作已经完成,通常我们需要一个队列来缓存原始图像数据,这是因为压缩图像会有一定的时间损耗,在我这里一般在30ms左右,而我们又希望能够尽可能地达到预设的视频帧率,假设帧率为30,那么一帧图像的采集间隔将是至少30ms,如果采集后直接压缩就远远无法达到我们的预期帧率了。
所以需要新开一个线程从图像数据队列中取出图像数据进行压缩。以下是压缩线程的while循环
void encoder_264::encode_loop()
{
int ret = 0;
AVPacket *packet = av_packet_alloc();
AVFrame frame;
while (_running)
{
std::unique_lock<std::mutex> lock(_mutex);
while (!_cond_notify)
_cond_var.wait(lock);
while (_ring_buffer->get(_buff, _buff_size, frame)) {
_frame->data[0] = _buff;
_frame->data[1] = _buff + _y_size;
_frame->data[2] = _buff + _y_size * 5 / 4;
_frame->pkt_dts = frame.pkt_dts;
_frame->pkt_dts = frame.pkt_dts;
_frame->pts = frame.pts;
ret = avcodec_send_frame(_encoder_ctx, _frame);
if (ret < 0) {
if (_on_error) _on_error(AE_FFMPEG_ENCODE_FRAME_FAILED);
al_fatal("encode yuv frame failed:%d", ret);
continue;
}
while (ret >= 0) {
ret = avcodec_receive_packet(_encoder_ctx, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
if (ret < 0) {
if (_on_error) _on_error(AE_FFMPEG_READ_PACKET_FAILED);
al_fatal("read aac packet failed:%d", ret);
}
if (_on_data)
_on_data(packet);
av_packet_unref(packet);
}
}
_cond_notify = false;
}
av_free_packet(packet);
}
这csdn的代码插入工具不好用啊。。。。缩进乱的不想理了。
至此已经完成了H264编码器的全部工作,下一篇将对系统播放和Micphone的音频数据捕获进行介绍。