diff --git a/README.md b/README.md index ae9a341..7f1e1f5 100644 --- a/README.md +++ b/README.md @@ -426,6 +426,7 @@ keytool -genkey -v -keystore android/release-key.jks -keyalg RSA -keysize 2048 - 3. Install the ScreenTinker app on your device: - **Android TV / tablets**: Download the APK from your instance (`/download/apk`) or build it from source (see above) - **Raspberry Pi**: `curl -sSL https://your-instance/scripts/raspberry-pi-setup.sh | bash` + - **Debian 13 (headless)**: `curl -sSL https://your-instance/scripts/debian-13-setup.sh | sudo bash` - **Windows**: Run the setup script from `scripts/windows-setup.bat` - **Samsung Tizen TV / signage**: point the TV's URL Launcher (or browser) at `https://your-instance/player` - no signing needed. For an installed native app, see [tizen/README.md](tizen/README.md) - **Any browser**: Open `https://your-instance/player` in kiosk/fullscreen mode diff --git a/scripts/debian-13-setup.sh b/scripts/debian-13-setup.sh new file mode 100755 index 0000000..73d68be --- /dev/null +++ b/scripts/debian-13-setup.sh @@ -0,0 +1,549 @@ +#!/bin/bash +# ScreenTinker - Debian 13 Setup Script +# +# Modes: +# - Server + Player (both) +# - Server only +# - Player only +# +# Usage: +# curl -sSL https://screentinker.com/scripts/debian-13-setup.sh | sudo bash +# curl -sSL https://screentinker.com/scripts/debian-13-setup.sh | sudo bash -s -- --server-only +# curl -sSL https://screentinker.com/scripts/debian-13-setup.sh | sudo bash -s -- --player-only https://screentinker.com + +set -euo pipefail + +# -- Configuration -- +SCREENTINKER_DIR="/opt/screentinker" +SCREENTINKER_PORT=3001 +NODE_MAJOR=20 +LOG_FILE="/var/log/screentinker-debian-setup.log" + +# -- Colors -- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log() { echo -e "${GREEN}[ScreenTinker]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARNING]${NC} $1"; } +err() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +MODE="both" +MODE_SET=false +SERVER_URL="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --server-only) + MODE="server" + MODE_SET=true + shift + ;; + --player-only) + MODE="player" + MODE_SET=true + shift + if [[ $# -gt 0 && "$1" == http* ]]; then + SERVER_URL="$1" + shift + fi + ;; + --both) + MODE="both" + MODE_SET=true + shift + ;; + --help|-h) + echo "Usage: sudo ./debian-13-setup.sh [OPTIONS] [SERVER_URL]" + echo "" + echo "Options:" + echo " --server-only Install only the server" + echo " --player-only [URL] Install only the player (URL required)" + echo " --both Install both server and player (default)" + echo " --help Show this help" + echo "" + echo "Examples:" + echo " sudo ./debian-13-setup.sh" + echo " sudo ./debian-13-setup.sh --server-only" + echo " sudo ./debian-13-setup.sh --player-only https://screentinker.com" + exit 0 + ;; + http*) + SERVER_URL="$1" + shift + ;; + *) + shift + ;; + esac +done + +if [ "$(id -u)" -ne 0 ]; then + err "This script must be run as root. Try: sudo bash debian-13-setup.sh" +fi + +if [ -r /etc/os-release ]; then + . /etc/os-release + if [ "${ID:-}" != "debian" ] || [ "${VERSION_ID:-}" != "13" ]; then + warn "Detected ${PRETTY_NAME:-unknown}. This script targets Debian 13." + read -p "Continue anyway? (y/N) " -n 1 -r; echo + [[ ! $REPLY =~ ^[Yy]$ ]] && exit 1 + else + log "Detected Debian 13" + fi +fi + +if [ "$MODE" = "player" ] && [ -z "$SERVER_URL" ]; then + echo "" + echo -e "${BLUE}======================================${NC}" + echo -e "${BLUE} ScreenTinker Debian 13 Setup${NC}" + echo -e "${BLUE}======================================${NC}" + echo "" + read -p "Server URL (e.g., https://screentinker.com): " SERVER_URL +elif [ "$MODE" = "both" ] && [ "$MODE_SET" = false ] && [ -z "$SERVER_URL" ]; then + echo "" + echo -e "${BLUE}======================================${NC}" + echo -e "${BLUE} ScreenTinker Debian 13 Setup${NC}" + echo -e "${BLUE}======================================${NC}" + echo "" + echo " 1) Server + Player (recommended for single-screen host)" + echo " 2) Server Only" + echo " 3) Player Only" + echo "" + read -p "Choose [1/2/3]: " MODE_CHOICE + case "$MODE_CHOICE" in + 2) + MODE="server" + ;; + 3) + MODE="player" + read -p "Server URL (e.g., https://screentinker.com): " SERVER_URL + ;; + *) + MODE="both" + ;; + esac +fi + +SERVER_URL="${SERVER_URL%/}" + +NEED_SERVER=false +NEED_PLAYER=false + +case "$MODE" in + server) + NEED_SERVER=true + ;; + player) + NEED_PLAYER=true + ;; + both) + NEED_SERVER=true + NEED_PLAYER=true + ;; + *) + err "Unknown mode: $MODE" + ;; +esac + +if [ "$NEED_PLAYER" = true ] && [ "$MODE" = "player" ] && [ -z "$SERVER_URL" ]; then + err "Player-only mode requires a server URL" +fi + +if [ "$NEED_PLAYER" = true ]; then + if [ "$MODE" = "player" ]; then + KIOSK_URL="${SERVER_URL}/player" + else + KIOSK_URL="http://localhost:${SCREENTINKER_PORT}/player" + fi +fi + +echo "" +log "Setup log: $LOG_FILE" +exec > >(tee -a "$LOG_FILE") 2>&1 + +log "Updating system packages..." +apt-get update -qq +apt-get upgrade -y -qq + +log "Installing base dependencies..." +apt-get install -y -qq \ + git curl wget unzip htop \ + avahi-daemon \ + fonts-liberation fonts-noto-color-emoji \ + >> "$LOG_FILE" 2>&1 + +RUNTIME_USER="${SUDO_USER:-$(logname 2>/dev/null || echo root)}" +if ! id "$RUNTIME_USER" &>/dev/null; then + warn "Could not resolve invoking user; defaulting to root" + RUNTIME_USER="root" +fi +RUNTIME_HOME=$(eval echo "~$RUNTIME_USER") + +if [ "$NEED_SERVER" = true ]; then + NEED_NODE=true + if command -v node &>/dev/null; then + CUR=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$CUR" -ge "$NODE_MAJOR" ]; then + log "Node.js $(node -v) already installed" + NEED_NODE=false + fi + fi + + if [ "$NEED_NODE" = true ]; then + log "Installing Node.js ${NODE_MAJOR}.x..." + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >> "$LOG_FILE" 2>&1 + apt-get install -y -qq nodejs >> "$LOG_FILE" 2>&1 + log "Node.js $(node -v) installed" + fi + + if [ -d "$SCREENTINKER_DIR/.git" ]; then + log "Repo exists at $SCREENTINKER_DIR, pulling latest..." + cd "$SCREENTINKER_DIR" && git pull origin main >> "$LOG_FILE" 2>&1 + else + log "Cloning ScreenTinker..." + git clone https://github.com/screentinker/screentinker.git "$SCREENTINKER_DIR" >> "$LOG_FILE" 2>&1 + fi + + log "Installing server dependencies..." + cd "$SCREENTINKER_DIR/server" + npm install --production >> "$LOG_FILE" 2>&1 + + mkdir -p "$SCREENTINKER_DIR/server/db" + mkdir -p "$SCREENTINKER_DIR/server/uploads" + chown -R "$RUNTIME_USER":"$RUNTIME_USER" "$SCREENTINKER_DIR" + + log "Creating screentinker-server service..." + cat > /etc/systemd/system/screentinker-server.service << SERVICEEOF +[Unit] +Description=ScreenTinker Digital Signage Server +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=${RUNTIME_USER} +WorkingDirectory=${SCREENTINKER_DIR}/server +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=5 +StartLimitBurst=5 +StartLimitIntervalSec=60 + +Environment=NODE_ENV=production +Environment=PORT=${SCREENTINKER_PORT} +Environment=SELF_HOSTED=true +Environment=HOST=0.0.0.0 + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=screentinker-server + +[Install] +WantedBy=multi-user.target +SERVICEEOF + + systemctl daemon-reload + systemctl enable screentinker-server.service + log "Server service enabled" +fi + +if [ "$NEED_PLAYER" = true ]; then + log "Installing player packages..." + apt-get install -y -qq \ + xserver-xorg xserver-xorg-legacy x11-xserver-utils xinit \ + chromium unclutter xdotool \ + >> "$LOG_FILE" 2>&1 || { + warn "Failed to install chromium package, trying chromium-browser..." + apt-get install -y -qq xserver-xorg xserver-xorg-legacy x11-xserver-utils xinit chromium-browser unclutter xdotool >> "$LOG_FILE" 2>&1 + } + + CHROMIUM_BIN=$(command -v chromium 2>/dev/null || command -v chromium-browser 2>/dev/null || echo "/usr/bin/chromium") + + log "Allowing non-root X server startup..." + mkdir -p /etc/X11 + cat > /etc/X11/Xwrapper.config << 'XWRAPEOF' +allowed_users=anybody +needs_root_rights=yes +XWRAPEOF + + log "Creating kiosk launcher..." + cat > "$RUNTIME_HOME/screentinker-kiosk.sh" << KIOSKEOF +#!/bin/bash +KIOSK_URL="${KIOSK_URL}" + +sleep 2 + +# Disable screen blanking and power management +xset s off +xset s noblank +xset -dpms +xset s 0 0 + +# Hide cursor after 3 seconds of inactivity +unclutter -idle 3 -root & + +# Clean Chromium crash flags (prevents restore session dialogs) +CDIR="\$HOME/.config/chromium/Default" +mkdir -p "\$CDIR" +if [ -f "\$CDIR/Preferences" ]; then + sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' "\$CDIR/Preferences" 2>/dev/null || true + sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' "\$CDIR/Preferences" 2>/dev/null || true +fi + +# Wait for local server if running all-in-one +if echo "\$KIOSK_URL" | grep -q "localhost"; then + echo "Waiting for ScreenTinker server..." + for i in \$(seq 1 60); do + if curl -sf "http://localhost:${SCREENTINKER_PORT}/api/status" >/dev/null 2>&1; then + echo "Server ready after \${i}x2s" + break + fi + sleep 2 + done +fi + +# Detect screen resolution so Chromium fills the display on minimal X11 (no WM) +SCREEN_RES=\$(xrandr 2>/dev/null | grep ' connected' | grep -oE '[0-9]+x[0-9]+' | head -1) +SCREEN_W=\${SCREEN_RES%%x*} +SCREEN_H=\${SCREEN_RES##*x} +if [ -z "\$SCREEN_W" ] || [ -z "\$SCREEN_H" ]; then + SCREEN_W=1920 + SCREEN_H=1080 +fi + +exec ${CHROMIUM_BIN} \\ + --kiosk \\ + --window-position=0,0 \\ + --window-size=\${SCREEN_W},\${SCREEN_H} \\ + --noerrdialogs \\ + --disable-infobars \\ + --disable-session-crashed-bubble \\ + --disable-features=TranslateUI \\ + --disable-component-update \\ + --check-for-update-interval=31536000 \\ + --autoplay-policy=no-user-gesture-required \\ + --no-first-run \\ + --disable-pinch \\ + --overscroll-history-navigation=0 \\ + --disable-translate \\ + --disable-sync \\ + --disable-background-networking \\ + --disable-default-apps \\ + --disable-extensions \\ + --disable-hang-monitor \\ + --disable-popup-blocking \\ + --disable-prompt-on-repost \\ + --metrics-recording-only \\ + --safebrowsing-disable-auto-update \\ + --ignore-certificate-errors \\ + "\$KIOSK_URL" +KIOSKEOF + + chmod +x "$RUNTIME_HOME/screentinker-kiosk.sh" + chown "$RUNTIME_USER":"$RUNTIME_USER" "$RUNTIME_HOME/screentinker-kiosk.sh" + + cat > "$RUNTIME_HOME/.xinitrc" << 'XINITEOF' +#!/bin/bash +exec ~/screentinker-kiosk.sh +XINITEOF + chmod +x "$RUNTIME_HOME/.xinitrc" + chown "$RUNTIME_USER":"$RUNTIME_USER" "$RUNTIME_HOME/.xinitrc" + + if [ "$NEED_SERVER" = true ]; then + KIOSK_AFTER="After=screentinker-server.service" + KIOSK_REQ="Requires=screentinker-server.service" + else + KIOSK_AFTER="After=network-online.target" + KIOSK_REQ="Wants=network-online.target" + fi + + log "Creating kiosk service..." + cat > /etc/systemd/system/screentinker-kiosk.service << SERVICEEOF +[Unit] +Description=ScreenTinker Kiosk Display +${KIOSK_AFTER} +${KIOSK_REQ} +# Prevent conflicts with getty on tty1 +Conflicts=getty@tty1.service +After=getty@tty1.service + +[Service] +Type=simple +User=${RUNTIME_USER} +Environment=DISPLAY=:0 +Environment=XAUTHORITY=${RUNTIME_HOME}/.Xauthority +# Remove stale X lock files from previous crashes before starting +ExecStartPre=/bin/bash -c 'rm -f /tmp/.X0-lock /tmp/.X11-unix/X0' +ExecStartPre=/bin/sleep 3 +ExecStart=/usr/bin/startx ${RUNTIME_HOME}/.xinitrc -- :0 -nolisten tcp vt1 +Restart=on-failure +RestartSec=10 +StartLimitBurst=5 +StartLimitIntervalSec=120 + +TTYPath=/dev/tty1 +StandardInput=tty +StandardOutput=journal +StandardError=journal +SyslogIdentifier=screentinker-kiosk + +[Install] +WantedBy=multi-user.target +SERVICEEOF + + systemctl daemon-reload + systemctl enable screentinker-kiosk.service + log "Kiosk service enabled" + + log "Configuring auto-login on tty1..." + mkdir -p /etc/systemd/system/getty@tty1.service.d + cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << AUTOLOGINEOF +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin ${RUNTIME_USER} --noclear %I \$TERM +AUTOLOGINEOF + + # Disable getty on tty1 so it doesn't conflict with the kiosk service + systemctl disable getty@tty1.service 2>/dev/null || true + systemctl mask getty@tty1.service 2>/dev/null || true +fi + +if [ "$NEED_SERVER" = true ]; then + log "Creating management scripts..." + + cat > /usr/local/bin/screentinker-update << 'UPDATEEOF' +#!/bin/bash +echo "Stopping services..." +sudo systemctl stop screentinker-kiosk.service 2>/dev/null || true +sudo systemctl stop screentinker-server.service 2>/dev/null || true + +echo "Pulling latest..." +cd /opt/screentinker && git pull origin main + +echo "Installing dependencies..." +cd server && npm install --production + +echo "Starting services..." +sudo systemctl start screentinker-server.service +if systemctl list-unit-files | grep -q '^screentinker-kiosk.service'; then + sleep 3 + sudo systemctl start screentinker-kiosk.service +fi + +echo "" +echo "Done! Server: $(systemctl is-active screentinker-server.service)" +if systemctl list-unit-files | grep -q '^screentinker-kiosk.service'; then + echo " Kiosk: $(systemctl is-active screentinker-kiosk.service)" +fi +UPDATEEOF + chmod +x /usr/local/bin/screentinker-update + + cat > /usr/local/bin/screentinker-status << 'STATUSEOF' +#!/bin/bash +echo "" +echo "=== ScreenTinker Status ===" +echo "" +IP=$(hostname -I | awk '{print $1}') + +if systemctl is-active screentinker-server.service &>/dev/null; then + echo "Server: RUNNING (PID $(systemctl show screentinker-server.service -p MainPID --value))" +else + echo "Server: STOPPED" +fi + +if systemctl list-unit-files | grep -q '^screentinker-kiosk.service'; then + if systemctl is-active screentinker-kiosk.service &>/dev/null; then + echo "Kiosk: RUNNING" + else + echo "Kiosk: STOPPED" + fi +fi + +echo "" +echo "Uptime: $(uptime -p)" +echo "Disk: $(df -h /opt/screentinker 2>/dev/null | tail -1 | awk '{print $3 "/" $2 " (" $5 " used)"}')" +echo "Memory: $(free -h | awk '/Mem:/ {print $3 " / " $2}')" +echo "" +echo "Dashboard: http://${IP}:3001" +echo "Player: http://${IP}:3001/player" +echo "mDNS: http://$(hostname).local:3001" +echo "" +STATUSEOF + chmod +x /usr/local/bin/screentinker-status + + cat > /usr/local/bin/screentinker-logs << 'LOGSEOF' +#!/bin/bash +case "${1:-server}" in + server) journalctl -u screentinker-server.service -f --no-hostname ;; + kiosk) journalctl -u screentinker-kiosk.service -f --no-hostname ;; + all) journalctl -u screentinker-server.service -u screentinker-kiosk.service -f --no-hostname ;; + *) echo "Usage: screentinker-logs [server|kiosk|all]" ;; +esac +LOGSEOF + chmod +x /usr/local/bin/screentinker-logs +fi + +cat > /etc/motd << 'MOTDEOF' + + ____ _____ _ + / ___| ___ _ __ ___ ___ |_ _|_ _ __ | | _____ _ __ + \___ \ / __| '__/ _ \/ _ \ | || | '_ \| |/ / _ \ '__| + ___) | (__| | | __/ __/ | || | | | | < __/ | + |____/ \___|_| \___|\___| |_||_|_| |_|_|\_\___|_| + + Open-Source Digital Signage for Any Screen + + Commands: + screentinker-status Show system info and URLs + screentinker-update Pull latest and restart + screentinker-logs Follow logs (server|kiosk|all) + +MOTDEOF + +if grep -q "#RuntimeWatchdogSec=0" /etc/systemd/system.conf 2>/dev/null; then + sed -i 's/#RuntimeWatchdogSec=0/RuntimeWatchdogSec=10/' /etc/systemd/system.conf + log "Hardware watchdog enabled (10s)" +fi + +# Disable console blanking so the screen stays on during boot +if [ -f /etc/default/grub ]; then + if ! grep -q "consoleblank=0" /etc/default/grub; then + sed -i 's/GRUB_CMDLINE_LINUX_DEFAULT="\(.*\)"/GRUB_CMDLINE_LINUX_DEFAULT="\1 consoleblank=0"/' /etc/default/grub + update-grub >> "$LOG_FILE" 2>&1 && log "Console blanking disabled in GRUB" || warn "update-grub failed (non-fatal)" + fi +fi + +echo "" +echo -e "${GREEN}======================================${NC}" +echo -e "${GREEN} ScreenTinker Setup Complete!${NC}" +echo -e "${GREEN}======================================${NC}" +echo "" + +IP=$(hostname -I | awk '{print $1}') + +if [ "$MODE" = "both" ]; then + echo "Mode: Server + Player" + echo "Dashboard: http://${IP}:${SCREENTINKER_PORT}" + echo "Player: http://${IP}:${SCREENTINKER_PORT}/player" +elif [ "$MODE" = "server" ]; then + echo "Mode: Server Only" + echo "Dashboard: http://${IP}:${SCREENTINKER_PORT}" +else + echo "Mode: Player Only" + echo "Server: $SERVER_URL" +fi + +echo "" +echo "Services:" +if [ "$NEED_SERVER" = true ]; then + echo " sudo systemctl [start|stop|restart] screentinker-server" +fi +if [ "$NEED_PLAYER" = true ]; then + echo " sudo systemctl [start|stop|restart] screentinker-kiosk" +fi +echo "" +echo -e "${YELLOW}Reboot to start: sudo reboot${NC}" +echo "" diff --git a/scripts/raspberry-pi-setup.sh b/scripts/raspberry-pi-setup.sh index 61a4a08..f7b1baf 100644 --- a/scripts/raspberry-pi-setup.sh +++ b/scripts/raspberry-pi-setup.sh @@ -280,7 +280,7 @@ fi if echo "\$KIOSK_URL" | grep -q "localhost"; then echo "Waiting for ScreenTinker server..." for i in \$(seq 1 30); do - if curl -sf "http://localhost:${SCREENTINKER_PORT}/api/health" >/dev/null 2>&1; then + if curl -sf "http://localhost:${SCREENTINKER_PORT}/api/status" >/dev/null 2>&1; then echo "Server ready" break fi @@ -288,8 +288,19 @@ if echo "\$KIOSK_URL" | grep -q "localhost"; then done fi +# Detect screen resolution so Chromium fills the display on minimal X11 (no WM) +SCREEN_RES=\$(xrandr 2>/dev/null | grep ' connected' | grep -oE '[0-9]+x[0-9]+' | head -1) +SCREEN_W=\${SCREEN_RES%%x*} +SCREEN_H=\${SCREEN_RES##*x} +if [ -z "\$SCREEN_W" ] || [ -z "\$SCREEN_H" ]; then + SCREEN_W=1920 + SCREEN_H=1080 +fi + exec ${CHROMIUM_BIN} \\ --kiosk \\ + --window-position=0,0 \\ + --window-size=\${SCREEN_W},\${SCREEN_H} \\ --noerrdialogs \\ --disable-infobars \\ --disable-session-crashed-bubble \\ @@ -298,7 +309,6 @@ exec ${CHROMIUM_BIN} \\ --check-for-update-interval=31536000 \\ --autoplay-policy=no-user-gesture-required \\ --no-first-run \\ - --start-fullscreen \\ --disable-pinch \\ --overscroll-history-navigation=0 \\ --disable-translate \\