From f6f87bd84c2469353c2162df014a41d5b8c3f984 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 28 Jul 2025 11:57:49 -0700 Subject: [PATCH 1/9] feat: use X11 for x86 devices --- bin/start_viewer.sh | 68 +++++++++++++++++++++++++++++++----- docker/Dockerfile.viewer.j2 | 6 +++- tools/image_builder/utils.py | 9 +++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh index cffd457ac..7c6e6028b 100755 --- a/bin/start_viewer.sh +++ b/bin/start_viewer.sh @@ -38,22 +38,72 @@ trap '' 16 # Disable swapping echo 0 > /sys/fs/cgroup/memory/memory.swappiness +# TODO: Only run X11 if it's an x86 device. + +# 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 +export DISPLAY=:0 +Xorg "$DISPLAY" -s 0 dpms & +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 + # 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/tools/image_builder/utils.py b/tools/image_builder/utils.py index 8416181d4..900a4710d 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', From 04261162238ad5c7e2b4adb2772cf06852c026cd Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 28 Jul 2025 12:35:53 -0700 Subject: [PATCH 2/9] chore: modify `deploy_to_balena.sh` to support x86 devices --- bin/deploy_to_balena.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 From a6a81ba41c44173cd3225161b1750a4ec2db34e5 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Mon, 28 Jul 2025 22:59:09 -0700 Subject: [PATCH 3/9] fix: only execute X11 setup if device type is x86 --- bin/start_viewer.sh | 72 ++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh index 7c6e6028b..27c599c87 100755 --- a/bin/start_viewer.sh +++ b/bin/start_viewer.sh @@ -38,47 +38,47 @@ trap '' 16 # Disable swapping echo 0 > /sys/fs/cgroup/memory/memory.swappiness -# TODO: Only run X11 if it's an x86 device. - -# 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 -export DISPLAY=:0 -Xorg "$DISPLAY" -s 0 dpms & -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" +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 + export DISPLAY=:0 + Xorg "$DISPLAY" -s 0 dpms & + 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 - 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 + # Now that X is ready, configure display settings + xset -display "$DISPLAY" s off + xset -display "$DISPLAY" s noblank + xset -display "$DISPLAY" -dpms fi -# Now that X is ready, configure display settings -xset -display "$DISPLAY" s off -xset -display "$DISPLAY" s noblank -xset -display "$DISPLAY" -dpms - # Start viewer sudo -E -u viewer dbus-run-session python -m viewer & VIEWER_PID=$! From 9b281a8912a9bfa656fa2cd91c54e83317d9ea02 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Fri, 1 Aug 2025 11:16:25 -0700 Subject: [PATCH 4/9] chore: temporarily run Docker image builds during pull requests --- .github/workflows/docker-build.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 26a75289d..4115d0dc1 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -1,7 +1,8 @@ name: Docker Image Build on: - push: + # TODO: Don't forget to revert this to `push` after testing. + pull_request: branches: - master paths: @@ -106,19 +107,16 @@ jobs: poetry run python -m tools.image_builder \ --build-target=pi4 \ --target-platform=linux/arm/v8 \ - --service=${{ matrix.service }} \ - --push + --service=${{ matrix.service }} elif [ "${{ matrix.board }}" == "pi4-64" ]; then poetry run python -m tools.image_builder \ --build-target=pi4 \ --target-platform=linux/arm64/v8 \ - --service=${{ matrix.service }} \ - --push + --service=${{ matrix.service }} else poetry run python -m tools.image_builder \ --build-target=${{ matrix.board }} \ - --service=${{ matrix.service }} \ - --push + --service=${{ matrix.service }} fi - name: Inspect cache after build From 8df0ad51d8a05a04e8a48affab138b60c248c4b6 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Fri, 1 Aug 2025 13:21:54 -0700 Subject: [PATCH 5/9] chore: revert temporary changes to GitHub workflows --- .github/workflows/docker-build.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index 4115d0dc1..26a75289d 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -1,8 +1,7 @@ name: Docker Image Build on: - # TODO: Don't forget to revert this to `push` after testing. - pull_request: + push: branches: - master paths: @@ -107,16 +106,19 @@ jobs: poetry run python -m tools.image_builder \ --build-target=pi4 \ --target-platform=linux/arm/v8 \ - --service=${{ matrix.service }} + --service=${{ matrix.service }} \ + --push elif [ "${{ matrix.board }}" == "pi4-64" ]; then poetry run python -m tools.image_builder \ --build-target=pi4 \ --target-platform=linux/arm64/v8 \ - --service=${{ matrix.service }} + --service=${{ matrix.service }} \ + --push else poetry run python -m tools.image_builder \ --build-target=${{ matrix.board }} \ - --service=${{ matrix.service }} + --service=${{ matrix.service }} \ + --push fi - name: Inspect cache after build From 3beb8f6a57ae5a7d65381397d9c877e1abfe5ba6 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Fri, 1 Aug 2025 15:49:28 -0700 Subject: [PATCH 6/9] fix: use VLC for video playback in x86 devices --- viewer/media_player.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/viewer/media_player.py b/viewer/media_player.py index 9a330fba7..4d496b5e6 100644 --- a/viewer/media_player.py +++ b/viewer/media_player.py @@ -70,6 +70,8 @@ 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: @@ -77,6 +79,8 @@ def get_alsa_audio_device(self): 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' @@ -108,7 +112,7 @@ class MediaPlayerProxy(): @classmethod def get_instance(cls): if cls.INSTANCE is None: - if get_device_type() in ['pi1', 'pi2', 'pi3', 'pi4']: + if get_device_type() in ['pi1', 'pi2', 'pi3', 'pi4', 'x86']: cls.INSTANCE = VLCMediaPlayer() else: cls.INSTANCE = FFMPEGMediaPlayer() From 8ff114743ebfd8593cdc7b210f3c26583a875cf1 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Fri, 1 Aug 2025 16:23:19 -0700 Subject: [PATCH 7/9] fix: hide "Previous Asset" and "Next Asset" buttons for x86 devices --- static/src/components/home.tsx | 69 ++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/static/src/components/home.tsx b/static/src/components/home.tsx index 13fedcdb7..d9d27b28d 100644 --- a/static/src/components/home.tsx +++ b/static/src/components/home.tsx @@ -8,7 +8,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' @@ -29,6 +30,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') @@ -46,6 +49,10 @@ export const ScheduleOverview = () => { fetchPlayerName() }, [dispatch, playerName]) + useEffect(() => { + dispatch(fetchDeviceModel()) + }, [dispatch]) + // Initialize tooltips useEffect(() => { const initializeTooltips = () => { @@ -133,34 +140,38 @@ export const ScheduleOverview = () => { Schedule Overview
- - - Previous Asset - - - Next Asset - - + {deviceModel !== 'Generic x86_64 Device' && ( + <> + + + Previous Asset + + + Next Asset + + + + )} Date: Sat, 2 Aug 2025 22:01:45 -0700 Subject: [PATCH 8/9] fix: hide cursor on start of video playback on x86 devices --- bin/start_viewer.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh index 27c599c87..b5cef8110 100755 --- a/bin/start_viewer.sh +++ b/bin/start_viewer.sh @@ -43,9 +43,9 @@ if [ "$DEVICE_TYPE" = "x86" ]; then pkill Xorg rm -f /tmp/.X0-lock /tmp/.X11-unix/X0 - # Start X server with dummy video driver + # Start X server with dummy video driver and cursor disabled export DISPLAY=:0 - Xorg "$DISPLAY" -s 0 dpms & + Xorg "$DISPLAY" -s 0 dpms -nocursor & XORG_PID=$! # Wait for X server to be ready with timeout @@ -77,6 +77,11 @@ if [ "$DEVICE_TYPE" = "x86" ]; then 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 From d136607ed580c98641bfb1b6b3d625c2f766b3a0 Mon Sep 17 00:00:00 2001 From: nicomiguelino Date: Sat, 2 Aug 2025 22:27:37 -0700 Subject: [PATCH 9/9] chore: revert back to using FFMPEG for x86 devices --- viewer/media_player.py | 43 +++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/viewer/media_player.py b/viewer/media_player.py index 4d496b5e6..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,24 +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' - 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' - def __get_options(self): return [ f'--alsa-audio-device={self.get_alsa_audio_device()}', @@ -112,7 +117,7 @@ class MediaPlayerProxy(): @classmethod def get_instance(cls): if cls.INSTANCE is None: - if get_device_type() in ['pi1', 'pi2', 'pi3', 'pi4', 'x86']: + if get_device_type() in ['pi1', 'pi2', 'pi3', 'pi4']: cls.INSTANCE = VLCMediaPlayer() else: cls.INSTANCE = FFMPEGMediaPlayer()