Spaces:
Runtime error
Runtime error
| import { debugLog, waitForEvent } from '../utils.js' | |
| import config from '../../config.js' | |
| import constants from '../../constants.js' | |
| import sources from '../sources.js' | |
| import Filters from '../filters.js' | |
| import inputHandler from './inputHandler.js' | |
| import voiceUtils from '../voice/utils.js' | |
| import discordVoice from '@performanc/voice' | |
| globalThis.nodelinkPlayersCount = 0 | |
| globalThis.nodelinkPlayingPlayersCount = 0 | |
| class VoiceConnection { | |
| constructor(guildId, client) { | |
| nodelinkPlayersCount++ | |
| this.client = { | |
| userId: client.userId, | |
| ws: client.ws | |
| } | |
| this.cache = { | |
| url: null, | |
| protocol: null, | |
| track: null | |
| } | |
| this.config = { | |
| guildId, | |
| track: null, | |
| volume: 100, | |
| paused: false, | |
| filters: {}, | |
| voice: { | |
| token: null, | |
| endpoint: null, | |
| sessionId: null | |
| } | |
| } | |
| this._setupVoice() | |
| } | |
| _setupVoice() { | |
| this.connection = discordVoice.joinVoiceChannel({ guildId: this.config.guildId, userId: this.client.userId, encryption: config.audio.encryption }) | |
| this.connection.on('speakStart', (userId, ssrc) => inputHandler.handleStartSpeaking(ssrc, userId, this.config.guildId)) | |
| this.connection.on('stateChange', async (oldState, newState) => { | |
| switch (newState.status) { | |
| case 'disconnected': { | |
| debugLog('websocketClosed', 2, { track: this.config.track?.info, exception: constants.VoiceWSCloseCodes[newState.closeCode] }) | |
| this.connection.destroy() | |
| this.connection = null | |
| this._stopTrack() | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'WebSocketClosedEvent', | |
| guildId: this.config.guildId, | |
| code: newState.code, | |
| reason: constants.VoiceWSCloseCodes[newState.code], | |
| byRemote: true | |
| })) | |
| break | |
| } | |
| } | |
| }) | |
| this.connection.on('playerStateChange', (_oldState, newState) => { | |
| if (newState.status === 'idle' && [ 'stopped', 'finished' ].includes(newState.reason)) { | |
| nodelinkPlayingPlayersCount-- | |
| debugLog('trackEnd', 2, { track: this.config.track.info, reason: newState.reason }) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackEndEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track, | |
| reason: newState.reason | |
| })) | |
| this._stopTrack() | |
| } | |
| if (newState.status === 'playing' && newState.reason === 'requested') { | |
| nodelinkPlayingPlayersCount++ | |
| debugLog('trackStart', 2, { track: this.config.track.info }) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackStartEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track | |
| })) | |
| } | |
| }) | |
| this.connection.on('error', (error) => { | |
| debugLog('trackException', 2, { track: this.config.track?.info, exception: error.message }) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackExceptionEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track, | |
| exception: { | |
| message: error.message, | |
| severity: 'fault', | |
| cause: `${error.name}: ${error.message}` | |
| } | |
| })) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackEndEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track, | |
| reason: 'loadFailed' | |
| })) | |
| this._stopTrack() | |
| }) | |
| } | |
| _stopTrack() { | |
| this.cache = { | |
| url: null, | |
| protocol: null, | |
| track: null | |
| } | |
| this.config = { | |
| ...this.config, | |
| track: null, | |
| paused: false | |
| } | |
| } | |
| _getRealTime() { | |
| return this.connection.statistics.packetsExpected * 20 | |
| } | |
| updateVoice(buffer) { | |
| this.config.voice = buffer | |
| if (!this.connection) this._setupVoice() | |
| this.connection.voiceStateUpdate({ | |
| session_id: buffer.sessionId | |
| }) | |
| this.connection.voiceServerUpdate({ | |
| token: buffer.token, | |
| endpoint: buffer.endpoint | |
| }) | |
| if (!this.connection.ws) this.connection.connect() | |
| } | |
| destroy() { | |
| if (this.connection) { | |
| this.connection.destroy() | |
| this.connection = null | |
| } | |
| this._stopTrack() | |
| } | |
| async getResource(decodedTrack, urlInfo) { | |
| const streamInfo = await sources.getTrackStream(decodedTrack, urlInfo.url, urlInfo.protocol, urlInfo.additionalData) | |
| if (streamInfo.exception) return streamInfo | |
| return { stream: voiceUtils.createAudioResource(streamInfo.stream, urlInfo.format) } | |
| } | |
| async play(track, decodedTrack, noReplace) { | |
| if (noReplace && this.config.track) return this.config | |
| const urlInfo = await sources.getTrackURL(decodedTrack) | |
| if (urlInfo.exception) { | |
| this._stopTrack() | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackExceptionEvent', | |
| guildId: this.config.guildId, | |
| track: { | |
| encoded: track, | |
| info: decodedTrack | |
| }, | |
| exception: urlInfo.exception | |
| })) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackEndEvent', | |
| guildId: this.config.guildId, | |
| track: { | |
| encoded: track, | |
| info: decodedTrack, | |
| userData: this.config.track?.userData | |
| }, | |
| reason: 'loadFailed' | |
| })) | |
| return this.config | |
| } | |
| if (this.config.track?.encoded) { | |
| debugLog('trackEnd', 2, { track: this.config.track.info, reason: 'replaced' }) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackEndEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track, | |
| reason: 'replaced' | |
| })) | |
| debugLog('trackStart', 2, { track: decodedTrack }) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackStartEvent', | |
| guildId: this.config.guildId, | |
| track: { | |
| encoded: track, | |
| info: decodedTrack, | |
| userData: this.config.track?.userData | |
| } | |
| })) | |
| } | |
| let resource = null | |
| if (Object.keys(this.config.filters).length > 0) { | |
| const filter = new Filters() | |
| this.config.filters = filter.configure(this.config.filters) | |
| resource = await filter.getResource(decodedTrack, urlInfo.protocol, urlInfo.url, null, null, this.cache.ffmpeg, urlInfo.additionalData) | |
| } else { | |
| resource = await this.getResource(decodedTrack, urlInfo) | |
| } | |
| if (resource.exception) { | |
| this._stopTrack() | |
| debugLog('trackException', 2, { track: decodedTrack, exception: resource.exception.message }) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackExceptionEvent', | |
| guildId: this.config.guildId, | |
| track: { | |
| encoded: track, | |
| info: decodedTrack, | |
| userData: this.config.track?.userData | |
| }, | |
| exception: resource.exception | |
| })) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackEndEvent', | |
| guildId: this.config.guildId, | |
| track: { | |
| encoded: track, | |
| info: decodedTrack, | |
| userData: this.config.track?.userData | |
| }, | |
| reason: 'loadFailed' | |
| })) | |
| return this.config | |
| } | |
| this.cache.url = urlInfo.url | |
| this.cache.protocol = urlInfo.protocol | |
| this.config.track = { encoded: track, info: decodedTrack } | |
| this.config.paused = false | |
| if (this.config.volume !== 100) | |
| resource.stream.setVolume(this.config.volume / 100) | |
| if (!this.connection) | |
| return this.config | |
| if (!this.connection.udpInfo?.secretKey) | |
| await waitForEvent(this.connection, 'stateChange', (_oldState, newState) => newState.status === 'connected', config.options.threshold || undefined) | |
| const oldResource = this.connection.audioStream | |
| this.connection.play(resource.stream) | |
| if (oldResource) | |
| resource.stream.once('readable', () => oldResource.destroy()) | |
| return this.config | |
| } | |
| stop() { | |
| if (!this.config.track) return this.config | |
| if (this.connection.audioStream) this.connection.stop() | |
| else this._stopTrack() | |
| } | |
| volume(volume) { | |
| if (this.connection.audioStream) | |
| this.connection.audioStream.setVolume(volume / 100) | |
| this.config.volume = volume | |
| return this.config | |
| } | |
| pause(pause) { | |
| if (this.connection.audioStream) { | |
| if (pause) this.connection.pause() | |
| else this.connection.unpause() | |
| } | |
| this.config.paused = pause | |
| return this.config | |
| } | |
| async filters(filters) { | |
| if (!this.config.track?.encoded || !config.filters.enabled) return this.config | |
| const filter = new Filters() | |
| this.config.filters = filter.configure(filters, this.config.track.info) | |
| if (!this.config.track) return this.config | |
| const resource = await filter.getResource(this.config.track.info, this.cache.protocol, this.cache.url, this._getRealTime(), filters.endTime, null, null) | |
| if (resource.exception) { | |
| this._stopTrack() | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackExceptionEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track, | |
| exception: resource.exception | |
| })) | |
| this.client.ws.send(JSON.stringify({ | |
| op: 'event', | |
| type: 'TrackEndEvent', | |
| guildId: this.config.guildId, | |
| track: this.config.track, | |
| reason: 'loadFailed' | |
| })) | |
| return this.config | |
| } | |
| resource.stream.setVolume(filters.volume || (this.config.volume / 100)) | |
| this.config.volume = (filters.volume * 100) || this.config.volume | |
| if (!this.connection) | |
| return this.config | |
| if (!this.connection.udpInfo?.secretKey) | |
| await waitForEvent(this.connection, 'stateChange', (_oldState, newState) => newState.status === 'connected', config.options.threshold || undefined) | |
| this.connection.play(resource.stream) | |
| return this.config | |
| } | |
| } | |
| export default VoiceConnection | |