nodelink / src /connection /voiceHandler.js
flameface's picture
Upload 25 files
b58c6cb verified
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