@dcl-regenesislabs/bevy-explorer-web 0.1.0-21146518146.commit-e5a08b4 → 0.1.0-21258122165.commit-c2a4a9d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,408 +8,465 @@ function error(...args) {
8
8
  console.error("[livekit]", ...args)
9
9
  }
10
10
 
11
- let currentMicTrack = false;
12
- const activeRooms = new Set();
13
-
14
- // Store audio elements and panner nodes for spatial audio
15
- const trackRigs = new Map();
16
- const participantAudioSids = new Map();
17
- const participantVideoSids = new Map();
18
11
  var audioContext = null;
19
12
 
20
- export async function connect_room(url, token, handler) {
21
- const room = new LivekitClient.Room({
22
- adaptiveStream: false,
23
- dynacast: false,
24
- });
13
+ /**
14
+ *
15
+ * @returns boolean
16
+ */
17
+ export function is_microphone_available() {
18
+ // Check if getUserMedia is available
19
+ const res = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
20
+ return res;
21
+ }
25
22
 
26
- set_room_event_handler(room, handler)
23
+ /**
24
+ *
25
+ * @param {string} url
26
+ * @param {string} token
27
+ * @param {livekit.RoomOptions} room_options
28
+ * @param {livekit.RoomConnectOptions} room_connect_options
29
+ * @param {function} handler
30
+ * @returns livekit.Room
31
+ */
32
+ export async function room_connect(url, token, room_options, room_connect_options, handler) {
33
+ const room = new LivekitClient.Room(room_options);
27
34
 
28
- await room.connect(url, token, {
29
- autoSubscribe: false,
30
- });
35
+ set_room_event_handler(room, handler);
31
36
 
32
- // Add to active rooms set
33
- activeRooms.add(room);
34
-
35
- // set up microphone
36
- if (currentMicTrack) {
37
- log(`sub ${room.name}`);
38
- const audioTrack = await LivekitClient.createLocalAudioTrack({
39
- echoCancellation: true,
40
- noiseSuppression: true,
41
- autoGainControl: true,
42
- });
43
- const pub = await room.localParticipant.publishTrack(audioTrack, {
44
- source: LivekitClient.Track.Source.Microphone,
45
- }).catch(error_msg => {
46
- error(`Failed to publish to room: ${error_msg}`);
47
- })
37
+ await room.connect(url, token, room_connect_options);
48
38
 
49
- // avoid race
50
- if (!currentMicTrack) {
51
- await room.localParticipant.unpublishTrack(pub.track);
52
- }
53
- }
39
+ return room;
40
+ }
41
+
42
+ /**
43
+ *
44
+ * @param {livekit.Room} room
45
+ */
46
+ export async function room_close(room) {
47
+ await room.disconnect();
48
+ }
54
49
 
55
- const room_name = room.name;
50
+ /**
51
+ *
52
+ * @param {livekit.Room} room
53
+ * @returns string
54
+ */
55
+ export function room_name(room) {
56
+ return room.name
57
+ }
58
+
59
+ /**
60
+ *
61
+ * @param {livekit.Room} room
62
+ * @returns livekit.LocalParticipant
63
+ */
64
+ export function room_local_participant(room) {
65
+ return room.localParticipant;
66
+ }
56
67
 
57
- // check existing streams
58
- const participants = Array.from(room.remoteParticipants.values());
59
- for (const participant of participants) {
68
+ /**
69
+ *
70
+ * @param {livekit.Room} room
71
+ * @param {function} handler
72
+ */
73
+ function set_room_event_handler(room, handler) {
74
+ room.on(LivekitClient.RoomEvent.Connected, () => {
75
+ const participants_with_tracks = Array
76
+ .from(room.remoteParticipants.values())
77
+ .filter(remote_participant => room.localParticipant.sid != remote_participant.sid)
78
+ .map(remote_participant => {
79
+ return {
80
+ participant: remote_participant,
81
+ tracks: Array.from(remote_participant.trackPublications.values())
82
+ };
83
+ });
60
84
  handler({
61
- type: 'participantConnected',
62
- room_name: room_name,
63
- participant: {
64
- identity: participant.identity,
65
- metadata: participant.metadata || ''
66
- }
85
+ type: 'connected',
86
+ participants_with_tracks
67
87
  })
68
-
69
- const audioPubs = Array.from(participant.trackPublications.values())
70
- .filter(pub => pub.kind === 'audio');
71
- for (const publication of audioPubs) {
72
- log(`found initial pub for ${participant}`);
88
+ });
89
+ room.on(LivekitClient.RoomEvent.ConnectionStateChanged, (state) => {
90
+ handler({
91
+ type: 'connectionStateChanged',
92
+ state: state
93
+ })
94
+ });
95
+ room.on(LivekitClient.RoomEvent.ConnectionQualityChanged, (connection_quality, participant) => {
96
+ handler({
97
+ type: 'connectionQualityChanged',
98
+ connection_quality,
99
+ participant
100
+ })
101
+ });
102
+ room.on(
103
+ LivekitClient.RoomEvent.DataReceived,
104
+ (payload, participant, kind, topic) => {
105
+ handler({
106
+ type: 'dataReceived',
107
+ payload,
108
+ participant,
109
+ kind,
110
+ topic
111
+ })
112
+ }
113
+ );
114
+ room.on(
115
+ LivekitClient.RoomEvent.ParticipantConnected,
116
+ (remote_participant) => {
117
+ handler({
118
+ type: 'participantConnected',
119
+ participant: remote_participant,
120
+ })
121
+ }
122
+ );
123
+ room.on(
124
+ LivekitClient.RoomEvent.ParticipantDisconnected,
125
+ (remote_participant) => {
126
+ handler({
127
+ type: 'participantDisconnected',
128
+ participant: remote_participant,
129
+ })
130
+ }
131
+ );
132
+ room.on(
133
+ LivekitClient.RoomEvent.ParticipantMetadataChanged,
134
+ (prev_metadata, participant) => {
135
+ handler({
136
+ type: 'participantMetadataChanged',
137
+ participant,
138
+ old_metadata: prev_metadata,
139
+ metadata: participant.metadata
140
+ })
141
+ }
142
+ );
143
+ room.on(
144
+ LivekitClient.RoomEvent.TrackPublished,
145
+ (remote_track_publication, remote_participant) => {
73
146
  handler({
74
147
  type: 'trackPublished',
75
- room_name: room_name,
76
- kind: publication.kind,
77
- participant: {
78
- identity: participant.identity,
79
- metadata: participant.metadata || ''
80
- }
148
+ publication: remote_track_publication,
149
+ participant: remote_participant
81
150
  })
82
151
  }
83
- }
84
-
85
- return room;
86
- }
152
+ );
153
+ room.on(
154
+ LivekitClient.RoomEvent.TrackUnpublished,
155
+ (remote_track_publication, remote_participant) => {
156
+ handler({
157
+ type: 'trackUnpublished',
158
+ publication: remote_track_publication,
159
+ participant: remote_participant
160
+ })
161
+ }
162
+ );
163
+ room.on(
164
+ LivekitClient.RoomEvent.TrackSubscribed,
165
+ (remote_track, remote_track_publication, remote_participant) => {
166
+ log(`Subscribed to track ${remote_track.sid} of ${remote_participant.sid} (${remote_participant.identity}).`);
167
+
168
+ if (remote_track.kind === "audio") {
169
+ if (remote_track.trackRig) {
170
+ error(`Rebuilding track rig of ${remote_track.sid} for ${remote_participant.sid} (${remote_participant.identity}).`);
171
+ track_rig_drop(remote_track);
172
+ }
173
+ if (!audioContext) {
174
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
175
+ }
87
176
 
88
- export function set_microphone_enabled(enabled) {
89
- if (enabled) {
90
- // Enable microphone
91
- if (!currentMicTrack) {
92
- currentMicTrack = true;
93
-
94
- // Publish to all active rooms
95
- const publishPromises = Array.from(activeRooms).map(async (room) => {
96
- log(`publish ${room.name}`);
97
- const audioTrack = await LivekitClient.createLocalAudioTrack({
98
- echoCancellation: true,
99
- noiseSuppression: true,
100
- autoGainControl: true,
101
- });
102
- let pub = await room.localParticipant.publishTrack(audioTrack, {
103
- source: LivekitClient.Track.Source.Microphone,
104
- }).catch(error_msg => {
105
- error(`Failed to publish to room: ${error_msg}`);
106
- });
107
-
108
- // avoid race
109
- if (!currentMicTrack) {
110
- await room.localParticipant.unpublishTrack(pub.track);
177
+ track_rig_new(remote_track);
178
+ } else if (remote_track.kind == "video") {
179
+ if (remote_track.videoElement) {
180
+ error(`Rebuilding video element of ${remote_track.sid} for ${remote_participant.sid} (${remote_participant.identity}).`);
181
+ const videoElement = remote_track.videoElement;
182
+ delete remote_track.videoElement;
183
+ remote_track.detach(videoElement);
111
184
  }
112
- });
185
+ const streamPlayerContainer = window.document.querySelector("#stream-player-container");
186
+ if (streamPlayerContainer) {
187
+ const videoElement = remote_track.attach();
188
+ streamPlayerContainer.append(videoElement);
189
+ remote_track.videoElement = videoElement;
190
+ }
191
+ }
113
192
 
114
- Promise.all(publishPromises).then(() => {
115
- log('Microphone enabled successfully for all rooms');
116
- }).catch(error_msg => {
117
- error('Failed to enable microphone:', error_msg);
118
- });
193
+ handler({
194
+ type: 'trackSubscribed',
195
+ track: remote_track,
196
+ publication: remote_track_publication,
197
+ participant: remote_participant
198
+ })
119
199
  }
120
- } else {
121
- // Disable microphone
122
- if (currentMicTrack) {
123
- const allRoomUnpublishPromises = Array.from(activeRooms).map(async (room) => {
124
- const audioPubs = Array.from(room.localParticipant.trackPublications.values())
125
- .filter(pub => pub.kind === 'audio');
126
-
127
- const roomSpecificPromises = audioPubs.map(pub => {
128
- try {
129
- room.localParticipant.unpublishTrack(pub.track);
130
- log(`unpublish ${room.name}`);
131
- } catch (error_msg) {
132
- error(`Failed to unpublish ${pub} from room ${room.name}:`, error_msg);
133
- }
134
- });
135
-
136
- try {
137
- await Promise.all(roomSpecificPromises);
138
- } catch (error_msg) {
139
- error(`Failed to unpublish audio from room ${room.name}:`, error_msg);
140
- }
141
- });
200
+ );
201
+ room.on(
202
+ LivekitClient.RoomEvent.TrackUnsubscribed,
203
+ // Note: The browser livekit docs say that the first parameter is a Livekit.Track,
204
+ // not a Livekit.RemoteTrack, verify if there is ever an event with a local
205
+ // track
206
+ (remote_track, remote_track_publication, remote_participant) => {
207
+ log(`Unsubscribed to track ${remote_track.sid} of ${remote_participant.sid} (${remote_participant.identity}).`);
208
+ if (remote_track.kind === "audio") {
209
+ track_rig_drop(remote_track);
210
+ }
142
211
 
143
- Promise.all(allRoomUnpublishPromises)
144
- .catch(error_msg => {
145
- error('A critical error occurred during the unpublish-all process:', error_msg);
146
- })
147
- .finally(() => {
148
- currentMicTrack = false;
149
- });
212
+ handler({
213
+ type: 'trackUnsubscribed',
214
+ track: remote_track,
215
+ publication: remote_track_publication,
216
+ participant: remote_participant
217
+ })
150
218
  }
151
- }
219
+ );
152
220
  }
153
221
 
154
- export function is_microphone_available() {
155
- // Check if getUserMedia is available
156
- const res = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
157
- return res;
222
+ /**
223
+ *
224
+ * @param {livekit.Participant} participant
225
+ * @returns bool
226
+ */
227
+ export async function particinpant_is_local(participant) {
228
+ return particinpant.isLocal;
158
229
  }
159
230
 
160
- export async function publish_data(room, data, reliable, destinations) {
161
- const options = {
162
- reliable: reliable,
163
- destination: destinations.length > 0 ? destinations : undefined,
164
- };
165
-
166
- await room.localParticipant.publishData(data, options);
231
+ /**
232
+ *
233
+ * @param {livekit.LocalParticipant} local_participant
234
+ * @param {Uint8Array} payload
235
+ * @param {livekit.DataPublishOptions} payload
236
+ * @returns string
237
+ */
238
+ export async function local_participant_publish_data(local_participant, payload, data_publish_options) {
239
+ local_participant.publishData(payload, data_publish_options).await;
167
240
  }
168
241
 
169
- export async function publish_audio_track(room, track) {
170
- const publication = await room.localParticipant.publishTrack(track, {
171
- source: LivekitClient.Track.Source.Microphone,
172
- });
173
- return publication.trackSid;
242
+ /**
243
+ *
244
+ * @param {livekit.LocalParticipant} local_participant
245
+ * @param {livekit.LocalTrack} local_track
246
+ * @param {livekit.TrackPublishingOptions} track_publishing_option
247
+ * @returns livekit.LocalTrackPublication
248
+ */
249
+ export async function local_participant_publish_track(local_participant, local_track, track_publishing_option) {
250
+ return await local_participant.publishTrack(local_track, track_publishing_option);
174
251
  }
175
252
 
176
- export async function unpublish_track(room, sid) {
177
- const publication = room.localParticipant.trackPublications.get(sid);
178
- if (publication) {
179
- await room.localParticipant.unpublishTrack(publication.track);
180
- }
253
+ /**
254
+ *
255
+ * @param {livekit.LocalParticipant} local_participant
256
+ * @param {livekit.LocalTrack} local_track
257
+ * @returns livekit.LocalTrackPublication
258
+ */
259
+ export async function local_participant_unpublish_track(local_participant, local_track) {
260
+ return await local_participant.unpublishTrack(local_track, true);
181
261
  }
182
262
 
183
- export async function close_room(room) {
184
- // Remove from active rooms set
185
- activeRooms.delete(room);
186
-
187
- // If mic is active, clean up
188
- if (currentMicTrack) {
189
- const audioPubs = Array.from(room.localParticipant.trackPublications.values())
190
- .filter(pub => pub.kind === 'audio');
191
-
192
- for (const pub of audioPubs) {
193
- log(`stop ${room.name} on exit`);
194
- pub.track.stop();
195
- }
196
- }
197
-
198
- await room.disconnect();
263
+ /**
264
+ *
265
+ * @param {livekit.LocalParticipant} local_participant
266
+ * @returns bool
267
+ */
268
+ export function local_participant_is_local(local_participant) {
269
+ return local_participant.isLocal;
199
270
  }
200
271
 
201
- export function set_room_event_handler(room, handler) {
202
- const room_name = room.name;
203
-
204
- room.on(LivekitClient.RoomEvent.DataReceived, (payload, participant) => {
205
- handler({
206
- type: 'dataReceived',
207
- room_name: room_name,
208
- payload,
209
- participant: {
210
- identity: participant.identity,
211
- metadata: participant.metadata || ''
212
- }
213
- });
214
- });
215
-
216
- room.on(LivekitClient.RoomEvent.TrackPublished, (publication, participant) => {
217
- log(`${room.name} ${participant.identity} rec pub ${publication.kind}`);
218
- handler({
219
- type: 'trackPublished',
220
- room_name: room_name,
221
- kind: publication.kind,
222
- participant: {
223
- identity: participant.identity,
224
- metadata: participant.metadata || ''
225
- }
226
- })
227
- });
228
-
229
- room.on(LivekitClient.RoomEvent.TrackUnpublished, (publication, participant) => {
230
- log(`${room.name} ${participant.identity} rec unpub ${publication.kind}`);
231
-
232
- const key = publication.trackSid;
233
- const rig = trackRigs.get(key);
272
+ /**
273
+ *
274
+ * @param {livekit.LocalParticipant} local_participant
275
+ * @returns string
276
+ */
277
+ export function local_participant_sid(local_participant) {
278
+ return local_participant.sid;
279
+ }
234
280
 
235
- if (rig) {
236
- log(`cleaning up audio rig for track: ${key}`);
281
+ /**
282
+ *
283
+ * @param {livekit.LocalParticipant} local_participant
284
+ * @returns string
285
+ */
286
+ export function local_participant_identity(local_participant) {
287
+ return local_participant.identity;
288
+ }
237
289
 
238
- rig.source.disconnect();
239
- rig.pannerNode.disconnect();
240
- rig.gainNode.disconnect();
290
+ /**
291
+ *
292
+ * @param {livekit.LocalParticipant} local_participant
293
+ * @returns string
294
+ */
295
+ export function local_participant_metadata(local_participant) {
296
+ return local_participant.metadata;
297
+ }
241
298
 
242
- trackRigs.delete(key);
243
- } else {
244
- log(`no cleanup for ${key}`);
245
- }
299
+ /**
300
+ *
301
+ * @param {livekit.LocalParticipant} remote_participant
302
+ * @returns bool
303
+ */
304
+ export function remote_participant_is_local(remote_participant) {
305
+ return remote_participant.isLocal;
306
+ }
246
307
 
247
- handler({
248
- type: 'trackUnpublished',
249
- room_name: room_name,
250
- kind: publication.kind,
251
- participant: {
252
- identity: participant.identity,
253
- metadata: participant.metadata || ''
254
- }
255
- })
256
- });
308
+ /**
309
+ *
310
+ * @param {livekit.RemoteParticipant} remote_participant
311
+ * @returns string
312
+ */
313
+ export function remote_participant_sid(remote_participant) {
314
+ return remote_participant.sid;
315
+ }
257
316
 
258
- room.on(LivekitClient.RoomEvent.TrackSubscribed, (track, publication, participant) => {
259
- log(`${room.name} ${participant.identity} rec sub ${publication.kind} (track sid ${track.sid})`);
260
- // For audio tracks, set up spatial audio
261
- if (track.kind === 'audio') {
262
- if (!audioContext) {
263
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
264
- }
317
+ /**
318
+ *
319
+ * @param {livekit.RemoteParticipant} remote_participant
320
+ * @returns string
321
+ */
322
+ export function remote_participant_identity(remote_participant) {
323
+ return remote_participant.identity;
324
+ }
265
325
 
266
- const key = track.sid;
267
-
268
- if (!trackRigs.has(key)) {
269
- log("create nodes for", key);
270
-
271
- // dummy audioElement
272
- const audioElement = track.attach();
273
- audioElement.volume = 0;
274
-
275
- // use the track internal stream in playback
276
- const stream = new MediaStream([track.mediaStreamTrack]);
277
- const source = audioContext.createMediaStreamSource(stream);
278
- const pannerNode = audioContext.createStereoPanner();
279
- const gainNode = audioContext.createGain();
280
-
281
- // Connect the audio graph: source -> panner -> gain -> destination
282
- source.connect(pannerNode);
283
- pannerNode.connect(gainNode);
284
- gainNode.connect(audioContext.destination);
285
-
286
- // Store the nodes for later control
287
- trackRigs.set(key, {
288
- audioElement,
289
- source,
290
- pannerNode,
291
- gainNode,
292
- stream,
293
- });
294
- }
326
+ /**
327
+ *
328
+ * @param {livekit.RemoteParticipant} remote_participant
329
+ * @returns string
330
+ */
331
+ export function remote_participant_metadata(remote_participant) {
332
+ return remote_participant.metadata;
333
+ }
295
334
 
296
- const audioElement = trackRigs.get(key).audioElement;
297
- audioElement.play(); // we have to do this to get the stream to start pumping
298
-
299
- log(`set rig for ${participant.identity}`, key);
300
- participantAudioSids.set(participant.identity, { room: room.name, audio: key })
301
- } else if (track.kind === "video") {
302
- const key = track.sid;
303
-
304
- if (!trackRigs.get(key)) {
305
- log("create video nodes for", key);
306
- const parentElement = window.document.querySelector("#stream-player-container");
307
- if (parentElement) {
308
- const element = track.attach();
309
- parentElement.appendChild(element);
310
- trackRigs.set(key, {
311
- videoElement: element,
312
- });
313
- }
314
- }
335
+ /**
336
+ *
337
+ * @param {livekit.RemoteTrackPublication} remote_track_publication
338
+ * @returns string
339
+ */
340
+ export function remote_track_publication_sid(remote_track_publication) {
341
+ return remote_track_publication.trackSid;
342
+ }
315
343
 
316
- participantVideoSids.set(participant.identity, { room: room.name, video: key })
317
- }
344
+ /**
345
+ *
346
+ * @param {livekit.RemoteTrackPublication} remote_track_publication
347
+ * @returns string
348
+ */
349
+ export function remote_track_publication_kind(remote_track_publication) {
350
+ return remote_track_publication.kind;
351
+ }
318
352
 
319
- handler({
320
- type: 'trackSubscribed',
321
- room_name: room_name,
322
- participant: {
323
- identity: participant.identity,
324
- metadata: participant.metadata || ''
325
- }
326
- });
327
- });
353
+ /**
354
+ *
355
+ * @param {livekit.RemoteTrackPublication} remote_track_publication
356
+ * @returns string
357
+ */
358
+ export function remote_track_publication_source(remote_track_publication) {
359
+ return remote_track_publication.source;
360
+ }
328
361
 
329
- room.on(LivekitClient.RoomEvent.TrackUnsubscribed, (track, publication, participant) => {
330
- log(`${room.name} ${participant.identity} rec unsub ${publication.kind} (track sid ${track.sid})`);
331
- if (participantAudioSids.get(participant.identity)?.room === room.name) {
332
- log(`delete lookup for ${participant.identity}`);
333
- participantAudioSids.delete(participant.identity);
334
- }
335
- if (participantVideoSids.get(participant.identity)?.room === room.name) {
336
- log(`delete video lookup for ${participant.identity}`);
337
- participantVideoSids.delete(participant.identity);
338
- }
362
+ /**
363
+ *
364
+ * @param {livekit.RemoteTrackPublication} remote_track_publication
365
+ * @param {boolean} subscribed
366
+ * @returns string
367
+ */
368
+ export function remote_track_publication_set_subscribed(remote_track_publication, subscribed) {
369
+ remote_track_publication.setSubscribed(subscribed);
370
+ }
339
371
 
340
- const key = track.sid;
341
372
 
342
- if (trackRigs.has(key)) {
343
- const audioElement = trackRigs.get(key).audioElement;
344
- if (audioElement) {
345
- log(`detach and pause audioElement for ${key}`)
346
- track.detach(audioElement);
347
- audioElement.pause();
348
- }
349
- const videoElement = trackRigs.get(key).videoElement;
350
- if (videoElement) {
351
- log(`detach videoElement for ${key}`)
352
- track.detach(videoElement);
353
- videoElement.remove();
354
- }
355
- trackRigs.delete(key);
356
- }
373
+ /**
374
+ *
375
+ * @param {livekit.RemoteTrackPublication} remote_track_publication
376
+ * @returns livekit.RemoteTrack | null
377
+ */
378
+ export function remote_track_publication_track(remote_track_publication) {
379
+ log(remote_track_publication);
380
+ return remote_track_publication.track;
381
+ }
357
382
 
383
+ /**
384
+ *
385
+ * @param {livekit.AudioCaptureOptions} options
386
+ * @returns livekit.LocalAudioTrack
387
+ */
388
+ export async function local_audio_track_new(options) {
389
+ try {
390
+ return await LivekitClient.createLocalAudioTrack(options);
391
+ } catch (err) {
392
+ error(err);
393
+ }
394
+ }
358
395
 
359
- handler({
360
- type: 'trackUnsubscribed',
361
- room_name: room_name,
362
- participant: {
363
- identity: participant.identity,
364
- metadata: participant.metadata || ''
365
- }
366
- });
367
- });
396
+ /**
397
+ *
398
+ * @param {livekit.LocalAudioTrack} local_audio_track
399
+ * @returns livekit.TrackSid
400
+ */
401
+ export function local_audio_track_sid(local_audio_track) {
402
+ return local_audio_track.sid;
403
+ }
368
404
 
369
- room.on(LivekitClient.RoomEvent.ParticipantConnected, (participant) => {
370
- handler({
371
- type: 'participantConnected',
372
- room_name: room_name,
373
- participant: {
374
- identity: participant.identity,
375
- metadata: participant.metadata || ''
376
- }
377
- });
378
- });
405
+ /**
406
+ *
407
+ * @param {livekit.RemoteTrack} remote_track
408
+ */
409
+ function track_rig_new(remote_track) {
410
+ log(`Creating new track rig for ${remote_track.sid}.`);
411
+
412
+ // dummy audioElement
413
+ const audioElement = remote_track.attach();
414
+ audioElement.volume = 0;
415
+
416
+ // use the track internal stream in playback
417
+ const stream = new MediaStream([remote_track.mediaStreamTrack]);
418
+ const source = audioContext.createMediaStreamSource(stream);
419
+ const pannerNode = audioContext.createStereoPanner();
420
+ const gainNode = audioContext.createGain();
421
+
422
+ // Connect the audio graph: source -> panner -> gain -> destination
423
+ source.connect(pannerNode);
424
+ pannerNode.connect(gainNode);
425
+ gainNode.connect(audioContext.destination);
426
+
427
+ // Store the nodes for later control
428
+ remote_track.trackRig = {
429
+ audioElement,
430
+ source,
431
+ pannerNode,
432
+ gainNode,
433
+ stream,
434
+ };
379
435
 
380
- room.on(LivekitClient.RoomEvent.ParticipantDisconnected, (participant) => {
381
- participantAudioSids.delete(participant.identity);
382
- participantVideoSids.delete(participant.identity);
383
- handler({
384
- type: 'participantDisconnected',
385
- room_name: room_name,
386
- participant: {
387
- identity: participant.identity,
388
- metadata: participant.metadata || ''
389
- }
390
- });
391
- });
436
+ audioElement.play();
392
437
  }
393
438
 
394
- // Spatial audio control functions
395
- export function set_participant_spatial_audio(participantIdentity, pan, volume) {
396
- const participantAudio = participantAudioSids.get(participantIdentity);
397
- if (!participantAudio) {
398
- log(`no rig for ${participantIdentity}`)
399
- return;
400
- }
401
-
402
- const nodes = trackRigs.get(participantAudio.audio);
403
- if (!nodes) {
404
- error(`no nodes for participant ${participantIdentity}, this should never happen`, audio);
405
- error("rigs:", trackRigs);
406
- return;
439
+ /**
440
+ *
441
+ * @param {livekit.RemoteTrack} remote_track
442
+ */
443
+ function track_rig_drop(remote_track) {
444
+ log(`Dropping track rig of ${remote_track.sid}.`);
445
+ const track_rig = remote_track.trackRig;
446
+ if (track_rig) {
447
+ delete remote_track.trackRig;
448
+
449
+ remote_track.detach(track_rig.audioElement);
450
+ track_rig.source.disconnect();
451
+ track_rig.pannerNode.disconnect();
452
+ track_rig.gainNode.disconnect();
453
+ track_rig.audioElement.pause();
407
454
  }
455
+ }
408
456
 
457
+ /**
458
+ *
459
+ * @param {livekit.RemoteTrack} remote_track
460
+ * @param {float} pan
461
+ * @param {float} volume
462
+ */
463
+ export function remote_track_pan_and_volume(remote_track, pan, volume) {
464
+ log(`Setting pan and volume for track ${remote_track.sid}.`);
465
+ const track_rig = remote_track.trackRig;
409
466
  // Pan value should be between -1 (left) and 1 (right)
410
- nodes.pannerNode.pan.value = Math.max(-1, Math.min(1, pan));
467
+ track_rig.pannerNode.pan.value = Math.max(-1, Math.min(1, pan));
411
468
  // Volume should be between 0 and 1 (or higher for boost)
412
- nodes.gainNode.gain.value = Math.max(0, volume);
469
+ track_rig.gainNode.gain.value = Math.max(0, volume);
413
470
 
414
471
  // nodes.analyser.getByteTimeDomainData(nodes.dataArray);
415
472
 
@@ -424,66 +481,3 @@ export function set_participant_spatial_audio(participantIdentity, pan, volume)
424
481
 
425
482
  // log(`[${audioContext.state}] Set spatial audio for ${participantIdentity} : pan=${nodes.pannerNode.pan.value}, volume=${nodes.gainNode.gain.value}`);
426
483
  }
427
-
428
- // Get all active participant identities with audio
429
- export function get_audio_participants() {
430
- return Array.from(participantAudioSids.keys());
431
- }
432
-
433
- export function subscribe_channel(roomName, participantId, subscribe) {
434
- const room = Array.from(activeRooms).find(room => room.name === roomName);
435
- if (!room) {
436
- warn(`couldn't find room ${roomName} for subscription`);
437
- return;
438
- }
439
-
440
- const participant = room.remoteParticipants.get(participantId);
441
- if (!participant) {
442
- warn(`couldn't find participant ${participantId} in room ${roomName} for subscription`);
443
- return;
444
- }
445
-
446
- const audioPubs = Array.from(participant.trackPublications.values())
447
- .filter(pub => pub.kind === 'audio');
448
-
449
- log(`subscribing to ${audioPubs.length} audio tracks`);
450
-
451
- for (const pub of audioPubs) {
452
- log(`sub ${roomName}-${participantId}`);
453
- pub.setSubscribed(subscribe);
454
- }
455
- }
456
-
457
- export function streamer_subscribe_channel(roomName, subscribe_audio, subscribe_video) {
458
- const room = Array.from(activeRooms).find(room => room.name === roomName);
459
- if (!room) {
460
- warn(`couldn't find room ${roomName} for subscription`);
461
- return;
462
- }
463
-
464
- const participant = room.remoteParticipants.values().find(participant => participant.identity.endsWith("-streamer"));
465
- if (!participant) {
466
- warn(`couldn't find streamer participant in room ${roomName} for subscription`);
467
- return;
468
- }
469
-
470
- const audioPubs = Array.from(participant.trackPublications.values())
471
- .filter(pub => pub.kind === 'audio');
472
- const videoPubs = Array.from(participant.trackPublications.values())
473
- .filter(pub => pub.kind === 'video');
474
-
475
- log(`subscribing to ${audioPubs.length} audio tracks and to ${videoPubs.length} video tracks`);
476
-
477
- for (const pub of audioPubs) {
478
- log(`sub(${subscribe_video}) ${roomName}-${participant.identity}`);
479
- pub.setSubscribed(subscribe_audio);
480
- }
481
- for (const pub of videoPubs) {
482
- log(`video sub(${subscribe_video}) ${roomName}-${participant.identity}`);
483
- pub.setSubscribed(subscribe_video);
484
- }
485
- }
486
-
487
- export function room_name(room) {
488
- return room.name
489
- }