Files
seojng-tv/components/room/StreamGrid.vue
T
ysj7705andCursor 14028e5d7c
CI / Type-check & Build (push) Successful in 10m32s
CI / Build & Push image (push) Successful in 12m57s
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>
2026-08-12 15:54:25 +09:00

100 lines
3.1 KiB
Vue

<script setup lang="ts">
import type { ActiveStream } from '~/types/room'
const props = defineProps<{
streams: ActiveStream[]
remoteStreams: Record<string, MediaStream>
localPreview?: { streamId: string; stream: MediaStream; title: string; startedAt: number } | null
layoutMode: 'grid' | 'focus'
focusStreamId: string | null
volumeMap: Record<string, number>
mutedMap: Record<string, boolean>
viewerNamesByStream: Record<string, string[]>
}>()
const emit = defineEmits<{
'update:volume': [streamId: string, value: number]
'update:muted': [streamId: string, value: boolean]
stop: [streamId: string]
focus: [streamId: string]
}>()
const cards = computed(() => {
const list: Array<{
streamId: string
title: string
mediaStream: MediaStream | null
startedAt: number
isLocal?: boolean
}> = []
if (props.localPreview) {
list.push({
streamId: props.localPreview.streamId,
title: props.localPreview.title,
mediaStream: props.localPreview.stream,
startedAt: props.localPreview.startedAt,
isLocal: true,
})
}
for (const s of props.streams) {
if (props.localPreview?.streamId === s.streamId) continue
list.push({
streamId: s.streamId,
title: s.broadcasterNickname,
mediaStream: props.remoteStreams[s.streamId] || null,
startedAt: s.startedAt,
})
}
if (props.layoutMode === 'focus' && props.focusStreamId) {
return [...list].sort((a, b) => {
if (a.streamId === props.focusStreamId) return -1
if (b.streamId === props.focusStreamId) return 1
return 0
})
}
return list
})
const gridClass = computed(() => {
if (props.layoutMode === 'focus') {
return 'grid grid-cols-1 gap-3 lg:grid-cols-4'
}
const n = cards.value.length
if (n <= 1) return 'grid grid-cols-1 gap-3'
if (n === 2) return 'grid grid-cols-1 gap-3 md:grid-cols-2'
if (n <= 4) return 'grid grid-cols-1 gap-3 md:grid-cols-2'
return 'grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3'
})
</script>
<template>
<div :class="gridClass">
<RoomVideoPlayerCard
v-for="(card, index) in cards"
:key="card.streamId"
:class="layoutMode === 'focus' && index === 0 ? 'lg:col-span-3 lg:row-span-2' : ''"
:stream-id="card.streamId"
:title="card.isLocal ? `${card.title} (미리보기)` : card.title"
:media-stream="card.mediaStream"
:started-at="card.startedAt"
:viewer-names="viewerNamesByStream[card.streamId] || []"
:volume="volumeMap[card.streamId] ?? 1"
:muted="mutedMap[card.streamId] ?? false"
: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)"
@focus="emit('focus', card.streamId)"
/>
<div
v-if="!cards.length"
class="flex min-h-[240px] items-center justify-center rounded-lg border border-dashed border-[var(--border)] text-sm text-[var(--text-muted)]"
>
시청할 화면을 왼쪽에서 선택하세요.
</div>
</div>
</template>