def load_icons(cls): """Load pipng icon queue""" if not CONFIG.ENABLE_ICONS or not GLOBALS.PIPNG_SUPPORT: return for display_idx in range(GLOBALS.NUM_DISPLAYS): if len(cls._icons[display_idx]) <= 0: continue pngview_cmd = ["pipng", "-b", "0", # No 2nd background layer under image "-l", str(cls._foreground_layer), # Set layer number "-d", "2" if display_idx == 0 else "7", # Set display number "-i", # Start with all images invisible "-x", str(CONSTANTS.ICON_OFFSET_X), # 60px offset x-axis "-y", str(CONSTANTS.ICON_OFFSET_Y), # 60px offset y-axis ] # Add all images, currently limited to 10 for image in cls._icons[display_idx]: pngview_cmd.append(CONSTANTS.RESOURCE_DIR_ICONS + image) LOG.DEBUG(cls._MODULE, "Loading pipng for display '%i' with command '%s'" % (display_idx, pngview_cmd)) cls._proc_icons[display_idx] = \ subprocess.Popen(pngview_cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def __init__(self, x1, y1, x2, y2, gridindex, screen_idx, window_idx, display_idx): self.x1 = x1 # Upper left x-position of window self.y1 = y1 # Upper left y-position of window self.x2 = x2 # Lower right x-position of window self.y2 = y2 # Lower right y-position of window self.gridindex = gridindex # Grid indices covered by this window self.omx_player_pid = 0 # OMXplayer PID self._omx_audio_enabled = False # OMXplayer audio stream enabled self._omx_duration = 0 # OMXplayer reported stream duration self._layer = 0 # Player dispmanx layer self.visible = False # Is window in visible area? self._forced_fullscreen = self.native_fullscreen # Is window forced in fullscreen mode? self._fail_rate_hr = 0 # Stream failure rate of last hour self._time_playstatus = 0 # Timestamp of last playstatus check self._time_streamstart = 0 # Timestamp of last stream start self.streams = [] # Assigned stream(s) self.active_stream = None # Currently playing stream self._display_name = "" # Video OSD display name self._player = PLAYER.NONE # Currently active player for this window (OMX or VLC) self.playstate = PLAYSTATE.NONE # Current stream play state for this window self._window_num = window_idx + 1 self._screen_num = screen_idx + 1 self._display_num = display_idx + 1 self._omx_dbus_ident = str( "org.mpris.MediaPlayer2.omxplayer_D%02d_S%02d_W%02d" % (self._display_num, self._screen_num, self._window_num)) LOG.DEBUG( self._LOG_NAME, "init window with position '%i %i %i %i', gridindex '%s', " "omxplayer dbus name '%s'" % (x1, y1, x2, y2, str(gridindex), self._omx_dbus_ident))
def pidpool_update(cls): """Update the PID pool of OMXplayer and VLC media player instances""" cls._player_pid_pool_cmdline = [[], []] try: player_pids = subprocess.check_output(['pidof', 'vlc'], universal_newlines=True, timeout=5).split() LOG.DEBUG("PIDpool", "active VLCplayer PIDs '%s'" % player_pids) for player_pid in player_pids: cls._player_pid_pool_cmdline[0].append(int(player_pid)) cls._player_pid_pool_cmdline[1].append( subprocess.check_output( ['cat', str('/proc/%s/cmdline' % player_pid)], universal_newlines=True, timeout=5)) except subprocess.CalledProcessError: pass try: player_pids = subprocess.check_output(['pidof', 'omxplayer.bin'], universal_newlines=True, timeout=5).split() LOG.DEBUG("PIDpool", "active OMXplayer PIDs '%s'" % player_pids) for player_pid in player_pids: cls._player_pid_pool_cmdline[0].append(int(player_pid)) cls._player_pid_pool_cmdline[1].append( subprocess.check_output( ['cat', str('/proc/%s/cmdline' % player_pid)], universal_newlines=True, timeout=5)) except subprocess.CalledProcessError: pass
def _pidpool_remove_pid(cls, pid): """Remove Player PID from pidpool""" for idx, _pid in enumerate(cls._player_pid_pool_cmdline[0]): if _pid == pid: LOG.DEBUG("PIDpool", "removed Player PID '%i' from pool" % pid) del cls._player_pid_pool_cmdline[0][idx] del cls._player_pid_pool_cmdline[1][idx] return True return False
def load_backgrounds(cls): """Load pipng background queue""" if CONFIG.BACKGROUND_MODE == BACKGROUND.OFF or not GLOBALS.PIPNG_SUPPORT: return if CONFIG.BACKGROUND_MODE == BACKGROUND.HIDE_FRAMEBUFFER: for display_idx in range(GLOBALS.NUM_DISPLAYS): if len(cls._backgrounds[display_idx]) <= 0: continue subprocess.Popen(["pipng", "-b", "000F", "-n", "-d", "2" if display_idx == 0 else "7"], shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE) else: static_background = CONFIG.BACKGROUND_MODE == BACKGROUND.STATIC for display_idx in range(GLOBALS.NUM_DISPLAYS): if len(cls._backgrounds[display_idx]) <= 0: continue pngview_cmd = ["pipng", "-b", "0", # No 2nd background layer under image "-l", str(cls._background_layer), # Set layer number "-d", "2" if display_idx == 0 else "7", # Set display number "-h", # Hide lower layers (less GPU performance impact) ] if not static_background: pngview_cmd.append("-i") # Start with all images invisible # Add all background images, currently limited to 10 for image in cls._backgrounds[display_idx]: pngview_cmd.append(image) # TODO: find best match with static backgrounds and multiple screens if static_background: break LOG.DEBUG(cls._MODULE, "Loading pipng for display '%i' with command '%s'" % (display_idx + 1, pngview_cmd)) cls._proc_background[display_idx] = \ subprocess.Popen(pngview_cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def show_icon(cls, filename, display_idx=0): """Show pipng icon from queue""" if not CONFIG.ENABLE_ICONS or not GLOBALS.PIPNG_SUPPORT: return display_idx = 1 if display_idx == 1 else 0 # Show new image/icon for idx, image in enumerate(cls._icons[display_idx]): if filename == image: LOG.DEBUG(cls._MODULE, "setting icon '%s' visible for display '%i" % (filename, display_idx)) cls._proc_icons[display_idx].stdin.write(str(idx).encode('utf-8')) cls._proc_icons[display_idx].stdin.flush() cls.active_icon[display_idx] = filename
def show_background(cls, filename, display_idx=0): """Show pipng background from queue""" if CONFIG.BACKGROUND_MODE != BACKGROUND.DYNAMIC or not GLOBALS.PIPNG_SUPPORT: return display_idx = 1 if display_idx == 1 else 0 if cls.active_background[display_idx] == filename: return # Show new image/icon for idx, image in enumerate(cls._backgrounds[display_idx]): if filename == image: LOG.DEBUG(cls._MODULE, "setting background '%s' visible for display '%i" % (filename, display_idx)) cls._proc_background[display_idx].stdin.write(str(idx).encode('utf-8')) cls._proc_background[display_idx].stdin.flush() cls.active_background[display_idx] = filename
def hide_icon(cls, display_idx=0): """Hide active pipng iconn""" if not CONFIG.ENABLE_ICONS or not GLOBALS.PIPNG_SUPPORT: return display_idx = 1 if display_idx == 1 else 0 if not cls.active_icon[display_idx]: return LOG.DEBUG(cls._MODULE, "hiding icon '%s' for display '%i" % (cls.active_icon[display_idx], display_idx)) cls._proc_icons[display_idx].stdin.write("i".encode('utf-8')) cls._proc_icons[display_idx].stdin.flush() cls.active_icon[display_idx] = "" # pipng needs some milliseconds to read stdin # Especially important when hide_icon() will be immediately followed by show_icon() time.sleep(0.025)
def _stream_start(self, stream=None): """Start the specified stream if any, else the default will be played""" if self.playstate != PLAYSTATE.NONE: return if len(self.streams) <= 0: return if not stream: stream = self.get_default_stream() if not stream: return win_width = CONSTANTS.VIRT_SCREEN_WIDTH if self.fullscreen_mode else self.window_width win_height = CONSTANTS.VIRT_SCREEN_HEIGHT if self.fullscreen_mode else self.window_height sub_file = "" if self._display_name and CONFIG.VIDEO_OSD: sub_file = CONSTANTS.CACHE_DIR + self._display_name + ".srt" LOG.INFO( self._LOG_NAME, "starting stream '%s' '%s' with resolution '%ix%i' and weight '%i' in a window '%ix%i'" % (self._omx_dbus_ident, stream.printable_url(), stream.width, stream.height, self.get_weight(stream), win_width, win_height)) # OMXplayer can play in fullscreen and windowed mode # One instance per window if stream.valid_video_windowed: self._player = PLAYER.OMXPLAYER # Layer should be unique to avoid visual glitches/collisions omx_layer_arg = (self._screen_num * CONSTANTS.MAX_WINDOWS) + self._window_num if self.fullscreen_mode and self.visible: # Window position also required for fullscreen playback, # otherwise lower layers will be disabled when moving the window position later on omx_pos_arg = str("%i %i %i %i" % (CONSTANTS.VIRT_SCREEN_OFFSET_X, CONSTANTS.VIRT_SCREEN_OFFSET_Y, CONSTANTS.VIRT_SCREEN_OFFSET_X + CONSTANTS.VIRT_SCREEN_WIDTH, CONSTANTS.VIRT_SCREEN_OFFSET_Y + CONSTANTS.VIRT_SCREEN_HEIGHT)) else: omx_pos_arg = str( "%i %i %i %i" % (self.x1 + (0 if self.visible else CONSTANTS.WINDOW_OFFSET), self.y1, self.x2 + (0 if self.visible else CONSTANTS.WINDOW_OFFSET), self.y2)) player_cmd = [ 'omxplayer', '--no-keys', # No keyboard input '--no-osd', # No OSD '--aspect-mode', 'stretch', # Stretch video if aspect doesn't match '--dbus_name', self. _omx_dbus_ident, # Dbus name for controlling position etc. '--threshold', str(CONFIG.BUFFERTIME_MS / 1000), # Threshold of buffer in seconds '--layer', str(omx_layer_arg), # Dispmanx layer '--alpha', '255', # No transparency '--nodeinterlace', # Assume progressive streams '--nohdmiclocksync', # Clock sync makes no sense with multiple clock sources '--avdict', 'rtsp_transport:tcp', # Force RTSP over TCP '--display', '7' if self._display_num == 2 else '2', # 2 is HDMI0 (default), 7 is HDMI1 (pi4) '--timeout', str(CONFIG.PLAYTIMEOUT_SEC ), # Give up playback after this period of trying '--win', omx_pos_arg # Window position ] if stream.url.startswith('file://'): player_cmd.append( '--loop') # Loop for local files (demo/test mode) else: player_cmd.append( '--live') # Avoid sync issues with long playing streams if CONFIG.AUDIO_MODE == AUDIOMODE.FULLSCREEN and \ self.visible and self.fullscreen_mode and stream.has_audio: # OMXplayer can only open 8 instances instead of 16 when audio is enabled, # this can also lead to total system lockups... # Work around this by disabling the audio stream when in windowed mode, # in fullscreen mode, we can safely enable audio again. # set_visible() and set_invisible() methods are also adopted for this. # Volume % to millibels conversion volume = int(2000 * math.log10(max(CONFIG.AUDIO_VOLUME, 0.001) / 100)) player_cmd.extend(['--vol', str(volume)]) # Set audio volume self._omx_audio_enabled = True else: player_cmd.extend(['--aidx', '-1']) # Disable audio stream self._omx_audio_enabled = False # Show our channel name with a custom subtitle file? # OMXplayer OSD not supported on pi4 hardware if sub_file and not "4B" in GLOBALS.PI_MODEL: if os.path.isfile(sub_file): player_cmd.extend(['--subtitles', sub_file ]) # Add channel name as subtitle player_cmd.extend([ '--no-ghost-box', '--align', 'center', '--lines', '1' ]) # Set subtitle properties # VLC media player can play only in fullscreen mode # One fullscreen instance per display elif self.fullscreen_mode and stream.valid_video_fullscreen: self._player = PLAYER.VLCPLAYER player_cmd = [ 'cvlc', '--fullscreen', # VLC does not support windowed mode without X11 '--network-caching=' + str(CONFIG.BUFFERTIME_MS ), # Threshold of buffer in miliseconds '--rtsp-tcp', # Force RTSP over TCP '--no-keyboard-events', # No keyboard events '--mmal-display=hdmi-' + str(self._display_num), # Select the correct display '--mmal-layer=0', # OMXplayer uses layers starting from 0, don't interference '--input-timeshift-granularity=0', # Disable timeshift feature '--vout=mmal_vout', # Force MMAL mode '--gain=1', # Audio gain '--no-video-title-show' # Disable filename popup on start ] # Keep in mind that VLC instances can be reused for # other windows with possibly other audio settings! # So don't disable the audio output to quickly! if CONFIG.AUDIO_MODE == AUDIOMODE.FULLSCREEN: # VLC does not have a command line volume argument?? pass else: player_cmd.append('--no-audio') # Disable audio stream if stream.url.startswith('file://'): player_cmd.append( '--repeat') # Loop for local files (demo/test mode) # Show our channel name with a custom subtitle file? if sub_file and os.path.isfile(sub_file): player_cmd.extend(['--sub-file', sub_file]) # Add channel name as subtitle # TODO: we need te reopen VLC every time for the correct sub? if ((sub_file and os.path.isfile(sub_file)) or Window._vlc_subs_enabled[self._display_num - 1]) and \ self.get_vlc_pid(self._display_num): LOG.WARNING( self._LOG_NAME, "closing already active VLC instance for display '%i' " "as subtitles (video OSD) are enabled" % self._display_num) player_pid = self.get_vlc_pid(self._display_num) utils.terminate_process(player_pid, force=True) self._pidpool_remove_pid(player_pid) Window.vlc_player_pid[self._display_num - 1] = 0 else: LOG.ERROR( self._LOG_NAME, "stream '%s' with codec '%s' is not valid for playback" % (stream.printable_url(), stream.codec_name)) return # Check hardware video decoder impact if Window._total_weight + self.get_weight( stream) > CONSTANTS.HW_DEC_MAX_WEIGTH and CONFIG.HARDWARE_CHECK: LOG.ERROR( self._LOG_NAME, "current hardware decoder weight is '%i', max decoder weight is '%i'" % (Window._total_weight, CONSTANTS.HW_DEC_MAX_WEIGTH)) return else: Window._total_weight += self.get_weight(stream) # Set URL before stripping self.active_stream = stream url = stream.url if self._player == PLAYER.VLCPLAYER and self.get_vlc_pid( self._display_num): LOG.DEBUG( self._LOG_NAME, "reusing already active VLC instance for display '%i'" % self._display_num) if self.visible: # VLC player instance can be playing or in idle state. # Sending the play command will start fullscreen playback of our video/stream. # When VLC is playing other content, we will hijack it. # Enable/disable audio if CONFIG.AUDIO_MODE == AUDIOMODE.FULLSCREEN: volume = CONFIG.AUDIO_VOLUME / 100 self._send_dbus_command(DBUS_COMMAND.PLAY_VOLUME, volume) # Start our stream self._send_dbus_command(DBUS_COMMAND.PLAY_PLAY) # Mark our steam as the active one for this display Window._vlc_active_stream_url[self._display_num - 1] = self.active_stream.url else: # Play command will be sent by 'stream_set_visible' later on. pass # Pretend like the player just started again self.playstate = PLAYSTATE.INIT2 self._time_streamstart = time.monotonic() return else: LOG.DEBUG(self._LOG_NAME, "starting player with arguments '%s'" % player_cmd) # Add URL now, as we don't want sensitive credentials in the logfile... if self._player == PLAYER.OMXPLAYER: player_cmd.append(url) elif self._player == PLAYER.VLCPLAYER and self.visible: player_cmd.append(url) Window._vlc_active_stream_url[self._display_num - 1] = url if self._player == PLAYER.VLCPLAYER: # VLC changes its DBus string to 'org.mpris.MediaPlayer2.vlc.instancePID' # when opening a second instance, so we have to adjust it later on when we know the PID # Max number of VLC instances = number of displays = 2 if self._pidpool_get_pid("--mmal-display=hdmi-"): Window._vlc_dbus_ident[ self._display_num - 1] = "org.mpris.MediaPlayer2.vlc.instance" else: Window._vlc_dbus_ident[self._display_num - 1] = "org.mpris.MediaPlayer2.vlc" # Save the subtitle state for later use Window._vlc_subs_enabled[self._display_num - 1] = (sub_file and os.path.isfile(sub_file)) subprocess.Popen(player_cmd, shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if self._player == PLAYER.VLCPLAYER: # VLC does not have a command line argument for volume control?? # As workaround, wait for VLC startup and send the desired volume with DBus time.sleep(0.5) self._send_dbus_command(DBUS_COMMAND.PLAY_VOLUME, CONFIG.AUDIO_VOLUME / 100, retries=5) self._time_streamstart = time.monotonic() self.playstate = PLAYSTATE.INIT1 self._omx_duration = 0
def _send_dbus_command(self, command, argument="", kill_player_on_error=True, retries=CONSTANTS.DBUS_RETRIES): """Send command to player with DBus""" response = "" command_destination = "" command_prefix = "" if self._player == PLAYER.OMXPLAYER: command_destination = self._omx_dbus_ident # OMXplayer needs some environment variables command_prefix = str( "export DBUS_SESSION_BUS_ADDRESS=`cat /tmp/omxplayerdbus.%s` && " "export DBUS_SESSION_BUS_PID=`cat /tmp/omxplayerdbus.%s.pid` && " % (GLOBALS.USERNAME, GLOBALS.USERNAME)) elif self._player == PLAYER.VLCPLAYER: command_destination = Window._vlc_dbus_ident[self._display_num - 1] # VLC changes its DBus string to 'org.mpris.MediaPlayer2.vlc.instancePID' # when opening a second instance, so we have to append this PID first. if 'instance' in command_destination: command_destination += str( Window.vlc_player_pid[self._display_num - 1]) for i in range(retries + 1): try: if command == DBUS_COMMAND.OMXPLAYER_VIDEOPOS: response = subprocess.check_output( command_prefix + "dbus-send --print-reply=literal --reply-timeout=%i " "--dest=%s /org/mpris/MediaPlayer2 " "org.mpris.MediaPlayer2.Player.%s objpath:/not/used " "string:'%s'" % (CONSTANTS.DBUS_TIMEOUT_MS, command_destination, command, argument), shell=True, stderr=subprocess.STDOUT).decode().strip() elif command == DBUS_COMMAND.PLAY_STOP: response = subprocess.check_output( command_prefix + "dbus-send --print-reply=literal --reply-timeout=%i " "--dest=%s /org/mpris/MediaPlayer2 " "org.mpris.MediaPlayer2.Player.%s" % (CONSTANTS.DBUS_TIMEOUT_MS, command_destination, command), shell=True, stderr=subprocess.STDOUT).decode().strip() elif command == DBUS_COMMAND.PLAY_PLAY: response = subprocess.check_output( command_prefix + "dbus-send --print-reply=literal --reply-timeout=%i " "--dest=%s /org/mpris/MediaPlayer2 " "org.mpris.MediaPlayer2.Player.%s string:'%s'" % (CONSTANTS.DBUS_TIMEOUT_MS, command_destination, command, self.active_stream.url), shell=True, stderr=subprocess.STDOUT).decode().strip() elif command == DBUS_COMMAND.PLAY_VOLUME: response = subprocess.check_output( command_prefix + "dbus-send --print-reply=literal --reply-timeout=%i " "--dest=%s /org/mpris/MediaPlayer2 " "org.freedesktop.DBus.Properties.Set " "string:'org.mpris.MediaPlayer2.Player' string:'%s' variant:double:%f" % (CONSTANTS.DBUS_TIMEOUT_MS, command_destination, command, argument), shell=True, stderr=subprocess.STDOUT).decode().strip() else: response = subprocess.check_output( command_prefix + "dbus-send --print-reply=literal --reply-timeout=%i " "--dest=%s /org/mpris/MediaPlayer2 " "org.freedesktop.DBus.Properties.Get " "string:'org.mpris.MediaPlayer2.Player' string:'%s'" % (CONSTANTS.DBUS_TIMEOUT_MS, command_destination, command), shell=True, stderr=subprocess.STDOUT).decode().strip() LOG.DEBUG( self._LOG_NAME, "DBus response to command '%s:%s %s' is '%s'" % (command_destination, command, argument, response)) except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex: if i == retries: if self._player == PLAYER.VLCPLAYER: player_pid = Window.vlc_player_pid[self._display_num - 1] else: player_pid = self.omx_player_pid LOG.ERROR( self._LOG_NAME, "DBus '%s' is not responding correctly after '%i' attemps, " "give up now" % (command_destination, retries + 1)) if kill_player_on_error and player_pid > 0: LOG.ERROR( self._LOG_NAME, "DBus '%s' closing the associated player " "with PID '%i' now" % (command_destination, player_pid)) try: os.kill(player_pid, signal.SIGKILL) except ProcessLookupError: LOG.DEBUG(self._LOG_NAME, "killing PID '%i' failed" % player_pid) self._pidpool_remove_pid(player_pid) if self._player == PLAYER.VLCPLAYER: Window.vlc_player_pid[self._display_num - 1] = 0 else: self.omx_player_pid = 0 else: LOG.WARNING( self._LOG_NAME, "DBus '%s' is not responding correctly, " "retrying within 250ms" % command_destination) time.sleep(0.25) continue break return response
def get_stream_playstate(self): """ Get and update the stream's playstate, don't use this time consuming method too often, use 'self.playstate' when you can. """ if self.playstate == PLAYSTATE.NONE: return self.playstate # Allow at least 1 second for the player to startup if self.playstate == PLAYSTATE.INIT1 and self.playtime < 1: return self.playstate old_playstate = self.playstate # Assign the player PID if self.playstate == PLAYSTATE.INIT1: if self._player == PLAYER.VLCPLAYER: pid = self.get_vlc_pid(self._display_num) else: pid = self.get_omxplayer_pid() if pid > 0: if self._player == PLAYER.VLCPLAYER: Window.vlc_player_pid[self._display_num - 1] = pid else: self.omx_player_pid = pid self.playstate = PLAYSTATE.INIT2 LOG.DEBUG( self._LOG_NAME, "assigned PID '%i' for stream '%s' '%s'" % (pid, self._omx_dbus_ident, self.active_stream.printable_url())) elif self.playtime > CONSTANTS.PLAYER_INITIALIZE_MS / 1000: self.playstate = PLAYSTATE.BROKEN # Check if the player is actually playing media # DBus calls are time consuming, so limit them elif time.monotonic() > (self._time_playstatus + 10) or \ (self.playstate == PLAYSTATE.INIT2 and time.monotonic() > (self._time_playstatus + 1)): LOG.DEBUG( self._LOG_NAME, "fetching playstate for stream '%s' '%s'" % (self._omx_dbus_ident, self.active_stream.printable_url())) duration_diff = 0 output = "" # Check playstate and kill the player if it does not respond properly # 04/04/2020: Under some circumstances omxplayer freezes with corrupt streams (bad wifi/network quality etc.), # while it still reports its playstate as 'playing', # therefore we monitor will monitor the reported 'duration' (for livestreams) from now on. if not self.active_stream.url.startswith( 'file://') and self._player == PLAYER.OMXPLAYER: output = self._send_dbus_command( DBUS_COMMAND.PLAY_DURATION, kill_player_on_error=self.playtime > CONFIG.PLAYTIMEOUT_SEC) try: duration = int(output.split("int64")[1].strip()) duration_diff = duration - self._omx_duration self._omx_duration = duration except Exception: self._omx_duration = 0 else: output = self._send_dbus_command( DBUS_COMMAND.PLAY_STATUS, kill_player_on_error=self.playtime > CONFIG.PLAYTIMEOUT_SEC) if (output and "playing" in str(output).lower()) or duration_diff > 0: self.playstate = PLAYSTATE.PLAYING else: # Only set broken after a timeout period, # so keep the init state the first seconds if self.playtime > CONFIG.PLAYTIMEOUT_SEC: if self._player == PLAYER.OMXPLAYER or self.visible: # Don't set broken when VLC is in "stopped" state # Stopped state occurs when not visible self.playstate = PLAYSTATE.BROKEN self._time_playstatus = time.monotonic() if old_playstate != self.playstate: LOG.INFO( self._LOG_NAME, "stream playstate '%s' for stream '%s' '%s'" % (self.playstate.name, self._omx_dbus_ident, self.active_stream.printable_url())) return self.playstate
def main(): """Application entry point""" global running num_array = [] last_added = time.monotonic() ignore_quit = False if not platform.system() == "Linux": sys.exit("'%s' OS not supported!" % platform.system()) if os.geteuid() == 0: sys.exit("Camplayer is not supposed to be run as root!") GLOBALS.PYTHON_VER = sys.version_info if GLOBALS.PYTHON_VER < CONSTANTS.PYTHON_VER_MIN: sys.exit("Python version '%i.%i' or newer required!" % (CONSTANTS.PYTHON_VER_MIN[0], CONSTANTS.PYTHON_VER_MIN[1])) # Started with arguments? if len(sys.argv) > 1: for idx, arg in enumerate(sys.argv): # 1st argument is application if idx == 0: continue # Help info if arg == "-h" or arg == "--help": print(" -h --help Print this help") print(" -v --version Print version info") print(" -c --config Use a specific config file") print(" --rebuild-cache Rebuild cache on startup") print(" --rebuild-cache-exit Rebuild cache and exit afterwards") print(" -d --demo Demo mode") print(" --ignorequit Don't quit when the 'Q' key is pressed") sys.exit(0) # Run in a specific mode if arg == "--rebuild-cache" or arg == "--rebuild-cache-exit": # Clearing the cache clear_cache() # Rebuild cache only and exit if arg == "--rebuild-cache-exit": # Exit when reaching the main loop running = False # Run with a specific config file if arg == "-c" or arg == "--config" and (idx + 1) < len(sys.argv): CONSTANTS.CONFIG_PATH = sys.argv[idx + 1] # Display version info if arg == "-v" or arg == "--version": print("version " + __version__) sys.exit(0) # Run demo mode if arg == "-d" or arg == "--demo": CONSTANTS.CONFIG_PATH = CONSTANTS.DEMO_CONFIG_PATH # Ignore keyboard 'quit' command if arg == "--ignorequit": ignore_quit = True # Load settings from config file CONFIG.load() # Signal handlers signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGINT, signal_handler) LOG.INFO(_LOG_NAME, "Starting camplayer version %s" % __version__) LOG.INFO(_LOG_NAME, "Using config file '%s' and cache directory '%s'" % (CONSTANTS.CONFIG_PATH, CONSTANTS.CACHE_DIR)) # Cleanup some stuff in case something went wrong on the previous run utils.kill_service('omxplayer.bin', force=True) utils.kill_service('vlc', force=True) utils.kill_service('pipng', force=True) # OMXplayer is absolutely required! if not utils.os_package_installed("omxplayer.bin"): sys.exit("OMXplayer not installed but required!") # ffprobe is absolutely required! if not utils.os_package_installed("ffprobe"): sys.exit("ffprobe not installed but required!") # Get system info sys_info = utils.get_system_info() gpu_mem = utils.get_gpu_memory() hw_info = utils.get_hardware_info() # Set some globals for later use GLOBALS.PI_SOC = hw_info.get("soc") # Not very reliable, usually reports BCM2835 GLOBALS.PI_MODEL = hw_info.get("model") GLOBALS.PI_SOC_HEVC = hw_info.get('hevc') GLOBALS.NUM_DISPLAYS = 2 if hw_info.get('dual_hdmi') else 1 GLOBALS.VLC_SUPPORT = utils.os_package_installed("vlc") GLOBALS.PIPNG_SUPPORT = utils.os_package_installed("pipng") GLOBALS.FFMPEG_SUPPORT = utils.os_package_installed("ffmpeg") GLOBALS.USERNAME = os.getenv('USER') # Log system info LOG.INFO(_LOG_NAME, "********************** SYSTEM INFO **********************") LOG.INFO(_LOG_NAME, str("Camplayer version = %s" % __version__)) LOG.INFO(_LOG_NAME, str("Operating system = %s" % sys_info)) LOG.INFO(_LOG_NAME, str("Raspberry Pi SoC = %s" % hw_info.get("soc"))) LOG.INFO(_LOG_NAME, str("Raspberry Pi revision = %s" % hw_info.get("revision"))) LOG.INFO(_LOG_NAME, str("Raspberry Pi model name = %s" % hw_info.get("model"))) LOG.INFO(_LOG_NAME, str("GPU memory allocation = %i MB" % gpu_mem)) LOG.INFO(_LOG_NAME, str("Python version = %s MB" % sys.version.splitlines()[0])) LOG.INFO(_LOG_NAME, str("VLC installed = %s" % GLOBALS.VLC_SUPPORT)) LOG.INFO(_LOG_NAME, str("pipng installed = %s" % GLOBALS.PIPNG_SUPPORT)) LOG.INFO(_LOG_NAME, str("ffmpeg installed = %s" % GLOBALS.FFMPEG_SUPPORT)) LOG.INFO(_LOG_NAME, "*********************************************************") # Register for keyboard 'press' events, requires root # TODO: check privileges? keyboard = InputMonitor(event_type=['press']) # Log overwrites for debugging purpose for setting in CONFIG.advanced_overwritten: LOG.INFO(_LOG_NAME, "advanced setting overwritten for '%s' is '%s'" % (setting[0], setting[1])) # Does this system fulfill the minimal requirements if CONFIG.HARDWARE_CHECK: if not hw_info.get("supported"): sys.exit("Unsupported hardware with revision %s ..." % hw_info.get("revision")) if gpu_mem < CONSTANTS.MIN_GPU_MEM: sys.exit("GPU memory of '%i' MB insufficient ..." % gpu_mem) # Auto detect screen resolution # For the raspberry pi 4: # both HDMI displays are supposed to have the same configuration if CONFIG.SCREEN_HEIGHT == 0 or CONFIG.SCREEN_WIDTH == 0: display_conf = utils.get_display_mode() CONFIG.SCREEN_HEIGHT = display_conf.get('res_height') CONFIG.SCREEN_WIDTH = display_conf.get('res_width') LOG.INFO(_LOG_NAME, "Detected screen resolution for HDMI0 is '%ix%i@%iHz'" % ( CONFIG.SCREEN_WIDTH, CONFIG.SCREEN_HEIGHT, display_conf.get('framerate'))) if CONFIG.SCREEN_HEIGHT <= 0: CONFIG.SCREEN_HEIGHT = 1080 if CONFIG.SCREEN_WIDTH <= 0: CONFIG.SCREEN_WIDTH = 1920 # Are we sure the 2nd HDMI is on for dual HDMI versions? if GLOBALS.NUM_DISPLAYS == 2: # Check for resolution instead of display name as the latter one is empty with force HDMI hotplug if not utils.get_display_mode(display=7).get('res_height'): GLOBALS.NUM_DISPLAYS = 1 # Calculate the virtual screen size now that we now the physical screen size CONSTANTS.VIRT_SCREEN_WIDTH = int(CONFIG.SCREEN_WIDTH * (100 - CONFIG.SCREEN_DOWNSCALE) / 100) CONSTANTS.VIRT_SCREEN_HEIGHT = int(CONFIG.SCREEN_HEIGHT * (100 - CONFIG.SCREEN_DOWNSCALE) / 100) CONSTANTS.VIRT_SCREEN_OFFSET_X = int((CONFIG.SCREEN_WIDTH - CONSTANTS.VIRT_SCREEN_WIDTH) / 2) CONSTANTS.VIRT_SCREEN_OFFSET_Y = int((CONFIG.SCREEN_HEIGHT - CONSTANTS.VIRT_SCREEN_HEIGHT) / 2) LOG.INFO(_LOG_NAME, "Using a virtual screen resolution of '%ix%i'" % (CONSTANTS.VIRT_SCREEN_WIDTH, CONSTANTS.VIRT_SCREEN_HEIGHT)) # Workaround: srt subtitles have a maximum display time of 99 hours if CONFIG.VIDEO_OSD and (not CONFIG.REFRESHTIME_MINUTES or CONFIG.REFRESHTIME_MINUTES >= 99 * 60): CONFIG.REFRESHTIME_MINUTES = 99 * 60 LOG.WARNING(_LOG_NAME, "Subtitle based OSD enabled, forcing 'refreshtime' to '%i'" % CONFIG.REFRESHTIME_MINUTES) # Show 'loading' on master display BackGroundManager.show_icon_instant(BackGround.LOADING, display_idx=0) # Initialize screens and windows screenmanager = ScreenManager() if screenmanager.valid_screens < 1: sys.exit("No valid screen configuration found, check your config file!") # Hide 'loading' message on master display BackGroundManager.hide_icon_instant(display_idx=0) # Working loop while running: # Trigger screenmanager working loop screenmanager.do_work() for event in keyboard.get_events(): last_added = time.monotonic() if event.code in KEYCODE.KEY_NUM.keys(): LOG.DEBUG(_LOG_NAME, "Numeric key event: %i" % KEYCODE.KEY_NUM.get(event.code)) num_array.append(KEYCODE.KEY_NUM.get(event.code)) # Two digit for numbers from 0 -> 99 if len(num_array) > 2: num_array.pop(0) else: # Non numeric key, clear numeric num_array num_array.clear() if event.code == KEYCODE.KEY_RIGHT: screenmanager.on_action(Action.SWITCH_NEXT) elif event.code == KEYCODE.KEY_LEFT: screenmanager.on_action(Action.SWITCH_PREV) elif event.code == KEYCODE.KEY_UP: screenmanager.on_action(Action.SWITCH_QUALITY_UP) elif event.code == KEYCODE.KEY_DOWN: screenmanager.on_action(Action.SWITCH_QUALITY_DOWN) elif event.code == KEYCODE.KEY_ENTER or event.code == KEYCODE.KEY_KPENTER: screenmanager.on_action(Action.SWITCH_SINGLE, 0) elif event.code == KEYCODE.KEY_ESC or event.code == KEYCODE.KEY_EXIT: screenmanager.on_action(Action.SWITCH_GRID) elif event.code == KEYCODE.KEY_SPACE: screenmanager.on_action(Action.SWITCH_PAUSE_UNPAUSE) elif event.code == KEYCODE.KEY_D: screenmanager.on_action(Action.SWITCH_DISPLAY_CONTROL) elif event.code == KEYCODE.KEY_Q and not ignore_quit: running = False break # Timeout between key presses expired? if time.monotonic() > (last_added + (CONSTANTS.KEY_TIMEOUT_MS / 1000)): num_array.clear() # 1 second delay to accept multiple digit numbers elif time.monotonic() > (last_added + (CONSTANTS.KEY_MULTIDIGIT_MS / 1000)) and len(num_array) > 0: LOG.INFO(_LOG_NAME, "Process numeric key input '%s'" % str(num_array)) number = 0 number += num_array[-2] * 10 if len(num_array) > 1 else 0 number += num_array[-1] if number == 0: num_array.clear() screenmanager.on_action(Action.SWITCH_GRID) else: num_array.clear() screenmanager.on_action(Action.SWITCH_SINGLE, number - 1) time.sleep(0.1) # Cleanup stuff before exit keyboard.destroy() BackGroundManager.destroy() utils.kill_service('omxplayer.bin', force=True) utils.kill_service('vlc', force=True) utils.kill_service('pipng', force=True) LOG.INFO(_LOG_NAME, "Exiting raspberry pi camplayer, have a nice day!") sys.exit(0)