Adiciona auto-reload do Chromium via CDP por stream

---

- Adicionado scripts/autoreload.sh: loop com reload via WebSocket CDP raw (net + frames manuais), sem dependências externas; trap de SIGTERM encerra limpo sem aguardar o sleep;
- Adicionado [program:autoreload-{{STREAM_ID}}] em stream.template.conf com autostart=false e autorestart=unexpected;
- Adicionados campos AUTO_RELOAD e AUTO_RELOAD_INTERVAL em reprovision.mjs e supervisor.ts (provisionStream);
- Adicionados campos autoReload e autoReloadInterval em src/types/stream.ts;
- Adicionado autoreload nas listas de startStream e stopStream em supervisor.ts; adicionada função applyAutoReload;
- Adicionado endpoint dedicado POST /api/streams/[id]/autoreload: salva, re-provisiona e aplica sem reiniciar o stream inteiro;
- Adicionado toggle + input de intervalo (minutos) no menu de 3 pontos do card em StreamCard.tsx; toggle pill corrigido com posicionamento left absoluto;
- Atualizado README e CHANGELOG com a nova feature;

---
This commit is contained in:
2026-04-27 22:05:41 -03:00
parent 6315cd1312
commit 14094cf5ed
9 changed files with 180 additions and 5 deletions
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server"
import { getStream, saveStream } from "@/lib/db"
import { provisionStream, applyAutoReload } from "@/lib/supervisor"
type Ctx = { params: Promise<{ id: string }> }
export async function POST(req: Request, { params }: Ctx) {
const { id } = await params
const stream = getStream(id)
if (!stream) return NextResponse.json({ error: "not found" }, { status: 404 })
const { enabled, interval } = await req.json() as { enabled: boolean; interval?: number }
const updated = {
...stream,
autoReload: enabled,
...(interval !== undefined ? { autoReloadInterval: interval } : {}),
updatedAt: new Date().toISOString(),
}
saveStream(updated)
provisionStream(updated)
applyAutoReload(id)
return NextResponse.json(updated)
}
+50 -1
View File
@@ -1,7 +1,7 @@
"use client"
import { useState, useEffect, useRef } from "react"
import { MoreHorizontal, Play, Globe, Monitor, Pencil, RotateCcw, Square, Trash2, Circle, Copy, Check, Video, ImageUp, GripVertical, Wrench } from "lucide-react"
import { MoreHorizontal, Play, Globe, Monitor, Pencil, RotateCcw, Square, Trash2, Circle, Copy, Check, Video, ImageUp, GripVertical, Wrench, RefreshCw } from "lucide-react"
import { cn } from "@/lib/utils"
import { Toggle } from "@/components/Toggle"
import type { Stream } from "@/types/stream"
@@ -106,6 +106,8 @@ export function StreamCard({ stream, status, localStatus, cardSize = "md", onRef
const [thumbError, setThumbError] = useState(false)
const [thumbCapturing, setThumbCapturing] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
const [autoReload, setAutoReload] = useState(stream.autoReload ?? false)
const [autoReloadMins, setAutoReloadMins] = useState(Math.round((stream.autoReloadInterval ?? 3600) / 60))
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
useEffect(() => {
@@ -164,6 +166,24 @@ export function StreamCard({ stream, status, localStatus, cardSize = "md", onRef
})
}
async function toggleAutoReload() {
const next = !autoReload
setAutoReload(next)
await fetch(`/api/streams/${stream.id}/autoreload`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: next, interval: autoReloadMins * 60 }),
})
}
async function saveAutoReloadInterval(mins: number) {
await fetch(`/api/streams/${stream.id}/autoreload`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled: autoReload, interval: mins * 60 }),
})
}
async function refreshThumb() {
setMenuOpen(false)
setThumbError(false)
@@ -275,6 +295,35 @@ export function StreamCard({ stream, status, localStatus, cardSize = "md", onRef
{thumbCapturing ? "Capturing..." : "Refresh thumbnail"}
</button>
<div className="border-t border-border" />
<div className="px-3 py-2 flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<span className="text-sm flex items-center gap-2">
<RefreshCw className="w-3.5 h-3.5 shrink-0" /> Auto-reload
</span>
<button
onClick={toggleAutoReload}
className={cn("relative w-9 h-5 rounded-full transition-colors shrink-0 overflow-hidden", autoReload ? "bg-blue-600" : "bg-zinc-600")}
>
<span className={cn("absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all", autoReload ? "left-[18px]" : "left-0.5")} />
</button>
</div>
{autoReload && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground whitespace-nowrap">Every</span>
<input
type="number"
min={1}
value={autoReloadMins}
onChange={e => setAutoReloadMins(Math.max(1, Number(e.target.value) || 1))}
onBlur={() => saveAutoReloadInterval(autoReloadMins)}
onKeyDown={e => { if (e.key === "Enter") saveAutoReloadInterval(autoReloadMins) }}
className="w-16 text-xs bg-[#2a2a2a] border border-border rounded px-2 py-0.5 text-center"
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">min</span>
</div>
)}
</div>
<div className="border-t border-border" />
<button onClick={remove} className={cn(menuItem, "text-destructive")}>
<Trash2 className="w-3.5 h-3.5" /> Delete
</button>
+12 -3
View File
@@ -63,7 +63,9 @@ export function provisionStream(stream: Stream): void {
THREADS: stream.threads ?? 0,
USER: stream.user ?? "",
PASS: stream.pass ?? "",
GPU_FLAGS: stream.gpu ? "" : " --disable-gpu \\\n",
GPU_FLAGS: stream.gpu ? "" : " --disable-gpu \\\n",
AUTO_RELOAD: stream.autoReload ? "true" : "false",
AUTO_RELOAD_INTERVAL: stream.autoReloadInterval ?? 3600,
}
const confTpl = fs.readFileSync("/opt/scripts/stream.template.conf", "utf-8")
@@ -91,16 +93,23 @@ export function recreateStream(id: string): void {
}
export function startStream(id: string): void {
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
const programs = ["xvfb", "chromium", "autologin", "autoreload", "x11vnc", "ffmpeg"]
for (const p of programs) supervisorctl(`start ${p}-${id}`)
captureThumb(id, 60)
}
export function stopStream(id: string): void {
const programs = ["ffmpeg", "x11vnc", "autologin", "chromium", "xvfb"]
const programs = ["ffmpeg", "x11vnc", "autoreload", "autologin", "chromium", "xvfb"]
for (const p of programs) supervisorctl(`stop ${p}-${id}`)
}
export function applyAutoReload(id: string): void {
const stream = getStream(id)
if (!stream) return
supervisorctl(`stop autoreload-${id}`)
if (stream.autoReload) supervisorctl(`start autoreload-${id}`)
}
export function restartStream(id: string): void {
stopStream(id)
startStream(id)
+3
View File
@@ -24,6 +24,9 @@ export interface Stream {
gpu: boolean
autoReload?: boolean
autoReloadInterval?: number // seconds
desiredState: "running" | "stopped" // persisted desired state, restored on container restart
order: number