Turning a Raspberry Pi into digital signage — maximizing Chromium with Playwright automation

2026-02-18 03:00 (3 months ago)
Cookie Velcro
Play a song themed on this article

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-fullscreen option

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 --kiosk flag 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 .desktop file
  • Dependencies are managed simply with uv
  • A scp-based deploy script makes it easy to deploy from a Mac to the Raspberry Pi
Please rate this article (No signup or login required)
Currently unrated
The author runs the application development company Cyberneura.
We look forward to discussing your development needs.

Categories

Archive