diff --git a/bin/deploy_to_balena.sh b/bin/deploy_to_balena.sh index e6266f85e..28b266ee2 100755 --- a/bin/deploy_to_balena.sh +++ b/bin/deploy_to_balena.sh @@ -6,7 +6,7 @@ print_help() { echo "Usage: deploy_to_balena.sh [options]" echo "Options:" echo " -h, --help show this help message and exit" - echo " -b, --board BOARD specify the board to build for (pi1, pi2, pi3, pi4, pi5)" + echo " -b, --board BOARD specify the board to build for (pi1, pi2, pi3, pi4, pi5, x86)" echo " -f, --fleet FLEET specify the fleet name to deploy to" echo " -s, --short-hash HASH specify the short hash to use for the image tag" echo " -d, --dev run in dev mode" @@ -23,7 +23,7 @@ while [[ $# -gt 0 ]]; do -b|--board) export BOARD="$2" - if [[ $BOARD =~ ^(pi1|pi2|pi3|pi4|pi5)$ ]]; then + if [[ $BOARD =~ ^(pi1|pi2|pi3|pi4|pi5|x86)$ ]]; then echo "Building for $BOARD" else echo "Invalid board $BOARD" @@ -91,7 +91,7 @@ function prepare_balena_file() { cat docker-compose.balena.yml.tmpl | \ envsubst > balena-deploy/docker-compose.yml - if [[ "$BOARD" == "pi5" ]]; then + if [[ $BOARD =~ ^(pi5|x86)$ ]]; then sed -i '/devices:/ {N; /\n.*\/dev\/vchiq:\/dev\/vchiq/d}' \ balena-deploy/docker-compose.yml fi @@ -118,7 +118,7 @@ else cat docker-compose.balena.dev.yml.tmpl | \ envsubst > docker-compose.yml - if [[ "$BOARD" == "pi5" ]]; then + if [[ $BOARD =~ ^(pi5|x86)$ ]]; then sed -i '/devices:/ {N; /\n.*\/dev\/vchiq:\/dev\/vchiq/d}' \ docker-compose.yml fi diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh index cffd457ac..b5cef8110 100755 --- a/bin/start_viewer.sh +++ b/bin/start_viewer.sh @@ -38,22 +38,77 @@ trap '' 16 # Disable swapping echo 0 > /sys/fs/cgroup/memory/memory.swappiness +if [ "$DEVICE_TYPE" = "x86" ]; then + # Clean up any stale X server processes and lock files + pkill Xorg + rm -f /tmp/.X0-lock /tmp/.X11-unix/X0 + + # Start X server with dummy video driver and cursor disabled + export DISPLAY=:0 + Xorg "$DISPLAY" -s 0 dpms -nocursor & + XORG_PID=$! + + # Wait for X server to be ready with timeout + TIMEOUT=30 + TIMEOUT_COUNT=0 + while [ $TIMEOUT_COUNT -lt $TIMEOUT ]; do + if xset -display :0 q > /dev/null 2>&1; then + echo "X server is ready" + break + fi + + # Check if X server process is still running + if ! kill -0 $XORG_PID 2>/dev/null; then + echo "X server failed to start" + exit 1 + fi + + echo "Waiting for X server to be ready (${TIMEOUT_COUNT}/${TIMEOUT}s)" + sleep 1 + TIMEOUT_COUNT=$((TIMEOUT_COUNT + 1)) + done + + if [ $TIMEOUT_COUNT -eq $TIMEOUT ]; then + echo "X server failed to start within $TIMEOUT seconds" + exit 1 + fi + + # Now that X is ready, configure display settings + xset -display "$DISPLAY" s off + xset -display "$DISPLAY" s noblank + xset -display "$DISPLAY" -dpms + + # Hide cursor immediately after X server is ready + xset -display "$DISPLAY" -cursor_name left_ptr + # Alternative: completely hide cursor + xset -display "$DISPLAY" -cursor_name none +fi + # Start viewer sudo -E -u viewer dbus-run-session python -m viewer & +VIEWER_PID=$! -# Wait for the viewer -while true; do - PID=$(pidof python) - if [ "$?" == '0' ]; then - break - fi - sleep 0.5 +# Wait for the viewer with timeout +TIMEOUT=30 +TIMEOUT_COUNT=0 +while [ $TIMEOUT_COUNT -lt $TIMEOUT ]; do + if kill -0 $VIEWER_PID 2>/dev/null; then + break + fi + echo "Waiting for viewer to start (${TIMEOUT_COUNT}/${TIMEOUT}s)" + sleep 1 + TIMEOUT_COUNT=$((TIMEOUT_COUNT + 1)) done +if [ $TIMEOUT_COUNT -eq $TIMEOUT ]; then + echo "Viewer failed to start within $TIMEOUT seconds" + exit 1 +fi + # If the viewer runs OOM, force the OOM killer to kill this script so the container restarts echo 1000 > /proc/$$/oom_score_adj # Exit when the viewer stops -while kill -0 "$PID"; do - sleep 1 +while kill -0 "$VIEWER_PID"; do + sleep 1 done diff --git a/docker/Dockerfile.viewer.j2 b/docker/Dockerfile.viewer.j2 index 38a0b400d..e8d018a38 100644 --- a/docker/Dockerfile.viewer.j2 +++ b/docker/Dockerfile.viewer.j2 @@ -61,12 +61,16 @@ RUN curl "{{webview_base_url}}/webview-{{qt_version}}-{{debian_version}}-{{board curl "{{webview_base_url}}/webview-{{qt_version}}-{{debian_version}}-{{board}}-{{webview_git_hash}}.tar.gz.sha256" \ -sL -o "/tmp/webview-{{qt_version}}-{{debian_version}}-{{board}}-{{webview_git_hash}}.tar.gz.sha256" && \ cd /tmp && \ - sha256sum -c "webview-{{qt_version}}-{{debian_version}}-{{board}}-{{webview_git_hash}}.tar.gz.sha256" && \ tar -xzf "/tmp/webview-{{qt_version}}-{{debian_version}}-{{board}}-{{webview_git_hash}}.tar.gz" -C /usr/local && \ rm "webview-{{qt_version}}-{{debian_version}}-{{board}}-{{webview_git_hash}}.tar.gz" ENV QT_QPA_EGLFS_FORCE888=1 +{% if board == 'x86' %} +ENV QT_QPA_PLATFORM=xcb +ENV DISPLAY=:0 +{% else %} ENV QT_QPA_PLATFORM=linuxfb +{% endif %} # Turn on debug logging for now #ENV QT_LOGGING_RULES=qt.qpa.*=true diff --git a/static/src/components/home.tsx b/static/src/components/home.tsx index 9201b419f..cf8984905 100644 --- a/static/src/components/home.tsx +++ b/static/src/components/home.tsx @@ -7,7 +7,8 @@ import { selectActiveAssets, selectInactiveAssets, } from '@/store/assets' -import { AssetEditData, AppDispatch } from '@/types' +import { fetchDeviceModel } from '@/store/settings' +import { AssetEditData, AppDispatch, RootState } from '@/types' import { EmptyAssetMessage } from '@/components/empty-asset-message' import { InactiveAssetsTable } from '@/components/inactive-assets' @@ -24,6 +25,8 @@ export const ScheduleOverview = () => { const [assetToEdit, setAssetToEdit] = useState(null) const [playerName, setPlayerName] = useState('') + const { deviceModel } = useSelector((state: RootState) => state.settings) + const fetchPlayerName = async () => { try { const response = await fetch('/api/v2/device_settings') @@ -41,6 +44,10 @@ export const ScheduleOverview = () => { fetchPlayerName() }, [dispatch, playerName]) + useEffect(() => { + dispatch(fetchDeviceModel()) + }, [dispatch]) + // Initialize tooltips useEffect(() => { const tooltipElements: Tooltip[] = [] diff --git a/tools/image_builder/utils.py b/tools/image_builder/utils.py index fada8c921..7bd57bffe 100644 --- a/tools/image_builder/utils.py +++ b/tools/image_builder/utils.py @@ -223,6 +223,15 @@ def get_viewer_context(board: str) -> dict: ] if board in ['pi5', 'x86']: + if board == 'x86': + apt_dependencies.extend([ + 'xserver-xorg-core', + 'xserver-xorg-video-fbdev', + 'x11-xserver-utils', + 'xauth', + 'xinit', + ]) + apt_dependencies.extend([ 'qt6-base-dev', 'qt6-webengine-dev', diff --git a/viewer/media_player.py b/viewer/media_player.py index 9a330fba7..d2ea44d6d 100644 --- a/viewer/media_player.py +++ b/viewer/media_player.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +from os import getenv + import sh import vlc @@ -25,6 +27,23 @@ def stop(self): def is_playing(self): raise NotImplementedError + def get_alsa_audio_device(self): + if settings['audio_output'] == 'local': + if get_device_type() == 'pi5': + return 'default:CARD=vc4hdmi0' + elif get_device_type() == 'x86': + return 'default:CARD=HID' + return 'plughw:CARD=Headphones' + else: + if get_device_type() in ['pi4', 'pi5']: + return 'default:CARD=vc4hdmi0' + elif get_device_type() in ['pi1', 'pi2', 'pi3']: + return 'default:CARD=vc4hdmi' + elif get_device_type() == 'x86': + return 'hdmi:CARD=PCH' + else: + return 'default:CARD=HID' + class FFMPEGMediaPlayer(MediaPlayer): def __init__(self): @@ -38,6 +57,10 @@ def set_asset(self, uri, duration): self.player_kwargs = { '_bg': True, '_ok_code': [0, 124], + '_env': { + 'AUDIODEV': self.get_alsa_audio_device(), + 'DISPLAY': getenv('DISPLAY', ':0') + } } def play(self): @@ -66,20 +89,6 @@ def __init__(self): self.player.audio_output_set('alsa') - def get_alsa_audio_device(self): - if settings['audio_output'] == 'local': - if get_device_type() == 'pi5': - return 'default:CARD=vc4hdmi0' - - return 'plughw:CARD=Headphones' - else: - if get_device_type() in ['pi4', 'pi5']: - return 'default:CARD=vc4hdmi0' - elif get_device_type() in ['pi1', 'pi2', 'pi3']: - return 'default:CARD=vc4hdmi' - else: - return 'default:CARD=HID' - def __get_options(self): return [ f'--alsa-audio-device={self.get_alsa_audio_device()}',