利用C++ 設計緩存隊列實現高效傳輸相機數據(SampleBuffer)

利用C++ 設計緩存隊列實現高效傳輸相機數據


需求:

在做例如直播功能,有時我們可能要對相機捕獲的圖像數據做一些額外操作(Crop, Scale, 美顏等)但由于某些操作算法本身很耗時明肮,以fps為30為例能真,如果某一幀處理較慢將可能會掉幀流炕,所以設計一個緩沖隊列先將捕獲到的相機數據放入空閑隊列中赁遗,隨后程序中如果需要使用到相機數據則從工作隊列中取出需要的數據曼追。

適用情況

  • 在相機回調中對每一幀圖像進行耗時操作(Crop, Scale...)
  • 提升處理圖像的效率
  • 高效處理其他大數據量工作

注意:本例通過設計使用C++ 隊列來實現相機SampleBuffer的緩存工作顽决,需要使用Objective-C 與 C++混編短条。


GitHub地址(附代碼) : C++緩存隊列

簡書地址 : C++緩存隊列

博客地址 : C++緩存隊列

掘金地址 : C++緩存隊列


總體流程:

  • 設置始終橫屏,初始化相機參數設置代理
  • 在捕捉相機數據的回調中將samplebuffer放入空閑隊列
  • 開啟一條線程每隔10ms從工作隊列中取出samplebuffer可在此對數據處理才菠,處理完后將結點放回空閑隊列

隊列實現及解析

1.原理

初始化固定數量的結點裝入空閑隊列茸时,當相機回調產生數據后,從空閑隊列頭部取出一個結點將產生的每一幀圖像buffer裝入赋访,然后入隊到工作隊列的尾部可都,處理buffer的線程從工作隊列的頭部取出一個結點中的Buffer進行處理,處理完成后會將裝有次buffer的結點中data置空并重新放入空閑隊列的頭部以供下次使用蚓耽。

原理.png

解析

  • 我們將空閑隊列設計為頭進頭出渠牲,影響不大,因為我們每次只需要從空閑隊列中取出一個空結點以供我們裝入相機數據步悠,所以沒必要按照尾進頭出的方式保證結點的順序签杈。
  • 我們將工作隊列設計為尾進頭出,因為我們要確保從相機中捕獲的數據是連續(xù)的鼎兽,以便后期我們播放出來的畫面也是連續(xù)的答姥,所以工作隊列必須保證尾進頭出。
  • 這樣做我們相當于實現了用空閑隊列當做緩沖隊列谚咬,在正常情況下(fps=30,即每秒產生30幀數據鹦付,大約每33ms產生一幀數據),如果在33ms內對數據進行的操作可以正常完成择卦,則工作隊列會保持始終為0或1敲长,但是如果長期工作或遇到某一幀數據處理較慢的情況(即處理時間大于33ms)則工作隊列的長度會增加,而正因為我們使用了這樣的隊列會保護那一幀處理慢的數據在仍然能夠正常處理完秉继。
注意:這種情景僅用于短時間內僅有幾幀數據處理較慢潘明,如果比如1s內有20幾幀數據都處理很慢則可能導致工作隊列太長,則提現不出此隊列的優(yōu)勢秕噪。
2.結構
  • 結點
typedef struct XDXCustomQueueNode {
    void    *data;
    size_t  size;  // data size
    long    index;
    struct  XDXCustomQueueNode *next;
} XDXCustomQueueNode;

結點中使用void *類型的data存放我們需要的sampleBuffer,使用index記錄當前裝入結點的sampleBuffer的索引钳降,以便我們在取出結點時比較是否是按照順序取出,結點中還裝著同類型下一個結點的元素腌巾。

  • 隊列類型
typedef struct XDXCustomQueue {
    int size;
    XDXCustomQueueType type;
    XDXCustomQueueNode *front;
    XDXCustomQueueNode *rear;
} XDXCustomQueue;

隊列中即為我們裝載的結點數量遂填,因為我們采用的是預先分配固定內存铲觉,所以工作隊列與空閑隊列的和始終不變(因為結點中的元素不在工作隊列就在空閑隊列)

  • 類的設計
class XDXCustomQueueProcess {
    
private:
    pthread_mutex_t free_queue_mutex;
    pthread_mutex_t work_queue_mutex;
    
public:
    XDXCustomQueue *m_free_queue;
    XDXCustomQueue *m_work_queue;
    
    XDXCustomQueueProcess();
    ~XDXCustomQueueProcess();
    
    // Queue Operation
    void InitQueue(XDXCustomQueue *queue,
                   XDXCustomQueueType type);
    void EnQueue(XDXCustomQueue *queue,
                 XDXCustomQueueNode *node);
    XDXCustomQueueNode *DeQueue(XDXCustomQueue *queue);
    void ClearXDXCustomQueue(XDXCustomQueue *queue);
    void FreeNode(XDXCustomQueueNode* node);
    void ResetFreeQueue(XDXCustomQueue *workQueue, XDXCustomQueue *FreeQueue);
};

因為涉及到異步操作,所以需要對結點的操作加鎖吓坚,使用時需要先初始化隊列撵幽,然后定義了入隊,出隊礁击,清除隊列中元素盐杂,釋放結點,重置空閑隊列等操作哆窿。

3.實現
  • 初始化隊列
const int XDXCustomQueueSize = 3;
XDXCustomQueueProcess::XDXCustomQueueProcess() {
    m_free_queue = (XDXCustomQueue *)malloc(sizeof(struct XDXCustomQueue));
    m_work_queue = (XDXCustomQueue *)malloc(sizeof(struct XDXCustomQueue));
    
    InitQueue(m_free_queue, XDXCustomFreeQueue);
    InitQueue(m_work_queue, XDXCustomWorkQueue);
    
    for (int i = 0; i < XDXCustomQueueSize; i++) {
        XDXCustomQueueNode *node = (XDXCustomQueueNode *)malloc(sizeof(struct XDXCustomQueueNode));
        node->data = NULL;
        node->size = 0;
        node->index= 0;
        this->EnQueue(m_free_queue, node);
    }
    
    pthread_mutex_init(&free_queue_mutex, NULL);
    pthread_mutex_init(&work_queue_mutex, NULL);
    
    NSLog(@"XDXCustomQueueProcess Init finish !");
}

假設空閑隊列結點總數為3.首先為工作隊列與空閑隊列分配內存链烈,其次對其分別進行初始化操作,具體過程可參考Demo,然后根據結點總數來為每個結點初始化分配內存挚躯,并將分配好內存的結點入隊到空閑隊列中强衡。

注意:結點的重用,我們僅僅初始化幾個固定數量的結點码荔,因為處理數據量較大漩勤,沒有必要讓程序始終做malloc與free,為了優(yōu)化我們這里的隊列相當于一個靜態(tài)鏈表缩搅,即結點的復用越败,因為當結點在工作隊列中使用完成后會將其中的數據置空并重新入隊到空閑隊列中,所以結點的總數始終保持不變硼瓣。
  • 入隊Enqueue
void XDXCustomQueueProcess::EnQueue(XDXCustomQueue *queue, XDXCustomQueueNode *node) {
    if (queue == NULL) {
        NSLog(@"XDXCustomQueueProcess Enqueue : current queue is NULL");
        return;
    }
    
    if (node==NULL) {
        NSLog(@"XDXCustomQueueProcess Enqueue : current node is NULL");
        return;
    }
    
    node->next = NULL;
    
    if (XDXCustomFreeQueue == queue->type) {
        pthread_mutex_lock(&free_queue_mutex);
        
        if (queue->front == NULL) {
            queue->front = node;
            queue->rear  = node;
        }else {
            /*
             // tail in,head out
             freeQueue->rear->next = node;
             freeQueue->rear = node;
             */
            
            // head in,head out
            node->next = queue->front;
            queue->front = node;
        }
        queue->size += 1;
        NSLog(@"XDXCustomQueueProcess Enqueue : free queue size=%d",queue->size);
        pthread_mutex_unlock(&free_queue_mutex);
    }
    
    if (XDXCustomWorkQueue == queue->type) {
        pthread_mutex_lock(&work_queue_mutex);
        //TODO
        static long nodeIndex = 0;
        node->index=(++nodeIndex);
        if (queue->front == NULL) {
            queue->front = node;
            queue->rear  = node;
        }else {
            queue->rear->next   = node;
            queue->rear         = node;
        }
        queue->size += 1;
        NSLog(@"XDXCustomQueueProcess Enqueue : work queue size=%d",queue->size);
        pthread_mutex_unlock(&work_queue_mutex);
    }
}

如上所述究飞,入隊操作如果是空閑隊列,則使用頭進的方式巨双,即始終讓入隊的結點在隊列的頭部噪猾,具體代碼實現即讓當前結點的next指向空閑隊列的頭結點霉祸,然后將當前結點變?yōu)榭臻e隊列的頭結點筑累;如果入隊操作是工作隊列,則使用尾進的方式丝蹭,并對結點的index賦值慢宗,以便我們在取出結點時可以打印Index是否連續(xù),如果連續(xù)則說明入隊時始終保持順序入隊奔穿。

這里使用了簡單的數據結構中的知識镜沽,如有不懂可上網進行簡單查閱

  • 出隊
XDXCustomQueueNode* XDXCustomQueueProcess::DeQueue(XDXCustomQueue *queue) {
    if (queue == NULL) {
        NSLog(@"XDXCustomQueueProcess DeQueue : current queue is NULL");
        return NULL;
    }
    
    const char *type = queue->type == XDXCustomWorkQueue ? "work queue" : "free queue";
    pthread_mutex_t *queue_mutex = ((queue->type == XDXCustomWorkQueue) ? &work_queue_mutex : &free_queue_mutex);
    XDXCustomQueueNode *element = NULL;
    
    pthread_mutex_lock(queue_mutex);
    element = queue->front;
    if(element == NULL) {
        pthread_mutex_unlock(queue_mutex);
        NSLog(@"XDXCustomQueueProcess DeQueue : The node is NULL");
        return NULL;
    }
    
    queue->front = queue->front->next;
    queue->size -= 1;
    pthread_mutex_unlock(queue_mutex);
    
    NSLog(@"XDXCustomQueueProcess DeQueue : %s size=%d",type,queue->size);
    return element;
}

出隊操作無論空閑隊列還是工作隊列都是從頭出,即取出當前隊列頭結點中的數據贱田。

注意:該結點為空與該結點中的數據為空不可混為一談缅茉,如果該結點為空則說明沒有從隊列中取出結點,即空結點沒有內存地址男摧,而結點中的數據則為node->data,在本Demo中為相機產生的每一幀sampleBuffer數據蔬墩。
  • 重置空閑隊列數據
void XDXCustomQueueProcess::ResetFreeQueue(XDXCustomQueue *workQueue, XDXCustomQueue *freeQueue) {
    if (workQueue == NULL) {
        NSLog(@"XDXCustomQueueProcess ResetFreeQueue : The WorkQueue is NULL");
        return;
    }
    
    if (freeQueue == NULL) {
        NSLog(@"XDXCustomQueueProcess ResetFreeQueue : The FreeQueue is NULL");
        return;
    }
    
    int workQueueSize = workQueue->size;
    if (workQueueSize > 0) {
        for (int i = 0; i < workQueueSize; i++) {
            XDXCustomQueueNode *node = DeQueue(workQueue);
            CFRelease(node->data);
            node->data = NULL;
            EnQueue(freeQueue, node);
        }
    }
    NSLog(@"XDXCustomQueueProcess ResetFreeQueue : The work queue size is %d, free queue size is %d",workQueue->size, freeQueue->size);
}

當我們將執(zhí)行一些中斷操作译打,例如從本View跳轉到其他View,或進入后臺等操作拇颅,我們需要將工作隊列中的結點均置空然后重新放回空閑隊列奏司,這樣可以保證我們最初申請的結點還均有效可用,保證結點不會丟失樟插。


流程

1.初始化相機相關參數

常規(guī)流程韵洋,Demo中有實現,在此不復述

2.將samplebuffer放入空閑隊列

設置相機代理后黄锤,在 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection 方法中將samplebuffer裝入空閑隊列

- (void)addBufferToWorkQueueWithSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    XDXCustomQueueNode *node = _captureBufferQueue->DeQueue(_captureBufferQueue->m_free_queue);
    if (node == NULL) {
        NSLog(@"XDXCustomQueueProcess addBufferToWorkQueueWithSampleBuffer : Data in , the node is NULL !");
        return;
    }
    CFRetain(sampleBuffer);
    node->data = sampleBuffer;
    _captureBufferQueue->EnQueue(_captureBufferQueue->m_work_queue, node);

    NSLog(@"XDXCustomQueueProcess addBufferToWorkQueueWithSampleBuffer : Data in ,  work size = %d, free size = %d !",_captureBufferQueue->m_work_queue->size, _captureBufferQueue->m_free_queue->size);
}

注意:因為相機回調中捕捉的sampleBuffer是有生命周期的所以需要手動CFRetain一下使我們隊列中的結點持有它搪缨。

3.開啟一條線程處理隊列中的Buffer

使用pthread創(chuàng)建一條線程,每隔10ms取一次數據,我們可以在此對取到的數據進行我們想要的操作猜扮,操作完成后再將清空釋放sampleBuffer再將其裝入空閑隊列供我們循環(huán)使用勉吻。

- (void)handleCacheThread {
    while (true) {
        // 從隊列取出在相機回調中放入隊列的線程
        XDXCustomQueueNode *node = _captureBufferQueue->DeQueue(_captureBufferQueue->m_work_queue);
        if (node == NULL) {
            NSLog(@"Crop handleCropThread : Data node is NULL");
            usleep(10*1000);
            continue;
        }
        
        CMSampleBufferRef sampleBuffer     = (CMSampleBufferRef)node->data;
        // 打印結點的index,如果連續(xù)則說明在相機回調中放入的samplebuffer是連續(xù)的
        NSLog(@"Test index : %ld",node->index);
        
        /* 可在此處理從隊列中拿到的Buffer旅赢,用完后記得釋放內存并將結點重新放回空閑隊列
         * ........
         */
        
        CFRelease(sampleBuffer);
        node->data = NULL;
        _captureBufferQueue->EnQueue(_captureBufferQueue->m_free_queue, node);
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末齿桃,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子煮盼,更是在濱河造成了極大的恐慌短纵,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僵控,死亡現場離奇詭異香到,居然都是意外死亡,警方通過查閱死者的電腦和手機报破,發(fā)現死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門悠就,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人充易,你說我怎么就攤上這事梗脾。” “怎么了盹靴?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵炸茧,是天一觀的道長。 經常有香客問我稿静,道長梭冠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任改备,我火速辦了婚禮控漠,結果婚禮上,老公的妹妹穿的比我還像新娘悬钳。我一直安慰自己盐捷,他們只是感情好柬脸,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毙驯,像睡著了一般倒堕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上爆价,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天垦巴,我揣著相機與錄音,去河邊找鬼铭段。 笑死骤宣,一個胖子當著我的面吹牛,可吹牛的內容都是我干的序愚。 我是一名探鬼主播憔披,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爸吮!你這毒婦竟也來了芬膝?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤形娇,失蹤者是張志新(化名)和其女友劉穎锰霜,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體桐早,經...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡癣缅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了哄酝。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片友存。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖陶衅,靈堂內的尸體忽然破棺而出屡立,到底是詐尸還是另有隱情,我是刑警寧澤万哪,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布侠驯,位于F島的核電站抡秆,受9級特大地震影響奕巍,放射性物質發(fā)生泄漏。R本人自食惡果不足惜儒士,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一的止、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧着撩,春花似錦诅福、人聲如沸匾委。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赂乐。三九已至,卻和暖如春咖气,著一層夾襖步出監(jiān)牢的瞬間挨措,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工崩溪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留浅役,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓伶唯,卻偏偏與公主長得像觉既,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乳幸,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353