Uyarılar değiştirildi kullanıcıya gösterildi. toast

This commit is contained in:
Sedat Öztürk 2025-09-01 00:52:24 +03:00
parent 60ced627b4
commit 9db84f137e
6 changed files with 258 additions and 226 deletions

View file

@ -17712,7 +17712,7 @@
"isActive": false,
"isScheduled": true,
"participantCount": 0,
"settingsJson": null
"settingsJson": "{\"AllowHandRaise\":true,\"AllowStudentChat\":true,\"AllowPrivateMessages\":true,\"AllowStudentScreenShare\":true,\"DefaultMicrophoneState\":\"muted\",\"DefaultCameraState\":\"off\",\"DefaultLayout\":\"grid\",\"AutoMuteNewParticipants\":true}"
},
{
"name": "Fizik 201 - Kuantum Mekaniği",
@ -17729,7 +17729,7 @@
"isActive": false,
"isScheduled": true,
"participantCount": 0,
"settingsJson": null
"settingsJson": "{\"AllowHandRaise\":true,\"AllowStudentChat\":true,\"AllowPrivateMessages\":true,\"AllowStudentScreenShare\":true,\"DefaultMicrophoneState\":\"muted\",\"DefaultCameraState\":\"off\",\"DefaultLayout\":\"grid\",\"AutoMuteNewParticipants\":true}"
},
{
"name": "Kimya 301 - Organik Kimya",
@ -17746,7 +17746,7 @@
"isActive": false,
"isScheduled": true,
"participantCount": 0,
"settingsJson": null
"settingsJson": "{\"AllowHandRaise\":true,\"AllowStudentChat\":true,\"AllowPrivateMessages\":true,\"AllowStudentScreenShare\":true,\"DefaultMicrophoneState\":\"muted\",\"DefaultCameraState\":\"off\",\"DefaultLayout\":\"grid\",\"AutoMuteNewParticipants\":true}"
}
]
}

View file

@ -121,8 +121,6 @@ public class ClassroomHub : Hub
[HubMethodName("JoinClass")]
public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher, bool isActive)
{
_logger.LogInformation("🔵 JoinClass çağrıldı: User={UserId}, Session={SessionId}, IsTeacher={IsTeacher}", userId, sessionId, isTeacher);
var classroom = await _classSessionRepository.GetAsync(sessionId);
if (classroom == null)
{
@ -235,9 +233,6 @@ public class ClassroomHub : Hub
await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantLeft", new { UserId = userId.Value, SessionId = sessionId });
_logger.LogInformation("User {UserId} left class {SessionId}", userId, sessionId);
}
[HubMethodName("MuteParticipant")]
@ -412,17 +407,13 @@ public class ClassroomHub : Hub
x => x.SessionId == sessionId && x.UserId == participantId
);
_logger.LogInformation("👢 KickParticipant çağrıldı: Session={SessionId}, Target={ParticipantId}", sessionId, participantId);
if (participant == null)
{
_logger.LogWarning("⚠️ KickParticipant: participant bulunamadı (Session={SessionId}, Target={ParticipantId})", sessionId, participantId);
_logger.LogWarning($"⚠️ KickParticipant: participant bulunamadı Session={sessionId}, Target={participantId})");
await Clients.Caller.SendAsync("Warning", "Participant bulunamadı");
return;
}
_logger.LogInformation("✅ Kick öncesi durum: IsActive={IsActive}, IsKicked={IsKicked}, ConnId={ConnId}", participant.IsActive, participant.IsKicked, participant.ConnectionId);
// ConnectionId'yi cache et (null yazmadan önce)
var connectionId = participant.ConnectionId;
@ -454,15 +445,12 @@ public class ClassroomHub : Hub
});
// 6. Log
_logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}",
participantId, sessionId);
_logger.LogInformation($"👢 Participant {participantId} kicked from session {sessionId}");
await Clients.Caller.SendAsync("Info", "Kick işlemi başarısız oldu.");
}
catch (Exception ex)
{
_logger.LogError(ex,
"❌ KickParticipant hata verdi (Session={SessionId}, Participant={ParticipantId})",
sessionId, participantId);
_logger.LogError(ex, $"❌ KickParticipant hata verdi (Session={sessionId}, Participant={participantId})");
await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu.");
}
}
@ -505,7 +493,6 @@ public class ClassroomHub : Hub
[HubMethodName("SendOffer")]
public async Task SendOfferAsync(Guid sessionId, Guid targetUserId, object offer)
{
_logger.LogInformation("➡️ SendOffer to {TargetUserId}, from {CurrentUser}", targetUserId, _currentUser.Id);
await Clients.User(targetUserId.ToString())
.SendAsync("ReceiveOffer", _currentUser.Id?.ToString(), offer);
}
@ -532,13 +519,11 @@ public class ClassroomHub : Hub
var userId = _currentUser.Id;
if (!userId.HasValue)
{
_logger.LogWarning("OnDisconnectedAsync çağrıldı fakat UserId bulunamadı. ConnId={ConnectionId}", Context.ConnectionId);
_logger.LogWarning($"OnDisconnectedAsync çağrıldı fakat UserId bulunamadı. ConnId={Context.ConnectionId}");
await Clients.Caller.SendAsync("Warning", "User bulunamadı");
return;
}
_logger.LogInformation("🔴 OnDisconnectedAsync: User={UserId}, ConnId={ConnId}, Exception={Exception}",
userId, Context.ConnectionId, exception?.Message);
// 🔑 Yeni scope aç (her disconnect için ayrı UoW)
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true);
var participants = await _participantRepository
@ -546,15 +531,15 @@ public class ClassroomHub : Hub
if (!participants.Any())
{
_logger.LogInformation("OnDisconnectedAsync: User {UserId} için aktif participant bulunamadı. ConnId={ConnectionId}", userId, Context.ConnectionId);
_logger.LogInformation($"OnDisconnectedAsync: User {userId} için aktif participant bulunamadı. ConnId={Context.ConnectionId}");
await Clients.Caller.SendAsync("Warning", "Aktif participant bulunamadı");
}
foreach (var participant in participants)
{
_logger.LogInformation("OnDisconnectedAsync: User {UserId}, Session {SessionId} bağlantısı koptu.",
userId, participant.SessionId);
_logger.LogInformation($"OnDisconnectedAsync: User {userId}, Session {participant.SessionId} bağlantısı koptu.");
await Clients.Caller.SendAsync("Info", $"Bağlantı koptu: User {userId}");
// 🔑 Attendance güncelle
var attendances = await _attendanceRepository.GetListAsync(
x => x.SessionId == participant.SessionId &&
x.StudentId == userId.Value &&
@ -606,10 +591,12 @@ public class ClassroomHub : Hub
catch (TaskCanceledException)
{
_logger.LogDebug("OnDisconnectedAsync iptal edildi (connection aborted). ConnId={ConnectionId}", Context.ConnectionId);
await Clients.Caller.SendAsync("Error", "OnDisconnectedAsync iptal edildi");
}
catch (Exception ex)
{
_logger.LogError(ex, "OnDisconnectedAsync hata. ConnId={ConnectionId}", Context.ConnectionId);
await Clients.Caller.SendAsync("Error", "OnDisconnectedAsync hata oluştu");
}
await base.OnDisconnectedAsync(exception);

View file

@ -51,7 +51,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
{/* Header */}
<div className="p-3 sm:p-4 border-b border-gray-200 flex justify-between items-center">
<h3 className="text-base sm:text-lg font-semibold">Sohbet</h3>
<button onClick={onClose} className="p-1 hover:bg-gray-100 rounded lg:hidden">
<button onClick={onClose}>
<FaTimes className="text-gray-500" size={16} />
</button>
</div>

View file

@ -1,13 +1,12 @@
import { toast } from '@/components/ui'
import {
ClassroomAttendanceDto,
ClassroomChatDto,
HandRaiseDto,
MessageType,
} from '@/proxy/classroom/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { store } from '@/store/store'
import * as signalR from '@microsoft/signalr'
import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification'
export class SignalRService {
@ -36,13 +35,10 @@ export class SignalRService {
constructor() {
const { auth } = store.getState()
// Only initialize connection if not in demo mode
// In production, replace with your actual SignalR hub URL
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, {
accessTokenFactory: () => auth.session.token || '',
})
//.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build()
@ -76,12 +72,10 @@ export class SignalRService {
})
this.connection.on('HandRaiseReceived', (payload: any) => {
// payload = { handRaiseId, studentId, studentName, ... }
this.onHandRaiseReceived?.(payload.studentId)
})
this.connection.on('HandRaiseDismissed', (payload: any) => {
// payload = { handRaiseId, studentId }
this.onHandRaiseDismissed?.(payload.studentId)
})
@ -102,33 +96,25 @@ export class SignalRService {
this.connection.onreconnected(async () => {
this.isConnected = true
console.log("🔄 SignalR reconnected. currentSessionId=", this.currentSessionId)
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, {
placement: 'top-center',
})
// Eğer sınıftayken bağlantı koptuysa → tekrar join et
if (this.currentSessionId && store.getState().auth.user) {
const u = store.getState().auth.user
await this.joinClass(this.currentSessionId, u.id, u.name, u.role === 'teacher', true)
}
})
this.connection.onclose(async (err) => {
console.warn("🔥 onclose triggered", { isKicked: this.isKicked, error: err })
this.connection.onclose(async () => {
if (this.isKicked) {
toast.push(
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />,
{
placement: 'top-center',
},
{ placement: 'top-center' },
)
this.isConnected = false
this.currentSessionId = undefined
return // ❗ Kick durumunda kesinlikle LeaveClass çağırma
return
}
this.isConnected = false
@ -136,46 +122,55 @@ export class SignalRService {
if (this.currentSessionId) {
await this.connection.invoke('LeaveClass', this.currentSessionId)
}
} catch {
console.warn('LeaveClass could not be sent, connection was already closed')
} finally {
this.currentSessionId = undefined
}
})
this.connection.on('Error', (message: string) => {
console.error('Hub error:', message)
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, {
placement: 'top-center',
})
})
this.connection.onreconnecting((err) => {
this.connection.on('Warning', (message: string) => {
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, {
placement: 'top-center',
})
})
this.connection.on('Info', (message: string) => {
toast.push(<Notification title={` Bilgi: ${message}`} type="info" />, {
placement: 'top-center',
})
})
this.connection.onreconnecting(() => {
if (this.isKicked) {
console.warn('Reconnect blocked because user was kicked')
toast.push(
<Notification
title="❌ Sınıftan çıkarıldığınız için yeniden bağlanma engellendi"
type="danger"
/>,
)
this.connection.stop()
throw new Error('Reconnect blocked after kick')
}
})
this.connection.on('ForceDisconnect', async (message: string) => {
console.warn("🚨 ForceDisconnect event alındı", message)
this.isKicked = true
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, {
placement: 'top-center',
})
if (this.onForceCleanup) {
console.warn('⚡ ForceCleanup callback çağrılıyor')
this.onForceCleanup()
}
try {
await this.connection.stop()
} catch (e) {
console.warn('connection.stop hata:', e)
}
} catch {}
this.isConnected = false
@ -193,8 +188,6 @@ export class SignalRService {
async start(): Promise<void> {
try {
console.log('🔌 SignalR start() çağrıldı')
const startPromise = this.connection.start()
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000),
@ -205,10 +198,13 @@ export class SignalRService {
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, {
placement: 'top-center',
})
} catch (error) {
console.error('Error starting SignalR connection:', error)
alert(
'⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin.',
} catch {
toast.push(
<Notification
title="⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin."
type="danger"
/>,
{ placement: 'top-center' },
)
this.isConnected = false
}
@ -222,18 +218,22 @@ export class SignalRService {
isActive: boolean,
): Promise<void> {
if (!this.isConnected) {
console.log('Error starting SignalR connection join class for', userName)
toast.push(
<Notification
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin."
type="warning"
/>,
)
return
}
console.log(`📡 joinClass: sessionId=${sessionId}, userId=${userId}, isTeacher=${isTeacher}`)
//Global değişkene yazılıyor.
this.currentSessionId = sessionId
try {
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
} catch (error) {
console.error('Error joining class:', error)
} catch {
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, {
placement: 'top-center',
})
}
}
@ -241,21 +241,17 @@ export class SignalRService {
const { auth } = store.getState()
if (!this.isConnected) {
console.log('Error starting SignalR connection simulating leave class for user', auth.user.id)
// Simulate successful leave in demo mode
setTimeout(() => {
this.onParticipantLeft?.({ userId: auth.user.id, sessionId })
}, 100)
this.onParticipantLeft?.({ userId: auth.user.id, sessionId })
return
}
try {
await this.connection.invoke('LeaveClass', sessionId)
//Global değişkene null atanıyor.
this.currentSessionId = undefined
} catch (error) {
console.error('Error leaving class:', error)
} catch {
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, {
placement: 'top-center',
})
}
}
@ -293,8 +289,10 @@ export class SignalRService {
isTeacher,
'public',
)
} catch (error) {
console.error('Error sending chat message:', error)
} catch {
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, {
placement: 'top-center',
})
}
}
@ -338,8 +336,10 @@ export class SignalRService {
isTeacher,
'private',
)
} catch (error) {
console.error('Error sending private message:', error)
} catch {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
placement: 'top-center',
})
}
}
@ -376,8 +376,10 @@ export class SignalRService {
message,
isTeacher,
)
} catch (error) {
console.error('Error sending chat message:', error)
} catch {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
placement: 'top-center',
})
}
}
@ -396,20 +398,15 @@ export class SignalRService {
try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
} catch (error) {
console.error('Error muting participant:', error)
} catch {
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, {
placement: 'top-center',
})
}
}
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
if (!this.isConnected) {
const handRaise: HandRaiseDto = {
id: crypto.randomUUID(),
studentId,
studentName,
timestamp: new Date().toISOString(),
isActive: true,
}
setTimeout(() => {
this.onHandRaiseReceived?.(studentId)
}, 100)
@ -418,8 +415,10 @@ export class SignalRService {
try {
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
} catch (error) {
console.error('Error raising hand:', error)
} catch {
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, {
placement: 'top-center',
})
}
}
@ -431,12 +430,12 @@ export class SignalRService {
return
}
console.log(`👢 kickParticipant çağrıldı: sessionId=${sessionId}, participantId=${participantId}`)
try {
await this.connection.invoke('KickParticipant', sessionId, participantId)
} catch (error) {
console.error('Error kicking participant:', error)
} catch {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
placement: 'top-center',
})
}
}
@ -450,8 +449,10 @@ export class SignalRService {
try {
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
} catch (error) {
console.error('Error approving hand raise:', error)
} catch {
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, {
placement: 'top-center',
})
}
}
@ -465,8 +466,10 @@ export class SignalRService {
try {
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
} catch (error) {
console.error('Error dismissing hand raise:', error)
} catch {
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, {
placement: 'top-center',
})
}
}
@ -541,8 +544,10 @@ export class SignalRService {
if (this.isConnected && this.currentSessionId) {
try {
await this.connection.invoke('LeaveClass', this.currentSessionId)
} catch (err) {
console.warn('LeaveClass gönderilemedi:', err)
} catch {
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, {
placement: 'top-center',
})
}
}
if (this.connection) {

View file

@ -1,9 +1,12 @@
import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification'
export class WebRTCService {
private peerConnections: Map<string, RTCPeerConnection> = new Map()
private retryCounts: Map<string, number> = new Map() // 🔑 her kullanıcı için retry sayacı
private maxRetries = 3 // 🔑 maksimum yeniden deneme sayısı
private signalRService: any // 👈 dışarıdan set edilecek SignalR servisi
private sessionId: string = '' // oturum için de lazım olabilir
private retryCounts: Map<string, number> = new Map()
private maxRetries = 3
private signalRService: any
private sessionId: string = ''
private localStream: MediaStream | null = null
private onRemoteStream?: (userId: string, stream: MediaStream) => void
@ -17,23 +20,8 @@ export class WebRTCService {
],
}
// private rtcConfiguration: RTCConfiguration = {
// iceServers: [
// { urls: 'stun:stun.l.google.com:19302' }, // STUN
// {
// urls: ['turn:your-server-ip:3478?transport=udp', 'turn:your-server-ip:3478?transport=tcp'],
// username: 'kurs', // static user/pass kullanmak istersen
// credential: 'kurs12345',
// },
// ],
// }
/**
* Local stream'i başlatır. Kamera/mikrofon ayarlarını parametreden alır.
*/
async initializeLocalStream(enableAudio: boolean, enableVideo: boolean): Promise<MediaStream> {
try {
// her zaman hem ses hem video al
this.localStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
@ -47,23 +35,26 @@ export class WebRTCService {
},
})
// başlangıç ayarlarını uygula
this.localStream.getAudioTracks().forEach((track) => (track.enabled = enableAudio))
this.localStream.getVideoTracks().forEach((track) => (track.enabled = enableVideo))
return this.localStream
} catch (error) {
console.error('Error accessing media devices:', error)
throw error
} catch {
toast.push(
<Notification
title="❌ Kamera/Mikrofon erişilemedi. Tarayıcı ayarlarınızı veya izinleri kontrol edin."
type="danger"
/>,
)
throw new Error('Media devices access failed')
}
}
async createPeerConnection(userId: string): Promise<RTCPeerConnection> {
const peerConnection = new RTCPeerConnection(this.rtcConfiguration)
this.peerConnections.set(userId, peerConnection)
this.retryCounts.set(userId, 0) // bağlantı başında sıfırla
this.retryCounts.set(userId, 0)
// Eğer local stream varsa track'leri ekle
if (this.localStream) {
this.localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, this.localStream!)
@ -83,7 +74,6 @@ export class WebRTCService {
peerConnection.onconnectionstatechange = async () => {
const state = peerConnection.connectionState
console.log(`Bağlantı durumu [${userId}]: ${state}`)
if (state === 'closed') {
this.closePeerConnection(userId)
@ -92,28 +82,38 @@ export class WebRTCService {
if (state === 'failed') {
let retries = this.retryCounts.get(userId) ?? 0
if (retries < this.maxRetries) {
console.warn(
`⚠️ Bağlantı failed oldu, ICE restart deneniyor [${userId}] (Deneme ${retries + 1})`,
toast.push(
<Notification
title={`⚠️ Bağlantı başarısız, yeniden deneniyor (${retries + 1}/${this.maxRetries})`}
type="warning"
/>,
)
this.retryCounts.set(userId, retries + 1)
await this.restartIce(peerConnection, userId)
} else {
console.error(
`❌ Bağlantı ${this.maxRetries} denemede başarısız [${userId}], peer kapatılıyor.`,
toast.push(
<Notification
title={`❌ Bağlantı kurulamadı (${this.maxRetries} deneme başarısız).`}
type="danger"
/>,
{ placement: 'top-center' },
)
this.closePeerConnection(userId)
}
}
}
// En sona ekle
if (this.candidateBuffer.has(userId)) {
for (const cand of this.candidateBuffer.get(userId)!) {
try {
await peerConnection.addIceCandidate(cand)
console.log(`Buffered ICE candidate eklendi [${userId}]`)
} catch (err) {
console.warn(`Buffered candidate eklenemedi [${userId}]:`, err)
} catch {
toast.push(
<Notification
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
type="warning"
/>,
)
}
}
this.candidateBuffer.delete(userId)
@ -139,9 +139,11 @@ export class WebRTCService {
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
return offer
} catch (err) {
console.error('Offer oluşturulurken hata:', err)
throw err
} catch {
toast.push(<Notification title="❌ Offer oluşturulamadı" type="danger" />, {
placement: 'top-center',
})
throw new Error('Offer creation failed')
}
}
@ -157,44 +159,46 @@ export class WebRTCService {
const answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return answer
} catch (err) {
console.error('Answer oluşturulurken hata:', err)
throw err
} catch {
toast.push(<Notification title="❌ Answer oluşturulamadı" type="danger" />, {
placement: 'top-center',
})
throw new Error('Answer creation failed')
}
}
async handleAnswer(userId: string, answer: RTCSessionDescriptionInit): Promise<void> {
const peerConnection = this.peerConnections.get(userId)
if (!peerConnection) throw new Error('Peer connection not found')
await peerConnection.setRemoteDescription(answer)
}
async addIceCandidate(userId: string, candidate: RTCIceCandidateInit): Promise<void> {
const pc = this.peerConnections.get(userId)
if (!pc) {
// Peer yoksa buffera at
if (!this.candidateBuffer.has(userId)) {
this.candidateBuffer.set(userId, [])
}
this.candidateBuffer.get(userId)!.push(candidate)
console.warn(`ICE candidate bufferlandı [${userId}]`)
return
}
if (pc.signalingState === 'stable' || pc.signalingState === 'have-remote-offer') {
try {
await pc.addIceCandidate(candidate)
} catch (err) {
console.warn(`ICE candidate eklenemedi [${userId}]:`, err)
} catch {
toast.push(
<Notification
title={`⚠️ ICE candidate eklenemedi. Kullanıcı: ${userId}`}
type="warning"
/>,
)
}
} else {
// signalling hazır değilse → buffera at
if (!this.candidateBuffer.has(userId)) {
this.candidateBuffer.set(userId, [])
}
this.candidateBuffer.get(userId)!.push(candidate)
console.warn(`ICE candidate bufferlandı [${userId}], state=${pc.signalingState}`)
}
}
@ -223,8 +227,10 @@ export class WebRTCService {
}
})
}
} catch (err) {
console.error('Video açılırken hata:', err)
} catch {
toast.push(<Notification title="❌ Kamera açılamadı" type="danger" />, {
placement: 'top-center',
})
}
}
}
@ -250,8 +256,10 @@ export class WebRTCService {
}
})
}
} catch (err) {
console.error('Audio açılırken hata:', err)
} catch {
toast.push(<Notification title="❌ Mikrofon açılamadı" type="danger" />, {
placement: 'top-center',
})
}
}
}
@ -264,17 +272,18 @@ export class WebRTCService {
try {
const offer = await peerConnection.createOffer({ iceRestart: true })
await peerConnection.setLocalDescription(offer)
console.log(`ICE restart başlatıldı [${userId}]`)
// 🔑 SignalR üzerinden karşı tarafa gönder
if (this.signalRService) {
await this.signalRService.sendOffer(this.sessionId, userId, offer)
console.log(`ICE restart offer karşıya gönderildi [${userId}]`)
} else {
console.warn('⚠️ SignalR servisi bağlı değil, offer gönderilemedi')
toast.push(<Notification title="⚠️ Tekrar bağlanma başarısız" type="warning" />, {
placement: 'top-center',
})
}
} catch (err) {
console.error(`ICE restart başarısız [${userId}]:`, err)
} catch {
toast.push(<Notification title="❌ ICE restart başarısız" type="danger" />, {
placement: 'top-center',
})
}
}
@ -284,7 +293,7 @@ export class WebRTCService {
peerConnection.getSenders().forEach((sender) => sender.track?.stop())
peerConnection.close()
this.peerConnections.delete(userId)
this.retryCounts.delete(userId) // sayaç temizle
this.retryCounts.delete(userId)
}
}
@ -311,7 +320,6 @@ export class WebRTCService {
const alreadyHas = pc.getSenders().some((s) => s.track?.id === track.id)
if (!alreadyHas) {
pc.addTrack(track, stream)
// 🔑 track bittiğinde otomatik sil
track.onended = () => {
this.removeTrackFromPeers(track)
}
@ -326,8 +334,10 @@ export class WebRTCService {
if (sender.track === track) {
try {
pc.removeTrack(sender)
} catch (err) {
console.warn('removeTrack hata verdi:', err)
} catch {
toast.push(<Notification title="⚠️ Track silinemedi" type="warning" />, {
placement: 'top-center',
})
}
if (sender.track?.readyState !== 'ended') {
sender.track?.stop()

View file

@ -241,7 +241,7 @@ const RoomDetail: React.FC = () => {
// WebRTC başlat
webRTCServiceRef.current = new WebRTCService()
webRTCServiceRef.current.setSignalRService(signalRServiceRef.current, classSession.id)
const stream = await webRTCServiceRef.current.initializeLocalStream(micEnabled, camEnabled)
if (stream) {
setLocalStream(stream)
@ -251,7 +251,6 @@ const RoomDetail: React.FC = () => {
// Setup WebRTC remote stream handler
webRTCServiceRef.current.onRemoteStreamReceived((userId, stream) => {
console.log('Received remote stream from:', userId)
setParticipants((prev) => prev.map((p) => (p.id === userId ? { ...p, stream } : p)))
})
@ -285,7 +284,6 @@ const RoomDetail: React.FC = () => {
if (remoteUserId === user.id) return
if (!isActive) return
console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`)
toast.push(<Notification title={`${name} sınıfa katıldı`} type="success" />, {
placement: 'top-center',
})
@ -366,11 +364,14 @@ const RoomDetail: React.FC = () => {
})
signalRServiceRef.current.setParticipantLeaveHandler(({ userId, sessionId }) => {
console.log(`👋 Participant left handler: ${userId}, sessionId=${sessionId}`)
if (userId !== user.id) {
const leftUser = participants.find((p) => p.id === userId)
const leftName = leftUser ? leftUser.name : 'Bilinmeyen'
toast.push(<Notification title={`Katılımcı ayrıldı: ${participants.find((p) => p.id === userId)?.name}`} type="warning" />, {
placement: 'top-center',
})
toast.push(<Notification title={`Katılımcı ayrıldı: ${leftName}`} type="warning" />, {
placement: 'top-center',
})
}
// peer connectionı kapat
webRTCServiceRef.current?.closePeerConnection(userId)
@ -438,7 +439,13 @@ const RoomDetail: React.FC = () => {
true,
)
} catch (error) {
console.error('Failed to initialize services:', error)
toast.push(
<Notification
title="❌ Sınıf servisleri başlatılamadı. Bağlantınızı veya tarayıcı izinlerini kontrol edin."
type="danger"
/>,
{ placement: 'top-center' },
)
}
}
@ -476,7 +483,7 @@ const RoomDetail: React.FC = () => {
// Başka sayfaya yönlendir
navigate(ROUTES_ENUM.protected.admin.classroom.classes)
} catch (err) {
console.error('Leave işlemi sırasında hata:', err)
toast.push(<Notification title="⚠️ Çıkış sırasında hata oluştu" type="warning" />)
navigate(ROUTES_ENUM.protected.admin.classroom.classes)
}
}
@ -485,31 +492,43 @@ const RoomDetail: React.FC = () => {
e.preventDefault()
if (newMessage.trim() && signalRServiceRef.current) {
if (messageMode === 'private' && selectedRecipient) {
await signalRServiceRef.current.sendPrivateMessage(
classSession.id,
user.id,
user.name,
newMessage.trim(),
selectedRecipient.id,
selectedRecipient.name,
user.role === 'teacher',
)
try {
await signalRServiceRef.current.sendPrivateMessage(
classSession.id,
user.id,
user.name,
newMessage.trim(),
selectedRecipient.id,
selectedRecipient.name,
user.role === 'teacher',
)
} catch (error) {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />)
}
} else if (messageMode === 'announcement' && user.role === 'teacher') {
await signalRServiceRef.current.sendAnnouncement(
classSession.id,
user.id,
user.name,
newMessage.trim(),
user.role === 'teacher',
)
try {
await signalRServiceRef.current.sendAnnouncement(
classSession.id,
user.id,
user.name,
newMessage.trim(),
user.role === 'teacher',
)
} catch (error) {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />)
}
} else {
await signalRServiceRef.current.sendChatMessage(
classSession.id,
user.id,
user.name,
newMessage.trim(),
user.role === 'teacher',
)
try {
await signalRServiceRef.current.sendChatMessage(
classSession.id,
user.id,
user.name,
newMessage.trim(),
user.role === 'teacher',
)
} catch (error) {
toast.push(<Notification title="❌ Genel mesaj gönderilemedi" type="danger" />)
}
}
setNewMessage('')
}
@ -521,12 +540,16 @@ const RoomDetail: React.FC = () => {
isTeacher: boolean,
) => {
if (signalRServiceRef.current && user.role === 'teacher') {
await signalRServiceRef.current.muteParticipant(
classSession.id,
participantId,
isMuted,
isTeacher,
)
try {
await signalRServiceRef.current.muteParticipant(
classSession.id,
participantId,
isMuted,
isTeacher,
)
} catch (err) {
toast.push(<Notification title="❌ Katılımcı susturulamadı" type="danger" />)
}
}
}
@ -563,24 +586,26 @@ const RoomDetail: React.FC = () => {
const handleKickParticipant = async (participantId: string) => {
if (signalRServiceRef.current && user.role === 'teacher') {
console.log(`👢 handleKickParticipant UIden çağrıldı: ${participantId}`)
await signalRServiceRef.current.kickParticipant(classSession.id, participantId)
setAttendanceRecords((prev) =>
prev.map((r) => {
if (r.studentId === participantId && !r.leaveTime) {
const leaveTime = new Date().toISOString()
const join = new Date(r.joinTime)
const leave = new Date(leaveTime)
const totalDurationMinutes = Math.max(
1,
Math.round((leave.getTime() - join.getTime()) / 60000),
)
return { ...r, leaveTime, totalDurationMinutes }
}
return r
}),
)
try {
await signalRServiceRef.current.kickParticipant(classSession.id, participantId)
setAttendanceRecords((prev) =>
prev.map((r) => {
if (r.studentId === participantId && !r.leaveTime) {
const leaveTime = new Date().toISOString()
const join = new Date(r.joinTime)
const leave = new Date(leaveTime)
const totalDurationMinutes = Math.max(
1,
Math.round((leave.getTime() - join.getTime()) / 60000),
)
return { ...r, leaveTime, totalDurationMinutes }
}
return r
}),
)
} catch (error) {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />)
}
}
}
@ -619,7 +644,12 @@ const RoomDetail: React.FC = () => {
try {
mic = await navigator.mediaDevices.getUserMedia({ audio: true })
} catch (err) {
console.warn('Mic alınamadı, sadece ekran paylaşılacak', err)
toast.push(
<Notification
title="⚠️ Mikrofon alınamadı. Sadece ekran paylaşımı yapılacak."
type="warning"
/>,
)
}
// 3. merge et
@ -637,7 +667,7 @@ const RoomDetail: React.FC = () => {
handleStopScreenShare()
}
} catch (error) {
console.error('Error starting screen share:', error)
toast.push(<Notification title="❌ Ekran paylaşımı başlatılamadı" type="danger" />)
}
}