diff --git a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs index d81bff4c..6501056e 100644 --- a/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs +++ b/api/src/Kurs.Platform.Application.Contracts/Classroom/ClassroomParticipantDto.cs @@ -12,6 +12,7 @@ public class ClassroomParticipantDto public bool IsAudioMuted { get; set; } public bool IsVideoMuted { get; set; } public bool IsHandRaised { get; set; } + public bool IsKicked { get; set; } public DateTime JoinTime { get; set; } public bool IsActive { get; set; } } diff --git a/api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs b/api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs index 9eaaada7..b0f83f0b 100644 --- a/api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs +++ b/api/src/Kurs.Platform.Domain/Classroom/ClassroomParticipant.cs @@ -12,6 +12,7 @@ public class ClassroomParticipant : FullAuditedEntity public bool IsAudioMuted { get; set; } = false; public bool IsVideoMuted { get; set; } = false; public bool IsHandRaised { get; set; } = false; + public bool IsKicked { get; set; } = false; public bool IsActive { get; set; } = true; public DateTime JoinTime { get; set; } public string ConnectionId { get; set; } @@ -32,6 +33,7 @@ public class ClassroomParticipant : FullAuditedEntity bool isAudioMuted, bool isVideoMuted, bool isHandRaised, + bool isKicked, bool isActive ) : base(id) { @@ -43,6 +45,7 @@ public class ClassroomParticipant : FullAuditedEntity IsVideoMuted = isVideoMuted; IsHandRaised = isHandRaised; IsActive = isActive; + IsKicked = isKicked; JoinTime = DateTime.UtcNow; } diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index eaf73ec3..a59d0e02 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -138,13 +138,14 @@ public class ClassroomHub : Hub ); // 🚨 Kick edilmiş kullanıcı tekrar giriş yapamaz - if (participant != null && !participant.IsActive) + if (participant != null && participant.IsKicked) { await Clients.Caller.SendAsync("Error", "You are not allowed to rejoin this class."); Context.Abort(); // bağlantıyı anında kapat return; } + if (participant == null) { participant = new ClassroomParticipant( @@ -155,7 +156,8 @@ public class ClassroomHub : Hub isTeacher, initialMuteState, classroomSettings.DefaultCameraState == "off", - false, + false, //isHandRaised + false, //isKicked isActive ); await _participantRepository.InsertAsync(participant, autoSave: true); @@ -393,11 +395,18 @@ public class ClassroomHub : Hub .SendAsync("ForceDisconnect", "You have been removed from the class."); await Groups.RemoveFromGroupAsync(participant.ConnectionId, sessionId.ToString()); - await DeactivateParticipantAsync(participant); + + // ❌ pasif + ✅ kicked işaretle + participant.IsActive = false; + participant.IsKicked = true; + participant.ConnectionId = null; + await _participantRepository.UpdateAsync(participant, autoSave: true); + await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId }); } + // 3. Diğerlerine duyur _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId); } @@ -521,11 +530,13 @@ public class ClassroomHub : Hub } } - // 🔑 Participant pasifleştir - participant.IsActive = false; - participant.ConnectionId = null; - - await _participantRepository.UpdateAsync(participant, autoSave: true); + // Eğer kullanıcı kick edilmemişse pasifleştir + if (!participant.IsKicked) + { + participant.IsActive = false; + participant.ConnectionId = null; + await _participantRepository.UpdateAsync(participant, autoSave: true); + } // 🔑 Frontend’e bildir await Clients.Group(participant.SessionId.ToString()) diff --git a/ui/src/proxy/classroom/models.ts b/ui/src/proxy/classroom/models.ts index d6da1abd..1d60dbc5 100644 --- a/ui/src/proxy/classroom/models.ts +++ b/ui/src/proxy/classroom/models.ts @@ -62,6 +62,7 @@ export interface ClassroomParticipantDto { isAudioMuted?: boolean isVideoMuted?: boolean isHandRaised?: boolean + isKicked?: boolean isActive?: boolean stream?: MediaStream screenStream?: MediaStream diff --git a/ui/src/services/classroom/signalr.ts b/ui/src/services/classroom/signalr.ts index d0092134..94fd18ab 100644 --- a/ui/src/services/classroom/signalr.ts +++ b/ui/src/services/classroom/signalr.ts @@ -39,7 +39,7 @@ export class SignalRService { .withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, { accessTokenFactory: () => auth.session.token || '', }) - //.withAutomaticReconnect() + .withAutomaticReconnect() .configureLogging(signalR.LogLevel.Information) .build() @@ -97,8 +97,15 @@ export class SignalRService { }, ) - this.connection.onreconnected(() => { + this.connection.onreconnected(async () => { this.isConnected = true + console.warn('🔄 SignalR reconnected') + + // 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) => { @@ -128,34 +135,45 @@ export class SignalRService { this.connection.onreconnecting((err) => { if (this.isKicked) { console.warn('Reconnect blocked because user was kicked') - // ❌ otomatik reconnect'i iptal etmek için stop çağır this.connection.stop() throw new Error('Reconnect blocked after kick') } }) - //2. tane problem var. - //1. problem kick yapamıyorum - //2. Öğrenci veya öğretmen farklı bir sayfaya gidince ekranda donuk şekilde duruyor. - this.connection.on('ForceDisconnect', async (message: string) => { console.warn('⚠️ ForceDisconnect received:', message) - this.isKicked = true // ✅ kick yediğini işaretle + this.isKicked = true await this.connection.stop() this.isConnected = false + // ✅ frontend state’den de çıkar + if (this.currentSessionId && store.getState().auth.user) { + this.onParticipantLeft?.({ + userId: store.getState().auth.user.id, + sessionId: this.currentSessionId, + }) + } + + this.currentSessionId = undefined window.location.href = ROUTES_ENUM.protected.admin.classroom.classes }) } async start(): Promise { try { - await this.connection.start() + const startPromise = this.connection.start() + const timeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000), + ) + + await Promise.race([startPromise, timeout]) this.isConnected = true } catch (error) { console.error('Error starting SignalR connection:', error) - // Switch to demo mode if connection fails + alert( + '⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin.', + ) this.isConnected = false } } @@ -481,11 +499,18 @@ export class SignalRService { } async disconnect(): Promise { - if (this.isConnected && this.connection) { - await this.connection.stop() - this.isConnected = false - this.currentSessionId = undefined + if (this.isConnected && this.currentSessionId) { + try { + await this.connection.invoke('LeaveClass', this.currentSessionId) + } catch (err) { + console.warn('LeaveClass gönderilemedi:', err) + } } + if (this.connection) { + await this.connection.stop() + } + this.isConnected = false + this.currentSessionId = undefined } getConnectionState(): boolean { diff --git a/ui/src/services/classroom/webrtc.ts b/ui/src/services/classroom/webrtc.ts index 8c204e74..073dc6f1 100644 --- a/ui/src/services/classroom/webrtc.ts +++ b/ui/src/services/classroom/webrtc.ts @@ -3,6 +3,7 @@ export class WebRTCService { private localStream: MediaStream | null = null private onRemoteStream?: (userId: string, stream: MediaStream) => void private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void + private candidateBuffer: Map = new Map() private rtcConfiguration: RTCConfiguration = { iceServers: [ @@ -11,6 +12,17 @@ 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. */ @@ -71,6 +83,19 @@ export class WebRTCService { } } + // 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) + } + } + this.candidateBuffer.delete(userId) + } + return peerConnection } @@ -119,7 +144,15 @@ export class WebRTCService { async addIceCandidate(userId: string, candidate: RTCIceCandidateInit): Promise { const pc = this.peerConnections.get(userId) - if (!pc) throw new Error('Peer connection not found') + if (!pc) { + // Peer yoksa buffer’a 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 { @@ -128,7 +161,12 @@ export class WebRTCService { console.warn(`ICE candidate eklenemedi [${userId}]:`, err) } } else { - console.warn(`ICE candidate atlandı [${userId}], signalingState=${pc.signalingState}`) + // signalling hazır değilse → buffer’a 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}`) } } diff --git a/ui/src/views/classroom/ClassList.tsx b/ui/src/views/classroom/ClassList.tsx index e7c23186..ca8008e0 100644 --- a/ui/src/views/classroom/ClassList.tsx +++ b/ui/src/views/classroom/ClassList.tsx @@ -486,6 +486,7 @@ const ClassList: React.FC = () => {