updated live video
This commit is contained in:
parent
a0a869c5a5
commit
d578d4e6aa
@ -61,6 +61,37 @@ func serveLiveNotReady(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(body))
|
||||
}
|
||||
|
||||
func maybeBlockHLSOnPreview(w http.ResponseWriter, r *http.Request, basePath, file string) bool {
|
||||
// Nur /api/preview (nicht /api/preview/live)
|
||||
if strings.TrimSpace(basePath) != "/api/preview" {
|
||||
return false
|
||||
}
|
||||
|
||||
low := strings.ToLower(strings.TrimSpace(file))
|
||||
if low == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Nur echte HLS-Dateien blocken (Manifest/Segmente)
|
||||
if strings.HasSuffix(low, ".m3u8") || strings.HasSuffix(low, ".ts") || strings.HasSuffix(low, ".m4s") {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("X-Preview-HLS-Disabled", "1")
|
||||
|
||||
// Optionales Debug (hilft dir, den Auslöser zu finden)
|
||||
fmt.Printf("[HLS-BLOCK] %s file=%q referer=%q ua=%q\n",
|
||||
r.URL.String(),
|
||||
file,
|
||||
r.Referer(),
|
||||
r.Header.Get("User-Agent"),
|
||||
)
|
||||
|
||||
http.Error(w, "HLS disabled on /api/preview; use /api/preview/live", http.StatusGone) // 410
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// stopPreview stops the running ffmpeg HLS preview process for a job and resets state.
|
||||
func stopPreview(job *RecordJob) {
|
||||
jobsMu.Lock()
|
||||
@ -81,8 +112,54 @@ func stopPreview(job *RecordJob) {
|
||||
}
|
||||
|
||||
func recordPreviewLive(w http.ResponseWriter, r *http.Request) {
|
||||
// identisch zu /api/preview, aber m3u8 rewriting soll auf /api/preview/live zeigen
|
||||
recordPreviewWithBase(w, r, "/api/preview/live")
|
||||
// ✅ Route bleibt /api/preview/live
|
||||
// Wenn kein "file" Parameter da ist, liefern wir den neuen Single-Request Stream (fMP4).
|
||||
file := strings.TrimSpace(r.URL.Query().Get("file"))
|
||||
if file == "" {
|
||||
recordPreviewLiveFMP4(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy: HLS file serving + m3u8 rewrite (falls du es noch irgendwo brauchst)
|
||||
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
||||
if id == "" {
|
||||
http.Error(w, "id fehlt", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
servePreviewHLSFileWithBase(w, r, id, file, "/api/preview/live")
|
||||
}
|
||||
|
||||
// recordPreviewFile serves ONLY the HLS file requests for /api/preview?file=...
|
||||
// preview.webp bleibt in preview.go (servePreviewWebPAlias).
|
||||
func recordPreviewFile(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
||||
if id == "" {
|
||||
id = strings.TrimSpace(r.URL.Query().Get("name"))
|
||||
}
|
||||
if id == "" {
|
||||
http.Error(w, "id fehlt", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file := strings.TrimSpace(r.URL.Query().Get("file"))
|
||||
if file == "" {
|
||||
http.Error(w, "file fehlt", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ HLS auf /api/preview blocken (Manifest/Segmente), um Polling-Storm zu verhindern.
|
||||
// Wenn du es NICHT blocken willst, diese if-Zeile entfernen.
|
||||
if maybeBlockHLSOnPreview(w, r, "/api/preview", file) {
|
||||
return
|
||||
}
|
||||
|
||||
// Alles andere (falls doch erlaubt) über die gemeinsame Serving-Funktion
|
||||
servePreviewHLSFileWithBase(w, r, id, file, "/api/preview")
|
||||
}
|
||||
|
||||
// servePreviewHLSFileWithBase serves a single HLS file (index/segment) for a job.
|
||||
|
||||
@ -1493,15 +1493,18 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri
|
||||
return
|
||||
}
|
||||
|
||||
// HLS / file serving
|
||||
// file serving
|
||||
if file := strings.TrimSpace(r.URL.Query().Get("file")); file != "" {
|
||||
low := strings.ToLower(file)
|
||||
low := strings.ToLower(strings.TrimSpace(file))
|
||||
|
||||
// ✅ preview.webp weiterhin hier behandeln
|
||||
if low == "preview.webp" {
|
||||
servePreviewWebPAlias(w, r, id)
|
||||
return
|
||||
}
|
||||
// ✅ Wichtig: HLS rewrite soll auf basePath zeigen (/api/preview oder /api/preview/live)
|
||||
servePreviewHLSFileWithBase(w, r, id, file, basePath)
|
||||
|
||||
// ✅ alles andere (m3u8/ts/m4s/...) liegt jetzt in live.go
|
||||
recordPreviewFile(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,15 @@ func RecordStream(
|
||||
return fmt.Errorf("playlist abrufen: %w", err)
|
||||
}
|
||||
|
||||
// ✅ WICHTIG: fMP4 live preview (/api/preview/live) braucht job.PreviewM3U8 als Input
|
||||
if job != nil {
|
||||
jobsMu.Lock()
|
||||
job.PreviewM3U8 = strings.TrimSpace(playlist.PlaylistURL)
|
||||
job.PreviewCookie = httpCookie
|
||||
job.PreviewUA = hc.userAgent
|
||||
jobsMu.Unlock()
|
||||
}
|
||||
|
||||
if job != nil && strings.TrimSpace(job.PreviewDir) == "" {
|
||||
assetID := assetIDForJob(job)
|
||||
if strings.TrimSpace(assetID) == "" {
|
||||
|
||||
@ -75,6 +75,15 @@ func RecordStreamMFC(
|
||||
return fmt.Errorf("mfc: keine m3u8 URL gefunden")
|
||||
}
|
||||
|
||||
// ✅ WICHTIG: fMP4 live preview (/api/preview/live) braucht job.PreviewM3U8 als Input
|
||||
if job != nil {
|
||||
jobsMu.Lock()
|
||||
job.PreviewM3U8 = strings.TrimSpace(m3u8URL)
|
||||
job.PreviewCookie = "" // MFC nutzt i.d.R. keine Cookies; wenn doch: hier setzen
|
||||
job.PreviewUA = hc.userAgent
|
||||
jobsMu.Unlock()
|
||||
}
|
||||
|
||||
// ✅ Job erst jetzt sichtbar machen (Stream wirklich verfügbar)
|
||||
if job != nil {
|
||||
_ = publishJob(job.ID)
|
||||
|
||||
@ -1,262 +0,0 @@
|
||||
// frontend\src\components\ui\LiveHlsVideo.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Hls from 'hls.js'
|
||||
import { applyInlineVideoPolicy, DEFAULT_INLINE_MUTED } from './videoPolicy'
|
||||
|
||||
function withNonce(url: string, nonce: number) {
|
||||
const sep = url.includes('?') ? '&' : '?'
|
||||
return `${url}${sep}v=${nonce}`
|
||||
}
|
||||
|
||||
export default function LiveHlsVideo({
|
||||
src,
|
||||
muted = DEFAULT_INLINE_MUTED,
|
||||
className,
|
||||
}: {
|
||||
src: string
|
||||
muted?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const ref = useRef<HTMLVideoElement>(null)
|
||||
const [broken, setBroken] = useState(false)
|
||||
const [brokenReason, setBrokenReason] = useState<'private' | 'offline' | null>(null)
|
||||
|
||||
// ✅ Nur für "harte Reloads" (triggert Effect neu)
|
||||
const [reloadKey, setReloadKey] = useState(1)
|
||||
|
||||
// ✅ manifestUrl ist stabil pro reloadKey
|
||||
const manifestUrl = useMemo(() => withNonce(src, reloadKey), [src, reloadKey])
|
||||
|
||||
const lastReloadAtRef = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let hls: Hls | null = null
|
||||
let stallTimer: number | null = null
|
||||
let watchdogTimer: number | null = null
|
||||
|
||||
const videoEl = ref.current
|
||||
if (!videoEl) return
|
||||
const video: HTMLVideoElement = videoEl
|
||||
|
||||
setBroken(false)
|
||||
setBrokenReason(null)
|
||||
|
||||
applyInlineVideoPolicy(video, { muted })
|
||||
|
||||
const cleanupTimers = () => {
|
||||
if (stallTimer) window.clearTimeout(stallTimer)
|
||||
if (watchdogTimer) window.clearInterval(watchdogTimer)
|
||||
stallTimer = null
|
||||
watchdogTimer = null
|
||||
}
|
||||
|
||||
const hardReload = () => {
|
||||
if (cancelled) return
|
||||
|
||||
const now = Date.now()
|
||||
// ✅ verhindert Reload-Stürme (z.B. wenn hls.js kurz zickt)
|
||||
if (now - lastReloadAtRef.current < 4000) return
|
||||
lastReloadAtRef.current = now
|
||||
|
||||
cleanupTimers()
|
||||
setReloadKey((x) => x + 1)
|
||||
}
|
||||
|
||||
async function waitForManifestWithSegments(): Promise<{ ok: boolean; reason?: 'private' | 'offline' }> {
|
||||
const started = Date.now()
|
||||
|
||||
while (!cancelled && Date.now() - started < 90_000) {
|
||||
try {
|
||||
// ✅ immer frisch pollen (Cache umgehen)
|
||||
const url = withNonce(src, Date.now())
|
||||
const r = await fetch(url, { cache: 'no-store' })
|
||||
|
||||
if (r.status === 403) return { ok: false, reason: 'private' }
|
||||
if (r.status === 404) return { ok: false, reason: 'offline' }
|
||||
|
||||
if (r.ok) {
|
||||
const txt = await r.text()
|
||||
if (txt.includes('#EXTINF')) return { ok: true }
|
||||
}
|
||||
} catch {}
|
||||
await new Promise((res) => setTimeout(res, 1200))
|
||||
}
|
||||
|
||||
// kein reason => "noch nicht ready"
|
||||
return { ok: false }
|
||||
}
|
||||
|
||||
async function start() {
|
||||
const res = await waitForManifestWithSegments()
|
||||
if (cancelled) return
|
||||
|
||||
if (!res.ok) {
|
||||
// ✅ Nur echte Endzustände dauerhaft anzeigen
|
||||
if (res.reason === 'private' || res.reason === 'offline') {
|
||||
setBrokenReason(res.reason)
|
||||
setBroken(true)
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Sonst: retry per reloadKey (ohne broken)
|
||||
window.setTimeout(() => {
|
||||
if (!cancelled) hardReload()
|
||||
}, 800)
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ Safari / iOS: Native HLS
|
||||
if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.pause()
|
||||
video.removeAttribute('src')
|
||||
video.load()
|
||||
|
||||
video.src = manifestUrl
|
||||
video.load()
|
||||
video.play().catch((e) => console.debug('[LiveHlsVideo] play() failed', e))
|
||||
|
||||
// ---- Stall Handling (native) ----
|
||||
let lastProgressTs = Date.now()
|
||||
let lastTime = -1
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
if (video.currentTime > lastTime + 0.01) {
|
||||
lastTime = video.currentTime
|
||||
lastProgressTs = Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleStallReload = () => {
|
||||
if (stallTimer) return
|
||||
stallTimer = window.setTimeout(() => {
|
||||
stallTimer = null
|
||||
if (!cancelled && Date.now() - lastProgressTs > 3500) hardReload()
|
||||
}, 800)
|
||||
}
|
||||
|
||||
video.addEventListener('timeupdate', onTimeUpdate)
|
||||
video.addEventListener('waiting', scheduleStallReload)
|
||||
video.addEventListener('stalled', scheduleStallReload)
|
||||
video.addEventListener('error', scheduleStallReload)
|
||||
|
||||
watchdogTimer = window.setInterval(() => {
|
||||
if (cancelled) return
|
||||
if (!video.paused && Date.now() - lastProgressTs > 6000) hardReload()
|
||||
}, 2000)
|
||||
|
||||
return () => {
|
||||
video.removeEventListener('timeupdate', onTimeUpdate)
|
||||
video.removeEventListener('waiting', scheduleStallReload)
|
||||
video.removeEventListener('stalled', scheduleStallReload)
|
||||
video.removeEventListener('error', scheduleStallReload)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Nicht-Safari: hls.js
|
||||
if (!Hls.isSupported()) {
|
||||
setBroken(true)
|
||||
return
|
||||
}
|
||||
|
||||
hls = new Hls({
|
||||
lowLatencyMode: false,
|
||||
|
||||
// ✅ Live: nicht super-aggressiv hinterherlaufen
|
||||
liveSyncDurationCount: 3,
|
||||
liveMaxLatencyDurationCount: 10,
|
||||
|
||||
// Buffer
|
||||
maxBufferLength: 12,
|
||||
backBufferLength: 30,
|
||||
|
||||
// ✅ Netzwerk-Retry-Backoff (verhindert Request-Stürme)
|
||||
manifestLoadingTimeOut: 8000,
|
||||
manifestLoadingMaxRetry: 6,
|
||||
manifestLoadingRetryDelay: 1000,
|
||||
manifestLoadingMaxRetryTimeout: 8000,
|
||||
|
||||
levelLoadingTimeOut: 8000,
|
||||
levelLoadingMaxRetry: 6,
|
||||
levelLoadingRetryDelay: 1000,
|
||||
levelLoadingMaxRetryTimeout: 8000,
|
||||
|
||||
fragLoadingTimeOut: 8000,
|
||||
fragLoadingMaxRetry: 6,
|
||||
fragLoadingRetryDelay: 1000,
|
||||
fragLoadingMaxRetryTimeout: 8000,
|
||||
})
|
||||
|
||||
hls.on(Hls.Events.ERROR, (_evt, data) => {
|
||||
if (!hls) return
|
||||
|
||||
if (data.fatal) {
|
||||
if (data.type === Hls.ErrorTypes.NETWORK_ERROR) {
|
||||
hls.startLoad()
|
||||
return
|
||||
}
|
||||
if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
|
||||
hls.recoverMediaError()
|
||||
return
|
||||
}
|
||||
// ✅ statt "für immer kaputt": einmal hart reloaden
|
||||
hardReload()
|
||||
}
|
||||
})
|
||||
|
||||
hls.loadSource(manifestUrl)
|
||||
hls.attachMedia(video)
|
||||
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
video.play().catch((e) => console.debug('[LiveHlsVideo] play() failed', e))
|
||||
})
|
||||
}
|
||||
|
||||
let nativeCleanup: void | (() => void) = undefined
|
||||
void (async () => {
|
||||
const maybeCleanup = await start()
|
||||
if (typeof maybeCleanup === 'function') nativeCleanup = maybeCleanup
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
cleanupTimers()
|
||||
try {
|
||||
nativeCleanup?.()
|
||||
} catch {}
|
||||
try {
|
||||
hls?.destroy()
|
||||
} catch {}
|
||||
}
|
||||
}, [src, muted, manifestUrl]) // <- manifestUrl ist OK, weil reloadKey NICHT im Effect gesetzt wird
|
||||
|
||||
if (broken) {
|
||||
return (
|
||||
<div className="text-xs text-gray-400 italic">
|
||||
{brokenReason === 'private' ? 'Private' : brokenReason === 'offline' ? 'Offline' : '–'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={ref}
|
||||
className={className}
|
||||
playsInline
|
||||
autoPlay
|
||||
muted={muted}
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
onClick={() => {
|
||||
const v = ref.current
|
||||
if (v) {
|
||||
v.muted = false
|
||||
v.play().catch(() => {})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
111
frontend/src/components/ui/LiveVideo.tsx
Normal file
111
frontend/src/components/ui/LiveVideo.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
// frontend/src/components/ui/LiveVideo.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { applyInlineVideoPolicy, DEFAULT_INLINE_MUTED } from './videoPolicy'
|
||||
|
||||
export default function LiveVideo({
|
||||
src,
|
||||
muted = DEFAULT_INLINE_MUTED,
|
||||
className,
|
||||
}: {
|
||||
src: string
|
||||
muted?: boolean
|
||||
className?: string
|
||||
}) {
|
||||
const ref = useRef<HTMLVideoElement>(null)
|
||||
const [broken, setBroken] = useState(false)
|
||||
const [brokenReason, setBrokenReason] = useState<'private' | 'offline' | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let watchdogTimer: number | null = null
|
||||
|
||||
const video = ref.current
|
||||
if (!video) return
|
||||
|
||||
setBroken(false)
|
||||
setBrokenReason(null)
|
||||
|
||||
applyInlineVideoPolicy(video, { muted })
|
||||
|
||||
const hardReset = () => {
|
||||
try {
|
||||
video.pause()
|
||||
video.removeAttribute('src')
|
||||
video.load()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// 1) src setzen (fMP4/MP4 stream, single HTTP response)
|
||||
hardReset()
|
||||
video.src = src
|
||||
video.load()
|
||||
video.play().catch(() => {})
|
||||
|
||||
// 2) leichter Watchdog: wenn gar kein Fortschritt -> einmal neu setzen
|
||||
let lastT = Date.now()
|
||||
const onTime = () => {
|
||||
lastT = Date.now()
|
||||
}
|
||||
video.addEventListener('timeupdate', onTime)
|
||||
|
||||
watchdogTimer = window.setInterval(() => {
|
||||
if (cancelled) return
|
||||
// wenn nicht paused, aber 12s keine timeupdate => neu verbinden
|
||||
if (!video.paused && Date.now() - lastT > 12_000) {
|
||||
hardReset()
|
||||
video.src = src
|
||||
video.load()
|
||||
video.play().catch(() => {})
|
||||
lastT = Date.now()
|
||||
}
|
||||
}, 4_000)
|
||||
|
||||
// 3) HTTP-Fehler (403/404) erkennen ist bei <video> nicht sauber möglich.
|
||||
// Wir machen hier bewusst KEIN aggressives Retry. Broken UI nur, wenn Video "error" signalisiert.
|
||||
const onError = () => {
|
||||
// best effort: 403/404 kannst du im Backend zusätzlich über Query/JSON o.ä. signalisieren,
|
||||
// aber hier: generic broken
|
||||
setBroken(true)
|
||||
}
|
||||
video.addEventListener('error', onError)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (watchdogTimer) window.clearInterval(watchdogTimer)
|
||||
watchdogTimer = null
|
||||
video.removeEventListener('timeupdate', onTime)
|
||||
video.removeEventListener('error', onError)
|
||||
hardReset()
|
||||
}
|
||||
}, [src, muted])
|
||||
|
||||
if (broken) {
|
||||
return (
|
||||
<div className="text-xs text-gray-400 italic">
|
||||
{brokenReason === 'private' ? 'Private' : brokenReason === 'offline' ? 'Offline' : '–'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={ref}
|
||||
className={className}
|
||||
playsInline
|
||||
autoPlay
|
||||
muted={muted}
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
onClick={() => {
|
||||
const v = ref.current
|
||||
if (v) {
|
||||
v.muted = false
|
||||
v.play().catch(() => {})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -38,7 +38,7 @@ import TagOverflowRow from './TagOverflowRow'
|
||||
import PreviewScrubber from './PreviewScrubber'
|
||||
import { formatResolution } from './formatters'
|
||||
import Pagination from './Pagination'
|
||||
import LiveHlsVideo from './LiveHlsVideo'
|
||||
import LiveVideo from './LiveVideo'
|
||||
|
||||
function cn(...parts: Array<string | false | null | undefined>) {
|
||||
return parts.filter(Boolean).join(' ')
|
||||
@ -2735,8 +2735,8 @@ export default function ModelDetails({
|
||||
{/* Live HLS inline (statt Teaser) */}
|
||||
{showLive ? (
|
||||
<div className="absolute inset-0 z-10">
|
||||
<LiveHlsVideo
|
||||
src={`/api/preview/live?id=${encodeURIComponent(String((j as any)?.id ?? ''))}&file=index_hq.m3u8&hover=1`}
|
||||
<LiveVideo
|
||||
src={`/api/preview/live?id=${encodeURIComponent(String((j as any)?.id ?? ''))}&hover=1`}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
muted
|
||||
/>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import HoverPopover from './HoverPopover'
|
||||
import LiveHlsVideo from './LiveHlsVideo'
|
||||
import LiveVideo from './LiveVideo'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
type Props = {
|
||||
@ -237,7 +237,7 @@ export default function ModelPreview({
|
||||
)
|
||||
|
||||
const hq = useMemo(
|
||||
() => `/api/preview?id=${encodeURIComponent(jobId)}&hover=1&file=index_hq.m3u8`,
|
||||
() => `/api/preview/live?id=${encodeURIComponent(jobId)}&hover=1`,
|
||||
[jobId]
|
||||
)
|
||||
|
||||
@ -269,7 +269,7 @@ export default function ModelPreview({
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0">
|
||||
<LiveHlsVideo src={hq} muted={true} className="w-full h-full object-contain object-bottom relative z-0" />
|
||||
<LiveVideo src={hq} muted={true} className="w-full h-full object-contain object-bottom relative z-0" />
|
||||
|
||||
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 rounded-full bg-red-600/90 px-2 py-1 text-[11px] font-semibold text-white shadow-sm">
|
||||
<span className="inline-block size-1.5 rounded-full bg-white animate-pulse" />
|
||||
|
||||
@ -17,7 +17,7 @@ import { DEFAULT_PLAYER_START_MUTED } from './videoPolicy'
|
||||
import RecordJobActions from './RecordJobActions'
|
||||
import Button from './Button'
|
||||
import { apiUrl, apiFetch } from '../../lib/api'
|
||||
import LiveHlsVideo from './LiveHlsVideo'
|
||||
import LiveVideo from './LiveVideo'
|
||||
|
||||
const baseName = (p: string) => (p || '').replaceAll('\\', '/').split('/').pop() || ''
|
||||
const stripHotPrefix = (s: string) => (s.startsWith('HOT ') ? s.slice(4) : s)
|
||||
@ -1762,7 +1762,7 @@ export default function Player({
|
||||
>
|
||||
{isRunning ? (
|
||||
<div className="absolute inset-0 bg-black">
|
||||
<LiveHlsVideo
|
||||
<LiveVideo
|
||||
src={liveHlsSrc}
|
||||
muted={startMuted}
|
||||
className="w-full h-full object-contain object-bottom"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user