fixed memory leak; removed unit tests;
This commit is contained in:
@@ -18,9 +18,32 @@ Please create an [issue](https://github.com/FrauJulian/DiscordAudioStreamNPM/iss
|
|||||||
### Install package
|
### Install package
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install discord-audio-stream opusscript @discordjs/voice ffmpeg-static sodium-native
|
npm install discord-audio-stream
|
||||||
|
yarn add discord-audio-stream
|
||||||
|
pnpm add discord-audio-stream
|
||||||
|
bun add discord-audio-stream
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Encryption Libraries (npm install):**
|
||||||
|
|
||||||
|
> You only need to install one of these libraries if your system does not support `aes-256-gcm` (verify by running `require('node:crypto').getCiphers().includes('aes-256-gcm')`).
|
||||||
|
|
||||||
|
- `sodium-native`
|
||||||
|
- `sodium`
|
||||||
|
- `@stablelib/xchacha20poly1305`
|
||||||
|
- `@noble/ciphers`
|
||||||
|
- `libsodium-wrappers`
|
||||||
|
|
||||||
|
**Opus Libraries (npm install):**
|
||||||
|
|
||||||
|
- `@discordjs/opus`
|
||||||
|
- `opusscript`
|
||||||
|
|
||||||
|
**FFmpeg:**
|
||||||
|
|
||||||
|
- [`FFmpeg`](https://ffmpeg.org/) (installed and added to environment)
|
||||||
|
- `ffmpeg-static`
|
||||||
|
|
||||||
### Create Instance
|
### Create Instance
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -65,6 +88,7 @@ let audioManager = new AudioManager({
|
|||||||
| `StopAudioOnConnection` | | void | Method to stop the audio without destroying voice connection. |
|
| `StopAudioOnConnection` | | void | Method to stop the audio without destroying voice connection. |
|
||||||
| `CreateConnectionAndPlayAudio` | | void | Method to join the voice connection and play audio. |
|
| `CreateConnectionAndPlayAudio` | | void | Method to join the voice connection and play audio. |
|
||||||
| `DestroyConnection` | | void | Method to destroy the voice connection. |
|
| `DestroyConnection` | | void | Method to destroy the voice connection. |
|
||||||
|
| `Dispose` | | void | Dispose all data in object. |
|
||||||
| `SetVolume` | `volume` (type of number, 0 - 100 percent) | void | Method to set the audio volume. | Method to set the volume of the audio. |
|
| `SetVolume` | `volume` (type of number, 0 - 100 percent) | void | Method to set the audio volume. | Method to set the volume of the audio. |
|
||||||
| `SetMaxListeners` | `maxListeners` (type of number) | void | Method to set the max listeners of the audio stream. |
|
| `SetMaxListeners` | `maxListeners` (type of number) | void | Method to set the max listeners of the audio stream. |
|
||||||
|
|
||||||
|
|||||||
-122
@@ -1,122 +0,0 @@
|
|||||||
// Jest unit tests for the AudioManager class focusing on functionality and performance
|
|
||||||
|
|
||||||
import AudioManager from './src/audioManager'
|
|
||||||
import { performance } from 'node:perf_hooks'
|
|
||||||
|
|
||||||
// Mocking @discordjs/voice
|
|
||||||
jest.mock('@discordjs/voice', () => {
|
|
||||||
return {
|
|
||||||
joinVoiceChannel: jest.fn(() => ({
|
|
||||||
subscribe: jest.fn(),
|
|
||||||
destroy: jest.fn(),
|
|
||||||
setMaxListeners: jest.fn()
|
|
||||||
})),
|
|
||||||
createAudioPlayer: jest.fn(() => ({
|
|
||||||
play: jest.fn(),
|
|
||||||
pause: jest.fn(),
|
|
||||||
unpause: jest.fn()
|
|
||||||
})),
|
|
||||||
createAudioResource: jest.fn(() => ({
|
|
||||||
volume: { setVolume: jest.fn() }
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
type VoiceConnectionDataModel = {
|
|
||||||
VoiceChannelId: string
|
|
||||||
GuildId: string
|
|
||||||
VoiceAdapter: any
|
|
||||||
}
|
|
||||||
|
|
||||||
type VoiceAudioDataModel = {
|
|
||||||
ResourceType: 'Link' | 'File'
|
|
||||||
Resource: string | any
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionData: VoiceConnectionDataModel = {
|
|
||||||
VoiceChannelId: '1',
|
|
||||||
GuildId: '1',
|
|
||||||
VoiceAdapter: jest.fn()
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioDataFile: VoiceAudioDataModel = {
|
|
||||||
ResourceType: 'File',
|
|
||||||
Resource: 'sound.mp3'
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioDataLink: VoiceAudioDataModel = {
|
|
||||||
ResourceType: 'Link',
|
|
||||||
Resource: 'https://example.com/sound.mp3'
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('AudioManager', () => {
|
|
||||||
beforeEach(() => jest.clearAllMocks())
|
|
||||||
|
|
||||||
test('CreateConnection initializes voice connection', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataFile)
|
|
||||||
const start = performance.now()
|
|
||||||
manager.CreateConnection()
|
|
||||||
const duration = performance.now() - start
|
|
||||||
expect(require('@discordjs/voice').joinVoiceChannel).toHaveBeenCalled()
|
|
||||||
expect(duration).toBeLessThan(50)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('PlayAudioOnConnection loads file resource', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataFile)
|
|
||||||
manager.CreateConnection()
|
|
||||||
manager.PlayAudioOnConnection()
|
|
||||||
const voice = require('@discordjs/voice')
|
|
||||||
expect(voice.createAudioResource).toHaveBeenCalled()
|
|
||||||
expect(voice.createAudioPlayer).toHaveBeenCalled()
|
|
||||||
expect(manager['IsAudioPlaying']).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('PlayAudioOnConnection loads link resource', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataLink)
|
|
||||||
manager.CreateConnection()
|
|
||||||
manager.PlayAudioOnConnection()
|
|
||||||
const voice = require('@discordjs/voice')
|
|
||||||
expect(voice.createAudioResource).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('StopAudioOnConnection resets connection', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataFile)
|
|
||||||
manager.CreateConnection()
|
|
||||||
manager.PlayAudioOnConnection()
|
|
||||||
manager.StopAudioOnConnection()
|
|
||||||
const voice = require('@discordjs/voice')
|
|
||||||
expect(voice.joinVoiceChannel).toHaveBeenCalledTimes(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Pause and Resume audio', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataFile)
|
|
||||||
manager.CreateConnection()
|
|
||||||
manager.PlayAudioOnConnection()
|
|
||||||
manager.PauseAudio()
|
|
||||||
manager.ResumeAudio()
|
|
||||||
const voice = require('@discordjs/voice')
|
|
||||||
const player = voice.createAudioPlayer.mock.results[0].value
|
|
||||||
expect(player.pause).toHaveBeenCalled()
|
|
||||||
expect(player.unpause).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('SetVolume validates range', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataFile)
|
|
||||||
manager.CreateConnection()
|
|
||||||
manager.PlayAudioOnConnection()
|
|
||||||
manager.SetVolume(50)
|
|
||||||
const res = require('@discordjs/voice').createAudioResource.mock.results[0].value
|
|
||||||
expect(res.volume.setVolume).toHaveBeenCalledWith(0.5)
|
|
||||||
expect(() => manager.SetVolume(101)).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('DestroyConnection clears flags', () => {
|
|
||||||
const manager = new AudioManager(connectionData, audioDataFile)
|
|
||||||
manager.CreateConnection()
|
|
||||||
manager.DestroyConnection()
|
|
||||||
const voice = require('@discordjs/voice')
|
|
||||||
const connection = voice.joinVoiceChannel.mock.results[0].value
|
|
||||||
expect(connection.destroy).toHaveBeenCalled()
|
|
||||||
expect(manager['IsActive']).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
testMatch: ['**/Unittests.ts'],
|
|
||||||
transform: {
|
|
||||||
'^.+\\.tsx?$': 'ts-jest'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Generated
+7244
-7464
File diff suppressed because it is too large
Load Diff
+64
-80
@@ -1,83 +1,67 @@
|
|||||||
{
|
{
|
||||||
"name": "discord-audio-stream",
|
"name": "discord-audio-stream",
|
||||||
"version": "0.6.12",
|
"version": "0.6.12",
|
||||||
"license": "LGPL-2.1-only",
|
"license": "LGPL-2.1-only",
|
||||||
"description": "NodeJS library to make stream any audio on discord easier.",
|
"description": "NodeJS library to make stream any audio on discord easier.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"require": {
|
"require": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"types": "./dist/index.d.mts",
|
"types": "./dist/index.d.mts",
|
||||||
"default": "./dist/index.mjs"
|
"default": "./dist/index.mjs"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"module": "./dist/index.mjs",
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"directories": {
|
|
||||||
"lib": "src"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"updateDependencies": "npx npm-check-updates --upgrade && npm install && echo SUCCEED UPDATE",
|
|
||||||
"cleanSolution": "del-cli dist && echo SUCCEED CLEAN",
|
|
||||||
"updateVersion": "npm version patch --no-git-tag-version && echo SUCCEED UPDATE VERSION",
|
|
||||||
"buildPackage": "tsup && echo SUCCEED BUILD && npm run updateVersion",
|
|
||||||
"rolloutBuild": "npm run updateDependencies && npm run cleanSolution && tsup",
|
|
||||||
"unit-tests": "npx jest --config jest.config.js --verbose --detectOpenHandles"
|
|
||||||
},
|
|
||||||
"repository": "https://github.com/FrauJulian/Discord-Audio-Stream",
|
|
||||||
"bugs": "https://github.com/FrauJulian/Discord-Audio-Stream/issues",
|
|
||||||
"funding": "https://ko-fi.com/FrauJulian",
|
|
||||||
"keywords": [
|
|
||||||
"discordjs",
|
|
||||||
"discord.js",
|
|
||||||
"discord",
|
|
||||||
"audio",
|
|
||||||
"voice",
|
|
||||||
"stream"
|
|
||||||
],
|
|
||||||
"contributors": [
|
|
||||||
{
|
|
||||||
"name": "Lechner Julian",
|
|
||||||
"nickname": "FrauJulian",
|
|
||||||
"email": "office@lechner-systems.at",
|
|
||||||
"website": "https://lechner-systems.at/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/ciphers": "^1.3.0",
|
|
||||||
"@stablelib/xchacha20poly1305": "^2.0.1",
|
|
||||||
"sodium-native": "^5.0.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/ejs": "^3.1.5",
|
|
||||||
"@types/jest": "^30.0.0",
|
|
||||||
"@types/node": "^24.1.0",
|
|
||||||
"del-cli": "^6.0.0",
|
|
||||||
"jest": "^30.0.5",
|
|
||||||
"npm-check-updates": "^18.0.2",
|
|
||||||
"ts-jest": "^29.4.0",
|
|
||||||
"tsup": "^8.5.0",
|
|
||||||
"typescript": "^5.8.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@discordjs/voice": "^0.18.0",
|
|
||||||
"ffmpeg-static": "^5.2.0",
|
|
||||||
"libsodium-wrappers": "^0.7.15",
|
|
||||||
"opusscript": "^0.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"private": false,
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.mjs",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"directories": {
|
||||||
|
"lib": "src"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"updateDependencies": "npx npm-check-updates --upgrade && npm install && echo SUCCEED UPDATE",
|
||||||
|
"cleanSolution": "del-cli dist && echo SUCCEED CLEAN",
|
||||||
|
"updateVersion": "npm version patch --no-git-tag-version && echo SUCCEED UPDATE VERSION",
|
||||||
|
"buildPackage": "tsup && echo SUCCEED BUILD && npm run updateVersion",
|
||||||
|
"rolloutBuild": "npm run updateDependencies && npm run cleanSolution && tsup",
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/FrauJulian/Discord-Audio-Stream",
|
||||||
|
"bugs": "https://github.com/FrauJulian/Discord-Audio-Stream/issues",
|
||||||
|
"funding": "https://ko-fi.com/FrauJulian",
|
||||||
|
"keywords": [
|
||||||
|
"discordjs",
|
||||||
|
"discord.js",
|
||||||
|
"discord",
|
||||||
|
"audio",
|
||||||
|
"voice",
|
||||||
|
"stream"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"Lechner Julian - FrauJulian <office@lechner-systems.at>"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@discordjs/voice": "^0.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ejs": "^3.1.5",
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"del-cli": "^6.0.0",
|
||||||
|
"npm-check-updates": "^18.0.2",
|
||||||
|
"ts-jest": "^29.4.0",
|
||||||
|
"tsup": "^8.5.0",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"private": false,
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-16
@@ -11,22 +11,27 @@ import {VoiceConnectionDataModel} from "./types/VoiceConnectionDataModel";
|
|||||||
import {VoiceAudioDataModel} from "./types/VoiceAudioDataModel";
|
import {VoiceAudioDataModel} from "./types/VoiceAudioDataModel";
|
||||||
|
|
||||||
export default class AudioManager {
|
export default class AudioManager {
|
||||||
public VoiceConnection: VoiceConnection | null = null;
|
public VoiceConnection?: VoiceConnection = undefined;
|
||||||
public AudioPlayer: AudioPlayer | null = null;
|
public AudioPlayer?: AudioPlayer = undefined;
|
||||||
public AudioResource: AudioResource | null = null;
|
public AudioResource?: AudioResource = undefined;
|
||||||
|
|
||||||
protected IsActive: boolean = false;
|
protected IsActive?: boolean = undefined;
|
||||||
protected IsAudioPlaying: boolean = false;
|
protected IsAudioPlaying?: boolean = undefined;
|
||||||
|
|
||||||
protected ConnectionData: VoiceConnectionDataModel | null;
|
protected ConnectionData?: VoiceConnectionDataModel;
|
||||||
protected AudioData: VoiceAudioDataModel | null;
|
protected AudioData?: VoiceAudioDataModel;
|
||||||
|
|
||||||
constructor(connectionData: VoiceConnectionDataModel | null = null, audioData: VoiceAudioDataModel | null = null) {
|
private TimeoutHandle?: NodeJS.Timeout;
|
||||||
|
private RenewInMs?: number;
|
||||||
|
|
||||||
|
constructor(connectionData?: VoiceConnectionDataModel, audioData?: VoiceAudioDataModel, renewInMs = 5400000) {
|
||||||
|
this.RenewInMs = renewInMs;
|
||||||
this.ConnectionData = connectionData;
|
this.ConnectionData = connectionData;
|
||||||
this.AudioData = audioData;
|
this.AudioData = audioData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OverrideOptions(connectionData: VoiceConnectionDataModel | null = null, audioData: VoiceAudioDataModel | null = null): void {
|
public OverrideOptions(connectionData?: VoiceConnectionDataModel, audioData?: VoiceAudioDataModel, renewInMs = 5400000): void {
|
||||||
|
this.RenewInMs = renewInMs;
|
||||||
this.ConnectionData = connectionData;
|
this.ConnectionData = connectionData;
|
||||||
this.AudioData = audioData;
|
this.AudioData = audioData;
|
||||||
}
|
}
|
||||||
@@ -48,9 +53,9 @@ export default class AudioManager {
|
|||||||
adapterCreator: this.ConnectionData!.VoiceAdapter
|
adapterCreator: this.ConnectionData!.VoiceAdapter
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout((): void => {
|
this.TimeoutHandle = setTimeout((): void => {
|
||||||
this.RenewConnectionAndAudio();
|
this.RenewConnectionAndAudio();
|
||||||
}, 5400000);
|
}, this.RenewInMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayAudioOnConnection(): void {
|
public PlayAudioOnConnection(): void {
|
||||||
@@ -82,13 +87,21 @@ export default class AudioManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PauseAudio(): void {
|
public PauseAudio(): void {
|
||||||
this.CheckIfNull(this.AudioPlayer);
|
if (this.IsAudioPlaying) {
|
||||||
this.AudioPlayer!.pause();
|
this.AudioPlayer!.pause();
|
||||||
|
this.IsAudioPlaying = false;
|
||||||
|
} else {
|
||||||
|
throw new TypeError("Audio is not playing.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResumeAudio(): void {
|
public ResumeAudio(): void {
|
||||||
this.CheckIfNull(this.AudioPlayer);
|
if (!this.IsAudioPlaying) {
|
||||||
this.AudioPlayer!.unpause();
|
this.AudioPlayer!.unpause();
|
||||||
|
this.IsAudioPlaying = true;
|
||||||
|
} else {
|
||||||
|
throw new TypeError("Audio is playing.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreateConnectionAndPlayAudio(): void {
|
public CreateConnectionAndPlayAudio(): void {
|
||||||
@@ -107,15 +120,34 @@ export default class AudioManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SetMaxListeners(maxListeners: number): void {
|
public SetMaxListeners(maxListeners: number): void {
|
||||||
this.CheckIfNull(this.VoiceConnection);
|
|
||||||
this.VoiceConnection!.setMaxListeners(maxListeners);
|
this.VoiceConnection!.setMaxListeners(maxListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetVolume(volumeInPercent: number): void {
|
public SetVolume(volumeInPercent: number): void {
|
||||||
if (volumeInPercent < 0 || volumeInPercent > 100) throw new Error("Volume must be between 0 and 100.");
|
if (volumeInPercent < 0 || volumeInPercent > 100) throw new Error("Volume must be between 0 and 100.");
|
||||||
|
this.CheckIfNull(this.VoiceConnection);
|
||||||
this.AudioResource!.volume!.setVolume(volumeInPercent / 100);
|
this.AudioResource!.volume!.setVolume(volumeInPercent / 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Dispose(): void {
|
||||||
|
if (this.IsActive != undefined) {
|
||||||
|
this.DestroyConnection();
|
||||||
|
this.ConnectionData = undefined;
|
||||||
|
this.VoiceConnection = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.IsAudioPlaying != undefined) {
|
||||||
|
this.AudioData = undefined;
|
||||||
|
this.AudioResource = undefined;
|
||||||
|
this.AudioPlayer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.TimeoutHandle) {
|
||||||
|
clearTimeout(this.TimeoutHandle);
|
||||||
|
this.TimeoutHandle = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private RenewConnectionAndAudio(): void {
|
private RenewConnectionAndAudio(): void {
|
||||||
this.DestroyConnection(false);
|
this.DestroyConnection(false);
|
||||||
if (this.IsActive) {
|
if (this.IsActive) {
|
||||||
|
|||||||
Reference in New Issue
Block a user