add 60s DVR buffer on focused stream and fix empty WebRTC offer
Viewer offers carried no m-lines, so no media could ever flow back. The focused card also never bound its stream because the DVR watcher ran before mount. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
88c7c632bf
commit
14028e5d7c
4 files changed
+377
-12
No files matched your search
@@ -20,13 +20,41 @@ const emit = defineEmits<{
|
||||
const videoRef = ref<HTMLVideoElement | null>(null)
|
||||
const { formatted } = useRoomTimer(() => props.startedAt)
|
||||
|
||||
const dvrEnabled = computed(() => Boolean(props.focused && props.mediaStream))
|
||||
|
||||
const {
|
||||
delayLabel,
|
||||
isLive,
|
||||
canSeekBack,
|
||||
canSeekForward,
|
||||
bufferMs,
|
||||
recording,
|
||||
error: dvrError,
|
||||
seekBack,
|
||||
seekForward,
|
||||
goLive,
|
||||
SEEK_STEP_MS,
|
||||
} = useStreamDvr({
|
||||
enabled: dvrEnabled,
|
||||
stream: () => props.mediaStream,
|
||||
videoRef,
|
||||
})
|
||||
|
||||
const bufferReadySec = computed(() => Math.floor(bufferMs.value / 1000))
|
||||
|
||||
function attachLiveStream(stream: MediaStream | null) {
|
||||
const el = videoRef.value
|
||||
if (!el) return
|
||||
el.srcObject = stream
|
||||
if (stream) el.play().catch(() => {})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.mediaStream,
|
||||
(stream) => {
|
||||
const el = videoRef.value
|
||||
if (!el) return
|
||||
el.srcObject = stream
|
||||
el.play().catch(() => {})
|
||||
() => [props.mediaStream, props.focused] as const,
|
||||
([stream, focused]) => {
|
||||
// Focused card: DVR composable owns the <video> element
|
||||
if (focused) return
|
||||
attachLiveStream(stream)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -43,12 +71,13 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.focused) {
|
||||
attachLiveStream(props.mediaStream)
|
||||
}
|
||||
const el = videoRef.value
|
||||
if (el && props.mediaStream) {
|
||||
el.srcObject = props.mediaStream
|
||||
if (el) {
|
||||
el.volume = props.volume
|
||||
el.muted = props.muted
|
||||
el.play().catch(() => {})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -63,13 +92,68 @@ onMounted(() => {
|
||||
<div class="pointer-events-none absolute left-2 top-2 rounded bg-black/60 px-2 py-1 text-xs">
|
||||
{{ title }} · {{ formatted }}
|
||||
</div>
|
||||
<div
|
||||
v-if="focused"
|
||||
class="absolute right-2 top-2 flex items-center gap-1.5"
|
||||
>
|
||||
<span
|
||||
class="rounded px-2 py-1 text-xs font-semibold"
|
||||
:class="isLive ? 'bg-red-600 text-white' : 'bg-amber-500/90 text-black'"
|
||||
>
|
||||
{{ delayLabel }}
|
||||
</span>
|
||||
<span
|
||||
v-if="recording"
|
||||
class="rounded bg-black/60 px-2 py-1 text-[10px] text-[var(--text-muted)]"
|
||||
>
|
||||
버퍼 {{ bufferReadySec }}s
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="viewerNames.length"
|
||||
class="pointer-events-none absolute bottom-2 left-2 max-w-[80%] truncate rounded bg-black/60 px-2 py-1 text-[11px] text-[var(--text-muted)]"
|
||||
class="pointer-events-none absolute bottom-2 left-2 max-w-[70%] truncate rounded bg-black/60 px-2 py-1 text-[11px] text-[var(--text-muted)]"
|
||||
>
|
||||
시청: {{ viewerNames.join(', ') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="focused"
|
||||
class="flex flex-wrap items-center gap-2 border-t border-[var(--border)] bg-[var(--bg-elevated)] px-3 py-2"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-ghost !px-2 !py-1 text-xs"
|
||||
:disabled="!canSeekBack"
|
||||
title="5초 뒤로 (←)"
|
||||
@click="seekBack"
|
||||
>
|
||||
⏪ -{{ SEEK_STEP_MS / 1000 }}s
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-ghost !px-2 !py-1 text-xs"
|
||||
:disabled="!canSeekForward"
|
||||
title="5초 앞으로 (→)"
|
||||
@click="seekForward"
|
||||
>
|
||||
+{{ SEEK_STEP_MS / 1000 }}s ⏩
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-primary !px-3 !py-1 text-xs"
|
||||
:disabled="isLive"
|
||||
title="라이브로 복귀 (L / End)"
|
||||
@click="goLive"
|
||||
>
|
||||
라이브로
|
||||
</button>
|
||||
<span class="text-[11px] text-[var(--text-muted)]">
|
||||
←/→ 5초 · L 라이브 · 최대 약 1분
|
||||
</span>
|
||||
<p v-if="dvrError" class="w-full text-xs text-[var(--danger)]">{{ dvrError }}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 border-t border-[var(--border)] bg-[var(--bg-panel)] px-3 py-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in new issue
Block a user