Migrate to Chromium, unified VNC, thumbnails, autologin CDP detection
--- - Migrado base Docker de ubuntu:22.04 + Google Chrome para debian:bookworm-slim + Chromium - Dockerfile refatorado com multi-stage build (node:22-alpine builder + debian runtime) e single RUN layer para imagem menor - VNC unificado: removido novnc por stream, substituído por websockify global na porta 6080 com token-based routing - Implementado sistema de thumbnails por stream via ffmpeg (captura do HLS) com endpoint GET/POST e atualização no card - Autologin reescrito com detecção via Chrome DevTools Protocol: pula credenciais se já autenticado - Adicionado padrão desiredState (running/stopped) persistido no JSON, restaurado via restore-streams.sh ao reiniciar container - UI traduzida para inglês, formulário reorganizado com tooltips, seção avançada colapsável e GOP automático - Player simplificado: modos HLS e HTML unificados, removido modo m3u8 separado - Adicionado campo threads no ffmpeg; suporte a seccomp:unconfined no docker-compose ---
This commit is contained in:
@@ -1,20 +1,29 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getStream } from "@/lib/db"
|
||||
import { getStream, saveStream } from "@/lib/db"
|
||||
import { startStream, stopStream, restartStream } from "@/lib/supervisor"
|
||||
|
||||
type Ctx = { params: Promise<{ id: string; action: string }> }
|
||||
|
||||
export async function POST(_req: Request, { params }: Ctx) {
|
||||
const { id, action } = await params
|
||||
|
||||
if (!getStream(id)) return NextResponse.json({ error: "não encontrado" }, { status: 404 })
|
||||
const stream = getStream(id)
|
||||
if (!stream) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
|
||||
switch (action) {
|
||||
case "start": startStream(id); break
|
||||
case "stop": stopStream(id); break
|
||||
case "restart": restartStream(id); break
|
||||
case "start":
|
||||
saveStream({ ...stream, desiredState: "running", updatedAt: new Date().toISOString() }) // #19
|
||||
startStream(id)
|
||||
break
|
||||
case "stop":
|
||||
saveStream({ ...stream, desiredState: "stopped", updatedAt: new Date().toISOString() }) // #19
|
||||
stopStream(id)
|
||||
break
|
||||
case "restart":
|
||||
saveStream({ ...stream, desiredState: "running", updatedAt: new Date().toISOString() }) // #19
|
||||
restartStream(id)
|
||||
break
|
||||
default:
|
||||
return NextResponse.json({ error: "ação inválida" }, { status: 400 })
|
||||
return NextResponse.json({ error: "invalid action" }, { status: 400 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
|
||||
@@ -8,14 +8,14 @@ type Ctx = { params: Promise<{ id: string }> }
|
||||
export async function GET(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
const stream = getStream(id)
|
||||
if (!stream) return NextResponse.json({ error: "não encontrado" }, { status: 404 })
|
||||
if (!stream) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
return NextResponse.json(stream)
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
const stream = getStream(id)
|
||||
if (!stream) return NextResponse.json({ error: "não encontrado" }, { status: 404 })
|
||||
if (!stream) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
|
||||
const body = (await req.json()) as StreamUpdate
|
||||
// id e portas não podem ser alterados via PATCH
|
||||
@@ -32,7 +32,7 @@ export async function PATCH(req: Request, { params }: Ctx) {
|
||||
|
||||
export async function DELETE(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
if (!getStream(id)) return NextResponse.json({ error: "não encontrado" }, { status: 404 })
|
||||
if (!getStream(id)) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
|
||||
removeStream(id)
|
||||
deleteStream(id)
|
||||
|
||||
@@ -6,6 +6,6 @@ type Ctx = { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
if (!getStream(id)) return NextResponse.json({ error: "não encontrado" }, { status: 404 })
|
||||
if (!getStream(id)) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
return NextResponse.json(getStreamStatus(id))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { NextResponse } from "next/server"
|
||||
import { getStream } from "@/lib/db"
|
||||
import { captureThumb } from "@/lib/supervisor"
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR ?? "/app/data"
|
||||
|
||||
type Ctx = { params: Promise<{ id: string }> }
|
||||
|
||||
export async function GET(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
const thumbPath = path.join(DATA_DIR, "streams", id, "thumb.jpg")
|
||||
if (!fs.existsSync(thumbPath)) return new Response("not found", { status: 404 })
|
||||
const buffer = fs.readFileSync(thumbPath)
|
||||
return new Response(buffer, {
|
||||
headers: { "Content-Type": "image/jpeg", "Cache-Control": "no-cache, no-store" },
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(_req: Request, { params }: Ctx) {
|
||||
const { id } = await params
|
||||
if (!getStream(id)) return NextResponse.json({ error: "not found" }, { status: 404 })
|
||||
captureThumb(id, 5)
|
||||
return NextResponse.json({ ok: true })
|
||||
}
|
||||
@@ -9,8 +9,8 @@ export async function GET(req: Request) {
|
||||
|
||||
const lines = ["#EXTM3U"]
|
||||
for (const s of streams) {
|
||||
lines.push(`#EXTINF:-1,${s.name}`)
|
||||
lines.push(`http://${host}:${port}/live/${s.id}/index.m3u8`)
|
||||
lines.push(`#EXTINF:-1 tvg-id="${s.id}" tvg-name="${s.name}" group-title="DecapStream",${s.name} [${s.id}] ${s.resolution} ${s.fps}fps`)
|
||||
lines.push(`http://${host}:${port}/live/${s.id}`)
|
||||
}
|
||||
|
||||
return new Response(lines.join("\n"), {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { readStreams, saveStream, allocatePorts, getStream } from "@/lib/db"
|
||||
import { provisionStream, startStream } from "@/lib/supervisor"
|
||||
import { provisionStream, startStream, normalizeScale, captureThumb } from "@/lib/supervisor"
|
||||
import { STREAM_DEFAULTS, type StreamCreate } from "@/types/stream"
|
||||
|
||||
export async function GET() {
|
||||
@@ -13,13 +13,13 @@ export async function POST(req: Request) {
|
||||
const body = (await req.json()) as StreamCreate
|
||||
|
||||
if (!body.id || !SLUG_RE.test(body.id))
|
||||
return NextResponse.json({ error: "id inválido: use apenas letras minúsculas, números e hífen" }, { status: 400 })
|
||||
return NextResponse.json({ error: "invalid id: use only lowercase letters, numbers and hyphens" }, { status: 400 })
|
||||
|
||||
if (!body.name || !body.url)
|
||||
return NextResponse.json({ error: "name e url são obrigatórios" }, { status: 400 })
|
||||
return NextResponse.json({ error: "name and url are required" }, { status: 400 })
|
||||
|
||||
if (getStream(body.id))
|
||||
return NextResponse.json({ error: "já existe uma stream com esse id" }, { status: 409 })
|
||||
return NextResponse.json({ error: "a stream with this id already exists" }, { status: 409 })
|
||||
|
||||
const ports = allocatePorts()
|
||||
const now = new Date().toISOString()
|
||||
@@ -27,7 +27,9 @@ export async function POST(req: Request) {
|
||||
const stream = {
|
||||
...STREAM_DEFAULTS,
|
||||
...body,
|
||||
scale: normalizeScale(body.scale ?? STREAM_DEFAULTS.scale), // #13
|
||||
...ports,
|
||||
desiredState: "running" as const, // #19
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
@@ -35,6 +37,7 @@ export async function POST(req: Request) {
|
||||
saveStream(stream)
|
||||
provisionStream(stream)
|
||||
startStream(stream.id)
|
||||
captureThumb(stream.id, 60)
|
||||
|
||||
return NextResponse.json(stream, { status: 201 })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user