import { createSocket , Socket } from 'node:dgram' ; import { isIPv4 } from 'node:net' ; import { TypedEmitter } from 'tiny-typed-emitter' ; import type { Awaited } from '../util/util' ; /**
 * Stores an IP address and port. Used to store socket details for the local client as well as
 * for Discord.
 */ export interface SocketConfig { ip : string ; port : number ; } interface KeepAlive { value : number ; timestamp : number ; } export interface VoiceUDPSocketEvents { error : ( error : Error ) => Awaited < void > ; close : ( ) => Awaited < void > ; debug : ( message : string ) => Awaited < void > ; message : ( message : Buffer ) => Awaited < void > ; } /**
 * The interval in milliseconds at which keep alive datagrams are sent.
 */ const KEEP_ALIVE_INTERVAL = 5e3 ; /**
 * The maximum number of keep alive packets which can be missed.
 */ const KEEP_ALIVE_LIMIT = 12 ; /**
 * The maximum value of the keep alive counter.
 */ const MAX_COUNTER_VALUE = 2 ** 32 - 1 ; /**
 * Manages the UDP networking for a voice connection.
 */ export class VoiceUDPSocket extends TypedEmitter < VoiceUDPSocketEvents > { /**
     * The underlying network Socket for the VoiceUDPSocket.
     */ private readonly socket : Socket ; /**
     * The socket details for Discord (remote)
     */ private readonly remote : SocketConfig ; /**
     * A list of keep alives that are waiting to be acknowledged.
     */ private readonly keepAlives : KeepAlive [ ] ; /**
     * The counter used in the keep alive mechanism.
     */ private keepAliveCounter = 0 ; /**
     * The buffer used to write the keep alive counter into.
     */ private readonly keepAliveBuffer : Buffer ; /**
     * The Node.js interval for the keep-alive mechanism.
     */ private readonly keepAliveInterval : NodeJS . Timeout ; /**
     * The time taken to receive a response to keep alive messages.
     */ public ping ? : number ; /**
     * The debug logger function, if debugging is enabled.
     */ private readonly debug : null | ( ( message : string ) => void ) ; param remote public constructor ( remote , debug = false ) { super ( ) ; this . socket = createSocket ( 'udp4' ) ; this . socket . on ( 'error' , ( error ) => this . emit ( 'error' , error ) ) ; this . socket . on ( 'message' , ( buffer ) => this . onMessage ( buffer ) ) ; this . socket . on ( 'close' , ( ) => this . emit ( 'close' ) ) ; this . remote = remote ; this . keepAlives = [ ] ; this . keepAliveBuffer = Buffer . alloc ( 8 ) ; this . keepAliveInterval = setInterval ( ( ) => this . keepAlive ( ) , KEEP_ALIVE_INTERVAL ) ; setImmediate ( ( ) => this . keepAlive ( ) ) ; this . debug = debug ? ( message ) => this . emit ( 'debug' , message ) : null ; } param buffer private onMessage ( buffer ) { if ( buffer . length === 8 ) { const counter = buffer . readUInt32LE ( 0 ) ; const index = this . keepAlives . findIndex ( ( { value } ) => value === counter ) ; if ( index === - 1 ) return ; this . ping = Date . now ( ) - this . keepAlives [ index ] . timestamp ; this . keepAlives . splice ( 0 , index ) ; } this . emit ( 'message' , buffer ) ; } /**
     * Called at a regular interval to check whether we are still able to send datagrams to Discord.
     */ private keepAlive ( ) { if ( this . keepAlives . length >= KEEP_ALIVE_LIMIT ) { this . debug ?. ( 'UDP socket has not received enough responses from Discord - closing socket' ) ; this . destroy ( ) ; return ; } this . keepAliveBuffer . writeUInt32LE ( this . keepAliveCounter , 0 ) ; this . send ( this . keepAliveBuffer ) ; this . keepAlives . push ( { value : this . keepAliveCounter , timestamp : Date . now ( ) , } ) ; this . keepAliveCounter ++ ; if ( this . keepAliveCounter > MAX_COUNTER_VALUE ) { this . keepAliveCounter = 0 ; } } param buffer public send ( buffer ) { return this . socket . send ( buffer , this . remote . port , this . remote . ip ) ; } /**
     * Closes the socket, the instance will not be able to be reused.
     */ public destroy ( ) { try { this . socket . close ( ) ; } catch { } clearInterval ( this . keepAliveInterval ) ; } param ssrc public performIPDiscovery ( ssrc ) { return new Promise ( ( resolve , reject ) => { const listener = ( message ) => { try { if ( message . readUInt16BE ( 0 ) !== 2 ) return ; const packet = parseLocalPacket ( message ) ; this . socket . off ( 'message' , listener ) ; resolve ( packet ) ; } catch { } } ; this . socket . on ( 'message' , listener ) ; this . socket . once ( 'close' , ( ) => reject ( new Error ( 'Cannot perform IP discovery - socket closed' ) ) ) ; const discoveryBuffer = Buffer . alloc ( 74 ) ; discoveryBuffer . writeUInt16BE ( 1 , 0 ) ; discoveryBuffer . writeUInt16BE ( 70 , 2 ) ; discoveryBuffer . writeUInt32BE ( ssrc , 4 ) ; this . send ( discoveryBuffer ) ; } ) ; } } param message export function parseLocalPacket ( message ) { const packet = Buffer . from ( message ) ; const ip = packet . slice ( 8 , packet . indexOf ( 0 , 8 ) ) . toString ( 'utf-8' ) ; if ( ! isIPv4 ( ip ) ) { throw new Error ( 'Malformed IP address' ) ; } const port = packet . readUInt16BE ( packet . length - 2 ) ; return { ip , port } ; }