diff --git a/STATUS.md b/STATUS.md index afe7d52..ccb7feb 100644 --- a/STATUS.md +++ b/STATUS.md @@ -31,7 +31,7 @@ Source of truth: this repo, `techy/Media-Server-Setup` on the stack's own Gitea | Streamer | One-time: dock → Timecode band → "Add band + clock source" (OBS) or leave the Go Live checkbox on — gives editors frame-accurate timecode | OBS dock / Go Live | live | | Streamer | Type a title (live preview + naming rules), Go Live, Stop. "Dump tonight" if it was junk (3-day undo). Manual-setup accordion with endpoints and a hidden token | OBS browser dock `https://live.…/dock/` | Discord login in the dock; dock mints a 12 h token per Go Live — **no passwords, verified live** | | Gamer | Self-serve pairing (paste Syncthing Device ID → folder + share created); then nothing, clips save where they always did | `https://clips.…/` + Syncthing | Discord (any server member) | -| Viewer | Live portal lays streams out as an even grid automatically when one joins or leaves ("Arrange" button redoes it after manual moves); audio follows the focused window | `https://live.…/` | Discord | +| Viewer | Live portal lays streams out as an even grid automatically when one joins or leaves ("Arrange" button redoes it after manual moves); audio follows the focused window; double-click a tab to rename a stream (kept in that browser), double-click the slot at the left of a tab bar to name the group (session) | `https://live.…/` | Discord | | Everyone | Landing page with role-aware links and instructions | `https://media.…/` | Discord | | Editor | Multicam: drop clips on a timeline, sync by the embedded timecode track (frame-accurate when the band was on), cross-check with voice audio; files are already at game resolution (band removed by the sweep) | Resolve / Premiere | — | | Editor | Browse by streamer/title, scrub in browser, download originals (zip/tar for folders), upload finals | copyparty `https://files.…/` | Discord (Authentik); rights by group `editors` / `media-admins` | diff --git a/live/src/App.jsx b/live/src/App.jsx index f5bbcb5..c8e2d9f 100644 --- a/live/src/App.jsx +++ b/live/src/App.jsx @@ -4,8 +4,12 @@ import Player from './Player.jsx' import { fetchStreams } from './api.js' import { audio } from './useAudio.js' import GoLive from './GoLive.jsx' +import Tab from './Tab.jsx' +import GroupLabel from './GroupLabel.jsx' +import { streamLabel } from './names.js' const components = { player: Player } +const tabComponents = { player: Tab } const POLL_MS = 3000 // Lay every panel out as a grid (cols = ceil(sqrt(n)), row-major in stream-start order) and equalize sizes. @@ -47,7 +51,7 @@ export default function App() { let changed = false for (const s of list) { if (!have.has(s.id)) { - api.addPanel({ id: s.id, component: 'player', title: `${s.user} · ${s.title}`, params: s, + api.addPanel({ id: s.id, component: 'player', tabComponent: 'player', title: streamLabel(s.id, s), params: s, position: api.panels.length ? { direction: 'right' } : undefined }) // relative to the whole grid; arrange() places it changed = true } else { @@ -87,7 +91,8 @@ export default function App() {

Nobody is live right now

This page updates itself when a stream starts.
)}
- +
)} diff --git a/live/src/GroupLabel.jsx b/live/src/GroupLabel.jsx new file mode 100644 index 0000000..2c18d83 --- /dev/null +++ b/live/src/GroupLabel.jsx @@ -0,0 +1,22 @@ +import React, { useEffect, useRef, useState } from 'react' +import { names } from './names.js' + +// Left end of every tab bar: an editable group name (double-click). Session-only. +export default function GroupLabel({ group }) { + const id = group.id + const name = names.use(s => s.groups[id]) + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState('') + const ref = useRef(null) + useEffect(() => { if (editing) { ref.current?.focus(); ref.current?.select() } }, [editing]) + const start = e => { e.stopPropagation(); setDraft(name || ''); setEditing(true) } + const commit = () => { if (!editing) return; names.renameGroup(id, draft.trim()); setEditing(false) } + return ( +
e.stopPropagation()}> + {editing + ? setDraft(e.target.value)} onBlur={commit} + onKeyDown={e => { e.stopPropagation(); if (e.key === 'Enter') commit(); if (e.key === 'Escape') setEditing(false) }} /> + : {name || '+ name'}} +
+ ) +} diff --git a/live/src/Player.jsx b/live/src/Player.jsx index 1d86d2b..79424a8 100644 --- a/live/src/Player.jsx +++ b/live/src/Player.jsx @@ -2,10 +2,12 @@ import React, { useEffect, useRef, useState } from 'react' import Hls from 'hls.js' import { whepUrl, hlsUrl } from './api.js' import { audio, effectiveMuted } from './useAudio.js' +import { names } from './names.js' // WebRTC (WHEP) first, HLS fallback. Mute/volume come from the shared audio store. export default function Player({ params, api }) { const { id, user, title } = params + const custom = names.use(s => s.streams[id]) // viewer-side rename (tab double-click) const whep = params.whep || id // Opus audio mirror path when the source audio is AAC const videoRef = useRef(null) const [status, setStatus] = useState('connecting…') @@ -105,7 +107,7 @@ export default function Player({ params, api }) {
LIVE {user} - {title} + {custom || title} 👁 {viewers}