@fluxerjs/voice 1.2.2 → 1.2.3
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.
- package/README.md +19 -1
- package/dist/index.d.mts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +177 -1
- package/dist/index.mjs +180 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -11,12 +11,30 @@ pnpm add @fluxerjs/voice @fluxerjs/core
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
|
-
import { getVoiceManager } from '@fluxerjs/voice';
|
|
14
|
+
import { getVoiceManager, LiveKitRtcConnection } from '@fluxerjs/voice';
|
|
15
15
|
|
|
16
16
|
const voiceManager = getVoiceManager(client);
|
|
17
17
|
const connection = await voiceManager.join(channel);
|
|
18
18
|
await connection.play(streamUrl);
|
|
19
19
|
|
|
20
|
+
// Inbound transcription / speech-to-text pipeline
|
|
21
|
+
if (connection instanceof LiveKitRtcConnection) {
|
|
22
|
+
const subs = voiceManager.subscribeChannelParticipants(channel.id);
|
|
23
|
+
connection.on('speakerStart', ({ participantId }) => {
|
|
24
|
+
console.log('speaker start', participantId);
|
|
25
|
+
});
|
|
26
|
+
connection.on('speakerStop', ({ participantId }) => {
|
|
27
|
+
console.log('speaker stop', participantId);
|
|
28
|
+
});
|
|
29
|
+
connection.on('audioFrame', (frame) => {
|
|
30
|
+
// frame.samples is Int16 PCM suitable for WAV/STT pipelines
|
|
31
|
+
console.log(frame.participantId, frame.sampleRate, frame.channels, frame.samples.length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// cleanup subscriptions when done
|
|
35
|
+
for (const sub of subs) sub.stop();
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
connection.stop();
|
|
21
39
|
voiceManager.leave(guildId);
|
|
22
40
|
```
|
package/dist/index.d.mts
CHANGED
|
@@ -67,6 +67,27 @@ type LiveKitRtcConnectionEvents = VoiceConnectionEvents & {
|
|
|
67
67
|
self_stream?: boolean;
|
|
68
68
|
self_video?: boolean;
|
|
69
69
|
}];
|
|
70
|
+
/** Emitted when a remote participant starts speaking. */
|
|
71
|
+
speakerStart: [payload: {
|
|
72
|
+
participantId: string;
|
|
73
|
+
}];
|
|
74
|
+
/** Emitted when a remote participant stops speaking. */
|
|
75
|
+
speakerStop: [payload: {
|
|
76
|
+
participantId: string;
|
|
77
|
+
}];
|
|
78
|
+
/** Emitted for each decoded inbound audio frame. */
|
|
79
|
+
audioFrame: [frame: LiveKitAudioFrame];
|
|
80
|
+
};
|
|
81
|
+
type LiveKitAudioFrame = {
|
|
82
|
+
participantId: string;
|
|
83
|
+
trackSid?: string;
|
|
84
|
+
sampleRate: number;
|
|
85
|
+
channels: number;
|
|
86
|
+
samples: Int16Array;
|
|
87
|
+
};
|
|
88
|
+
type LiveKitReceiveSubscription = {
|
|
89
|
+
participantId: string;
|
|
90
|
+
stop: () => void;
|
|
70
91
|
};
|
|
71
92
|
/**
|
|
72
93
|
* Options for video playback via {@link LiveKitRtcConnection.playVideo}.
|
|
@@ -133,6 +154,10 @@ declare class LiveKitRtcConnection extends EventEmitter {
|
|
|
133
154
|
private lastServerEndpoint;
|
|
134
155
|
private lastServerToken;
|
|
135
156
|
private _disconnectEmitted;
|
|
157
|
+
private readonly receiveSubscriptions;
|
|
158
|
+
private readonly requestedSubscriptions;
|
|
159
|
+
private readonly participantTrackSids;
|
|
160
|
+
private readonly activeSpeakers;
|
|
136
161
|
/**
|
|
137
162
|
* @param client - The Fluxer client instance
|
|
138
163
|
* @param channel - The voice channel to connect to
|
|
@@ -156,6 +181,13 @@ declare class LiveKitRtcConnection extends EventEmitter {
|
|
|
156
181
|
setVolume(volumePercent: number): void;
|
|
157
182
|
/** Get current volume (0-200). */
|
|
158
183
|
getVolume(): number;
|
|
184
|
+
private isAudioTrack;
|
|
185
|
+
private getParticipantId;
|
|
186
|
+
private subscribeParticipantTrack;
|
|
187
|
+
subscribeParticipantAudio(participantId: string, options?: {
|
|
188
|
+
autoResubscribe?: boolean;
|
|
189
|
+
}): LiveKitReceiveSubscription;
|
|
190
|
+
private clearReceiveSubscriptions;
|
|
159
191
|
playOpus(_stream: NodeJS.ReadableStream): void;
|
|
160
192
|
/**
|
|
161
193
|
* Connect to the LiveKit room using voice server and state from the gateway.
|
|
@@ -248,6 +280,17 @@ declare class VoiceManager extends EventEmitter {
|
|
|
248
280
|
* @param userId - User ID to look up
|
|
249
281
|
*/
|
|
250
282
|
getVoiceChannelId(guildId: string, userId: string): string | null;
|
|
283
|
+
/**
|
|
284
|
+
* List participant user IDs currently in a specific voice channel.
|
|
285
|
+
*/
|
|
286
|
+
listParticipantsInChannel(guildId: string, channelId: string): string[];
|
|
287
|
+
/**
|
|
288
|
+
* Subscribe to inbound audio for all known participants currently in a voice channel.
|
|
289
|
+
* Only supported for LiveKit connections.
|
|
290
|
+
*/
|
|
291
|
+
subscribeChannelParticipants(channelId: string, opts?: {
|
|
292
|
+
autoResubscribe?: boolean;
|
|
293
|
+
}): LiveKitReceiveSubscription[];
|
|
251
294
|
private handleVoiceStateUpdate;
|
|
252
295
|
private handleVoiceServerUpdate;
|
|
253
296
|
private storeConnectionId;
|
|
@@ -317,4 +360,4 @@ declare function getVoiceManager(client: Client, options?: {
|
|
|
317
360
|
shardId?: number;
|
|
318
361
|
}): VoiceManager;
|
|
319
362
|
|
|
320
|
-
export { LiveKitRtcConnection, type LiveKitRtcConnectionEvents, type VideoPlayOptions, VoiceConnection, type VoiceConnectionEvents, type VoiceConnectionLike, VoiceManager, type VoiceManagerOptions, type VoiceStateMap, getVoiceManager, joinVoiceChannel };
|
|
363
|
+
export { type LiveKitAudioFrame, type LiveKitReceiveSubscription, LiveKitRtcConnection, type LiveKitRtcConnectionEvents, type VideoPlayOptions, VoiceConnection, type VoiceConnectionEvents, type VoiceConnectionLike, VoiceManager, type VoiceManagerOptions, type VoiceStateMap, getVoiceManager, joinVoiceChannel };
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,27 @@ type LiveKitRtcConnectionEvents = VoiceConnectionEvents & {
|
|
|
67
67
|
self_stream?: boolean;
|
|
68
68
|
self_video?: boolean;
|
|
69
69
|
}];
|
|
70
|
+
/** Emitted when a remote participant starts speaking. */
|
|
71
|
+
speakerStart: [payload: {
|
|
72
|
+
participantId: string;
|
|
73
|
+
}];
|
|
74
|
+
/** Emitted when a remote participant stops speaking. */
|
|
75
|
+
speakerStop: [payload: {
|
|
76
|
+
participantId: string;
|
|
77
|
+
}];
|
|
78
|
+
/** Emitted for each decoded inbound audio frame. */
|
|
79
|
+
audioFrame: [frame: LiveKitAudioFrame];
|
|
80
|
+
};
|
|
81
|
+
type LiveKitAudioFrame = {
|
|
82
|
+
participantId: string;
|
|
83
|
+
trackSid?: string;
|
|
84
|
+
sampleRate: number;
|
|
85
|
+
channels: number;
|
|
86
|
+
samples: Int16Array;
|
|
87
|
+
};
|
|
88
|
+
type LiveKitReceiveSubscription = {
|
|
89
|
+
participantId: string;
|
|
90
|
+
stop: () => void;
|
|
70
91
|
};
|
|
71
92
|
/**
|
|
72
93
|
* Options for video playback via {@link LiveKitRtcConnection.playVideo}.
|
|
@@ -133,6 +154,10 @@ declare class LiveKitRtcConnection extends EventEmitter {
|
|
|
133
154
|
private lastServerEndpoint;
|
|
134
155
|
private lastServerToken;
|
|
135
156
|
private _disconnectEmitted;
|
|
157
|
+
private readonly receiveSubscriptions;
|
|
158
|
+
private readonly requestedSubscriptions;
|
|
159
|
+
private readonly participantTrackSids;
|
|
160
|
+
private readonly activeSpeakers;
|
|
136
161
|
/**
|
|
137
162
|
* @param client - The Fluxer client instance
|
|
138
163
|
* @param channel - The voice channel to connect to
|
|
@@ -156,6 +181,13 @@ declare class LiveKitRtcConnection extends EventEmitter {
|
|
|
156
181
|
setVolume(volumePercent: number): void;
|
|
157
182
|
/** Get current volume (0-200). */
|
|
158
183
|
getVolume(): number;
|
|
184
|
+
private isAudioTrack;
|
|
185
|
+
private getParticipantId;
|
|
186
|
+
private subscribeParticipantTrack;
|
|
187
|
+
subscribeParticipantAudio(participantId: string, options?: {
|
|
188
|
+
autoResubscribe?: boolean;
|
|
189
|
+
}): LiveKitReceiveSubscription;
|
|
190
|
+
private clearReceiveSubscriptions;
|
|
159
191
|
playOpus(_stream: NodeJS.ReadableStream): void;
|
|
160
192
|
/**
|
|
161
193
|
* Connect to the LiveKit room using voice server and state from the gateway.
|
|
@@ -248,6 +280,17 @@ declare class VoiceManager extends EventEmitter {
|
|
|
248
280
|
* @param userId - User ID to look up
|
|
249
281
|
*/
|
|
250
282
|
getVoiceChannelId(guildId: string, userId: string): string | null;
|
|
283
|
+
/**
|
|
284
|
+
* List participant user IDs currently in a specific voice channel.
|
|
285
|
+
*/
|
|
286
|
+
listParticipantsInChannel(guildId: string, channelId: string): string[];
|
|
287
|
+
/**
|
|
288
|
+
* Subscribe to inbound audio for all known participants currently in a voice channel.
|
|
289
|
+
* Only supported for LiveKit connections.
|
|
290
|
+
*/
|
|
291
|
+
subscribeChannelParticipants(channelId: string, opts?: {
|
|
292
|
+
autoResubscribe?: boolean;
|
|
293
|
+
}): LiveKitReceiveSubscription[];
|
|
251
294
|
private handleVoiceStateUpdate;
|
|
252
295
|
private handleVoiceServerUpdate;
|
|
253
296
|
private storeConnectionId;
|
|
@@ -317,4 +360,4 @@ declare function getVoiceManager(client: Client, options?: {
|
|
|
317
360
|
shardId?: number;
|
|
318
361
|
}): VoiceManager;
|
|
319
362
|
|
|
320
|
-
export { LiveKitRtcConnection, type LiveKitRtcConnectionEvents, type VideoPlayOptions, VoiceConnection, type VoiceConnectionEvents, type VoiceConnectionLike, VoiceManager, type VoiceManagerOptions, type VoiceStateMap, getVoiceManager, joinVoiceChannel };
|
|
363
|
+
export { type LiveKitAudioFrame, type LiveKitReceiveSubscription, LiveKitRtcConnection, type LiveKitRtcConnectionEvents, type VideoPlayOptions, VoiceConnection, type VoiceConnectionEvents, type VoiceConnectionLike, VoiceManager, type VoiceManagerOptions, type VoiceStateMap, getVoiceManager, joinVoiceChannel };
|
package/dist/index.js
CHANGED
|
@@ -499,6 +499,7 @@ var import_node_util = require("util");
|
|
|
499
499
|
var import_mp4box = require("mp4box");
|
|
500
500
|
var SAMPLE_RATE = 48e3;
|
|
501
501
|
var CHANNELS2 = 1;
|
|
502
|
+
var RECEIVE_READ_TIMEOUT_MS = 100;
|
|
502
503
|
function getNaluByteLength(nalu) {
|
|
503
504
|
if (ArrayBuffer.isView(nalu)) return nalu.byteLength;
|
|
504
505
|
if (nalu instanceof ArrayBuffer) return nalu.byteLength;
|
|
@@ -605,6 +606,10 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
605
606
|
lastServerEndpoint = null;
|
|
606
607
|
lastServerToken = null;
|
|
607
608
|
_disconnectEmitted = false;
|
|
609
|
+
receiveSubscriptions = /* @__PURE__ */ new Map();
|
|
610
|
+
requestedSubscriptions = /* @__PURE__ */ new Map();
|
|
611
|
+
participantTrackSids = /* @__PURE__ */ new Map();
|
|
612
|
+
activeSpeakers = /* @__PURE__ */ new Set();
|
|
608
613
|
/**
|
|
609
614
|
* @param client - The Fluxer client instance
|
|
610
615
|
* @param channel - The voice channel to connect to
|
|
@@ -655,6 +660,110 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
655
660
|
getVolume() {
|
|
656
661
|
return this._volume ?? 100;
|
|
657
662
|
}
|
|
663
|
+
isAudioTrack(track) {
|
|
664
|
+
return track.kind === import_rtc_node.TrackKind.KIND_AUDIO;
|
|
665
|
+
}
|
|
666
|
+
getParticipantId(participant) {
|
|
667
|
+
return participant.identity;
|
|
668
|
+
}
|
|
669
|
+
subscribeParticipantTrack(participant, track, options = {}) {
|
|
670
|
+
if (!this.isAudioTrack(track)) return;
|
|
671
|
+
const participantId = this.getParticipantId(participant);
|
|
672
|
+
if (!options.autoSubscribe && !this.requestedSubscriptions.has(participantId)) return;
|
|
673
|
+
const current = this.receiveSubscriptions.get(participantId);
|
|
674
|
+
if (current) current.stop();
|
|
675
|
+
const audioStream = new import_rtc_node.AudioStream(track, {
|
|
676
|
+
sampleRate: SAMPLE_RATE,
|
|
677
|
+
numChannels: CHANNELS2,
|
|
678
|
+
frameSizeMs: 10
|
|
679
|
+
});
|
|
680
|
+
let stopped = false;
|
|
681
|
+
let reader = null;
|
|
682
|
+
const pump = async () => {
|
|
683
|
+
try {
|
|
684
|
+
reader = audioStream.getReader();
|
|
685
|
+
while (!stopped) {
|
|
686
|
+
let readTimeout = null;
|
|
687
|
+
const next = await Promise.race([
|
|
688
|
+
reader.read(),
|
|
689
|
+
new Promise((resolve) => {
|
|
690
|
+
readTimeout = setTimeout(() => resolve(null), RECEIVE_READ_TIMEOUT_MS);
|
|
691
|
+
})
|
|
692
|
+
]);
|
|
693
|
+
if (readTimeout) clearTimeout(readTimeout);
|
|
694
|
+
if (next === null) continue;
|
|
695
|
+
const { done, value } = next;
|
|
696
|
+
if (done || !value) break;
|
|
697
|
+
this.emit("audioFrame", {
|
|
698
|
+
participantId,
|
|
699
|
+
trackSid: track.sid,
|
|
700
|
+
sampleRate: value.sampleRate,
|
|
701
|
+
channels: value.channels,
|
|
702
|
+
samples: value.data
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
if (!stopped) {
|
|
707
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
708
|
+
}
|
|
709
|
+
} finally {
|
|
710
|
+
if (reader) {
|
|
711
|
+
try {
|
|
712
|
+
reader.releaseLock();
|
|
713
|
+
} catch {
|
|
714
|
+
}
|
|
715
|
+
reader = null;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
const stop = () => {
|
|
720
|
+
if (stopped) return;
|
|
721
|
+
stopped = true;
|
|
722
|
+
if (reader) {
|
|
723
|
+
reader.cancel().catch(() => {
|
|
724
|
+
});
|
|
725
|
+
try {
|
|
726
|
+
reader.releaseLock();
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
audioStream.cancel().catch(() => {
|
|
731
|
+
});
|
|
732
|
+
this.receiveSubscriptions.delete(participantId);
|
|
733
|
+
};
|
|
734
|
+
this.receiveSubscriptions.set(participantId, { participantId, stop });
|
|
735
|
+
this.participantTrackSids.set(participantId, track.sid ?? "");
|
|
736
|
+
void pump();
|
|
737
|
+
}
|
|
738
|
+
subscribeParticipantAudio(participantId, options = {}) {
|
|
739
|
+
const autoResubscribe = options.autoResubscribe === true;
|
|
740
|
+
const stop = () => {
|
|
741
|
+
this.receiveSubscriptions.get(participantId)?.stop();
|
|
742
|
+
this.receiveSubscriptions.delete(participantId);
|
|
743
|
+
this.participantTrackSids.delete(participantId);
|
|
744
|
+
this.requestedSubscriptions.delete(participantId);
|
|
745
|
+
};
|
|
746
|
+
this.requestedSubscriptions.set(participantId, autoResubscribe);
|
|
747
|
+
const room = this.room;
|
|
748
|
+
if (!room || !room.isConnected) return { participantId, stop };
|
|
749
|
+
const participant = room.remoteParticipants.get(participantId);
|
|
750
|
+
if (!participant) return { participantId, stop };
|
|
751
|
+
for (const pub of participant.trackPublications.values()) {
|
|
752
|
+
const maybeTrack = pub.track;
|
|
753
|
+
if (maybeTrack && this.isAudioTrack(maybeTrack)) {
|
|
754
|
+
this.subscribeParticipantTrack(participant, maybeTrack);
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return { participantId, stop };
|
|
759
|
+
}
|
|
760
|
+
clearReceiveSubscriptions() {
|
|
761
|
+
for (const sub of this.receiveSubscriptions.values()) sub.stop();
|
|
762
|
+
this.receiveSubscriptions.clear();
|
|
763
|
+
this.requestedSubscriptions.clear();
|
|
764
|
+
this.participantTrackSids.clear();
|
|
765
|
+
this.activeSpeakers.clear();
|
|
766
|
+
}
|
|
658
767
|
playOpus(_stream) {
|
|
659
768
|
this.emit(
|
|
660
769
|
"error",
|
|
@@ -693,7 +802,48 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
693
802
|
room.on(import_rtc_node.RoomEvent.Reconnected, () => {
|
|
694
803
|
this.debug("Room reconnected");
|
|
695
804
|
});
|
|
696
|
-
|
|
805
|
+
room.on(import_rtc_node.RoomEvent.TrackSubscribed, (track, _publication, participant) => {
|
|
806
|
+
if (!this.isAudioTrack(track)) return;
|
|
807
|
+
this.subscribeParticipantTrack(participant, track);
|
|
808
|
+
});
|
|
809
|
+
room.on(import_rtc_node.RoomEvent.TrackUnsubscribed, (track, _publication, participant) => {
|
|
810
|
+
if (!this.isAudioTrack(track)) return;
|
|
811
|
+
const participantId = this.getParticipantId(participant);
|
|
812
|
+
this.receiveSubscriptions.get(participantId)?.stop();
|
|
813
|
+
this.receiveSubscriptions.delete(participantId);
|
|
814
|
+
if (this.requestedSubscriptions.get(participantId) !== true) {
|
|
815
|
+
this.requestedSubscriptions.delete(participantId);
|
|
816
|
+
}
|
|
817
|
+
this.participantTrackSids.delete(participantId);
|
|
818
|
+
});
|
|
819
|
+
room.on(import_rtc_node.RoomEvent.ParticipantDisconnected, (participant) => {
|
|
820
|
+
const participantId = this.getParticipantId(participant);
|
|
821
|
+
this.receiveSubscriptions.get(participantId)?.stop();
|
|
822
|
+
this.receiveSubscriptions.delete(participantId);
|
|
823
|
+
if (this.requestedSubscriptions.get(participantId) !== true) {
|
|
824
|
+
this.requestedSubscriptions.delete(participantId);
|
|
825
|
+
}
|
|
826
|
+
this.participantTrackSids.delete(participantId);
|
|
827
|
+
if (this.activeSpeakers.delete(participantId)) {
|
|
828
|
+
this.emit("speakerStop", { participantId });
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
room.on(import_rtc_node.RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
832
|
+
const next = new Set(speakers.map((speaker) => speaker.identity));
|
|
833
|
+
for (const participantId of next) {
|
|
834
|
+
if (!this.activeSpeakers.has(participantId)) {
|
|
835
|
+
this.emit("speakerStart", { participantId });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const participantId of this.activeSpeakers) {
|
|
839
|
+
if (!next.has(participantId)) {
|
|
840
|
+
this.emit("speakerStop", { participantId });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
this.activeSpeakers.clear();
|
|
844
|
+
for (const participantId of next) this.activeSpeakers.add(participantId);
|
|
845
|
+
});
|
|
846
|
+
await room.connect(url, token, { autoSubscribe: true, dynacast: false });
|
|
697
847
|
this.lastServerEndpoint = raw;
|
|
698
848
|
this.lastServerToken = token;
|
|
699
849
|
this.debug("connected to room");
|
|
@@ -1704,6 +1854,7 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
1704
1854
|
stop() {
|
|
1705
1855
|
this._playing = false;
|
|
1706
1856
|
this.stopVideo();
|
|
1857
|
+
this.clearReceiveSubscriptions();
|
|
1707
1858
|
if (this.currentStream?.destroy) this.currentStream.destroy();
|
|
1708
1859
|
this.currentStream = null;
|
|
1709
1860
|
if (this.audioTrack) {
|
|
@@ -1787,6 +1938,31 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
1787
1938
|
if (!guildMap) return null;
|
|
1788
1939
|
return guildMap.get(userId) ?? null;
|
|
1789
1940
|
}
|
|
1941
|
+
/**
|
|
1942
|
+
* List participant user IDs currently in a specific voice channel.
|
|
1943
|
+
*/
|
|
1944
|
+
listParticipantsInChannel(guildId, channelId) {
|
|
1945
|
+
const guildMap = this.voiceStates.get(guildId);
|
|
1946
|
+
if (!guildMap) return [];
|
|
1947
|
+
const participants = [];
|
|
1948
|
+
for (const [userId, voiceChannelId] of guildMap.entries()) {
|
|
1949
|
+
if (voiceChannelId === channelId) participants.push(userId);
|
|
1950
|
+
}
|
|
1951
|
+
return participants;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Subscribe to inbound audio for all known participants currently in a voice channel.
|
|
1955
|
+
* Only supported for LiveKit connections.
|
|
1956
|
+
*/
|
|
1957
|
+
subscribeChannelParticipants(channelId, opts) {
|
|
1958
|
+
const conn = this.connections.get(channelId);
|
|
1959
|
+
if (!(conn instanceof LiveKitRtcConnection)) return [];
|
|
1960
|
+
const guildId = conn.channel.guildId;
|
|
1961
|
+
const participants = this.listParticipantsInChannel(guildId, channelId).filter(
|
|
1962
|
+
(participantId) => participantId !== this.client.user?.id
|
|
1963
|
+
);
|
|
1964
|
+
return participants.map((participantId) => conn.subscribeParticipantAudio(participantId, opts));
|
|
1965
|
+
}
|
|
1790
1966
|
handleVoiceStateUpdate(data) {
|
|
1791
1967
|
const guildId = data.guild_id ?? "";
|
|
1792
1968
|
if (!guildId) return;
|
package/dist/index.mjs
CHANGED
|
@@ -365,6 +365,7 @@ var VoiceConnection = class extends EventEmitter {
|
|
|
365
365
|
import { execFile, spawn } from "child_process";
|
|
366
366
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
367
367
|
import {
|
|
368
|
+
AudioStream,
|
|
368
369
|
Room,
|
|
369
370
|
RoomEvent,
|
|
370
371
|
AudioSource,
|
|
@@ -375,7 +376,8 @@ import {
|
|
|
375
376
|
TrackSource,
|
|
376
377
|
VideoBufferType,
|
|
377
378
|
VideoFrame,
|
|
378
|
-
VideoSource
|
|
379
|
+
VideoSource,
|
|
380
|
+
TrackKind
|
|
379
381
|
} from "@livekit/rtc-node";
|
|
380
382
|
|
|
381
383
|
// src/livekit.ts
|
|
@@ -471,6 +473,7 @@ import { promisify } from "util";
|
|
|
471
473
|
import { createFile } from "mp4box";
|
|
472
474
|
var SAMPLE_RATE = 48e3;
|
|
473
475
|
var CHANNELS2 = 1;
|
|
476
|
+
var RECEIVE_READ_TIMEOUT_MS = 100;
|
|
474
477
|
function getNaluByteLength(nalu) {
|
|
475
478
|
if (ArrayBuffer.isView(nalu)) return nalu.byteLength;
|
|
476
479
|
if (nalu instanceof ArrayBuffer) return nalu.byteLength;
|
|
@@ -577,6 +580,10 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
577
580
|
lastServerEndpoint = null;
|
|
578
581
|
lastServerToken = null;
|
|
579
582
|
_disconnectEmitted = false;
|
|
583
|
+
receiveSubscriptions = /* @__PURE__ */ new Map();
|
|
584
|
+
requestedSubscriptions = /* @__PURE__ */ new Map();
|
|
585
|
+
participantTrackSids = /* @__PURE__ */ new Map();
|
|
586
|
+
activeSpeakers = /* @__PURE__ */ new Set();
|
|
580
587
|
/**
|
|
581
588
|
* @param client - The Fluxer client instance
|
|
582
589
|
* @param channel - The voice channel to connect to
|
|
@@ -627,6 +634,110 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
627
634
|
getVolume() {
|
|
628
635
|
return this._volume ?? 100;
|
|
629
636
|
}
|
|
637
|
+
isAudioTrack(track) {
|
|
638
|
+
return track.kind === TrackKind.KIND_AUDIO;
|
|
639
|
+
}
|
|
640
|
+
getParticipantId(participant) {
|
|
641
|
+
return participant.identity;
|
|
642
|
+
}
|
|
643
|
+
subscribeParticipantTrack(participant, track, options = {}) {
|
|
644
|
+
if (!this.isAudioTrack(track)) return;
|
|
645
|
+
const participantId = this.getParticipantId(participant);
|
|
646
|
+
if (!options.autoSubscribe && !this.requestedSubscriptions.has(participantId)) return;
|
|
647
|
+
const current = this.receiveSubscriptions.get(participantId);
|
|
648
|
+
if (current) current.stop();
|
|
649
|
+
const audioStream = new AudioStream(track, {
|
|
650
|
+
sampleRate: SAMPLE_RATE,
|
|
651
|
+
numChannels: CHANNELS2,
|
|
652
|
+
frameSizeMs: 10
|
|
653
|
+
});
|
|
654
|
+
let stopped = false;
|
|
655
|
+
let reader = null;
|
|
656
|
+
const pump = async () => {
|
|
657
|
+
try {
|
|
658
|
+
reader = audioStream.getReader();
|
|
659
|
+
while (!stopped) {
|
|
660
|
+
let readTimeout = null;
|
|
661
|
+
const next = await Promise.race([
|
|
662
|
+
reader.read(),
|
|
663
|
+
new Promise((resolve) => {
|
|
664
|
+
readTimeout = setTimeout(() => resolve(null), RECEIVE_READ_TIMEOUT_MS);
|
|
665
|
+
})
|
|
666
|
+
]);
|
|
667
|
+
if (readTimeout) clearTimeout(readTimeout);
|
|
668
|
+
if (next === null) continue;
|
|
669
|
+
const { done, value } = next;
|
|
670
|
+
if (done || !value) break;
|
|
671
|
+
this.emit("audioFrame", {
|
|
672
|
+
participantId,
|
|
673
|
+
trackSid: track.sid,
|
|
674
|
+
sampleRate: value.sampleRate,
|
|
675
|
+
channels: value.channels,
|
|
676
|
+
samples: value.data
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
} catch (err) {
|
|
680
|
+
if (!stopped) {
|
|
681
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
682
|
+
}
|
|
683
|
+
} finally {
|
|
684
|
+
if (reader) {
|
|
685
|
+
try {
|
|
686
|
+
reader.releaseLock();
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
reader = null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
const stop = () => {
|
|
694
|
+
if (stopped) return;
|
|
695
|
+
stopped = true;
|
|
696
|
+
if (reader) {
|
|
697
|
+
reader.cancel().catch(() => {
|
|
698
|
+
});
|
|
699
|
+
try {
|
|
700
|
+
reader.releaseLock();
|
|
701
|
+
} catch {
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
audioStream.cancel().catch(() => {
|
|
705
|
+
});
|
|
706
|
+
this.receiveSubscriptions.delete(participantId);
|
|
707
|
+
};
|
|
708
|
+
this.receiveSubscriptions.set(participantId, { participantId, stop });
|
|
709
|
+
this.participantTrackSids.set(participantId, track.sid ?? "");
|
|
710
|
+
void pump();
|
|
711
|
+
}
|
|
712
|
+
subscribeParticipantAudio(participantId, options = {}) {
|
|
713
|
+
const autoResubscribe = options.autoResubscribe === true;
|
|
714
|
+
const stop = () => {
|
|
715
|
+
this.receiveSubscriptions.get(participantId)?.stop();
|
|
716
|
+
this.receiveSubscriptions.delete(participantId);
|
|
717
|
+
this.participantTrackSids.delete(participantId);
|
|
718
|
+
this.requestedSubscriptions.delete(participantId);
|
|
719
|
+
};
|
|
720
|
+
this.requestedSubscriptions.set(participantId, autoResubscribe);
|
|
721
|
+
const room = this.room;
|
|
722
|
+
if (!room || !room.isConnected) return { participantId, stop };
|
|
723
|
+
const participant = room.remoteParticipants.get(participantId);
|
|
724
|
+
if (!participant) return { participantId, stop };
|
|
725
|
+
for (const pub of participant.trackPublications.values()) {
|
|
726
|
+
const maybeTrack = pub.track;
|
|
727
|
+
if (maybeTrack && this.isAudioTrack(maybeTrack)) {
|
|
728
|
+
this.subscribeParticipantTrack(participant, maybeTrack);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return { participantId, stop };
|
|
733
|
+
}
|
|
734
|
+
clearReceiveSubscriptions() {
|
|
735
|
+
for (const sub of this.receiveSubscriptions.values()) sub.stop();
|
|
736
|
+
this.receiveSubscriptions.clear();
|
|
737
|
+
this.requestedSubscriptions.clear();
|
|
738
|
+
this.participantTrackSids.clear();
|
|
739
|
+
this.activeSpeakers.clear();
|
|
740
|
+
}
|
|
630
741
|
playOpus(_stream) {
|
|
631
742
|
this.emit(
|
|
632
743
|
"error",
|
|
@@ -665,7 +776,48 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
665
776
|
room.on(RoomEvent.Reconnected, () => {
|
|
666
777
|
this.debug("Room reconnected");
|
|
667
778
|
});
|
|
668
|
-
|
|
779
|
+
room.on(RoomEvent.TrackSubscribed, (track, _publication, participant) => {
|
|
780
|
+
if (!this.isAudioTrack(track)) return;
|
|
781
|
+
this.subscribeParticipantTrack(participant, track);
|
|
782
|
+
});
|
|
783
|
+
room.on(RoomEvent.TrackUnsubscribed, (track, _publication, participant) => {
|
|
784
|
+
if (!this.isAudioTrack(track)) return;
|
|
785
|
+
const participantId = this.getParticipantId(participant);
|
|
786
|
+
this.receiveSubscriptions.get(participantId)?.stop();
|
|
787
|
+
this.receiveSubscriptions.delete(participantId);
|
|
788
|
+
if (this.requestedSubscriptions.get(participantId) !== true) {
|
|
789
|
+
this.requestedSubscriptions.delete(participantId);
|
|
790
|
+
}
|
|
791
|
+
this.participantTrackSids.delete(participantId);
|
|
792
|
+
});
|
|
793
|
+
room.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
794
|
+
const participantId = this.getParticipantId(participant);
|
|
795
|
+
this.receiveSubscriptions.get(participantId)?.stop();
|
|
796
|
+
this.receiveSubscriptions.delete(participantId);
|
|
797
|
+
if (this.requestedSubscriptions.get(participantId) !== true) {
|
|
798
|
+
this.requestedSubscriptions.delete(participantId);
|
|
799
|
+
}
|
|
800
|
+
this.participantTrackSids.delete(participantId);
|
|
801
|
+
if (this.activeSpeakers.delete(participantId)) {
|
|
802
|
+
this.emit("speakerStop", { participantId });
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
room.on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
|
|
806
|
+
const next = new Set(speakers.map((speaker) => speaker.identity));
|
|
807
|
+
for (const participantId of next) {
|
|
808
|
+
if (!this.activeSpeakers.has(participantId)) {
|
|
809
|
+
this.emit("speakerStart", { participantId });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
for (const participantId of this.activeSpeakers) {
|
|
813
|
+
if (!next.has(participantId)) {
|
|
814
|
+
this.emit("speakerStop", { participantId });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
this.activeSpeakers.clear();
|
|
818
|
+
for (const participantId of next) this.activeSpeakers.add(participantId);
|
|
819
|
+
});
|
|
820
|
+
await room.connect(url, token, { autoSubscribe: true, dynacast: false });
|
|
669
821
|
this.lastServerEndpoint = raw;
|
|
670
822
|
this.lastServerToken = token;
|
|
671
823
|
this.debug("connected to room");
|
|
@@ -1676,6 +1828,7 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1676
1828
|
stop() {
|
|
1677
1829
|
this._playing = false;
|
|
1678
1830
|
this.stopVideo();
|
|
1831
|
+
this.clearReceiveSubscriptions();
|
|
1679
1832
|
if (this.currentStream?.destroy) this.currentStream.destroy();
|
|
1680
1833
|
this.currentStream = null;
|
|
1681
1834
|
if (this.audioTrack) {
|
|
@@ -1759,6 +1912,31 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1759
1912
|
if (!guildMap) return null;
|
|
1760
1913
|
return guildMap.get(userId) ?? null;
|
|
1761
1914
|
}
|
|
1915
|
+
/**
|
|
1916
|
+
* List participant user IDs currently in a specific voice channel.
|
|
1917
|
+
*/
|
|
1918
|
+
listParticipantsInChannel(guildId, channelId) {
|
|
1919
|
+
const guildMap = this.voiceStates.get(guildId);
|
|
1920
|
+
if (!guildMap) return [];
|
|
1921
|
+
const participants = [];
|
|
1922
|
+
for (const [userId, voiceChannelId] of guildMap.entries()) {
|
|
1923
|
+
if (voiceChannelId === channelId) participants.push(userId);
|
|
1924
|
+
}
|
|
1925
|
+
return participants;
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Subscribe to inbound audio for all known participants currently in a voice channel.
|
|
1929
|
+
* Only supported for LiveKit connections.
|
|
1930
|
+
*/
|
|
1931
|
+
subscribeChannelParticipants(channelId, opts) {
|
|
1932
|
+
const conn = this.connections.get(channelId);
|
|
1933
|
+
if (!(conn instanceof LiveKitRtcConnection)) return [];
|
|
1934
|
+
const guildId = conn.channel.guildId;
|
|
1935
|
+
const participants = this.listParticipantsInChannel(guildId, channelId).filter(
|
|
1936
|
+
(participantId) => participantId !== this.client.user?.id
|
|
1937
|
+
);
|
|
1938
|
+
return participants.map((participantId) => conn.subscribeParticipantAudio(participantId, opts));
|
|
1939
|
+
}
|
|
1762
1940
|
handleVoiceStateUpdate(data) {
|
|
1763
1941
|
const guildId = data.guild_id ?? "";
|
|
1764
1942
|
if (!guildId) return;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.3",
|
|
7
7
|
"description": "Voice support for Fluxer bots",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prism-media": "^1.3.5",
|
|
40
40
|
"tweetnacl": "^1.0.3",
|
|
41
41
|
"ws": "^8.18.0",
|
|
42
|
-
"@fluxerjs/collection": "1.2.
|
|
43
|
-
"@fluxerjs/
|
|
44
|
-
"@fluxerjs/
|
|
42
|
+
"@fluxerjs/collection": "1.2.3",
|
|
43
|
+
"@fluxerjs/core": "1.2.3",
|
|
44
|
+
"@fluxerjs/types": "1.2.3"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^20.0.0",
|