Uyarılar değiştirildi kullanıcıya gösterildi. toast
This commit is contained in:
parent
60ced627b4
commit
9db84f137e
6 changed files with 258 additions and 226 deletions
|
|
@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 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 {
|
||||
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 → 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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 UI’den ç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" />)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue