diff --git a/README.md b/README.md index bb45dac..0445a25 100644 --- a/README.md +++ b/README.md @@ -99,4 +99,26 @@ client.on("messageCreate", message => { client.login("XXX"); ``` -See **[https://github.com/discord-player/voice-recorder-example](https://github.com/discord-player/voice-recorder-example)** for a complete voice recorder example. \ No newline at end of file +> See **[https://github.com/discord-player/voice-recorder-example](https://github.com/discord-player/voice-recorder-example)** for a complete voice recorder example. + +### Smooth Volume + +This feature enables smooth volume transition. + +> This library will attempt to polyfill smooth volume api by default. This can be disabled by setting `DARTJS_DISABLE_INJECTION` in env. + +**Example:** + +```js +// injecting manually +require("dartjs").injectSmoothVolume(); + +// Set smoothness before playing +dispatcher.play(stream, { + // use whatever value feels better + volumeSmoothness: 0.08 +}); + +// setting smoothness on-the-fly +dispatcher.setVolumeSmoothness(0.08) +``` \ No newline at end of file diff --git a/package.json b/package.json index a4eb54b..93b6762 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dartjs", - "version": "1.1.1", + "version": "1.2.0", "description": "Very simple framework that provides discord.js v12 voice interface", "main": "dist/index.js", "module": "dist/index.mjs", @@ -34,7 +34,9 @@ "v13", "discord.js-v12-voice", "discord.js-v13-voice", - "discord.js-v12-voice-v13" + "discord.js-v12-voice-v13", + "polyfill", + "discord-player" ], "author": "DevAndromeda", "license": "MIT", @@ -43,6 +45,7 @@ }, "homepage": "https://github.com/DevAndromeda/typescript-template#readme", "devDependencies": { + "@discordjs/opus": "^0.7.0", "@favware/rollup-type-bundler": "^1.0.3", "@types/node": "^16.7.1", "@typescript-eslint/eslint-plugin": "^4.29.2", @@ -58,7 +61,7 @@ "tweetnacl": "^1.0.3", "typescript": "^4.3.5", "youtube-sr": "^4.1.13", - "ytdl-core": "^4.10.1" + "ytdl-core": "^4.11.0" }, "dependencies": { "@discordjs/voice": "^0.8.0" diff --git a/src/Utils/Util.ts b/src/Utils/Util.ts index e182ccb..496e3ae 100644 --- a/src/Utils/Util.ts +++ b/src/Utils/Util.ts @@ -14,3 +14,7 @@ export function wait(duration: number) { setTimeout(resolve, duration).unref(); }); } + +export function randomId() { + return `${Date.now()}::${Math.random().toString(32)}`; +} diff --git a/src/core/DartVoiceManager.ts b/src/core/DartVoiceManager.ts index 0d68ddf..658495d 100644 --- a/src/core/DartVoiceManager.ts +++ b/src/core/DartVoiceManager.ts @@ -7,6 +7,11 @@ export default class DartVoiceManager { public connections = new Collection(); public constructor(public readonly client: Client) {} + /** + * Join a voice channel + * @param channel The voice based channel + * @param options Join config + */ public async join(channel: GuildVoiceChannelResolvable, options?: VoiceJoinConfig) { const vc = this.client.channels.resolve(channel ?? ""); if (!vc || !vc.isVoice()) throw new Error("Voice channel was not provided!"); @@ -16,12 +21,7 @@ export default class DartVoiceManager { } if (this.connections.has(vc.guildId)) { - if (this.connections.get(vc.guildId).channel.id !== vc.id) { - return await this.updateChannel(this.connections.get(vc.guildId), vc); - } else { - const connection = this.connections.get(vc.guildId); - return connection; - } + return await this.updateChannel(this.connections.get(vc.guildId), vc); } else { const connection = await VoiceConnection.createConnection(vc, this, options); this.connections.set(vc.guildId, connection); @@ -30,6 +30,10 @@ export default class DartVoiceManager { } } + /** + * Leave a voice channel + * @param channel The voice channel + */ public leave(channel: GuildVoiceChannelResolvable) { const vc = this.client.channels.resolve(channel ?? ""); if (!vc || !vc.isVoice()) throw new Error("Voice channel was not provided!"); @@ -46,6 +50,11 @@ export default class DartVoiceManager { this.connections.delete(vc.guildId); } + /** + * Update voice connection + * @param connection The voice connection + * @param channel The new voice channel + */ public async updateChannel(connection: VoiceConnection, channel: VoiceChannels) { const vc = await VoiceConnection.joinChannel(channel); connection.voice = vc; diff --git a/src/core/StreamDispatcher.ts b/src/core/StreamDispatcher.ts index df4fec3..a4d7634 100644 --- a/src/core/StreamDispatcher.ts +++ b/src/core/StreamDispatcher.ts @@ -1,16 +1,25 @@ -import { createAudioResource, createAudioPlayer, AudioResource, StreamType, AudioPlayerStatus, VoiceConnectionStatus, VoiceConnectionDisconnectReason, entersState } from "@discordjs/voice"; +import { createAudioResource, createAudioPlayer, AudioResource, StreamType, AudioPlayerStatus, VoiceConnectionStatus, VoiceConnectionDisconnectReason, entersState, CreateAudioPlayerOptions } from "@discordjs/voice"; import type { Readable } from "stream"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; +import { VolumeTransformer } from "../smoothVolume/VolumeTransformer"; import { DispatcherEvents, PlayOptions } from "../types/types"; -import { wait } from "../Utils/Util"; +import { randomId, wait } from "../Utils/Util"; import VoiceConnection from "./VoiceConnection"; -export default class StreamDispatcher extends EventEmitter { - public audioPlayer = createAudioPlayer(); - public audioResource: AudioResource = null; - private readyLock = false; +export interface ArMetadata { + nonce: string; + data: T; +} + +export default class StreamDispatcher extends EventEmitter> { + public audioPlayer = createAudioPlayer(this.audioPlayerOptions || {}); + public audioResource: AudioResource> = null; + private _readyLock = false; + private _ignoreList = new Set(); + private _nextTickCallbacks = new Array<() => unknown>(); + private _immediateCallbacks = new Array<() => unknown>(); - public constructor(public readonly connection: VoiceConnection) { + public constructor(public readonly connection: VoiceConnection, public audioPlayerOptions: CreateAudioPlayerOptions = {}) { super(); this.attachEvents(); this.connection.voice.subscribe(this.audioPlayer); @@ -26,6 +35,7 @@ export default class StreamDispatcher extends EventEmitter { private attachEvents() { if (!this.connection.voice.eventNames().includes("stateChange")) + // @ts-expect-error Argument of type '"stateChange"' is not assignable to parameter of type 'VoiceConnectionStatus.Signalling'? this.connection.voice.on("stateChange", async (_, newState) => { if (newState.status === VoiceConnectionStatus.Disconnected) { if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) { @@ -46,8 +56,8 @@ export default class StreamDispatcher extends EventEmitter { } } else if (newState.status === VoiceConnectionStatus.Destroyed) { this.audioPlayer?.stop(); - } else if (!this.readyLock && (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)) { - this.readyLock = true; + } else if (!this._readyLock && (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)) { + this._readyLock = true; try { await entersState(this.connection.voice, VoiceConnectionStatus.Ready, 20_000); } catch { @@ -55,17 +65,25 @@ export default class StreamDispatcher extends EventEmitter { this.connection.voiceManager.connections.delete(this.connection.channel.guildId); this.connection.emit("disconnect"); } finally { - this.readyLock = false; + this._readyLock = false; } } }); if (!this.audioPlayer.eventNames().includes("stateChange")) + // @ts-expect-error Argument of type '"stateChange"' is not assignable to parameter of type 'AudioPlayerStatus.Idle'? this.audioPlayer.on("stateChange", (oldState, newState) => { if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) { - this.emit("finish"); + this._immediateCall(); + const nonce = this.audioResource?.metadata?.nonce; + if (typeof nonce === "string" && this._ignoreList.has(nonce)) return this._ignoreList.delete(nonce); + this.emit("finish", this.audioResource?.metadata as ArMetadata); } else if (newState.status === AudioPlayerStatus.Playing && oldState.status === AudioPlayerStatus.Buffering) { - this.emit("start"); + this._nextTickCall(); + const nonce = this.audioResource?.metadata?.nonce; + if (this._ignoreList.has(nonce)) return this._ignoreList.delete(nonce); + // emit the event + this.emit("start", this.audioResource?.metadata as ArMetadata); } }); @@ -75,16 +93,29 @@ export default class StreamDispatcher extends EventEmitter { if (!this.audioPlayer.eventNames().includes("error")) this.audioPlayer.on("error", (error) => void this.emit("error", error)); } + /** + * Stop the player + * @param [force=false] If the playback should be forcefully stopped + */ public end(force = false) { this.audioPlayer.stop(force); } + /** + * Stop the player + * @param [force=false] If the playback should be forcefully stopped + */ public stop(force = false) { this.end(force); } - public playStream(stream: Readable | string, options?: PlayOptions) { - this.audioResource = createAudioResource(stream, { + /** + * Play stream over voice connection + * @param stream The readable stream or stream source url to play + * @param options Play options + */ + public playStream(stream: Readable | string, options?: PlayOptions) { + const audioResource = createAudioResource(stream, { inputType: // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error @@ -95,13 +126,45 @@ export default class StreamDispatcher extends EventEmitter { }[options?.type as StreamType] || (options?.type as StreamType) || StreamType.Arbitrary, - inlineVolume: options?.inlineVolume ?? true + inlineVolume: options?.inlineVolume ?? true, + silencePaddingFrames: typeof options?.silencePaddingFrames !== "number" ? 5 : options?.silencePaddingFrames, + metadata: Object.defineProperties( + {}, + { + nonce: { + value: randomId(), + enumerable: true, + writable: false, + configurable: false + }, + data: { + value: options?.metadata, + writable: true, + enumerable: true, + configurable: true + } + } + ) as ArMetadata }); + if (typeof options?.initialVolume === "number" && audioResource.volume) { + Reflect.set(audioResource.volume, "volume", options.initialVolume); + } + + if (typeof options?.volumeSmoothness === "number" && audioResource.volume && (audioResource.volume as VolumeTransformer).hasSmoothness) { + Reflect.set(audioResource.volume, "_smoothing", options.volumeSmoothness || 0); + } + + if (options?.ignorePrevious && this.audioResource?.metadata?.nonce) this._ignoreList.add(this.audioResource.metadata.nonce); this.end(true); - this.audioPlayer.play(this.audioResource); + this.audioResource = audioResource; + this.audioPlayer.play(audioResource); } + /** + * Set volume + * @param amount The volume amount to set + */ public setVolume(amount: number) { const lastVolume = this.volume; if (lastVolume === amount || !this.audioResource?.volume) return false; @@ -110,6 +173,22 @@ export default class StreamDispatcher extends EventEmitter { return true; } + /** + * Set volume in percentage + * @param amount The volume amount to set + */ + public setVolumePercentage(percentage: number) { + const lastVolume = this.volumePercentage; + if (lastVolume === percentage || !this.audioResource?.volume) return false; + this.audioResource?.volume?.setVolume(percentage / 100); + this.emit("volumeChange", lastVolume / 100, this.volume); + return true; + } + + /** + * Set volume in logarithmic value + * @param amount The volume to set + */ public setVolumeLogarithmic(amount: number) { const lastVolume = this.volume; if (lastVolume === amount || !this.audioResource?.volume) return false; @@ -118,6 +197,10 @@ export default class StreamDispatcher extends EventEmitter { return true; } + /** + * Set volume in decibels + * @param amount The volume in decibels + */ public setVolumeDecibels(amount: number) { const lastVolume = this.volume; if (lastVolume === amount || !this.audioResource?.volume) return false; @@ -126,41 +209,134 @@ export default class StreamDispatcher extends EventEmitter { return true; } + /** + * Get current volume amount + */ public get volume() { return this.audioResource?.volume?.volume ?? 1; } + /** + * Get current volume in percentage + */ + public get volumePercentage() { + return this.volume * 100; + } + + /** + * Get current volume as decibels + */ public get volumeDecibels() { return this.audioResource?.volume?.volumeDecibels ?? 1; } + /** + * Get current volume as logarithmic value + */ public get volumeLogarithmic() { return this.audioResource?.volume?.volumeLogarithmic ?? 1; } + /** + * Check if the volume is editable + */ public get volumeEditable() { return Boolean(this.audioResource?.volume); } + /** + * Volume smoothness availability + */ + public get volumeSmoothnessEditable() { + return !!(this.audioResource.volume as VolumeTransformer)?.hasSmoothness; + } + + /** + * Set volume smoothness + * @param smoothness + */ + public setVolumeSmoothness(smoothness: number) { + if (!this.volumeSmoothnessEditable) return false; + Reflect.set(this.audioResource.volume, "_smoothing", smoothness); + return true; + } + + /** + * Get volume smoothness + */ + public get volumeSmoothness() { + return (this.audioResource.volume as VolumeTransformer)?.smoothness || 0; + } + + /** + * The actual streamed duration in ms of current audio resource + */ public get streamTime() { return this.audioResource?.playbackDuration ?? 0; } + /** + * The total streamed duration in ms of the current audio resource, including paused states + */ public get totalStreamTime() { return this.audioPlayer?.state.status === AudioPlayerStatus.Playing ? this.audioPlayer?.state.playbackDuration : 0; } + /** + * The paused state + */ public get paused() { return this.audioPlayer.state.status === AudioPlayerStatus.Paused || this.audioPlayer.state.status === AudioPlayerStatus.AutoPaused; } + /** + * Pause the player + * @param silence Send silence frame during paused state + */ public pause(silence = false) { this.audioPlayer?.pause(silence); } + /** + * Resumes the player + */ public resume() { this.audioPlayer?.unpause(); } + + /** + * Callback provided here runs whenever next track is playable + * @param cb The callback function + */ + public next(cb: () => unknown) { + if (!cb || typeof cb !== "function") throw new TypeError("Next tick callback must be a function"); + this._nextTickCallbacks.push(cb); + } + + /** + * Callback provided here runs whenever next track is playable + * @param cb The callback function + */ + public immediate(cb: () => unknown) { + if (!cb || typeof cb !== "function") throw new TypeError("Next tick callback must be a function"); + this._immediateCallbacks.push(cb); + } + + private _immediateCall() { + if (!this._immediateCallbacks.length) return; + this._immediateCallbacks.forEach((cb, idx) => { + void this._immediateCallbacks.splice(idx, 1); + cb(); + }); + } + + private _nextTickCall() { + if (!this._nextTickCallbacks.length) return; + this._nextTickCallbacks.forEach((cb, idx) => { + void this._nextTickCallbacks.splice(idx, 1); + cb(); + }); + } } export { StreamDispatcher }; diff --git a/src/core/VoiceConnection.ts b/src/core/VoiceConnection.ts index b4a7f8d..063c8ad 100644 --- a/src/core/VoiceConnection.ts +++ b/src/core/VoiceConnection.ts @@ -1,4 +1,4 @@ -import { joinVoiceChannel, entersState, VoiceConnectionStatus, VoiceConnection as VoiceConnectionNative, DiscordGatewayAdapterCreator } from "@discordjs/voice"; +import { joinVoiceChannel, entersState, VoiceConnectionStatus, VoiceConnection as VoiceConnectionNative, DiscordGatewayAdapterCreator, NoSubscriberBehavior } from "@discordjs/voice"; import { TypedEmitter as EventEmitter } from "tiny-typed-emitter"; import { VoiceChannels, VoiceEvents, VoiceConnectionData, PlayOptions, VoiceJoinConfig } from "../types/types"; import { catchError } from "../Utils/Util"; @@ -18,10 +18,16 @@ export default class VoiceConnection extends EventEmitter { super(); } + /** + * The audio player + */ public get audioPlayer() { return this.dispatcher?.audioPlayer; } + /** + * Disconnect from this connection + */ public disconnect() { try { this.dispatcher.removeAllListeners(); @@ -32,6 +38,9 @@ export default class VoiceConnection extends EventEmitter { } } + /** + * Destroy this connection + */ public destroy() { try { this.voiceManager.connections.delete(this.channel.guildId); @@ -43,6 +52,12 @@ export default class VoiceConnection extends EventEmitter { } } + /** + * Create a voice connection + * @param channel The voice channel + * @param manager The voice manager + * @param options Join config + */ public static createConnection(channel: VoiceChannels, manager: DartVoiceManager, options?: VoiceJoinConfig): Promise { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { @@ -56,6 +71,11 @@ export default class VoiceConnection extends EventEmitter { }); } + /** + * Join a voice channel + * @param channel The voice channel + * @param options The join config + */ public static joinChannel(channel: VoiceChannels, options?: VoiceJoinConfig): Promise { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { @@ -76,19 +96,45 @@ export default class VoiceConnection extends EventEmitter { }); } - public play(stream: Readable | string, options?: PlayOptions) { + /** + * Play readable stream or remote stream source in this connection + * @param stream The stream source + * @param options The play options + */ + public play( + stream: Readable | string, + options?: PlayOptions & { + behaviours?: { + noSubscriber?: NoSubscriberBehavior; + maxMissedFrames?: number; + }; + } + ) { if (!this.dispatcher) { - const dispatcher = new StreamDispatcher(this); + const dispatcher = new StreamDispatcher( + this, + options?.behaviours + ? { + behaviors: options.behaviours + } + : {} + ); this.dispatcher = dispatcher; } this.dispatcher.playStream(stream, options); - return this.dispatcher; + return this.dispatcher as StreamDispatcher; } + /** + * The voice connection status + */ public get status() { return this.voice.state.status; } + /** + * The voice connection latency (udp) + */ public get ping() { const latency = this.voice.ping.udp; diff --git a/src/core/VoiceReceiver.ts b/src/core/VoiceReceiver.ts index 140e716..793ddd0 100644 --- a/src/core/VoiceReceiver.ts +++ b/src/core/VoiceReceiver.ts @@ -16,6 +16,11 @@ export class VoiceReceiver extends EventEmitter { this.connection.voice.receiver.speaking.removeAllListeners("end"); } + /** + * Create receiver stream + * @param user The target user to listen to + * @param options Receiver options + */ public createStream(user: UserResolvable, options: ReceiveStreamOptions = {}) { const _user = this.client.users.resolveId(user); options ??= { end: "silence", mode: "opus" }; diff --git a/src/index.ts b/src/index.ts index 0c6be14..b4253dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,14 @@ +import { injectSmoothVolume } from "./smoothVolume/injection"; + +if (!("DARTJS_DISABLE_INJECTION" in process.env)) { + injectSmoothVolume(); +} + export { DartVoiceManager } from "./core/DartVoiceManager"; export { StreamDispatcher } from "./core/StreamDispatcher"; export { VoiceConnection } from "./core/VoiceConnection"; export { VoiceReceiver } from "./core/VoiceReceiver"; +export { VolumeTransformer, VolumeTransformerOptions } from "./smoothVolume/VolumeTransformer"; +export { injectSmoothVolume }; export * from "./types/types"; export * from "./Utils/Util"; diff --git a/src/smoothVolume/VolumeTransformer.ts b/src/smoothVolume/VolumeTransformer.ts new file mode 100644 index 0000000..b493dd1 --- /dev/null +++ b/src/smoothVolume/VolumeTransformer.ts @@ -0,0 +1,144 @@ +// prism's volume transformer with smooth volume support + +import { Transform, TransformOptions } from "stream"; + +export interface VolumeTransformerOptions extends TransformOptions { + type?: "s16le" | "s16be" | "s32le" | "s32be"; + smoothness?: number; + volume?: number; +} + +export class VolumeTransformer extends Transform { + private _bits: number; + private _smoothing: number; + private _bytes: number; + private _extremum: number; + private _chunk: Buffer; + public volume: number; + private _targetVolume: number; + public type: "s16le" | "s32le" | "s16be" | "s32be"; + constructor(options: VolumeTransformerOptions = {}) { + super(options); + switch (options.type) { + case "s16le": + this._readInt = (buffer, index) => buffer.readInt16LE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt16LE(int, index); + this._bits = 16; + break; + case "s16be": + this._readInt = (buffer, index) => buffer.readInt16BE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt16BE(int, index); + this._bits = 16; + break; + case "s32le": + this._readInt = (buffer, index) => buffer.readInt32LE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt32LE(int, index); + this._bits = 32; + break; + case "s32be": + this._readInt = (buffer, index) => buffer.readInt32BE(index); + this._writeInt = (buffer, int, index) => buffer.writeInt32BE(int, index); + this._bits = 32; + break; + default: + throw new Error("VolumeTransformer type should be one of s16le, s16be, s32le, s32be"); + } + this.type = options.type; + this._bytes = this._bits / 8; + this._extremum = Math.pow(2, this._bits - 1); + this.volume = Number.isNaN(options.volume) ? 1 : Number(options.volume); + if (!Number.isFinite(this.volume)) this.volume = 1; + this._targetVolume = this.volume; + this._chunk = Buffer.alloc(0); + this._smoothing = options.smoothness || 0; + } + + _readInt(buffer: Buffer, index: number) { + return index; + } + _writeInt(buffer: Buffer, int: number, index: number) { + return index; + } + + _applySmoothness() { + if (this.volume < this._targetVolume) { + this.volume = this.volume + this._smoothing >= this._targetVolume ? this._targetVolume : this.volume + this._smoothing; + } else if (this.volume > this._targetVolume) { + this.volume = this.volume - this._smoothing <= this._targetVolume ? this._targetVolume : this.volume - this._smoothing; + } + } + + _transform(chunk: Buffer, encoding: BufferEncoding, done: () => unknown) { + if (this.smoothingEnabled() && this.volume !== this._targetVolume) this._applySmoothness(); + + if (this.volume === 1) { + this.push(chunk); + return done(); + } + + const { _bytes, _extremum } = this; + + chunk = this._chunk = Buffer.concat([this._chunk, chunk]); + if (chunk.length < _bytes) return done(); + + const complete = Math.floor(chunk.length / _bytes) * _bytes; + + for (let i = 0; i < complete; i += _bytes) { + const int = Math.min(_extremum - 1, Math.max(-_extremum, Math.floor(this.volume * this._readInt(chunk, i)))); + this._writeInt(chunk, int, i); + } + + this._chunk = chunk.slice(complete); + this.push(chunk.slice(0, complete)); + return done(); + } + + _destroy(err: Error, cb: (error: Error) => void) { + super._destroy(err, cb); + this._chunk = null; + } + + setVolume(volume: number) { + if (Number.isNaN(volume)) volume = 1; + if (typeof volume !== "number") volume = Number(volume); + if (!Number.isFinite(volume)) volume = volume < 0 ? 0 : 1; + this._targetVolume = volume; + if (this._smoothing <= 0) this.volume = volume; + } + + setVolumeDecibels(db: number) { + this.setVolume(Math.pow(10, db / 20)); + } + + setVolumeLogarithmic(value: number) { + this.setVolume(Math.pow(value, 1.660964)); + } + + get volumeDecibels() { + return Math.log10(this.volume) * 20; + } + + get volumeLogarithmic() { + return Math.pow(this.volume, 1 / 1.660964); + } + + get smoothness() { + return this._smoothing; + } + + setSmoothness(smoothness: number) { + this._smoothing = smoothness; + } + + smoothingEnabled() { + return Number.isFinite(this._smoothing) && this._smoothing > 0; + } + + get hasSmoothness() { + return true; + } + + static get hasSmoothing() { + return true; + } +} diff --git a/src/smoothVolume/injection.ts b/src/smoothVolume/injection.ts new file mode 100644 index 0000000..846c0c5 --- /dev/null +++ b/src/smoothVolume/injection.ts @@ -0,0 +1,14 @@ +import { VolumeTransformer as VolumeTransformerMock } from "./VolumeTransformer"; + +export function injectSmoothVolume() { + try { + // eslint-disable-next-line + const mod = require("prism-media") as typeof import("prism-media") & { VolumeTransformer: typeof VolumeTransformerMock }; + + if (typeof mod.VolumeTransformer.hasSmoothing !== "boolean") { + Reflect.set(mod, "VolumeTransformer", VolumeTransformerMock); + } + } catch { + /* do nothing */ + } +} diff --git a/src/types/types.ts b/src/types/types.ts index 7b37c89..9c3e5ba 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -14,9 +14,9 @@ export interface VoiceReceiverEvents { debug: (message: string) => void; } -export interface DispatcherEvents { - start: () => void; - finish: () => void; +export interface DispatcherEvents { + start: (metadata?: { nonce?: string; data?: T }) => void; + finish: (metadata?: { nonce?: string; data?: T }) => void; error: (error: Error) => void; debug: (message: string) => void; volumeChange: (oldVolume: number, newVolume: number) => void; @@ -27,9 +27,21 @@ export interface VoiceConnectionData { manager: DartVoiceManager; } -export interface PlayOptions { +export interface PlayOptions { + /** The stream type */ type?: `${StreamType}` | StreamType | "converted" | "unknown"; + /** Enable/disable on-the-fly volume controls */ inlineVolume?: boolean; + /** Silence padding frames */ + silencePaddingFrames?: number; + /** Initial volume */ + initialVolume?: number; + /** Volume smoothness */ + volumeSmoothness?: number; + /** Track metadata */ + metadata?: T; + /** Ignore previous track event on running `.play()` */ + ignorePrevious?: boolean; } export interface ReceiveStreamOptions {