feat: try implement djs voice + video (v12)

This commit is contained in:
Elysia
2024-07-24 19:27:50 +07:00
parent 7fa4666df0
commit 26aa85c126
31 changed files with 3768 additions and 9 deletions

View 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,
};

View 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;

View 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
}
})();

View 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;

View 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 };

View 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));
}
};