屏幕录像机 开发笔记

项目介绍

image-20220325112028641.png

本项目主要实现功能实现一个屏幕录制器,从显卡中抓屏,从mic中获取pcm音频数据,
将其编码封装成一个MP4文件,以当前日期存放到指定路径中

git地址:https://gitee.com/lisiwen945/av_codec/tree/master/screen_recode

主要用到的技术:
directx3d:从显卡中抓屏,抓取的数据是bgra格式
ffmpeg:pcm编码成aac数据,音频重采样,视频像素转换,视频编码 ,音视频封装成mp4
qt:屏幕录制器界面开发,事件响应,音频采集

image-20220325114400254.png

音频raw数据获取

// 设置音频格式
QAudioFormat fmt;
fmt.setSampleRate(m_sampleRate);
fmt.setChannelCount(m_channels);
fmt.setSampleSize(16); // 采样精度
fmt.setSampleType(QAudioFormat::UnSignedInt);
fmt.setByteOrder(QAudioFormat::LittleEndian);
fmt.setCodec("audio/pcm");
// 创建音频录制设备(测试不插耳机会失败,电脑自带的录音设备好像不行)
m_input = new QAudioInput(fmt);
// 开始录制
m_io = m_input->start();

LynPcmData pcmData;
int size =  1024 * 2 * 2; // 每次读1024个样本,采样精度是16(两个字节),双声道
std::unique_lock<std::mutex> lock(m_mux);
pcmData.data = (unsigned char *)malloc(size);
int readedSize = 0;
while (readedSize < size && !m_isExit)
{
    int br = m_input->bytesReady();
    if (br < 1024) {
        usleep(100);
        continue;
    }
    int s = 1024;
    s = s < (size - readedSize) ? s : size - readedSize;
    int len = m_io->read((char *)pcmData.data + readedSize, s);
    readedSize += len;
}
pcmData.size = readedSize;

音频重采样

// 因为aac编码中要求输入的pcm数据是float类型,而且要求是平面的(左右声道分开存放),所以需要重采样
m_asc = swr_alloc_set_opts(m_asc,
                           m_actx->channel_layout,m_actx->sample_fmt,m_actx->sample_rate,   // 输出格式
                           av_get_default_channel_layout(2), AV_SAMPLE_FMT_S16, 44100,      // 输入格式
                           0,0);                       
int ret = swr_init(m_asc);
// 构造输出信息
m_pcm = av_frame_alloc();
m_pcm->format = m_actx->sample_fmt;
m_pcm->channels = m_actx->channels;
m_pcm->channel_layout = m_actx->channel_layout;
m_pcm->nb_samples = pcmData->size / 4; // 样本数量
int ret = av_frame_get_buffer(m_pcm, 0);

// 输入数据
const uint8_t *data[AV_NUM_DATA_POINTERS] = {0};
data[0] = (uint8_t *)pcmData->data;
// 开始转换
int len  = swr_convert(m_asc, m_pcm->data, pcmData->size / 4,
                       data, pcmData->size / 4);

视频raw数据获取

//1 创建directx3d对象
static IDirect3D9 *d3d = NULL;
if (!d3d)
{
    d3d = Direct3DCreate9(D3D_SDK_VERSION);
}
if (!d3d) return;

//2 创建显卡的设备对象
static IDirect3DDevice9 *device = NULL;
if (!device)
{
    D3DPRESENT_PARAMETERS pa;
    ZeroMemory(&pa, sizeof(pa));
    pa.Windowed = true;
    pa.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
    pa.SwapEffect = D3DSWAPEFFECT_DISCARD;
    pa.hDeviceWindow = GetDesktopWindow();
    d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, 0,
                      D3DCREATE_HARDWARE_VERTEXPROCESSING, &pa, &device
                     );
}
if (!device) {
    return;
}

//3创建离屏表面
int w = GetSystemMetrics(SM_CXSCREEN);
int h = GetSystemMetrics(SM_CYSCREEN);
static IDirect3DSurface9 *sur = NULL;
if (!sur)
{
    device->CreateOffscreenPlainSurface(w, h,
                                        D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &sur, 0);
}
if (!sur)return;

//4 抓屏
device->GetFrontBufferData(0, sur);

//5 取出数据
D3DLOCKED_RECT rect;
ZeroMemory(&rect, sizeof(rect));
if (sur->LockRect(&rect, 0, 0) != S_OK)
{
    return;
}
frame.w = w;
frame.h  = h;
frame.dataSize = w * h * 4;
frame.data = malloc(frame.dataSize);
memcpy(frame.data , rect.pBits, frame.dataSize);
sur->UnlockRect();

视频像素转换

m_vsc = sws_getCachedContext(m_vsc,
    frame->w, frame->h, AV_PIX_FMT_BGRA,           // 输入信息
    m_outWidth, m_outHeight, AV_PIX_FMT_YUV420P,   // 输出信息
    SWS_BICUBIC,
    NULL,NULL,NULL);

m_yuv = av_frame_alloc();
m_yuv->format = AV_PIX_FMT_YUV420P;
m_yuv->width = m_outWidth;
m_yuv->height = m_outHeight;
m_yuv->pts = 0;
int ret = av_frame_get_buffer(m_yuv, 32);
if (ret != 0) {
    std::cout << "av_frame_get_buffer failed!" << std::endl;
    return false;
}

// rgb to yuv
uint8_t *indata[AV_NUM_DATA_POINTERS] = { 0 };
indata[0] = (uint8_t *)frame->data;

int insize[AV_NUM_DATA_POINTERS] = { 0 };
insize[0] = frame->w * 4;

int hight = sws_scale(m_vsc, indata, insize, 0, frame->h,
                      m_yuv->data, m_yuv->linesize);
CHECK_ERROR(hight <= 0, ret, "error");

音视频编码

// 获取视频解码器
const AVCodec *videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
// 创建上下文
m_vctx = avcodec_alloc_context3(videoCodec);

//比特率,压缩后每秒大小
m_vctx->bit_rate = 4000000;
m_vctx->width = m_outWidth;
m_vctx->height = m_outHeight;

//时间基数
m_vctx->time_base = { 1, 10 };
m_vctx->framerate = { 10, 1 };

//画面组大小,多少帧一个关键帧
m_vctx->gop_size = 10;

m_vctx->max_b_frames = 0;

m_vctx->pix_fmt = AV_PIX_FMT_YUV420P;
m_vctx->codec_id = AV_CODEC_ID_H264;
av_opt_set(m_vctx->priv_data, "preset", "superfast", 0);
m_vctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
// 打开视频编码器
int ret = avcodec_open2(m_vctx, videoCodec, NULL);
m_videoStream = avformat_new_stream(m_ctx, NULL);
m_videoStream->codecpar->codec_tag = 0;
avcodec_parameters_from_context(m_videoStream->codecpar, m_vctx);

// 音频编码器
const AVCodec *audioCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
m_actx = avcodec_alloc_context3(audioCodec);
// 设置参数
m_actx->bit_rate = 64000;
m_actx->sample_rate = 44100;
m_actx->sample_fmt = AV_SAMPLE_FMT_FLTP;
m_actx->channels = 2;
m_actx->channel_layout = av_get_default_channel_layout(2);
m_actx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
// 打开设备
ret = avcodec_open2(m_actx, audioCodec, NULL);
m_audioStream->codecpar->codec_tag = 0;
avcodec_parameters_from_context(m_audioStream->codecpar, m_actx);

// 后面都是往解码器灌入frame数据,获取编码后的包
avcodec_send_frame()
avcodec_receive_packet()

音视频pts同步

pkt->pts = m_apts;
pkt->dts = pkt->pts;
// 音频pts根据样本数和采样率和时间基数确定
m_apts += av_rescale_q(m_pcm->nb_samples, { 1, m_actx->sample_rate}, m_actx->time_base);

m_yuv->pts = m_vpts++;
// 视频pts 根据时间基数确定,时间基数和帧率相关
av_packet_rescale_ts(p, m_vctx->time_base, m_videoStream->time_base);

MP4封装

// 封装文件输出上下文
avformat_alloc_output_context2(&m_ctx, NULL, NULL, fileName.c_str());
av_dump_format(m_ctx, 0, m_fileName.c_str(), 1);
// 打开io
ret = avio_open(&m_ctx->pb, m_fileName.c_str(), AVIO_FLAG_WRITE);
// 写入封装头
ret = avformat_write_header(m_ctx, NULL);
// 将包写入文件
av_interleaved_write_frame(m_ctx, pkt);
// 写入尾部信息
av_write_trailer(m_ctx)

注意事项

  1. 音频编码和视频编码在不同的线程中,需要考虑多线程同步的问题
  2. 视频的帧率控制需要考虑屏幕抓屏和编码的时间,不能简单的用sleep控制,得用定时器
  3. 要注意编解码packet和frame的释放时机
  4. 编码器内部会有缓存,在结束时理论上应该将frame设置成NULL冲刷缓冲区,因为是视频录制结束缺个几帧影响不大
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容