Luisnguyen1 commited on
Commit
c77381d
·
1 Parent(s): 3feb9c4

sua track

Browse files
Files changed (8) hide show
  1. .env +2 -2
  2. .env.example +15 -0
  3. basic.html +522 -0
  4. go.mod +22 -18
  5. go.sum +43 -37
  6. index.js +7 -0
  7. main.go +769 -864
  8. public/js/room.js +2 -0
.env CHANGED
@@ -3,8 +3,8 @@ DEBUG=false
3
  DEBUG=true
4
  NODE_ENV='development'
5
 
6
- CLOUDFLARE_APP_ID='b0ec9f519563a78aacb031048bd9bb2b'
7
- CLOUDFLARE_APP_SECRET='e5aa7484fe2ddf300e69e0e66dd3dd06d8628a5764ac8c237f63f7762d5db234'
8
 
9
  CLOUDFLARE_TURN_ID='4f88cf5ab3d61f069a9c7fe38502ddef'
10
  CLOUDFLARE_TURN_TOKEN='5a8acf5e87457eea45222ec73195fb560704cbfe949928946694f05b7c76f5f3'
 
3
  DEBUG=true
4
  NODE_ENV='development'
5
 
6
+ CLOUDFLARE_APP_ID='d8f5af0fe38ea134ebd03e7bf0a8ae14'
7
+ CLOUDFLARE_APP_SECRET='3d9031d1c83112f68636517a1b4c765161bf0b7667fe9a665b528f760b1f4f28'
8
 
9
  CLOUDFLARE_TURN_ID='4f88cf5ab3d61f069a9c7fe38502ddef'
10
  CLOUDFLARE_TURN_TOKEN='5a8acf5e87457eea45222ec73195fb560704cbfe949928946694f05b7c76f5f3'
.env.example ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cloudflare Calls Configuration
2
+ CLOUDFLARE_APP_ID=your_app_id
3
+ CLOUDFLARE_APP_SECRET=your_app_secret
4
+ CLOUDFLARE_TURN_ID=your_turn_id
5
+ CLOUDFLARE_TURN_TOKEN=your_turn_token
6
+ CLOUDFLARE_APPS_URL=https://rtc.live.cloudflare.com/v1/apps
7
+
8
+ # Server Configuration
9
+ PORT=50000
10
+ GIN_MODE=debug
11
+ JWT_SECRET=your_jwt_secret
12
+
13
+ # Feature Flags
14
+ AUTH_REQUIRED=false
15
+ DEBUG=true
basic.html ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Simple Video Call</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/8.1.2/adapter.min.js"></script>
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <h1>Video Call</h1>
12
+ <div class="video-grid">
13
+ <div>
14
+ <h2>Local Video</h2>
15
+ <video id="localVideo" autoplay muted playsinline></video>
16
+ </div>
17
+ <div>
18
+ <h2>Remote Video</h2>
19
+ <video id="remoteVideo" autoplay playsinline></video>
20
+ </div>
21
+ </div>
22
+ <div class="controls">
23
+ <button id="startButton">Start Call</button>
24
+ <input id="sessionInput" placeholder="Enter session ID to join">
25
+ <button id="joinButton">Join Call</button>
26
+ <button id="endButton">End Call</button>
27
+ <div>
28
+ <label for="sessionId">Session ID:</label>
29
+ <input id="sessionId" readonly>
30
+ <button id="copyButton">Copy</button>
31
+ </div>
32
+ </div>
33
+ </div>
34
+
35
+ <script>
36
+ const APP_ID = "d8f5af0fe38ea134ebd03e7bf0a8ae14"; // Thay thế bằng APP_ID của bạn
37
+ const APP_TOKEN = "3d9031d1c83112f68636517a1b4c765161bf0b7667fe9a665b528f760b1f4f28"; // Thay thế bằng APP_TOKEN của bạn
38
+ const API_BASE = `https://rtc.live.cloudflare.com/v1/apps/${APP_ID}`;
39
+
40
+ let localStream;
41
+ let localPeerConnection;
42
+ let localSessionId;
43
+ let remoteSessionId = null; // Thêm biến để lưu session id của người tham gia
44
+
45
+ const localVideo = document.getElementById('localVideo');
46
+ const remoteVideo = document.getElementById('remoteVideo');
47
+ const startButton = document.getElementById('startButton');
48
+ const endButton = document.getElementById('endButton');
49
+ const sessionInput = document.getElementById('sessionInput');
50
+ const joinButton = document.getElementById('joinButton');
51
+ const sessionIdInput = document.getElementById('sessionId');
52
+ const copyButton = document.getElementById('copyButton');
53
+
54
+ startButton.onclick = startCall;
55
+ endButton.onclick = endCall;
56
+ joinButton.onclick = joinCall;
57
+ copyButton.onclick = () => {
58
+ sessionIdInput.select();
59
+ document.execCommand('copy');
60
+ alert('Session ID copied to clipboard');
61
+ };
62
+
63
+ async function createSession() {
64
+ const response = await fetch(`${API_BASE}/sessions/new`, {
65
+ method: "POST",
66
+ headers: {
67
+ "Authorization": `Bearer ${APP_TOKEN}`,
68
+ "Content-Type": "application/json"
69
+ }
70
+ }).then(res => res.json());
71
+
72
+ if (response.errorCode) {
73
+ throw new Error(response.errorDescription);
74
+ }
75
+ return response.sessionId;
76
+ }
77
+
78
+ async function createPeerConnection() {
79
+ const peerConnection = new RTCPeerConnection({
80
+ iceServers: [{
81
+ urls: "stun:stun.cloudflare.com:3478"
82
+ }],
83
+ bundlePolicy: "max-bundle"
84
+ });
85
+
86
+ // Add ICE candidate handling
87
+ peerConnection.onicecandidate = (event) => {
88
+ if (event.candidate) {
89
+ console.log("New ICE candidate:", event.candidate);
90
+ }
91
+ };
92
+
93
+ // Add connection state handling
94
+ peerConnection.onconnectionstatechange = (event) => {
95
+ console.log("Connection state:", peerConnection.connectionState);
96
+ };
97
+
98
+ // Add track handling
99
+ peerConnection.ontrack = (event) => {
100
+ console.log("Received remote track:", event);
101
+ if (remoteVideo.srcObject !== event.streams[0]) {
102
+ remoteVideo.srcObject = event.streams[0];
103
+ }
104
+ };
105
+
106
+ return peerConnection;
107
+ }
108
+
109
+ async function startCall() {
110
+ try {
111
+ // Get local media stream
112
+ localStream = await navigator.mediaDevices.getUserMedia({
113
+ audio: true,
114
+ video: true
115
+ });
116
+ localVideo.srcObject = localStream;
117
+
118
+ // Create new session
119
+ localSessionId = await createSession();
120
+ localPeerConnection = await createPeerConnection();
121
+
122
+ // Add tracks using addTransceiver API like in demo.html
123
+ const transceivers = localStream.getTracks().map(track =>
124
+ localPeerConnection.addTransceiver(track, {
125
+ direction: "sendonly"
126
+ })
127
+ );
128
+
129
+ // Create and set local offer
130
+ const offer = await localPeerConnection.createOffer();
131
+ await localPeerConnection.setLocalDescription(offer);
132
+
133
+ // Format request according to API schema
134
+ const requestBody = {
135
+ sessionDescription: {
136
+ sdp: offer.sdp,
137
+ type: "offer"
138
+ },
139
+ tracks: transceivers.map(({mid, sender}) => ({
140
+ location: "local",
141
+ mid: mid,
142
+ trackName: sender.track?.id
143
+ }))
144
+ };
145
+
146
+ // Set up ICE connection state handler
147
+ const connected = new Promise((resolve, reject) => {
148
+ setTimeout(() => reject(new Error("ICE connection timeout")), 5000);
149
+
150
+ const iceConnectionStateChangeHandler = () => {
151
+ if (localPeerConnection.iceConnectionState === "connected") {
152
+ localPeerConnection.removeEventListener(
153
+ "iceconnectionstatechange",
154
+ iceConnectionStateChangeHandler
155
+ );
156
+ resolve();
157
+ }
158
+ };
159
+
160
+ localPeerConnection.addEventListener(
161
+ "iceconnectionstatechange",
162
+ iceConnectionStateChangeHandler
163
+ );
164
+ });
165
+
166
+ // Send tracks to API
167
+ const response = await fetch(`${API_BASE}/sessions/${localSessionId}/tracks/new`, {
168
+ method: "POST",
169
+ headers: {
170
+ "Authorization": `Bearer ${APP_TOKEN}`,
171
+ "Content-Type": "application/json"
172
+ },
173
+ body: JSON.stringify(requestBody)
174
+ });
175
+
176
+ if (!response.ok) {
177
+ throw new Error(`API error: ${response.status}`);
178
+ }
179
+
180
+ const responseData = await response.json();
181
+
182
+ // Set remote description from response
183
+ await localPeerConnection.setRemoteDescription(
184
+ new RTCSessionDescription(responseData.sessionDescription)
185
+ );
186
+
187
+ // Wait for ICE connection
188
+ await connected;
189
+
190
+ // Update UI
191
+ sessionIdInput.value = localSessionId;
192
+ startSessionCheck(localSessionId);
193
+
194
+ } catch (err) {
195
+ console.error("Error starting call:", err);
196
+ alert("Error starting call: " + err.message);
197
+ }
198
+ }
199
+
200
+ async function joinCall() {
201
+ try {
202
+ const sessionToJoin = sessionInput.value.trim();
203
+ if (!sessionToJoin) {
204
+ alert('Please enter a session ID');
205
+ return;
206
+ }
207
+
208
+ // Get session state first
209
+ const sessionState = await fetch(`${API_BASE}/sessions/${sessionToJoin}`, {
210
+ headers: {
211
+ "Authorization": `Bearer ${APP_TOKEN}`
212
+ }
213
+ }).then(res => res.json());
214
+
215
+ console.log("Session state:", sessionState);
216
+
217
+ if (!sessionState.tracks || sessionState.tracks.length === 0) {
218
+ throw new Error("No active tracks in session");
219
+ }
220
+
221
+ // Initialize local resources
222
+ localStream = await navigator.mediaDevices.getUserMedia({
223
+ audio: true,
224
+ video: true
225
+ });
226
+ localVideo.srcObject = localStream;
227
+ localPeerConnection = await createPeerConnection();
228
+ localSessionId = await createSession();
229
+
230
+ // First add our local tracks
231
+ const localTransceivers = localStream.getTracks().map(track =>
232
+ localPeerConnection.addTransceiver(track, {
233
+ direction: "sendonly"
234
+ })
235
+ );
236
+
237
+ // Create and set local offer for our tracks
238
+ const localOffer = await localPeerConnection.createOffer();
239
+ await localPeerConnection.setLocalDescription(localOffer);
240
+
241
+ // Send our tracks first
242
+ const addLocalTracksResponse = await fetch(`${API_BASE}/sessions/${localSessionId}/tracks/new`, {
243
+ method: "POST",
244
+ headers: {
245
+ "Authorization": `Bearer ${APP_TOKEN}`,
246
+ "Content-Type": "application/json"
247
+ },
248
+ body: JSON.stringify({
249
+ sessionDescription: {
250
+ sdp: localOffer.sdp,
251
+ type: "offer"
252
+ },
253
+ tracks: localTransceivers.map(({mid, sender}) => ({
254
+ location: "local",
255
+ mid: mid,
256
+ trackName: sender.track?.id
257
+ }))
258
+ })
259
+ }).then(res => res.json());
260
+
261
+ // Handle local tracks response
262
+ await localPeerConnection.setRemoteDescription(
263
+ new RTCSessionDescription(addLocalTracksResponse.sessionDescription)
264
+ );
265
+
266
+ // Now pull the remote tracks
267
+ const pullRequest = {
268
+ tracks: sessionState.tracks
269
+ .filter(track => track.status === 'active')
270
+ .map(track => ({
271
+ location: "remote",
272
+ sessionId: sessionToJoin,
273
+ trackName: track.trackName
274
+ }))
275
+ };
276
+
277
+ // Set up track handling for remote tracks
278
+ const resolvingTracks = new Promise((resolve, reject) => {
279
+ let receivedTracks = [];
280
+ const timeout = setTimeout(() => reject(new Error("Track timeout")), 10000);
281
+
282
+ const handleTrack = (event) => {
283
+ console.log("Received track:", event.track);
284
+ receivedTracks.push(event.track);
285
+
286
+ if (receivedTracks.length === pullRequest.tracks.length) {
287
+ clearTimeout(timeout);
288
+ localPeerConnection.removeEventListener('track', handleTrack);
289
+ resolve(receivedTracks);
290
+ }
291
+ };
292
+
293
+ localPeerConnection.addEventListener('track', handleTrack);
294
+ });
295
+
296
+ // Pull remote tracks
297
+ const pullResponse = await fetch(`${API_BASE}/sessions/${localSessionId}/tracks/new`, {
298
+ method: "POST",
299
+ headers: {
300
+ "Authorization": `Bearer ${APP_TOKEN}`,
301
+ "Content-Type": "application/json"
302
+ },
303
+ body: JSON.stringify(pullRequest)
304
+ }).then(res => res.json());
305
+
306
+ // Handle remote tracks
307
+ if (pullResponse.requiresImmediateRenegotiation) {
308
+ await localPeerConnection.setRemoteDescription(
309
+ new RTCSessionDescription(pullResponse.sessionDescription)
310
+ );
311
+
312
+ const answer = await localPeerConnection.createAnswer();
313
+ await localPeerConnection.setLocalDescription(answer);
314
+
315
+ await fetch(`${API_BASE}/sessions/${localSessionId}/renegotiate`, {
316
+ method: "PUT",
317
+ headers: {
318
+ "Authorization": `Bearer ${APP_TOKEN}`,
319
+ "Content-Type": "application/json"
320
+ },
321
+ body: JSON.stringify({
322
+ sessionDescription: {
323
+ sdp: answer.sdp,
324
+ type: "answer"
325
+ }
326
+ })
327
+ }).then(res => res.json());
328
+ }
329
+
330
+ // Wait for and display remote tracks
331
+ const pulledTracks = await resolvingTracks;
332
+ const remoteStream = new MediaStream();
333
+ pulledTracks.forEach(track => remoteStream.addTrack(track));
334
+ remoteVideo.srcObject = remoteStream;
335
+
336
+ startSessionCheck(localSessionId);
337
+ startSessionCheck(sessionToJoin);
338
+
339
+ } catch (err) {
340
+ console.error("Error joining call:", err);
341
+ alert("Error joining call: " + err.message);
342
+ }
343
+ }
344
+
345
+ async function endCall() {
346
+ try {
347
+ if (localSessionId) {
348
+ // Close tracks following schema
349
+ await fetch(`${API_BASE}/sessions/${localSessionId}/tracks/close`, {
350
+ method: "PUT",
351
+ headers: {
352
+ "Authorization": `Bearer ${APP_TOKEN}`,
353
+ "Content-Type": "application/json"
354
+ },
355
+ body: JSON.stringify({
356
+ tracks: [], // Close all tracks
357
+ force: true // Force close without renegotiation
358
+ })
359
+ });
360
+ }
361
+
362
+ // Clean up resources
363
+ if (localStream) {
364
+ localStream.getTracks().forEach(track => track.stop());
365
+ }
366
+ if (localPeerConnection) {
367
+ localPeerConnection.close();
368
+ }
369
+ localVideo.srcObject = null;
370
+ remoteVideo.srcObject = null;
371
+ sessionIdInput.value = '';
372
+ sessionInput.value = '';
373
+
374
+ } catch (err) {
375
+ console.error("Error ending call:", err);
376
+ }
377
+ }
378
+
379
+ // Sửa lại hàm startSessionCheck để thêm xử lý khi có người tham gia
380
+ function startSessionCheck(sessionId) {
381
+ const checkInterval = setInterval(async () => {
382
+ try {
383
+ const response = await fetch(`${API_BASE}/sessions/${sessionId}`, {
384
+ headers: {
385
+ "Authorization": `Bearer ${APP_TOKEN}`
386
+ }
387
+ }).then(res => res.json());
388
+
389
+ if (response.errorCode) {
390
+ clearInterval(checkInterval);
391
+ console.log(`Session ${sessionId} ended`);
392
+ return;
393
+ }
394
+
395
+ // Kiểm tra nếu là session gốc và có tracks mới
396
+ if (sessionId === localSessionId && response.tracks) {
397
+ const newTracks = response.tracks.filter(track =>
398
+ track.status === 'active' &&
399
+ track.sessionId !== localSessionId &&
400
+ track.sessionId !== remoteSessionId
401
+ );
402
+
403
+ if (newTracks.length > 0) {
404
+ console.log("New participant joined, pulling their tracks");
405
+ remoteSessionId = newTracks[0].sessionId;
406
+ await pullRemoteTracks(newTracks);
407
+ }
408
+ }
409
+ } catch (err) {
410
+ clearInterval(checkInterval);
411
+ console.error("Session check failed:", err);
412
+ }
413
+ }, 5000);
414
+ }
415
+
416
+ // Thêm hàm mới để pull tracks từ người tham gia
417
+ async function pullRemoteTracks(tracks) {
418
+ try {
419
+ // Set up track handling
420
+ const resolvingTracks = new Promise((resolve, reject) => {
421
+ let receivedTracks = [];
422
+ const timeout = setTimeout(() => reject(new Error("Track timeout")), 10000);
423
+
424
+ const handleTrack = (event) => {
425
+ console.log("Received remote participant track:", event.track);
426
+ receivedTracks.push(event.track);
427
+
428
+ if (receivedTracks.length === tracks.length) {
429
+ clearTimeout(timeout);
430
+ localPeerConnection.removeEventListener('track', handleTrack);
431
+ resolve(receivedTracks);
432
+ }
433
+ };
434
+
435
+ localPeerConnection.addEventListener('track', handleTrack);
436
+ });
437
+
438
+ // Pull remote tracks
439
+ const pullResponse = await fetch(`${API_BASE}/sessions/${localSessionId}/tracks/new`, {
440
+ method: "POST",
441
+ headers: {
442
+ "Authorization": `Bearer ${APP_TOKEN}`,
443
+ "Content-Type": "application/json"
444
+ },
445
+ body: JSON.stringify({
446
+ tracks: tracks.map(track => ({
447
+ location: "remote",
448
+ sessionId: track.sessionId,
449
+ trackName: track.trackName
450
+ }))
451
+ })
452
+ }).then(res => res.json());
453
+
454
+ if (pullResponse.requiresImmediateRenegotiation) {
455
+ await localPeerConnection.setRemoteDescription(
456
+ new RTCSessionDescription(pullResponse.sessionDescription)
457
+ );
458
+
459
+ const answer = await localPeerConnection.createAnswer();
460
+ await localPeerConnection.setLocalDescription(answer);
461
+
462
+ await fetch(`${API_BASE}/sessions/${localSessionId}/renegotiate`, {
463
+ method: "PUT",
464
+ headers: {
465
+ "Authorization": `Bearer ${APP_TOKEN}`,
466
+ "Content-Type": "application/json"
467
+ },
468
+ body: JSON.stringify({
469
+ sessionDescription: {
470
+ sdp: answer.sdp,
471
+ type: "answer"
472
+ }
473
+ })
474
+ }).then(res => res.json());
475
+ }
476
+
477
+ // Wait for and display remote tracks
478
+ const pulledTracks = await resolvingTracks;
479
+ const remoteStream = remoteVideo.srcObject instanceof MediaStream ?
480
+ remoteVideo.srcObject : new MediaStream();
481
+ pulledTracks.forEach(track => remoteStream.addTrack(track));
482
+ remoteVideo.srcObject = remoteStream;
483
+
484
+ } catch (err) {
485
+ console.error("Error pulling remote tracks:", err);
486
+ }
487
+ }
488
+ </script>
489
+
490
+ <style>
491
+ .container {
492
+ max-width: 1200px;
493
+ margin: 0 auto;
494
+ padding: 20px;
495
+ }
496
+ .video-grid {
497
+ display: grid;
498
+ grid-template-columns: 1fr 1fr;
499
+ gap: 20px;
500
+ margin: 20px 0;
501
+ }
502
+ video {
503
+ width: 100%;
504
+ background: #333;
505
+ }
506
+ .controls {
507
+ text-align: center;
508
+ }
509
+ button {
510
+ padding: 10px 20px;
511
+ margin: 0 10px;
512
+ }
513
+ .controls div {
514
+ margin-top: 10px;
515
+ }
516
+ #sessionId {
517
+ width: 200px;
518
+ margin-right: 10px;
519
+ }
520
+ </style>
521
+ </body>
522
+ </html>
go.mod CHANGED
@@ -1,40 +1,44 @@
1
- module cloudflare-calls-backend
2
 
3
- go 1.23.5
 
 
4
 
5
  require (
 
6
  github.com/gin-gonic/gin v1.10.0
7
- github.com/golang-jwt/jwt/v5 v5.2.1
8
- github.com/google/uuid v1.6.0
9
- github.com/gorilla/websocket v1.5.3
10
  github.com/joho/godotenv v1.5.1
11
  )
12
 
13
  require (
14
- github.com/bytedance/sonic v1.11.6 // indirect
15
- github.com/bytedance/sonic/loader v0.1.1 // indirect
16
  github.com/cloudwego/base64x v0.1.4 // indirect
17
  github.com/cloudwego/iasm v0.2.0 // indirect
18
- github.com/gabriel-vasile/mimetype v1.4.3 // indirect
19
  github.com/gin-contrib/sse v0.1.0 // indirect
20
  github.com/go-playground/locales v0.14.1 // indirect
21
  github.com/go-playground/universal-translator v0.18.1 // indirect
22
- github.com/go-playground/validator/v10 v10.20.0 // indirect
23
- github.com/goccy/go-json v0.10.2 // indirect
24
  github.com/json-iterator/go v1.1.12 // indirect
25
- github.com/klauspost/cpuid/v2 v2.2.7 // indirect
 
26
  github.com/leodido/go-urn v1.4.0 // indirect
27
  github.com/mattn/go-isatty v0.0.20 // indirect
28
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
29
  github.com/modern-go/reflect2 v1.0.2 // indirect
30
- github.com/pelletier/go-toml/v2 v2.2.2 // indirect
31
  github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
32
  github.com/ugorji/go/codec v1.2.12 // indirect
33
- golang.org/x/arch v0.8.0 // indirect
34
- golang.org/x/crypto v0.33.0 // indirect
35
- golang.org/x/net v0.35.0 // indirect
36
- golang.org/x/sys v0.30.0 // indirect
37
- golang.org/x/text v0.22.0 // indirect
38
- google.golang.org/protobuf v1.34.1 // indirect
39
  gopkg.in/yaml.v3 v3.0.1 // indirect
40
  )
 
1
+ module dappmeeting
2
 
3
+ go 1.21.0
4
+
5
+ toolchain go1.23.5
6
 
7
  require (
8
+ github.com/gin-contrib/cors v1.7.3
9
  github.com/gin-gonic/gin v1.10.0
10
+ github.com/golang-jwt/jwt/v5 v5.0.0
11
+ github.com/google/uuid v1.3.0
12
+ github.com/gorilla/websocket v1.5.0
13
  github.com/joho/godotenv v1.5.1
14
  )
15
 
16
  require (
17
+ github.com/bytedance/sonic v1.12.6 // indirect
18
+ github.com/bytedance/sonic/loader v0.2.1 // indirect
19
  github.com/cloudwego/base64x v0.1.4 // indirect
20
  github.com/cloudwego/iasm v0.2.0 // indirect
21
+ github.com/gabriel-vasile/mimetype v1.4.7 // indirect
22
  github.com/gin-contrib/sse v0.1.0 // indirect
23
  github.com/go-playground/locales v0.14.1 // indirect
24
  github.com/go-playground/universal-translator v0.18.1 // indirect
25
+ github.com/go-playground/validator/v10 v10.23.0 // indirect
26
+ github.com/goccy/go-json v0.10.4 // indirect
27
  github.com/json-iterator/go v1.1.12 // indirect
28
+ github.com/klauspost/cpuid/v2 v2.2.9 // indirect
29
+ github.com/kr/text v0.2.0 // indirect
30
  github.com/leodido/go-urn v1.4.0 // indirect
31
  github.com/mattn/go-isatty v0.0.20 // indirect
32
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
33
  github.com/modern-go/reflect2 v1.0.2 // indirect
34
+ github.com/pelletier/go-toml/v2 v2.2.3 // indirect
35
  github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
36
  github.com/ugorji/go/codec v1.2.12 // indirect
37
+ golang.org/x/arch v0.12.0 // indirect
38
+ golang.org/x/crypto v0.31.0 // indirect
39
+ golang.org/x/net v0.33.0 // indirect
40
+ golang.org/x/sys v0.28.0 // indirect
41
+ golang.org/x/text v0.21.0 // indirect
42
+ google.golang.org/protobuf v1.36.1 // indirect
43
  gopkg.in/yaml.v3 v3.0.1 // indirect
44
  )
go.sum CHANGED
@@ -1,16 +1,20 @@
1
- github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
2
- github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
3
- github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
4
  github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
 
 
5
  github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
6
  github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
7
  github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
8
  github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 
9
  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
- github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
13
- github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
 
 
14
  github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
15
  github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
16
  github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@@ -21,27 +25,31 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
21
  github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
22
  github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
23
  github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
24
- github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
25
- github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
26
- github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
27
- github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
28
- github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
29
- github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
30
  github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
31
  github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
32
  github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
33
- github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
34
- github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
35
- github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
36
- github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
37
  github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
38
  github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
39
  github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
40
  github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
41
  github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
42
- github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
43
- github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
44
  github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 
 
 
 
45
  github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
46
  github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
47
  github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -51,47 +59,45 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
51
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
52
  github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
53
  github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
54
- github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
55
- github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
56
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
57
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 
 
58
  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
59
  github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
60
  github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
61
- github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
62
  github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
63
  github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
64
  github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
65
  github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
66
  github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
67
- github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
68
  github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
69
  github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
70
  github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
71
  github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
72
  github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
73
  github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
74
- golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
75
- golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
76
- golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
77
- golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
78
- golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
79
- golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
80
- golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
81
- golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
82
  golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
83
- golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
84
- golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
85
- golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
86
- golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
87
  golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
88
  golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
89
- google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
90
- google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
91
- gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
92
  gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 
 
93
  gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
94
  gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
95
  gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
96
  nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
97
- rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 
1
+ github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
2
+ github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
 
3
  github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
4
+ github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
5
+ github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
6
  github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
7
  github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
8
  github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
9
  github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
10
+ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
11
  github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
  github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13
  github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14
+ github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
15
+ github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
16
+ github.com/gin-contrib/cors v1.7.3 h1:hV+a5xp8hwJoTw7OY+a70FsL8JkVVFTXw9EcfrYUdns=
17
+ github.com/gin-contrib/cors v1.7.3/go.mod h1:M3bcKZhxzsvI+rlRSkkxHyljJt1ESd93COUvemZ79j4=
18
  github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
19
  github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
20
  github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 
25
  github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
26
  github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
27
  github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
28
+ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
29
+ github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
30
+ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
31
+ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
32
+ github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
33
+ github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
34
  github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
35
  github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
36
  github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
37
+ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
38
+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
39
+ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
40
+ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
41
  github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
42
  github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
43
  github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
44
  github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
45
  github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
46
+ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
47
+ github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
48
  github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
49
+ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
50
+ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
51
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
52
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
53
  github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
54
  github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
55
  github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 
59
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
60
  github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
61
  github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
62
+ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
63
+ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
64
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
65
  github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
66
+ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
67
+ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
68
  github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
69
  github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
70
  github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 
71
  github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
72
  github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
73
  github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
74
  github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
75
  github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 
76
  github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
77
  github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
78
  github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
79
  github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
80
  github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
81
  github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
82
+ golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
83
+ golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
84
+ golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
85
+ golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
86
+ golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
87
+ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
 
 
88
  golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
89
+ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
90
+ golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
91
+ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
92
+ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
93
  golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
94
  golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
95
+ google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
96
+ google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 
97
  gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
98
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
99
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
100
  gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
101
  gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
102
  gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
103
  nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
 
index.js CHANGED
@@ -265,6 +265,13 @@ app.post('/api/rooms/:roomId/sessions/:sessionId/publish', verifyToken, async (r
265
  tracks
266
  })
267
  });
 
 
 
 
 
 
 
268
  const data = await cfResp.json();
269
  if (data.sessionDescription) {
270
  // Emit a 'track-published' event to other participants in the room
 
265
  tracks
266
  })
267
  });
268
+ const requestData = {
269
+ sessionDescription: offer,
270
+ tracks
271
+ };
272
+
273
+ // Log request JSON
274
+ console.log('Request JSON:', JSON.stringify(requestData, null, 2));
275
  const data = await cfResp.json();
276
  if (data.sessionDescription) {
277
  // Emit a 'track-published' event to other participants in the room
main.go CHANGED
@@ -11,10 +11,8 @@ import (
11
  "log"
12
  "net/http"
13
  "os"
14
- "os/signal"
15
  "strconv"
16
  "sync"
17
- "syscall"
18
  "time"
19
 
20
  "github.com/gin-gonic/gin"
@@ -57,6 +55,14 @@ func initConfig() {
57
  if cloudflareAppID == "" || cloudflareAppSecret == "" {
58
  log.Fatal("CLOUDFLARE_APP_ID and CLOUDFLARE_APP_SECRET must be set")
59
  }
 
 
 
 
 
 
 
 
60
  }
61
 
62
  // Helper function to get environment variables with default values
@@ -142,7 +148,7 @@ var wsConnections = struct {
142
 
143
  // --- JWT Verification Middleware ---
144
  func verifyToken(c *gin.Context) {
145
- if (!authRequired) {
146
  // Even when auth is disabled, set a default user
147
  defaultUser := &User{
148
  UserID: uuid.NewString(),
@@ -161,7 +167,7 @@ func verifyToken(c *gin.Context) {
161
  }
162
 
163
  authHeader := c.GetHeader("Authorization")
164
- if (authHeader == "*") {
165
  defaultUser := &User{
166
  UserID: uuid.NewString(),
167
  Username: "Anonymous",
@@ -224,8 +230,11 @@ func verifyToken(c *gin.Context) {
224
 
225
  func createCloudflareSession() (string, error) {
226
  url := fmt.Sprintf("%s/sessions/new", cloudflareBasePath)
 
 
227
  req, err := http.NewRequest("POST", url, nil)
228
  if err != nil {
 
229
  return "", err
230
  }
231
 
@@ -233,82 +242,165 @@ func createCloudflareSession() (string, error) {
233
  client := &http.Client{}
234
  resp, err := client.Do(req)
235
  if err != nil {
 
236
  return "", err
237
  }
238
  defer resp.Body.Close()
239
 
240
  body, err := io.ReadAll(resp.Body)
241
  if err != nil {
 
242
  return "", err
243
  }
244
 
 
 
245
  var responseData map[string]interface{}
246
  if err := json.Unmarshal(body, &responseData); err != nil {
 
247
  return "", err
248
  }
249
 
250
  sessionID, ok := responseData["sessionId"].(string)
251
  if !ok {
 
252
  return "", fmt.Errorf("sessionId not found in response: %s", string(body))
253
  }
 
 
254
  return sessionID, nil
255
  }
256
 
257
- func publishToCloudflare(sessionId string, offer map[string]interface{}, tracks []map[string]interface{}) (map[string]interface{}, error) {
258
- url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId)
259
-
260
- requestBody := map[string]interface{}{
261
- "sessionDescription": offer,
262
- "tracks": tracks,
263
- }
264
-
265
- jsonData, err := json.Marshal(requestBody)
266
- if (err != nil) {
267
- return nil, err
268
- }
269
-
270
- if debug {
271
- log.Printf("Publishing to Cloudflare: %s", string(jsonData))
272
- }
273
-
274
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
275
- if (err != nil) {
276
- return nil, err
277
- }
278
-
279
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cloudflareAppSecret))
280
- req.Header.Set("Content-Type", "application/json")
281
-
282
- client := &http.Client{
283
- Timeout: time.Second * 30,
284
- }
285
-
286
- resp, err := client.Do(req)
287
- if (err != nil) {
288
- return nil, err
289
- }
290
- defer resp.Body.Close()
291
-
292
- body, err := io.ReadAll(resp.Body)
293
- if (err != nil) {
294
- return nil, err
295
- }
296
-
297
- if debug {
298
- log.Printf("Cloudflare publish response: %s", string(body))
299
- }
300
-
301
- var responseData map[string]interface{}
302
- if err := json.Unmarshal(body, &responseData); err != nil {
303
- return nil, fmt.Errorf("error parsing Cloudflare response: %v. Response body: %s", err, string(body))
304
- }
305
-
306
- // Check for non-2xx status codes
307
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
308
- return responseData, fmt.Errorf("cloudflare error (status %d): %s", resp.StatusCode, string(body))
309
- }
310
-
311
- return responseData, nil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  }
313
 
314
  func unpublishToCloudflare(cfUrl string, requestBody map[string]interface{}) (map[string]interface{}, error) {
@@ -345,103 +437,78 @@ func unpublishToCloudflare(cfUrl string, requestBody map[string]interface{}) (ma
345
  }
346
 
347
  func pullFromCloudflare(sessionId string, tracksToPull []map[string]interface{}) (map[string]interface{}, error) {
348
- url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId)
349
-
350
- // Properly structure the request body as Cloudflare expects
351
- requestBody := map[string]interface{}{
352
- "tracks": tracksToPull,
353
- }
354
-
355
- jsonData, err := json.Marshal(requestBody)
356
- if err != nil {
357
- return nil, err
358
- }
359
-
360
- if debug {
361
- log.Printf("Sending request to Cloudflare: %s", string(jsonData))
362
- }
363
-
364
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
365
- if err != nil {
366
- return nil, err
367
- }
368
-
369
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cloudflareAppSecret))
370
- req.Header.Set("Content-Type", "application/json")
371
-
372
- client := &http.Client{}
373
- resp, err := client.Do(req)
374
- if err != nil {
375
- return nil, err
376
- }
377
- defer resp.Body.Close()
378
-
379
- body, err := io.ReadAll(resp.Body)
380
- if err != nil {
381
- return nil, err
382
- }
383
-
384
- if debug {
385
- log.Printf("Cloudflare response: %s", string(body))
386
- }
387
-
388
- var responseData map[string]interface{}
389
- if err := json.Unmarshal(body, &responseData); err != nil {
390
- return nil, fmt.Errorf("error parsing Cloudflare response: %v. Response body: %s", err, string(body))
391
- }
392
-
393
- return responseData, nil
394
  }
395
 
396
  func renegotiateWithCloudflare(sessionId string, body map[string]interface{}) (map[string]interface{}, error) {
397
- url := fmt.Sprintf("%s/sessions/%s/renegotiate", cloudflareBasePath, sessionId)
398
-
399
- jsonData, err := json.Marshal(body)
400
- if err != nil {
401
- return nil, err
402
- }
403
-
404
- if debug {
405
- log.Printf("Renegotiate request to Cloudflare: %s", string(jsonData))
406
- }
407
-
408
- req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
409
- if err != nil {
410
- return nil, err
411
- }
412
-
413
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cloudflareAppSecret))
414
- req.Header.Set("Content-Type", "application/json")
415
-
416
- client := &http.Client{
417
- Timeout: time.Second * 30, // Add reasonable timeout
418
- }
419
- resp, err := client.Do(req)
420
- if err != nil {
421
- return nil, err
422
- }
423
- defer resp.Body.Close()
424
-
425
- responseBody, err := io.ReadAll(resp.Body)
426
- if err != nil {
427
- return nil, err
428
- }
429
-
430
- if debug {
431
- log.Printf("Cloudflare renegotiate response: %s", string(responseBody))
432
- }
433
-
434
- var responseData map[string]interface{}
435
- if err := json.Unmarshal(responseBody, &responseData); err != nil {
436
- return nil, fmt.Errorf("error parsing Cloudflare response: %v. Response body: %s", err, string(responseBody))
437
- }
438
-
439
- // If not a 2xx status code, return the error from Cloudflare
440
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
441
- return responseData, fmt.Errorf("cloudflare error: %s", string(responseBody))
442
- }
443
-
444
- return responseData, nil
445
  }
446
 
447
  func manageDataChannelsWithCloudflare(cfUrl string, dataChannels []map[string]interface{}) (map[string]interface{}, error) {
@@ -478,9 +545,11 @@ func manageDataChannelsWithCloudflare(cfUrl string, dataChannels []map[string]in
478
 
479
  func getSessionStateFromCloudflare(sessionId string) (map[string]interface{}, error) {
480
  url := fmt.Sprintf("%s/sessions/%s", cloudflareBasePath, sessionId)
 
481
 
482
  req, err := http.NewRequest("GET", url, nil)
483
  if err != nil {
 
484
  return nil, err
485
  }
486
 
@@ -488,17 +557,22 @@ func getSessionStateFromCloudflare(sessionId string) (map[string]interface{}, er
488
  client := &http.Client{}
489
  resp, err := client.Do(req)
490
  if err != nil {
 
491
  return nil, err
492
  }
493
  defer resp.Body.Close()
494
 
495
  body, err := io.ReadAll(resp.Body)
496
  if err != nil {
 
497
  return nil, err
498
  }
499
 
 
 
500
  var responseData map[string]interface{}
501
  if err := json.Unmarshal(body, &responseData); err != nil {
 
502
  return nil, err
503
  }
504
 
@@ -517,83 +591,6 @@ func serializeRoom(roomId string, room *Room) gin.H {
517
  }
518
  }
519
 
520
- func contains(slice []string, str string) bool {
521
- for _, v := range slice {
522
- if v == str {
523
- return true
524
- }
525
- }
526
- return false
527
- }
528
-
529
- // Add these struct methods to improve participant management
530
-
531
- func (r *Room) FindParticipant(sessionId string) *Participant {
532
- r.RLock()
533
- defer r.RUnlock()
534
- for _, p := range r.Participants {
535
- if p.SessionID == sessionId {
536
- return p
537
- }
538
- }
539
- return nil
540
- }
541
-
542
- func (r *Room) AddParticipant(p *Participant) {
543
- r.Lock()
544
- defer r.Unlock()
545
- r.Participants = append(r.Participants, p)
546
- }
547
-
548
- func (r *Room) RemoveParticipant(sessionId string) *Participant {
549
- r.Lock()
550
- defer r.Unlock()
551
- for i, p := range r.Participants {
552
- if p.SessionID == sessionId {
553
- // Remove participant
554
- r.Participants = append(r.Participants[:i], r.Participants[i+1:]...)
555
- return p
556
- }
557
- }
558
- return nil
559
- }
560
-
561
- func (r *Room) GetOtherParticipants(excludeSessionId string) []SessionInfo {
562
- r.RLock()
563
- defer r.RUnlock()
564
- others := make([]SessionInfo, 0)
565
- for _, p := range r.Participants {
566
- if p.SessionID != excludeSessionId {
567
- others = append(others, SessionInfo{
568
- UserId: p.UserID,
569
- SessionId: p.SessionID,
570
- PublishedTracks: p.PublishedTracks,
571
- })
572
- }
573
- }
574
- return others
575
- }
576
-
577
- func (p *Participant) AddTrack(trackName string) bool {
578
- for _, t := range p.PublishedTracks {
579
- if t == trackName {
580
- return false // Track already exists
581
- }
582
- }
583
- p.PublishedTracks = append(p.PublishedTracks, trackName)
584
- return true
585
- }
586
-
587
- func (p *Participant) RemoveTrack(trackName string) bool {
588
- for i, t := range p.PublishedTracks {
589
- if t == trackName {
590
- p.PublishedTracks = append(p.PublishedTracks[:i], p.PublishedTracks[i+1:]...)
591
- return true
592
- }
593
- }
594
- return false
595
- }
596
-
597
  // --- Route Handlers ---
598
 
599
  func createRoom(c *gin.Context) {
@@ -656,203 +653,240 @@ func inspectRooms(c *gin.Context) {
656
  }
657
 
658
  func joinRoom(c *gin.Context) {
659
- roomId := c.Param("roomId")
660
- user, _ := c.Get("user")
661
- currentUser, ok := user.(*User)
662
- if !ok {
663
- c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid user"})
664
- return
665
- }
666
- userId := currentUser.UserID
667
-
668
- rooms.Lock()
669
- room, exists := rooms.m[roomId]
670
- if !exists {
671
- // Create new room if it doesn't exist
672
- room = &Room{
673
- RoomId: roomId,
674
- Name: "New Room",
675
- Metadata: make(map[string]interface{}),
676
- Participants: make([]*Participant, 0),
677
- CreatedAt: time.Now().Unix(),
678
- }
679
- rooms.m[roomId] = room
680
- }
681
- rooms.Unlock()
682
-
683
- // Create Cloudflare Session
684
- sessionID, err := createCloudflareSession()
685
- if err != nil {
686
- if debug {
687
- log.Printf("Error creating Cloudflare session: %v", err)
688
- }
689
- c.JSON(http.StatusInternalServerError, gin.H{
690
- "errorCode": "SESSION_CREATION_ERROR",
691
- "errorDescription": "Could not create Calls session",
692
- })
693
- return
694
- }
695
-
696
- // Create new participant
697
- participant := &Participant{
698
- UserID: userId,
699
- SessionID: sessionID,
700
- CreatedAt: time.Now().Unix(),
701
- PublishedTracks: make([]string, 0),
702
- }
703
-
704
- // Add participant to room
705
- room.AddParticipant(participant)
706
-
707
- // Get other participants
708
- otherSessions := room.GetOtherParticipants(sessionID)
709
-
710
- // Ensure WebSocket connections map exists for this room
711
- wsConnections.Lock()
712
- if _, exists := wsConnections.m[roomId]; !exists {
713
- wsConnections.m[roomId] = make(map[string]*websocket.Conn)
714
- }
715
- wsConnections.Unlock()
716
-
717
- // Broadcast join event
718
- broadcastToRoom(roomId, gin.H{
719
- "type": "participant-joined",
720
- "payload": gin.H{
721
- "userId": userId,
722
- "username": currentUser.Username,
723
- "sessionId": sessionID,
724
- },
725
- }, userId)
726
-
727
- if debug {
728
- log.Printf("User %s joined room %s with session %s", userId, roomId, sessionID)
729
- }
730
-
731
- response := SessionResponse{
732
- SessionId: sessionID,
733
- OtherSessions: otherSessions,
734
- }
735
-
736
- c.JSON(http.StatusOK, response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
737
  }
738
 
 
739
  func publishTracks(c *gin.Context) {
740
- roomId := c.Param("roomId")
741
- sessionId := c.Param("sessionId")
742
-
743
- rooms.RLock()
744
- room, ok := rooms.m[roomId]
745
- rooms.RUnlock()
746
- if (!ok) {
747
- c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
748
- return
749
- }
750
-
751
- var participant *Participant
752
- room.RLock()
753
- for _, p := range room.Participants {
754
- if p.SessionID == sessionId {
755
- participant = p
756
- break
757
- }
758
- }
759
- room.RUnlock()
760
- if (participant == nil) {
761
- c.JSON(http.StatusNotFound, gin.H{"error": "Session not found in this room"})
762
- return
763
- }
764
-
765
- var req struct {
766
- Offer map[string]interface{} `json:"offer"`
767
- Tracks []struct {
768
- TrackName string `json:"trackName"`
769
- Mid string `json:"mid"`
770
- Location string `json:"location,omitempty"`
771
- } `json:"tracks"`
772
- }
773
-
774
- if err := c.ShouldBindJSON(&req); err != nil {
775
- c.JSON(http.StatusBadRequest, gin.H{
776
- "errorCode": "INVALID_REQUEST",
777
- "errorDescription": err.Error(),
778
- })
779
- return
780
- }
781
-
782
- // Validate required fields
783
- if req.Offer == nil || len(req.Tracks) == 0 {
784
- c.JSON(http.StatusBadRequest, gin.H{
785
- "errorCode": "INVALID_REQUEST",
786
- "errorDescription": "offer and tracks are required",
787
- })
788
- return
789
- }
790
-
791
- // Build tracks array with proper structure
792
- tracks := make([]map[string]interface{}, len(req.Tracks))
793
- for i, t := range req.Tracks {
794
- track := map[string]interface{}{
795
- "trackName": t.TrackName,
796
- "mid": t.Mid,
797
- }
798
- if t.Location != "" {
799
- track["location"] = t.Location
800
- } else {
801
- track["location"] = "local" // Default to local if not specified
802
- }
803
- tracks[i] = track
804
- }
805
-
806
- if debug {
807
- log.Printf("Publishing tracks with data: %+v", tracks)
808
- }
809
-
810
- data, err := publishToCloudflare(sessionId, req.Offer, tracks)
811
- if err != nil {
812
- if debug {
813
- log.Printf("Error from Cloudflare: %v", err)
814
- }
815
- c.JSON(http.StatusInternalServerError, gin.H{
816
- "errorCode": "PUBLISH_ERROR",
817
- "errorDescription": err.Error(),
818
- })
819
- return
820
- }
821
-
822
- // If there's a Cloudflare error, return it
823
- if errorCode, hasError := data["errorCode"]; hasError {
824
- c.JSON(http.StatusBadRequest, gin.H{
825
- "errorCode": errorCode,
826
- "errorDescription": data["errorDescription"],
827
- })
828
- return
829
- }
830
-
831
- // Update participant's published tracks
832
- room.Lock()
833
- for _, t := range req.Tracks {
834
- if !contains(participant.PublishedTracks, t.TrackName) {
835
- participant.PublishedTracks = append(participant.PublishedTracks, t.TrackName)
836
- }
837
- }
838
- room.Unlock()
839
-
840
- // Emit track-published event if successful
841
- if data["sessionDescription"] != nil {
842
- trackNames := make([]string, len(req.Tracks))
843
- for i, t := range req.Tracks {
844
- trackNames[i] = t.TrackName
845
- }
846
- broadcastToRoom(roomId, gin.H{
847
- "type": "track-published",
848
- "payload": gin.H{
849
- "sessionId": sessionId,
850
- "trackNames": trackNames,
851
- },
852
- }, participant.UserID)
853
- }
854
-
855
- c.JSON(http.StatusOK, data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
  }
857
 
858
  func unpublishTrack(c *gin.Context) {
@@ -866,19 +900,19 @@ func unpublishTrack(c *gin.Context) {
866
  }
867
 
868
  var req struct {
869
- TrackName string `json:"trackName"`
870
- Mid string `json:"mid"`
871
- Force bool `json:"force"`
872
- SessionDescription map[string]interface{} `json:"sessionDescription"`
873
  }
874
 
875
  if err := c.ShouldBindJSON(&req); err != nil {
876
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
877
  return
878
  }
879
- // If trying to force unpublish someone else's track
880
  if req.Force && sessionId != currentUser.UserID {
881
- // Check if user is moderator
882
  if !currentUser.IsModerator {
883
  c.JSON(http.StatusForbidden, gin.H{
884
  "errorCode": "NOT_AUTHORIZED",
@@ -911,16 +945,16 @@ func unpublishTrack(c *gin.Context) {
911
  if debug {
912
  log.Println("Calling Cloudflare API:", cfUrl)
913
  }
914
- // The 'tracks' array MUST contain objects with a 'mid' field
915
  requestBody := map[string]interface{}{
916
- "tracks": []map[string]interface{}{
917
  {"mid": req.Mid},
918
  },
919
  "force": req.Force,
920
  "sessionDescription": req.SessionDescription,
921
  }
922
  if debug {
923
- log.Printf("Request body: %+v\n", requestBody)
924
  }
925
 
926
  data, err := unpublishToCloudflare(cfUrl, requestBody)
@@ -931,134 +965,133 @@ func unpublishTrack(c *gin.Context) {
931
  if debug {
932
  log.Println("Cloudflare API response:", data)
933
  }
934
-
935
- // Check for Cloudflare errors before broadcasting
936
- if cfErr, ok := data["errorCode"]; ok {
937
- c.JSON(http.StatusBadRequest, gin.H{"error": cfErr, "message": data["errorDescription"]})
938
- return
939
- }
940
-
941
  broadcastToRoom(roomId, gin.H{
942
  "type": "track-unpublished",
943
  "payload": gin.H{"sessionId": sessionId, "trackName": req.TrackName},
944
  }, sessionId)
945
 
946
  c.JSON(http.StatusOK, data)
 
947
  }
948
 
949
  func pullTracks(c *gin.Context) {
950
- roomId := c.Param("roomId")
951
- sessionId := c.Param("sessionId")
952
-
953
- rooms.RLock()
954
- room, ok := rooms.m[roomId]
955
- rooms.RUnlock()
956
-
957
- if (!ok) {
958
- c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
959
- return
960
- }
961
-
962
- var participant *Participant
963
- room.RLock()
964
- for _, p := range room.Participants {
965
- if p.SessionID == sessionId {
966
- participant = p
967
- break
968
- }
969
- }
970
- room.RUnlock()
971
-
972
- if participant == nil {
973
- c.JSON(http.StatusNotFound, gin.H{"error": "Session not found in this room"})
974
- return
975
- }
976
-
977
- var req struct {
978
- RemoteSessionID string `json:"remoteSessionId"`
979
- TrackName string `json:"trackName"`
980
- }
981
-
982
- if err := c.ShouldBindJSON(&req); err != nil {
983
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
984
- return
985
- }
986
-
987
- // Validate required fields
988
- if req.RemoteSessionID == "" || req.TrackName == "" {
989
- c.JSON(http.StatusBadRequest, gin.H{
990
- "errorCode": "INVALID_REQUEST",
991
- "errorDescription": "remoteSessionId and trackName are required",
992
- })
993
- return
994
- }
995
-
996
- // Build tracks array exactly as Cloudflare expects it
997
- tracksToPull := []map[string]interface{}{
998
- {
999
- "location": "remote",
1000
- "sessionId": req.RemoteSessionID,
1001
- "trackName": req.TrackName,
1002
- },
1003
- }
1004
-
1005
- if debug {
1006
- log.Printf("Pulling track with data: %+v", tracksToPull)
1007
- }
1008
-
1009
- data, err := pullFromCloudflare(sessionId, tracksToPull)
1010
- if err != nil {
1011
- if debug {
1012
- log.Printf("Error from Cloudflare: %v", err)
1013
- }
1014
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1015
- return
1016
- }
1017
-
1018
- // Check for Cloudflare errors
1019
- if errorCode, hasError := data["errorCode"]; hasError {
1020
- c.JSON(http.StatusBadRequest, gin.H{
1021
- "errorCode": errorCode,
1022
- "errorDescription": data["errorDescription"],
1023
- })
1024
- return
1025
- }
1026
-
1027
- c.JSON(http.StatusOK, data)
1028
  }
1029
 
1030
  func renegotiateSession(c *gin.Context) {
1031
- sessionId := c.Param("sessionId")
1032
-
1033
- // Fixed: Accept raw JSON structure to match Express implementation
1034
- var req struct {
1035
- SDP string `json:"sdp"`
1036
- Type string `json:"type"`
1037
- }
1038
-
1039
- if err := c.ShouldBindJSON(&req); err != nil {
1040
- c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1041
- return
1042
- }
1043
-
1044
- body := map[string]interface{}{
1045
- "sessionDescription": map[string]interface{}{
1046
- "sdp": req.SDP,
1047
- "type": req.Type,
1048
- },
1049
- }
1050
-
1051
- data, err := renegotiateWithCloudflare(sessionId, body)
1052
- if err != nil {
1053
- if data != nil && data["errorCode"] != nil {
1054
- c.JSON(http.StatusBadRequest, data)
1055
- return
1056
- }
1057
- c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1058
- return
1059
- }
1060
-
1061
- c.JSON(http.StatusOK, data)
 
 
 
 
 
 
 
 
 
 
 
 
1062
  }
1063
 
1064
  func manageDataChannels(c *gin.Context) {
@@ -1067,9 +1100,9 @@ func manageDataChannels(c *gin.Context) {
1067
 
1068
  rooms.RLock()
1069
  room, ok := rooms.m[roomId]
1070
- log.Println(room)
1071
  rooms.RUnlock()
1072
- if (!ok) {
1073
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1074
  return
1075
  }
@@ -1086,16 +1119,15 @@ func manageDataChannels(c *gin.Context) {
1086
  }
1087
 
1088
  cfUrl := fmt.Sprintf("%s/sessions/%s/datachannels/new", cloudflareBasePath, sessionId)
1089
- // --- CRITICAL: Build the dataChannels array correctly ---
1090
  dataChannels := make([]map[string]interface{}, len(req.DataChannels))
1091
  for i, dc := range req.DataChannels {
1092
  dataChannels[i] = map[string]interface{}{
1093
  "location": dc.Location,
1094
- "dataChannelName": dc.DataChannelName, // Use the correct field name
1095
  }
1096
  }
1097
 
1098
- data, err := manageDataChannelsWithCloudflare(cfUrl, dataChannels) // Use the helper
1099
  if err != nil {
1100
  if data != nil && data["errorCode"] != nil { // Check for Cloudflare error
1101
  c.JSON(http.StatusBadRequest, data)
@@ -1120,7 +1152,7 @@ func getParticipants(c *gin.Context) {
1120
  room, ok := rooms.m[roomId]
1121
  rooms.RUnlock()
1122
 
1123
- if (!ok) {
1124
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1125
  return
1126
  }
@@ -1135,7 +1167,7 @@ func getParticipantTracks(c *gin.Context) {
1135
  rooms.RLock()
1136
  room, ok := rooms.m[roomId]
1137
  rooms.RUnlock()
1138
- if (!ok) {
1139
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1140
  return
1141
  }
@@ -1149,7 +1181,7 @@ func getParticipantTracks(c *gin.Context) {
1149
  }
1150
  room.RUnlock()
1151
 
1152
- if (participant == nil) {
1153
  c.JSON(http.StatusNotFound, gin.H{"error": "Participant not found"})
1154
  return
1155
  }
@@ -1202,7 +1234,6 @@ func getICEServers(c *gin.Context) {
1202
 
1203
  c.JSON(http.StatusOK, iceServers)
1204
  }
1205
-
1206
  func getToken(c *gin.Context) {
1207
  var req struct {
1208
  Username string `json:"username"`
@@ -1270,7 +1301,7 @@ func getUserInfo(c *gin.Context) {
1270
  users.RLock()
1271
  userInfo, ok := users.m[currentUser.UserID]
1272
  users.RUnlock()
1273
- if (!ok) {
1274
  c.JSON(http.StatusNotFound, gin.H{
1275
  "errorCode": "USER_NOT_FOUND",
1276
  "errorDescription": "Current user not found",
@@ -1284,7 +1315,7 @@ func getUserInfo(c *gin.Context) {
1284
  users.RLock()
1285
  requestedUser, ok := users.m[userIdParam]
1286
  users.RUnlock()
1287
- if (!ok) {
1288
  c.JSON(http.StatusNotFound, gin.H{
1289
  "errorCode": "USER_NOT_FOUND",
1290
  "errorDescription": "User not found",
@@ -1312,7 +1343,7 @@ func leaveRoom(c *gin.Context) {
1312
  rooms.RLock()
1313
  room, ok := rooms.m[roomId]
1314
  rooms.RUnlock()
1315
- if (!ok) {
1316
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1317
  return
1318
  }
@@ -1422,7 +1453,7 @@ func updateRoomMetadata(c *gin.Context) {
1422
  room, ok := rooms.m[roomId]
1423
  rooms.RUnlock()
1424
 
1425
- if (!ok) {
1426
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1427
  return
1428
  }
@@ -1466,132 +1497,76 @@ var upgrader = websocket.Upgrader{
1466
  }
1467
 
1468
  func websocketHandler(c *gin.Context) {
1469
- // Set headers for WebSocket upgrade
1470
- c.Writer.Header().Set("Sec-WebSocket-Protocol", "")
1471
- c.Writer.Header().Set("Connection", "Upgrade")
1472
- c.Writer.Header().Set("Upgrade", "websocket")
1473
-
1474
- if debug {
1475
- log.Printf("Attempting WebSocket upgrade for: %s\n", c.Request.RemoteAddr)
1476
- log.Printf("Headers: %+v\n", c.Request.Header)
1477
- }
1478
-
1479
- ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
1480
- if err != nil {
1481
- log.Printf("Failed to upgrade connection: %v\n", err)
1482
- return
1483
- }
1484
-
1485
- // Set connection parameters
1486
- ws.SetReadLimit(512 * 1024) // 512KB read limit
1487
- ws.SetReadDeadline(time.Time{}) // No read deadline for now
1488
- ws.SetWriteDeadline(time.Time{}) // No write deadline for now
1489
-
1490
- // Handle the WebSocket connection
1491
- go handleWebSocket(ws)
1492
  }
1493
 
1494
  func handleWebSocket(ws *websocket.Conn) {
1495
- if debug {
1496
- log.Printf("New WebSocket connection established from: %s\n", ws.RemoteAddr().String())
1497
- }
1498
-
1499
- // Set ping handler
1500
- ws.SetPingHandler(func(data string) error {
1501
- return ws.WriteControl(websocket.PongMessage, []byte(data), time.Now().Add(10*time.Second))
1502
- })
1503
-
1504
- // Create a done channel to signal when to stop the ping routine
1505
- done := make(chan struct{})
1506
- defer close(done)
1507
-
1508
- // Start a goroutine to send pings periodically
1509
- go func() {
1510
- ticker := time.NewTicker(30 * time.Second)
1511
- defer ticker.Stop()
1512
- for {
1513
- select {
1514
- case <-ticker.C:
1515
- if err := ws.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(10*time.Second)); err != nil {
1516
- log.Printf("ping error: %v", err)
1517
- return
1518
- }
1519
- case <-done:
1520
- return
1521
- }
1522
- }
1523
- }()
1524
-
1525
- var userId string
1526
- var roomId string
1527
-
1528
- for {
1529
- messageType, message, err := ws.ReadMessage()
1530
- if err != nil {
1531
- if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
1532
- log.Printf("WebSocket error: %v", err)
1533
- }
1534
- handleWSDisconnect(ws, roomId, userId)
1535
- break
1536
- }
1537
-
1538
- // Skip processing ping/pong messages
1539
- if messageType == websocket.PingMessage || messageType == websocket.PongMessage {
1540
- continue
1541
- }
1542
-
1543
- var data map[string]interface{}
1544
- if err := json.Unmarshal(message, &data); err != nil {
1545
- log.Println("Error parsing message:", err)
1546
- continue
1547
- }
1548
-
1549
- messageTypeStr, ok := data["type"].(string)
1550
- if !ok {
1551
- log.Println("Invalid message format: missing or invalid 'type'")
1552
- continue
1553
- }
1554
-
1555
- switch messageTypeStr {
1556
- case "join-websocket":
1557
- payload, ok := data["payload"].(map[string]interface{})
1558
- if !ok {
1559
- log.Println("Invalid 'join-websocket' payload")
1560
- continue
1561
- }
1562
-
1563
- // Store these values for disconnect handling
1564
- roomId, _ = payload["roomId"].(string)
1565
- userId, _ = payload["userId"].(string)
1566
-
1567
- handleWSJoin(ws, payload)
1568
-
1569
- case "data-message":
1570
- if authRequired && (!ws.IsAuthenticated()) {
1571
- _ = ws.WriteJSON(map[string]string{"error": "Unauthorized: Please authenticate first"})
1572
- if debug {
1573
- log.Println("Unauthenticated websocket request to send data-message")
1574
- }
1575
- continue
1576
- }
1577
-
1578
- payload, ok := data["payload"].(map[string]interface{})
1579
- if !ok {
1580
- log.Println("Invalid 'data-message' payload")
1581
- continue
1582
- }
1583
- handleDataMessage(ws, payload)
1584
-
1585
- default:
1586
- log.Println("Unknown message type:", messageTypeStr)
1587
- }
1588
- }
1589
- }
1590
 
1591
- // Add IsAuthenticated method to *websocket.Conn
1592
- func (ws *websocket.Conn) IsAuthenticated() bool {
1593
- // You can store this as a property on connection establishment
1594
- return true // For now, always return true since auth is disabled
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1595
  }
1596
 
1597
  // --- WebSocket Helper Functions ---
@@ -1700,196 +1675,126 @@ func handleDataMessage(ws *websocket.Conn, payload map[string]interface{}) {
1700
  }, from) // Exclude the sender
1701
  }
1702
 
1703
- func handleWSDisconnect(ws *websocket.Conn, roomId, userId string) {
1704
  wsConnections.Lock()
1705
  defer wsConnections.Unlock()
1706
-
1707
- // Remove user from the specific room
1708
- if roomConns, ok := wsConnections.m[roomId]; ok {
1709
- delete(roomConns, userId)
1710
  if debug {
1711
  log.Printf("User %s disconnected from room %s\n", userId, roomId)
1712
  }
1713
-
1714
- // Optionally, clean up the room map if it's now empty
1715
- if len(roomConns) == 0 {
1716
- delete(wsConnections.m, roomId)
1717
- }
1718
  }
1719
  }
1720
 
1721
  func broadcastToRoom(roomId string, message gin.H, excludeUserId string) {
1722
- if debug {
1723
- log.Printf("Broadcasting to room %s: %+v (excluding: %s)", roomId, message, excludeUserId)
1724
- }
1725
-
1726
- // Check room exists
1727
- rooms.RLock()
1728
- _, ok := rooms.m[roomId]
1729
- rooms.RUnlock()
1730
- if !ok {
1731
- if debug {
1732
- log.Printf("Room %s not found for broadcast", roomId)
1733
- }
1734
- return
1735
- }
1736
-
1737
- // Safely get connections
1738
- wsConnections.RLock()
1739
- connections, ok := wsConnections.m[roomId]
1740
- if !ok {
1741
- wsConnections.RUnlock()
1742
- if debug {
1743
- log.Printf("No WebSocket connections found for room %s", roomId)
1744
- }
1745
- return
1746
- }
1747
-
1748
- // Create a copy of connections to avoid holding the lock
1749
- connectionsCopy := make(map[string]*websocket.Conn)
1750
- for userId, conn := range connections {
1751
- if userId != excludeUserId {
1752
- connectionsCopy[userId] = conn
1753
- }
1754
- }
1755
- wsConnections.RUnlock()
1756
-
1757
- // Prepare message once
1758
- messageBytes, err := json.Marshal(message)
1759
- if err != nil {
1760
- log.Printf("Error marshaling broadcast message: %v", err)
1761
- return
1762
- }
1763
-
1764
- // Broadcast to all connections
1765
- for userId, conn := range connectionsCopy {
1766
- if conn == nil {
1767
- continue
1768
- }
1769
-
1770
- // Use a write deadline to prevent hanging
1771
- _ = conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
1772
- err := conn.WriteMessage(websocket.TextMessage, messageBytes)
1773
- if err != nil {
1774
- log.Printf("Error broadcasting to user %s: %v", userId, err)
1775
-
1776
- // Clean up dead connection
1777
- wsConnections.Lock()
1778
- if roomConns, exists := wsConnections.m[roomId]; exists {
1779
- delete(roomConns, userId)
1780
- // If room is empty, remove it
1781
- if len(roomConns) == 0 {
1782
- delete(wsConnections.m, roomId)
1783
- }
1784
- }
1785
- wsConnections.Unlock()
1786
- } else if debug {
1787
- log.Printf("Successfully broadcast to user %s in room %s", userId, roomId)
1788
- }
1789
- }
1790
  }
1791
 
1792
  func main() {
1793
- initConfig()
1794
-
1795
- r := gin.Default()
1796
-
1797
- // Comprehensive CORS middleware
1798
- r.Use(func(c *gin.Context) {
1799
- c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
1800
- c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
1801
- c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
1802
- c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-Auth-Token, Upgrade, Connection")
1803
-
1804
- // Handle WebSocket pre-flight
1805
- if c.Request.Method == "OPTIONS" {
1806
- // Special handling for WebSocket upgrade requests
1807
- if c.Request.Header.Get("Upgrade") == "websocket" {
1808
- c.Writer.Header().Set("Connection", "Upgrade")
1809
- c.Writer.Header().Set("Upgrade", "websocket")
1810
- }
1811
- c.AbortWithStatus(204)
1812
- return
1813
- }
1814
-
1815
- c.Next()
1816
- })
1817
-
1818
- // Initialize WebSocket upgrader with proper settings
1819
- upgrader = websocket.Upgrader{
1820
- CheckOrigin: func(r *http.Request) bool {
1821
- return true // Be careful with this in production
1822
- },
1823
- ReadBufferSize: 1024,
1824
- WriteBufferSize: 1024,
1825
- HandshakeTimeout: 10 * time.Second,
1826
- EnableCompression: true,
1827
- }
1828
-
1829
- // Auth endpoints
1830
- r.POST("/auth/token", getToken)
1831
-
1832
- // WebSocket endpoint - no auth required for upgrade
1833
- r.GET("/ws", func(c *gin.Context) {
1834
- websocketHandler(c)
1835
- })
1836
-
1837
- // Protected API routes
1838
- api := r.Group("/api", verifyToken)
1839
- {
1840
- api.POST("/rooms", createRoom)
1841
- api.GET("/rooms", getRooms)
1842
- api.GET("/inspect-rooms", inspectRooms)
1843
- api.POST("/rooms/:roomId/join", joinRoom)
1844
- api.POST("/rooms/:roomId/sessions/:sessionId/publish", publishTracks)
1845
- api.POST("/rooms/:roomId/sessions/:sessionId/unpublish", unpublishTrack)
1846
- api.POST("/rooms/:roomId/sessions/:sessionId/pull", pullTracks)
1847
- api.PUT("/rooms/:roomId/sessions/:sessionId/renegotiate", renegotiateSession)
1848
- api.POST("/rooms/:roomId/sessions/:sessionId/datachannels/new", manageDataChannels)
1849
- api.GET("/rooms/:roomId/participants", getParticipants)
1850
- api.GET("/rooms/:roomId/participant/:sessionId/tracks", getParticipantTracks)
1851
- api.GET("/ice-servers", getICEServers)
1852
- api.GET("/rooms/:roomId/sessions/:sessionId/state", getSessionState)
1853
- api.GET("/users/:userId", getUserInfo)
1854
- api.POST("/rooms/:roomId/leave", leaveRoom)
1855
- api.POST("/rooms/:roomId/sessions/:sessionId/track-status", updateTrackStatus)
1856
- api.PUT("/rooms/:roomId/metadata", updateRoomMetadata)
1857
- }
1858
-
1859
- // Set up signal handling for graceful shutdown
1860
- quit := make(chan os.Signal, 1)
1861
- signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
1862
- go func() {
1863
- <-quit
1864
- log.Println("Shutting down server...")
1865
-
1866
- // Clean up code here
1867
- users.Lock()
1868
- users.m = make(map[string]*User)
1869
- users.Unlock()
1870
-
1871
- rooms.Lock()
1872
- rooms.m = make(map[string]*Room)
1873
- rooms.Unlock()
1874
-
1875
- wsConnections.Lock()
1876
- for roomId, conns := range wsConnections.m {
1877
- for userId, conn := range conns {
1878
- if conn != nil {
1879
- conn.Close()
1880
- }
1881
- delete(conns, userId)
1882
- }
1883
- delete(wsConnections.m, roomId)
1884
- }
1885
- wsConnections.Unlock()
1886
-
1887
- os.Exit(0)
1888
- }()
1889
-
1890
- // Start the server
1891
- log.Printf("Server listening on http://localhost:%s\n", port)
1892
- if err := r.Run(":" + port); err != nil {
1893
- log.Fatal("Failed to start server:", err)
1894
- }
1895
  }
 
11
  "log"
12
  "net/http"
13
  "os"
 
14
  "strconv"
15
  "sync"
 
16
  "time"
17
 
18
  "github.com/gin-gonic/gin"
 
55
  if cloudflareAppID == "" || cloudflareAppSecret == "" {
56
  log.Fatal("CLOUDFLARE_APP_ID and CLOUDFLARE_APP_SECRET must be set")
57
  }
58
+
59
+ // Thêm log để kiểm tra biến môi trường
60
+ log.Println("Biến môi trường đã tải:")
61
+ log.Println("CLOUDFLARE_APP_ID:", cloudflareAppID)
62
+ log.Println("CLOUDFLARE_APP_SECRET:", cloudflareAppSecret)
63
+ log.Println("JWT_SECRET:", secretKey)
64
+ log.Println("CLOUDFLARE_APPS_URL:", cloudflareCallsBaseURL)
65
+ log.Println("DEBUG:", debug)
66
  }
67
 
68
  // Helper function to get environment variables with default values
 
148
 
149
  // --- JWT Verification Middleware ---
150
  func verifyToken(c *gin.Context) {
151
+ if !authRequired {
152
  // Even when auth is disabled, set a default user
153
  defaultUser := &User{
154
  UserID: uuid.NewString(),
 
167
  }
168
 
169
  authHeader := c.GetHeader("Authorization")
170
+ if authHeader == "*" {
171
  defaultUser := &User{
172
  UserID: uuid.NewString(),
173
  Username: "Anonymous",
 
230
 
231
  func createCloudflareSession() (string, error) {
232
  url := fmt.Sprintf("%s/sessions/new", cloudflareBasePath)
233
+ log.Printf("[Cloudflare API] Creating new session: %s", url)
234
+
235
  req, err := http.NewRequest("POST", url, nil)
236
  if err != nil {
237
+ log.Printf("[Cloudflare API Error] Failed to create request: %v", err)
238
  return "", err
239
  }
240
 
 
242
  client := &http.Client{}
243
  resp, err := client.Do(req)
244
  if err != nil {
245
+ log.Printf("[Cloudflare API Error] Failed to execute request: %v", err)
246
  return "", err
247
  }
248
  defer resp.Body.Close()
249
 
250
  body, err := io.ReadAll(resp.Body)
251
  if err != nil {
252
+ log.Printf("[Cloudflare API Error] Failed to read response body: %v", err)
253
  return "", err
254
  }
255
 
256
+ log.Printf("[Cloudflare API Response] Status: %d, Body: %s", resp.StatusCode, string(body))
257
+
258
  var responseData map[string]interface{}
259
  if err := json.Unmarshal(body, &responseData); err != nil {
260
+ log.Printf("[Cloudflare API Error] Failed to parse response: %v", err)
261
  return "", err
262
  }
263
 
264
  sessionID, ok := responseData["sessionId"].(string)
265
  if !ok {
266
+ log.Printf("[Cloudflare API Error] Session ID not found in response")
267
  return "", fmt.Errorf("sessionId not found in response: %s", string(body))
268
  }
269
+
270
+ log.Printf("[Cloudflare API Success] Created session: %s", sessionID)
271
  return sessionID, nil
272
  }
273
 
274
+ func makeCloudflareRequest(method string, url string, requestBody map[string]interface{}) (map[string]interface{}, error, int) {
275
+ var reqBodyBytes []byte = nil
276
+ if requestBody != nil {
277
+ var errMarshal error
278
+ reqBodyBytes, errMarshal = json.Marshal(requestBody)
279
+ if errMarshal != nil {
280
+ log.Printf("Lỗi Marshal body yêu cầu JSON: %v", errMarshal)
281
+ return nil, errMarshal, http.StatusInternalServerError
282
+ }
283
+ }
284
+
285
+ req, errNewRequest := http.NewRequest(method, url, bytes.NewBuffer(reqBodyBytes))
286
+ if errNewRequest != nil {
287
+ log.Printf("Lỗi tạo yêu cầu HTTP mới: %v", errNewRequest)
288
+ return nil, errNewRequest, http.StatusInternalServerError
289
+ }
290
+
291
+ // Thiết lập các header quan trọng và phổ biến
292
+ req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret) // Đảm bảo cloudflareAppSecret có giá trị
293
+ if requestBody != nil {
294
+ req.Header.Set("Content-Type", "application/json") // Cho các yêu cầu có body JSON
295
+ }
296
+ req.Header.Set("Accept", "application/json") // Yêu cầu phản hồi JSON
297
+ req.Header.Set("User-Agent", "CloudflareCalls-Backend-Go") // Thêm User-Agent để nhận diện backend (tùy chọn)
298
+ // Bạn có thể thêm các header khác nếu cần, ví dụ:
299
+ // req.Header.Set("Cache-Control", "no-cache")
300
+ // req.Header.Set("Connection", "keep-alive")
301
+
302
+ log.Println("Yêu cầu HTTP:")
303
+ log.Println(" Method:", method)
304
+ log.Println(" URL:", url)
305
+ log.Println(" Headers:", req.Header)
306
+ if requestBody != nil {
307
+ log.Println(" Body yêu cầu:", string(reqBodyBytes))
308
+ }
309
+
310
+ client := &http.Client{}
311
+ resp, errClientDo := client.Do(req)
312
+ if errClientDo != nil {
313
+ log.Printf("Lỗi khi thực hiện yêu cầu HTTP: %v", errClientDo)
314
+ return nil, errClientDo, http.StatusInternalServerError
315
+ }
316
+ defer resp.Body.Close()
317
+
318
+ respBodyBytes, errReadAll := io.ReadAll(resp.Body)
319
+ if errReadAll != nil {
320
+ log.Printf("Lỗi đọc body phản hồi: %v", errReadAll)
321
+ return nil, errReadAll, http.StatusInternalServerError
322
+ }
323
+
324
+ log.Println("Phản hồi HTTP:")
325
+ log.Println(" trạng thái:", resp.StatusCode)
326
+ log.Println(" Body phản hồi:", string(respBodyBytes))
327
+
328
+ var responseData map[string]interface{}
329
+ errUnmarshal := json.Unmarshal(respBodyBytes, &responseData)
330
+ if errUnmarshal != nil {
331
+ log.Printf("Lỗi Unmarshal body phản hồi JSON: %v", errUnmarshal)
332
+ return nil, errUnmarshal, http.StatusInternalServerError
333
+ }
334
+
335
+ return responseData, nil, resp.StatusCode
336
+ }
337
+
338
+ func publishToCloudflare(sessionId string, offer map[string]interface{}, tracks []struct {
339
+ TrackName string `json:"trackName"`
340
+ Mid string `json:"mid"`
341
+ Location string `json:"location"`
342
+ }) (map[string]interface{}, error) {
343
+ url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId)
344
+ log.Printf("[Cloudflare API] Publishing tracks to session %s", sessionId)
345
+
346
+ trackData := make([]map[string]interface{}, len(tracks))
347
+ for i, t := range tracks {
348
+ location := t.Location
349
+ if location == "" {
350
+ location = "local"
351
+ }
352
+ trackData[i] = map[string]interface{}{
353
+ "trackName": t.TrackName,
354
+ "mid": t.Mid,
355
+ "location": location,
356
+ }
357
+ }
358
+
359
+ requestBody := map[string]interface{}{
360
+ "sessionDescription": offer,
361
+ "tracks": trackData,
362
+ }
363
+
364
+ jsonData, err := json.Marshal(requestBody)
365
+ if err != nil {
366
+ log.Printf("[Cloudflare API Error] Failed to marshal request body: %v", err)
367
+ return nil, err
368
+ }
369
+
370
+ log.Printf("[Cloudflare API Request] URL: %s, Body: %s", url, string(jsonData))
371
+
372
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
373
+ if err != nil {
374
+ log.Printf("[Cloudflare API Error] Failed to create request: %v", err)
375
+ return nil, err
376
+ }
377
+
378
+ req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret)
379
+ req.Header.Set("Content-Type", "application/json")
380
+
381
+ client := &http.Client{}
382
+ resp, err := client.Do(req)
383
+ if err != nil {
384
+ log.Printf("[Cloudflare API Error] Failed to execute request: %v", err)
385
+ return nil, err
386
+ }
387
+ defer resp.Body.Close()
388
+
389
+ body, err := io.ReadAll(resp.Body)
390
+ if err != nil {
391
+ log.Printf("[Cloudflare API Error] Failed to read response body: %v", err)
392
+ return nil, err
393
+ }
394
+
395
+ log.Printf("[Cloudflare API Response] Status: %d, Body: %s", resp.StatusCode, string(body))
396
+
397
+ var responseData map[string]interface{}
398
+ if err := json.Unmarshal(body, &responseData); err != nil {
399
+ log.Printf("[Cloudflare API Error] Failed to parse response: %v", err)
400
+ return nil, err
401
+ }
402
+
403
+ return responseData, nil
404
  }
405
 
406
  func unpublishToCloudflare(cfUrl string, requestBody map[string]interface{}) (map[string]interface{}, error) {
 
437
  }
438
 
439
  func pullFromCloudflare(sessionId string, tracksToPull []map[string]interface{}) (map[string]interface{}, error) {
440
+ url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId)
441
+
442
+ requestBody := map[string]interface{}{
443
+ "tracks": tracksToPull,
444
+ }
445
+
446
+ jsonData, err := json.Marshal(requestBody)
447
+ if err != nil {
448
+ return nil, err
449
+ }
450
+
451
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
452
+ if err != nil {
453
+ return nil, err
454
+ }
455
+
456
+ req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret)
457
+ req.Header.Set("Content-Type", "application/json")
458
+
459
+ client := &http.Client{}
460
+ resp, err := client.Do(req)
461
+ if err != nil {
462
+ return nil, err
463
+ }
464
+ defer resp.Body.Close()
465
+
466
+ body, err := io.ReadAll(resp.Body)
467
+ if err != nil {
468
+ return nil, err
469
+ }
470
+
471
+ var responseData map[string]interface{}
472
+ if err := json.Unmarshal(body, &responseData); err != nil {
473
+ return nil, err
474
+ }
475
+
476
+ return responseData, nil
 
 
 
 
 
 
 
 
 
477
  }
478
 
479
  func renegotiateWithCloudflare(sessionId string, body map[string]interface{}) (map[string]interface{}, error) {
480
+ url := fmt.Sprintf("%s/sessions/%s/renegotiate", cloudflareBasePath, sessionId)
481
+
482
+ jsonData, err := json.Marshal(body)
483
+ if err != nil {
484
+ return nil, err
485
+ }
486
+
487
+ req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
488
+ if err != nil {
489
+ return nil, err
490
+ }
491
+
492
+ req.Header.Set("Authorization", "Bearer "+cloudflareAppSecret)
493
+ req.Header.Set("Content-Type", "application/json")
494
+
495
+ client := &http.Client{}
496
+ resp, err := client.Do(req)
497
+ if err != nil {
498
+ return nil, err
499
+ }
500
+ defer resp.Body.Close()
501
+
502
+ responseBody, err := io.ReadAll(resp.Body)
503
+ if err != nil {
504
+ return nil, err
505
+ }
506
+
507
+ var responseData map[string]interface{}
508
+ if err := json.Unmarshal(responseBody, &responseData); err != nil {
509
+ return nil, err
510
+ }
511
+ return responseData, nil
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  }
513
 
514
  func manageDataChannelsWithCloudflare(cfUrl string, dataChannels []map[string]interface{}) (map[string]interface{}, error) {
 
545
 
546
  func getSessionStateFromCloudflare(sessionId string) (map[string]interface{}, error) {
547
  url := fmt.Sprintf("%s/sessions/%s", cloudflareBasePath, sessionId)
548
+ log.Printf("[Cloudflare API] Getting session state: %s", url)
549
 
550
  req, err := http.NewRequest("GET", url, nil)
551
  if err != nil {
552
+ log.Printf("[Cloudflare API Error] Failed to create request: %v", err)
553
  return nil, err
554
  }
555
 
 
557
  client := &http.Client{}
558
  resp, err := client.Do(req)
559
  if err != nil {
560
+ log.Printf("[Cloudflare API Error] Failed to execute request: %v", err)
561
  return nil, err
562
  }
563
  defer resp.Body.Close()
564
 
565
  body, err := io.ReadAll(resp.Body)
566
  if err != nil {
567
+ log.Printf("[Cloudflare API Error] Failed to read response body: %v", err)
568
  return nil, err
569
  }
570
 
571
+ log.Printf("[Cloudflare API Response] Status: %d, Body: %s", resp.StatusCode, string(body))
572
+
573
  var responseData map[string]interface{}
574
  if err := json.Unmarshal(body, &responseData); err != nil {
575
+ log.Printf("[Cloudflare API Error] Failed to parse response: %v", err)
576
  return nil, err
577
  }
578
 
 
591
  }
592
  }
593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  // --- Route Handlers ---
595
 
596
  func createRoom(c *gin.Context) {
 
653
  }
654
 
655
  func joinRoom(c *gin.Context) {
656
+ roomId := c.Param("roomId")
657
+ user, _ := c.Get("user")
658
+ currentUser, ok := user.(*User)
659
+ if !ok {
660
+ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Forbidden: Invalid user"})
661
+ return
662
+ }
663
+ userId := currentUser.UserID
664
+
665
+ rooms.Lock() // Lock for writing
666
+ room, exists := rooms.m[roomId]
667
+ if !exists {
668
+ // Create new room if it doesn't exist
669
+ room = &Room{
670
+ Name: "New Room",
671
+ Metadata: make(map[string]interface{}),
672
+ Participants: make([]*Participant, 0),
673
+ CreatedAt: time.Now().Unix(),
674
+ }
675
+ rooms.m[roomId] = room
676
+ }
677
+ rooms.Unlock()
678
+
679
+ // Create Calls Session
680
+ sessionID, err := createCloudflareSession()
681
+ if err != nil {
682
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not create Calls session"})
683
+ return
684
+ }
685
+
686
+ participant := &Participant{
687
+ UserID: userId,
688
+ SessionID: sessionID,
689
+ CreatedAt: time.Now().Unix(),
690
+ PublishedTracks: make([]string, 0),
691
+ }
692
+
693
+ room.Lock()
694
+ room.Participants = append(room.Participants, participant)
695
+
696
+ // Create otherSessions list safely with complete track information
697
+ otherSessions := make([]SessionInfo, 0)
698
+ for _, p := range room.Participants {
699
+ if p.UserID != userId {
700
+ // Lấy state của session từ Cloudflare để có thông tin track đầy đủ
701
+ state, err := getSessionStateFromCloudflare(p.SessionID)
702
+ if err != nil {
703
+ log.Printf("Error getting session state for %s: %v", p.SessionID, err)
704
+ continue
705
+ }
706
+
707
+ // Extract tracks info from state
708
+ tracks := make([]string, 0)
709
+ if tracksArray, ok := state["tracks"].([]interface{}); ok {
710
+ for _, track := range tracksArray {
711
+ if trackMap, ok := track.(map[string]interface{}); ok {
712
+ if trackName, ok := trackMap["trackName"].(string); ok {
713
+ tracks = append(tracks, trackName)
714
+ }
715
+ }
716
+ }
717
+ }
718
+
719
+ otherSessions = append(otherSessions, SessionInfo{
720
+ UserId: p.UserID,
721
+ SessionId: p.SessionID,
722
+ PublishedTracks: tracks,
723
+ })
724
+ }
725
+ }
726
+ room.Unlock()
727
+
728
+ // Initialize WebSocket connections map for the room
729
+ wsConnections.Lock()
730
+ if _, exists := wsConnections.m[roomId]; !exists {
731
+ wsConnections.m[roomId] = make(map[string]*websocket.Conn)
732
+ }
733
+ wsConnections.Unlock()
734
+
735
+ // Add more detail to the participant-joined event
736
+ broadcastToRoom(roomId, gin.H{
737
+ "type": "participant-joined",
738
+ "payload": gin.H{
739
+ "userId": userId,
740
+ "username": currentUser.Username,
741
+ "sessionId": sessionID,
742
+ },
743
+ }, userId)
744
+
745
+ response := SessionResponse{
746
+ SessionId: sessionID,
747
+ OtherSessions: otherSessions,
748
+ }
749
+
750
+ if debug {
751
+ log.Printf("Join room response: %+v\n", response)
752
+ }
753
+
754
+ c.JSON(http.StatusOK, response)
755
  }
756
 
757
+ // Sửa lại struct request cho publishTracks để match với Express
758
  func publishTracks(c *gin.Context) {
759
+ roomId := c.Param("roomId")
760
+ sessionId := c.Param("sessionId")
761
+
762
+ rooms.RLock()
763
+ room, ok := rooms.m[roomId]
764
+ rooms.RUnlock()
765
+ if !ok {
766
+ c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
767
+ return
768
+ }
769
+
770
+ var participant *Participant
771
+ room.RLock()
772
+ for _, p := range room.Participants {
773
+ if p.SessionID == sessionId {
774
+ participant = p
775
+ break
776
+ }
777
+ }
778
+ room.RUnlock()
779
+ if participant == nil {
780
+ c.JSON(http.StatusNotFound, gin.H{"error": "Session not found in this room"})
781
+ return
782
+ }
783
+
784
+ var req struct {
785
+ Offer map[string]interface{} `json:"offer"`
786
+ Tracks []struct {
787
+ TrackName string `json:"trackName"`
788
+ Mid string `json:"mid"` // Thêm mid
789
+ Location string `json:"location"`
790
+ } `json:"tracks"`
791
+ }
792
+
793
+ if err := c.ShouldBindJSON(&req); err != nil {
794
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
795
+ return
796
+ }
797
+
798
+ if debug {
799
+ log.Printf("Publishing tracks for session %s: %+v\n", sessionId, req.Tracks)
800
+ }
801
+
802
+ // Prepare track data for Cloudflare API
803
+ tracks := make([]map[string]interface{}, len(req.Tracks))
804
+ trackNames := make([]string, len(req.Tracks))
805
+ for i, t := range req.Tracks {
806
+ location := t.Location
807
+ if location == "" {
808
+ location = "local"
809
+ }
810
+ tracks[i] = map[string]interface{}{
811
+ "trackName": t.TrackName,
812
+ "location": location,
813
+ "mid": t.Mid,
814
+ }
815
+ trackNames[i] = t.TrackName
816
+ }
817
+
818
+ // Call Cloudflare API
819
+ requestBody := map[string]interface{}{
820
+ "sessionDescription": req.Offer,
821
+ "tracks": tracks,
822
+ }
823
+
824
+ url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId)
825
+ jsonData, err := json.Marshal(requestBody)
826
+ if err != nil {
827
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
828
+ return
829
+ }
830
+
831
+ cfReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
832
+ if err != nil {
833
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
834
+ return
835
+ }
836
+
837
+ cfReq.Header.Set("Authorization", "Bearer "+cloudflareAppSecret)
838
+ cfReq.Header.Set("Content-Type", "application/json")
839
+
840
+ client := &http.Client{}
841
+ resp, err := client.Do(cfReq)
842
+ if err != nil {
843
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
844
+ return
845
+ }
846
+ defer resp.Body.Close()
847
+
848
+ var data map[string]interface{}
849
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
850
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
851
+ return
852
+ }
853
+
854
+ // Update participant's published tracks
855
+ room.Lock()
856
+ for _, t := range req.Tracks {
857
+ if !contains(participant.PublishedTracks, t.TrackName) {
858
+ participant.PublishedTracks = append(participant.PublishedTracks, t.TrackName)
859
+ }
860
+ }
861
+ room.Unlock()
862
+
863
+ // Broadcast track-published event with complete information
864
+ if data["sessionDescription"] != nil {
865
+ broadcastToRoom(roomId, gin.H{
866
+ "type": "track-published",
867
+ "payload": gin.H{
868
+ "userId": participant.UserID,
869
+ "sessionId": sessionId,
870
+ "tracks": trackNames,
871
+ },
872
+ }, participant.UserID)
873
+
874
+ if debug {
875
+ log.Printf("Track published event broadcasted for session %s with tracks: %v\n", sessionId, trackNames)
876
+ }
877
+ }
878
+
879
+ c.JSON(http.StatusOK, data)
880
+ }
881
+
882
+ // Helper function to check if slice contains string
883
+ func contains(slice []string, str string) bool {
884
+ for _, v := range slice {
885
+ if v == str {
886
+ return true
887
+ }
888
+ }
889
+ return false
890
  }
891
 
892
  func unpublishTrack(c *gin.Context) {
 
900
  }
901
 
902
  var req struct {
903
+ TrackName string `json:"trackName"`
904
+ Mid string `json:"mid"`
905
+ Force bool `json:"force"`
906
+ SessionDescription map[string]interface{} `json:"sessionDescription"`
907
  }
908
 
909
  if err := c.ShouldBindJSON(&req); err != nil {
910
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
911
  return
912
  }
913
+ //If trying to force unpublish someone else's track
914
  if req.Force && sessionId != currentUser.UserID {
915
+ //Check if user is moderator
916
  if !currentUser.IsModerator {
917
  c.JSON(http.StatusForbidden, gin.H{
918
  "errorCode": "NOT_AUTHORIZED",
 
945
  if debug {
946
  log.Println("Calling Cloudflare API:", cfUrl)
947
  }
948
+
949
  requestBody := map[string]interface{}{
950
+ "tracks": []map[string]string{
951
  {"mid": req.Mid},
952
  },
953
  "force": req.Force,
954
  "sessionDescription": req.SessionDescription,
955
  }
956
  if debug {
957
+ log.Printf("Request body: %+v\n", requestBody) // Use %+v for detailed printing
958
  }
959
 
960
  data, err := unpublishToCloudflare(cfUrl, requestBody)
 
965
  if debug {
966
  log.Println("Cloudflare API response:", data)
967
  }
 
 
 
 
 
 
 
968
  broadcastToRoom(roomId, gin.H{
969
  "type": "track-unpublished",
970
  "payload": gin.H{"sessionId": sessionId, "trackName": req.TrackName},
971
  }, sessionId)
972
 
973
  c.JSON(http.StatusOK, data)
974
+
975
  }
976
 
977
  func pullTracks(c *gin.Context) {
978
+ roomId := c.Param("roomId")
979
+ sessionId := c.Param("sessionId")
980
+
981
+ // Struct to hold request body, khớp với frontend JavaScript (_pullTracks)
982
+ var req struct {
983
+ RemoteSessionId string `json:"remoteSessionId"` // camelCase, khớp với frontend
984
+ TrackName string `json:"trackName"` // Kéo từng track một
985
+ }
986
+
987
+ // Bind JSON request body từ request Gin
988
+ if err := c.ShouldBindJSON(&req); err != nil {
989
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
990
+ return
991
+ }
992
+
993
+ // Lấy thông tin room từ bộ nhớ
994
+ rooms.RLock()
995
+ room, ok := rooms.m[roomId]
996
+ rooms.RUnlock()
997
+ if !ok {
998
+ c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
999
+ return
1000
+ }
1001
+
1002
+ participant := findParticipantBySessionId(room, sessionId)
1003
+ if participant == nil {
1004
+ c.JSON(http.StatusNotFound, gin.H{"error": "Session not found in this room"})
1005
+ return
1006
+ }
1007
+
1008
+ // Tạo cấu trúc tracksToPull cho Cloudflare API (kéo 1 track)
1009
+ tracksToPull := []map[string]interface{}{
1010
+ {
1011
+ "location": "remote",
1012
+ "sessionId": req.RemoteSessionId, // Dùng RemoteSessionId từ request struct
1013
+ "trackName": req.TrackName, // Dùng TrackName từ request struct
1014
+ },
1015
+ }
1016
+
1017
+ // Gọi Cloudflare API để pull track (dùng hàm makeCloudflareRequest helper)
1018
+ url := fmt.Sprintf("%s/sessions/%s/tracks/new", cloudflareBasePath, sessionId)
1019
+ requestBody := map[string]interface{}{
1020
+ "tracks": tracksToPull,
1021
+ }
1022
+
1023
+ // Gọi makeCloudflareRequest để thực hiện request đến Cloudflare API
1024
+ data, err, statusCode := makeCloudflareRequest("POST", url, requestBody)
1025
+ if err != nil {
1026
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to pull track", "detail": err.Error()})
1027
+ return
1028
+ }
1029
+ // Kiểm tra mã trạng thái lỗi từ Cloudflare API
1030
+ if statusCode >= 400 {
1031
+ c.JSON(statusCode, gin.H{"error": "Cloudflare API error during pull track", "detail": data})
1032
+ return
1033
+ }
1034
+
1035
+ // Trả về phản hồi từ Cloudflare API cho frontend
1036
+ c.JSON(http.StatusOK, data)
1037
+ }
1038
+
1039
+ // Helper function để tìm participant theo sessionId (tái sử dụng từ code của bạn)
1040
+ func findParticipantBySessionId(room *Room, sessionId string) *Participant {
1041
+ room.RLock()
1042
+ defer room.RUnlock()
1043
+ for _, p := range room.Participants {
1044
+ if p.SessionID == sessionId {
1045
+ return p
1046
+ }
1047
+ }
1048
+ return nil
 
 
 
 
 
 
 
1049
  }
1050
 
1051
  func renegotiateSession(c *gin.Context) {
1052
+ sessionId := c.Param("sessionId")
1053
+ var req struct {
1054
+ SDP string `json:"sdp"`
1055
+ SDPType string `json:"type"`
1056
+ }
1057
+
1058
+ if err := c.ShouldBindJSON(&req); err != nil {
1059
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1060
+ return
1061
+ }
1062
+
1063
+ sdp := req.SDP
1064
+ sdpType := req.SDPType
1065
+ // var req struct {
1066
+ // SessionDescription struct {
1067
+ // SDP string `json:"sdp"`
1068
+ // Type string `json:"type"`
1069
+ // } `json:"sessionDescription"`
1070
+ // }
1071
+
1072
+ // if err := c.ShouldBindJSON(&req); err != nil {
1073
+ // c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
1074
+ // return
1075
+ // }
1076
+
1077
+ body := map[string]interface{}{
1078
+ "sessionDescription": map[string]string{
1079
+ "sdp": sdp,
1080
+ "type": sdpType,
1081
+ },
1082
+ }
1083
+
1084
+ data, err := renegotiateWithCloudflare(sessionId, body)
1085
+ if err != nil {
1086
+ if data != nil && data["errorCode"] != nil { // Check for Cloudflare error
1087
+ c.JSON(http.StatusBadRequest, data)
1088
+ return
1089
+ }
1090
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
1091
+ return
1092
+ }
1093
+
1094
+ c.JSON(http.StatusOK, data)
1095
  }
1096
 
1097
  func manageDataChannels(c *gin.Context) {
 
1100
 
1101
  rooms.RLock()
1102
  room, ok := rooms.m[roomId]
1103
+ print(room)
1104
  rooms.RUnlock()
1105
+ if !ok {
1106
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1107
  return
1108
  }
 
1119
  }
1120
 
1121
  cfUrl := fmt.Sprintf("%s/sessions/%s/datachannels/new", cloudflareBasePath, sessionId)
 
1122
  dataChannels := make([]map[string]interface{}, len(req.DataChannels))
1123
  for i, dc := range req.DataChannels {
1124
  dataChannels[i] = map[string]interface{}{
1125
  "location": dc.Location,
1126
+ "dataChannelName": dc.DataChannelName,
1127
  }
1128
  }
1129
 
1130
+ data, err := manageDataChannelsWithCloudflare(cfUrl, dataChannels)
1131
  if err != nil {
1132
  if data != nil && data["errorCode"] != nil { // Check for Cloudflare error
1133
  c.JSON(http.StatusBadRequest, data)
 
1152
  room, ok := rooms.m[roomId]
1153
  rooms.RUnlock()
1154
 
1155
+ if !ok {
1156
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1157
  return
1158
  }
 
1167
  rooms.RLock()
1168
  room, ok := rooms.m[roomId]
1169
  rooms.RUnlock()
1170
+ if !ok {
1171
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1172
  return
1173
  }
 
1181
  }
1182
  room.RUnlock()
1183
 
1184
+ if participant == nil {
1185
  c.JSON(http.StatusNotFound, gin.H{"error": "Participant not found"})
1186
  return
1187
  }
 
1234
 
1235
  c.JSON(http.StatusOK, iceServers)
1236
  }
 
1237
  func getToken(c *gin.Context) {
1238
  var req struct {
1239
  Username string `json:"username"`
 
1301
  users.RLock()
1302
  userInfo, ok := users.m[currentUser.UserID]
1303
  users.RUnlock()
1304
+ if !ok {
1305
  c.JSON(http.StatusNotFound, gin.H{
1306
  "errorCode": "USER_NOT_FOUND",
1307
  "errorDescription": "Current user not found",
 
1315
  users.RLock()
1316
  requestedUser, ok := users.m[userIdParam]
1317
  users.RUnlock()
1318
+ if !ok {
1319
  c.JSON(http.StatusNotFound, gin.H{
1320
  "errorCode": "USER_NOT_FOUND",
1321
  "errorDescription": "User not found",
 
1343
  rooms.RLock()
1344
  room, ok := rooms.m[roomId]
1345
  rooms.RUnlock()
1346
+ if !ok {
1347
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1348
  return
1349
  }
 
1453
  room, ok := rooms.m[roomId]
1454
  rooms.RUnlock()
1455
 
1456
+ if !ok {
1457
  c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
1458
  return
1459
  }
 
1497
  }
1498
 
1499
  func websocketHandler(c *gin.Context) {
1500
+ if debug {
1501
+ log.Printf("Incoming WebSocket request from: %s\n", c.Request.RemoteAddr)
1502
+ }
1503
+
1504
+ ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
1505
+ if err != nil {
1506
+ log.Printf("Failed to upgrade connection: %v\n", err)
1507
+ return
1508
+ }
1509
+ defer ws.Close()
1510
+
1511
+ handleWebSocket(ws)
 
 
 
 
 
 
 
 
 
 
 
1512
  }
1513
 
1514
  func handleWebSocket(ws *websocket.Conn) {
1515
+ if debug {
1516
+ log.Printf("New WebSocket connection established from: %s\n", ws.RemoteAddr().String())
1517
+ }
1518
+ defer ws.Close()
1519
+
1520
+ var userId string
1521
+ var roomId string
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522
 
1523
+ for {
1524
+ // Đọc message
1525
+ messageType, message, err := ws.ReadMessage()
1526
+ log.Println("messageType", messageType)
1527
+ if err != nil {
1528
+ if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
1529
+ log.Printf("WebSocket error: %v", err)
1530
+ }
1531
+ handleWSDisconnect(ws, roomId, userId)
1532
+ break
1533
+ }
1534
+
1535
+ // Parse message
1536
+ var data map[string]interface{}
1537
+ if err := json.Unmarshal(message, &data); err != nil {
1538
+ log.Println("Error parsing message:", err)
1539
+ continue
1540
+ }
1541
+
1542
+ messageTypeStr, ok := data["type"].(string)
1543
+ if !ok {
1544
+ log.Println("Invalid message format: missing or invalid 'type'")
1545
+ continue
1546
+ }
1547
+
1548
+ switch messageTypeStr {
1549
+ case "join-websocket":
1550
+ // Format payload giống Express
1551
+ payload := map[string]interface{}{
1552
+ "roomId": data["payload"].(map[string]interface{})["roomId"],
1553
+ "userId": data["payload"].(map[string]interface{})["userId"],
1554
+ "token": data["payload"].(map[string]interface{})["token"],
1555
+ }
1556
+ handleWSJoin(ws, payload)
1557
+
1558
+ case "data-message":
1559
+ // Format payload giống Express
1560
+ payload := map[string]interface{}{
1561
+ "from": data["payload"].(map[string]interface{})["from"],
1562
+ "data": data["payload"].(map[string]interface{})["data"],
1563
+ }
1564
+ handleDataMessage(ws, payload)
1565
+
1566
+ default:
1567
+ log.Println("Unknown message type:", messageTypeStr)
1568
+ }
1569
+ }
1570
  }
1571
 
1572
  // --- WebSocket Helper Functions ---
 
1675
  }, from) // Exclude the sender
1676
  }
1677
 
1678
+ func handleWSDisconnect(ws *websocket.Conn, roomId string, userId string) {
1679
  wsConnections.Lock()
1680
  defer wsConnections.Unlock()
1681
+ //remove user from room
1682
+ if _, ok := wsConnections.m[roomId]; ok {
1683
+ delete(wsConnections.m[roomId], userId)
 
1684
  if debug {
1685
  log.Printf("User %s disconnected from room %s\n", userId, roomId)
1686
  }
 
 
 
 
 
1687
  }
1688
  }
1689
 
1690
  func broadcastToRoom(roomId string, message gin.H, excludeUserId string) {
1691
+ if debug {
1692
+ log.Printf("Broadcasting to room %s: %+v (excluding: %s)\n", roomId, message, excludeUserId)
1693
+ }
1694
+ rooms.RLock()
1695
+ _, ok := rooms.m[roomId]
1696
+ rooms.RUnlock()
1697
+ if !ok {
1698
+ log.Printf("Room %s not found for broadcast\n", roomId)
1699
+ return
1700
+ }
1701
+
1702
+ wsConnections.RLock()
1703
+ connections, ok := wsConnections.m[roomId]
1704
+ wsConnections.RUnlock()
1705
+ if !ok {
1706
+ log.Printf("No WebSocket connections found for room %s\n", roomId)
1707
+ return
1708
+ }
1709
+
1710
+ // Serialize message once
1711
+ msgBytes, err := json.Marshal(message)
1712
+ if err != nil {
1713
+ log.Printf("Error serializing broadcast message: %v\n", err)
1714
+ return
1715
+ }
1716
+
1717
+ if debug {
1718
+ log.Printf("Serialized message for broadcast: %s\n", string(msgBytes))
1719
+ }
1720
+
1721
+ wsConnections.RLock()
1722
+ defer wsConnections.RUnlock()
1723
+
1724
+ for userId, conn := range connections {
1725
+ if userId == excludeUserId {
1726
+ continue
1727
+ }
1728
+ if conn != nil {
1729
+ err := conn.WriteMessage(websocket.TextMessage, msgBytes)
1730
+ if err != nil {
1731
+ log.Printf("Error broadcasting to user %s: %v\n", userId, err)
1732
+ // Clean up failed connection
1733
+ wsConnections.Lock()
1734
+ delete(wsConnections.m[roomId], userId)
1735
+ wsConnections.Unlock()
1736
+ } else if debug {
1737
+ log.Printf("Successfully sent broadcast message to user: %s\n", userId)
1738
+ }
1739
+ }
1740
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1741
  }
1742
 
1743
  func main() {
1744
+ initConfig() // Initialize configuration
1745
+
1746
+ r := gin.Default()
1747
+
1748
+ // CORS middleware (configure as needed)
1749
+ r.Use(func(c *gin.Context) {
1750
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // For development only!
1751
+ c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
1752
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Upgrade, Connection")
1753
+ c.Writer.Header().Set("Access-Control-Max-Age", "3600")
1754
+
1755
+ // Handle WebSocket pre-flight
1756
+ if c.Request.Method == "OPTIONS" {
1757
+ if c.Request.Header.Get("Upgrade") == "websocket" {
1758
+ c.Writer.Header().Set("Connection", "Upgrade")
1759
+ c.Writer.Header().Set("Upgrade", "websocket")
1760
+ }
1761
+ c.AbortWithStatus(204)
1762
+ return
1763
+ }
1764
+ c.Next()
1765
+ })
1766
+
1767
+ r.POST("/auth/token", getToken)
1768
+ // Protected routes (require JWT)
1769
+ api := r.Group("/api", verifyToken)
1770
+ {
1771
+ // Match Express routes
1772
+ api.POST("/rooms", createRoom)
1773
+ api.GET("/rooms", getRooms)
1774
+ api.GET("/inspect-rooms", inspectRooms)
1775
+ api.POST("/rooms/:roomId/join", joinRoom)
1776
+ api.POST("/rooms/:roomId/sessions/:sessionId/publish", publishTracks)
1777
+ api.POST("/rooms/:roomId/sessions/:sessionId/unpublish", unpublishTrack)
1778
+ api.POST("/rooms/:roomId/sessions/:sessionId/pull", pullTracks)
1779
+ api.PUT("/rooms/:roomId/sessions/:sessionId/renegotiate", renegotiateSession)
1780
+ api.POST("/rooms/:roomId/sessions/:sessionId/datachannels/new", manageDataChannels)
1781
+ api.GET("/rooms/:roomId/participants", getParticipants)
1782
+ api.GET("/rooms/:roomId/participant/:sessionId/tracks", getParticipantTracks)
1783
+ api.GET("/ice-servers", getICEServers)
1784
+ api.GET("/rooms/:roomId/sessions/:sessionId/state", getSessionState)
1785
+ api.GET("/users/:userId", getUserInfo)
1786
+ api.POST("/rooms/:roomId/leave", leaveRoom)
1787
+ api.POST("/rooms/:roomId/sessions/:sessionId/track-status", updateTrackStatus)
1788
+ api.PUT("/rooms/:roomId/metadata", updateRoomMetadata)
1789
+ }
1790
+
1791
+ r.GET("/ws", websocketHandler)
1792
+
1793
+ // Start the server
1794
+ log.Printf("Server listening on http://localhost:%s\n", port)
1795
+
1796
+ //listen and serve for http
1797
+ if err := r.Run(":" + port); err != nil {
1798
+ log.Fatal("Failed to start server:", err)
1799
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1800
  }
public/js/room.js CHANGED
@@ -286,6 +286,8 @@ function setupCallbacks() {
286
  container.appendChild(video);
287
  container.appendChild(name);
288
  videoGrid.appendChild(container);
 
 
289
  }
290
 
291
  const video = container.querySelector('video');
 
286
  container.appendChild(video);
287
  container.appendChild(name);
288
  videoGrid.appendChild(container);
289
+
290
+
291
  }
292
 
293
  const video = container.querySelector('video');