Nagrywanie spotkania: przycisk w panelu i domykanie statusu
Deploy PROD / deploy (push) Successful in 26s
Deploy PROD / deploy (push) Successful in 26s
Nagrywania NIE DALO SIE URUCHOMIC. Backend mial `startRecording` i `stopRecording`, Egress byl skonfigurowany i pisal prosto do MinIO, ale brakowalo dwoch ogniw: przycisku w panelu i domkniecia stanu po zakonczeniu. `completeRecording` nie bylo wolane z ZADNEGO miejsca w kodzie. PRZYCISK W PANELU PROWADZENIA Obok trybow mikrofonu i wideo, bo tam prowadzacy podejmuje decyzje o sali. Osobno od nich, bo nagrywanie nie dotyczy uprawnien uczestnikow: to decyzja organizatora o utrwaleniu szkolenia. Start bez potwierdzenia (nagrywanie niczego nie ujawnia i da sie je zatrzymac), stop z potwierdzeniem, bo tego fragmentu nie nagra sie drugi raz. Stan czytany jest przy wejsciu z listy nagran, nie tylko trzymany lokalnie: prowadzacy moze odswiezyc strone albo wrocic z innego urzadzenia i musi zobaczyc, ze nagranie trwa, zamiast zaczynac drugie rownolegle. Sluzy do tego `providerRecordingId` dopisane do `RecordingView`. DOMYKANIE PRZEZ ODPYTYWANIE, NIE WEBHOOK LiveKit podpisuje webhooki tokenem JWT, ktorego `VideoWebhookController` nie rozpoznaje: przyjmuje wylacznie wspolny sekret w naglowku, tak jak Daily. Odpytywanie jest przy tym ODPORNIEJSZE: zgloszenie, ktore nie dojdzie (restart backendu w zlej chwili, awaria sieci), zostawia wpis w stanie DOWNLOADING na zawsze, a zadanie w tle samo to nadrabia. Opoznienie do minuty nie ma znaczenia, bo montaz pliku po stronie Egress i tak trwa dluzej. EGRESS_COMPLETE domyka wpis rozmiarem i czasem trwania, FAILED/ABORTED/ LIMIT_REACHED oznacza go jako nieudany Z POWODEM: wpis zostaje w bazie, zeby administrator widzial, ze proba byla i czym sie skonczyla. Zadanie nie rusza, gdy dostawca nie jest LiveKit, i nie oznacza nagrania jako nieudanego, gdy Egress zwyczajnie nie zna juz zadania: brak informacji to nie to samo co blad. `reconcileRecordings` jest nietransakcyjne (wola serwer po sieci) i domyka kazdy wpis przez `@Lazy self`: samowywolanie omija proxy Springa, a wtedy zapis odbywa sie bez transakcji i stan nagrania nie zmienia sie wcale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -322,6 +322,8 @@ export type Recording = {
|
||||
/** Adres odtwarzania. Null, dopoki nagranie nie jest gotowe. */
|
||||
url: string | null
|
||||
error: string | null
|
||||
/** Identyfikator zadania u dostawcy. Panel rozpoznaje po nim nagranie w toku. */
|
||||
providerRecordingId: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,75 @@ export function SpeakQueue({
|
||||
// okno potwierdzenia wlaczenia otwartego mikrofonu (zamiast window.confirm)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
|
||||
/*
|
||||
* Nagrywanie. `null` oznacza "nie nagrywamy", a wartosc to identyfikator
|
||||
* zadania Egress, ktory trzeba oddac przy zatrzymaniu.
|
||||
*
|
||||
* Stan pobierany jest przy wejsciu z listy nagran, a nie tylko trzymany
|
||||
* lokalnie: prowadzacy moze odswiezyc strone albo wrocic z innego urzadzenia
|
||||
* i musi zobaczyc, ze nagranie trwa, zamiast zaczynac drugie rownolegle.
|
||||
*/
|
||||
const [egressId, setEgressId] = useState<string | null>(null)
|
||||
const [recordingBusy, setRecordingBusy] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await client.get<{ id: number; status: string; providerRecordingId: string | null }[]>(
|
||||
`/admin/webinars/${webinarId}/recordings`,
|
||||
)
|
||||
if (!active) return
|
||||
// DOWNLOADING oznacza "plik powstaje", czyli nagranie w toku
|
||||
const running = response.data.find(
|
||||
(r) => r.status === 'DOWNLOADING' && r.providerRecordingId,
|
||||
)
|
||||
setEgressId(running?.providerRecordingId ?? null)
|
||||
} catch {
|
||||
// brak listy nie moze zablokowac panelu prowadzenia
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [webinarId])
|
||||
|
||||
/**
|
||||
* Wlaczenie i zatrzymanie nagrywania.
|
||||
*
|
||||
* Bez potwierdzenia przy starcie: nagrywanie niczego nie ujawnia uczestnikom
|
||||
* i da sie je zatrzymac w kazdej chwili. Przy zatrzymaniu pytamy, bo drugi
|
||||
* raz tego fragmentu sie nie nagra.
|
||||
*/
|
||||
async function toggleRecording() {
|
||||
if (recordingBusy) return
|
||||
|
||||
if (egressId && !confirm('Zatrzymać nagrywanie? Tego fragmentu nie nagrasz ponownie.')) {
|
||||
return
|
||||
}
|
||||
|
||||
setRecordingBusy(true)
|
||||
try {
|
||||
if (egressId) {
|
||||
await client.post(
|
||||
`/admin/webinars/${webinarId}/recording/stop?egressId=${encodeURIComponent(egressId)}`,
|
||||
)
|
||||
setEgressId(null)
|
||||
toasts.success('Nagrywanie zatrzymane. Plik pojawi się w zakładce Pliki za chwilę.')
|
||||
} else {
|
||||
const response = await client.post<{ egressId: string }>(
|
||||
`/admin/webinars/${webinarId}/recording/start`,
|
||||
)
|
||||
setEgressId(response.data.egressId)
|
||||
toasts.success('Nagrywanie rozpoczęte.')
|
||||
}
|
||||
} catch (error) {
|
||||
toasts.error(errorMessage(error, 'Nie udało się zmienić stanu nagrywania'))
|
||||
} finally {
|
||||
setRecordingBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Przelaczenie trybu otwartego mikrofonu.
|
||||
*
|
||||
@@ -230,6 +299,35 @@ export function SpeakQueue({
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/*
|
||||
Nagrywanie. Osobno od trybow mikrofonu i wideo, bo nie dotyczy
|
||||
uprawnien uczestnikow: to decyzja organizatora o tym, czy szkolenie
|
||||
ma zostac utrwalone. Zgoda uczestnikow na nagranie jest jego sprawa.
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleRecording}
|
||||
disabled={recordingBusy}
|
||||
aria-pressed={egressId !== null}
|
||||
className={`flex w-full items-center gap-2 rounded-md border px-3 py-2 text-left text-sm transition-colors duration-fast ease-standard disabled:opacity-50 ${
|
||||
egressId
|
||||
? 'border-alarm-500/30 bg-alarm-50 text-alarm-600'
|
||||
: 'border-slate-200 text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<Icon name={recordingBusy ? 'loader' : 'video'} size={16} className="shrink-0" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-medium">
|
||||
{egressId ? 'Nagrywanie trwa' : 'Nagraj spotkanie'}
|
||||
</span>
|
||||
<span className="block text-xs opacity-80">
|
||||
{egressId
|
||||
? 'Kliknij, żeby zatrzymać. Plik trafi do zakładki Pliki.'
|
||||
: 'Zapis obrazu i dźwięku na serwerze'}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3">
|
||||
|
||||
Reference in New Issue
Block a user