From d1ae106db7d79de39bbe3809c0b8e06d18154199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Fri, 29 Aug 2025 22:46:42 +0300 Subject: [PATCH] =?UTF-8?q?Classroom=20Videoplayer=20k=C4=B1s=C4=B1mlar?= =?UTF-8?q?=C4=B1=20d=C3=BCzeltildi.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Classroom/ClassroomHub.cs | 22 ++++++++ ui/src/services/classroom/signalr.ts | 51 +++++++++++++++++++ ui/src/services/classroom/webrtc.ts | 10 ++++ ui/src/views/classroom/RoomDetail.tsx | 32 +++++++++++- 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index 01e50287..3f18b460 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -360,6 +360,28 @@ public class ClassroomHub : Hub await Clients.Group(sessionId.ToString()).SendAsync("HandRaiseDismissed", new { studentId }); } + [HubMethodName("SendOffer")] + public async Task SendOfferAsync(Guid sessionId, Guid targetUserId, object offer) + { + // Tek hedef kullanıcıya gönderiyoruz + await Clients.User(targetUserId.ToString()) + .SendAsync("ReceiveOffer", _currentUser.Id?.ToString(), offer); + } + + [HubMethodName("SendAnswer")] + public async Task SendAnswerAsync(Guid sessionId, Guid targetUserId, object answer) + { + await Clients.User(targetUserId.ToString()) + .SendAsync("ReceiveAnswer", _currentUser.Id?.ToString(), answer); + } + + [HubMethodName("SendIceCandidate")] + public async Task SendIceCandidateAsync(Guid sessionId, Guid targetUserId, object candidate) + { + await Clients.User(targetUserId.ToString()) + .SendAsync("ReceiveIceCandidate", _currentUser.Id?.ToString(), candidate); + } + public override async Task OnDisconnectedAsync(Exception exception) { try diff --git a/ui/src/services/classroom/signalr.ts b/ui/src/services/classroom/signalr.ts index 777886fd..8365fe0d 100644 --- a/ui/src/services/classroom/signalr.ts +++ b/ui/src/services/classroom/signalr.ts @@ -12,6 +12,9 @@ export class SignalRService { 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 constructor() { const { auth } = store.getState() @@ -62,6 +65,21 @@ export class SignalRService { 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(() => { console.log('SignalR reconnected') }) @@ -350,6 +368,21 @@ export class SignalRService { } } + 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) + } + setAttendanceUpdatedHandler(callback: (record: ClassroomAttendanceDto) => void) { this.onAttendanceUpdate = callback } @@ -378,6 +411,24 @@ export class SignalRService { 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 { if (this.isConnected && this.connection) { await this.connection.stop() diff --git a/ui/src/services/classroom/webrtc.ts b/ui/src/services/classroom/webrtc.ts index 975eeb75..23d6267f 100644 --- a/ui/src/services/classroom/webrtc.ts +++ b/ui/src/services/classroom/webrtc.ts @@ -2,6 +2,7 @@ export class WebRTCService { private peerConnections: Map = new Map() private localStream: MediaStream | null = null private onRemoteStream?: (userId: string, stream: MediaStream) => void + private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void // STUN servers for NAT traversal private rtcConfiguration: RTCConfiguration = { @@ -55,6 +56,7 @@ export class WebRTCService { if (event.candidate) { console.log('ICE candidate generated for user:', userId, event.candidate) // In a real implementation, this would be sent via SignalR + this.onIceCandidate?.(userId, event.candidate) } } @@ -68,6 +70,10 @@ export class WebRTCService { return peerConnection } + setIceCandidateHandler(callback: (userId: string, candidate: RTCIceCandidateInit) => void) { + this.onIceCandidate = callback + } + async createOffer(userId: string): Promise { const peerConnection = this.peerConnections.get(userId) if (!peerConnection) throw new Error('Peer connection not found') @@ -138,6 +144,10 @@ export class WebRTCService { } } + getPeerConnection(userId: string): RTCPeerConnection | undefined { + return this.peerConnections.get(userId) + } + closeAllConnections(): void { this.peerConnections.forEach((pc) => pc.close()) this.peerConnections.clear() diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index 4a8eecc5..d2182997 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -240,8 +240,32 @@ const RoomDetail: React.FC = () => { setParticipants((prev) => prev.map((p) => (p.id === userId ? { ...p, stream } : p))) }) + webRTCServiceRef.current.setIceCandidateHandler(async (toUserId, candidate) => { + if (signalRServiceRef.current) { + await signalRServiceRef.current.sendIceCandidate(classSession.id, toUserId, candidate) + } + }) + + signalRServiceRef.current.setOfferReceivedHandler(async (fromUserId, offer) => { + if (!webRTCServiceRef.current?.getPeerConnection(fromUserId)) { + await webRTCServiceRef.current?.createPeerConnection(fromUserId) + } + const answer = await webRTCServiceRef.current?.createAnswer(fromUserId, offer) + if (answer) { + await signalRServiceRef.current?.sendAnswer(classSession.id, fromUserId, answer) + } + }) + + signalRServiceRef.current.setAnswerReceivedHandler(async (fromUserId, answer) => { + await webRTCServiceRef.current?.handleAnswer(fromUserId, answer) + }) + + signalRServiceRef.current.setIceCandidateReceivedHandler(async (fromUserId, candidate) => { + await webRTCServiceRef.current?.addIceCandidate(fromUserId, candidate) + }) + // Setup SignalR event handlers - signalRServiceRef.current.setParticipantJoinHandler((userId, name) => { + signalRServiceRef.current.setParticipantJoinHandler(async (userId, name) => { // 🔑 Eğer gelen participant bizsek, listeye ekleme if (userId === user.id) return @@ -250,6 +274,12 @@ const RoomDetail: React.FC = () => { // Create WebRTC connection for new participant if (webRTCServiceRef.current) { webRTCServiceRef.current.createPeerConnection(userId) + + // Eğer biz teacher isek offer oluşturup gönderelim + if (user.role === 'teacher') { + const offer = await webRTCServiceRef.current.createOffer(userId) + await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) + } } setParticipants((prev) => {