Example #1
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
Example #2
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
Example #3
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)