#!/usr/bin/env python3 # # omxplayer-sync # # Copyright 2016, Simon Josi # Simon Josi me(at)yokto(dot)net # # This program is free software; you can redistribute # it and/or modify it under the terms of the GNU # General Public License version 3 as published by # the Free Software Foundation. # import re import os import sys import math import socket import signal import dbus import getpass import itertools import collections from time import sleep, time from optparse import OptionParser, BadOptionError, AmbiguousOptionError from subprocess import Popen try: from subprocess import DEVNULL except ImportError: import os DEVNULL = open(os.devnull, 'wb') SYNC_TOLERANCE = .05 SYNC_GRACE_TIME = 5 SYNC_JUMP_AHEAD = 3 OMXPLAYER = 'omxplayer' OMXPLAYER_DBUS_ADDR='/tmp/omxplayerdbus.%s' % getpass.getuser() PORT = 1666 # # Unknown option pass-through OptionParser # class PassThroughOptionParser(OptionParser): """ An unknown option pass-through implementation of OptionParser. When unknown arguments are encountered, bundle with largs and try again, until rargs is depleted. sys.exit(status) will still be called if a known argument is passed incorrectly (e.g. missing arguments or bad argument types, etc.) """ def _process_args(self, largs, rargs, values): while rargs: try: OptionParser._process_args(self,largs,rargs,values) except (BadOptionError,AmbiguousOptionError) as e: largs.append(e.opt_str) # # D-Bus player interface # class PlayerInterface(): def _get_dbus_interface(self): try: bus = dbus.bus.BusConnection( open(OMXPLAYER_DBUS_ADDR).readlines()[0].rstrip()) proxy = bus.get_object( 'org.mpris.MediaPlayer2.omxplayer', '/org/mpris/MediaPlayer2', introspect=False) self.methods = dbus.Interface( proxy, 'org.mpris.MediaPlayer2.Player') self.properties = dbus.Interface( proxy, 'org.freedesktop.DBus.Properties') return True except Exception as e: print("WARNING: dbus connection could not be established") print(e) sleep(5) return False def initialize(self): sleep(10) # wait for omxplayer to appear on dbus return self._get_dbus_interface() def playPause(self): try: self.methods.Action(16) return True except: print(e) return False def setPosition(self, seconds): try: self.methods.SetPosition( dbus.ObjectPath('/not/used'), dbus.Int64(seconds * 1000000)) except Exception as e: print(e) return False return True def Position(self): try: return self.properties.Get( 'org.mpris.MediaPlayer2.Player', 'Position') except Exception as e: return False # # OMXPlayer-Sync main class # class OMXPlayerSync(): def __init__(self): self.sock = self.init_socket() self.controller = PlayerInterface() self.options = None self.omxplayer_options = [] self.playlist = [] self.playlist_index = 0 self.filename = '' self.position_local = 0.0 self.position_local_oldage = 0.0 self.position_local_oldage_count = 0 self.position_master = 0.0 self.filename_master = '' self.process = None signal.signal(signal.SIGINT, self.kill_omxplayer_and_exit) def run(self): p = PassThroughOptionParser() p.add_option('--master', '-m', action='store_true') p.add_option('--slave', '-l', action='store_true') p.add_option('--destination', '-x', default='255.255.255.255') p.add_option('--loop', '-u', action='store_true') p.add_option('--verbose', '-v', action='store_true') p.add_option('--adev', '-o', default='both') self.options, arguments = p.parse_args() for argument in arguments: if argument.startswith('-'): self.omxplayer_options.append(argument) else: self.playlist.append(argument) if not len(self.playlist): sys.exit(0) if self.options.loop and len(self.playlist) == 1: self.omxplayer_options.append("--loop") if not filter(lambda x: os.path.isfile(x), self.playlist): print("ERROR: none of the supplied filenames are found") sys.exit(1) self.omxplayer_options.append("-o %s" % self.options.adev) self.omxplayer_options.append('--no-keys') if not self.options.verbose: self.omxplayer_options.append('--no-osd') if self.options.master: self.socket_enable_broadcast() self.socket_connect(self.options.destination) if self.options.slave: self.read_position_master() self.set_playlist_index() while True: self.play_file(self.playlist[self.playlist_index]) if not self.options.loop and self.playlist_index == 0: break def play_file(self, filename): if not os.path.isfile(filename): if self.options.verbose: print("WARNING: %s file not found" % filename) self.increment_playlist_index() return self.filename = filename self.position_local = 0.0 self.position_local_oldage = 0.0 self.position_local_oldage_count = 0 if self.options.verbose: last_frame_local, current_frame_local = 0, 0 if self.options.slave: last_frame_master, current_frame_master = 0, 0 if self.options.master: self.send_position_local() self.process = Popen([OMXPLAYER] \ + list(itertools.chain(*map(lambda x: x.split(' '), self.omxplayer_options))) \ + [self.filename], preexec_fn=os.setsid, stdout=DEVNULL, stderr=DEVNULL, stdin=DEVNULL) self.controller.initialize() if not self.read_position_local(): print("WARNING: omxplayer did not start. Try to test with `omxplayer -s OPTIONS`") self.kill_omxplayer_and_exit() if self.options.slave: wait_for_sync = False wait_after_sync = False deviations = collections.deque(maxlen=10) while True: if self.options.slave: self.read_position_master() if wait_for_sync: sync_timer = time() if not self.read_position_local(): self.increment_playlist_index() break if self.hangup_detected(): break if self.options.verbose: current_frame_local = math.modf(self.position_local)[0]*100/4 if self.options.slave: current_frame_master = math.modf(self.position_master)[0]*100/4 if self.options.master: self.send_position_local() if self.options.verbose: sys.stdout.write("local: %.2f %.0f\n" % (self.position_local, current_frame_local)) if self.options.slave: if self.filename != self.filename_master: self.set_playlist_index() break deviation = self.position_master - self.position_local deviations.append(deviation) median_deviation = self.median(list(deviations)) if self.options.verbose: sys.stdout.write("local: %.2f %.0f master: %.2f %.0f deviation: %.2f median_deviation: %.2f filename: %s\n" % ( self.position_local, current_frame_local, self.position_master, current_frame_master, deviation, median_deviation, self.filename)) if wait_for_sync: while True: if abs(deviation) - (time() - sync_timer) < 0: if self.options.verbose: print("we are sync, play...") if not self.controller.playPause(): break wait_for_sync = False wait_after_sync = time() break continue if wait_after_sync: if (time() - wait_after_sync) > SYNC_GRACE_TIME: wait_after_sync = False continue if abs(median_deviation) > SYNC_TOLERANCE \ and self.position_local > SYNC_GRACE_TIME \ and self.position_master > SYNC_GRACE_TIME: if self.options.verbose: print("jump to %.2f" % (self.position_master + SYNC_JUMP_AHEAD)) print("enter pause...") if not self.controller.playPause(): break if not self.controller.setPosition(self.position_master + SYNC_JUMP_AHEAD): break wait_for_sync = True wait_after_sync = time() if self.options.master: sleep(1) self.kill_omxplayer() def read_position_local(self): position_local = self.controller.Position() if position_local: self.position_local = float(position_local)/1000000 else: return False return True def set_playlist_index(self): if self.filename_master == '': print("WARNING: unable to get current filename from master") sleep(5) return try: self.playlist_index = self.playlist.index(self.filename_master) except: print("WARNING: current master file '%s' is unavailable" % self.filename_master) def increment_playlist_index(self): if len(self.playlist) == (self.playlist_index + 1): self.playlist_index = 0 else: self.playlist_index += 1 def hangup_detected(self): self.position_local_oldage_count += 1 if self.position_local_oldage_count == 200: if self.position_local_oldage == self.position_local: return True self.position_local_oldage = self.position_local self.position_local_oldage_count = 0 return False def init_socket(self): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) sock.bind(('0.0.0.0', PORT)) return sock def kill_omxplayer(self): try: os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) except: pass try: self.process.wait() except: pass def kill_omxplayer_and_exit(self, *args): self.kill_omxplayer() sys.exit(0) # # master specific # def socket_enable_broadcast(self): self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) def socket_connect(self, destination): try: self.sock.connect((destination, PORT)) except: print("connect: Network is unreachable") pass def send_position_local(self): try: self.sock.send(("%s%%%s" % (str(self.position_local), self.filename)).encode('utf-8')) except socket.error: pass # # slave specific # def read_position_master(self): data = self.sock.recvfrom(1024)[0].decode('utf-8').split('%', 1) self.position_master = float(data[0]) self.filename_master = data[1] def median(self, lst): quotient, remainder = divmod(len(lst), 2) if remainder: return sorted(lst)[quotient] return float(sum(sorted(lst)[quotient - 1:quotient + 1]) / 2.0) if __name__ == '__main__': OMXPlayerSync().run()