IsKicked prop ayrı oluşturuldu

This commit is contained in:
Sedat Öztürk 2025-08-31 18:19:28 +03:00
parent 3b72c8e28a
commit ae3a59e619
8 changed files with 128 additions and 32 deletions

View file

@ -12,6 +12,7 @@ public class ClassroomParticipantDto
public bool IsAudioMuted { get; set; } public bool IsAudioMuted { get; set; }
public bool IsVideoMuted { get; set; } public bool IsVideoMuted { get; set; }
public bool IsHandRaised { get; set; } public bool IsHandRaised { get; set; }
public bool IsKicked { get; set; }
public DateTime JoinTime { get; set; } public DateTime JoinTime { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
} }

View file

@ -12,6 +12,7 @@ public class ClassroomParticipant : FullAuditedEntity<Guid>
public bool IsAudioMuted { get; set; } = false; public bool IsAudioMuted { get; set; } = false;
public bool IsVideoMuted { get; set; } = false; public bool IsVideoMuted { get; set; } = false;
public bool IsHandRaised { get; set; } = false; public bool IsHandRaised { get; set; } = false;
public bool IsKicked { get; set; } = false;
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public DateTime JoinTime { get; set; } public DateTime JoinTime { get; set; }
public string ConnectionId { get; set; } public string ConnectionId { get; set; }
@ -32,6 +33,7 @@ public class ClassroomParticipant : FullAuditedEntity<Guid>
bool isAudioMuted, bool isAudioMuted,
bool isVideoMuted, bool isVideoMuted,
bool isHandRaised, bool isHandRaised,
bool isKicked,
bool isActive bool isActive
) : base(id) ) : base(id)
{ {
@ -43,6 +45,7 @@ public class ClassroomParticipant : FullAuditedEntity<Guid>
IsVideoMuted = isVideoMuted; IsVideoMuted = isVideoMuted;
IsHandRaised = isHandRaised; IsHandRaised = isHandRaised;
IsActive = isActive; IsActive = isActive;
IsKicked = isKicked;
JoinTime = DateTime.UtcNow; JoinTime = DateTime.UtcNow;
} }

View file

@ -138,13 +138,14 @@ public class ClassroomHub : Hub
); );
// 🚨 Kick edilmiş kullanıcı tekrar giriş yapamaz // 🚨 Kick edilmiş kullanıcı tekrar giriş yapamaz
if (participant != null && !participant.IsActive) if (participant != null && participant.IsKicked)
{ {
await Clients.Caller.SendAsync("Error", "You are not allowed to rejoin this class."); await Clients.Caller.SendAsync("Error", "You are not allowed to rejoin this class.");
Context.Abort(); // bağlantıyı anında kapat Context.Abort(); // bağlantıyı anında kapat
return; return;
} }
if (participant == null) if (participant == null)
{ {
participant = new ClassroomParticipant( participant = new ClassroomParticipant(
@ -155,7 +156,8 @@ public class ClassroomHub : Hub
isTeacher, isTeacher,
initialMuteState, initialMuteState,
classroomSettings.DefaultCameraState == "off", classroomSettings.DefaultCameraState == "off",
false, false, //isHandRaised
false, //isKicked
isActive isActive
); );
await _participantRepository.InsertAsync(participant, autoSave: true); await _participantRepository.InsertAsync(participant, autoSave: true);
@ -393,11 +395,18 @@ public class ClassroomHub : Hub
.SendAsync("ForceDisconnect", "You have been removed from the class."); .SendAsync("ForceDisconnect", "You have been removed from the class.");
await Groups.RemoveFromGroupAsync(participant.ConnectionId, sessionId.ToString()); await Groups.RemoveFromGroupAsync(participant.ConnectionId, sessionId.ToString());
await DeactivateParticipantAsync(participant);
// ❌ pasif + ✅ kicked işaretle
participant.IsActive = false;
participant.IsKicked = true;
participant.ConnectionId = null;
await _participantRepository.UpdateAsync(participant, autoSave: true);
await Clients.Group(sessionId.ToString()) await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId }); .SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId });
} }
// 3. Diğerlerine duyur // 3. Diğerlerine duyur
_logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId); _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId);
} }
@ -521,11 +530,13 @@ public class ClassroomHub : Hub
} }
} }
// 🔑 Participant pasifleştir // Eğer kullanıcı kick edilmemişse pasifleştir
participant.IsActive = false; if (!participant.IsKicked)
participant.ConnectionId = null; {
participant.IsActive = false;
await _participantRepository.UpdateAsync(participant, autoSave: true); participant.ConnectionId = null;
await _participantRepository.UpdateAsync(participant, autoSave: true);
}
// 🔑 Frontende bildir // 🔑 Frontende bildir
await Clients.Group(participant.SessionId.ToString()) await Clients.Group(participant.SessionId.ToString())

View file

@ -62,6 +62,7 @@ export interface ClassroomParticipantDto {
isAudioMuted?: boolean isAudioMuted?: boolean
isVideoMuted?: boolean isVideoMuted?: boolean
isHandRaised?: boolean isHandRaised?: boolean
isKicked?: boolean
isActive?: boolean isActive?: boolean
stream?: MediaStream stream?: MediaStream
screenStream?: MediaStream screenStream?: MediaStream

View file

@ -39,7 +39,7 @@ export class SignalRService {
.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() .withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information) .configureLogging(signalR.LogLevel.Information)
.build() .build()
@ -97,8 +97,15 @@ export class SignalRService {
}, },
) )
this.connection.onreconnected(() => { this.connection.onreconnected(async () => {
this.isConnected = true this.isConnected = true
console.warn('🔄 SignalR reconnected')
// Eğer sınıftayken bağlantı koptuysa → tekrar join et
if (this.currentSessionId && store.getState().auth.user) {
const u = store.getState().auth.user
await this.joinClass(this.currentSessionId, u.id, u.name, u.role === 'teacher', true)
}
}) })
this.connection.onclose(async (err) => { this.connection.onclose(async (err) => {
@ -128,34 +135,45 @@ export class SignalRService {
this.connection.onreconnecting((err) => { this.connection.onreconnecting((err) => {
if (this.isKicked) { if (this.isKicked) {
console.warn('Reconnect blocked because user was kicked') console.warn('Reconnect blocked because user was kicked')
// ❌ otomatik reconnect'i iptal etmek için stop çağır
this.connection.stop() this.connection.stop()
throw new Error('Reconnect blocked after kick') throw new Error('Reconnect blocked after kick')
} }
}) })
//2. tane problem var.
//1. problem kick yapamıyorum
//2. Öğrenci veya öğretmen farklı bir sayfaya gidince ekranda donuk şekilde duruyor.
this.connection.on('ForceDisconnect', async (message: string) => { this.connection.on('ForceDisconnect', async (message: string) => {
console.warn('⚠️ ForceDisconnect received:', message) console.warn('⚠️ ForceDisconnect received:', message)
this.isKicked = true // ✅ kick yediğini işaretle this.isKicked = true
await this.connection.stop() await this.connection.stop()
this.isConnected = false this.isConnected = false
// ✅ frontend stateden de çıkar
if (this.currentSessionId && store.getState().auth.user) {
this.onParticipantLeft?.({
userId: store.getState().auth.user.id,
sessionId: this.currentSessionId,
})
}
this.currentSessionId = undefined
window.location.href = ROUTES_ENUM.protected.admin.classroom.classes window.location.href = ROUTES_ENUM.protected.admin.classroom.classes
}) })
} }
async start(): Promise<void> { async start(): Promise<void> {
try { try {
await this.connection.start() const startPromise = this.connection.start()
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000),
)
await Promise.race([startPromise, timeout])
this.isConnected = true this.isConnected = true
} catch (error) { } catch (error) {
console.error('Error starting SignalR connection:', error) console.error('Error starting SignalR connection:', error)
// Switch to demo mode if connection fails alert(
'⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin.',
)
this.isConnected = false this.isConnected = false
} }
} }
@ -481,11 +499,18 @@ export class SignalRService {
} }
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (this.isConnected && this.connection) { if (this.isConnected && this.currentSessionId) {
await this.connection.stop() try {
this.isConnected = false await this.connection.invoke('LeaveClass', this.currentSessionId)
this.currentSessionId = undefined } catch (err) {
console.warn('LeaveClass gönderilemedi:', err)
}
} }
if (this.connection) {
await this.connection.stop()
}
this.isConnected = false
this.currentSessionId = undefined
} }
getConnectionState(): boolean { getConnectionState(): boolean {

View file

@ -3,6 +3,7 @@ export class WebRTCService {
private localStream: MediaStream | null = null private localStream: MediaStream | null = null
private onRemoteStream?: (userId: string, stream: MediaStream) => void private onRemoteStream?: (userId: string, stream: MediaStream) => void
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
private candidateBuffer: Map<string, RTCIceCandidateInit[]> = new Map()
private rtcConfiguration: RTCConfiguration = { private rtcConfiguration: RTCConfiguration = {
iceServers: [ iceServers: [
@ -11,6 +12,17 @@ export class WebRTCService {
], ],
} }
// private rtcConfiguration: RTCConfiguration = {
// iceServers: [
// { urls: 'stun:stun.l.google.com:19302' }, // STUN
// {
// urls: ['turn:your-server-ip:3478?transport=udp', 'turn:your-server-ip:3478?transport=tcp'],
// username: 'kurs', // static user/pass kullanmak istersen
// credential: 'kurs12345',
// },
// ],
// }
/** /**
* Local stream'i başlatır. Kamera/mikrofon ayarlarını parametreden alır. * Local stream'i başlatır. Kamera/mikrofon ayarlarını parametreden alır.
*/ */
@ -71,6 +83,19 @@ export class WebRTCService {
} }
} }
// En sona ekle
if (this.candidateBuffer.has(userId)) {
for (const cand of this.candidateBuffer.get(userId)!) {
try {
await peerConnection.addIceCandidate(cand)
console.log(`Buffered ICE candidate eklendi [${userId}]`)
} catch (err) {
console.warn(`Buffered candidate eklenemedi [${userId}]:`, err)
}
}
this.candidateBuffer.delete(userId)
}
return peerConnection return peerConnection
} }
@ -119,7 +144,15 @@ export class WebRTCService {
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) throw new Error('Peer connection not found') if (!pc) {
// Peer yoksa buffera 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') { if (pc.signalingState === 'stable' || pc.signalingState === 'have-remote-offer') {
try { try {
@ -128,7 +161,12 @@ export class WebRTCService {
console.warn(`ICE candidate eklenemedi [${userId}]:`, err) console.warn(`ICE candidate eklenemedi [${userId}]:`, err)
} }
} else { } else {
console.warn(`ICE candidate atlandı [${userId}], signalingState=${pc.signalingState}`) // signalling hazır değilse → buffera 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}`)
} }
} }

View file

@ -486,6 +486,7 @@ const ClassList: React.FC = () => {
<button <button
onClick={event} onClick={event}
disabled={status === 'Katılıma Açık' ? true : false}
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${ className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
classes classes
}`} }`}

View file

@ -144,6 +144,7 @@ const RoomDetail: React.FC = () => {
const signalRServiceRef = useRef<SignalRService>() const signalRServiceRef = useRef<SignalRService>()
const webRTCServiceRef = useRef<WebRTCService>() const webRTCServiceRef = useRef<WebRTCService>()
const [teacherDisconnected, setTeacherDisconnected] = useState(false)
const layouts: VideoLayoutDto[] = [ const layouts: VideoLayoutDto[] = [
{ {
@ -281,6 +282,7 @@ const RoomDetail: React.FC = () => {
async (userId: string, name: string, isTeacher: boolean, isActive: boolean) => { async (userId: string, name: string, isTeacher: boolean, isActive: boolean) => {
if (userId === user.id) return if (userId === user.id) return
if (!isActive) return // ❌ pasif kullanıcıyı ekleme if (!isActive) return // ❌ pasif kullanıcıyı ekleme
if (isTeacher) setTeacherDisconnected(false)
console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`) console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`)
@ -308,9 +310,11 @@ const RoomDetail: React.FC = () => {
} }
// ✅ öğretmen ise her zaman offer başlatır // ✅ öğretmen ise her zaman offer başlatır
if (isActive && user.id < userId) { if (isActive) {
const offer = await webRTCServiceRef.current!.createOffer(userId) if (user.role === 'teacher') {
await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) const offer = await webRTCServiceRef.current!.createOffer(userId)
await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer)
}
} }
})() })()
} }
@ -350,15 +354,15 @@ const RoomDetail: React.FC = () => {
if (teacherExists) { if (teacherExists) {
;(async () => { ;(async () => {
for (const participant of existing) { for (const participant of existing) {
if (!participant.isActive) continue // ❌ pasifleri bağlama if (!participant.isActive) continue
if (participant.userId === user.id) continue if (participant.userId === user.id) continue
if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) { if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) {
await webRTCServiceRef.current?.createPeerConnection(participant.userId) await webRTCServiceRef.current?.createPeerConnection(participant.userId)
} }
if (
participant.isTeacher || // ✅ sadece öğretmen offer başlatır
(participant.isActive && user.id < participant.userId) if (user.role === 'teacher') {
) {
const offer = await webRTCServiceRef.current!.createOffer(participant.userId) const offer = await webRTCServiceRef.current!.createOffer(participant.userId)
await signalRServiceRef.current?.sendOffer( await signalRServiceRef.current?.sendOffer(
classSession.id, classSession.id,
@ -385,6 +389,10 @@ const RoomDetail: React.FC = () => {
setParticipants((prev) => setParticipants((prev) =>
prev.filter((p) => !(p.id === userId && p.sessionId === sessionId)), prev.filter((p) => !(p.id === userId && p.sessionId === sessionId)),
) )
if (participants.find((p) => p.id === userId)?.isTeacher) {
setTeacherDisconnected(true)
}
}) })
signalRServiceRef.current.setAttendanceUpdatedHandler((record) => { signalRServiceRef.current.setAttendanceUpdatedHandler((record) => {
@ -446,7 +454,9 @@ const RoomDetail: React.FC = () => {
const cleanup = async () => { const cleanup = async () => {
if (signalRServiceRef.current) { if (signalRServiceRef.current) {
// ✅ önce LeaveClass
await signalRServiceRef.current.leaveClass(classSession.id) await signalRServiceRef.current.leaveClass(classSession.id)
// sonra disconnect
await signalRServiceRef.current.disconnect() await signalRServiceRef.current.disconnect()
} }
@ -810,6 +820,12 @@ const RoomDetail: React.FC = () => {
defaultTitle="Kurs Platform" defaultTitle="Kurs Platform"
></Helmet> ></Helmet>
{teacherDisconnected && (
<div className="bg-red-600 text-white p-2 text-center">
Öğretmenin bağlantısı koptu, tekrar bağlanmasını bekleyin...
</div>
)}
<div className="min-h-screen h-screen flex flex-col bg-gray-900 text-white overflow-hidden"> <div className="min-h-screen h-screen flex flex-col bg-gray-900 text-white overflow-hidden">
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}