diff --git a/src/client/voice/VoiceBroadcast.js b/src/client/voice/VoiceBroadcast.js index f06ae96c6..d8bcf9ede 100644 --- a/src/client/voice/VoiceBroadcast.js +++ b/src/client/voice/VoiceBroadcast.js @@ -1,6 +1,7 @@ const EventEmitter = require('events'); const BroadcastAudioPlayer = require('./player/BroadcastAudioPlayer'); const DispatcherSet = require('./util/DispatcherSet'); +const PlayInterface = require('./util/PlayInterface'); /** * A voice broadcast can be played across multiple voice connections for improved shared-stream efficiency. @@ -15,6 +16,7 @@ const DispatcherSet = require('./util/DispatcherSet'); * } * ``` * @implements {VolumeInterface} + * @implements {PlayInterface} */ class VoiceBroadcast extends EventEmitter { constructor(client) { @@ -35,74 +37,8 @@ class VoiceBroadcast extends EventEmitter { get dispatcher() { return this.player.dispatcher; } - - /** - * Plays the given file in the voice connection. - * @param {string} file The absolute path to the file - * @param {StreamOptions} [options] Options for playing the stream - * @returns {BroadcastDispatcher} - * @example - * // Play files natively - * voiceChannel.join() - * .then(connection => { - * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); - * }) - * .catch(console.error); - */ - playFile(file, options) { - return this.player.playUnknown(file, options); - } - - /** - * Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) - * @param {string} input the arbitrary input - * @param {StreamOptions} [options] Options for playing the stream - * @returns {BroadcastDispatcher} - */ - playArbitraryInput(input, options) { - return this.player.playUnknown(input, options); - } - - /** - * Plays and converts an audio stream in the voice connection. - * @param {ReadableStream} stream The audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {BroadcastDispatcher} - * @example - * // Play streams using ytdl-core - * const ytdl = require('ytdl-core'); - * const streamOptions = { seek: 0, volume: 1 }; - * voiceChannel.join() - * .then(connection => { - * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); - * const dispatcher = connection.playStream(stream, streamOptions); - * }) - * .catch(console.error); - */ - playStream(stream, options) { - return this.player.playUnknown(stream, options); - } - - /** - * Plays a stream of 16-bit signed stereo PCM. - * @param {ReadableStream} stream The audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {BroadcastDispatcher} - */ - playConvertedStream(stream, options) { - return this.player.playPCMStream(stream, options); - } - - /** - * Plays an Opus encoded stream. - * Note that inline volume is not compatible with this method. - * @param {ReadableStream} stream The Opus audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {BroadcastDispatcher} - */ - playOpusStream(stream, options) { - return this.player.playOpusStream(stream, options); - } } +PlayInterface.applyToClass(VoiceBroadcast); + module.exports = VoiceBroadcast; diff --git a/src/client/voice/VoiceConnection.js b/src/client/voice/VoiceConnection.js index 700db600e..5543f96ff 100644 --- a/src/client/voice/VoiceConnection.js +++ b/src/client/voice/VoiceConnection.js @@ -6,6 +6,7 @@ const AudioPlayer = require('./player/AudioPlayer'); const VoiceReceiver = require('./receiver/Receiver'); const EventEmitter = require('events'); const { Error } = require('../../errors'); +const PlayInterface = require('./util/PlayInterface'); /** * Represents a connection to a guild's voice server. @@ -17,6 +18,7 @@ const { Error } = require('../../errors'); * }); * ``` * @extends {EventEmitter} + * @implements {PlayInterface} */ class VoiceConnection extends EventEmitter { constructor(voiceManager, channel) { @@ -425,106 +427,6 @@ class VoiceConnection extends EventEmitter { guild._memberSpeakUpdate(user_id, speaking); } - /** - * Options that can be passed to stream-playing methods: - * @typedef {Object} StreamOptions - * @property {number} [seek=0] The time to seek to - * @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for - * this stream to improve performance. - * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss - * @property {number} [plp] Expected packet loss percentage - * @property {boolean} [fec] Enabled forward error correction - * @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps. - * If set to 'auto', the voice channel's bitrate will be used - * @property {number} [highWaterMark=8] The maximum number of opus packets to make and store before they are - * actually needed. See https://nodejs.org/en/docs/guides/backpressuring-in-streams/. Setting this value to - * 1 means that changes in volume will be more instant. - */ - - /** - * Plays the given file in the voice connection. - * @param {string} file The absolute path to the file - * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} - * @example - * // Play files natively - * voiceChannel.join() - * .then(connection => { - * const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3'); - * }) - * .catch(console.error); - */ - playFile(file, options) { - return this.player.playUnknown(file, options); - } - - /** - * Plays an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description) - * @param {string} input the arbitrary input - * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} - */ - playArbitraryInput(input, options) { - return this.player.playUnknown(input, options); - } - - /** - * Plays and converts an audio stream in the voice connection. - * @param {ReadableStream} stream The audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} - * @example - * // Play streams using ytdl-core - * const ytdl = require('ytdl-core'); - * const streamOptions = { seek: 0, volume: 1 }; - * voiceChannel.join() - * .then(connection => { - * const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' }); - * const dispatcher = connection.playStream(stream, streamOptions); - * }) - * .catch(console.error); - */ - playStream(stream, options) { - return this.player.playUnknown(stream, options); - } - - /** - * Plays a stream of 16-bit signed stereo PCM. - * @param {ReadableStream} stream The audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} - */ - playConvertedStream(stream, options) { - return this.player.playPCMStream(stream, options); - } - - /** - * Plays an Opus encoded stream. - * Note that inline volume is not compatible with this method. - * @param {ReadableStream} stream The Opus audio stream to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} - */ - playOpusStream(stream, options) { - return this.player.playOpusStream(stream, options); - } - - /** - * Plays a voice broadcast. - * @param {VoiceBroadcast} broadcast The broadcast to play - * @param {StreamOptions} [options] Options for playing the stream - * @returns {StreamDispatcher} - * @example - * // Play a broadcast - * const broadcast = client - * .createVoiceBroadcast() - * .playFile('./test.mp3'); - * const dispatcher = voiceConnection.playBroadcast(broadcast); - */ - playBroadcast(broadcast, options) { - return this.player.playBroadcast(broadcast, options); - } - /** * Creates a VoiceReceiver so you can start listening to voice data. * It's recommended to only create one of these. @@ -537,4 +439,6 @@ class VoiceConnection extends EventEmitter { } } +PlayInterface.applyToClass(VoiceConnection); + module.exports = VoiceConnection; diff --git a/src/client/voice/util/PlayInterface.js b/src/client/voice/util/PlayInterface.js new file mode 100644 index 000000000..1c3eaeef1 --- /dev/null +++ b/src/client/voice/util/PlayInterface.js @@ -0,0 +1,55 @@ +/** + * Options that can be passed to stream-playing methods: + * @typedef {Object} StreamOptions + * @property {string} [type='unknown'] The type of stream. 'unknown', 'converted', 'opus', 'broadcast. + * @property {number} [seek=0] The time to seek to + * @property {number|boolean} [volume=1] The volume to play at. Set this to false to disable volume transforms for + * this stream to improve performance. + * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss + * @property {number} [plp] Expected packet loss percentage + * @property {boolean} [fec] Enabled forward error correction + * @property {number|string} [bitrate=96] The bitrate (quality) of the audio in kbps. + * If set to 'auto', the voice channel's bitrate will be used + * @property {number} [highWaterMark=12] The maximum number of opus packets to make and store before they are + * actually needed. See https://nodejs.org/en/docs/guides/backpressuring-in-streams/. Setting this value to + * 1 means that changes in volume will be more instant. + */ + +/** + * An interface class to allow you to play audio over VoiceConnections and VoiceBroadcasts. + */ +class PlayInterface { + constructor(player) { + this.player = player; + } + + /** + * Play an audio resource. + * @param {ReadableStream|string} resource The resource to play. + * @param {StreamOptions} [options] The options to play. + * @returns {StreamDispatcher} + */ + play(resource, options = {}) { + const type = options.type || 'unknown'; + if (type === 'unknown') { + return this.player.playUnknown(resource, options); + } else if (type === 'converted') { + return this.player.playPCMStream(resource, options); + } else if (type === 'opus') { + return this.player.playOpusStream(resource, options); + } else if (type === 'broadcast') { + if (!this.player.playBroadcast) throw Error('VOICE_PLAY_INTERFACE_NO_BROADCAST'); + return this.player.playBroadcast(resource, options); + } + throw Error('VOICE_PLAY_INTERFACE_BAD_TYPE'); + } + + static applyToClass(structure) { + for (const prop of ['play']) { + Object.defineProperty(structure.prototype, prop, + Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop)); + } + } +} + +module.exports = PlayInterface; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 70ee8d47e..91956cf99 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -54,6 +54,8 @@ const Messages = { VOICE_NO_BROWSER: 'Voice connections are not available in browsers.', VOICE_CONNECTION_ATTEMPTS_EXCEEDED: attempts => `Too many connection attempts (${attempts}).`, VOICE_JOIN_SOCKET_CLOSED: 'Tried to send join packet, but the WebSocket is not open.', + VOICE_PLAY_INTERFACE_NO_BROADCAST: 'A broadcast cannot be played in this context.', + VOICE_PLAY_INTERFACE_BAD_TYPE: 'Unknown stream type', OPUS_ENGINE_MISSING: 'Couldn\'t find an Opus engine.', diff --git a/test/voice.js b/test/voice.js index 2f57ee8dc..07cc3a4bf 100644 --- a/test/voice.js +++ b/test/voice.js @@ -42,7 +42,7 @@ client.on('message', m => { if (!connections.has(m.guild.id)) connections.set(m.guild.id, { conn, queue: [] }); m.reply('ok!'); // conn.playOpusStream(fs.createReadStream('C:/users/amish/downloads/z.ogg').pipe(new prism.OggOpusDemuxer())); - d = conn.playStream(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); + d = conn.play(ytdl('https://www.youtube.com/watch?v=_XXOSf0s2nk', { filter: 'audioonly' }, { passes: 3 })); }); } else { m.reply('Specify a voice channel!');