0

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.

WinEunuuchs2Unix
  • 1,801
  • 1
  • 17
  • 34
  • Have you tried `.after()` in tkinter? – jizhihaoSAMA Jul 27 '20 at 03:50
  • @jizhihaoSAMA Yes the original design was using `.after()` referencing all kinds of frames and labels, etc. I gave up and went with `root.after` which always seemed to work and is in the last line of code. That doesn't help the time compensation though... – WinEunuuchs2Unix Jul 27 '20 at 04:38
  • How do you start the `ffplay`? Using `subprocess.Popen()`? – acw1668 Jul 27 '20 at 06:38
  • @acw1668 The first line is `os.popen('ffplay -autoexit ' + '"' + self.current_song_path + '"'` continued on the second line: ``+ ' -nodisp 2>' + TMP_CURR_SONG + ' &')`` – WinEunuuchs2Unix Jul 27 '20 at 10:53
  • Try using `subprocess.Popen()` instead of `os.popen()` and redirect the `stderr` to PIPE, then you can read the `stderr` to update the progress. – acw1668 Jul 27 '20 at 11:11
  • @acw1668 That is a great solution for this particular use case. In general though someone without `ffplay` may want to know progress to total time. Accounting for system overhead would be needed in those cases. – WinEunuuchs2Unix Jul 27 '20 at 11:31

1 Answers1

1

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()
acw1668
  • 40,144
  • 5
  • 22
  • 34