const _guilds = {}
const { spawn } = require('child_process')
/**
* Handles audio playback on guilds.
*
* An instance is automatically created for each guild by the GuildManager
* To access it from a command callback, use the data.audioPlayerObject.
*/
class AudioPlayer {
/**
* Instantiates a new audio player.
* @param {Discord.Guild} guild - Discord guild object.
*/
constructor (guild) {
/** Associated guild */
this.guild = guild
/** Current voice connection */
this.voiceConnection = undefined
/** Current stream object */
this.currentStream = undefined
/** FFMPEG process */
this.ffmpegProcess = undefined
}
/**
* Plays an audio stream.
* @param {Discord.VoiceChannel} voiceChannel - Discordie VoiceChannel object
* @param {String} path - Stream path or URL
* @param {object} flags - Flags to append to the FFMpeg command
* @param {string[]} flags.input - Input flags
* @param {string[]} flags.output - Output flags
* @return {Promise<Object>} Discord encoder object
*/
async play (voiceChannel, path, flags = {}, offset = 0) {
if (this.currentStream) {
throw new Error('Bot is currently playing another file on the server.')
}
await this.join(voiceChannel)
// Launch the FFMPEG process
this.ffmpegProcess = spawn(Core.properties.ffmpegBin || 'ffmpeg',
[].concat(
// workaround for shitty connections
path.indexOf('http') === 0 ? [
'-reconnect', '1',
'-reconnect_at_eof', '1',
// '-reconnect_streamed', '1',
'-reconnect_delay_max', '2'
] : []
)
.concat(flags.input)
.concat([
'-hide_banner',
'-analyzeduration', '0',
'-loglevel', Core.properties.debug ? 'warning' : '0',
'-i', path,
// disable video encoding
'-vn'
])
.concat(flags.output)
.concat(
'-f', 'data',
'-map', '0:a',
'-ar', '48k',
'-ac', '2',
'-acodec', 'libopus',
'-sample_fmt', 's16',
'-vbr', 'off',
'-b:a', '64000',
'pipe:1'
)
.filter(f => f)
)
// Play the output stream
this.currentStream = this.voiceConnection.playOpusStream(this.ffmpegProcess.stdout)
// Debug FFMPEG output
if (Core.properties.debug) this.ffmpegProcess.stderr.on('data', d => Core.log(String(d), 1))
this._offset = offset
this.currentStream.once('end', () => this.clean())
return this.currentStream
}
/**
* Joins a voice channel.
* @param {object} voiceChannel - Discord voice channel object
* @return {Promise<Object>} Discord voice connection object
*/
async join (voiceChannel) {
this.voiceConnection = await voiceChannel.join()
return this.voiceConnection
}
/**
* Attempts to stop playback.
* @param {boolean} disconnect - Set to true to also disconnect from the voice channel
* @param {boolean} removeEvents - Remove event listeners before sending the stream.
*/
stop (disconnect, removeEvents) {
try {
if (removeEvents) this.currentStream.removeAllListeners('end')
this.currentStream.end()
} catch (e) {}
this.clean(disconnect)
}
/**
* Current playback timestamp.
* @type {number}
*/
get timestamp () {
if (this.currentStream) return (this.currentStream.time / 1000) + this._offset
return NaN
}
/**
* Cleans resources and (optionally) disconnects from the voice channel.
* @param {boolean} disconnect - Set to true to disconnect
*/
clean (disconnect) {
if (this.ffmpegProcess) {
try {
this.ffmpegProcess.kill()
} catch (e) {
Core.log(e,2)
}
}
delete this.currentStream
if (disconnect) {
try {
this.voiceConnection.disconnect()
delete this.voiceConnection
} catch (e) { }
}
}
static getForGuild (guild) {
if (!_guilds[guild.id]) _guilds[guild.id] = new AudioPlayer(guild)
return _guilds[guild.id]
}
}
module.exports = AudioPlayer