Ejemplo n.º 1
0
    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!!")
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
    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
Ejemplo n.º 5
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
Ejemplo n.º 6
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
Ejemplo n.º 7
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
Ejemplo n.º 8
0
    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
Ejemplo n.º 9
0
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)