def __init__(self, stream_url): # Make absolute paths from relative ones if stream_url.startswith('file://.'): stream_url = "file://" + os.path.abspath(stream_url.lstrip('file:/')) self.url = stream_url self._cache_file = CONSTANTS.CACHE_DIR + "streaminfo" self.codec_name = "" self.height = 0 self.width = 0 self.framerate = 0 self.has_audio = False self.force_udp = False self._parse_stream_details() self.valid_url = self._is_url_valid() self.valid_video_windowed = self._is_video_valid(windowed=True) self.valid_video_fullscreen = self._is_video_valid(windowed=False) self.weight = self._calculate_weight() self.quality = self.width * self.height LOG.INFO(self._LOG_NAME, "stream properties '%s', resolution '%ix%i@%i', codec '%s', " "calculated weight '%i', valid url '%i', has audio '%s', " "valid video 'windowed %i fullscreen %i', force UDP '%s'" % ( self.printable_url(), self.width, self.height, self.framerate, self.codec_name, self.weight, self.valid_url, self.has_audio, self.valid_video_windowed, self.valid_video_fullscreen, self.force_udp)) LOG.INFO(self._LOG_NAME, "RUN 'camplayer --rebuild-cache' IF THIS STREAM INFORMATION IS OUT OF DATE!!")
def stream_switch_quality_down(self, check_only=False): """Switch to the next lower quality stream, if any""" if self.active_stream and self.playstate != PLAYSTATE.NONE: resolution = 10000 stream = None # Select the the next lower resolution stream for strm in self.streams: video_valid = strm.valid_video_fullscreen \ if self.fullscreen_mode else strm.valid_video_windowed if resolution < strm.quality < self.active_stream.quality and video_valid: resolution = strm.quality stream = strm # The lowest quality stream is already playing if not stream: LOG.INFO(self._LOG_NAME, "lowest quality stream already playing") return False if not check_only: self.stream_stop() time.sleep(0.1) self._stream_start(stream) return stream return False
def stream_set_invisible(self, _async=False): """Keep the stream open but set it off screen""" if self.playstate == PLAYSTATE.NONE: return if self.visible: LOG.INFO( self._LOG_NAME, "stream set invisible '%s' '%s'" % (self._omx_dbus_ident, self.active_stream.printable_url())) if self._player == PLAYER.OMXPLAYER: # OMXplayer instance is playing inside the visible screen area. # Sending the position command with offset will move this instance out of the visible screen area. if self._omx_audio_enabled: self.visible = False self.stream_refresh() return videopos_arg = str( "%i %i %i %i" % (self.x1 + CONSTANTS.WINDOW_OFFSET, self.y1, self.x2 + CONSTANTS.WINDOW_OFFSET, self.y2)) if _async: setinvisible_thread = threading.Thread( target=self._send_dbus_command, args=( DBUS_COMMAND.OMXPLAYER_VIDEOPOS, videopos_arg, )) setinvisible_thread.start() else: self._send_dbus_command(DBUS_COMMAND.OMXPLAYER_VIDEOPOS, videopos_arg) else: # It's possible that another window hijacked our vlc instance, so do not send 'stop' then. if self.active_stream.url == Window._vlc_active_stream_url[ self._display_num - 1]: self._send_dbus_command(DBUS_COMMAND.PLAY_STOP) Window._vlc_active_stream_url[self._display_num - 1] = "" self.visible = False
def stream_stop(self): """Stop the playing stream""" if self.playstate == PLAYSTATE.NONE: return LOG.INFO( self._LOG_NAME, "stopping stream '%s' '%s'" % (self._omx_dbus_ident, self.active_stream.printable_url())) # VLC: # - send Dbus stop command, vlc stays idle in the background # - not every window has it's own vlc instance as vlc can only be used for fullscreen playback, # therefore we have to be sure that 'our instance' isn't already playing another stream. if self._player == PLAYER.VLCPLAYER and \ Window._vlc_active_stream_url[self._display_num - 1] == self.active_stream.url: # Stop playback but do not quit self._send_dbus_command(DBUS_COMMAND.PLAY_STOP) Window._vlc_active_stream_url[self._display_num - 1] = "" # OMXplayer: # - omxplayer doen't support an idle state, stopping playback will close omxplayer, # so in this case we also have to cleanup the pids. elif self._player == PLAYER.OMXPLAYER and self.omx_player_pid: try: os.kill(self.omx_player_pid, signal.SIGTERM) except Exception as error: LOG.ERROR(self._LOG_NAME, "pid kill error: %s" % str(error)) self._pidpool_remove_pid(self.omx_player_pid) self.omx_player_pid = 0 if self.active_stream: Window._total_weight -= self.get_weight(self.active_stream) self.active_stream = None self.playstate = PLAYSTATE.NONE self._omx_duration = 0
def stream_switch_quality_up(self, check_only=False, limit_default=True): """Switch to the next higher quality stream, if any""" if self.active_stream and self.playstate != PLAYSTATE.NONE: resolution = sys.maxsize stream = None # Limit max quality to the default for performance reasons if limit_default: resolution = self.get_default_stream( windowed=not self.fullscreen_mode).quality + 1 # Select the the next higher resolution stream for strm in self.streams: video_valid = strm.valid_video_fullscreen \ if self.fullscreen_mode else strm.valid_video_windowed if resolution > strm.quality > self.active_stream.quality and video_valid: resolution = strm.quality stream = strm # The highest quality stream is already playing if not stream: LOG.INFO(self._LOG_NAME, "highest quality stream already playing") return False if not check_only: self.stream_stop() time.sleep(0.1) self._stream_start(stream) return stream return False
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 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 stream_set_visible(self, _async=False, fullscreen=None): """Set an active off screen stream back on screen""" if self._player == PLAYER.VLCPLAYER \ and not self.get_vlc_pid(self._display_num): return if self.playstate == PLAYSTATE.NONE: return if fullscreen is None: fullscreen = self.fullscreen_mode if not self.visible or (fullscreen != self.fullscreen_mode): LOG.INFO( self._LOG_NAME, "stream set visible '%s' '%s'" % (self._omx_dbus_ident, self.active_stream.printable_url())) self.fullscreen_mode = fullscreen if self._player == PLAYER.OMXPLAYER: # OMXplayer instance is playing outside the visible screen area. # Sending the position command will move this instance into the visible screen area. if fullscreen: videopos_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: videopos_arg = str("%i %i %i %i" % (self.x1, self.y1, self.x2, self.y2)) # Re-open OMXplayer with the audio stream enabled if CONFIG.AUDIO_MODE == AUDIOMODE.FULLSCREEN and fullscreen \ and not self._omx_audio_enabled and self.active_stream.has_audio: self.visible = True self.stream_refresh() return # Re-open OMXplayer with the audio stream disabled if self._omx_audio_enabled and not fullscreen: self.visible = True self.stream_refresh() return if _async: setvisible_thread = threading.Thread( target=self._send_dbus_command, args=( DBUS_COMMAND.OMXPLAYER_VIDEOPOS, videopos_arg, )) setvisible_thread.start() else: self._send_dbus_command(DBUS_COMMAND.OMXPLAYER_VIDEOPOS, videopos_arg) elif fullscreen: # 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. # Start our stream self._send_dbus_command(DBUS_COMMAND.PLAY_PLAY) # Pretend like the player just started again self.playstate = PLAYSTATE.INIT2 self._time_streamstart = time.monotonic() # Mark our steam as the active one for this display Window._vlc_active_stream_url[self._display_num - 1] = self.active_stream.url else: # Windowed with VLC not supported -> stop video self.stream_stop() self.visible = True
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)