Turning a Raspberry Pi into digital signage — maximizing Chromium with Playwright automation
Overview
A setup was built to use a Raspberry Pi as a “signage terminal” that continuously displays an internal web application in full-screen mode.
For browser automation, Playwright (Python) is used, and full-screen display is achieved via Chromium’s --kiosk mode.
What this setup can do
- When the Raspberry Pi boots, Chromium automatically launches and displays the specified web page in full-screen mode
- Automatically authenticates even for pages that require login
- Persists cookies to keep the logged-in state even after reboot
- During development, can switch to windowed mode with the
--no-fullscreenoption
Environment
- Raspberry Pi 5 / Raspberry Pi OS (Debian bookworm)
- Python 3.13
- uv (Python package manager)
- Playwright
Setup
Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
Project dependencies
pyproject.toml:
[project]
name = "rpi-signage"
version = "0.1.0"
description = "Automated Web page launcher for Raspberry Pi using Playwright"
requires-python = "==3.13.*"
dependencies = [
"playwright>=1.57.0",
"python-dotenv>=1.2.1",
]
Install Playwright and Chromium
uv sync
uv run playwright install chromium
With playwright install chromium, the Chromium binary required for Playwright to automate the browser is downloaded. This is different from the Chromium preinstalled on Raspberry Pi OS.
However, if Chromium is already installed on the OS, that one is preferred (see get_browser_channel() below).
Python code
Main script
#!/usr/bin/env python3
import argparse
import json
import shutil
import time
from pathlib import Path
import dotenv
from playwright._impl._errors import TargetClosedError
from playwright.sync_api import BrowserContext, sync_playwright
COOKIES_FILE = Path(__file__).parent / "cookies.json"
def save_cookies(context: BrowserContext):
"""Cookie を JSON ファイルに保存する"""
try:
cookies = context.cookies()
COOKIES_FILE.write_text(json.dumps(cookies, ensure_ascii=False))
print(f"Saved {len(cookies)} cookies.")
except Exception as e:
print(f"Failed to save cookies: {e}")
def load_cookies(context: BrowserContext):
"""保存済みの Cookie を読み込んでブラウザコンテキストに適用する"""
if not COOKIES_FILE.exists():
return
try:
cookies = json.loads(COOKIES_FILE.read_text())
context.add_cookies(cookies)
print(f"Loaded {len(cookies)} cookies.")
except Exception as e:
print(f"Failed to load cookies: {e}")
def get_browser_channel() -> str | None:
"""OS にインストールされた Chromium/Chrome を検出する。
なければ None を返し、Playwright バンドル版を使用する。"""
if shutil.which("chromium") or shutil.which("chromium-browser"):
return "chromium"
if shutil.which("google-chrome") or shutil.which("chrome"):
return "chrome"
return None
def main(args):
with sync_playwright() as p:
channel = get_browser_channel()
chromium_args = [
"--window-size=1920,1040",
"--window-position=0,0",
]
if not args.no_fullscreen:
chromium_args.append("--kiosk")
context = p.chromium.launch_persistent_context(
user_data_dir="./browser_data",
headless=False,
channel=channel,
args=chromium_args,
no_viewport=True,
ignore_https_errors=True,
)
page = context.pages[0] if context.pages else context.new_page()
load_cookies(context)
page.goto("https://your-webapp.example.com/")
time.sleep(2)
# ログインページにリダイレクトされた場合の認証処理
if page.locator("#username").count() > 0:
print("Logging in...")
page.fill("#username", dotenv.get_key(".env", "USERNAME"))
page.fill("#password", dotenv.get_key(".env", "PASSWORD"))
page.click("button[type='submit']")
time.sleep(3)
save_cookies(context)
# Ctrl+C で終了するまで無限待機
print("Press Ctrl+C to exit.")
try:
while True:
time.sleep(2)
except KeyboardInterrupt:
save_cookies(context)
except TargetClosedError:
print("Browser window was closed.")
context.close()
if __name__ == "__main__":
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument(
"--no-fullscreen",
action="store_true",
help="フルスクリーンにしない。開発時に指定する。",
)
args = arg_parser.parse_args()
main(args)
Key points in the code
launch_persistent_context
Instead of Playwright’s standard launch(), launch_persistent_context() is used.
This persists the browser profile data (cache, local storage, etc.) under user_data_dir.
Cookie persistence
save_cookies() / load_cookies() save and restore cookies to/from a JSON file.
While launch_persistent_context preserves some session state, explicitly saving cookies makes it more reliable to restore the logged-in state.
get_browser_channel()
On Raspberry Pi OS, chromium-browser may be preinstalled.
Using shutil.which(), it detects Chromium/Chrome installed on the system and uses it if available; otherwise it falls back to Playwright’s bundled Chromium.
Native OS Chromium tends to perform better on a Raspberry Pi.
no_viewport=True
By default, Playwright fixes the viewport size. Setting no_viewport=True makes the viewport adjust dynamically to the window size. This is necessary for signage use where the entire display should be utilized.
Loading credentials from .env
Credentials such as passwords are stored in a .env file and loaded via python-dotenv.
USERNAME=your-username
PASSWORD=your-password
Trial and error for full-screen mode
Several approaches were tried to display Chromium in full-screen on the Raspberry Pi.
Approaches that didn’t work
1. xdotool key F11
A method that sends the F11 key with a delayed execution from a shell script.
sleep 10 && xdotool key F11 &
It wasn’t stable due to timing issues—sometimes the key was sent before the browser fully started, or focus wasn’t on the right window.
2. --start-maximized
A Chromium launch option. It maximizes the window, but the address bar and tab bar remain, which is insufficient for signage.
3. JavaScript requestFullscreen()
A method that runs JavaScript from Playwright to enter full-screen.
page.evaluate("document.documentElement.requestFullscreen()")
Programmatic calls without a user gesture (such as a click) may be blocked by the browser’s security policy.
Chosen approach: --kiosk
chromium_args.append("--kiosk")
Chromium’s --kiosk flag hides all UI elements including the address bar and tab bar, displaying the page in true full-screen mode.
It’s designed for kiosk terminals (public terminals) and is ideal for signage as well.
During development, adding the --no-fullscreen option disables --kiosk so behavior can be checked in a normal window.
# Normal launch (kiosk mode)
uv run ./open_signage.py
# During development (window mode)
uv run ./open_signage.py --no-fullscreen
Configure desktop autostart
Shell script
Shell script to run on autostart:
#!/usr/bin/env bash
set -e
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting signage"
cd "$(dirname "$0")"
export DISPLAY=:0
/home/pi/.local/bin/uv run ./open_signage.py
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Signage finished"
export DISPLAY=:0 is required to access the X Window System from an autostart environment that otherwise lacks GUI context.
.desktop file
By placing a .desktop file under ~/.config/autostart/, the script runs automatically when the Raspberry Pi desktop starts.
[Desktop Entry]
Type=Application
Name=Web Signage
Exec=/bin/bash -c "/home/pi/signage/autostart.sh >> /tmp/signage.log 2>&1"
Terminal=false
X-GNOME-Autostart-enabled=true
Since logs are redirected to /tmp/signage.log, check this file if any issues occur.
Deploying to the Raspberry Pi from a Mac
A simple deploy script that transfers files via scp:
#!/usr/bin/env zsh
set -e
cd "$(dirname "$0")"
FILES=(
autostart.sh
open_signage.py
.env
pyproject.toml
)
for FILE in "${FILES[@]}"; do
scp "$FILE" pi@rpi-signage.local:~/signage/
done
scp signage.desktop pi@rpi-signage.local:~/.config/autostart/signage.desktop
After deployment, rebooting the Raspberry Pi will autostart the updated script.
On the Raspberry Pi side, run the following once for initial setup:
mkdir -p ~/signage
mkdir -p ~/.config/autostart
cd ~/signage
uv sync
uv run playwright install chromium
Summary
- Built a signage terminal by automating Chromium with Playwright + Python
- The
--kioskflag is the most reliable way to force Chromium into full-screen - Cookie persistence enables automatic login even for pages that require authentication
- Autostart at OS boot is configured via a
.desktopfile - Dependencies are managed simply with
uv - A
scp-based deploy script makes it easy to deploy from a Mac to the Raspberry Pi
We look forward to discussing your development needs.