import type { GatewayVoiceServerUpdateDispatchData , GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v9' ; import type { CreateVoiceConnectionOptions } from '.' ; import type { AudioPlayer } from './audio/AudioPlayer' ; import type { PlayerSubscription } from './audio/PlayerSubscription' ; import { getVoiceConnection , createJoinVoiceChannelPayload , trackVoiceConnection , JoinConfig , untrackVoiceConnection , } from './DataStore' ; import type { DiscordGatewayAdapterImplementerMethods } from './util/adapter' ; import { Networking , NetworkingState , NetworkingStatusCode } from './networking/Networking' ; import { Awaited , noop } from './util/util' ; import { TypedEmitter } from 'tiny-typed-emitter' ; import { VoiceReceiver } from './receive' ; import type { VoiceWebSocket , VoiceUDPSocket } from './networking' ; /**
 * The various status codes a voice connection can hold at any one time.
 */ export enum VoiceConnectionStatus { /**
     * Sending a packet to the main Discord gateway to indicate we want to change our voice state.
     */ Signalling = 'signalling' , /**
     * The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection.
     */ Connecting = 'connecting' , /**
     * A voice connection has been established, and is ready to be used.
     */ Ready = 'ready' , /**
     * The voice connection has either been severed or not established.
     */ Disconnected = 'disconnected' , /**
     * The voice connection has been destroyed and untracked, it cannot be reused.
     */ Destroyed = 'destroyed' } /**
 * The state that a VoiceConnection will be in when it is waiting to receive a VOICE_SERVER_UPDATE and
 * VOICE_STATE_UPDATE packet from Discord, provided by the adapter.
 */ export interface VoiceConnectionSignallingState { status : VoiceConnectionStatus . Signalling ; subscription ? : PlayerSubscription ; adapter : DiscordGatewayAdapterImplementerMethods ; } /**
 * The reasons a voice connection can be in the disconnected state.
 */ export enum VoiceConnectionDisconnectReason { /**
     * When the WebSocket connection has been closed.
     */ WebSocketClose , /**
     * When the adapter was unable to send a message requested by the VoiceConnection.
     */ AdapterUnavailable , /**
     * When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed.
     */ EndpointRemoved , /**
     * When a manual disconnect was requested.
     */ Manual } /**
 * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is
 * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.
 */ export interface VoiceConnectionDisconnectedBaseState { status : VoiceConnectionStatus . Disconnected ; subscription ? : PlayerSubscription ; adapter : DiscordGatewayAdapterImplementerMethods ; } /**
 * The state that a VoiceConnection will be in when it is not connected to a Discord voice server nor is
 * it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.
 */ export interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDisconnectedBaseState { reason : Exclude < VoiceConnectionDisconnectReason , VoiceConnectionDisconnectReason . WebSocketClose > ; } /**
 * The state that a VoiceConnection will be in when its WebSocket connection was closed.
 * You can manually attempt to reconnect using VoiceConnection#reconnect.
 */ export interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState { reason : VoiceConnectionDisconnectReason . WebSocketClose ; /**
     * The close code of the WebSocket connection to the Discord voice server.
     */ closeCode : number ; } /**
 * The states that a VoiceConnection can be in when it is not connected to a Discord voice server nor is
 * it attempting to connect. You can manually attempt to connect using VoiceConnection#reconnect.
 */ export type VoiceConnectionDisconnectedState = VoiceConnectionDisconnectedOtherState | VoiceConnectionDisconnectedWebSocketState ; /**
 * The state that a VoiceConnection will be in when it is establishing a connection to a Discord
 * voice server.
 */ export interface VoiceConnectionConnectingState { status : VoiceConnectionStatus . Connecting ; networking : Networking ; subscription ? : PlayerSubscription ; adapter : DiscordGatewayAdapterImplementerMethods ; } /**
 * The state that a VoiceConnection will be in when it has an active connection to a Discord
 * voice server.
 */ export interface VoiceConnectionReadyState { status : VoiceConnectionStatus . Ready ; networking : Networking ; subscription ? : PlayerSubscription ; adapter : DiscordGatewayAdapterImplementerMethods ; } /**
 * The state that a VoiceConnection will be in when it has been permanently been destroyed by the
 * user and untracked by the library. It cannot be reconnected, instead, a new VoiceConnection
 * needs to be established.
 */ export interface VoiceConnectionDestroyedState { status : VoiceConnectionStatus . Destroyed ; } /**
 * The various states that a voice connection can be in.
 */ export type VoiceConnectionState = VoiceConnectionSignallingState | VoiceConnectionDisconnectedState | VoiceConnectionConnectingState | VoiceConnectionReadyState | VoiceConnectionDestroyedState ; export type VoiceConnectionEvents = { error : ( error : Error ) => Awaited < void > ; debug : ( message : string ) => Awaited < void > ; stateChange : ( oldState : VoiceConnectionState , newState : VoiceConnectionState ) => Awaited < void > ; } & { [ status in VoiceConnectionStatus ] : ( oldState : VoiceConnectionState , newState : VoiceConnectionState & { status : status ; } ) => Awaited < void > ; } ; /**
 * A connection to the voice server of a Guild, can be used to play audio in voice channels.
 */ export class VoiceConnection extends TypedEmitter < VoiceConnectionEvents > { /**
     * The number of consecutive rejoin attempts. Initially 0, and increments for each rejoin.
     * When a connection is successfully established, it resets to 0.
     */ public rejoinAttempts : number ; /**
     * The state of the voice connection.
     */ private _state : VoiceConnectionState ; internal public readonly joinConfig : JoinConfig ; /**
     * The two packets needed to successfully establish a voice connection. They are received
     * from the main Discord gateway after signalling to change the voice state.
     */ private readonly packets : { server : GatewayVoiceServerUpdateDispatchData | undefined ; state : GatewayVoiceStateUpdateDispatchData | undefined ; } ; /**
     * The receiver of this voice connection. You should join the voice channel with `selfDeaf` set
     * to false for this feature to work properly.
     */ public readonly receiver : VoiceReceiver ; /**
     * The debug logger function, if debugging is enabled.
     */ private readonly debug : null | ( ( message : string ) => void ) ; param joinConfig param options public constructor ( joinConfig , { debug , adapterCreator } ) { super ( ) ; this . debug = debug ? ( message ) => this . emit ( 'debug' , message ) : null ; this . rejoinAttempts = 0 ; this . receiver = new VoiceReceiver ( this ) ; this . onNetworkingClose = this . onNetworkingClose . bind ( this ) ; this . onNetworkingStateChange = this . onNetworkingStateChange . bind ( this ) ; this . onNetworkingError = this . onNetworkingError . bind ( this ) ; this . onNetworkingDebug = this . onNetworkingDebug . bind ( this ) ; const adapter = adapterCreator ( { onVoiceServerUpdate : ( data ) => this . addServerPacket ( data ) , onVoiceStateUpdate : ( data ) => this . addStatePacket ( data ) , destroy : ( ) => this . destroy ( false ) , } ) ; this . _state = { status : VoiceConnectionStatus . Signalling , adapter } ; this . packets = { server : undefined , state : undefined , } ; this . joinConfig = joinConfig ; } /**
     * The current state of the voice connection.
     */ public get state ( ) { return this . _state ; } /**
     * Updates the state of the voice connection, performing clean-up operations where necessary.
     */ public set state ( newState ) { const oldState = this . _state ; const oldNetworking = Reflect . get ( oldState , 'networking' ) ; const newNetworking = Reflect . get ( newState , 'networking' ) ; const oldSubscription = Reflect . get ( oldState , 'subscription' ) ; const newSubscription = Reflect . get ( newState , 'subscription' ) ; if ( oldNetworking !== newNetworking ) { if ( oldNetworking ) { oldNetworking . on ( 'error' , noop ) ; oldNetworking . off ( 'debug' , this . onNetworkingDebug ) ; oldNetworking . off ( 'error' , this . onNetworkingError ) ; oldNetworking . off ( 'close' , this . onNetworkingClose ) ; oldNetworking . off ( 'stateChange' , this . onNetworkingStateChange ) ; oldNetworking . destroy ( ) ; } if ( newNetworking ) this . updateReceiveBindings ( newNetworking . state , oldNetworking ?. state ) ; } if ( newState . status === VoiceConnectionStatus . Ready ) { this . rejoinAttempts = 0 ; } else if ( newState . status === VoiceConnectionStatus . Destroyed ) { for ( const stream of this . receiver . subscriptions . values ( ) ) { if ( ! stream . destroyed ) stream . destroy ( ) ; } } if ( oldState . status !== VoiceConnectionStatus . Destroyed && newState . status === VoiceConnectionStatus . Destroyed ) { oldState . adapter . destroy ( ) ; } this . _state = newState ; if ( oldSubscription && oldSubscription !== newSubscription ) { oldSubscription . unsubscribe ( ) ; } this . emit ( 'stateChange' , oldState , newState ) ; if ( oldState . status !== newState . status ) { this . emit ( newState . status , oldState , newState as any ) ; } } param packet private addServerPacket ( packet ) { this . packets . server = packet ; if ( packet . endpoint ) { this . configureNetworking ( ) ; } else if ( this . state . status !== VoiceConnectionStatus . Destroyed ) { this . state = { ... this . state , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . EndpointRemoved , } ; } } param packet private addStatePacket ( packet ) { this . packets . state = packet ; if ( typeof packet . self_deaf !== 'undefined' ) this . joinConfig . selfDeaf = packet . self_deaf ; if ( typeof packet . self_mute !== 'undefined' ) this . joinConfig . selfMute = packet . self_mute ; if ( packet . channel_id ) this . joinConfig . channelId = packet . channel_id ; } param newState param oldState private updateReceiveBindings ( newState , oldState ? ) { const oldWs = Reflect . get ( oldState ?? { } , 'ws' ) as VoiceWebSocket | undefined ; const newWs = Reflect . get ( newState , 'ws' ) as VoiceWebSocket | undefined ; const oldUdp = Reflect . get ( oldState ?? { } , 'udp' ) as VoiceUDPSocket | undefined ; const newUdp = Reflect . get ( newState , 'udp' ) as VoiceUDPSocket | undefined ; if ( oldWs !== newWs ) { oldWs ?. off ( 'packet' , this . receiver . onWsPacket ) ; newWs ?. on ( 'packet' , this . receiver . onWsPacket ) ; } if ( oldUdp !== newUdp ) { oldUdp ?. off ( 'message' , this . receiver . onUdpMessage ) ; newUdp ?. on ( 'message' , this . receiver . onUdpMessage ) ; } this . receiver . connectionData = Reflect . get ( newState , 'connectionData' ) ?? { } ; } remarks public configureNetworking ( ) { const { server , state } = this . packets ; if ( ! server || ! state || this . state . status === VoiceConnectionStatus . Destroyed || ! server . endpoint ) return ; const networking = new Networking ( { endpoint : server . endpoint , serverId : server . guild_id , token : server . token , sessionId : state . session_id , userId : state . user_id , } , Boolean ( this . debug ) ) ; networking . once ( 'close' , this . onNetworkingClose ) ; networking . on ( 'stateChange' , this . onNetworkingStateChange ) ; networking . on ( 'error' , this . onNetworkingError ) ; networking . on ( 'debug' , this . onNetworkingDebug ) ; this . state = { ... this . state , status : VoiceConnectionStatus . Connecting , networking , } ; } remarks param code private onNetworkingClose ( code ) { if ( this . state . status === VoiceConnectionStatus . Destroyed ) return ; if ( code === 4014 ) { this . state = { ... this . state , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . WebSocketClose , closeCode : code , } ; } else { this . state = { ... this . state , status : VoiceConnectionStatus . Signalling , } ; this . rejoinAttempts ++ ; if ( ! this . state . adapter . sendPayload ( createJoinVoiceChannelPayload ( this . joinConfig ) ) ) { this . state = { ... this . state , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . AdapterUnavailable , } ; } } } param oldState param newState private onNetworkingStateChange ( oldState , newState ) { this . updateReceiveBindings ( newState , oldState ) ; if ( oldState . code === newState . code ) return ; if ( this . state . status !== VoiceConnectionStatus . Connecting && this . state . status !== VoiceConnectionStatus . Ready ) return ; if ( newState . code === NetworkingStatusCode . Ready ) { this . state = { ... this . state , status : VoiceConnectionStatus . Ready , } ; } else if ( newState . code !== NetworkingStatusCode . Closed ) { this . state = { ... this . state , status : VoiceConnectionStatus . Connecting , } ; } } param error private onNetworkingError ( error ) { this . emit ( 'error' , error ) ; } param message private onNetworkingDebug ( message ) { this . debug ?. ( `[NW] ${ message }` ) ; } param buffer public prepareAudioPacket ( buffer ) { const state = this . state ; if ( state . status !== VoiceConnectionStatus . Ready ) return ; return state . networking . prepareAudioPacket ( buffer ) ; } /**
     * Dispatches the previously prepared audio packet (if any)
     */ public dispatchAudio ( ) { const state = this . state ; if ( state . status !== VoiceConnectionStatus . Ready ) return ; return state . networking . dispatchAudio ( ) ; } param buffer public playOpusPacket ( buffer ) { const state = this . state ; if ( state . status !== VoiceConnectionStatus . Ready ) return ; state . networking . prepareAudioPacket ( buffer ) ; return state . networking . dispatchAudio ( ) ; } param adapterAvailable public destroy ( adapterAvailable = true ) { if ( this . state . status === VoiceConnectionStatus . Destroyed ) { throw new Error ( 'Cannot destroy VoiceConnection - it has already been destroyed' ) ; } if ( getVoiceConnection ( this . joinConfig . guildId ) === this ) { untrackVoiceConnection ( this ) ; } if ( adapterAvailable ) { this . state . adapter . sendPayload ( createJoinVoiceChannelPayload ( { ... this . joinConfig , channelId : null } ) ) ; } this . state = { status : VoiceConnectionStatus . Destroyed , } ; } returns public disconnect ( ) { if ( this . state . status === VoiceConnectionStatus . Destroyed || this . state . status === VoiceConnectionStatus . Signalling ) { return false ; } this . joinConfig . channelId = null ; if ( ! this . state . adapter . sendPayload ( createJoinVoiceChannelPayload ( this . joinConfig ) ) ) { this . state = { adapter : this . state . adapter , subscription : this . state . subscription , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . AdapterUnavailable , } ; return false ; } this . state = { adapter : this . state . adapter , reason : VoiceConnectionDisconnectReason . Manual , status : VoiceConnectionStatus . Disconnected , } ; return true ; } remarks public rejoin ( joinConfig ? ) { if ( this . state . status === VoiceConnectionStatus . Destroyed ) { return false ; } const notReady = this . state . status !== VoiceConnectionStatus . Ready ; if ( notReady ) this . rejoinAttempts ++ ; Object . assign ( this . joinConfig , joinConfig ) ; if ( this . state . adapter . sendPayload ( createJoinVoiceChannelPayload ( this . joinConfig ) ) ) { if ( notReady ) { this . state = { ... this . state , status : VoiceConnectionStatus . Signalling , } ; } return true ; } this . state = { adapter : this . state . adapter , subscription : this . state . subscription , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . AdapterUnavailable , } ; return false ; } param enabled public setSpeaking ( enabled ) { if ( this . state . status !== VoiceConnectionStatus . Ready ) return false ; return this . state . networking . setSpeaking ( enabled ) ; } param player returns public subscribe ( player ) { if ( this . state . status === VoiceConnectionStatus . Destroyed ) return ; const subscription = player [ 'subscribe' ] ( this ) ; this . state = { ... this . state , subscription , } ; return subscription ; } remarks public get ping ( ) { if ( this . state . status === VoiceConnectionStatus . Ready && this . state . networking . state . code === NetworkingStatusCode . Ready ) { return { ws : this . state . networking . state . ws . ping , udp : this . state . networking . state . udp . ping , } ; } return { ws : undefined , udp : undefined , } ; } param subscription private onSubscriptionRemoved ( subscription ) { if ( this . state . status !== VoiceConnectionStatus . Destroyed && this . state . subscription === subscription ) { this . state = { ... this . state , subscription : undefined , } ; } } } param joinConfig param options export function createVoiceConnection ( joinConfig , options ) { const payload = createJoinVoiceChannelPayload ( joinConfig ) ; const existing = getVoiceConnection ( joinConfig . guildId ) ; if ( existing && existing . state . status !== VoiceConnectionStatus . Destroyed ) { if ( existing . state . status === VoiceConnectionStatus . Disconnected ) { existing . rejoin ( { channelId : joinConfig . channelId , selfDeaf : joinConfig . selfDeaf , selfMute : joinConfig . selfMute , } ) ; } else if ( ! existing . state . adapter . sendPayload ( payload ) ) { existing . state = { ... existing . state , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . AdapterUnavailable , } ; } return existing ; } const voiceConnection = new VoiceConnection ( joinConfig , options ) ; trackVoiceConnection ( voiceConnection ) ; if ( voiceConnection . state . status !== VoiceConnectionStatus . Destroyed ) { if ( ! voiceConnection . state . adapter . sendPayload ( payload ) ) { voiceConnection . state = { ... voiceConnection . state , status : VoiceConnectionStatus . Disconnected , reason : VoiceConnectionDisconnectReason . AdapterUnavailable , } ; } } return voiceConnection ; }