Corrige lentidão do frontend com múltiplos clientes simultâneos
---
- Substituída consulta individual por stream (supervisorctl status {p}-{id} × 5) por uma única chamada supervisorctl status que retorna todos os programas de uma vez, com cache de 3s em fetchAllStatuses();
- Adicionado endpoint GET /api/streams/statuses que retorna os statuses de todos os streams em uma resposta batch;
- Frontend alterado para usar o endpoint batch em vez de N requests paralelos por ciclo de polling;
- Token de sessão HMAC-SHA256 em auth.ts agora é computado uma vez e cacheado no módulo, eliminando operações de crypto a cada requisição;
- Rolling session no middleware limitado a rotas de página, removendo overhead de Set-Cookie em respostas de API e HLS;
---
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
import { NextResponse } from "next/server"
|
||||||
|
import { getAllStreamStatuses } from "@/lib/supervisor"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json(getAllStreamStatuses())
|
||||||
|
}
|
||||||
+3
-8
@@ -135,14 +135,9 @@ export default function GalleryPage() {
|
|||||||
|
|
||||||
const fetchStatuses = useCallback(async (list: Stream[]) => {
|
const fetchStatuses = useCallback(async (list: Stream[]) => {
|
||||||
if (list.length === 0) return
|
if (list.length === 0) return
|
||||||
const results = await Promise.all(
|
const res = await fetch("/api/streams/statuses")
|
||||||
list.map(async (s) => {
|
const data: Record<string, Record<string, string>> = await res.json()
|
||||||
const res = await fetch(`/api/streams/${s.id}/status`)
|
setStatuses(data)
|
||||||
const data = await res.json()
|
|
||||||
return [s.id, data] as const
|
|
||||||
})
|
|
||||||
)
|
|
||||||
setStatuses(Object.fromEntries(results))
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
+10
-8
@@ -3,17 +3,19 @@
|
|||||||
export const AUTH_ENABLED = !!(process.env.AUTH_USER && process.env.AUTH_PASS)
|
export const AUTH_ENABLED = !!(process.env.AUTH_USER && process.env.AUTH_PASS)
|
||||||
export const COOKIE_NAME = "ds_session"
|
export const COOKIE_NAME = "ds_session"
|
||||||
|
|
||||||
|
// Cached promise — token is deterministic (env vars never change at runtime)
|
||||||
|
let _tokenCache: Promise<string> | null = null
|
||||||
|
|
||||||
// HMAC-SHA256(user, key=pass) — deterministic, no in-memory state, survives restarts
|
// HMAC-SHA256(user, key=pass) — deterministic, no in-memory state, survives restarts
|
||||||
// Works in both Edge (SubtleCrypto) and Node.js runtime
|
// Works in both Edge (SubtleCrypto) and Node.js runtime
|
||||||
export async function computeSessionToken(): Promise<string> {
|
export function computeSessionToken(): Promise<string> {
|
||||||
|
if (_tokenCache) return _tokenCache
|
||||||
const user = process.env.AUTH_USER ?? ""
|
const user = process.env.AUTH_USER ?? ""
|
||||||
const pass = process.env.AUTH_PASS ?? ""
|
const pass = process.env.AUTH_PASS ?? ""
|
||||||
const enc = new TextEncoder()
|
const enc = new TextEncoder()
|
||||||
const key = await globalThis.crypto.subtle.importKey(
|
_tokenCache = globalThis.crypto.subtle
|
||||||
"raw", enc.encode(pass),
|
.importKey("raw", enc.encode(pass), { name: "HMAC", hash: "SHA-256" }, false, ["sign"])
|
||||||
{ name: "HMAC", hash: "SHA-256" },
|
.then(key => globalThis.crypto.subtle.sign("HMAC", key, enc.encode(user)))
|
||||||
false, ["sign"]
|
.then(sig => Array.from(new Uint8Array(sig), b => b.toString(16).padStart(2, "0")).join(""))
|
||||||
)
|
return _tokenCache
|
||||||
const sig = await globalThis.crypto.subtle.sign("HMAC", key, enc.encode(user))
|
|
||||||
return Array.from(new Uint8Array(sig), b => b.toString(16).padStart(2, "0")).join("")
|
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-16
@@ -132,25 +132,51 @@ export function captureThumb(streamId: string, delay = 60): void {
|
|||||||
|
|
||||||
export type ProgramStatus = "RUNNING" | "STOPPED" | "FATAL" | "STARTING" | "UNKNOWN"
|
export type ProgramStatus = "RUNNING" | "STOPPED" | "FATAL" | "STARTING" | "UNKNOWN"
|
||||||
|
|
||||||
export function getStreamStatus(id: string): Record<string, ProgramStatus> {
|
// Cache for supervisorctl status — refreshed at most once every 3 seconds across all callers
|
||||||
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
let _statusCache: Record<string, Record<string, ProgramStatus>> | null = null
|
||||||
|
let _statusCacheAt = 0
|
||||||
|
const STATUS_CACHE_TTL = 3000
|
||||||
|
|
||||||
if (IS_DEV) {
|
function fetchAllStatuses(): Record<string, Record<string, ProgramStatus>> {
|
||||||
return Object.fromEntries(programs.map((p) => [p, "STOPPED" as ProgramStatus]))
|
const now = Date.now()
|
||||||
}
|
if (_statusCache && now - _statusCacheAt < STATUS_CACHE_TTL) return _statusCache
|
||||||
|
|
||||||
const result: Record<string, ProgramStatus> = {}
|
const result: Record<string, Record<string, ProgramStatus>> = {}
|
||||||
for (const p of programs) {
|
try {
|
||||||
try {
|
// One call for all programs — avoid N×5 blocking execSync calls per poll cycle
|
||||||
const out = execSync(
|
const out = execSync(
|
||||||
`supervisorctl -c /etc/supervisor/supervisord.conf status ${p}-${id}`,
|
`supervisorctl -c /etc/supervisor/supervisord.conf status`,
|
||||||
{ stdio: "pipe" }
|
{ stdio: "pipe" }
|
||||||
).toString()
|
).toString()
|
||||||
const match = out.match(/\b(RUNNING|STOPPED|FATAL|STARTING)\b/)
|
for (const line of out.split("\n")) {
|
||||||
result[p] = (match?.[1] as ProgramStatus) ?? "UNKNOWN"
|
// e.g. "ffmpeg-abc123 RUNNING pid 42, uptime 0:01:00"
|
||||||
} catch {
|
const m = line.match(/^(\S+)-(\S+)\s+(RUNNING|STOPPED|FATAL|STARTING)/)
|
||||||
result[p] = "UNKNOWN"
|
if (!m) continue
|
||||||
|
const [, program, id, status] = m
|
||||||
|
if (!result[id]) result[id] = {}
|
||||||
|
result[id][program] = status as ProgramStatus
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// supervisorctl can exit non-zero; return whatever was parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_statusCache = result
|
||||||
|
_statusCacheAt = now
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getStreamStatus(id: string): Record<string, ProgramStatus> {
|
||||||
|
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
||||||
|
if (IS_DEV) return Object.fromEntries(programs.map((p) => [p, "STOPPED" as ProgramStatus]))
|
||||||
|
const all = fetchAllStatuses()
|
||||||
|
return all[id] ?? Object.fromEntries(programs.map((p) => [p, "UNKNOWN" as ProgramStatus]))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllStreamStatuses(): Record<string, Record<string, ProgramStatus>> {
|
||||||
|
const programs = ["xvfb", "chromium", "autologin", "x11vnc", "ffmpeg"]
|
||||||
|
if (IS_DEV) {
|
||||||
|
const { readStreams } = require("./db") as typeof import("./db")
|
||||||
|
return Object.fromEntries(readStreams().map((s) => [s.id, Object.fromEntries(programs.map((p) => [p, "STOPPED" as ProgramStatus]))]))
|
||||||
|
}
|
||||||
|
return fetchAllStatuses()
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -22,7 +22,10 @@ export async function middleware(request: NextRequest) {
|
|||||||
return NextResponse.redirect(url)
|
return NextResponse.redirect(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rolling session — refresh cookie on every request, resetting the 30-day timer
|
// Rolling session — refresh cookie only on page navigations, not API/HLS/asset requests
|
||||||
|
if (pathname.startsWith("/api/") || pathname.startsWith("/player")) {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
const res = NextResponse.next()
|
const res = NextResponse.next()
|
||||||
res.cookies.set(COOKIE_NAME, cookie, {
|
res.cookies.set(COOKIE_NAME, cookie, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user