1. 程式人生 > >Qt與FFmpeg聯合開發指南(三)——編碼(1):代碼流程演示

Qt與FFmpeg聯合開發指南(三)——編碼(1):代碼流程演示

開啟 fault 原因 上下 sizeof ffmpeg 不同步 目前 直接

前兩講演示了基本的解碼流程和簡單功能封裝,今天我們開始學習編碼。編碼就是封裝音視頻流的過程,在整個編碼教程中,我會首先在一個函數中演示完成的編碼流程,再解釋其中存在的問題。下一講我們會將編碼功能進行封裝並解釋針對不同的輸出環境代碼上需要註意的地方。最後我們還會把之前做好的解碼器添加進開發環境,實現PC屏幕和攝像頭錄制然後再通過播放器播放。

首先說明一下本章的目標:

  1. 通過Qt進行視頻采集
  2. 通過Qt進行音頻采集
  3. 對音視頻編碼成mp4文件並能夠通過vlc播放

一、通過Qt進行視頻采集

Qt提供了對桌面錄屏的支持,我們可以很輕松的完成開發

// 首先獲取到完整桌面的窗口句柄已經寬高信息
WId wid = QApplication::desktop()->winId();
int width = QApplication::desktop()->width(); int height = QApplication::desktop()->height(); // 截屏獲得圖片 static QScreen *screen = NULL; if (!screen) { screen = QGuiApplication::primaryScreen(); } QPixmap pix = screen->grabWindow(wid); const uchar *rgb = pix.toImage().bits();

這裏有一點需要特別註意,當我們把上面的代碼封裝進函數以後,我們無法直接通過返回值獲取到rgb數據。這個地方曾經卡了我好幾天,原因在於經過grabWindow(wid)函數獲取到的QPixmap對象是屬於函數的局部變量,在函數結束以後這個該變量包括bits()包含的數據都會被清理掉。所以如果我們想在函數外部繼續使用圖片數據就必須對QImage進行一次深拷貝。我提供兩條思路,一是直接將QImage對象進行深拷貝,然後使用它的bits()數據。但是這樣的話如果我們只在外部析構bits()中的數據其實對內存的清理工作並不完整。另一個方法是我們直接對bits()裏的數據進行拷貝,但是由於QImage對圖片的保存數據並非是連續的尋址空間所以我們需要做一次轉換。為了方便起見我們先按照第一種思路設計。

const uchar* VideoAcquisition::getRGB()
{
    static QScreen *screen = NULL;
    if (!screen) {
        screen = QGuiApplication::primaryScreen();
    }
        WId wid = QApplication::desktop()->winId();
    int width = QApplication::desktop()->width();
    int height = QApplication::desktop()->height();

    QPixmap pix 
= screen->grabWindow(wid); QImage *image = new QImage(pix.toImage().copy(0, 0, width, height)); return image->bits(); }

二、通過Qt進行音頻采集

與視頻采集的圖片不同,音頻數據對應的是一段時間的錄音。雖然Qt也提供了統一的音頻錄制接口,不過我們首先需要對錄音設備進行初始化。主要是設置錄音的參數和控制每次從音頻緩存中讀取的數據大小。這裏我們以CD音質為標準,即采樣率:44100Hz,通道數:2,采樣位數:16bit,編碼格式:audio/pcm。

首先初始化一個錄音設備:QIODevice

QAudioFormat fmt;
fmt.setSampleRate(44100);
fmt.setChannelCount(2);
fmt.setSampleSize(16); // 采樣大小 = 采樣位數 * 8
fmt.setSampleType(QAudioFormat::UnSignedInt);
fmt.setByteOrder(QAudioFormat::LittleEndian);
fmt.setCodec("audio/pcm");
QAudioInput *audioInput = new QAudioInput(fmt);
QIODevice *device = audioInput->start();

假設我們每次從音頻緩存中讀取1024個采樣點的數據,已知采樣的其它條件為雙通道和每個采樣點兩位。則我們用於保存數據的數組大小為:char *pcm = new char[1024 * 2 * 2]

const char* AudioAcquisition::getPCM()
{
    int readOnceSize = 1024; // 每次從音頻設備中讀取的數據大小
    int offset = 0; // 當前已經讀到的數據大小,作為pcm的偏移量
    int pcmSize = 1024 * 2 * 2;
    char *pcm = new char[pcmSize];
    while (audioInput) {
        int remains = pcmSize - offset; // 剩余空間
        int ready = audioInput->bytesReady(); // 音頻采集設備目前已經準備好的數據大小
        if (ready < readOnceSize) { // 當前音頻設備中的數據不足
            QThread::msleep(1);
            continue;
        }
        if (remains < readOnceSize) { // 當幀存儲(pcmSize)的剩余空間(remainSize)小於單次讀取數據預設(readSizeOnce)時
            device->read(pcm + offset, remains); // 從設備中讀取剩余空間大小的數據
            // 讀滿一幀數據退出
            break;
        }
        int len = device->read(pcm + offset, readOnceSize);
        offset += len;
    }
    return pcm;
}

完成了音視頻采集工作以後,接下來是本章的重點——編碼——也就是調用FFmpeg庫的過程。

三、對音視頻編碼成mp4文件

(1)初始化FFmpeg

av_register_all();
avcodec_register_all();
avformat_network_init();

(2)設置三個參數分別用於保存錯誤代碼、錯誤信息和輸出文件路徑

int errnum = 0;
char errbuf[1024] = { 0 };
char *filename = "D:/test.mp4";
// 視頻采集對象
VideoAcquisition *va = new VideoAcquisition();
// 音頻采集對象
AudioAcquisition *aa = new AudioAcquisition();

(3)創建輸出的包裝器

AVFormatContext *pFormatCtx = NULL;
errnum = avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, filename);
if (errnum < 0) {
    av_strerror(errnum, errbuf, sizeof(errbuf));
}

(4)創建這對h264的編碼器和編碼器上下文,並向編碼器上下文中配置參數

// h264視頻編碼器
const AVCodec *vcodec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_H264);
if (!vcodec) {
    cout << "avcodec_find_encoder failed!" << endl;
}
// 創建編碼器上下文
AVCodecContext *pVideoCodecCtx = avcodec_alloc_context3(vcodec);
if (!pVideoCodecCtx) {
    cout << "avcodec_alloc_context3 failed!" << endl;
}

// 比特率、寬度、高度
pVideoCodecCtx->bit_rate = 4000000;
pVideoCodecCtx->width = va->getWidth(); // 視頻寬度
pVideoCodecCtx->height = va->getHeight(); // 視頻高度
// 時間基數、幀率
pVideoCodecCtx->time_base = { 1, 25 };
pVideoCodecCtx->framerate = { 25, 1 };
// 關鍵幀間隔
pVideoCodecCtx->gop_size = 10;
// 不使用b幀
pVideoCodecCtx->max_b_frames = 0;
// 幀、編碼格式
pVideoCodecCtx->pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P;
pVideoCodecCtx->codec_id = AVCodecID::AV_CODEC_ID_H264;
// 預設:快速
av_opt_set(pVideoCodecCtx->priv_data, "preset", "superfast", 0);
// 全局頭
pVideoCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

(5)開啟編碼器

errnum = avcodec_open2(pVideoCodecCtx, vcodec, NULL);
if (errnum < 0) {
    cout << "avcodec_open2 failed!" << endl;
}

(6)為封裝器創建視頻流

// 為封裝器創建視頻流
AVStream *pVideoStream = avformat_new_stream(pFormatCtx, NULL);
if (!pVideoStream) {
    cout << "avformat_new_stream video stream failed!" << endl;
}
pVideoStream->codec->codec_tag = 0;
pVideoStream->codecpar->codec_tag = 0;
// 配置視頻流的編碼參數
avcodec_parameters_from_context(pVideoStream->codecpar, pVideoCodecCtx);

(7)創建從RGB格式到YUV420格式的轉碼器

SwsContext *pSwsCtx = sws_getContext(
    va->getWidth(), va->getHeight(), AVPixelFormat::AV_PIX_FMT_BGRA, // 輸入
    va->getWidth(), va->getHeight(), AVPixelFormat::AV_PIX_FMT_YUV420P, // 輸出
    SWS_BICUBIC, // 算法
    0, 0, 0);
if (!pSwsCtx) {
    cout << "sws_getContext failed" << endl;
}

(8)初始化一個視頻幀的對象並分配空間

// 編碼階段的視頻幀結構
AVFrame *vframe = av_frame_alloc();
vframe->format = AVPixelFormat::AV_PIX_FMT_YUV420P;
vframe->width = va->getWidth();
vframe->height = va->getHeight();
vframe->pts = 0;
// 為視頻幀分配空間
errnum = av_frame_get_buffer(vframe, 32);
if (errnum < 0) {
    cout << "av_frame_get_buffer failed" << endl;
}

以上8個步驟是對視頻部分的代碼演示,下面是音頻部分。基本的操作過程和視頻一致。

(9)創建aac的音頻編碼器和編碼器上下文

// 創建音頻編碼器,指定類型為AAC
const AVCodec *acodec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_AAC);
if (!acodec) {
    cout << "avcodec_find_encoder failed!" << endl;
}

// 根據編碼器創建編碼器上下文
AVCodecContext *pAudioCodecCtx = avcodec_alloc_context3(acodec);
if (!pAudioCodecCtx) {
    cout << "avcodec_alloc_context3 failed!" << endl;
}

// 比特率、采樣率、采樣類型、音頻通道、文件格式
pAudioCodecCtx->bit_rate = 64000;
pAudioCodecCtx->sample_rate = 44100;
pAudioCodecCtx->sample_fmt = AVSampleFormat::AV_SAMPLE_FMT_FLTP;
pAudioCodecCtx->channels = 2;
pAudioCodecCtx->channel_layout = av_get_default_channel_layout(2); // 根據音頻通道數自動選擇輸出類型(默認為立體聲)
pAudioCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

(10)開啟編碼器

// 打開編碼器
errnum = avcodec_open2(pAudioCodecCtx, acodec, NULL);
if (errnum < 0) {
    avcodec_free_context(&pAudioCodecCtx);
    cout << "avcodec_open2 failed" << endl;
}

(11)向封裝器添加音頻流

// 添加音頻流
AVStream *pAudioStream = avformat_new_stream(pFormatCtx, NULL);
if (!pAudioStream) {
    cout << "avformat_new_stream failed" << endl;
    return -1;
}
pAudioStream->codec->codec_tag = 0;
pAudioStream->codecpar->codec_tag = 0;
// 配置音頻流的編碼器參數
avcodec_parameters_from_context(pAudioStream->codecpar, pAudioCodecCtx);

(12)創建從FLTP到S16的音頻重采樣上下文

SwrContext *swrCtx = NULL;
swrCtx = swr_alloc_set_opts(swrCtx,
    av_get_default_channel_layout(2), AVSampleFormat::AV_SAMPLE_FMT_FLTP, 44100, // 輸出
    av_get_default_channel_layout(2), AVSampleFormat::AV_SAMPLE_FMT_S16, 44100, // 輸入
    0, 0);
errnum = swr_init(swrCtx);
if (errnum < 0) {
    cout << "swr_init failed" << endl;
}

(13)初始化音頻幀的結構

// 創建音頻幀
AVFrame *aframe = av_frame_alloc();
aframe->format = AVSampleFormat::AV_SAMPLE_FMT_FLTP;
aframe->channels = 2;
aframe->channel_layout = av_get_default_channel_layout(2);
aframe->nb_samples = 1024;
// 為音頻幀分配空間
errnum = av_frame_get_buffer(aframe, 0);
if (errnum < 0) {
    cout << "av_frame_get_buffer failed" << endl;
}

音頻部分的代碼演示完成。下面是開啟輸出流,並循環進行音視頻采集編碼。

(14)打開輸出的IO

// 打開輸出流IO
errnum = avio_open(&pFormatCtx->pb, filename, AVIO_FLAG_WRITE); // 打開AVIO流
if (errnum < 0) {
    avio_close(pFormatCtx->pb);
    cout << "avio_open failed" << endl;
}

(15)寫頭文件

// 寫文件頭
errnum = avformat_write_header(pFormatCtx, NULL);
if (errnum < 0) {
    cout << "avformat_write_header failed" << endl;
}

(16)編碼並將數據寫入文件,由於我們還沒有設計出控制功能,暫且只編碼200幀視頻幀。按25幀/秒計算,應該生成長度為8秒視頻文件。可由於緩存的緣故,最後往往會丟幾幀數據。因此實際長度不足8秒。

int vpts = 0;
int apts = 0;

while (vpts < 200) {
    // 視頻編碼
    const uchar *rgb = va->getRGB();
    // 固定寫法:配置1幀原始視頻畫面的數據結構通常為RGBA的形式
    uint8_t *srcSlice[AV_NUM_DATA_POINTERS] = { 0 };
    srcSlice[0] = (uint8_t *)rgb;
    int srcStride[AV_NUM_DATA_POINTERS] = { 0 };
    srcStride[0] = va->getWidth() * 4;
    // 轉換
    int h = sws_scale(pSwsCtx, srcSlice, srcStride, 0, va->getHeight(), vframe->data, vframe->linesize);
    if (h < 0) {
        cout << "sws_scale failed" << endl;
        break;
    }
    // pts遞增
    vframe->pts = vpts++;
    errnum = avcodec_send_frame(pVideoCodecCtx, vframe);
    if (errnum < 0) {
        cout << "avcodec_send_frame failed" << endl;
        continue;
    }
    // 視頻編碼報文
    AVPacket *vpkt = av_packet_alloc();

    errnum = avcodec_receive_packet(pVideoCodecCtx, vpkt);
    if (errnum < 0 || vpkt->size <= 0) {
        av_packet_free(&vpkt);
        cout << "avcodec_receive_packet failed" << endl;
        continue;
    }
    // 轉換pts
    av_packet_rescale_ts(vpkt, pVideoCodecCtx->time_base, pVideoStream->time_base);
    vpkt->stream_index = pVideoStream->index;

    // 向封裝器中寫入壓縮報文,該函數會自動釋放pkt空間,不需要調用者手動釋放
    errnum = av_interleaved_write_frame(pFormatCtx, vpkt);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        cout << errbuf << endl;
        cout << "av_interleaved_write_frame failed" << endl;
        continue;
    }
    // 析構圖像數據:註意這裏只析構了圖片的數據,實際的QImage對象還在內存中
    delete rgb;

    // 音頻編碼

    // 固定寫法:配置一幀音頻的數據結構
    const char *pcm = aa->getPCM();
    if (!pcm) {
        continue;
    }
    const uint8_t *in[AV_NUM_DATA_POINTERS] = { 0 };
    in[0] = (uint8_t *)pcm;

    // 音頻重采樣
    int len = swr_convert(swrCtx,
        aframe->data, aframe->nb_samples, // 輸出
        in, aframe->nb_samples); // 輸入
    if (len < 0) {
        cout << "swr_convert failed" << endl;
        continue;
    }
    // 音頻編碼
    errnum = avcodec_send_frame(pAudioCodecCtx, aframe);
    if (errnum < 0) {
        cout << "avcodec_send_frame failed" << endl;
        continue;
    }
    // 音頻編碼報文
    AVPacket *apkt = av_packet_alloc();
    errnum = avcodec_receive_packet(pAudioCodecCtx, apkt);
    if (errnum < 0) {
        av_packet_free(&apkt);
        cout << "avcodec_receive_packet failed" << endl;
        continue;
    }
    apkt->stream_index = pAudioStream->index;
    apkt->pts = apts;
    apkt->dts = apts;
    apts += av_rescale_q(aframe->nb_samples, { 1, pAudioCodecCtx->sample_rate }, pAudioCodecCtx->time_base);
    // 寫音頻幀
    errnum = av_interleaved_write_frame(pFormatCtx, apkt);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        cout << errbuf << endl;
        cout << "av_interleaved_write_frame failed" << endl;
        continue;
    }
    delete pcm;
}

(17)寫入文件尾和關閉IO

// 寫入文件尾
errnum = av_write_trailer(pFormatCtx);
if (errnum != 0) {
    cout << "av_write_trailer failed" << endl;
}
errnum = avio_closep(&pFormatCtx->pb); // 關閉AVIO流
if (errnum != 0) {
    cout << "avio_close failed" << endl;
}

(18)清理

if (pFormatCtx) {
    avformat_close_input(&pFormatCtx); // 關閉封裝上下文
}
// 關閉編碼器和清理上下文的所有空間
if (pVideoCodecCtx) {
    avcodec_close(pVideoCodecCtx);
    avcodec_free_context(&pVideoCodecCtx);
}
if (pAudioCodecCtx) {
    avcodec_close(pAudioCodecCtx);
    avcodec_free_context(&pAudioCodecCtx);
}
// 音視頻轉換上下文
if (pSwsCtx) {
    sws_freeContext(pSwsCtx);
    pSwsCtx = NULL;
}
if (swrCtx) {
    swr_free(&swrCtx);
}
// 清理音視頻幀
if (vframe) {
    av_frame_free(&vframe);
}
if (aframe) {
    av_frame_free(&aframe);
}

四、遺留問題

運行代碼我們可以在設置的盤符下找到生成的mp4文件。查看文件屬性,我們可以看到音視頻數據都與我們之前的設置完全一致。也可以被播放器正常播放。

技術分享圖片

但是我們發現,音視頻並不同步。另外就是視頻采集的時候,QImage也沒有被正確析構。我們將在下一章提供解決方案。

項目源碼地址:

https://gitee.com/learnhow/ffmpeg_studio/blob/master/_64bit/src/screen_vcr_v12/demo.cpp

Qt與FFmpeg聯合開發指南(三)——編碼(1):代碼流程演示