利用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置空並從新放入空閒隊列的頭部以供下次使用。node

原理.png

解析ios

  • 咱們將空閒隊列設計爲頭進頭出,影響不大,由於咱們每次只須要從空閒隊列中取出一個空結點以供咱們裝入相機數據,因此不必按照尾進頭出的方式保證結點的順序。
  • 咱們將工做隊列設計爲尾進頭出,由於咱們要確保從相機中捕獲的數據是連續的,以便後期咱們播放出來的畫面也是連續的,因此工做隊列必須保證尾進頭出。
  • 這樣作咱們至關於實現了用空閒隊列當作緩衝隊列,在正常狀況下(fps=30,即每秒產生30幀數據,大約每33ms產生一幀數據),若是在33ms內對數據進行的操做能夠正常完成,則工做隊列會保持始終爲0或1,可是若是長期工做或遇到某一幀數據處理較慢的狀況(即處理時間大於33ms)則工做隊列的長度會增長,而正由於咱們使用了這樣的隊列會保護那一幀處理慢的數據在仍然可以正常處理完。
注意:這種情景僅用於短期內僅有幾幀數據處理較慢,若是好比1s內有20幾幀數據都處理很慢則可能致使工做隊列太長,則提現不出此隊列的優點。
2.結構
  • 結點
typedef struct XDXCustomQueueNode {
    void    *data;
    size_t  size;  // data size
    long    index;
    struct  XDXCustomQueueNode *next;
} XDXCustomQueueNode;
複製代碼

結點中使用void *類型的data存放咱們須要的sampleBuffer,使用index記錄當前裝入結點的sampleBuffer的索引,以便咱們在取出結點時比較是不是按照順序取出,結點中還裝着同類型下一個結點的元素。git

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

隊列中即爲咱們裝載的結點數量,由於咱們採用的是預先分配固定內存,因此工做隊列與空閒隊列的和始終不變(由於結點中的元素不在工做隊列就在空閒隊列)github

  • 類的設計
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,爲了優化咱們這裏的隊列至關於一個靜態鏈表,即結點的複用,由於當結點在工做隊列中使用完成後會將其中的數據置空並從新入隊到空閒隊列中,因此結點的總數始終保持不變。
  • 入隊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指向空閒隊列的頭結點,而後將當前結點變爲空閒隊列的頭結點;若是入隊操做是工做隊列,則使用尾進的方式,並對結點的index賦值,以便咱們在取出結點時能夠打印Index是否連續,若是連續則說明入隊時始終保持順序入隊。bash

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

  • 出隊
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);
}
複製代碼

當咱們將執行一些中斷操做,例如從本View跳轉到其餘View,或進入後臺等操做,咱們須要將工做隊列中的結點均置空而後從新放回空閒隊列,這樣能夠保證咱們最初申請的結點還均有效可用,保證結點不會丟失。post

--------------------------------------------------------

流程

1.初始化相機相關參數

常規流程,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建立一條線程,每隔10ms取一次數據,咱們能夠在此對取到的數據進行咱們想要的操做,操做完成後再將清空釋放sampleBuffer再將其裝入空閒隊列供咱們循環使用。

- (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,若是連續則說明在相機回調中放入的samplebuffer是連續的
        NSLog(@"Test index : %ld",node->index);
        
        /* 可在此處理從隊列中拿到的Buffer,用完後記得釋放內存並將結點從新放回空閒隊列
         * ........
         */
        
        CFRelease(sampleBuffer);
        node->data = NULL;
        _captureBufferQueue->EnQueue(_captureBufferQueue->m_free_queue, node);
    }
}
複製代碼
相關文章
相關標籤/搜索