diff --git a/bin/start_viewer.sh b/bin/start_viewer.sh
index cffd457ac..405b92f5f 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" = "pi5" ]; 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..813987d32 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 == 'pi5' %}
+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 fada8c921..bd831c7b2 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 == 'pi5':
+            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()}',
