pythonkivykivymdffpyplayer

How to Implement Smooth 5-Second Rewind and Fast-Forward in Kivy Video Player with FFpyPlayer and KivyMD


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:

  1. Timing Sync: The video does not always smoothly transition when seeking, resulting in some noticeable stutter or lag.

  2. Button Responsiveness: The responsiveness of the rewind and fast-forward buttons is inconsistent, possibly due to limitations in the seek() method.

  3. 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:

  1. Is there a more efficient approach in FFpyPlayer or Kivy to enhance the performance of the seek() function for smooth 5-second skips?

  2. Are there any libraries, methods, or techniques that offer better control over video playback in Kivy, especially for implementing rewind and fast-forward features?


Solution

  • .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

    Github Source Code

    """
    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()