I'm working on a custom video player using Kivy, KivyMD, and FFpyPlayer in Python. The player currently supports basic play, pause, and stop functions, but I'm aiming to add rewind and fast-forward buttons that will skip 5 seconds back or forward, as seen in most media players.
My goal is to implement buttons for:
I'm aware of potential timing and sync issues when adjusting the playhead position manually, so I'd like to understand the best way to ensure smooth functionality.
My current setup is:
I attempted to update the playhead position using the seek()
method in FFpyPlayer, adjusting it by 5 seconds forward or backward. However, this approach has led to issues with smoothness, as the video sometimes lags or stutters when seeking.
Below is a simplified version of my setup. The code includes buttons to rewind and fast-forward by 5 seconds. However, the buttons occasionally don't work as expected and sometimes jump to the beginning or end of the video rather than skipping by the desired amount.
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.video import Video
from kivy.clock import Clock
from kivymd.app import MDApp
from kivymd.uix.button import MDRaisedButton
from kivymd.uix.slider import MDSlider
from kivymd.uix.screen import MDScreen
KV = '''
MDScreen:
BoxLayout:
orientation: 'vertical'
Video:
id: video
source: 'videos/test.mp4'
state: 'stop'
allow_stretch: True
BoxLayout:
size_hint_y: None
height: '50dp'
padding: '10dp'
spacing: '10dp'
MDRaisedButton:
text: 'Play'
on_release: app.play_pause_video()
MDRaisedButton:
text: 'Pause'
on_release: app.play_pause_video()
MDRaisedButton:
text: 'Rewind 5s'
on_release: app.rewind_5s()
MDRaisedButton:
text: 'Forward 5s'
on_release: app.forward_5s()
MDSlider:
id: slider
min: 0
max: 100
value: 0
on_touch_up: app.on_slider_touch_up(*args)
'''
class VideoPlayerApp(MDApp):
def build(self):
self.screen = Builder.load_string(KV)
self.video = self.screen.ids.video
self.slider = self.screen.ids.slider
Clock.schedule_interval(self.update_slider, 1 / 30)
return self.screen
def play_pause_video(self):
if self.video.state == 'play':
self.video.state = 'pause'
else:
self.video.state = 'play'
def rewind_5s(self):
new_position = max(self.video.position - 5, 0)
self.video.seek(new_position)
def forward_5s(self):
new_position = min(self.video.position + 5, self.video.duration)
self.video.seek(new_position)
def update_slider(self, dt):
if self.video.duration > 0:
self.slider.max = self.video.duration
self.slider.value = self.video.position
def on_slider_touch_up(self, instance, touch):
if instance.collide_point(*touch.pos):
self.video.seek(instance.value)
if __name__ == '__main__':
VideoPlayerApp().run()
The issues I've encountered are:
Timing Sync: The video does not always smoothly transition when seeking, resulting in some noticeable stutter or lag.
Button Responsiveness: The responsiveness of the rewind and fast-forward buttons is inconsistent, possibly due to limitations in the seek()
method.
Jumping to Start/End: Sometimes when pressing the Rewind 5s or Forward 5s buttons, the video unexpectedly jumps to the very beginning or end instead of skipping by just 5 seconds.
My questions are:
Is there a more efficient approach in FFpyPlayer or Kivy to enhance the performance of the seek()
function for smooth 5-second skips?
Are there any libraries, methods, or techniques that offer better control over video playback in Kivy, especially for implementing rewind and fast-forward features?
.seek
method accepts values from 0 to 1 so you have to convert the seconds you want to skip to fit 0 to 1
you will then go ahead to use triggered clock to control slider updates to avoid bringing you back to same position whenever your forward or rewind
"""
How to Implement Smooth 5-Second Rewind and Fast-Forward
in Kivy Video Player with FFpyPlayer and KivyMD
"""
from kivy.lang import Builder
from kivy.properties import NumericProperty, ObjectProperty
from kivy.clock import Clock
from kivymd.app import MDApp
KV = '''
MDScreen:
BoxLayout:
orientation: 'vertical'
Video:
id: video
source: 'videos/test.mp4'
state: 'stop'
allow_stretch: True
BoxLayout:
size_hint_y: None
height: '50dp'
padding: '10dp'
spacing: '10dp'
MDRaisedButton:
text: 'Play'
on_release: app.play_pause_video()
MDRaisedButton:
text: 'Pause'
on_release: app.play_pause_video()
MDRaisedButton:
text: 'Rewind 5s'
on_release: app.rewind_5s()
MDRaisedButton:
text: 'Forward 5s'
on_release: app.forward_5s()
MDSlider:
id: slider
min: 0
max: 1
value: 0
on_touch_up: app.on_slider_touch_up(*args)
'''
class VideoPlayerApp(MDApp):
_fwd_position = NumericProperty(0)
_bwd_position = NumericProperty(0)
screen = ObjectProperty()
video = ObjectProperty()
slider = ObjectProperty()
clock = ObjectProperty()
def build(self):
self.screen = Builder.load_string(KV)
self.video = self.screen.ids.video
self.slider = self.screen.ids.slider
self.clock = Clock.create_trigger(self.update_slider, 1 / 30)
return self.screen
def play_pause_video(self):
if self.video.state == 'play':
self.video.state = 'pause'
self.clock.cancel()
else:
self.video.state = 'play'
self.clock()
def rewind_5s(self):
self.clock.cancel()
if self._bwd_position > self.video.position or self._bwd_position == 0:
new_position = max(self.video.position - 5, 0)
self._bwd_position = new_position
else:
self._bwd_position = new_position = self._bwd_position - 5
duration = self.video.duration
self.video.seek(new_position / duration)
Clock.schedule_once(lambda _: self.clock(), 2)
def forward_5s(self):
self.clock.cancel()
if self._fwd_position < self.video.position:
new_position = min(self.video.position + 5, self.video.duration)
self._fwd_position = new_position
else:
self._fwd_position = new_position = self._fwd_position + 5
duration = self.video.duration
self.video.seek(new_position / duration)
Clock.schedule_once(lambda _: self.clock(), 2)
def update_slider(self, _):
duration = self.video.duration
position = self.video.position
self.slider.value = min(1, max(position / duration, 0))
def on_slider_touch_up(self, instance, touch):
self.clock.cancel()
if instance.collide_point(*touch.pos):
self.video.seek(instance.value)
Clock.schedule_once(lambda _: self.clock(), 2)
if __name__ == '__main__':
VideoPlayerApp().run()