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

View file

@ -138,13 +138,14 @@ public class ClassroomHub : Hub
);
// 🚨 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.");
Context.Abort(); // bağlantıyı anında kapat
return;
}
if (participant == null)
{
participant = new ClassroomParticipant(
@ -155,7 +156,8 @@ public class ClassroomHub : Hub
isTeacher,
initialMuteState,
classroomSettings.DefaultCameraState == "off",
false,
false, //isHandRaised
false, //isKicked
isActive
);
await _participantRepository.InsertAsync(participant, autoSave: true);
@ -393,11 +395,18 @@ public class ClassroomHub : Hub
.SendAsync("ForceDisconnect", "You have been removed from the class.");
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())
.SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId });
}
// 3. Diğerlerine duyur
_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
if (!participant.IsKicked)
{
participant.IsActive = false;
participant.ConnectionId = null;
await _participantRepository.UpdateAsync(participant, autoSave: true);
}
// 🔑 Frontende bildir
await Clients.Group(participant.SessionId.ToString())

View file

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

View file

@ -39,7 +39,7 @@ export class SignalRService {
.withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, {
accessTokenFactory: () => auth.session.token || '',
})
//.withAutomaticReconnect()
.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information)
.build()
@ -97,8 +97,15 @@ export class SignalRService {
},
)
this.connection.onreconnected(() => {
this.connection.onreconnected(async () => {
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) => {
@ -128,34 +135,45 @@ export class SignalRService {
this.connection.onreconnecting((err) => {
if (this.isKicked) {
console.warn('Reconnect blocked because user was kicked')
// ❌ otomatik reconnect'i iptal etmek için stop çağır
this.connection.stop()
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) => {
console.warn('⚠️ ForceDisconnect received:', message)
this.isKicked = true // ✅ kick yediğini işaretle
this.isKicked = true
await this.connection.stop()
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
})
}
async start(): Promise<void> {
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
} catch (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
}
}
@ -481,12 +499,19 @@ export class SignalRService {
}
async disconnect(): Promise<void> {
if (this.isConnected && this.connection) {
if (this.isConnected && this.currentSessionId) {
try {
await this.connection.invoke('LeaveClass', this.currentSessionId)
} catch (err) {
console.warn('LeaveClass gönderilemedi:', err)
}
}
if (this.connection) {
await this.connection.stop()
}
this.isConnected = false
this.currentSessionId = undefined
}
}
getConnectionState(): boolean {
return this.isConnected

View file

@ -3,6 +3,7 @@ export class WebRTCService {
private localStream: MediaStream | null = null
private onRemoteStream?: (userId: string, stream: MediaStream) => void
private onIceCandidate?: (userId: string, candidate: RTCIceCandidateInit) => void
private candidateBuffer: Map<string, RTCIceCandidateInit[]> = new Map()
private rtcConfiguration: RTCConfiguration = {
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.
*/
@ -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
}
@ -119,7 +144,15 @@ export class WebRTCService {
async addIceCandidate(userId: string, candidate: RTCIceCandidateInit): Promise<void> {
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') {
try {
@ -128,7 +161,12 @@ export class WebRTCService {
console.warn(`ICE candidate eklenemedi [${userId}]:`, err)
}
} 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
onClick={event}
disabled={status === 'Katılıma Açık' ? true : false}
className={`px-3 sm:px-4 py-2 rounded-lg transition-colors ${
classes
}`}

View file

@ -144,6 +144,7 @@ const RoomDetail: React.FC = () => {
const signalRServiceRef = useRef<SignalRService>()
const webRTCServiceRef = useRef<WebRTCService>()
const [teacherDisconnected, setTeacherDisconnected] = useState(false)
const layouts: VideoLayoutDto[] = [
{
@ -281,6 +282,7 @@ const RoomDetail: React.FC = () => {
async (userId: string, name: string, isTeacher: boolean, isActive: boolean) => {
if (userId === user.id) return
if (!isActive) return // ❌ pasif kullanıcıyı ekleme
if (isTeacher) setTeacherDisconnected(false)
console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`)
@ -308,10 +310,12 @@ const RoomDetail: React.FC = () => {
}
// ✅ öğretmen ise her zaman offer başlatır
if (isActive && user.id < userId) {
if (isActive) {
if (user.role === 'teacher') {
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) {
;(async () => {
for (const participant of existing) {
if (!participant.isActive) continue // ❌ pasifleri bağlama
if (!participant.isActive) continue
if (participant.userId === user.id) continue
if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) {
await webRTCServiceRef.current?.createPeerConnection(participant.userId)
}
if (
participant.isTeacher ||
(participant.isActive && user.id < participant.userId)
) {
// ✅ sadece öğretmen offer başlatır
if (user.role === 'teacher') {
const offer = await webRTCServiceRef.current!.createOffer(participant.userId)
await signalRServiceRef.current?.sendOffer(
classSession.id,
@ -385,6 +389,10 @@ const RoomDetail: React.FC = () => {
setParticipants((prev) =>
prev.filter((p) => !(p.id === userId && p.sessionId === sessionId)),
)
if (participants.find((p) => p.id === userId)?.isTeacher) {
setTeacherDisconnected(true)
}
})
signalRServiceRef.current.setAttendanceUpdatedHandler((record) => {
@ -446,7 +454,9 @@ const RoomDetail: React.FC = () => {
const cleanup = async () => {
if (signalRServiceRef.current) {
// ✅ önce LeaveClass
await signalRServiceRef.current.leaveClass(classSession.id)
// sonra disconnect
await signalRServiceRef.current.disconnect()
}
@ -810,6 +820,12 @@ const RoomDetail: React.FC = () => {
defaultTitle="Kurs Platform"
></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">
<motion.div
initial={{ opacity: 0 }}