def set_display_name(self, display_name): """Set player OSD text for this window""" if not display_name or self._display_name: return sub_file = CONSTANTS.CACHE_DIR + display_name + ".srt" try: # Create folder if not exist if not os.path.isdir(os.path.dirname(sub_file)): os.system("mkdir -p %s" % os.path.dirname(sub_file)) # Create subtitle file if not exist if not os.path.isfile(sub_file): with open(sub_file, 'w+') as file: # Important note: we can only show subs for a 99 hour period! file.write('00:00:00,00 --> 99:00:00,00\n') file.write(display_name + '\n') self._display_name = display_name except: # TODO: filter for read-only error only LOG.ERROR(self._LOG_NAME, "writing subtitle file failed, read only?")
def stop_all_players(cls, sigkill=False): """Stop all players the fast and hard way""" term_cmd = '-9' if sigkill else '-15' try: subprocess.Popen(['killall', term_cmd, 'omxplayer.bin'], shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.Popen(['killall', term_cmd, 'vlc'], shell=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as error: LOG.ERROR(cls._LOG_NAME, "stop_all_players pid kill error: %s" % str(error))
def scale_background(cls, src_path, dest_path, dest_width, dest_height): """Scale background image to the requested width and height""" if not GLOBALS.FFMPEG_SUPPORT: return False ffmpeg_cmd = str("ffmpeg -i '%s' -vf scale=%i:%i '%s'" % (src_path, dest_width, dest_height, dest_path)) try: subprocess.check_output(ffmpeg_cmd, shell=True, stderr=subprocess.STDOUT, timeout=5) except (subprocess.CalledProcessError, subprocess.TimeoutExpired): LOG.ERROR(cls._MODULE, "Scaling background image '%s' failed" % src_path) if os.path.isfile(dest_path): return True return 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_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 _parse_stream_details(self): """Read stream details from cache file or parse stream directly""" if not self._is_url_valid(): return parsed_ok = False video_found = False if os.path.isfile(self._cache_file): with open(self._cache_file, 'r') as stream_file: data = json.load(stream_file) if self.printable_url() in data.keys(): stream_props = data.get(self.printable_url()) self.codec_name = stream_props.get('codec_name') self.height = stream_props.get('height') self.width = stream_props.get('width') self.framerate = stream_props.get('framerate') self.has_audio = stream_props.get('audio') parsed_ok = True if not parsed_ok: try: # Invoke ffprobe, 20s timeout required for pi zero streams = subprocess.check_output([ 'ffprobe', '-v', 'error', '-show_entries', 'stream=codec_type,height,width,codec_name,bit_rate,max_bit_rate,avg_frame_rate', self.url], universal_newlines=True, timeout=10, stderr=subprocess.STDOUT).split("[STREAM]") for stream in streams: streamprops = stream.split() if "codec_type=video" in stream and not video_found: video_found = True for streamproperty in streamprops: if "codec_name" in streamproperty: self.codec_name = streamproperty.split("=")[1] if "height" in streamproperty: self.height = int(streamproperty.split("=")[1]) if "width" in streamproperty: self.width = int(streamproperty.split("=")[1]) if "avg_frame_rate" in streamproperty: try: framerate = streamproperty.split("=")[1] # ffprobe returns framerate as fraction, # a zero division exception is therefore possible self.framerate = int( framerate.split("/")[0])/int(framerate.split("/")[1]) except Exception: self.framerate = 0 elif "codec_type=audio" in stream and not self.has_audio: self.has_audio = True if video_found: try: self._write_stream_details() # TODO: filter read-only exception except Exception: LOG.ERROR(self._LOG_NAME, "writing ffprobe results to file failed, read only?") # TODO: logging exceptions can spawn credentials?? except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as ex: LOG.ERROR(self._LOG_NAME, "ffprobe exception: %s" % str(ex))