sozsoft-platform/ui/src/services/videoroom/signalr.tsx
Sedat ÖZTÜRK bdc7f744aa Video Rooms
2026-05-08 08:34:29 +03:00

573 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'
import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models'
export class SignalRService {
private connection!: signalR.HubConnection
private isConnected: boolean = false
private currentSessionId?: string
private isKicked: boolean = false
private onAttendanceUpdate?: (record: VideoroomAttendanceDto) => void
private onParticipantJoined?: (
userId: string,
name: string,
isTeacher: boolean,
isActive: boolean,
) => void
private onParticipantLeft?: (payload: {
userId: string
sessionId: string
userName: string
}) => void
private onChatMessage?: (message: VideoroomChatDto) => void
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
private onHandRaiseReceived?: (studentId: string) => void
private onHandRaiseDismissed?: (studentId: string) => void
private onOfferReceived?: (fromUserId: string, offer: RTCSessionDescriptionInit) => void
private onAnswerReceived?: (fromUserId: string, answer: RTCSessionDescriptionInit) => void
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
private onForceCleanup?: () => void
constructor() {
const { auth } = store.getState()
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${import.meta.env.VITE_API_URL}/videoroomhub`, {
accessTokenFactory: () => auth.session.token || '',
})
.configureLogging(signalR.LogLevel.Information)
.build()
this.setupEventHandlers()
}
private setupEventHandlers() {
if (!this.connection) return
this.connection.on('AttendanceUpdated', (record: VideoroomAttendanceDto) => {
this.onAttendanceUpdate?.(record)
})
this.connection.on(
'ParticipantJoined',
(userId: string, name: string, isTeacher: boolean, isActive: boolean) => {
this.onParticipantJoined?.(userId, name, isTeacher, isActive)
},
)
this.connection.on(
'ParticipantLeft',
(payload: { userId: string; sessionId: string; userName: string }) => {
this.onParticipantLeft?.(payload)
},
)
this.connection.on('ChatMessage', (message: any) => {
this.onChatMessage?.(message)
})
this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => {
this.onParticipantMuted?.(userId, isMuted)
})
this.connection.on('HandRaiseReceived', (payload: any) => {
this.onHandRaiseReceived?.(payload.studentId)
})
this.connection.on('HandRaiseDismissed', (payload: any) => {
this.onHandRaiseDismissed?.(payload.studentId)
})
this.connection.on('ReceiveOffer', (fromUserId: string, offer: RTCSessionDescriptionInit) => {
this.onOfferReceived?.(fromUserId, offer)
})
this.connection.on('ReceiveAnswer', (fromUserId: string, answer: RTCSessionDescriptionInit) => {
this.onAnswerReceived?.(fromUserId, answer)
})
this.connection.on(
'ReceiveIceCandidate',
(fromUserId: string, candidate: RTCIceCandidateInit) => {
this.onIceCandidateReceived?.(fromUserId, candidate)
},
)
this.connection.onreconnected(async () => {
this.isConnected = true
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, {
placement: 'top-end',
})
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 () => {
if (this.isKicked) {
toast.push(
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />,
{ placement: 'top-end' },
)
this.isConnected = false
this.currentSessionId = undefined
return
}
this.isConnected = false
try {
if (this.currentSessionId) {
await this.connection.invoke('LeaveClass', this.currentSessionId)
}
} finally {
this.currentSessionId = undefined
}
})
this.connection.on('Error', (message: string) => {
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, {
placement: 'top-end',
})
})
this.connection.on('Warning', (message: string) => {
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, {
placement: 'top-end',
})
})
this.connection.on('Info', (message: string) => {
toast.push(<Notification title={` Bilgi: ${message}`} type="info" />, {
placement: 'top-end',
})
})
this.connection.onreconnecting(() => {
if (this.isKicked) {
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) => {
this.isKicked = true
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, {
placement: 'top-end',
})
if (this.onForceCleanup) {
this.onForceCleanup()
}
try {
await this.connection.stop()
} catch {}
this.isConnected = false
if (this.currentSessionId && store.getState().auth.user) {
this.onParticipantLeft?.({
userId: store.getState().auth.user.id,
sessionId: this.currentSessionId,
userName: store.getState().auth.user.name,
})
}
this.currentSessionId = undefined
window.location.href = ROUTES_ENUM.protected.admin.videoroom.roomList
})
}
async start(): Promise<void> {
try {
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
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, {
placement: 'top-end',
})
} 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-end' },
)
this.isConnected = false
}
}
async joinClass(
sessionId: string,
userId: string,
userName: string,
isTeacher: boolean,
isActive: boolean,
): Promise<void> {
if (!this.isConnected) {
toast.push(
<Notification
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin."
type="warning"
/>,
)
return
}
this.currentSessionId = sessionId
try {
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
} catch {
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
async leaveClass(sessionId: string): Promise<void> {
const { auth } = store.getState()
if (!this.isConnected) {
this.onParticipantLeft?.({ userId: auth.user.id, sessionId, userName: auth.user.name })
return
}
try {
await this.connection.invoke('LeaveClass', sessionId)
this.currentSessionId = undefined
} catch {
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async sendChatMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: VideoroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'public',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendChatMessage',
sessionId,
senderId,
senderName,
message,
isTeacher,
'public',
)
} catch {
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async sendPrivateMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
recipientId: string,
recipientName: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: VideoroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
recipientId,
recipientName,
messageType: 'private',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendPrivateMessage',
sessionId,
senderId,
senderName,
message,
recipientId,
recipientName,
isTeacher,
'private',
)
} catch {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async sendAnnouncement(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: VideoroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'announcement',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendAnnouncement',
sessionId,
senderId,
senderName,
message,
isTeacher,
)
} catch {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async muteParticipant(
sessionId: string,
userId: string,
isMuted: boolean,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onParticipantMuted?.(userId, isMuted)
}, 100)
return
}
try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
} catch {
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, {
placement: 'top-end',
})
}
}
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseReceived?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
} catch {
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, {
placement: 'top-end',
})
}
}
async kickParticipant(sessionId: string, participantId: string, userName: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onParticipantLeft?.({ userId: participantId, sessionId, userName })
}, 100)
return
}
try {
await this.connection.invoke('KickParticipant', sessionId, participantId)
} catch {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
async approveHandRaise(sessionId: string, studentId: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseDismissed?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
} catch {
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async dismissHandRaise(sessionId: string, studentId: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseDismissed?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
} catch {
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async sendOffer(sessionId: string, targetUserId: string, offer: RTCSessionDescriptionInit) {
if (!this.isConnected) return
await this.connection.invoke('SendOffer', sessionId, targetUserId, offer)
}
async sendAnswer(sessionId: string, targetUserId: string, answer: RTCSessionDescriptionInit) {
if (!this.isConnected) return
await this.connection.invoke('SendAnswer', sessionId, targetUserId, answer)
}
async sendIceCandidate(sessionId: string, targetUserId: string, candidate: RTCIceCandidateInit) {
if (!this.isConnected) return
await this.connection.invoke('SendIceCandidate', sessionId, targetUserId, candidate)
}
setExistingParticipantsHandler(callback: (participants: any[]) => void) {
this.connection.on('ExistingParticipants', callback)
}
setAttendanceUpdatedHandler(callback: (record: VideoroomAttendanceDto) => void) {
this.onAttendanceUpdate = callback
}
setParticipantJoinHandler(
callback: (userId: string, name: string, isTeacher: boolean, isActive: boolean) => void,
) {
this.onParticipantJoined = callback
}
setParticipantLeaveHandler(
callback: (payload: { userId: string; sessionId: string; userName: string }) => void,
) {
this.onParticipantLeft = callback
}
setChatMessageReceivedHandler(callback: (message: VideoroomChatDto) => void) {
this.onChatMessage = callback
}
setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) {
this.onParticipantMuted = callback
}
setHandRaiseReceivedHandler(callback: (studentId: string) => void) {
this.onHandRaiseReceived = callback
}
setHandRaiseDismissedHandler(callback: (studentId: string) => void) {
this.onHandRaiseDismissed = callback
}
setOfferReceivedHandler(
callback: (fromUserId: string, offer: RTCSessionDescriptionInit) => void,
) {
this.onOfferReceived = callback
}
setAnswerReceivedHandler(
callback: (fromUserId: string, answer: RTCSessionDescriptionInit) => void,
) {
this.onAnswerReceived = callback
}
setIceCandidateReceivedHandler(
callback: (fromUserId: string, candidate: RTCIceCandidateInit) => void,
) {
this.onIceCandidateReceived = callback
}
async disconnect(): Promise<void> {
if (this.isConnected && this.currentSessionId) {
try {
await this.connection.invoke('LeaveClass', this.currentSessionId)
} catch {
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, {
placement: 'top-end',
})
}
}
if (this.connection) {
await this.connection.stop()
}
this.isConnected = false
this.currentSessionId = undefined
}
getConnectionState(): boolean {
return this.isConnected
}
setForceCleanupHandler(callback: () => void) {
this.onForceCleanup = callback
}
}