diff --git a/tests/test_viewer.py b/tests/test_viewer.py index 539e3235..6fa5d3a0 100644 --- a/tests/test_viewer.py +++ b/tests/test_viewer.py @@ -79,18 +79,6 @@ class TestLoadBrowser(ViewerTestCase): self.m_cmd.assert_called_once_with('ScreenlyWebview') -class TestSignalHandlers(ViewerTestCase): - @mock.patch('vlc.Instance', mock.MagicMock()) - @mock.patch( - 'viewer.media_player.get_device_type', - return_value='pi4' - ) - def test_usr1(self, lookup_mock): - self.p_killall.start() - self.assertEqual(None, self.u.sigusr1(None, None)) - self.p_killall.stop() - - class TestWatchdog(ViewerTestCase): def test_watchdog_should_create_file_if_not_exists(self): try: diff --git a/viewer/__init__.py b/viewer/__init__.py index 6c7228cb..f35df0ea 100644 --- a/viewer/__init__.py +++ b/viewer/__init__.py @@ -7,7 +7,7 @@ import logging import sys from builtins import range from os import getenv, path -from signal import SIGALRM, SIGUSR1, signal +from signal import SIGALRM, signal from time import sleep import django @@ -31,8 +31,8 @@ from viewer.media_player import MediaPlayerProxy from viewer.playback import navigate_to_asset, play_loop, skip_asset, stop_loop from viewer.utils import ( command_not_found, + get_skip_event, sigalrm, - sigusr1, wait_for_server, watchdog, ) @@ -196,9 +196,13 @@ def view_video(uri, duration): view_image('null') try: - while media_player.is_playing(): - watchdog() - sleep(1) + skip_event = get_skip_event() + skip_event.clear() + if skip_event.wait(timeout=int(duration)): + logging.info('Skip detected during video playback, stopping video') + media_player.stop() + else: + pass except sh.ErrorReturnCode_1: logging.info( 'Resource URI is not correct, remote host is not responding or ' @@ -225,7 +229,15 @@ def asset_loop(scheduler): logging.info( 'Playlist is empty. Sleeping for %s seconds', EMPTY_PL_DELAY) view_image(STANDBY_SCREEN) - sleep(EMPTY_PL_DELAY) + skip_event = get_skip_event() + skip_event.clear() + if skip_event.wait(timeout=EMPTY_PL_DELAY): + # Skip was triggered, continue immediately to next iteration + logging.info( + 'Skip detected during empty playlist wait, continuing') + else: + # Duration elapsed normally, continue to next iteration + pass elif ( path.isfile(asset['uri']) or @@ -248,12 +260,27 @@ def asset_loop(scheduler): if 'image' in mime or 'web' in mime: duration = int(asset['duration']) logging.info('Sleeping for %s', duration) - sleep(duration) + skip_event = get_skip_event() + skip_event.clear() + if skip_event.wait(timeout=duration): + # Skip was triggered, continue immediately to next iteration + logging.info('Skip detected, moving to next asset immediately') + else: + # Duration elapsed normally, continue to next asset + pass else: logging.info('Asset %s at %s is not available, skipping.', asset['name'], asset['uri']) - sleep(0.5) + skip_event = get_skip_event() + skip_event.clear() + if skip_event.wait(timeout=0.5): + # Skip was triggered, continue immediately to next iteration + logging.info( + 'Skip detected during asset unavailability wait, continuing') + else: + # Duration elapsed normally, continue to next iteration + pass def setup(): @@ -266,7 +293,7 @@ def setup(): # or we can create a new class that extends Exception. sys.exit(1) - signal(SIGUSR1, sigusr1) + # Skip event is now handled via threading instead of signals signal(SIGALRM, sigalrm) load_settings() diff --git a/viewer/media_player.py b/viewer/media_player.py index 9a330fba..29dc62ef 100644 --- a/viewer/media_player.py +++ b/viewer/media_player.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals -import sh +import logging +import subprocess + import vlc from lib.device_helper import get_device_type @@ -29,31 +31,30 @@ class MediaPlayer(): class FFMPEGMediaPlayer(MediaPlayer): def __init__(self): MediaPlayer.__init__(self) - self.run = None - self.player_args = list() - self.player_kwargs = dict() + self.process = None def set_asset(self, uri, duration): - self.player_args = ['ffplay', uri, '-autoexit'] - self.player_kwargs = { - '_bg': True, - '_ok_code': [0, 124], - } + self.uri = uri def play(self): - self.run = sh.Command(self.player_args[0])( - *self.player_args[1:], **self.player_kwargs + self.process = subprocess.Popen( + ['ffplay', '-autoexit', self.uri], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL ) def stop(self): try: - if self.run: - self.run.kill() - except OSError: - pass + if self.process: + self.process.terminate() + self.process = None + except Exception as e: + logging.error(f'Exception in stop(): {e}') def is_playing(self): - return bool(self.run.process.alive) + if self.process: + return self.process.poll() is None + return False class VLCMediaPlayer(MediaPlayer): diff --git a/viewer/playback.py b/viewer/playback.py index c592f057..eb9d736c 100644 --- a/viewer/playback.py +++ b/viewer/playback.py @@ -1,15 +1,18 @@ -from os import system +import threading + +# Global event for instant asset switching +skip_event = threading.Event() def skip_asset(scheduler, back=False): if back is True: scheduler.reverse = True - system('pkill -SIGUSR1 -f viewer') + skip_event.set() def navigate_to_asset(scheduler, asset_id): scheduler.extra_asset = asset_id - system('pkill -SIGUSR1 -f viewer') + skip_event.set() def stop_loop(scheduler): diff --git a/viewer/utils.py b/viewer/utils.py index ea1b93c2..77a0f350 100644 --- a/viewer/utils.py +++ b/viewer/utils.py @@ -6,7 +6,6 @@ import requests from lib.errors import SigalrmError from settings import LISTEN, PORT -from viewer.media_player import MediaPlayerProxy WATCHDOG_PATH = '/tmp/screenly.watchdog' @@ -18,13 +17,12 @@ def sigalrm(signum, frame): raise SigalrmError("SigalrmError") -def sigusr1(signum, frame): +def get_skip_event(): """ - The signal interrupts sleep() calls, so the currently - playing web or image asset is skipped. + Get the global skip event for instant asset switching. """ - logging.info('USR1 received, skipping.') - MediaPlayerProxy.get_instance().stop() + from viewer.playback import skip_event + return skip_event def command_not_found():