Python Tkinter account for lost time

101 Views Asked by At

I've read similar questions that have been answered:

Here's my window:

play progress.gif

Problem is song ends when counter still has 20% to go. I know the reason is primarily due to system call to check if process is still running pgrep ffplay 10 times every second. Secondary reason is simply Python and Tkinter overhead.

To "band-aid fix" the problem I used 1.24 deciseconds instead of 1 every decisecond as my code illustrates now:

def play_to_end(self):
    ''' 
    Play single song, checking status every decisecond
    Called from:
        self.play_forever() to start a new song
        self.pp_toggle() to restart song after pausing
    '''
    while True:
        if not self.top2_is_active: return          # Play window closed?
        root.update()                               # Process other events
        if self.pp_state is "Paused": 
            time.sleep(.1)                          # Wait until playing
            continue

        PID = os.popen("pgrep ffplay").read()       # Get PID for ffplay
        if len(PID) < 2:                            # Has song ended?
            return                                  # Song has ended

        #self.current_song_time += .1                # Add decisecond
        self.current_song_time += .124              # Add 1.24 deciseconds
                                                    #  compensatation .24
        self.current_progress.set(str('%.1f' % self.current_song_time) + \
                    " seconds of: " + str(self.DurationSecs))
        root.update()                               # Process other events
        root.after(100)                             # Sleep 1 decisecond

The problem with this band-aid fix is it is highly machine dependent. My machine is a Skylake for example. Also it is highly dependent on what other processes are running at the same time. When testing my machine load was relatively light:

play progress overhead.gif

How can I programmatically account for lost time in order to increment elapsed time accurately?

Perhaps there is a better way of simply querying ffplay to find out song progress?

As an aside (I know it's frowned upon to ask two questions at once) why can't I simply check if PID is null? I have tried .rstrip() and .strip() after .read() to no avail with checking PID equal to "" or None. If ffplay every has a process ID under 10 program will misbehave.

1

There are 1 best solutions below

2
On

You can use subprocess.Popen() to execute ffplay and redirect stderr to PIPE, then you can read the progress from stderr and update the progress label.

Below is an example:

import tkinter as tk
import subprocess as subp
import threading

class MediaPlayer(tk.Tk):
    def __init__(self):
        super().__init__()
        self.init_ui()
        self.proc = None
        self.protocol('WM_DELETE_WINDOW', self.quit)

    def init_ui(self):
        self.current_progress = tk.StringVar()
        self.progress = tk.Label(self, textvariable=self.current_progress)
        self.progress.grid(row=0, column=0, columnspan=2)

        btn_close = tk.Button(self, text='Stop', width=20, command=self.stop_playing)
        btn_close.grid(row=1, column=0, sticky='ew')

        btn_play = tk.Button(self, text='Play', width=20, command=self.play_song)
        btn_play.grid(row=1, column=1, sticky='ew')

    def play_to_end(self):
        self.proc = subp.Popen(
            ['ffplay', '-nodisp', '-hide_banner', '-autoexit', self.current_song_path],
            stderr=subp.PIPE, bufsize=1, text=1
        )
        duration = ''
        while self.proc.poll() is None:
            msg = self.proc.stderr.readline().strip()
            if msg:
                if msg.startswith('Duration'):
                    duration = msg.split(',')[0].split(': ')[1]
                else:
                    msg = msg.split()[0]
                    if '.' in msg:
                        elapsed = float(msg)
                        mins, secs = divmod(elapsed, 60)
                        hrs, mins = divmod(mins, 60)
                        self.current_progress.set('Play Progress: {:02d}:{:02d}:{:04.1f} / {}'.format(int(hrs), int(mins), secs, duration))
        print('done')
        self.proc = None

    def play_song(self):
        self.current_song_path = '/path/to/song.mp3'
        if self.proc is None:
            threading.Thread(target=self.play_to_end, daemon=True).start()

    def stop_playing(self):
        if self.proc:
            self.proc.terminate()

    def quit(self):
        self.stop_playing()
        self.destroy()

app = MediaPlayer()
app.mainloop()