1. 程式人生 > >Qt與FFmpeg聯合開發指南(四)——編碼(2):完善功能和基礎封裝

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

v_op buffer 目前 front from 幀率 inter 博客 int

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

一、遺留問題和解決方法

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

在我們的演示代碼中之所以發現音視頻錄制不同步的主要原因是音頻幀和視頻幀不應該簡單的按照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。

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

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)如何正確析構QImage

通過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做截屏操作。有興趣的同學可以參考我的源碼,這裏就不做過多討論了。

二、功能封裝

首先說明一下我們的封裝目標。由於主線程需要留給界面和事件循環,因此音視頻采集以及編碼都各自運行在獨立的線程中。音視頻的采集可以和編碼分離,通過隊列暫存數據。

(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();
    
}

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

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