gstreamer學習筆記---顯示videosink

videosink

  使用gstreamer播放視頻,在gst-launch-1.0添加參數video-sink=“xxx”,即可指定顯示的element,那麼顯示的element一般操作又是怎樣的呢,它是如何知道它將要顯示的數據格式、分辨率、幀率等參數呢,下面我們一起來學習一下。
  爲了減少平臺硬件相關性,下面將通過fbdevsink瞭解videosink,而videosink又是繼承與basesink的,所以也將會瞭解到basesink。先來看看fbdevsink的繼承關係:

GObject
 +----GInitiallyUnowned
       +----GstObject
             +----GstElement
                   +----GstBaseSink
                         +----GstVideoSink
                               +----GstFBDEVSink

  創建element的過程沒什麼好介紹的,就是加載相應的動態庫,加載plugin,創建element,下面,我們將介紹videosink在各個狀態切換的操作。

element link

  之前的文章有介紹過,element的link操作,一般的是從src到sink,而videosink是sink element,將會是最後一個element。在link的時候,都將會通過gst_pad_query_caps (pad, NULL)查詢pad支持的caps,由於videosink都沒有重載查詢函數,所以將會調用到basesink的這裏將會調用到gst_base_sink_sink_query(),最終調用到gst_base_sink_default_query (GstBaseSink * basesink, GstQuery * query)。由於是查詢caps,將會調用gst_base_sink_query_caps(),在該函數中最終通過bclass->get_caps (bsink, filter)調用到fbdevsink的gst_fbdevsink_getcaps()獲取template caps。當獲取到caps,與上游element caps有交集,即可link成功。

NULL->READY

  之前就介紹過,element從NULL切換到READY,一般都是打開設備,初始化相應的硬件,同樣的,在fbdevsink這裏也是,fbdevsink、videosink在該狀態都沒有什麼實際性的操作,將會在basesink的gst_base_sink_change_state()調用bclass->start (basesink)。這個調用將會調用到fbdevsink的gst_fbdevsink_start (GstBaseSink * bsink)。剛纔已經說了,從NULL切換到READY,將會設置初始化設備,而在fbdevsink這裏,將會打開/dev/fb0節點,通過ioctl獲取機器framebuffer的信息,並將framebuffer映射保存到fbdevsink->framebuffer,start完成操作。

READY->PAUSED

  在該狀態下,將會進行設備初始化、相應資源申請等操作,而顯示的videosink,又將會進行什麼操作呢,往下看。
  由於我們是減少平臺相關行而採用fbdevsink介紹videosink,在fbdevsink、videosink在這個狀態切換都沒有相應的操作,只是在basesink中進行一些element的成員變量初始化而已。需要注意的,basesink是異步的將狀態切換到PAUSED,會發送async_start消息,只是bin接收到該消息在這裏沒有進一步操作而已。在該切換狀態下,將會進行相應的pad激活操作,這個是在gstelement.c的gst_element_change_state_func()實現的。
  在激活pad的過程中將會通過GST_PAD_ACTIVATEFUNC (pad)調用到gstbasesink.c的gst_base_sink_pad_activate()。由於basesink默認是不能激活爲pull模式,所以在繼承類沒有修改的情況下,將會通過gst_pad_activate_mode (pad, GST_PAD_MODE_PUSH, TRUE)激活爲push模式。

static gboolean
gst_base_sink_pad_activate (GstPad * pad, GstObject * parent)
{
  gboolean result = FALSE;
  GstBaseSink *basesink;
  GstQuery *query;
  gboolean pull_mode;

  basesink = GST_BASE_SINK (parent);

  gst_base_sink_set_flushing (basesink, pad, FALSE);

  /* we need to have the pull mode enabled */
  if (!basesink->can_activate_pull) {
    GST_DEBUG_OBJECT (basesink, "pull mode disabled");
    goto fallback;
  }
  
  ...

  /* push mode fallback */
fallback:
  GST_DEBUG_OBJECT (basesink, "Falling back to push mode");
  if ((result = gst_pad_activate_mode (pad, GST_PAD_MODE_PUSH, TRUE))) {
    GST_DEBUG_OBJECT (basesink, "Success activating push mode");
  }

  ...

  return result;
}

  在gst_pad_activate_mode()中,最後又將會通過GST_PAD_ACTIVATEMODEFUNC (pad) (pad, parent, mode, active)調用到gstbasesink.c的gst_base_sink_pad_activate_mode(),模式爲push,所以又將調用gst_base_sink_pad_activate_push(),而最終該函數也只是簡單的填充basesink->pad_mode而已,狀態切換完成。

PAUSED操作

  前幾篇文章也都介紹過,雖然已經link、caps有交集,但是,使用那個caps還是沒有協商清楚的,所以接下來將會有多次的caps query,直至caps協商完成,在這個過程是多次調用到bclass->get_caps(),具體的操作可以看videosink繼承類等的實現,在link的時候,也有進行這個查詢,只是由於當時硬件設備還沒有初始化,不知道設備具體支持的caps,所以,雖然多次查詢,調用的是同一個函數,但是在該函數中,將會在不同的狀態返回不同的信息,具體的得根據element的實現得知。
  caps確定下來之後,將會接收到stream-start事件,最終在gst_base_sink_default_event()處理該事件。在該函數中,並沒有對該事件進行太多的處理,相應的信息也沒有保存。但是,videosink,是sink,gstreamer中,部分事件是從頭到尾貫穿pipeline的,而stream-start就是其中一個,所以,videosink接收到該事件,最終將會把該事件轉換爲message,發送到消息總線,這樣消息總線的監聽者將會接收並處理該消息。
  雖然caps已經確定了,但是最後還有accept-caps query,這裏上游將會發送最後確定的caps到videosink,videosink檢查自身是否支持該caps,支持將會在query中設置result爲1。
  確定下來之後,上游又將會發送caps事件到videosink。在gst_base_sink_default_event()中處理該事件,將會先從EVENT中解析得到caps,然後檢查當前pad是否已經設置了caps,對比是否有改變,改變將會調用bclass->set_caps (basesink, caps)設置caps。這裏將會調用到fbdevsink的gst_fbdevsink_setcaps()。在video顯示中,大概應該會了解顯示主要關注的是什麼,主要也是這麼幾個,數據格式、分辨率、幀率、顯示位置與大小等信息。明白這些,大概就知道videosink的set_caps將會設置什麼參數。在fbdevsink的set_caps()中,將會從caps中獲取相應的格式、分辨率、幀率這些信息,然後設置相應的成員變量。我想videosink型的element應該都是類似的吧。這樣,caps EVENT處理完畢。
  在設置caps之後,部分element需要設置buffer緩衝區的,當然有些element也會不設置,這個得具體element分析。但是,上游會發送GST_QUERY_ALLOCATION查詢緩衝區的屬性。在basesink這裏將會通過gst_base_sink_default_query()調用bclass->propose_allocation (basesink, query)查詢element的buffer緩衝區屬性,協商buffer pool等。這個propose_allocation()函數主要是從query中獲取caps信息,同時獲取是否需要創建pool,設置buffer pool參數,創建pool等。而fbdevsink不需要進行該操作,上游將自己申請buffer pool。
  最後,應該就是GST_EVENT_SEGMENT事件了。音視頻數據在gstreamer中都是通過segment描述其時間範圍,所以,在都準備好的時候,播放之前,將會發送segment事件,該事件中帶有描述接下來播放數據的時間範圍,element接收到數據的時候,將會檢查該幀數據的時間戳是否在segment內,一般情況下是需要在這個範圍內纔會正常使用。在gst_base_sink_default_event()處理,將會從EVENT提取segment信息保存在basesink->segment,將會在接收到數據的時候使用待segment信息。
  有時候我們播放歌曲獲取視頻,都會有一些附帶信息,比如歌手是誰、歌名叫什麼、視頻格式又是什麼、波特率、language-code等信息,這些信息,都會多次的通過GST_EVENT_TAG通過pipeline貫穿管道的element。但是basesink只是簡單的將tag EVENT轉換爲message併發送到消息總線。
  在切換到PLAYING狀態之前,也會有一幀數據先貫穿pipeline,所以下面我們來分析一下,當buffer數據達到的時候,又將會進行什麼操作。
  數據預滾達到basesink類element的時候,最先將會是調用gst_base_sink_chain(),最終將調用gst_base_sink_chain_unlocked (basesink, pad, buf, FALSE)。視頻播放,播放器會按照每幀視頻的時間戳排序,播放各幀視頻,所以,gst_base_sink_chain_unlocked()的主要操作概括如下:

  1. 先獲取buffer中的時間戳,通過bclass->gst_times()調用basesink子類的gst_times()獲取buffer的時間戳,如果子類沒有實現,將會通過gst_base_sink_default_get_times()獲取,幀的pts賦值爲start,pts加上duration即爲end;
  2. 之前我們不是保存segment信息在basesink->segment中麼,接下來,將會通過gst_segment_clip()檢查buffer的pts是否在該segment內,不在將會捨棄該幀;
  3. 接下來將會檢查basesink子類是否實現了bclass->prepare或者bclass->prepare_list,basesink是沒有實現這兩個函數的,他們將會根據buffer做一些prepare操作,每個element都是不一樣的,這裏不詳細介紹;
  4. 在渲染之前,也將會通過gst_base_sink_do_sync()進行同步,確保該幀數據是可以在當前播放;
  5. 在還沒有切換到PLAYING狀態的時候,時鐘是沒有運行的,但gstreamer是有一個預滾的過程(preroll:prepare roll)。在gst_base_sink_do_sync()中將會通過gst_base_sink_do_preroll()函數進行預滾操作;
  6. 在預滾函數gst_base_sink_do_preroll()中,將會由於當前狀態need_preroll是置位的,將會進入預滾操作,預滾是通過bclass->preroll (sink, buf)進行渲染;
  7. 渲染之後,將會通過gst_base_sink_commit_state()確認狀態,當前是PAUSED狀態,將會發送GST_STATE_PAUSED消息到總線告知當前basesink爲PAUSED狀態,同時會發送async-done消息,至此。pipeline整體進入PAUSED狀態;

PAUSED->PLAYING

  在pipeline接收到PAUSED消息之後,又將會設置element進入PLAYING狀態,那麼。videosink在該狀態又將會進行什麼操作呢,接着看。
  切換江湖調用到gst_base_sink_change_state(),由於我們之前使能了預滾,所以將會把預滾標誌位置爲FALSE,同時發送preroll信號,這樣,上面的預滾顯示將會接收到該信號,接着運行gst_base_sink_chain_unlocked()。剩餘的操作就是設置clock以及時鐘運行了,這些就不介紹了。下面我們看看,在PLAYING狀態下,數據的處理過程。
  回到數據處理函數gst_base_sink_chain_unlocked(),接着上面介紹的跟蹤下去:

  1. 先獲取buffer中的時間戳,通過bclass->gst_times()調用basesink子類的gst_times()獲取buffer的時間戳,如果子類沒有實現,將會通過gst_base_sink_default_get_times()獲取,幀的pts賦值爲start,pts加上duration即爲end;
  2. 之前我們不是保存segment信息在basesink->segment中麼,接下來,將會通過gst_segment_clip()檢查buffer的pts是否在該segment內,不在將會捨棄該幀;
  3. 接下來將會檢查basesink子類是否實現了bclass->prepare或者bclass->prepare_list,basesink是沒有實現這兩個函數的,他們將會根據buffer做一些prepare操作,每個element都是不一樣的,這裏不詳細介紹;
  4. 在渲染之前,也將會通過gst_base_sink_do_sync()進行同步,確保該幀數據是可以在當前播放;
  5. 同步完之後,將會通過bclass->render()渲染顯示;
  6. 通過gst_base_sink_perform_qos (basesink, late)與上游element QOS EVENT交互,決定視頻質量等。

  下面我們先來看看,gst_base_sink_do_sync()是如何同步的?代碼註釋已經講清楚了。

static GstFlowReturn
gst_base_sink_do_sync (GstBaseSink * basesink,
    GstMiniObject * obj, gboolean * late, gboolean * step_end)
{
  GstClockTime rstart, rstop, rnext, sstart, sstop, stime;
  
  /* 通過該函數獲取buf的pts、duration等信息,知道該幀該什麼時候顯示,顯示多長時間 */
  syncable = gst_base_sink_get_sync_times (basesink, obj,
      &sstart, &sstop, &rstart, &rstop, &rnext, &do_sync, &stepped, current,
      step_end);

  /* 得到開始顯示時間之後,還需要考慮basesink->priv->latency
   * 以及basesink->priv->render_delay時間 */
  stime = gst_base_sink_adjust_time (basesink, rstart);
  
  /* 完了之後,stime就是顯示時間了,然後通過該函數,
   * 確定當前時間就是顯示時間,不是將會等待 */
  status = gst_base_sink_wait_clock (basesink, stime, &jitter);
  
  /* 保存該幀的時間差,在qos使用到 */
  priv->current_jitter = jitter;
}

  同步完成之後,將會通過bclass->render()調用具體的子類渲染函數。videosink都將會調用到gstvideosink.c的gst_video_sink_show_frame(),而在該函數將會調用videosink子類的klass->show_frame(),在fbdevsink將會調用到gst_fbdevsink_show_frame()。fbdevsink的show_frame()就是從buffer拷貝數據到之前映射的framebuffer,即完成渲染操作。
  gstreamer是通過qos事件說明當前播放速度,所以在播放完一幀之後,都會反饋當先的播放情況到上游element,在videosink這裏,是通過gstbasesink.c的gst_base_sink_perform_qos()完成這個操作的。在該函數中,將會根據剛纔顯示的這幀數據時間戳,得到本身時間戳是多少,真正顯示時間又是多少,在該element處理,又花了多少時間,同時還會將之前各幀的處理時間去平均值保存在priv->avg_pt,最終根據處理時間以及到達該element的時間延遲計算一個比例priv->avg_rate,最終將這個比例以及顯示時間、時間差距等信息通過QOS EVENT發送到上游。上游element接收到該事件,將會保存相應的信息,這些信息將會在下一次它自身處理數據的時候,影響相應的時間戳等,這樣達到一個反饋。
  至此,videosink介紹完成。

總結

  videosink和之前介紹的element主體流程也是非常類似的,只是由於videosink是pipeline的最後一個,很多時候需要轉發message到消息總線上。同時,videosink主要是用於顯示視頻的,所以還會有一個時間戳的問題,需要等到相應的時間再進行render。




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

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