-
-
- {renderParticipant(teacher, true)}
-
-
+
+
+ {renderParticipant(teacher, { isMain: true })}
)
}
- const renderParticipant = (
- participant: VideoroomParticipantDto,
- isMain: boolean = false,
- isSmall: boolean = false,
- ) => (
-
!isMain && onParticipantFocus?.(participant.id)}
- style={{ minHeight: 0, minWidth: 0 }}
- >
-
- }>
-
-
-
- {/* Teacher controls for students */}
- {isTeacher && participant.id !== currentUserId && (
-
- {
- 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'}
- >
-
-
- {
- 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"
- >
-
-
-
- )}
-
- )
-
- 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
{renderLayout()}
+ switch (layout?.type) {
+ case 'sidebar':
+ return
{renderSidebarLayout()}
+ case 'teacher-focus':
+ return
{renderTeacherFocusLayout()}
+ default:
+ return
{renderGridLayout()}
+ }
}
diff --git a/ui/src/views/admin/videoroom/ScreenSharePanel.tsx b/ui/src/views/admin/videoroom/ScreenSharePanel.tsx
index 81a54aa7..08afad98 100644
--- a/ui/src/views/admin/videoroom/ScreenSharePanel.tsx
+++ b/ui/src/views/admin/videoroom/ScreenSharePanel.tsx
@@ -20,8 +20,16 @@ export const ScreenSharePanel = ({
const videoRef = useRef
(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])
diff --git a/ui/src/views/admin/videoroom/VideoPlayer.tsx b/ui/src/views/admin/videoroom/VideoPlayer.tsx
index fca74673..30e18685 100644
--- a/ui/src/views/admin/videoroom/VideoPlayer.tsx
+++ b/ui/src/views/admin/videoroom/VideoPlayer.tsx
@@ -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
-}) =>
-
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(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 (
-
- {/* Video sadece kamera açıkken göster */}
+
+ {/*
+ 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.
+ */}
- {/* User name overlay */}
-
- {userName} {isLocal && '(You)'}
-
-
- {/* Video kapalıysa avatar/placeholder göster */}
- {!isVideoEnabled && (
-
-
-
-
{userName}
+ {!showVideo && (
+
+
+ {getInitials(userName) || }
+
{userName}
)}
- {/* Audio indicator */}
+
+ {userName}
+ {isLocal && ' (Siz)'}
+
+
{!isAudioEnabled && (
-