天天看點

視訊圖像進行中的錯幀同步是怎麼實作的?

作者:位元組流動

來源:

https://blog.csdn.net/Kennethdroid/article/details/105714382

為什麼會用到錯幀同步?

一般 Android 系統相機的最高幀率在 30 FPS 左右,當幀率低于 20 FPS 時,使用者可以明顯感覺到相機畫面卡頓和延遲。

我們在做相機預覽和視訊流處理時,對每幀圖像處理時間過長(超過 30 ms)就很容易造成畫面卡頓,這個場景就需要用到錯幀同步方法去提升畫面的流暢度。

錯幀同步,簡單來說就是把目前的幾幀緩沖到子線程中處理,主線程直接傳回子線程之前的處理結果,屬于典型的以空間換時間政策。

錯幀同步政策也有不足之處,它不能在子線程中緩沖太多的幀,否則造成畫面延遲。另外,每個子線程配置設定的任務也要均衡(即每幀在子線程中的處理時間大緻相同),不然會因為 CPU 線程排程的時間消耗适得其反。

視訊圖像進行中的錯幀同步是怎麼實作的?

錯幀同步的原理如上圖所示,我們開啟三個線程:一個主線程,兩個工作線程,每一幀圖像的處理任務分為 2 步,第一個工作線程完成第一步處理,第二個工作線程完成第二步處理,每一幀都要經過這兩步的處理。

當主線程輸入第 n + 1 幀到第一個工作線程後,主線程會等待第二個工作線程中第 n 幀的處理結果然後傳回,這種情況下你肯定會問第 0 幀怎麼辦?第 0 幀就直接傳回就行了。

這些步驟下來,可以看成第 n+1 幀和第 n 幀在 2 個工作線程中同時處理,若忽略 CPU 線程排程時間,2 線程錯幀可以提升一倍的性能(性能提升情況,下面會給出實測資料)。

錯幀同步的簡單實作

錯幀同步在實作上類似于“生産者-消費者”模式,我們借助于 C 語言信号量

#include <semaphore.h>

可以很友善的實作錯幀同步模型。

C 的信号量常用的幾個 API :

--------------------------------------------------------------------
int sem_init(sem_t *sem, int pshared, unsigned int value);
    功能:初始化信号量
    參數:
        sem:指定要初始化的信号量
        pshared:0:應用于多線程
                非 0:多程序
        value:指定了信号量的初始值
    傳回值:0 成功
          -1 失敗
----------------------------------------------------------------------
int sem_destroy(sem_t *sem);
    功能:銷毀信号量
    參數:sem:指定要銷毀的信号量
    傳回值:0 成功
          -1 錯誤 
----------------------------------------------------------------------
int sem_post(sem_t *sem);
    功能:信号量的值加 1 操作
    參數:
        sem:指定的信号量,就是這個信号量加 1 
    傳回值:0 成功
          -1 錯誤 
-----------------------------------------------------------------------
int sem_wait(sem_t *sem);
    功能:信号量的值減 1 , 如果信号量的值為 0 , 阻塞等待
    參數:
        sem:指定的信号量, 如果信号量的值為 0, 阻塞等待, 否則信号量的值減 1
    傳回值:0 成功
          -1 錯誤      

在這裡為了簡化代碼邏輯,我們用字元串來表示視訊幀,每個工作線程對輸入的字元串進行标記,表示工作線程對視訊幀做了處理,最後的輸出(第 0 幀除外)都是經過工作線程标記過的字元串。

//初始化
void AsyncFramework::Init() {
    LOGCATE("AsyncFramework::Init");
    memset(work_buffers, 0, sizeof(work_buffers));
    work_thread_running = true;
    main_thread_running = true;
    index = 0;
    // 初始化 3 個信号量
    sem_init(&main_sem, 0, 0);
    sem_init(&first_thread_sem, 0, 0);
    sem_init(&second_thread_sem, 0, 0);
    // WORK_THREAD_NUM = 2 ,為 2 個工作線程申請 2 塊 buffer
    for (int i = 0; i < WORK_THREAD_NUM; ++i) {
        work_buffers[i] = static_cast<char *>(malloc(WORK_BUFFER_SIZE));
    }
    // 啟動三個線程
    main_thread = new thread(MainThreadProcess);
    first_thread = new thread(FirstStepProcess);
    second_thread = new thread(SecondStepProcess);
}      
// 反初始化
void AsyncFramework::UnInit() {
    LOGCATE("AsyncFramework::UnInit");
    //等待三個線程結束
    main_thread_running = false;
    main_thread->join();
    delete main_thread;
    main_thread = nullptr;
    work_thread_running = false;
    sem_post(&first_thread_sem);
    sem_post(&second_thread_sem);
    first_thread->join();
    second_thread->join();
    delete first_thread;
    first_thread = nullptr;
    delete second_thread;
    second_thread = nullptr;
    //銷毀信号量
    sem_destroy(&main_sem);
    sem_destroy(&first_thread_sem);
    sem_destroy(&second_thread_sem);
    //釋放緩沖區
    for (int i = 0; i < WORK_THREAD_NUM; ++i) {
        if (work_buffers[i]) {
            free(work_buffers[i]);
            work_buffers[i] = nullptr;
        }
    }
}      

主線程的邏輯就是不斷地生成“視訊幀”,将“視訊幀”傳給第一個工作線程進行第一步處理,然後等待第二個工作線程的處理結果。

void AsyncFramework::MainThreadProcess() {
    LOGCATE("AsyncFramework::MainThreadProcess start");
    while (main_thread_running) {
        memset(work_buffers[index % WORK_THREAD_NUM], 0, WORK_BUFFER_SIZE);
        sprintf(work_buffers[index % WORK_THREAD_NUM], "FrameIndex=%d ", index);
        //通知第一個工作線程處理
        sem_post(&first_thread_sem);
        if (index == 0) {
            //第 0 幀直接傳回,不交給工作線程處理
            LOGCATE("AsyncFramework::MainThreadProcess %s", work_buffers[index % WORK_THREAD_NUM]);
            index++;
            continue;
        } else {
            //等待第二個工作線程的處理結果 
            sem_wait(&main_sem);
        }
        LOGCATE("AsyncFramework::MainThreadProcess %s", work_buffers[(index - 1) % WORK_THREAD_NUM]);
        index++;
        if (index == 100) break;//生成100幀
    }
    LOGCATE("AsyncFramework::MainThreadProcess end");
}      

2個工作線程的處理邏輯類似,第一個工作線程收到主線程發來的信号,然後進行第一步處理,處理完成後通知第二個工作線程進行第二步處理,等到第二步處理完成後再通知主線程結束等待,取出處理結果。

void AsyncFramework::FirstStepProcess() {
    LOGCATE("AsyncFramework::FirstStepProcess start");
    int index = 0;
    while (true) {
        //等待主線程發來的信号
        sem_wait(&first_thread_sem);
        if(!work_thread_running) break;
        LOGCATE("AsyncFramework::FirstStepProcess index=%d", index);
        strcat(work_buffers[index % WORK_THREAD_NUM], "FirstStep ");
        //休眠模拟處理耗時
        this_thread::sleep_for(chrono::milliseconds(200));
        //處理完成後通知第二個工作線程進行第二步處理
        sem_post(&second_thread_sem);
        index++;
    }
    LOGCATE("AsyncFramework::FirstStepProcess end");
}
void AsyncFramework::SecondStepProcess() {
    LOGCATE("AsyncFramework::SecondStepProcess start");
    int index = 0;
    while (true) {
        //等待第一個工作線程發來的信号
        sem_wait(&second_thread_sem);
        if(!work_thread_running) break;
        LOGCATE("AsyncFramework::SecondStepProces index=%d", index);
        strcat(work_buffers[index % WORK_THREAD_NUM], "SecondStep");
        //休眠模拟處理耗時
        this_thread::sleep_for(chrono::milliseconds(200));
        //第二步處理完成後通知主線程結束等待
        sem_post(&main_sem);
        index++;
    }
    LOGCATE("AsyncFramework::SecondStepProcess end");
}      

主線程列印的處理結果(第 0 幀直接傳回,沒被處理):

視訊圖像進行中的錯幀同步是怎麼實作的?

我們設定視訊幀的 2 步處理一共耗時 400 ms (各休眠 200 ms),由于采用錯幀同步方式,主線程耗時隻有 200 ms 左右,性能提升一倍。

視訊圖像進行中的錯幀同步是怎麼實作的?
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。
視訊圖像進行中的錯幀同步是怎麼實作的?

繼續閱讀