iT邦幫忙

2021 iThome 鐵人賽

DAY 26
0
Software Development

【今年還是不夠錢買psQQ,不如我們用PyQt自己寫一個】系列 第 26

【沒錢買ps,PyQt自己寫】Day 26 - project / 替我們影片播放器增加一個顯示進度的滑條 video player add slider (與昨日 bottleneck 處理細節)

看完這篇文章你會得到的成果圖

  • 多了一條滑條,我們可以直接控制,另外我們也可以直接透過滑條來操控進度
  • 另外這次有解決上一篇 lag 的問題,會說明原因以及解法。

此篇文章的範例程式碼 github

https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day26_video_player_add_slider_project

之前內容的重點複習 (前情提要)

我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。

https://www.wongwonggoods.com/python/pyqt5-5/

設計我們的 UI

主要就是新增滑條的部分,新元素的名稱:

  • self.slider_videoframe:滑條

轉換 day26.ui -> UI.py

pyuic5 -x day26.ui -o UI.py

執行看看 UI.py 畫面是否如同我們想像

一樣,這程式只有介面 (視覺上的呈現),沒有任何互動功能

  • 看看我們製作出來的介面
python UI.py

設計我們的 controller

使用 QSlider

我們已經在 【PyQt5】Day 14 – 使用 QSlider 製作可拖曳的滑條 有詳細的教學 QSlider 該如何使用了,
這邊我們就直接使用吧!

def init_video_info(self):
    self.ui.slider_videoframe.setRange(0, self.video_total_frame_count-1)
    self.ui.slider_videoframe.valueChanged.connect(self.getslidervalue)

def __get_frame_from_frame_no(self, frame_no):
    self.setslidervalue(frame_no)

def getslidervalue(self):
    self.current_frame_no = self.ui.slider_videoframe.value()

def setslidervalue(self, value):
    self.ui.slider_videoframe.setValue(self.current_frame_no)

1. 取得滑條值的部分

我們在 init_video_info() 新增了關於滑條初始化的功能,
我們設定好這個滑條的 range 為 (0, 全部 frame 數 -1),
並且將這個滑條連結於 getslidervalue() 的功能上,
只要我們移動滑條,就會啟動這個函數。

2.變更滑條值的部分

我們製作了一個函數 setslidervalue(),當我們更改 frame 的時候,
我們可以直接也更改滑條的值。

而呼叫這個 setslidervalue() 的 function 位於取得 frame from frame number 的時候,
也同步呼叫這個函數,就可以完成「隨著 frame 變化更改滑條的值」。

優化我們的播放器效能 (解決昨天的 lag 問題)

昨天我們提到我們程式執行的時候會有 lag 的問題,
那時我是直接給個優化的方向,是我們可以考慮提程式的 decode 加入「multiprocessing」的平行運算功能。
不過今天處理的過程中,我稍微替我的程式加了幾個計時器,
後來意外發現卡住的 function 其實只有一個,這樣就好處理了!

處理我們先前程式的 bottleneck

以這支程式來說,很直覺的我會認為會慢都是牽扯到 decode 那一段的速度,
因為處理圖片基本上就是最花時間的地方...

因此我加了一些 timer 在昨天的 code

def __get_frame_from_frame_no(self, frame_no):
    time_start = time.time()
    self.vc.set(1, frame_no)
    ret, frame = self.vc.read()
    time_end = time.time()
    print(time_end - time_start)

我們來計時一下,這段處理到底花了多少時間。
結果發現了一個很有趣的現象:

  • stop 時,平均一個 frame 只需要處理 0.01~0.02 秒左右
  • 一但進入 start 或 pause 的狀態,平均一個 frame 需要處理 0.06~0.07 秒左右

這就很奇怪了!!! 照理來說處理一個圖片,應該也不會到有那麼大的誤差。
而且是平均時間,還不是幾張圖片或許資訊比較豐富所以處理比較久。

這表示我們設計的機制一定有什麼可優化的問題。

再繼續往下查,抓出產生問題的關鍵 function

最後我們發現一件有趣的事情:

原本我以為是處理處片的時間很久,結果只花了 0.001 秒

ret, frame = self.vc.read()

然而卻是以下這行,設定人在哪個 frame 的函數,可能會造成約 0.05 秒左右的延遲。

self.vc.set(1, frame_no)

但是,我之前做過的專案經驗告訴我,正常來說的解碼不會那麼久,
所以一定是我不夠正確的使用這一行。

所以我決定修改機制。

重新設計使用 vc.set() 的機制,減少使用

照官方文件的定義,即使沒有 vc.set(),只需要一直 vc.read() 也能夠一直往下取 frame,

我猜可能這就是原因了,因為 OpenCV (或說是他使用的 ffmpeg library) 在 decode 的時候,
針對連續的 frame 有做優化的演化法,
所以如果我每次都重新設定第幾個 frame,會導致這個優化演算法失效
可以想像是,因為我們的影片都是連續的,
所以搞不好可以透過計算向量差的方式,更快的算出下一張圖片。(而這機制被我的設計弄到失效)

於是我們更改一下原本的邏輯,「只要必須要設定 frame 時,才使用 vc.set()」

把顯示 frame 的函數拆成兩個 function

我們把 self.vc.set(1, frame_no) 這個會造成 bottleneck 的 funciton 獨立出來。

def set_current_frame_no(self, frame_no):
    self.vc.set(1, frame_no) # bottleneck

def __get_next_frame(self):
    ret, frame = self.vc.read()
    self.ui.label_framecnt.setText(f"frame number: {self.current_frame_no}/{self.video_total_frame_count}")
    self.setslidervalue(self.current_frame_no)
    return frame

配合上述機制的修改對應設計

def timer_timeout_job(self):
    if (self.videoplayer_state == "play"):
        if self.current_frame_no >= self.video_total_frame_count-1:
            #self.videoplayer_state = "pause"
            self.current_frame_no = 0 # auto replay
            self.set_current_frame_no(self.current_frame_no)
        else:
            self.current_frame_no += 1

    if (self.videoplayer_state == "stop"):
        self.current_frame_no = 0
        self.set_current_frame_no(self.current_frame_no)

    if (self.videoplayer_state == "pause"):
        self.current_frame_no = self.current_frame_no
        self.set_current_frame_no(self.current_frame_no)

    frame = self.__get_next_frame() 
    self.__update_label_frame(frame)

原本會更新畫面的函數位置不變,而我們在 pause、stop、與影片播放完畢後,
都啟用 set_current_frame_no() 這個函數,才會去啟動 vc.set() 修改 frame index。

def getslidervalue(self):
    self.current_frame_no = self.ui.slider_videoframe.value()
    self.set_current_frame_no(self.current_frame_no)

另外一個也會影響到 frame index 的就是滑條,
我們也是在滑條「被移動」的時候,才會去呼叫 set_current_frame_no() 啟動 vc.set()

測試結果

我的影片播放器終於順暢了!!! 耶!!!

此外昨天保留的計算 fps 機制就可以拿回來用了。
如果不想要讓影片已超快的 1ms 更新,可以改回上面 timer 的做法。

self.timer.start(1000//self.video_fps) # start Timer, here we set '1000ms//Nfps' while timeout one time
self.timer.start(1) # but if CPU can not decode as fast as fps, we set 1 (need decode time)

Reference


★ 本文也同步發於我的個人網站(會有內容目錄與顯示各個小節,閱讀起來更流暢):【PyQt5】Day 26 project / 替我們影片播放器增加一個顯示進度的滑條 video player add slider (與昨日 bottleneck 處理細節)


上一篇
【沒錢買ps,PyQt自己寫】Day 25 - project / 自己做一個影片播放器 DIY video player (結合 PyQt + OpenCV)
下一篇
【沒錢買ps,PyQt自己寫】Day 27 - project / 製作影片 ROI 標註工具 (PyQt 結合 OpenCV 在圖上畫點畫線)
系列文
【今年還是不夠錢買psQQ,不如我們用PyQt自己寫一個】30

尚未有邦友留言

立即登入留言