Qt與FFmpeg聯合開發指南(四)——編碼(2):完善功能和基礎封裝

上一章我用一個demo函數演示了基於Qt的音視頻採集到編碼的完整流程,最後通過測試咱們也發現了代碼中存在的問題。本章咱們就先處理幾個遺留問題,再對代碼進行完善,最後把編碼功能作基礎封裝。git

1、遺留問題和解決方法數據結構

(1)如何讓音視頻的錄製保持同步?ide

在咱們的演示代碼中之因此發現音視頻錄製不一樣步的主要緣由是音頻幀和視頻幀不該該簡單的按照1:1的比例進行編碼。那麼到底應該以什麼樣的比例控制呢?首先建議你們回顧一下以前寫過的解碼過程。若是咱們把解碼音視頻的過程輸出到控制檯,咱們會注意到大體每解碼一幀畫面應該解碼2~4幀聲音。按照這個思路咱們先嚐試修改一下demo中的編碼步驟,人爲控制視頻和音頻的編碼比例爲1:3。修改之後的代碼以下:函數

// 音頻編碼
for (int i = 0; i < 3; ++i) {
    // 固定寫法:配置一幀音頻的數據結構
    const char *pcm = aa->getPCM();

    /* 此處省略的代碼請參考上一章的內容或查看源碼 */

    delete pcm;
}

而後再嘗試錄製,咱們發現音頻彷佛能夠正常播放,可是畫面和音頻並無同步。另外,若是仔細一些的同窗可能還會發現。在上一篇博客的最後一張截圖中,音頻的比特率顯示爲35kbps。測試

讓咱們先了解一下視頻幀率和音頻幀率的概念:一般fps10表明1秒顯示10幅畫面,這個比較容易理解。不太容易理解的是音頻,以CD音質爲例44100Hz的採樣率,假設一幀音頻包含1024個採樣數據,那麼1秒鐘的音頻大約有43幀。在編碼階段不管是視頻仍是音頻咱們都須要提供一個基礎的pts做爲參考。表明視頻的vpts每次自增1便可,而表明音頻的apts須要每次自增1024。ui

FFmpeg提供了一個比較函數 av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b) 來幫助開發人員計算音視頻pts同步。this

while (true) {
    // 音頻編碼
    const char *pcm = aa->getPCM();
    if (pcm) {
        ...

        apkt->pts = apts;
        apkt->dts = apkt->pts;
        apts += av_rescale_q(aframe->nb_samples, { 1, pAudioCodecCtx->sample_rate }, pAudioCodecCtx->time_base); // 1024
        errnum = av_interleaved_write_frame(pFormatCtx, apkt);

        ...
        delete pcm;
        av_packet_free(&apkt);
    }

    // 比較音視頻pts,大於0表示視頻幀在前,音頻須要連續編碼。小於0表示,音頻幀在前,應該至少編碼一幀視頻
    int ts = av_compare_ts(vpts, pVideoCodecCtx->time_base, apts, pAudioCodecCtx->time_base);
    if (ts > 0) {
        continue;
    }

    // 視頻編碼
    const uchar *rgb = va->getRGB();
    if (rgb) {
        ...
        
        vframe->pts = vpts++;

        ...
        errnum = av_interleaved_write_frame(pFormatCtx, vpkt);
        
        ...
        delete rgb;
        av_packet_free(&vpkt);
    }    
}

 這樣音視頻同步的部分就基本完成。編碼

 (2)如何正確析構QImagespa

經過memcpy函數將QImage中的數據拷貝一份線程

QPixmap pix = screen->grabWindow(wid);
uchar *rgb = new uchar[width * height * 4]; // 申請圖像存儲空間
memcpy(rgb, pix.toImage().bits(), width * height * 4); // 拷貝數據到新的內存區域

這樣外部的調用者正常對rgb數據析構就不會有任何問題了。

(3)有關Qt截屏的效率討論*

Qt提供的截屏方案雖然簡單,可是時間開銷有點大。若是咱們但願錄製fps25以上的畫面時可能不盡如人意。所以若是是在Windows環境下,我推薦經過DirectX作截屏操做。有興趣的同窗能夠參考個人源碼,這裏就不作過多討論了。

2、功能封裝

首先說明一下咱們的封裝目標。因爲主線程須要留給界面和事件循環,所以音視頻採集以及編碼都各自運行在獨立的線程中。音視頻的採集能夠和編碼分離,經過隊列暫存數據。

(1)界面設計(略)

這個部分不是本文的重點

(2)視頻捕獲線程(VideoAcquisitionThread)

const uchar* VideoAcquisitionThread::getRGB()
{
    mtx.lock();
    if (rgbs.size() > 0) {
        uchar *rgb = rgbs.front();
        rgbs.pop_front();
        mtx.unlock();
        return rgb;
    }
    mtx.unlock();
    return NULL;
}

void VideoAcquisitionThread::run()
{
    int interval = 1000 / fps;
    QTime rt;
    while (!isThreadQuit) {
        if (rgbs.size() < listSize) {
            rt.restart();
            mtx.lock();
            QPixmap pix = screen->grabWindow(wid);
            uchar *rgb = new uchar[width * height * 4]; // 申請圖像存儲空間
            memcpy(rgb, pix.toImage().bits(), width * height * 4); // 拷貝數據到新的內存區域
            rgbs.push_back(rgb);
            cout << ".";
            mtx.unlock();
            int el = rt.restart();
            if (interval > el) {
                msleep(interval - el);
            }
        }
    }
}

(3)音頻捕獲線程(AudioAcquishtionThread)

const char* AudioAcquishtionThread::getPCM()
{
    mtx.lock();
    if (pcms.size() > 0) {
        char *pcm = pcms.front();
        pcms.pop_front();
        mtx.unlock();
        return pcm;
    }
    mtx.unlock();
    return NULL;
}

void AudioAcquishtionThread::run()
{
    
    while (!isThreadQuit) {
        mtx.lock();
        if (pcms.size() < listSize) {
            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;
            }
            pcms.push_back(pcm);
        }
        mtx.unlock();
    }
}

(4)初始化封裝器,音視頻流和音視頻轉碼器

bool EncoderThread::init(QString filename, int fps)
{
    close();
    mtx.lock();
    at = new AudioAcquishtionThread();
    vt = new VideoAcquisitionThread();
    // 啓動音視頻採集線程
    vt->start(fps);
    at->start();
    this->filename = filename;
    errnum = avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, filename.toLocal8Bit().data());
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        mtx.unlock();
        return false;
    }
    // 建立視頻編碼器
    const AVCodec *vcodec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_H264);
    if (!vcodec) {
        mtx.unlock();
        return false;
    }

    pVideoCodecCtx = avcodec_alloc_context3(vcodec);
    if (!pVideoCodecCtx) {
        mtx.unlock();
        return false;
    }

    // 比特率、寬度、高度
    pVideoCodecCtx->bit_rate = 4000000;
    pVideoCodecCtx->width = vt->getWidth();
    pVideoCodecCtx->height = vt->getHeight();
    // 時間基數、幀率
    pVideoCodecCtx->time_base = { 1, fps };
    pVideoCodecCtx->framerate = { fps, 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;

    // 打開編碼器
    errnum = avcodec_open2(pVideoCodecCtx, vcodec, NULL);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        mtx.unlock();
        return false;
    }

    // 建立音頻編碼器
    const AVCodec *acodec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_AAC);
    if (!acodec) {
        mtx.unlock();
        return false;
    }

    // 根據編碼器建立編碼器上下文
    pAudioCodecCtx = avcodec_alloc_context3(acodec);
    if (!pAudioCodecCtx) {
        mtx.unlock();
        return false;
    }

    // 比特率、採樣率、採樣類型、音頻通道、文件格式
    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;

    // 打開編碼器
    errnum = avcodec_open2(pAudioCodecCtx, acodec, NULL);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        mtx.unlock();
        return false;
    }

    // 初始化視頻轉碼器
    swsCtx = sws_getContext(
        vt->getWidth(), vt->getHeight(), AVPixelFormat::AV_PIX_FMT_BGRA,
        vt->getWidth(), vt->getHeight(), AVPixelFormat::AV_PIX_FMT_YUV420P,
        SWS_BICUBIC,
        0, 0, 0);
    if (!swsCtx) {
        mtx.unlock();
        return false;
    }

    // 初始化音頻轉碼器
    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) {
        mtx.unlock();
        return false;
    }
    
    mtx.unlock();
    return true;
}

(5)添加視頻流

bool EncoderThread::addVideoStream()
{
    mtx.lock();
    // 爲封裝器建立視頻流
    pVideoStream = avformat_new_stream(pFormatCtx, NULL);
    if (!pVideoStream) {
        mtx.unlock();
        return false;
    }
    
    // 配置視頻流的編碼參數
    errnum = avcodec_parameters_from_context(pVideoStream->codecpar, pVideoCodecCtx);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        mtx.unlock();
        return false;
    }

    pVideoStream->codec->codec_tag = 0;
    pVideoStream->codecpar->codec_tag = 0;

    mtx.unlock();
    return true;
}

(6)添加音頻流

bool EncoderThread::addAudioStream()
{
    mtx.lock();
    // 添加音頻流
    pAudioStream = avformat_new_stream(pFormatCtx, NULL);
    if (!pAudioStream) {
        mtx.unlock();
        return false;
    }
    // 配置音頻流的編碼器參數
    errnum = avcodec_parameters_from_context(pAudioStream->codecpar, pAudioCodecCtx);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        mtx.unlock();
        return false;
    }
    pAudioStream->codec->codec_tag = 0;
    pAudioStream->codecpar->codec_tag = 0;
    mtx.unlock();
    return true;
}

 

(7)重寫線程啓動方法(代理模式)

void EncoderThread::start()
{
    
    mtx.lock();
    // 打開輸出流
    errnum = avio_open(&pFormatCtx->pb, filename.toLocal8Bit().data(), AVIO_FLAG_WRITE); // 打開AVIO流
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        avio_closep(&pFormatCtx->pb);
        mtx.unlock();
        return;
    }
    // 寫文件頭
    errnum = avformat_write_header(pFormatCtx, NULL);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        mtx.unlock();
        return;
    }

    quitFlag = false;
    mtx.unlock();
    QThread::start();
}

(8)編碼線程

void EncoderThread::run()
{
    // 初始化視頻幀
    AVFrame *vframe = av_frame_alloc();
    vframe->format = AVPixelFormat::AV_PIX_FMT_YUV420P;
    vframe->width = vt->getWidth();
    vframe->height = vt->getHeight();
    vframe->pts = 0;
    // 爲視頻幀分配空間
    errnum = av_frame_get_buffer(vframe, 32);
    if (errnum < 0) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        return;
    }

    // 初始化音頻幀
    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) {
        av_strerror(errnum, errbuf, sizeof(errbuf));
        return;
    }

    int vpts = 0;
    int apts = 0;

    while (!quitFlag) {
        // 音頻編碼
        const char *pcm = at->getPCM();
        if (pcm) {
            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) {
                continue;
            }
            // 音頻編碼
            errnum = avcodec_send_frame(pAudioCodecCtx, aframe);
            if (errnum < 0) {
                av_strerror(errnum, errbuf, sizeof(errbuf));
                continue;
            }
            AVPacket *apkt = av_packet_alloc();
            errnum = avcodec_receive_packet(pAudioCodecCtx, apkt);
            if (errnum < 0) {
                av_strerror(errnum, errbuf, sizeof(errbuf));
                av_packet_free(&apkt);
                continue;
            }
            apkt->stream_index = pAudioStream->index;

            apkt->pts = apts;
            apkt->dts = apkt->pts;
            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));
                continue;
            }
            delete pcm;
            av_packet_free(&apkt);
            cout << ".";
        }

        int ts = av_compare_ts(vpts, pVideoCodecCtx->time_base, apts, pAudioCodecCtx->time_base);
        if (ts > 0) {
            continue;
        }

        // 視頻編碼
        const uchar *rgb = vt->getRGB();
        if (rgb) {
            // 固定寫法:配置1幀原始視頻畫面的數據結構一般爲RGBA的形式
            uint8_t *srcSlice[AV_NUM_DATA_POINTERS] = { 0 };
            srcSlice[0] = (uint8_t *)rgb;
            int srcStride[AV_NUM_DATA_POINTERS] = { 0 };
            srcStride[0] = vt->getWidth() * 4;
            // 轉換
            int h = sws_scale(swsCtx, srcSlice, srcStride, 0, vt->getHeight(), vframe->data, vframe->linesize);
            if (h < 0) {
                continue;
            }

            vframe->pts = vpts++;
            errnum = avcodec_send_frame(pVideoCodecCtx, vframe);
            if (errnum < 0) {
                av_strerror(errnum, errbuf, sizeof(errbuf));
                continue;
            }
            AVPacket *vpkt = av_packet_alloc();

            errnum = avcodec_receive_packet(pVideoCodecCtx, vpkt);
            if (errnum < 0 || vpkt->size <= 0) {
                av_packet_free(&vpkt);
                av_strerror(errnum, errbuf, sizeof(errbuf));
                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));
                continue;
            }
            delete rgb;
            av_packet_free(&vpkt);
            cout << "*";
        }
    }

    errnum = av_write_trailer(pFormatCtx);
    if (errnum != 0) {
        return;
    }
    errnum = avio_closep(&pFormatCtx->pb); // 關閉AVIO流
    if (errnum != 0) {
        return;
    }

    // 清理音視頻幀
    if (vframe) {
        av_frame_free(&vframe);
    }
    if (aframe) {
        av_frame_free(&aframe);
    }
}

(9)關閉與成員變量析構

void EncoderThread::close()
{
    mtx.lock();
    quitFlag = true;
    wait();
    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 (swsCtx) {
        sws_freeContext(swsCtx);
        swsCtx = NULL;
    }
    if (swrCtx) {
        swr_free(&swrCtx);
    }
    mtx.unlock();
    
}

這個部分都是對代碼的封裝處理,這裏就不作什麼解釋了。最後附上完整的源碼地址,僅供參考。

相關文章
相關標籤/搜索