【没钱买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 处理细节)


<<:  Day29物件导向

>>:  追求JS小姊姊系列 Day26 -- 不是被已读,而是JS回覆你却没看到:`console`

【从零开始的 C 语言笔记】第二十三篇-Switch条件式

不怎麽重要的前言 上一篇介绍了两个小题目,稍微带过解题的思路,以及多重回圈(巢状回圈)的概念。 现在...

Day04 - Python基本语法 Part 1

今天开始将进行Python基本语法练习,因大部分语法跟很多程序语言相似,故这个部分将主要以笔记方式注...

D15/ 为什麽 remember 是 composable function? - @Composable 是什麽

今天大概会聊到的范围 @Composable compose compiler & run...

D22 - 用 Swift 和公开资讯,打造投资理财的 Apps { 台股成交量实作.2 }

上一篇在 TwMarketTradingInfoManager 完成了拿取大盘成交量的 API,接下...

Swift纯Code之旅 Day9. 「TableView(1) - TableView Cell内容制作」

前言 昨天已经将TableView给建立完毕了,今天来跟大家聊聊TableViewCell的建立方法...