gstreamer學習筆記---gst-omx

一、openMAX理解1

  gst-omx是基於openMAX開發的插件,所以在介紹gst-omx之前,我們先了解一下openMAX。
  openMAX:open media acceleration,開源多媒體加速器,與多媒體相關,爲多媒體處理提供統一的接口標準,以此達到跨平臺的多媒體軟硬件開發目標。
  一個標準的多媒體設備包括硬件、設備驅動以及設備應用程序,與之對應的是openMAX的DL、IL和AL層。

openMAX 框架圖

  • DL:Delelopment Layer 在這一層,定義了衆多的API,包含openMAX音頻、視頻以及圖像處理功能,這些功能由芯片開發商完成並優化;
  • IL:Integration Layer 使得應用程序與多媒體框架可以以一種統一的方式與多媒體編解碼器對接;
  • AL:Application LAyer AL層爲多媒體中間件與應用層之間提供了一個標準化的API接口,爲多媒體接口提供跨平臺可移植性;

  這些層級,可以互相結合實現,也可以單獨實現某個層級,比如有些芯片廠商,根據芯片編解碼硬件實現openMAX的IL層,這樣使得基於該廠商硬件以及相關中間件開發的應用程序可以快速移植到其他符合openMAX IL的軟硬件設備上。
  在IL層,有兩個關鍵部件:component(組件)和 IL Client。

  • component:音視頻、字幕分解組件音頻解碼組件、音頻輸出組件、視頻解碼組件、視頻輸出組件;
  • IL Client:IL的客戶端,組件的管理者,Client通過組件內部提供相關的回調函數對組件進行管理,應用程序可以調用client的接口來操作組件,包括設置組件的狀態、添加一個組件、銷燬一個組件等。

  buffer發送模塊

tunnel模式(綁定模式)

如果組件A是數據提供者,完整的數據傳遞如下:

  1. 組件A調用組件B(通過port實現)的OMX_EmptyThisBuffer(B, pBuffer)宏來實現數據從A傳遞到B;
  2. 組件B調用組件A(通過port實現)的OMX_FillThisBuffer(A, pBuffer)宏來完成數據從B到A的還回過程;

如果組件B是數據提供者:

  1. 組件B調用組件A(通過port實現)的OMX_FillThisBuffer(A, pBuffer)宏來完成buffer數據從B到A的傳遞過程;
  2. 組件A填充收到的buffer數據結構,然後調用組件B(通過port實現)的OMX_EmptyThisBuffer(B, pBuffer)宏來實現數據從A到B的還回過程;
Non-tunnel模式(非綁定模式)

在該模式下,數據傳遞的雙方變成了IL Client與組件了,過程如下:

  1. IL Client通過OMX_FillThisBuffer(pHandle, 1, pBuffer)回調方法向組件A的output端口提供一個空的buffer結構以待填充,該宏是通過IL Core來完成對組件A的FillThisBuffer回調的調用;
  2. 然後IL Client向組件A傳遞一個編碼數據buffer,然後處理完畢之後生成新的buffer數據填充到上一步接收到的空buffer結構;
  3. 等待組件A將output的buffer填充完畢,組件A就用OMX_FillBufferDone(pBuffeerOut)方法通知IL Client接收處理後的數據;
  4. IL Client數據處理完畢之後,就會再次調用OMX_FillThisBuffer(pHandle, 1, pBuffer)來還回buffer到output列表等待再次填充,這樣完成一次數據交互;

  在gst-omx中,一般情況下應該使用的是Non-tunnel模式,具體情況具體分析。然後,gst-omx源碼結構大概如下:

gst-omx-1.14.2
    |---omx
         |---gstomxvideodec.c   /* 類似編解碼這個的,相當與是openMAX AL,在他們上面有更詳細的編解碼插件 */
         |---gstomx.c           /* 相當於IL Client,對接openMAX IL,內有回調函數,事件、消息處理函數,負責與component交互 */
         |---openmax            /* openMAX IL層頭文件,需要芯片廠商實現其中必要函數 */

  openMAX與gst-omx簡單介紹完畢,下面將相應瞭解gst-omx的解碼流程。

二、gst-omx流程

  以h264dec爲例,瞭解gst-omx流程(以 gst-omx 1.14.2 爲例)。
  在進入到具體的OMX類element之前,先了解omx相關的element是如何登記到gstreamer。
  通過gst-omx中src目錄下的Makefile.am可以瞭解到,該package最終將會編譯爲一個叫libgstomx.so的庫,庫是gstreamer element庫,將會存放在機器端的/usr/lib/gstreamer-1.0目錄下。在加載插件的時候,gstreamer核心將會逐個掃描該目錄下的文件,符合相關符號標準的將會加載,識別其中的feature並登記。在這個過程,將會掃描 libgstomx.so,然後在加載插件的時候將會調用到gstomx.c中的plugin_init()註冊插件,omx相關的插件,就是在這個函數註冊到gstreamer。
  gstomx.c的plugin_init()主要操作如下:

  • 獲取gstomx的配置文件gstomx.conf,該文件一般將會放在機器端的/etc/xdg目錄下,該配置文件主要是說明element的名稱、rank、port等信息,在加載的時候將會先獲取該信息,再加載相應的插件;
  • 在前面的文章有說過,向glib核心註冊一種新的類型,將會是第一次調用gst_xxx_get_type(),所以接下來又將會通過gstomx.c文件定義的靜態數組types,逐個初始化omx相關的類型;
  • 最後就是通過解析配置文件,獲取相應element的信息,而後調用gst_element_register()註冊;

  上面提到的獲取element信息之後,再註冊到gstreamer core,瞭解該步驟可以先看一下配置文件的內容,以omxh264dec爲例:

[omxh264dec]
type-name=GstOMXH264Dec
core-name=/usr/local/lib/libomxil-bellagio.so.0
component-name=OMX.st.video_decoder.avc
rank=257
in-port-index=0
out-port-index=1
hacks=event-port-settings-changed-ndata-parameter-swap;event-port-settings-changed-port-0-to-1

  在gstomx.c中,將會讀取配置文件gstomx.conf,將每一項類似[omxh264dec]的元素當作一個element,然後進行註冊:

  1. 將會type-name得到之前初始化的omx相關的類型GType;
  2. 根據core-name提供的庫路徑,檢查element所在的庫是否符合要求,檢查component-name元素是否存在等;
  3. 通過g_type_register_static(),從omx相關的類型GType衍生一個新的omx類型,並通過gst_element_register()註冊爲element;
  4. 在通過g_type_register_static()衍生新的omx類的時候,有傳進type_info參數,該參數賦值了class_init等成員,通過該成員,將會在初始化該類的時候,調用到該成員函數(賦值爲gstomx.c的_class_init()),在該函數中將會讀取配置項填充該衍生類,在操作該衍生類的時候將會使用到這些參數。

  所以,根據上述介紹,我們再看看omxh264dec的繼承關係,將會一目瞭然,其中,GstOMXH264Dec-omxh264dec就是衍生類:

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstVideoDecoder
                         +----GstOMXVideoDec
                               +----GstOMXH264Dec
                                     +----GstOMXH264Dec-omxh264dec

  創建omxh264dec之後,將會進行link操作,先是omxh264dec與上游element link,這個操作將會查詢omxh264dec sink pad支持的caps,而查詢caps的操作,又將會查詢src pad的PEER pad支持的caps,但是此時並沒有PEER pad,所以這個返回爲NULL,而查詢omxh264dec sink pad支持的caps將會返回sink pad的template caps,與上游element取交集之後,有交集則link成功。
  同樣的,omxh264dec與下游element link的時候,也都將會查詢src pad caps,將會返回template caps,有交集則link成功。
  link成功之後,接下來的將會是element狀態切換。

  2.1 NULL—>READY

  在gstomx中,切換到該狀態並沒有進行什麼操作,主要操作是在gstvideodecoder的狀態切換函數。當切換到READY時,將會通過decoder_class->open (decoder)打開設備或者解碼庫,通過這樣的一個方式,將會調用到gst_omx_video_dec_open()。
  在該函數中,將會進行以下操作:

  1. 將會先通過gst_omx_component_new()創建新的component,而component組件的信息,就是在上面提到的,gstomx.c的_class_init()函數中填充,在這裏開始起作用的,用於創建組件時填充相應的信息,由於我們這裏介紹的是解碼,自然就是self->dec組件了;
  2. 接下來gst_omx_video_dec_open()的操作就是確認dec component的Status爲OMX_StateLoaded,獲取dec component的port index,然後通過gst_omx_component_get_parameter()獲取port的OMX_IndexParamPortDefinition參數信息,並將port添加到dec component,這樣就完成了dec open操作;
  self->dec =
      gst_omx_component_new (GST_OBJECT_CAST (self), klass->cdata.core_name,
      klass->cdata.component_name, klass->cdata.component_role,
      klass->cdata.hacks);
  • gst_omx_component_new()將會通過調用gst_omx_core_acquire()打開配置文件core-name聲明的element庫,然後通過g_module_symbol()獲取相應的OMXCore函數句柄,這樣子,gst-omx就與openMAX IL層對接起來了;
  • 在gst_omx_core_acquire()之後,將會通過core->get_handle()根據component_name將相應的OMX_CALLBACKTYPE回調函數註冊到openMAX IL層component;
  • 最後就是根據傳進來的一些信息,填充component;

  至此,NULL—>READY切換完成。

  2.2 READY—>PAUSED

  在該過程,GstOMXVideoDec的狀態切換隻是對dec component的某些狀態變量復位,重要的操作,還是通過GstVideoDecoder的狀態切換函數gst_video_decoder_change_state()完成。
  在這個切換過程,將會對component進行reset,然後再調用start函數。在gst_video_decoder_reset()中,就是對input/output port的時間管理、狀態管理、緩衝池管理等進行釋放或者失能操作,相應的狀態變量復位。而start函數gst_omx_video_dec_start(),也只是簡單的進行dec的狀態變量復位而已。
  是不是感覺gstomx在這個過程並沒有進行什麼操作?其實並不然,只不過是因爲gstreamer都已經幫我們封裝好了。還記得之前的文章《gstreamer學習筆記—pad定義、連接、流動》,就有將會,在從READY切換到PAUSED的時候,將會在gst_element_change_state_func()中調用gst_element_pads_activate (element, TRUE)激活pad,但是看了一遍下來,GstOMXH264Dec-omxh264dec的繼承關係都沒有重載激活pad相關的操作,所以調用常規的激活函數,選擇了push模式,最終完成這個狀態切換。

  2.3 PAUSED操作

  之前的文章有介紹過,pipeline的狀態切換,是從sink到src進行,所以在dec完成該狀態切換之後,pipeline將會繼續設置上游element的狀態爲PAUSED。有一點需要注意的,gstomx中的大多數都是編碼或者解碼的element,這裏我們以解碼來介紹,編碼也將會是類似的。decoder的上游將會是parser、demux以及最終的source,根據pipeline的屬性,將會逐個設置,當將demux的狀態切換爲PAUSED時,激活pad的時候,將會與source element協商,然後採用PULL模式,demux從source獲取文件數據(這個source可以是本地文件、網絡文件以及其他,同時demux與上游element一般是採用pull模式)。
  在demux切換到PAUSED狀態時,將會激活pad,在這個過程demux將會進行解封裝操作,知道音視頻的類型等信息之後,將會添加pad到demux,這個操作將會觸發pipeline的pad-added信號,一般的在信號的接收函數中將會把demux與下游的parser link起來(這個在之前的文章《gstreamer學習筆記—demux使用》介紹過)。
  同時,之前的文章也介紹過,gstreamer是以stream的形式傳輸數據,所以在數據開始流通之前,將會發送stream-start事件。
  那麼,在這裏,又將會是誰負責處理EVENT呢?最終將會是gstvideodecoder.c中的gst_video_decoder_sink_event_default()負責處理sinkpad EVENT。
  接收到stream-startEVENT,將會調用gst_video_decoder_drain_out (decoder, FALSE),在該函數中,最終將會調用到gst_omx_video_dec_drain(),而gst_omx_video_dec_drain()函數是負責將dec component中的input buffer全都送往解碼器的,但是由於此時解碼器還沒有工作,所以直接返回了。在dec sink pad的事件處理函數初始化upstream_tags,再將該事件往下游element送。
  前面說到,element間已經link,link成功只是說明它們之間caps有交集,但是,相互之間,具體使用哪一種caps並沒有協商好,所以在這個時候,將會有多次的pad caps查詢操作,但是在omx dec中,都只是返回template caps,知道收到上游的GST_EVENT_CAPS。這個caps中將會是包含什麼信息呢,簡單理解,播放一個視頻,在解封裝的時候,就應該知道它的編碼格式、視頻分辨率以及幀率吧,這個caps EVENT包含的就是這些信息,當然,還有其他信息,但是我們先這樣理解便好。
  而在上游element push caps EVENT時,真正調用GST_PAD_EVENTFUNC()之前,會先通過pre_eventfunc_check()發送accept caps query,這個查詢操作,是根據上游傳進來的caps信息,再與自身caps取交集,有交集則將會把caps EVENT傳遞到下游element。
  gstomx接收到caps EVENT,從event解析得到caps之後,將會調用gst_video_decoder_setcaps (decoder, caps)將caps信息設置到解碼器。

static gboolean
gst_video_decoder_sink_event_default (GstVideoDecoder * decoder,
    GstEvent * event)
{
  switch (GST_EVENT_TYPE (event)) {
  case GST_EVENT_CAPS:
  {
      GstCaps *caps;

      gst_event_parse_caps (event, &caps);
      ret = gst_video_decoder_setcaps (decoder, caps);
      gst_event_unref (event);
      event = NULL;
      break;
  }
  ...
  }
}

  在dec中,一樣的將會有一個GstVideoCodecState類型的input_state變量標示輸入port的狀態信息,所以,在setcaps()中,將會先檢查這個狀態信息是否存在,存在的話再確認信息是否和將要設置的caps一致,一致則不設置,退出函數,不一致,將釋放input_state,我們第一次setcaps,這個變量爲NULL,將進行設置操作。在setcaps()中,主要是調用decoder_class->set_format (decoder, state)設置dec參數,在這裏,將會調用到gst_omx_video_dec_set_format(),下面,我們將一起來分析一下,這個函數是如何完成解碼器參數設置。

  1. 將通過gst_omx_port_get_port_definition (self->dec_in_port, &port_def)獲取omx IL層的dec input port的OMX_IndexParamPortDefinition參數信息,至於在這裏,openMAX IL應該返回什麼信息,我們接着往下看,看是怎麼處理這個port_def參數的;
  2. 根據返回的信息,與將要設置的caps比較分辨率、幀率、codec_data(解碼或者編碼需要用到的信息)等數據是否一致;
  3. 將通過klass->is_format_change()調用具體的解碼element特有的參數,是否一致,比如h264 dec將會調用gst_omx_h264_dec_is_format_change(),判斷相應的信息再返回;
  4. 因爲在設置caps的時候,可能這個解碼器之前已經工作了,所以將會再次獲取dec 組件的狀態,如果不是OMX_StateLoaded狀態,那麼將會需要失能當前dec component;
  5. 如果dec component已經使能且caps沒有改變,那麼直接替換GstVideoCodecState返回成功,否則繼續往下走;
  6. 如果需要失能dec component且caps已經改變,那麼將失能dec component,再重新通過gst_omx_port_get_port_definition()獲取dec component信息;
  7. 至此,dec component已經失能且caps確認改變,接着,將會根據caps信息,填充port_def,並通過gst_omx_port_update_port_definition()將port_def設置到dec component;
  8. 調用klass->set_format (self, self->dec_in_port, state)設置視頻數據格式到解碼器,由於gstomxvideodec.c只是omx解碼器的繼承類,所以這個函數將會由相應的解碼component實現,比如H264的將會是gst_omx_h264_dec_set_format(),這樣解碼器就知道輸出數據的格式、分辨率、幀率等信息;
  9. 最後通過gst_omx_port_update_port_definition (self->dec_out_port, NULL)更新port的port_def信息,更新self->input_state,setcaps操作完成。

  設置了caps,音視頻數據在gstreamer中是通過segment表示,所以,上游又將會發送一個GST_FORMAT_TIME類型的segment EVENT,segment中將包含將要播放視頻的時間片段範圍,dec將會保存該segment信息。
  接下來將會有一些tag EVENT,包含編碼格式、幀率、language-code等信息,這些我們先忽略。我們設置瞭解碼器格式、分辨率,也都與上游element協商了caps,但是解碼器的輸出呢,輸出格式、分辨率等信息都還沒有與下游element協商好,什麼時候會最終定下來呢,我們接着往下看。

  2.4 PLAYING操作

  來到最重要的接收數據環節,GstVideoDecoder的繼承類,一般的都將會是gst_video_decoder_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)負責處理上游element push的數據,除非繼承類重寫了sink pad chain_function。
  gst_video_decoder_chain()很簡單,核心在於通過gst_video_decoder_chain_forward (decoder, buf, FALSE)完成解碼等操作。在gst_video_decoder_chain_forward()中,將會通過priv->current_frame保存視頻buf,由於一般的解碼數據都是壓縮的,所以將會進入gst_video_decoder_decode_frame (decoder, priv->current_frame)。而在該函數,將會獲取buf的pts、dts、duration等信息,最終通過decoder_class->handle_frame()調用到gstomxvideodec.c的gst_omx_video_dec_handle_frame()。

static GstFlowReturn
gst_video_decoder_decode_frame (GstVideoDecoder * decoder,
    GstVideoCodecFrame * frame)
{
  GstVideoDecoderPrivate *priv = decoder->priv;
  GstVideoDecoderClass *decoder_class;
  GstFlowReturn ret = GST_FLOW_OK;

  decoder_class = GST_VIDEO_DECODER_GET_CLASS (decoder);

  frame->distance_from_sync = priv->distance_from_sync;
  priv->distance_from_sync++;
  frame->pts = GST_BUFFER_PTS (frame->input_buffer);
  frame->dts = GST_BUFFER_DTS (frame->input_buffer);
  frame->duration = GST_BUFFER_DURATION (frame->input_buffer);

  ...
  
  /* 將該frame保存到decoder->priv->frames,最終解碼完成的時候有用到 */
  priv->frames = g_list_append (priv->frames, frame);
  
  ...

  /* do something with frame */
  ret = decoder_class->handle_frame (decoder, frame);

  return ret;
}

  gst_omx_video_dec_handle_frame()又將會進行什麼操作呢,繼續看。

static GstFlowReturn
gst_omx_video_dec_handle_frame (GstVideoDecoder * decoder,
    GstVideoCodecFrame * frame)
{
  /* 在這裏,將會檢查dec component是否已經開始解碼了,如果還沒有,
   * 還需要檢查當前幀是否是關鍵幀,因爲解碼需要從關鍵幀開始 */
  if (!self->started) {
    if (!GST_VIDEO_CODEC_FRAME_IS_SYNC_POINT (frame)) {
      gst_video_decoder_drop_frame (GST_VIDEO_DECODER (self), frame);
      return GST_FLOW_OK;
    }

    /* 還沒有開始解碼,當前幀已經是關鍵幀,如果
     * output port flushing,enable dec component */
    if (gst_omx_port_is_flushing (self->dec_out_port)) {
      if (!gst_omx_video_dec_enable (self, frame->input_buffer))
        goto enable_error;
    }

    /* 關鍵幀、解碼component已經使能,運行解碼線程 */
    gst_pad_start_task (GST_VIDEO_DECODER_SRC_PAD (self),
        (GstTaskFunction) gst_omx_video_dec_loop, decoder, NULL);
  }
  ...
}

  通過上面我們可以清晰的知道,剛開始進行handle_frame()的操作,先不管dec component是如何進行解碼的,我們看看gst_omx_video_dec_enable()幹了什麼。


static gboolean
gst_omx_video_dec_enable (GstOMXVideoDec * self, GstBuffer * input)
{
  GstOMXVideoDecClass *klass = GST_OMX_VIDEO_DEC_GET_CLASS (self);

  /* 選擇內存申請的方式,是動態還是其它 */
  self->input_allocation = gst_omx_video_dec_pick_input_allocation_mode (self,
      input);

  if (self->disabled) {
    ...
  } else {
    /* 進行協商 */
    if (!gst_omx_video_dec_negotiate (self))
      GST_LOG_OBJECT (self, "Negotiation failed, will get output format later");

    if (!(klass->cdata.hacks & GST_OMX_HACK_NO_DISABLE_OUTPORT)) {
      ...
    } else {
      /* 使能dec component */
      if (gst_omx_component_set_state (self->dec,
              OMX_StateIdle) != OMX_ErrorNone)
        return FALSE;

      /* 申請input port buffer */
      if (!gst_omx_video_dec_allocate_in_buffers (self))
        return FALSE;
      /* 申請output port buffer */
      if (gst_omx_port_allocate_buffers (self->dec_out_port) != OMX_ErrorNone)
        return FALSE;
    }

    ...
  }

  /* 設置dec_in_port->flushing和dec_out_port->flushing爲FALSE */
  gst_omx_port_set_flushing (self->dec_in_port, 5 * GST_SECOND, FALSE);
  gst_omx_port_set_flushing (self->dec_out_port, 5 * GST_SECOND, FALSE);

  ...

  self->disabled = FALSE;

  return TRUE;
}

  在gst_omx_video_dec_enable()中,由於我們是第一次使能dec component,所以將會進行協商,協商之後就進行內存申請。
  需要注意的是,這裏的協商函數gst_omx_video_dec_negotiate(),是 與下游element進行協商,上游的element之前已經協商完成了。gst_omx_video_dec_negotiate()主要完成以下操作:

  1. 通過gst_pad_peer_query_caps()獲取下游element支持的caps;
  2. 通過gst_omx_video_get_supported_colorformats()調用gst_omx_component_get_parameter()獲取OMX_IndexParamVideoPortFormat參數信息,這樣參數將會返回openMAX IL支持的format,這裏將會是一個枚舉過程;
  3. 得到雙方支持的format之後,取交集,然後通過gst_omx_component_set_parameter (self->dec, OMX_IndexParamVideoPortFormat, &param)將format等信息設置到解碼器,至此,協商完成。

  協商完成之後,接下來就是input port和output port的內存申請。
  在通過gst_omx_video_dec_allocate_in_buffers()申請input buf的時候,最終通過gst_omx_port_allocate_buffers()調用到gst_omx_port_allocate_buffers_unlocked()。該函數主要是做三件事:

  • 通過gst_omx_port_update_port_definition (port, NULL)更新port信息;
  • 通過宏OMX_AllocateBuffer調用openMAX IL的AllocateBuffer函數申請buffer;
  • 將申請到的buffer添加到port->pending_buffers。

  同樣的,在通過gst_omx_port_allocate_buffers (self->dec_out_port)給output port申請buffer也是一樣的操作,輸出buffer一般是解碼器申請的物理地址連續的內存,GstOMXBuffer類型的變量buf->omx_buf->pBuffer就是指向這塊內存(虛擬地址)。
  協商之後,申請buffer,接着將會設置dec component狀態爲OMX_StateExecuting,最後將輸入輸出port的flushing置爲FALSE,使能dec component完成。
  回到handle_frame()—gst_omx_video_dec_handle_frame(),在使能dec component之後,將會創建task運行gst_omx_video_dec_loop()負責解碼,到這裏,我們的解碼器硬件已經初始化完成,數據的格式、分辨率、解碼輸出格式、buffer等參數已經設置,解碼task已經運行,接下來就是解碼操作了。
  在task中,通過gst_omx_port_acquire_buffer()申請buffer之後,由於是第一次運行,所以將會有以下這個操作:

static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
  ...
  /* 這個判斷,在剛進這個task的時候會進入 
   * 因爲gst_pad_has_current_caps()檢查的是輸入參數pad是否有
   * GST_EVENT_CAPS EVENT,在這個時候,dec src pad是沒有的,
   * 所以將會返回空,證明此時caps改變了。此處具體原因我也不是很明白 */
  if (!gst_pad_has_current_caps (GST_VIDEO_DECODER_SRC_PAD (self)) ||
      acq_return == GST_OMX_ACQUIRE_BUFFER_RECONFIGURE) {
    /* Reallocate all buffers */
    if (acq_return == GST_OMX_ACQUIRE_BUFFER_RECONFIGURE
        && gst_omx_port_is_enabled (port)) {
      ...
    }

    if (acq_return == GST_OMX_ACQUIRE_BUFFER_RECONFIGURE) {
      ...
    } else {
      /* Just update caps */

      /* 獲取port參數信息 */
      gst_omx_port_get_port_definition (port, &port_def);

      format =
          gst_omx_video_get_format_from_omx (port_def.format.video.
          eColorFormat);

      /* 設置format等信息保存到output port Status */
      state = gst_video_decoder_set_output_state (GST_VIDEO_DECODER (self),
          format, port_def.format.video.nFrameWidth,
          port_def.format.video.nFrameHeight, self->input_state);

      /* Take framerate and pixel-aspect-ratio from sinkpad caps */
      /* 與下游element協商,並完成pool申請 */
      if (!gst_video_decoder_negotiate (GST_VIDEO_DECODER (self))) {
      }
  }
  ...
}

  接下來,我們將分析一下gst_video_decoder_negotiate(),看看它又是如何協商的,在該函數中,將會通過klass->negotiate()形式調用到gstvideodecoder.c的gst_video_decoder_negotiate_default()。

static gboolean
gst_video_decoder_negotiate_default (GstVideoDecoder * decoder)
{
  ...
  if (state->allocation_caps == NULL)
    state->allocation_caps = gst_caps_ref (state->caps);
  ...
    /* 此處設置之後,下次loop task就不會因爲gst_pad_has_current_caps()而進入協商 */
    ret = gst_pad_set_caps (decoder->srcpad, state->caps);
  ...
  decoder->priv->output_state_changed = FALSE;
  /* Negotiate pool */
  ret = gst_video_decoder_negotiate_pool (decoder, state->allocation_caps);
}

  在gst_video_decoder_negotiate_default()中,我們主要介紹一下gst_video_decoder_negotiate_pool()。
在函數中,將會先與下游element進行GST_QUERY_ALLOCATION查詢,一般的將會調用下游propose_allocation(),通過這樣的方式,查詢下游element buffer pool的配置,由於gstreamer可以兩個element共用buffer pool的,所以會有這樣的一個查詢操作。通過klass->decide_allocation (decoder, query)調用到gst_omx_video_dec_decide_allocation(),最終將會從QUERY中獲取相應的信息並創建pool,激活pool。在這個過程中,將會調用到gstomxbufferpool.c中的類函數,完成buffer pool的參數設置,buffer pool方面的不是很瞭解,這個就暫時不說了。真正的解碼buffer我們之前已經申請了,而gstomxbufferpool.c中的buffer pool,更多的是管理我們之前申請的buffer信息,比如物理地址、分辨率、對齊方式等一些信息,而後將會根據解碼buffer與這些buffer pool的關係,得到相應的數據信息傳遞給下游。一般的,buffer傳遞到下游之後,將buffer unref,buffer內存將會被回收釋放,但是有些element是不希望自身產生的buffer傳遞到下游使用之後被回收釋放,而是返回自身buffer管理隊列中,這個時候就將使用到bufferpool管理。

  可能有些小夥伴看到這裏會混淆了,有點暈,一會gstbuffer,一會gstbufferpool,同時他們兩個都會有創建相應的buffer,是不是重複申請了呢?其實並沒有。比如說這裏,解碼器輸出的RAW數據buffer(gstbuffer),他們申請了物理內存連續的內存塊,然後,我們需要通過bufferpool將這些buffer管理起來,自然的就會需要保存相應的信息,這個時候又申請buffer(常用的內存)來保存這些信息,並通過特定的關係與我們之前申請到的物理內存連續的buffer連續起來,這樣就可以通過這個關係,達到管理pool即爲管理解碼器buffer。pool的buffer爲常規buffer,常規buffer保存的信息可以找到解碼器使用的buffer。

  上面我們在介紹gst_omx_video_dec_handle_frame()的時候,就說到在在使能的時候通過gst_omx_port_allocate_buffers (self->dec_out_port)申請output buffer,但是,實際上比這個還要複雜。因爲有和openMAX IL交互的,底層反饋回來的命令也就會影響到相應的buffer。在實際測試,gst_omx_video_dec_loop()前兩次循環,第一次會因爲gst_pad_has_current_caps (GST_VIDEO_DECODER_SRC_PAD (self))返回NULL,而重新與下游進行協商,這個我們上面簡單的介紹了;第二次,會因爲gst_omx_port_acquire_buffer()返回GST_OMX_ACQUIRE_BUFFER_RECONFIGURE,將會調用gst_omx_video_dec_reconfigure_output_port()重新分配output buffer,同時建立bufferpool管理buffer。
  編碼前數據如何傳遞到openMAX IL層的解碼器,解碼後的數據又是如何傳回gstomx,接下來繼續分析。
  我們繼續看gst_omx_video_dec_handle_frame()操作:

static GstFlowReturn
gst_omx_video_dec_handle_frame (GstVideoDecoder * decoder,
    GstVideoCodecFrame * frame)
{
  self = GST_OMX_VIDEO_DEC (decoder);
  klass = GST_OMX_VIDEO_DEC_GET_CLASS (self);
  
  port = self->dec_in_port;

  ...
  
  /* 獲取上游element傳遞的buffer大小 */
  size = gst_buffer_get_size (frame->input_buffer);
  while (!done) {
    /* 向解碼器申請輸入buf */
    acq_ret = gst_omx_port_acquire_buffer (port, &buf);
    
    ...
    
     /* buf->omx_buf->nAllocLen代表buffer總長度 */
     /* buf->omx_buf->nOffset代表buffer已經使用的長度 */
      buf->omx_buf->nFilledLen =
          MIN (size - offset, buf->omx_buf->nAllocLen - buf->omx_buf->nOffset);
          
      /* 拷貝編碼數據到dec input port buffer */
      gst_buffer_extract (frame->input_buffer, offset,
          buf->omx_buf->pBuffer + buf->omx_buf->nOffset,
          buf->omx_buf->nFilledLen);
          
      offset += buf->omx_buf->nFilledLen;
      if (offset == size)
        done = TRUE;
        
    ...
    
    self->started = TRUE;
    /* 將編碼數據送往解碼器 */
    err = gst_omx_port_release_buffer (port, buf);
    if (err != OMX_ErrorNone)
      goto release_error;

    first_ouput_buffer = FALSE;
  }
  
  ...

}

  下面,我們將會介紹,dec component是如何向openMAX IL申請輸入buffer,這個過程是gstomx.c中的gst_omx_port_acquire_buffer (GstOMXPort * port, GstOMXBuffer ** buf)完成:

  1. 由於openMAX內部消息也都是採用消息的機制,所以,在該函數中,將會先通過gst_omx_component_handle_messages (comp)處理消息事件;
  2. 接着將會檢查port是否是flushing狀態,進行解碼處理,不能是處於該狀態;
  3. 判斷是input port還是output port申請buffer,將會檢查是否需要重新配置port;
  4. 還記得我們上面介紹過的,在申請buffer的時候,buffer是添加到port->pending_buffers鏈表,所以檢查該鏈表是否爲空;
  5. 最終通過g_queue_pop_head (&port->pending_buffers)從port鏈表中拿到buffer並返回。

  在gst_omx_video_dec_handle_frame()中拷貝解碼數據到input port buffer的過程,上面代碼已經解析得很清楚了,接下來將看看,是如何通過gst_omx_port_release_buffer (port, buf)將編碼數據送往解碼器的。在該函數中,一樣的,將會先處理消息隊列數據,接着檢查port狀態,最後,如果是input port,將會通過OMX_EmptyThisBuffer (comp->handle, buf->omx_buf)將input buffer傳遞到解碼器;如果是output port,將通過OMX_FillThisBuffer (comp->handle, buf->omx_buf)將output buffer返還解碼器。宏OMX_EmptyThisBuffer和OMX_FillThisBuffer都是對openMAX IL層函數的封裝,通過他們將會完成以上的操作,這些函數,都是需要芯片廠商根據自己平臺的編解碼接口按照openMAX協議封裝提供的。
  至此,編碼數據已成功送往解碼器,gst_omx_video_dec_handle_frame()解析完畢。那麼,解碼完成後的RAW數據又是什麼時候返回呢,我們繼續看代碼。
  還記得我們在handle_frame()中,發現還沒有開始解碼將會啓動task運行gst_omx_video_dec_loop(),解碼完成之後的數據會不會是在這裏輸出呢,我們來看看它的操作。

static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
  port = self->dec_out_port;
  
  /* 向dec output port申請解碼完成數據 */
  acq_return = gst_omx_port_acquire_buffer (port, &buf);
}

  同樣的,dec output port與input一樣,也是通過gst_omx_port_acquire_buffer()向解碼器申請buffer,只不過這次是向output port申請buffer,一樣是從port->pending_buffers隊列中獲取buffer的,究竟是誰將編碼完成RAW buffer添加到該隊列呢 ,同樣的,編碼前向解碼器申請buffer填充編碼數據,這個buffer又是誰添加到pending_buffers隊列呢?

static OMX_CALLBACKTYPE callbacks =
    { EventHandler, EmptyBufferDone, FillBufferDone };

  還記得上面在介紹從NULL切換到READY時通過gst_omx_component_new()創建component,而在該函數中,通過get_handle()獲取component handle的時候將回調函數組callbacks傳遞到openMAX IL中。就是這個回調函數組,在送往解碼器解碼的數據使用完畢之後,調用callbacks中的EmptyBufferDone()將input port buffer添加到pending_buffers隊列,FillBufferDone()將解碼完成後output port輸出RAW數據buffer添加到pending_buffers隊列。
  無論是EmptyBufferDone()還是FillBufferDone(),都只是將相應的參數信息封裝成GST_OMX_MESSAGE_BUFFER_DONE類型的消息,然後發送到comp->messages,最後當其他函數調用gst_omx_component_handle_messages()處理消息的時候,將會進行相應的處理,最後解析消息,將buffer添加到相應port的pending_buffers隊列。這樣,應該知道input/output port是如果進行buffer隊列管理了吧。
  繼續回到gst_omx_video_dec_loop (),通過gst_omx_port_acquire_buffer()拿到解碼完成的RAW數據之後,接下來應該是送往下游element了吧,但是,視頻數據有dts、pts,dts爲解碼時間戳,pts爲顯示時間戳,所以,接下來,gst_omx_video_dec_loop ()將會根據buffer的時間戳檢查有效性,並丟棄比當前buffer時間戳前的frame,認爲這幀已經丟失了,代碼如下:

static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
  ...
  
  /* 首先,通過gst_video_decoder_get_frames()返回的frames鏈表數據
   * 是在gst_video_decoder_decode_frame()中將新來的frame添加到鏈
   * 表中,而gst_omx_video_find_nearest_frame()則是根據傳進來的buf
    * 時間戳,從參數鏈表中找到時間戳相差最近的frame並返回 */
  frame = gst_omx_video_find_nearest_frame (buf,
      gst_video_decoder_get_frames (GST_VIDEO_DECODER (self)));
    
  /* 根據buf時間戳,將鏈表中比該時間早的frame刪除出鏈表 */
  gst_omx_video_dec_clean_older_frames (self, buf,
      gst_video_decoder_get_frames (GST_VIDEO_DECODER (self)));
      
  ...
}

  完成相應的時間戳等信息填充之後,又將會進入關鍵環節,將數據push到下游,一般的代碼流程如下:

static void
gst_omx_video_dec_loop (GstOMXVideoDec * self)
{
  ...
  if(...) {
    ...
  } else if (buf->omx_buf->nFilledLen > 0 || buf->eglimage) {
    if (self->out_port_pool) {
      gint i, n;
      GstBuffer *outbuf;
      GstBufferPoolAcquireParams params = { 0, };

      n = port->buffers->len;
      for (i = 0; i < n; i++) {
        GstOMXBuffer *tmp = g_ptr_array_index (port->buffers, i);
        /* 在得到output buffer之後,通過這樣的一種形式,得到當前buffer的序列 */
        if (tmp == buf)
          break;
      }
      g_assert (i != n);

      /* 得到序列之後,保存到current_buffer_index變量中,待會
       * 通過這個索引從buffer pool得到buffer相應信息保存到outbuf */
      GST_OMX_BUFFER_POOL (self->out_port_pool)->current_buffer_index = i;
      flow_ret =
          gst_buffer_pool_acquire_buffer (self->out_port_pool,
          &outbuf, &params);
      if (flow_ret != GST_FLOW_OK) {
        ...
        goto invalid_buffer;
      }

      ...

      frame->output_buffer = outbuf;

      /* 成功完成相應的數據獲取了,將會通過gst_video_decoder_finish_frame()
       * 調用gst_video_decoder_clip_and_push_buf()將buffer傳遞到下游*/
      flow_ret =
          gst_video_decoder_finish_frame (GST_VIDEO_DECODER (self), frame);
      frame = NULL;
      buf = NULL;
    } else {
      ...
    }
  } else if (frame != NULL) {
    ...
  }
  ...

  解碼前的buffer申請、釋放我們知道了,那麼解碼後的數據buffer又是在哪裏釋放的呢,這個就是bufferpool的管理功能,當下遊使用完buffer之後,將會unref,此時將會通過buffer的釋放函數,調用到gstomxbufferpool.c中的gst_omx_buffer_pool_release_buffer(),完成相應的buffer釋放,完成一個循環。
  由於我是忽略了與gstomx對接的openMAX IL來解析gstomxvideodec,所以有很多地方忽略了,比如上面我們說到的OMX_CALLBACKTYPE類型的對調函數,這個當中還有一個重要的事件回調函數EventHandler()。openMAX IL需要與gstomx交互時,將會發送相應的事件,而該函數就是就是處理相應的事件,而後將事件信息封裝爲相應的message,在gstomx.c中的gst_omx_component_handle_messages()函數將會處理相應的message,這樣,就完成了gstomx與openMAX IL的相應交互處理。除此之外,還有很多的地方理解不到位,希望看到這裏,有理解到位的小夥伴可以提點一下我,謝謝。

三、總結

  在上面兩節,我們介紹了gstomx是如何基於openMAX完成了解碼操作,下面我們通過簡單的序列圖理解這個關係流程,詳細如下:

圖已經被CSDN吃了,日後再補回來。。。




  以上是個人理解,有理解錯誤的地方,歡迎指出,感謝


  1. openMAX參考-YellowMax-博客 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章