chore: release v1.0.7
CI / test (push) Successful in 9m11s
CI / docker (push) Failing after 2m39s
CI / release (push) Skipped
Release / release (push) Canceled after 2m11s

Playout: slate fallback, strict ffprobe validation, Prometheus metrics
Chat: history on join, DM offline delivery, env-config rate limits,
  Redis auto-reconnect, heartbeat, ban/mute enforcement, DM metrics
Viewer: presence indicators, schedule overlay, public /api/schedule
Admin: playlist drag-reorder, schedule overrides CRUD, user actions,
  inline mod buttons, CSV export, config editor, restart buttons,
  upload normalization gate, analytics time range
nginx: CORS preflight, JSON health, stub_status
Docs: test-lab vs production distinction across all configs/docs
Infra: Redis maxmemory, CD health IP-agnostic, slate volume mount
This commit is contained in:
zwitschi committed 2026-08-06 17:41:52 +02:00
1 parent 17ac04856b
commit 5b5cdc4cd8
26 files changed
+1723 -110

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Generate emergency slate video + pre-rendered HLS segments for failover.
# Output: data/slate/ (slate MP4 + HLS segments + manifest)
# Requires: ffmpeg
set -e
SLATE_DIR="${1:-data/slate}"
mkdir -p "$SLATE_DIR"
SLATE_MP4="$SLATE_DIR/slate.mp4"
SLATE_HLS="$SLATE_DIR/hls"
mkdir -p "$SLATE_HLS"
echo "=== Generating Emergency Slate ==="
# 1. Generate 10s slate video: color bars + "We'll Be Right Back" text
echo "[1/3] Generating slate video (10s, H.264/AAC, 30fps, 2s keyframes)..."
ffmpeg -y -f lavfi -i "color=c=#1a1a2e:s=1280x720:d=10:r=30" \
-f lavfi -i "sine=frequency=440:duration=10" \
-filter_complex "[0]drawtext=text='Linear Void':fontcolor=#587792:fontsize=60:x=(w-text_w)/2:y=(h-text_h)/2-40,drawtext=text='We'\''ll Be Right Back':fontcolor=#ede8e6:fontsize=36:x=(w-text_w)/2:y=(h-text_h)/2+30[bg];[1]volume=0.15[a]" \
-map "[bg]" -map "[a]" \
-c:v libx264 -preset medium -crf 21 \
-r 30 -g 60 -keyint_min 60 -sc_threshold 0 \
-pix_fmt yuv420p \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-movflags +faststart \
-t 10 \
"$SLATE_MP4"
echo " Slate MP4: $(du -h "$SLATE_MP4" | cut -f1)"
# 2. Verify slate meets normalization profile
echo "[2/3] Verifying slate normalization..."
ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt,sample_rate \
-of default=noprint_wrappers=1 "$SLATE_MP4" 2>&1 | head -10
# 3. Package as pre-rendered HLS (fMP4, 6s segments)
echo "[3/3] Packaging as HLS..."
ffmpeg -y -i "$SLATE_MP4" \
-c copy \
-f hls \
-hls_time 6 \
-hls_list_size 5 \
-hls_segment_type fmp4 \
-hls_fmp4_init_filename "init.mp4" \
-hls_flags "independent_segments" \
-hls_segment_filename "$SLATE_HLS/slate_segment_%03d.m4s" \
"$SLATE_HLS/slate.m3u8"
echo ""
echo "=== Slate Generated ==="
echo " MP4: $SLATE_MP4"
echo " Manifest: $SLATE_HLS/slate.m3u8"
echo " Segments: $(find "$SLATE_HLS" -name '*.m4s' | wc -l) files"
echo " Init: $SLATE_HLS/init.mp4"
echo ""
echo "To use: mount $SLATE_HLS as /app/slate in playout container"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Normalize video to Linear Void required profile.
# Output: H.264/AAC, 30fps constant, 2s keyframes, yuv420p, 48kHz audio.
# Usage: ./normalize.sh <input> [output]
set -e
INPUT="$1"
if [ -z "$INPUT" ]; then
echo "Usage: $0 <input-file> [output-file]"
echo " Normalizes video to Linear Void profile: H.264/AAC, 30fps, 2s keyframes"
exit 1
fi
if [ ! -f "$INPUT" ]; then
echo "Error: input file not found: $INPUT"
exit 1
fi
OUTPUT="${2:-${INPUT%.*}-normalized.mp4}"
CRF="${NORMALIZE_CRF:-21}"
PRESET="${NORMALIZE_PRESET:-medium}"
AUDIO_BITRATE="${NORMALIZE_AUDIO_BITRATE:-160k}"
echo "=== Linear Void Video Normalization ==="
echo " Input: $INPUT ($(du -h "$INPUT" | cut -f1))"
echo " Output: $OUTPUT"
echo " CRF: $CRF"
echo " Preset: $PRESET"
echo " Audio rate: $AUDIO_BITRATE"
echo ""
# Probe input
echo "--- Input Properties ---"
ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt,sample_rate,channels \
-of default=noprint_wrappers=1 "$INPUT" 2>&1
echo ""
# Normalize
echo "--- Normalizing ---"
ffmpeg -y -i "$INPUT" \
-c:v libx264 -preset "$PRESET" -crf "$CRF" \
-r 30 -g 60 -keyint_min 60 -sc_threshold 0 \
-pix_fmt yuv420p \
-c:a aac -b:a "$AUDIO_BITRATE" -ar 48000 -ac 2 \
-movflags +faststart \
"$OUTPUT"
echo ""
echo "--- Output Properties ---"
ffprobe -v error -show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt,sample_rate,channels \
-of default=noprint_wrappers=1 "$OUTPUT" 2>&1
echo ""
echo "=== Normalization Complete ==="
echo " Output: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
echo ""
echo "Verify with: ffprobe -v error -show_entries stream=codec_name,r_frame_rate,pix_fmt -of default=noprint_wrappers=1 $OUTPUT"
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# End-to-end playout test: single file → 3-file playlist → verify seamless transition
# Requires: Docker, ffprobe, curl. Run from repo root.
set -e
echo "=== Playout End-to-End Test ==="
# 1. Build playout image
echo "[1/7] Building playout image..."
docker compose build playout
# 2. Create test playlist with 3 demo files (reuse demodata if exists)
TEST_PLAYLIST="data/playlist/playlist.txt"
mkdir -p data/playlist data/hls
echo "[2/7] Creating 3-file test playlist..."
cat > "$TEST_PLAYLIST" << 'EOF'
file '/app/media/demo1.mp4'
file '/app/media/demo2.mp4'
file '/app/media/demo1.mp4'
EOF
echo "Playlist created with 3 entries:"
cat "$TEST_PLAYLIST"
# 3. Validate playlist parses correctly
echo "[3/7] Validating playlist parse..."
LINE_COUNT=$(grep -c "^file " "$TEST_PLAYLIST" || true)
if [ "$LINE_COUNT" -ne 3 ]; then
echo "FAIL: Expected 3 entries, got $LINE_COUNT"
exit 1
fi
echo "OK: $LINE_COUNT entries parsed"
# 4. Start playout container
echo "[4/7] Starting playout container..."
docker compose up -d playout
sleep 8
# 5. Verify health endpoint
echo "[5/7] Checking playout health..."
HEALTH=$(curl -sf http://localhost:8081/health || echo "FAIL")
if echo "$HEALTH" | grep -q "healthy"; then
echo "OK: Health endpoint: $HEALTH"
else
echo "FAIL: Health check returned: $HEALTH"
docker compose logs playout --tail 30
exit 1
fi
# 6. Verify HLS output
echo "[6/7] Verifying HLS output..."
sleep 10 # Let FFmpeg produce some segments
if [ -f "data/hls/playlist.m3u8" ]; then
echo "OK: playlist.m3u8 exists"
MANIFEST_LINES=$(wc -l < data/hls/playlist.m3u8)
echo " Manifest lines: $MANIFEST_LINES"
else
echo "FAIL: No playlist.m3u8 generated"
docker compose logs playout --tail 30
exit 1
fi
SEGMENT_COUNT=$(find data/hls -name "segment_*.m4s" | wc -l)
if [ "$SEGMENT_COUNT" -ge 1 ]; then
echo "OK: $SEGMENT_COUNT segment(s) generated"
else
echo "FAIL: No segments generated"
exit 1
fi
# 7. Check for seamless transition markers (no FFmpeg errors in logs)
echo "[7/7] Checking for seamless transitions..."
ERRORS=$(docker compose logs playout 2>&1 | grep -ci "error" || true)
if [ "$ERRORS" -eq 0 ]; then
echo "OK: No FFmpeg errors in logs"
else
echo "WARN: $ERRORS error(s) in playout logs (may be R2 connection — check manually)"
fi
echo ""
echo "=== Playout E2E Test PASSED ==="
echo "Verified: playlist parse → HLS generation → health endpoint → segment output"
echo ""
echo "Cleanup: docker compose down playout"