From 14028e5d7c8563d5ad07587be67452e1b0427cde Mon Sep 17 00:00:00 2001 From: Sejong Yoo Date: Wed, 12 Aug 2026 15:54:25 +0900 Subject: [PATCH] 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 --- components/room/StreamGrid.vue | 2 +- components/room/VideoPlayerCard.vue | 104 ++++++++++- composables/useStreamDvr.ts | 275 ++++++++++++++++++++++++++++ composables/useWebRTC.ts | 8 +- 4 files changed, 377 insertions(+), 12 deletions(-) create mode 100644 composables/useStreamDvr.ts diff --git a/components/room/StreamGrid.vue b/components/room/StreamGrid.vue index f4ff6f8..4d949fb 100644 --- a/components/room/StreamGrid.vue +++ b/components/room/StreamGrid.vue @@ -83,7 +83,7 @@ const gridClass = computed(() => { :viewer-names="viewerNamesByStream[card.streamId] || []" :volume="volumeMap[card.streamId] ?? 1" :muted="mutedMap[card.streamId] ?? false" - :focused="focusStreamId === card.streamId" + :focused="layoutMode === 'focus' ? index === 0 : focusStreamId === card.streamId" @update:volume="emit('update:volume', card.streamId, $event)" @update:muted="emit('update:muted', card.streamId, $event)" @stop="emit('stop', card.streamId)" diff --git a/components/room/VideoPlayerCard.vue b/components/room/VideoPlayerCard.vue index 65c2497..b00fbf4 100644 --- a/components/room/VideoPlayerCard.vue +++ b/components/room/VideoPlayerCard.vue @@ -20,13 +20,41 @@ const emit = defineEmits<{ const videoRef = ref(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