fix(viewer): make asset navigation instantaneous (#2539)

This commit is contained in:
Nico Miguelino
2025-10-22 12:55:42 -07:00
committed by GitHub
parent 428d36254e
commit 992e33f1e6
5 changed files with 63 additions and 46 deletions

View File

@@ -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:

View File

@@ -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()

View File

@@ -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):

View File

@@ -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):

View File

@@ -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():