import { addAudioPlayer , deleteAudioPlayer } from '../DataStore' ; import { Awaited , noop } from '../util/util' ; import { VoiceConnection , VoiceConnectionStatus } from '../VoiceConnection' ; import { AudioPlayerError } from './AudioPlayerError' ; import type { AudioResource } from './AudioResource' ; import { PlayerSubscription } from './PlayerSubscription' ; import { TypedEmitter } from 'tiny-typed-emitter' ; export const SILENCE_FRAME = Buffer . from ( [ 0xf8 , 0xff , 0xfe ] ) ; /**
 * Describes the behavior of the player when an audio packet is played but there are no available
 * voice connections to play to.
 */ export enum NoSubscriberBehavior { /**
     * Pauses playing the stream until a voice connection becomes available.
     */ Pause = 'pause' , /**
     * Continues to play through the resource regardless.
     */ Play = 'play' , /**
     * The player stops and enters the Idle state.
     */ Stop = 'stop' } export enum AudioPlayerStatus { /**
     * When there is currently no resource for the player to be playing.
     */ Idle = 'idle' , /**
     * When the player is waiting for an audio resource to become readable before transitioning to Playing.
     */ Buffering = 'buffering' , /**
     * When the player has been manually paused.
     */ Paused = 'paused' , /**
     * When the player is actively playing an audio resource.
     */ Playing = 'playing' , /**
     * When the player has paused itself. Only possible with the "pause" no subscriber behavior.
     */ AutoPaused = 'autopaused' } /**
 * Options that can be passed when creating an audio player, used to specify its behavior.
 */ export interface CreateAudioPlayerOptions { debug ? : boolean ; behaviors ? : { noSubscriber ? : NoSubscriberBehavior ; maxMissedFrames ? : number ; } ; } /**
 * The state that an AudioPlayer is in when it has no resource to play. This is the starting state.
 */ export interface AudioPlayerIdleState { status : AudioPlayerStatus . Idle ; } /**
 * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this
 * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then
 * it will re-enter the Idle state.
 */ export interface AudioPlayerBufferingState { status : AudioPlayerStatus . Buffering ; /**
     * The resource that the AudioPlayer is waiting for
     */ resource : AudioResource ; onReadableCallback : ( ) => void ; onFailureCallback : ( ) => void ; onStreamError : ( error : Error ) => void ; } /**
 * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends,
 * it will enter the Idle state.
 */ export interface AudioPlayerPlayingState { status : AudioPlayerStatus . Playing ; /**
     * The number of consecutive times that the audio resource has been unable to provide an Opus frame.
     */ missedFrames : number ; /**
     * The playback duration in milliseconds of the current audio resource. This includes filler silence packets
     * that have been played when the resource was buffering.
     */ playbackDuration : number ; /**
     * The resource that is being played.
     */ resource : AudioResource ; onStreamError : ( error : Error ) => void ; } /**
 * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done
 * automatically by the AudioPlayer itself if there are no available subscribers.
 */ export interface AudioPlayerPausedState { status : AudioPlayerStatus . Paused | AudioPlayerStatus . AutoPaused ; /**
     * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing.
     */ silencePacketsRemaining : number ; /**
     * The playback duration in milliseconds of the current audio resource. This includes filler silence packets
     * that have been played when the resource was buffering.
     */ playbackDuration : number ; /**
     * The current resource of the audio player.
     */ resource : AudioResource ; onStreamError : ( error : Error ) => void ; } /**
 * The various states that the player can be in.
 */ export type AudioPlayerState = AudioPlayerIdleState | AudioPlayerBufferingState | AudioPlayerPlayingState | AudioPlayerPausedState ; export type AudioPlayerEvents = { error : ( error : AudioPlayerError ) => Awaited < void > ; debug : ( message : string ) => Awaited < void > ; stateChange : ( oldState : AudioPlayerState , newState : AudioPlayerState ) => Awaited < void > ; subscribe : ( subscription : PlayerSubscription ) => Awaited < void > ; unsubscribe : ( subscription : PlayerSubscription ) => Awaited < void > ; } & { [ status in AudioPlayerStatus ] : ( oldState : AudioPlayerState , newState : AudioPlayerState & { status : status ; } ) => Awaited < void > ; } ; remarks export class AudioPlayer extends TypedEmitter < AudioPlayerEvents > { /**
     * The state that the AudioPlayer is in.
     */ private _state : AudioPlayerState ; /**
     * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
     * to the streams in this list.
     */ private readonly subscribers : PlayerSubscription [ ] = [ ] ; /**
     * The behavior that the player should follow when it enters certain situations.
     */ private readonly behaviors : { noSubscriber : NoSubscriberBehavior ; maxMissedFrames : number ; } ; /**
     * The debug logger function, if debugging is enabled.
     */ private readonly debug : null | ( ( message : string ) => void ) ; /**
     * Creates a new AudioPlayer.
     */ public constructor ( options = { } ) { super ( ) ; this . _state = { status : AudioPlayerStatus . Idle } ; this . behaviors = { noSubscriber : NoSubscriberBehavior . Pause , maxMissedFrames : 5 , ... options . behaviors , } ; this . debug = options . debug === false ? null : ( message ) => this . emit ( 'debug' , message ) ; } /**
     * A list of subscribed voice connections that can currently receive audio to play.
     */ public get playable ( ) { return this . subscribers . filter ( ( { connection } ) => connection . state . status === VoiceConnectionStatus . Ready ) . map ( ( { connection } ) => connection ) ; } remarks param connection returns private subscribe ( connection ) { const existingSubscription = this . subscribers . find ( ( subscription ) => subscription . connection === connection ) ; if ( ! existingSubscription ) { const subscription = new PlayerSubscription ( connection , this ) ; this . subscribers . push ( subscription ) ; setImmediate ( ( ) => this . emit ( 'subscribe' , subscription ) ) ; return subscription ; } return existingSubscription ; } remarks param subscription returns private unsubscribe ( subscription ) { const index = this . subscribers . indexOf ( subscription ) ; const exists = index !== - 1 ; if ( exists ) { this . subscribers . splice ( index , 1 ) ; subscription . connection . setSpeaking ( false ) ; this . emit ( 'unsubscribe' , subscription ) ; } return exists ; } /**
     * The state that the player is in.
     */ public get state ( ) { return this . _state ; } /**
     * Sets a new state for the player, performing clean-up operations where necessary.
     */ public set state ( newState ) { const oldState = this . _state ; const newResource = Reflect . get ( newState , 'resource' ) as AudioResource | undefined ; if ( oldState . status !== AudioPlayerStatus . Idle && oldState . resource !== newResource ) { oldState . resource . playStream . on ( 'error' , noop ) ; oldState . resource . playStream . off ( 'error' , oldState . onStreamError ) ; oldState . resource . audioPlayer = undefined ; oldState . resource . playStream . destroy ( ) ; oldState . resource . playStream . read ( ) ; } if ( oldState . status === AudioPlayerStatus . Buffering && ( newState . status !== AudioPlayerStatus . Buffering || newState . resource !== oldState . resource ) ) { oldState . resource . playStream . off ( 'end' , oldState . onFailureCallback ) ; oldState . resource . playStream . off ( 'close' , oldState . onFailureCallback ) ; oldState . resource . playStream . off ( 'finish' , oldState . onFailureCallback ) ; oldState . resource . playStream . off ( 'readable' , oldState . onReadableCallback ) ; } if ( newState . status === AudioPlayerStatus . Idle ) { this . _signalStopSpeaking ( ) ; deleteAudioPlayer ( this ) ; } if ( newResource ) { addAudioPlayer ( this ) ; } const didChangeResources = oldState . status !== AudioPlayerStatus . Idle && newState . status === AudioPlayerStatus . Playing && oldState . resource !== newState . resource ; this . _state = newState ; this . emit ( 'stateChange' , oldState , this . _state ) ; if ( oldState . status !== newState . status || didChangeResources ) { this . emit ( newState . status , oldState , this . _state as any ) ; } this . debug ?. ( `state change:\nfrom ${ stringifyState ( oldState ) }\nto ${ stringifyState ( newState ) }` ) ; } remarks param resource throws public play < T > ( resource ) { if ( resource . ended ) { throw new Error ( 'Cannot play a resource that has already ended.' ) ; } if ( resource . audioPlayer ) { if ( resource . audioPlayer === this ) { return ; } throw new Error ( 'Resource is already being played by another audio player.' ) ; } resource . audioPlayer = this ; const onStreamError = ( error ) => { if ( this . state . status !== AudioPlayerStatus . Idle ) { event type { AudioPlayerError } this . emit ( 'error' , new AudioPlayerError ( error , this . state . resource ) ) ; } if ( this . state . status !== AudioPlayerStatus . Idle && this . state . resource === resource ) { this . state = { status : AudioPlayerStatus . Idle , } ; } } ; resource . playStream . once ( 'error' , onStreamError ) ; if ( resource . started ) { this . state = { status : AudioPlayerStatus . Playing , missedFrames : 0 , playbackDuration : 0 , resource , onStreamError , } ; } else { const onReadableCallback = ( ) => { if ( this . state . status === AudioPlayerStatus . Buffering && this . state . resource === resource ) { this . state = { status : AudioPlayerStatus . Playing , missedFrames : 0 , playbackDuration : 0 , resource , onStreamError , } ; } } ; const onFailureCallback = ( ) => { if ( this . state . status === AudioPlayerStatus . Buffering && this . state . resource === resource ) { this . state = { status : AudioPlayerStatus . Idle , } ; } } ; resource . playStream . once ( 'readable' , onReadableCallback ) ; resource . playStream . once ( 'end' , onFailureCallback ) ; resource . playStream . once ( 'close' , onFailureCallback ) ; resource . playStream . once ( 'finish' , onFailureCallback ) ; this . state = { status : AudioPlayerStatus . Buffering , resource , onReadableCallback , onFailureCallback , onStreamError , } ; } } param interpolateSilence returns public pause ( interpolateSilence = true ) { if ( this . state . status !== AudioPlayerStatus . Playing ) return false ; this . state = { ... this . state , status : AudioPlayerStatus . Paused , silencePacketsRemaining : interpolateSilence ? 5 : 0 , } ; return true ; } returns public unpause ( ) { if ( this . state . status !== AudioPlayerStatus . Paused ) return false ; this . state = { ... this . state , status : AudioPlayerStatus . Playing , missedFrames : 0 , } ; return true ; } param force returns public stop ( force = false ) { if ( this . state . status === AudioPlayerStatus . Idle ) return false ; if ( force || this . state . resource . silencePaddingFrames === 0 ) { this . state = { status : AudioPlayerStatus . Idle , } ; } else if ( this . state . resource . silenceRemaining === - 1 ) { this . state . resource . silenceRemaining = this . state . resource . silencePaddingFrames ; } return true ; } returns public checkPlayable ( ) { const state = this . _state ; if ( state . status === AudioPlayerStatus . Idle || state . status === AudioPlayerStatus . Buffering ) return false ; if ( ! state . resource . readable ) { this . state = { status : AudioPlayerStatus . Idle , } ; return false ; } return true ; } /**
     * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
     * by the active connections of this audio player.
     */ private _stepDispatch ( ) { const state = this . _state ; if ( state . status === AudioPlayerStatus . Idle || state . status === AudioPlayerStatus . Buffering ) return ; this . playable . forEach ( ( connection ) => connection . dispatchAudio ( ) ) ; } /**
     * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
     * underlying resource of the stream, and then has all the active connections of the audio player prepare it
     * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
     */ private _stepPrepare ( ) { const state = this . _state ; if ( state . status === AudioPlayerStatus . Idle || state . status === AudioPlayerStatus . Buffering ) return ; const playable = this . playable ; if ( state . status === AudioPlayerStatus . AutoPaused && playable . length > 0 ) { this . state = { ... state , status : AudioPlayerStatus . Playing , missedFrames : 0 , } ; } if ( state . status === AudioPlayerStatus . Paused || state . status === AudioPlayerStatus . AutoPaused ) { if ( state . silencePacketsRemaining > 0 ) { state . silencePacketsRemaining -- ; this . _preparePacket ( SILENCE_FRAME , playable , state ) ; if ( state . silencePacketsRemaining === 0 ) { this . _signalStopSpeaking ( ) ; } } return ; } if ( playable . length === 0 ) { if ( this . behaviors . noSubscriber === NoSubscriberBehavior . Pause ) { this . state = { ... state , status : AudioPlayerStatus . AutoPaused , silencePacketsRemaining : 5 , } ; return ; } else if ( this . behaviors . noSubscriber === NoSubscriberBehavior . Stop ) { this . stop ( true ) ; } } /**
         * Attempt to read an Opus packet from the resource. If there isn't an available packet,
         * play a silence packet. If there are 5 consecutive cycles with failed reads, then the
         * playback will end.
         */ const packet = state . resource . read ( ) ; if ( state . status === AudioPlayerStatus . Playing ) { if ( packet ) { this . _preparePacket ( packet , playable , state ) ; state . missedFrames = 0 ; } else { this . _preparePacket ( SILENCE_FRAME , playable , state ) ; state . missedFrames ++ ; if ( state . missedFrames >= this . behaviors . maxMissedFrames ) { this . stop ( ) ; } } } } /**
     * Signals to all the subscribed connections that they should send a packet to Discord indicating
     * they are no longer speaking. Called once playback of a resource ends.
     */ private _signalStopSpeaking ( ) { return this . subscribers . forEach ( ( { connection } ) => connection . setSpeaking ( false ) ) ; } param packet param receivers private _preparePacket ( packet , receivers , state ) { state . playbackDuration += 20 ; receivers . forEach ( ( connection ) => connection . prepareAudioPacket ( packet ) ) ; } } param state function stringifyState ( state ) { return JSON . stringify ( { ... state , resource : Reflect . has ( state , 'resource' ) , stepTimeout : Reflect . has ( state , 'stepTimeout' ) , } ) ; } /**
 * Creates a new AudioPlayer to be used.
 */ export function createAudioPlayer ( options ? ) { return new AudioPlayer ( options ) ; }