IsKicked prop ayrı oluşturuldu
This commit is contained in:
parent
3b72c8e28a
commit
ae3a59e619
8 changed files with 128 additions and 32 deletions
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
participant.IsActive = false;
|
||||
participant.ConnectionId = null;
|
||||
|
||||
await _participantRepository.UpdateAsync(participant, autoSave: true);
|
||||
// Eğer kullanıcı kick edilmemişse pasifleştir
|
||||
if (!participant.IsKicked)
|
||||
{
|
||||
participant.IsActive = false;
|
||||
participant.ConnectionId = null;
|
||||
await _participantRepository.UpdateAsync(participant, autoSave: true);
|
||||
}
|
||||
|
||||
// 🔑 Frontend’e bildir
|
||||
await Clients.Group(participant.SessionId.ToString())
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ export interface ClassroomParticipantDto {
|
|||
isAudioMuted?: boolean
|
||||
isVideoMuted?: boolean
|
||||
isHandRaised?: boolean
|
||||
isKicked?: boolean
|
||||
isActive?: boolean
|
||||
stream?: MediaStream
|
||||
screenStream?: MediaStream
|
||||
|
|
|
|||
|
|
@ -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 state’den 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,11 +499,18 @@ export class SignalRService {
|
|||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.isConnected && this.connection) {
|
||||
await this.connection.stop()
|
||||
this.isConnected = false
|
||||
this.currentSessionId = undefined
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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 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 {
|
||||
|
|
@ -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 → 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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -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,9 +310,11 @@ const RoomDetail: React.FC = () => {
|
|||
}
|
||||
|
||||
// ✅ öğretmen ise her zaman offer başlatır
|
||||
if (isActive && user.id < userId) {
|
||||
const offer = await webRTCServiceRef.current!.createOffer(userId)
|
||||
await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer)
|
||||
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 }}
|
||||
|
|
|
|||
Loading…
Reference in a new issue