diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index ac44ff866..08cc1f924 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -5,6 +5,7 @@ const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/VoiceReceiver'); const EventEmitter = require('events').EventEmitter; const fs = require('fs'); +const Prism = require('prism-media'); /** * Represents a connection to a voice channel in Discord. @@ -26,6 +27,11 @@ class VoiceConnection extends EventEmitter { */ this.voiceManager = pendingConnection.voiceManager; + /** + * The audio transcoder for this connection + */ + this.prism = new Prism(); + /** * The voice channel this connection is currently serving * @type {VoiceChannel} diff --git a/src/client/voice/dispatcher/StreamDispatcher.js b/src/client/voice/dispatcher/StreamDispatcher.js index e08a36537..a56a4a98e 100644 --- a/src/client/voice/dispatcher/StreamDispatcher.js +++ b/src/client/voice/dispatcher/StreamDispatcher.js @@ -16,17 +16,10 @@ nonce.fill(0); * @extends {EventEmitter} */ class StreamDispatcher extends EventEmitter { - constructor(player, stream, sd, streamOptions) { + constructor(player, stream, streamOptions) { super(); this.player = player; this.stream = stream; - this.streamingData = { - channels: 2, - count: 0, - sequence: sd.sequence, - timestamp: sd.timestamp, - pausedTime: 0, - }; this._startStreaming(); this._triggered = false; this._volume = streamOptions.volume; @@ -47,6 +40,10 @@ class StreamDispatcher extends EventEmitter { this.setVolume(streamOptions.volume || 1); } + get streamingData() { + return this.player.streamingData; + } + /** * How long the stream dispatcher has been "speaking" for * @type {number} diff --git a/src/client/voice/pcm/ConverterEngine.js b/src/client/voice/pcm/ConverterEngine.js deleted file mode 100644 index 6b7502f90..000000000 --- a/src/client/voice/pcm/ConverterEngine.js +++ /dev/null @@ -1,14 +0,0 @@ -const EventEmitter = require('events').EventEmitter; - -class ConverterEngine extends EventEmitter { - constructor(player) { - super(); - this.player = player; - } - - createConvertStream() { - return; - } -} - -module.exports = ConverterEngine; diff --git a/src/client/voice/pcm/ConverterEngineList.js b/src/client/voice/pcm/ConverterEngineList.js deleted file mode 100644 index 56d430e48..000000000 --- a/src/client/voice/pcm/ConverterEngineList.js +++ /dev/null @@ -1 +0,0 @@ -exports.fetch = () => require('./FfmpegConverterEngine'); diff --git a/src/client/voice/pcm/FfmpegConverterEngine.js b/src/client/voice/pcm/FfmpegConverterEngine.js deleted file mode 100644 index 8fb725bda..000000000 --- a/src/client/voice/pcm/FfmpegConverterEngine.js +++ /dev/null @@ -1,86 +0,0 @@ -const ConverterEngine = require('./ConverterEngine'); -const ChildProcess = require('child_process'); -const EventEmitter = require('events').EventEmitter; - -class PCMConversionProcess extends EventEmitter { - constructor(process) { - super(); - this.process = process; - this.input = null; - this.process.on('error', e => this.emit('error', e)); - } - - setInput(stream) { - this.input = stream; - stream.pipe(this.process.stdin, { end: false }); - this.input.on('error', e => this.emit('error', e)); - this.process.stdin.on('error', e => this.emit('error', e)); - } - - destroy() { - this.emit('debug', 'destroying a ffmpeg process:'); - if (this.input && this.input.unpipe && this.process.stdin) { - this.input.unpipe(this.process.stdin); - this.emit('unpiped the user input stream from the process input stream'); - } - if (this.process.stdin) { - this.process.stdin.end(); - this.emit('ended the process stdin'); - } - if (this.process.stdin.destroy) { - this.process.stdin.destroy(); - this.emit('destroyed the process stdin'); - } - if (this.process.kill) { - this.process.kill(); - this.emit('killed the process'); - } - } - -} - -class FfmpegConverterEngine extends ConverterEngine { - constructor(player) { - super(player); - this.command = chooseCommand(); - } - - handleError(encoder, err) { - if (encoder.destroy) encoder.destroy(); - this.emit('error', err); - } - - createConvertStream(seek = 0) { - super.createConvertStream(); - const encoder = ChildProcess.spawn(this.command, [ - '-analyzeduration', '0', - '-loglevel', '0', - '-i', '-', - '-f', 's16le', - '-ar', '48000', - '-ac', '2', - '-ss', String(seek), - 'pipe:1', - ], { stdio: ['pipe', 'pipe', 'ignore'] }); - return new PCMConversionProcess(encoder); - } -} - -function chooseCommand() { - for (const cmd of [ - 'ffmpeg', - 'avconv', - './ffmpeg', - './avconv', - 'node_modules\\ffmpeg-binaries\\bin\\ffmpeg', - 'node_modules/ffmpeg-binaries/bin/ffmpeg', - ]) { - if (!ChildProcess.spawnSync(cmd, ['-h']).error) return cmd; - } - throw new Error( - 'FFMPEG was not found on your system, so audio cannot be played. ' + - 'Please make sure FFMPEG is installed and in your PATH.' - ); -} - -module.exports = FfmpegConverterEngine; diff --git a/src/client/voice/player/AudioPlayer.js b/src/client/voice/player/AudioPlayer.js index 96c6c24ae..64c54630f 100644 --- a/src/client/voice/player/AudioPlayer.js +++ b/src/client/voice/player/AudioPlayer.js @@ -1,30 +1,24 @@ -const PCMConverters = require('../pcm/ConverterEngineList'); -const OpusEncoders = require('../opus/OpusEngineList'); const EventEmitter = require('events').EventEmitter; +const Prism = require('prism-media'); const StreamDispatcher = require('../dispatcher/StreamDispatcher'); +const Collection = require('../../../util/Collection'); +const OpusEncoders = require('../opus/OpusEngineList'); + +const ffmpegArguments = [ + '-analyzeduration', '0', + '-loglevel', '0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', +]; -/** - * Represents the Audio Player of a Voice Connection - * @extends {EventEmitter} - * @private - */ class AudioPlayer extends EventEmitter { constructor(voiceConnection) { super(); - /** - * The voice connection the player belongs to - * @type {VoiceConnection} - */ this.voiceConnection = voiceConnection; - this.audioToPCM = new (PCMConverters.fetch())(); + this.prism = new Prism(); this.opusEncoder = OpusEncoders.fetch(); - this.currentConverter = null; - /** - * The current stream dispatcher, if a stream is being played - * @type {StreamDispatcher} - */ - this.dispatcher = null; - this.audioToPCM.on('error', e => this.emit('error', e)); + this.transcoders = new Collection(); this.streamingData = { channels: 2, count: 0, @@ -32,49 +26,38 @@ class AudioPlayer extends EventEmitter { timestamp: 0, pausedTime: 0, }; - this.voiceConnection.on('closing', () => this.cleanup(null, 'voice connection closing')); + } + + get currentTranscoder() { + return this.transcoders.last(); + } + + destroyAllTranscoders(exceptLatest) { + for (const stream of this.transcoders.keys()) { + const transcoder = this.transcoders.get(stream); + if (exceptLatest && transcoder === this.currentTranscoder) continue; + transcoder.kill(); + } } playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { const options = { seek, volume, passes }; - stream.on('end', () => { - this.emit('debug', 'Input stream to converter has ended'); + const transcoder = this.prism.transcode({ + type: 'ffmpeg', + media: stream, + ffmpegArguments, }); - stream.on('error', e => this.emit('error', e)); - const conversionProcess = this.audioToPCM.createConvertStream(options.seek); - conversionProcess.on('error', e => this.emit('error', e)); - conversionProcess.setInput(stream); - return this.playPCMStream(conversionProcess.process.stdout, conversionProcess, options); + this.transcoders.set(stream, transcoder); + this.playPCMStream(transcoder.output, options); } - cleanup(checkStream, reason) { - // cleanup is a lot less aggressive than v9 because it doesn't try to kill every single stream it is aware of - this.emit('debug', `Clean up triggered due to ${reason}`); - const filter = checkStream && this.dispatcher && this.dispatcher.stream === checkStream; - if (this.currentConverter && (checkStream ? filter : true)) { - this.currentConverter.destroy(); - this.currentConverter = null; - } - } - - playPCMStream(stream, converter, { seek = 0, volume = 1, passes = 1 } = {}) { + playPCMStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { const options = { seek, volume, passes }; - stream.on('end', () => this.emit('debug', 'PCM input stream ended')); - this.cleanup(null, 'outstanding play stream'); - this.currentConverter = converter; - if (this.dispatcher) { - this.streamingData = this.dispatcher.streamingData; - } - stream.on('error', e => this.emit('error', e)); - const dispatcher = new StreamDispatcher(this, stream, this.streamingData, options); - dispatcher.on('error', e => this.emit('error', e)); - dispatcher.on('end', () => this.cleanup(dispatcher.stream, 'dispatcher ended')); + this.destroyAllTranscoders(true); + const dispatcher = new StreamDispatcher(this, stream, options); dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); - this.dispatcher = dispatcher; - dispatcher.on('debug', m => this.emit('debug', `Stream dispatch - ${m}`)); return dispatcher; } - } module.exports = AudioPlayer; diff --git a/src/client/voice/player/AudioPlayer.old.js b/src/client/voice/player/AudioPlayer.old.js new file mode 100644 index 000000000..7e9ce616b --- /dev/null +++ b/src/client/voice/player/AudioPlayer.old.js @@ -0,0 +1,100 @@ +const OpusEncoders = require('../opus/OpusEngineList'); +const EventEmitter = require('events').EventEmitter; +const StreamDispatcher = require('../dispatcher/StreamDispatcher'); + +const ffmpegArguments = [ + '-analyzeduration', '0', + '-loglevel', '0', + '-f', 's16le', + '-ar', '48000', + '-ac', '2', +]; + +/** + * Represents the Audio Player of a Voice Connection + * @extends {EventEmitter} + * @private + */ +class AudioPlayer extends EventEmitter { + constructor(voiceConnection) { + super(); + /** + * The voice connection the player belongs to + * @type {VoiceConnection} + */ + this.voiceConnection = voiceConnection; + this.opusEncoder = OpusEncoders.fetch(); + this.currentConverter = null; + /** + * The current stream dispatcher, if a stream is being played + * @type {StreamDispatcher} + */ + this.dispatcher = null; + // this.prism.on('error', e => this.emit('error', e)); + this.streamingData = { + channels: 2, + count: 0, + sequence: 0, + timestamp: 0, + pausedTime: 0, + }; + this.voiceConnection.on('closing', () => this.cleanup(null, 'voice connection closing')); + } + + get prism() { + return this.voiceConnection.prism; + } + + playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + const transcoder = this.prism.transcode({ + type: 'ffmpeg', + media: stream, + ffmpegArguments, + }); + this.playPCMStream(transcoder.output, options); + } + + /*playUnknownStream(stream, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + stream.on('end', () => { + this.emit('debug', 'Input stream to converter has ended'); + }); + stream.on('error', e => this.emit('error', e)); + const conversionProcess = this.audioToPCM.createConvertStream(options.seek); + conversionProcess.on('error', e => this.emit('error', e)); + conversionProcess.setInput(stream); + return this.playPCMStream(conversionProcess.process.stdout, conversionProcess, options); +}*/ + + cleanup(checkStream, reason) { + // cleanup is a lot less aggressive than v9 because it doesn't try to kill every single stream it is aware of + this.emit('debug', `Clean up triggered due to ${reason}`); + const filter = checkStream && this.dispatcher && this.dispatcher.stream === checkStream; + if (this.currentConverter && (checkStream ? filter : true)) { + this.currentConverter.destroy(); + this.currentConverter = null; + } + } + + playPCMStream(stream, converter, { seek = 0, volume = 1, passes = 1 } = {}) { + const options = { seek, volume, passes }; + stream.on('end', () => this.emit('debug', 'PCM input stream ended')); + this.cleanup(null, 'outstanding play stream'); + this.currentConverter = converter; + if (this.dispatcher) { + this.streamingData = this.dispatcher.streamingData; + } + stream.on('error', e => this.emit('error', e)); + const dispatcher = new StreamDispatcher(this, stream, this.streamingData, options); + dispatcher.on('error', e => this.emit('error', e)); + dispatcher.on('end', () => this.cleanup(dispatcher.stream, 'dispatcher ended')); + dispatcher.on('speaking', value => this.voiceConnection.setSpeaking(value)); + this.dispatcher = dispatcher; + dispatcher.on('debug', m => this.emit('debug', `Stream dispatch - ${m}`)); + return dispatcher; + } + +} + +module.exports = AudioPlayer; diff --git a/src/client/voice/player/BasePlayer.js b/src/client/voice/player/BasePlayer.old.js similarity index 100% rename from src/client/voice/player/BasePlayer.js rename to src/client/voice/player/BasePlayer.old.js diff --git a/src/client/voice/player/DefaultPlayer.js b/src/client/voice/player/DefaultPlayer.old.js similarity index 100% rename from src/client/voice/player/DefaultPlayer.js rename to src/client/voice/player/DefaultPlayer.old.js