feat: try implement djs voice + video (v12)
This commit is contained in:
14
src/client/voice/util/Function.js
Normal file
14
src/client/voice/util/Function.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
function parseStreamKey(key) {
|
||||
const Arr = key.split(':');
|
||||
const type = Arr[0];
|
||||
const guildId = type == 'guild' ? Arr[1] : null;
|
||||
const channelId = type == 'guild' ? Arr[2] : Arr[1];
|
||||
const userId = type == 'guild' ? Arr[3] : Arr[2];
|
||||
return { type, guildId, channelId, userId };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseStreamKey,
|
||||
};
|
||||
121
src/client/voice/util/PlayInterface.js
Normal file
121
src/client/voice/util/PlayInterface.js
Normal file
@@ -0,0 +1,121 @@
|
||||
'use strict';
|
||||
|
||||
const { Readable } = require('stream');
|
||||
const prism = require('prism-media');
|
||||
const { Error } = require('../../../errors');
|
||||
|
||||
/**
|
||||
* Options that can be passed to stream-playing methods:
|
||||
* @typedef {Object} StreamOptions
|
||||
* @property {StreamType} [type='unknown'] The type of stream.
|
||||
* @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams
|
||||
* @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} [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 option passed as part of `StreamOptions` specifying the type of the stream.
|
||||
* * `unknown`: The default type, streams/input will be passed through to ffmpeg before encoding.
|
||||
* Will play most streams.
|
||||
* * `converted`: Play a stream of 16bit signed stereo PCM data, skipping ffmpeg.
|
||||
* * `opus`: Play a stream of opus packets, skipping ffmpeg. You lose the ability to alter volume.
|
||||
* * `ogg/opus`: Play an ogg file with the opus encoding, skipping ffmpeg. You lose the ability to alter volume.
|
||||
* * `webm/opus`: Play a webm file with opus audio, skipping ffmpeg. You lose the ability to alter volume.
|
||||
* @typedef {string} StreamType
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface class to allow you to play audio over VoiceConnections.
|
||||
*/
|
||||
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.
|
||||
* @example
|
||||
* // Play a local audio file
|
||||
* connection.playAudio('/home/hydrabolt/audio.mp3', { volume: 0.5 });
|
||||
* @example
|
||||
* // Play a ReadableStream
|
||||
* connection.playAudio(ytdl('https://www.youtube.com/watch?v=ZlAU_w7-Xp8', { quality: 'highestaudio' }));
|
||||
* @example
|
||||
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
|
||||
* connection.playAudio('http://www.sample-videos.com/audio/mp3/wave.mp3');
|
||||
* @returns {AudioDispatcher}
|
||||
*/
|
||||
playAudio(resource, options = {}) {
|
||||
if (resource instanceof Readable || typeof resource === 'string') {
|
||||
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 === 'ogg/opus') {
|
||||
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
|
||||
return this.player.playOpusStream(resource.pipe(new prism.opus.OggDemuxer()), options);
|
||||
} else if (type === 'webm/opus') {
|
||||
if (!(resource instanceof Readable)) throw new Error('VOICE_PRISM_DEMUXERS_NEED_STREAM');
|
||||
return this.player.playOpusStream(resource.pipe(new prism.opus.WebmDemuxer()), options);
|
||||
}
|
||||
}
|
||||
throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be passed to stream-playing methods:
|
||||
* @typedef {Object} VideoOptions
|
||||
* @property {number} [seek=0] The time to seek to, will be ignored when playing `ogg/opus` or `webm/opus` streams
|
||||
* @property {number} [fps=30] Video fps
|
||||
* @property {boolean} [copy=false] Copy codec ?
|
||||
* @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.
|
||||
* @property {'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'} [preset='veryfast'] ffmpeg preset
|
||||
* @property {boolean} [hwAccel=false] Enables hardware accelerated video decoding. Enabling this option might result in an exception
|
||||
* being thrown by Ffmpeg process if your system does not support hardware acceleration
|
||||
* @property {string[]} [inputFFmpegArgs] input ffmpeg
|
||||
* Ex: ['-config1', 'value1', '-config2', 'value2']
|
||||
* @property {string[]} [outputFFmpegArgs] output ffmpeg
|
||||
* Ex: ['-config1', 'value1', '-config2', 'value2']
|
||||
*/
|
||||
|
||||
/**
|
||||
* Play an video resource.
|
||||
* @param {ReadableStream|string} resource The resource to play.
|
||||
* @param {VideoOptions} [options] The options to play.
|
||||
* @example
|
||||
* // Play a local video file
|
||||
* connection.playVideo('/home/hydrabolt/video.mp4');
|
||||
* @example
|
||||
* // Using different protocols: https://ffmpeg.org/ffmpeg-protocols.html
|
||||
* connection.playVideo('http://www.sample-videos.com/video/mp4/wave.mp4');
|
||||
* @returns {VideoDispatcher}
|
||||
*/
|
||||
playVideo(resource, options = {}) {
|
||||
if (resource instanceof Readable || typeof resource === 'string') {
|
||||
return this.player.playUnknownVideo(resource, options);
|
||||
}
|
||||
throw new Error('VOICE_PLAY_INTERFACE_BAD_TYPE');
|
||||
}
|
||||
|
||||
static applyToClass(structure) {
|
||||
for (const prop of ['playAudio', 'playVideo']) {
|
||||
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(PlayInterface.prototype, prop));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlayInterface;
|
||||
32
src/client/voice/util/Secretbox.js
Normal file
32
src/client/voice/util/Secretbox.js
Normal file
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const libs = {
|
||||
sodium: sodium => ({
|
||||
open: sodium.api.crypto_secretbox_open_easy,
|
||||
close: sodium.api.crypto_secretbox_easy,
|
||||
random: n => sodium.randombytes_buf(n),
|
||||
}),
|
||||
'libsodium-wrappers': sodium => ({
|
||||
open: sodium.crypto_secretbox_open_easy,
|
||||
close: sodium.crypto_secretbox_easy,
|
||||
random: n => sodium.randombytes_buf(n),
|
||||
}),
|
||||
tweetnacl: tweetnacl => ({
|
||||
open: tweetnacl.secretbox.open,
|
||||
close: tweetnacl.secretbox,
|
||||
random: n => tweetnacl.randomBytes(n),
|
||||
}),
|
||||
};
|
||||
|
||||
exports.methods = {};
|
||||
|
||||
(async () => {
|
||||
for (const libName of Object.keys(libs)) {
|
||||
try {
|
||||
const lib = require(libName);
|
||||
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready; // eslint-disable-line no-await-in-loop
|
||||
exports.methods = libs[libName](lib);
|
||||
break;
|
||||
} catch {} // eslint-disable-line no-empty
|
||||
}
|
||||
})();
|
||||
16
src/client/voice/util/Silence.js
Normal file
16
src/client/voice/util/Silence.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
const { Buffer } = require('node:buffer');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
|
||||
|
||||
class Silence extends Readable {
|
||||
_read() {
|
||||
this.push(SILENCE_FRAME);
|
||||
}
|
||||
}
|
||||
|
||||
Silence.SILENCE_FRAME = SILENCE_FRAME;
|
||||
|
||||
module.exports = Silence;
|
||||
63
src/client/voice/util/Socket.js
Normal file
63
src/client/voice/util/Socket.js
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
Credit: https://github.com/dank074/Discord-video-stream
|
||||
The use of video streaming in this library is an incomplete implementation with many bugs, primarily aimed at lazy users.
|
||||
The video streaming features in this library are sourced from https://github.com/dank074/Discord-video-stream.
|
||||
|
||||
Please use the @dank074/discord-video-stream library to access all advanced and professional features,
|
||||
along with comprehensive support. I will not actively fix bugs related to streaming and encourage everyone to
|
||||
use https://github.com/dank074/Discord-video-stream for stable and smooth streaming.
|
||||
|
||||
To reiterate: This is an incomplete implementation of the library https://github.com/dank074/Discord-video-stream.
|
||||
|
||||
Thanks to dank074 and longnguyen2004 for implementing new codecs (H264, H265).
|
||||
Thanks to mrjvs for discovering how Discord transmits data and the VP8 codec.
|
||||
|
||||
Please use the @dank074/discord-video-stream library for the best support.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const process = require('process');
|
||||
|
||||
let counter = 0;
|
||||
|
||||
class UnixStream {
|
||||
constructor(stream, onSocket) {
|
||||
if (process.platform === 'win32') {
|
||||
const pipePrefix = '\\\\.\\pipe\\';
|
||||
const pipeName = `node-webrtc.${++counter}.sock`;
|
||||
|
||||
this.socketPath = path.join(pipePrefix, pipeName);
|
||||
this.url = this.socketPath;
|
||||
} else {
|
||||
this.socketPath = `./${++counter}.sock`;
|
||||
this.url = `unix:${this.socketPath}`;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.statSync(this.socketPath);
|
||||
fs.unlinkSync(this.socketPath);
|
||||
} catch (err) {
|
||||
console.error('UnixStream', err);
|
||||
}
|
||||
|
||||
const server = net.createServer(onSocket);
|
||||
stream.on('finish', () => {
|
||||
server.close();
|
||||
});
|
||||
server.listen(this.socketPath);
|
||||
}
|
||||
}
|
||||
|
||||
function StreamInput(stream) {
|
||||
return new UnixStream(stream, socket => stream.pipe(socket));
|
||||
}
|
||||
|
||||
function StreamOutput(stream) {
|
||||
return new UnixStream(stream, socket => socket.pipe(stream));
|
||||
}
|
||||
|
||||
module.exports = { StreamOutput, StreamInput };
|
||||
104
src/client/voice/util/VolumeInterface.js
Normal file
104
src/client/voice/util/VolumeInterface.js
Normal file
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { Buffer } = require('node:buffer');
|
||||
|
||||
/**
|
||||
* An interface class for volume transformation.
|
||||
* @extends {EventEmitter}
|
||||
*/
|
||||
class VolumeInterface extends EventEmitter {
|
||||
constructor({ volume = 1 } = {}) {
|
||||
super();
|
||||
this.setVolume(volume);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the volume of this stream is editable
|
||||
* @type {boolean}
|
||||
* @readonly
|
||||
*/
|
||||
get volumeEditable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the stream
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get volume() {
|
||||
return this._volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the stream in decibels
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get volumeDecibels() {
|
||||
return Math.log10(this.volume) * 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current volume of the stream from a logarithmic scale
|
||||
* @type {number}
|
||||
* @readonly
|
||||
*/
|
||||
get volumeLogarithmic() {
|
||||
return Math.pow(this.volume, 1 / 1.660964);
|
||||
}
|
||||
|
||||
applyVolume(buffer, volume) {
|
||||
volume = volume || this._volume;
|
||||
if (volume === 1) return buffer;
|
||||
|
||||
const out = Buffer.alloc(buffer.length);
|
||||
for (let i = 0; i < buffer.length; i += 2) {
|
||||
if (i >= buffer.length - 1) break;
|
||||
const uint = Math.min(32767, Math.max(-32767, Math.floor(volume * buffer.readInt16LE(i))));
|
||||
out.writeInt16LE(uint, i);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
|
||||
* @param {number} volume The volume that you want to set
|
||||
*/
|
||||
setVolume(volume) {
|
||||
/**
|
||||
* Emitted when the volume of this interface changes.
|
||||
* @event VolumeInterface#volumeChange
|
||||
* @param {number} oldVolume The old volume of this interface
|
||||
* @param {number} newVolume The new volume of this interface
|
||||
*/
|
||||
this.emit('volumeChange', this._volume, volume);
|
||||
this._volume = volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume in decibels.
|
||||
* @param {number} db The decibels
|
||||
*/
|
||||
setVolumeDecibels(db) {
|
||||
this.setVolume(Math.pow(10, db / 20));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the volume so that a perceived value of 0.5 is half the perceived volume etc.
|
||||
* @param {number} value The value for the volume
|
||||
*/
|
||||
setVolumeLogarithmic(value) {
|
||||
this.setVolume(Math.pow(value, 1.660964));
|
||||
}
|
||||
}
|
||||
|
||||
const props = ['volumeDecibels', 'volumeLogarithmic', 'setVolumeDecibels', 'setVolumeLogarithmic'];
|
||||
|
||||
exports.applyToClass = function applyToClass(structure) {
|
||||
for (const prop of props) {
|
||||
Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(VolumeInterface.prototype, prop));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user