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

View file

@ -51,7 +51,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
{/* Header */} {/* Header */}
<div className="p-3 sm:p-4 border-b border-gray-200 flex justify-between items-center"> <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> <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} /> <FaTimes className="text-gray-500" size={16} />
</button> </button>
</div> </div>

View file

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

View file

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

View file

@ -241,7 +241,7 @@ const RoomDetail: React.FC = () => {
// WebRTC başlat // WebRTC başlat
webRTCServiceRef.current = new WebRTCService() webRTCServiceRef.current = new WebRTCService()
webRTCServiceRef.current.setSignalRService(signalRServiceRef.current, classSession.id) webRTCServiceRef.current.setSignalRService(signalRServiceRef.current, classSession.id)
const stream = await webRTCServiceRef.current.initializeLocalStream(micEnabled, camEnabled) const stream = await webRTCServiceRef.current.initializeLocalStream(micEnabled, camEnabled)
if (stream) { if (stream) {
setLocalStream(stream) setLocalStream(stream)
@ -251,7 +251,6 @@ const RoomDetail: React.FC = () => {
// Setup WebRTC remote stream handler // Setup WebRTC remote stream handler
webRTCServiceRef.current.onRemoteStreamReceived((userId, stream) => { webRTCServiceRef.current.onRemoteStreamReceived((userId, stream) => {
console.log('Received remote stream from:', userId)
setParticipants((prev) => prev.map((p) => (p.id === userId ? { ...p, stream } : p))) 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 (remoteUserId === user.id) return
if (!isActive) return if (!isActive) return
console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`)
toast.push(<Notification title={`${name} sınıfa katıldı`} type="success" />, { toast.push(<Notification title={`${name} sınıfa katıldı`} type="success" />, {
placement: 'top-center', placement: 'top-center',
}) })
@ -366,11 +364,14 @@ const RoomDetail: React.FC = () => {
}) })
signalRServiceRef.current.setParticipantLeaveHandler(({ userId, sessionId }) => { 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" />, { toast.push(<Notification title={`Katılımcı ayrıldı: ${leftName}`} type="warning" />, {
placement: 'top-center', placement: 'top-center',
}) })
}
// peer connectionı kapat // peer connectionı kapat
webRTCServiceRef.current?.closePeerConnection(userId) webRTCServiceRef.current?.closePeerConnection(userId)
@ -438,7 +439,13 @@ const RoomDetail: React.FC = () => {
true, true,
) )
} catch (error) { } 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 // Başka sayfaya yönlendir
navigate(ROUTES_ENUM.protected.admin.classroom.classes) navigate(ROUTES_ENUM.protected.admin.classroom.classes)
} catch (err) { } 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) navigate(ROUTES_ENUM.protected.admin.classroom.classes)
} }
} }
@ -485,31 +492,43 @@ const RoomDetail: React.FC = () => {
e.preventDefault() e.preventDefault()
if (newMessage.trim() && signalRServiceRef.current) { if (newMessage.trim() && signalRServiceRef.current) {
if (messageMode === 'private' && selectedRecipient) { if (messageMode === 'private' && selectedRecipient) {
await signalRServiceRef.current.sendPrivateMessage( try {
classSession.id, await signalRServiceRef.current.sendPrivateMessage(
user.id, classSession.id,
user.name, user.id,
newMessage.trim(), user.name,
selectedRecipient.id, newMessage.trim(),
selectedRecipient.name, selectedRecipient.id,
user.role === 'teacher', selectedRecipient.name,
) user.role === 'teacher',
)
} catch (error) {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />)
}
} else if (messageMode === 'announcement' && user.role === 'teacher') { } else if (messageMode === 'announcement' && user.role === 'teacher') {
await signalRServiceRef.current.sendAnnouncement( try {
classSession.id, await signalRServiceRef.current.sendAnnouncement(
user.id, classSession.id,
user.name, user.id,
newMessage.trim(), user.name,
user.role === 'teacher', newMessage.trim(),
) user.role === 'teacher',
)
} catch (error) {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />)
}
} else { } else {
await signalRServiceRef.current.sendChatMessage( try {
classSession.id, await signalRServiceRef.current.sendChatMessage(
user.id, classSession.id,
user.name, user.id,
newMessage.trim(), user.name,
user.role === 'teacher', newMessage.trim(),
) user.role === 'teacher',
)
} catch (error) {
toast.push(<Notification title="❌ Genel mesaj gönderilemedi" type="danger" />)
}
} }
setNewMessage('') setNewMessage('')
} }
@ -521,12 +540,16 @@ const RoomDetail: React.FC = () => {
isTeacher: boolean, isTeacher: boolean,
) => { ) => {
if (signalRServiceRef.current && user.role === 'teacher') { if (signalRServiceRef.current && user.role === 'teacher') {
await signalRServiceRef.current.muteParticipant( try {
classSession.id, await signalRServiceRef.current.muteParticipant(
participantId, classSession.id,
isMuted, participantId,
isTeacher, 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) => { const handleKickParticipant = async (participantId: string) => {
if (signalRServiceRef.current && user.role === 'teacher') { if (signalRServiceRef.current && user.role === 'teacher') {
console.log(`👢 handleKickParticipant UIden çağrıldı: ${participantId}`) try {
await signalRServiceRef.current.kickParticipant(classSession.id, participantId)
await signalRServiceRef.current.kickParticipant(classSession.id, participantId) setAttendanceRecords((prev) =>
setAttendanceRecords((prev) => prev.map((r) => {
prev.map((r) => { if (r.studentId === participantId && !r.leaveTime) {
if (r.studentId === participantId && !r.leaveTime) { const leaveTime = new Date().toISOString()
const leaveTime = new Date().toISOString() const join = new Date(r.joinTime)
const join = new Date(r.joinTime) const leave = new Date(leaveTime)
const leave = new Date(leaveTime) const totalDurationMinutes = Math.max(
const totalDurationMinutes = Math.max( 1,
1, Math.round((leave.getTime() - join.getTime()) / 60000),
Math.round((leave.getTime() - join.getTime()) / 60000), )
) return { ...r, leaveTime, totalDurationMinutes }
return { ...r, leaveTime, totalDurationMinutes } }
} return r
return r }),
}), )
) } catch (error) {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />)
}
} }
} }
@ -619,7 +644,12 @@ const RoomDetail: React.FC = () => {
try { try {
mic = await navigator.mediaDevices.getUserMedia({ audio: true }) mic = await navigator.mediaDevices.getUserMedia({ audio: true })
} catch (err) { } 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 // 3. merge et
@ -637,7 +667,7 @@ const RoomDetail: React.FC = () => {
handleStopScreenShare() handleStopScreenShare()
} }
} catch (error) { } catch (error) {
console.error('Error starting screen share:', error) toast.push(<Notification title="❌ Ekran paylaşımı başlatılamadı" type="danger" />)
} }
} }