Claude güncellemesi Video Player güncellemesi
This commit is contained in:
parent
1fe4abaeb6
commit
5217887bb8
3 changed files with 255 additions and 244 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { lazy, Suspense } from 'react'
|
||||
import { lazy, Suspense, useMemo, type KeyboardEvent } from 'react'
|
||||
import { FaMicrophoneSlash, FaUserTimes } from 'react-icons/fa'
|
||||
import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models'
|
||||
import { Button } from '@/components/ui'
|
||||
|
|
@ -16,13 +16,24 @@ interface RoomParticipantProps {
|
|||
isAudioEnabled: boolean
|
||||
isVideoEnabled: boolean
|
||||
onMuteParticipant?: (participantId: string, isMuted: boolean, isTeacher: boolean) => void
|
||||
layout: VideoroomLayoutDto
|
||||
layout?: VideoroomLayoutDto
|
||||
focusedParticipant?: string
|
||||
onParticipantFocus?: (participantId: string | undefined) => void
|
||||
onKickParticipant?: (participantId: string) => void
|
||||
hasSidePanel?: boolean
|
||||
}
|
||||
|
||||
/** Katılımcı sayısına göre kolon sayısı. Satırlar `auto-rows-fr` ile eşit yüksekliğe dağıtılır. */
|
||||
const getGridColumns = (count: number) => {
|
||||
if (count <= 1) return 'grid-cols-1'
|
||||
if (count <= 2) return 'grid-cols-1 sm:grid-cols-2'
|
||||
if (count <= 4) return 'grid-cols-2 sm:grid-cols-2'
|
||||
if (count <= 9) return 'grid-cols-2 sm:grid-cols-3'
|
||||
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
|
||||
}
|
||||
|
||||
const VideoTileFallback = () => <div className="h-full w-full animate-pulse bg-gray-900" />
|
||||
|
||||
export const RoomParticipant = ({
|
||||
participants,
|
||||
localStream,
|
||||
|
|
@ -38,248 +49,191 @@ export const RoomParticipant = ({
|
|||
onKickParticipant,
|
||||
hasSidePanel = false,
|
||||
}: RoomParticipantProps) => {
|
||||
// Only show current user's video once
|
||||
const currentUserParticipant = {
|
||||
id: currentUserId,
|
||||
name: currentUserName,
|
||||
const allParticipants = useMemo<VideoroomParticipantDto[]>(() => {
|
||||
const remote = (participants ?? []).filter((p) => p.id !== currentUserId)
|
||||
const self: VideoroomParticipantDto = {
|
||||
id: currentUserId,
|
||||
name: currentUserName,
|
||||
sessionId: remote[0]?.sessionId ?? '',
|
||||
isTeacher,
|
||||
isAudioMuted: !isAudioEnabled,
|
||||
isVideoMuted: !isVideoEnabled,
|
||||
isActive: true,
|
||||
stream: localStream ?? undefined,
|
||||
}
|
||||
return [self, ...remote]
|
||||
}, [
|
||||
participants,
|
||||
currentUserId,
|
||||
currentUserName,
|
||||
isTeacher,
|
||||
stream: localStream ?? undefined,
|
||||
} as unknown as VideoroomParticipantDto
|
||||
isAudioEnabled,
|
||||
isVideoEnabled,
|
||||
localStream,
|
||||
])
|
||||
|
||||
// Eğer hiç katılımcı yoksa ve localStream de yoksa hiçbir şey render etme
|
||||
if (!localStream && (!participants || participants.length === 0)) {
|
||||
return null
|
||||
const renderParticipant = (
|
||||
participant: VideoroomParticipantDto,
|
||||
{ isMain = false }: { isMain?: boolean } = {},
|
||||
) => {
|
||||
const isSelf = participant.id === currentUserId
|
||||
const isFocusable = !isMain && !!onParticipantFocus
|
||||
const canModerate = isTeacher && !isSelf
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative h-full w-full min-h-0 min-w-0 ${isFocusable ? 'cursor-pointer' : ''}`}
|
||||
{...(isFocusable
|
||||
? {
|
||||
role: 'button' as const,
|
||||
tabIndex: 0,
|
||||
title: `${participant.name} görüntüsünü büyüt`,
|
||||
onClick: () => onParticipantFocus?.(participant.id),
|
||||
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onParticipantFocus?.(participant.id)
|
||||
}
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<Suspense fallback={<VideoTileFallback />}>
|
||||
<VideoPlayer
|
||||
stream={participant.stream}
|
||||
isLocal={isSelf}
|
||||
userName={participant.name}
|
||||
isAudioEnabled={isSelf ? isAudioEnabled : !participant.isAudioMuted}
|
||||
isVideoEnabled={isSelf ? isVideoEnabled : !participant.isVideoMuted}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
{participant.isHandRaised && !isSelf && (
|
||||
<div className="absolute right-1 top-8 rounded-full bg-yellow-500 px-2 py-0.5 text-xs text-white sm:right-2 sm:top-10">
|
||||
✋
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canModerate && (
|
||||
<div className="absolute left-2 top-2 z-10 flex space-x-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
|
||||
}}
|
||||
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-1 text-xs text-white ${
|
||||
participant.isAudioMuted ? '!bg-red-600' : '!bg-gray-600 hover:!bg-gray-700'
|
||||
} transition-colors`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
<FaMicrophoneSlash size={12} />
|
||||
</Button>
|
||||
{onKickParticipant && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onKickParticipant(participant.id)
|
||||
}}
|
||||
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-1 text-xs text-white transition-colors hover:!bg-red-700"
|
||||
title="Sınıftan Çıkar"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const allParticipants = [currentUserParticipant, ...participants]
|
||||
|
||||
// Ortak ana video kutusu container class'ı
|
||||
const mainVideoContainerClass = 'w-full h-full flex flex-col justify-center'
|
||||
|
||||
const renderGridLayout = () => {
|
||||
const getGridClass = (participantCount: number) => {
|
||||
if (participantCount === 1) return 'grid-cols-1'
|
||||
if (participantCount <= 2) return 'grid-cols-1 sm:grid-cols-2'
|
||||
if (participantCount <= 4) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2'
|
||||
if (participantCount <= 6) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
|
||||
if (participantCount <= 9) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'
|
||||
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
|
||||
}
|
||||
const count = allParticipants.length
|
||||
const gap = count <= 4 ? 'gap-2 sm:gap-3' : 'gap-1 sm:gap-2'
|
||||
const padding = count === 1 ? 'p-0' : count <= 4 ? 'p-2 sm:p-4' : 'p-1 sm:p-2'
|
||||
|
||||
const getGridRows = (participantCount: number) => {
|
||||
if (participantCount === 1) return 'grid-rows-1'
|
||||
if (participantCount <= 2) return 'grid-rows-1 sm:grid-rows-1'
|
||||
if (participantCount <= 4) return 'grid-rows-2 lg:grid-rows-2'
|
||||
if (participantCount <= 6) return 'grid-rows-3 sm:grid-rows-2'
|
||||
if (participantCount <= 9) return 'grid-rows-3'
|
||||
return 'grid-rows-4 sm:grid-rows-3'
|
||||
}
|
||||
|
||||
const getPadding = (participantCount: number) => {
|
||||
if (participantCount === 1) return ''
|
||||
if (participantCount <= 4) return 'p-2 sm:p-4'
|
||||
return 'p-1 sm:p-2'
|
||||
}
|
||||
|
||||
const getGap = (participantCount: number) => {
|
||||
if (participantCount === 1) return 'gap-0'
|
||||
if (participantCount <= 4) return 'gap-2 sm:gap-3'
|
||||
return 'gap-1 sm:gap-2'
|
||||
}
|
||||
|
||||
// Mobilde: En üstte öğretmen, altında katılımcılar 2'li grid ve dikey scroll
|
||||
const mainParticipant = allParticipants[0]
|
||||
const otherParticipants = allParticipants.slice(1)
|
||||
return (
|
||||
<>
|
||||
{/* Mobil özel layout */}
|
||||
<div className="sm:hidden w-full h-full flex flex-col items-center overflow-hidden p-2">
|
||||
{/* Ana katılımcı */}
|
||||
<div className="w-full max-w-md mx-auto flex-none flex items-center justify-center mb-2">
|
||||
<div className="w-full aspect-video max-h-[40vh] rounded-xl overflow-hidden flex bg-white/10 shadow-md border border-white/10">
|
||||
{renderParticipant(mainParticipant, true)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Diğer katılımcılar 2'li grid ve dikey scroll */}
|
||||
{otherParticipants.length > 0 && (
|
||||
<div className={`h-full min-h-0 overflow-y-auto ${padding}`}>
|
||||
<div
|
||||
className={`grid h-full min-h-0 auto-rows-fr ${getGridColumns(count)} ${gap} place-items-stretch`}
|
||||
>
|
||||
{allParticipants.map((participant, index) => (
|
||||
<div
|
||||
className="w-full max-w-md mx-auto flex-1 overflow-y-auto grid grid-cols-1 gap-2 pb-2 min-h-0"
|
||||
style={{ maxHeight: '55vh' }}
|
||||
key={participant.id}
|
||||
className={`min-h-0 min-w-0 overflow-hidden rounded-lg sm:rounded-xl ${
|
||||
// Mobilde ilk katılımcı tam genişlik kaplar
|
||||
index === 0 && count > 2 ? 'col-span-2 sm:col-span-1' : ''
|
||||
}`}
|
||||
>
|
||||
{Array.from({ length: Math.ceil(otherParticipants.length / 2) }).map((_, rowIdx) => (
|
||||
<div key={rowIdx} className="flex gap-2">
|
||||
{otherParticipants.slice(rowIdx * 2, rowIdx * 2 + 2).map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="flex-1 aspect-video rounded-lg overflow-hidden flex bg-white/10 shadow border border-white/10"
|
||||
>
|
||||
{renderParticipant(participant, false, true)}
|
||||
</div>
|
||||
))}
|
||||
{otherParticipants.length % 2 === 1 &&
|
||||
rowIdx === Math.floor(otherParticipants.length / 2) ? (
|
||||
<div className="flex-1" />
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{renderParticipant(participant)}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
{/* Masaüstü ve tablet için eski grid layout */}
|
||||
<div className="hidden sm:flex h-full items-center justify-center overflow-hidden">
|
||||
<div
|
||||
className={`w-full h-full flex flex-col justify-center ${getPadding(allParticipants.length)}`}
|
||||
>
|
||||
<div
|
||||
className={`h-full grid ${getGridClass(allParticipants.length)} ${getGridRows(allParticipants.length)} ${getGap(allParticipants.length)} place-items-stretch`}
|
||||
>
|
||||
{allParticipants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="w-full h-full max-h-full flex items-stretch justify-stretch min-h-0"
|
||||
>
|
||||
<div className="w-full h-full rounded-lg sm:rounded-xl overflow-hidden flex">
|
||||
{renderParticipant(participant, false)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSidebarLayout = () => {
|
||||
const mainParticipant = focusedParticipant
|
||||
? allParticipants.find((p) => p.id === focusedParticipant) || allParticipants[0]
|
||||
: allParticipants[0]
|
||||
const mainParticipant =
|
||||
allParticipants.find((p) => p.id === focusedParticipant) ?? allParticipants[0]
|
||||
const otherParticipants = allParticipants.filter((p) => p.id !== mainParticipant.id)
|
||||
|
||||
const sidebarWidth = hasSidePanel
|
||||
? 'w-20 sm:w-24 md:w-32 lg:w-40'
|
||||
: 'w-24 sm:w-32 md:w-40 lg:w-48'
|
||||
|
||||
// Eğer hiç katılımcı yoksa, video player öğretmen odaklı gibi ortalanır ve geniş olur
|
||||
return (
|
||||
<div className="h-screen flex items-center justify-center p-0">
|
||||
<div className={mainVideoContainerClass + ' h-full'}>
|
||||
<div className="flex h-full">
|
||||
<div className={`flex-1 min-w-0 flex items-center justify-center`}>
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="w-full h-full rounded-xl overflow-hidden transition-all duration-200">
|
||||
{renderParticipant(mainParticipant, true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{otherParticipants.length > 0 && (
|
||||
<div className={`${sidebarWidth} p-2 overflow-y-auto rounded-l-lg h-full`}>
|
||||
<div className="flex flex-col gap-2 h-full min-w-0">
|
||||
{otherParticipants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="rounded-lg border border-blue-300/40 shadow shadow-blue-200/20 backdrop-blur-sm transition-all duration-200"
|
||||
>
|
||||
{renderParticipant(participant, false, true)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex h-full min-h-0">
|
||||
<div className="flex min-w-0 flex-1 items-center justify-center p-1 sm:p-2">
|
||||
<div className="h-full w-full overflow-hidden rounded-xl">
|
||||
{renderParticipant(mainParticipant, { isMain: true })}
|
||||
</div>
|
||||
</div>
|
||||
{otherParticipants.length > 0 && (
|
||||
<div className={`${sidebarWidth} h-full min-h-0 overflow-y-auto p-2`}>
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
{otherParticipants.map((participant) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className="aspect-video shrink-0 overflow-hidden rounded-lg border border-blue-300/40 shadow shadow-blue-200/20 transition-all duration-200"
|
||||
>
|
||||
{renderParticipant(participant)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderTeacherFocusLayout = () => {
|
||||
// Sadece öğretmen gösterilecek, katılımcılar asla gösterilmeyecek
|
||||
const teacher = allParticipants.find((p) => p.isTeacher) || allParticipants[0]
|
||||
const teacher = allParticipants.find((p) => p.isTeacher) ?? allParticipants[0]
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center overflow-hidden">
|
||||
<div className="w-full h-full flex flex-col justify-center ">
|
||||
<div className="h-full w-full max-h-full flex items-center justify-center">
|
||||
<div className="w-full h-full rounded-lg sm:rounded-xl overflow-hidden">
|
||||
{renderParticipant(teacher, true)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full min-h-0 overflow-hidden">
|
||||
<div className="h-full w-full overflow-hidden rounded-lg sm:rounded-xl">
|
||||
{renderParticipant(teacher, { isMain: true })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderParticipant = (
|
||||
participant: VideoroomParticipantDto,
|
||||
isMain: boolean = false,
|
||||
isSmall: boolean = false,
|
||||
) => (
|
||||
<div
|
||||
key={participant.id}
|
||||
className={`relative w-full h-full ${isMain ? '' : isSmall ? 'aspect-video' : ''} ${!isMain && onParticipantFocus ? 'cursor-pointer' : ''}`}
|
||||
onClick={() => !isMain && onParticipantFocus?.(participant.id)}
|
||||
style={{ minHeight: 0, minWidth: 0 }}
|
||||
>
|
||||
<div className="absolute inset-0 w-full h-full">
|
||||
<Suspense fallback={<div className="h-full w-full bg-gray-900" />}>
|
||||
<VideoPlayer
|
||||
stream={participant.stream}
|
||||
isLocal={participant.id === currentUserId}
|
||||
userName={participant.name}
|
||||
isAudioEnabled={
|
||||
participant.id === currentUserId ? isAudioEnabled : !participant.isAudioMuted
|
||||
}
|
||||
isVideoEnabled={
|
||||
participant.id === currentUserId ? isVideoEnabled : !participant.isVideoMuted
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* Teacher controls for students */}
|
||||
{isTeacher && participant.id !== currentUserId && (
|
||||
<div className="absolute top-2 left-2 flex space-x-1 z-10">
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onMuteParticipant?.(participant.id, !participant.isAudioMuted, isTeacher)
|
||||
}}
|
||||
className={`!inline-flex !h-auto items-center justify-center rounded-full !p-1 text-xs text-white ${
|
||||
participant.isAudioMuted ? '!bg-red-600' : '!bg-gray-600 hover:!bg-gray-700'
|
||||
} transition-colors`}
|
||||
title={participant.isAudioMuted ? 'Sesi Aç' : 'Sesi Kapat'}
|
||||
>
|
||||
<FaMicrophoneSlash size={12} />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="plain"
|
||||
shape="circle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onKickParticipant?.(participant.id)
|
||||
}}
|
||||
className="!inline-flex !h-auto items-center justify-center rounded-full !bg-red-600 !p-1 text-xs text-white transition-colors hover:!bg-red-700"
|
||||
title="Sınıftan Çıkar"
|
||||
>
|
||||
<FaUserTimes size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderLayout = () => {
|
||||
switch (layout.type) {
|
||||
case 'sidebar':
|
||||
return renderSidebarLayout()
|
||||
case 'teacher-focus':
|
||||
return renderTeacherFocusLayout()
|
||||
default:
|
||||
return renderGridLayout()
|
||||
}
|
||||
// Ne yerel yayın ne de katılımcı varsa gösterilecek bir şey yok
|
||||
if (!localStream && allParticipants.length <= 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className="h-full min-h-0 flex flex-col">{renderLayout()}</div>
|
||||
switch (layout?.type) {
|
||||
case 'sidebar':
|
||||
return <div className="h-full min-h-0">{renderSidebarLayout()}</div>
|
||||
case 'teacher-focus':
|
||||
return <div className="h-full min-h-0">{renderTeacherFocusLayout()}</div>
|
||||
default:
|
||||
return <div className="h-full min-h-0">{renderGridLayout()}</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,16 @@ export const ScreenSharePanel = ({
|
|||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = sharedScreen ?? null
|
||||
const videoEl = videoRef.current
|
||||
if (!videoEl) return
|
||||
|
||||
videoEl.srcObject = sharedScreen ?? null
|
||||
if (sharedScreen) {
|
||||
void videoEl.play().catch(() => undefined)
|
||||
}
|
||||
|
||||
return () => {
|
||||
videoEl.srcObject = null
|
||||
}
|
||||
}, [sharedScreen])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
import { useRef, useEffect } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { FaMicrophoneSlash, FaVideoSlash } from 'react-icons/fa'
|
||||
|
||||
const VideoOff = ({
|
||||
size = 24,
|
||||
className = '',
|
||||
}: {
|
||||
size?: number
|
||||
className?: string
|
||||
}) => <FaVideoSlash size={size} className={className} />
|
||||
|
||||
interface VideoPlayerProps {
|
||||
stream?: MediaStream
|
||||
isLocal?: boolean
|
||||
|
|
@ -17,6 +9,53 @@ interface VideoPlayerProps {
|
|||
isVideoEnabled?: boolean
|
||||
}
|
||||
|
||||
const getInitials = (name: string) =>
|
||||
name
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.slice(0, 2)
|
||||
.map((part) => part.charAt(0).toLocaleUpperCase('tr-TR'))
|
||||
.join('')
|
||||
|
||||
/** Stream üzerinde oynatılabilir bir video track var mı? Track ekleme/çıkarma ve mute olaylarını da izler. */
|
||||
const useHasLiveVideoTrack = (stream?: MediaStream) => {
|
||||
const [hasVideo, setHasVideo] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!stream) {
|
||||
setHasVideo(false)
|
||||
return
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
setHasVideo(stream.getVideoTracks().some((track) => track.readyState === 'live' && !track.muted))
|
||||
}
|
||||
|
||||
sync()
|
||||
|
||||
const tracks = stream.getVideoTracks()
|
||||
tracks.forEach((track) => {
|
||||
track.addEventListener('mute', sync)
|
||||
track.addEventListener('unmute', sync)
|
||||
track.addEventListener('ended', sync)
|
||||
})
|
||||
stream.addEventListener('addtrack', sync)
|
||||
stream.addEventListener('removetrack', sync)
|
||||
|
||||
return () => {
|
||||
tracks.forEach((track) => {
|
||||
track.removeEventListener('mute', sync)
|
||||
track.removeEventListener('unmute', sync)
|
||||
track.removeEventListener('ended', sync)
|
||||
})
|
||||
stream.removeEventListener('addtrack', sync)
|
||||
stream.removeEventListener('removetrack', sync)
|
||||
}
|
||||
}, [stream])
|
||||
|
||||
return hasVideo
|
||||
}
|
||||
|
||||
export const VideoPlayer = ({
|
||||
stream,
|
||||
isLocal = false,
|
||||
|
|
@ -25,55 +64,65 @@ export const VideoPlayer = ({
|
|||
isVideoEnabled = true,
|
||||
}: VideoPlayerProps) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const hasLiveVideoTrack = useHasLiveVideoTrack(stream)
|
||||
const showVideo = isVideoEnabled && hasLiveVideoTrack
|
||||
|
||||
useEffect(() => {
|
||||
const videoEl = videoRef.current
|
||||
if (!videoEl) return
|
||||
|
||||
videoEl.srcObject = stream ?? null
|
||||
|
||||
if (stream) {
|
||||
videoEl.srcObject = stream
|
||||
} else {
|
||||
videoEl.srcObject = null
|
||||
// Autoplay politikası nedeniyle reddedilebilir; kullanıcı etkileşiminde tekrar denenir.
|
||||
void videoEl.play().catch(() => undefined)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (videoEl) {
|
||||
videoEl.srcObject = null
|
||||
}
|
||||
videoEl.srcObject = null
|
||||
}
|
||||
}, [stream])
|
||||
|
||||
return (
|
||||
<div className="relative bg-gray-900 rounded-md sm:rounded-lg overflow-hidden p-1 sm:p-2 h-full">
|
||||
{/* Video sadece kamera açıkken göster */}
|
||||
<div className="relative h-full w-full overflow-hidden rounded-md bg-gray-900 sm:rounded-lg">
|
||||
{/*
|
||||
Uzak katılımcının sesi bu element üzerinden çalındığı için kamera kapalıyken de
|
||||
element DOM'da kalır, yalnızca görünürlüğü kapatılır.
|
||||
*/}
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted={isLocal}
|
||||
className="w-full h-full object-cover"
|
||||
style={{ display: isVideoEnabled ? 'block' : 'none' }}
|
||||
aria-label={`${userName} video`}
|
||||
className={`h-full w-full object-cover ${isLocal ? 'scale-x-[-1]' : ''} ${
|
||||
showVideo ? '' : 'invisible'
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* User name overlay */}
|
||||
<div className="absolute bottom-1 sm:bottom-2 left-1 sm:left-2 bg-black bg-opacity-50 text-white px-1 sm:px-2 py-0.5 sm:py-1 rounded text-xs sm:text-sm">
|
||||
{userName} {isLocal && '(You)'}
|
||||
</div>
|
||||
|
||||
{/* Video kapalıysa avatar/placeholder göster */}
|
||||
{!isVideoEnabled && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-800">
|
||||
<div className="text-center text-white">
|
||||
<VideoOff size={24} className="mx-auto mb-1 sm:mb-2 text-white sm:size-8" />
|
||||
<p className="text-xs sm:text-sm">{userName}</p>
|
||||
{!showVideo && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-gray-800 text-white">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gray-600 text-sm font-semibold sm:h-16 sm:w-16 sm:text-lg">
|
||||
{getInitials(userName) || <FaVideoSlash size={20} />}
|
||||
</div>
|
||||
<p className="max-w-full truncate px-2 text-xs sm:text-sm">{userName}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio indicator */}
|
||||
<div
|
||||
className="absolute bottom-1 left-1 max-w-[calc(100%-0.5rem)] truncate rounded bg-black/50 px-1 py-0.5 text-xs text-white sm:bottom-2 sm:left-2 sm:px-2 sm:py-1 sm:text-sm"
|
||||
title={userName}
|
||||
>
|
||||
{userName}
|
||||
{isLocal && ' (Siz)'}
|
||||
</div>
|
||||
|
||||
{!isAudioEnabled && (
|
||||
<div className="absolute top-1 sm:top-2 right-1 sm:right-2 bg-red-500 rounded-full p-0.5 sm:p-1">
|
||||
<FaMicrophoneSlash size={12} className="text-white sm:size-4" />
|
||||
<div
|
||||
className="absolute right-1 top-1 rounded-full bg-red-500 p-1 sm:right-2 sm:top-2"
|
||||
title="Mikrofon kapalı"
|
||||
>
|
||||
<FaMicrophoneSlash size={12} className="text-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue