屏幕錄像機(jī) 開(kāi)發(fā)筆記

項(xiàng)目介紹

image-20220325112028641.png

本項(xiàng)目主要實(shí)現(xiàn)功能實(shí)現(xiàn)一個(gè)屏幕錄制器袭灯,從顯卡中抓屏捉捅,從mic中獲取pcm音頻數(shù)據(jù)脱篙,
將其編碼封裝成一個(gè)MP4文件,以當(dāng)前日期存放到指定路徑中

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

主要用到的技術(shù):
directx3d:從顯卡中抓屏贫橙,抓取的數(shù)據(jù)是bgra格式
ffmpeg:pcm編碼成aac數(shù)據(jù),音頻重采樣麦箍,視頻像素轉(zhuǎn)換腌紧,視頻編碼 ,音視頻封裝成mp4
qt:屏幕錄制器界面開(kāi)發(fā)夜涕,事件響應(yīng)犯犁,音頻采集

image-20220325114400254.png

音頻raw數(shù)據(jù)獲取

// 設(shè)置音頻格式
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");
// 創(chuàng)建音頻錄制設(shè)備(測(cè)試不插耳機(jī)會(huì)失敗,電腦自帶的錄音設(shè)備好像不行)
m_input = new QAudioInput(fmt);
// 開(kāi)始錄制
m_io = m_input->start();

LynPcmData pcmData;
int size =  1024 * 2 * 2; // 每次讀1024個(gè)樣本女器,采樣精度是16(兩個(gè)字節(jié))酸役,雙聲道
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;

音頻重采樣

// 因?yàn)閍ac編碼中要求輸入的pcm數(shù)據(jù)是float類(lèi)型,而且要求是平面的(左右聲道分開(kāi)存放)驾胆,所以需要重采樣
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);
// 構(gòu)造輸出信息
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; // 樣本數(shù)量
int ret = av_frame_get_buffer(m_pcm, 0);

// 輸入數(shù)據(jù)
const uint8_t *data[AV_NUM_DATA_POINTERS] = {0};
data[0] = (uint8_t *)pcmData->data;
// 開(kāi)始轉(zhuǎn)換
int len  = swr_convert(m_asc, m_pcm->data, pcmData->size / 4,
                       data, pcmData->size / 4);

視頻raw數(shù)據(jù)獲取

//1 創(chuàng)建directx3d對(duì)象
static IDirect3D9 *d3d = NULL;
if (!d3d)
{
    d3d = Direct3DCreate9(D3D_SDK_VERSION);
}
if (!d3d) return;

//2 創(chuàng)建顯卡的設(shè)備對(duì)象
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創(chuàng)建離屏表面
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 取出數(shù)據(jù)
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();

視頻像素轉(zhuǎn)換

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);
// 創(chuàng)建上下文
m_vctx = avcodec_alloc_context3(videoCodec);

//比特率涣澡,壓縮后每秒大小
m_vctx->bit_rate = 4000000;
m_vctx->width = m_outWidth;
m_vctx->height = m_outHeight;

//時(shí)間基數(shù)
m_vctx->time_base = { 1, 10 };
m_vctx->framerate = { 10, 1 };

//畫(huà)面組大小,多少幀一個(gè)關(guān)鍵幀
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;
// 打開(kāi)視頻編碼器
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);
// 設(shè)置參數(shù)
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;
// 打開(kāi)設(shè)備
ret = avcodec_open2(m_actx, audioCodec, NULL);
m_audioStream->codecpar->codec_tag = 0;
avcodec_parameters_from_context(m_audioStream->codecpar, m_actx);

// 后面都是往解碼器灌入frame數(shù)據(jù)丧诺,獲取編碼后的包
avcodec_send_frame()
avcodec_receive_packet()

音視頻pts同步

pkt->pts = m_apts;
pkt->dts = pkt->pts;
// 音頻pts根據(jù)樣本數(shù)和采樣率和時(shí)間基數(shù)確定
m_apts += av_rescale_q(m_pcm->nb_samples, { 1, m_actx->sample_rate}, m_actx->time_base);

m_yuv->pts = m_vpts++;
// 視頻pts 根據(jù)時(shí)間基數(shù)確定入桂,時(shí)間基數(shù)和幀率相關(guān)
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);
// 打開(kāi)io
ret = avio_open(&m_ctx->pb, m_fileName.c_str(), AVIO_FLAG_WRITE);
// 寫(xiě)入封裝頭
ret = avformat_write_header(m_ctx, NULL);
// 將包寫(xiě)入文件
av_interleaved_write_frame(m_ctx, pkt);
// 寫(xiě)入尾部信息
av_write_trailer(m_ctx)

注意事項(xiàng)

  1. 音頻編碼和視頻編碼在不同的線(xiàn)程中,需要考慮多線(xiàn)程同步的問(wèn)題
  2. 視頻的幀率控制需要考慮屏幕抓屏和編碼的時(shí)間驳阎,不能簡(jiǎn)單的用sleep控制抗愁,得用定時(shí)器
  3. 要注意編解碼packet和frame的釋放時(shí)機(jī)
  4. 編碼器內(nèi)部會(huì)有緩存,在結(jié)束時(shí)理論上應(yīng)該將frame設(shè)置成NULL沖刷緩沖區(qū)呵晚,因?yàn)槭且曨l錄制結(jié)束缺個(gè)幾幀影響不大
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蜘腌,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子饵隙,更是在濱河造成了極大的恐慌撮珠,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件癞季,死亡現(xiàn)場(chǎng)離奇詭異劫瞳,居然都是意外死亡倘潜,警方通過(guò)查閱死者的電腦和手機(jī)绷柒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)涮因,“玉大人废睦,你說(shuō)我怎么就攤上這事⊙荩” “怎么了嗜湃?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)澜掩。 經(jīng)常有香客問(wèn)我购披,道長(zhǎng),這世上最難降的妖魔是什么肩榕? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任刚陡,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筐乳。我一直安慰自己歌殃,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布蝙云。 她就那樣靜靜地躺著氓皱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勃刨。 梳的紋絲不亂的頭發(fā)上波材,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音朵你,去河邊找鬼各聘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛抡医,可吹牛的內(nèi)容都是我干的躲因。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼忌傻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼大脉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起水孩,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤镰矿,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后俘种,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秤标,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年宙刘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了苍姜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悬包,死狀恐怖衙猪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情布近,我是刑警寧澤垫释,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站撑瞧,受9級(jí)特大地震影響棵譬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜预伺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一订咸、第九天 我趴在偏房一處隱蔽的房頂上張望琅束。 院中可真熱鬧,春花似錦算谈、人聲如沸涩禀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)艾船。三九已至,卻和暖如春高每,著一層夾襖步出監(jiān)牢的瞬間屿岂,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工鲸匿, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爷怀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓带欢,卻偏偏與公主長(zhǎng)得像运授,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乔煞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容