Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdaa30af1a | ||
|
|
94bb33308a | ||
|
|
a5b17dfd2a | ||
|
|
f84a115e32 | ||
|
|
ac20d12b13 | ||
|
|
2b90b2775f | ||
|
|
00875123f5 | ||
|
|
04eba4de45 | ||
|
|
7bd03b5e5f | ||
|
|
fefad30ad5 | ||
|
|
9b14d8e9f0 | ||
|
|
2430f6e667 | ||
|
|
afe67ebdc1 | ||
|
|
c4cd464bfa | ||
|
|
08b103a26b | ||
|
|
f83d78ab4e | ||
|
|
242de19cf2 | ||
|
|
36f546eaba |
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
name: Release and Deploy Build
|
name: CD
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -6,11 +6,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
|
||||||
VERSION_MAJOR: '1'
|
|
||||||
VERSION_MINOR: '1'
|
|
||||||
VERSION_PATCH: ${{ gitea.run_number }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -22,10 +17,10 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Use NodeJS 24
|
- name: Use Node.js 24.17.0
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24.17.0'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
package-manager-cache: false
|
package-manager-cache: false
|
||||||
|
|
||||||
@@ -33,8 +28,9 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Set Release Version
|
- name: Set Release Version
|
||||||
id: version
|
|
||||||
run: npm run version:place
|
run: npm run version:place
|
||||||
|
env:
|
||||||
|
VERSION_PATCH: ${{ gitea.run_number }}
|
||||||
|
|
||||||
- name: Export Release Version
|
- name: Export Release Version
|
||||||
id: release_version
|
id: release_version
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build Validation
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -6,25 +6,78 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: use NodeJS 24
|
- name: Use Node.js 24.17.0
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24.17.0'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run Checks
|
- name: Check Version
|
||||||
run: npm run check
|
run: npm run version:check
|
||||||
|
|
||||||
|
- name: Check Formatting
|
||||||
|
run: npm run format:check
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 24.17.0
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24.17.0'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run Lint
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 24.17.0
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24.17.0'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run Type Check
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
test-and-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 24.17.0
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24.17.0'
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: npm test -- --runInBand
|
||||||
|
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Discord Audio Stream
|
# Discord Audio Stream
|
||||||
|
|
||||||
[](http://npmjs.org/package/discord-audio-stream)
|
[](https://www.npmjs.com/package/discord-audio-stream)
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
`discord-audio-stream` is a small TypeScript library for managed Discord voice playback through
|
`discord-audio-stream` is a small TypeScript library for managed Discord voice playback through
|
||||||
`@discordjs/voice` and ffmpeg.
|
`@discordjs/voice` and ffmpeg.
|
||||||
@@ -18,7 +18,7 @@ Create an [issue](https://git.lechner-systems.at/FrauJulian/Discord-Audio-Stream
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Node.js `24.x` is required.
|
Node.js `>=22.12.0` and `<25` is required.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install discord-audio-stream @discordjs/voice @discordjs/opus
|
npm install discord-audio-stream @discordjs/voice @discordjs/opus
|
||||||
@@ -114,6 +114,7 @@ type AudioManagerOptions = {
|
|||||||
};
|
};
|
||||||
source?: { type: 'url'; url: string } | { type: 'file'; path: string };
|
source?: { type: 'url'; url: string } | { type: 'file'; path: string };
|
||||||
renewIntervalMs?: number | false;
|
renewIntervalMs?: number | false;
|
||||||
|
reconnectAttempts?: number;
|
||||||
connectTimeoutMs?: number;
|
connectTimeoutMs?: number;
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
volume?: {
|
volume?: {
|
||||||
@@ -123,21 +124,26 @@ type AudioManagerOptions = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`connectTimeoutMs` and numeric `renewIntervalMs` values must be whole milliseconds from `1` through `2_147_483_647`.
|
||||||
|
`reconnectAttempts` must be a non-negative safe integer. URL sources must use `http:` or `https:`.
|
||||||
|
`volume.initialPercent` must be a finite number from `0` through `100` and requires `volume.enabled: true`.
|
||||||
|
|
||||||
### Defaults
|
### Defaults
|
||||||
|
|
||||||
| Option | Default |
|
| Option | Default |
|
||||||
| ------------------ | ---------- |
|
| ------------------- | ---------- |
|
||||||
| `ffmpeg.mode` | `'native'` |
|
| `ffmpeg.mode` | `'native'` |
|
||||||
| `connectTimeoutMs` | `20_000` |
|
| `connectTimeoutMs` | `20_000` |
|
||||||
| `renewIntervalMs` | `false` |
|
| `renewIntervalMs` | `false` |
|
||||||
|
| `reconnectAttempts` | `0` |
|
||||||
| `volume.enabled` | `false` |
|
| `volume.enabled` | `false` |
|
||||||
|
|
||||||
### Methods
|
### Methods
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
| ------------------------ | ---------------------------------------------------------------------------------- |
|
| ------------------------ | ---------------------------------------------------------------------------------- |
|
||||||
| `setConnection(options)` | Replaces the voice connection target. |
|
| `setConnection(options)` | Sets the target for the next `connect()` or `start()` call. |
|
||||||
| `setSource(source)` | Replaces the audio source. |
|
| `setSource(source)` | Sets the source for the next `play()` or `start()` call. |
|
||||||
| `connect()` | Joins the configured Discord voice channel. |
|
| `connect()` | Joins the configured Discord voice channel. |
|
||||||
| `play(source?)` | Starts playback on an existing connection. |
|
| `play(source?)` | Starts playback on an existing connection. |
|
||||||
| `start()` | Connects and starts playback. |
|
| `start()` | Connects and starts playback. |
|
||||||
@@ -185,6 +191,9 @@ Connection renewal is disabled by default because `@discordjs/voice` handles rec
|
|||||||
`renewIntervalMs` only when an application has a measured need for periodic restarts. `stop()` and `dispose()` always
|
`renewIntervalMs` only when an application has a measured need for periodic restarts. `stop()` and `dispose()` always
|
||||||
clear the renewal timer.
|
clear the renewal timer.
|
||||||
|
|
||||||
|
After an unrecoverable disconnect, playback stops by default. Set `reconnectAttempts` to retry joining the configured
|
||||||
|
voice channel and restarting the current source.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
The package exports these error classes:
|
The package exports these error classes:
|
||||||
@@ -200,6 +209,8 @@ operations, such as calling `pause()` while nothing is playing, throw `AudioMana
|
|||||||
Use `onError` to observe asynchronous audio player and voice connection errors. The manager cleans up failed playback
|
Use `onError` to observe asynchronous audio player and voice connection errors. The manager cleans up failed playback
|
||||||
and unrecoverable connections before invoking the callback.
|
and unrecoverable connections before invoking the callback.
|
||||||
|
|
||||||
|
`onError` does not handle rejected `connect()`, `play()`, or `start()` calls. Await and catch those calls explicitly.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Generated
+1857
-1685
File diff suppressed because it is too large.
Load diff
+10
-10
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://json.schemastore.org/package.json",
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
"name": "discord-audio-stream",
|
"name": "discord-audio-stream",
|
||||||
"version": "x.x.x",
|
"version": "x.x.x",
|
||||||
"license": "LGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"description": "A small Discord voice audio streaming library with managed ffmpeg playback.",
|
"description": "A small Discord voice audio streaming library with managed ffmpeg playback.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
@@ -70,22 +70,22 @@
|
|||||||
"@discordjs/opus": "^0.10.0",
|
"@discordjs/opus": "^0.10.0",
|
||||||
"@jest/globals": "^30.4.1",
|
"@jest/globals": "^30.4.1",
|
||||||
"@types/ejs": "^3.1.5",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.19.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
"@typescript-eslint/eslint-plugin": "^8.71.0",
|
||||||
"@typescript-eslint/parser": "^8.67.0",
|
"@typescript-eslint/parser": "^8.71.0",
|
||||||
|
"@typescript/native": "npm:typescript@^7.0.2",
|
||||||
"eslint": "^9.39.5",
|
"eslint": "^9.39.5",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-import-resolver-typescript": "^4.4.5",
|
"eslint-import-resolver-typescript": "^4.4.5",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-n": "^18.3.0",
|
"eslint-plugin-n": "^18.4.0",
|
||||||
"eslint-plugin-promise": "^7.3.0",
|
"eslint-plugin-promise": "^7.3.0",
|
||||||
"eslint-plugin-unused-imports": "^4.4.1",
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^30.4.2",
|
"jest": "^30.5.2",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.9",
|
||||||
"ts-jest": "^29.4.12",
|
"ts-jest": "^29.4.14",
|
||||||
"tsup": "^8.5.1",
|
"tsup": "^8.5.1",
|
||||||
"@typescript/native": "npm:typescript@^7.0.2",
|
|
||||||
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
"esbuild": "^0.28.2"
|
"esbuild": "^0.28.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "24.x"
|
"node": ">=22.12.0 <25"
|
||||||
},
|
},
|
||||||
"private": false,
|
"private": false,
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
+6
-9
@@ -3,6 +3,8 @@ const fs = require('fs');
|
|||||||
const lockFilePath = 'package-lock.json';
|
const lockFilePath = 'package-lock.json';
|
||||||
const packageFilePath = 'package.json';
|
const packageFilePath = 'package.json';
|
||||||
const placeholderVersion = 'x.x.x';
|
const placeholderVersion = 'x.x.x';
|
||||||
|
const versionMajor = 1;
|
||||||
|
const versionMinor = 2;
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
|
|
||||||
function readJson(path) {
|
function readJson(path) {
|
||||||
@@ -24,7 +26,7 @@ function getLockFile() {
|
|||||||
function writeVersions(version) {
|
function writeVersions(version) {
|
||||||
const pkg = readJson(packageFilePath);
|
const pkg = readJson(packageFilePath);
|
||||||
pkg.version = version;
|
pkg.version = version;
|
||||||
writeJson(packageFilePath, pkg, 2);
|
writeJson(packageFilePath, pkg, 4);
|
||||||
|
|
||||||
const lock = getLockFile();
|
const lock = getLockFile();
|
||||||
|
|
||||||
@@ -74,19 +76,14 @@ function fixPlaceholder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCiVersion() {
|
function getCiVersion() {
|
||||||
const major = process.env.VERSION_MAJOR;
|
|
||||||
const minor = process.env.VERSION_MINOR;
|
|
||||||
const patch = process.env.VERSION_PATCH;
|
const patch = process.env.VERSION_PATCH;
|
||||||
const segments = { major, minor, patch };
|
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(segments)) {
|
if (!/^\d+$/.test(patch ?? '')) {
|
||||||
if (!/^\d+$/.test(value ?? '')) {
|
console.error(`Missing or invalid patch version segment: "${patch ?? ''}"`);
|
||||||
console.error(`Missing or invalid ${name} version segment: "${value ?? ''}"`);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return `${major}.${minor}.${patch}`;
|
return `${versionMajor}.${versionMinor}.${patch}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'check-placeholder') {
|
if (command === 'check-placeholder') {
|
||||||
|
|||||||
+35
-1
@@ -48,6 +48,7 @@ export default class AudioManager implements Disposable {
|
|||||||
private connectAttempt: AbortController | undefined;
|
private connectAttempt: AbortController | undefined;
|
||||||
private renewTimer: NodeJS.Timeout | undefined;
|
private renewTimer: NodeJS.Timeout | undefined;
|
||||||
private playbackState: PlaybackState = 'idle';
|
private playbackState: PlaybackState = 'idle';
|
||||||
|
private reconnectGeneration = 0;
|
||||||
private connectionOptions: VoiceConnectionOptions | undefined;
|
private connectionOptions: VoiceConnectionOptions | undefined;
|
||||||
private audioSource: AudioSource | undefined;
|
private audioSource: AudioSource | undefined;
|
||||||
private readonly options: Required<Pick<AudioManagerOptions, 'connectTimeoutMs'>> &
|
private readonly options: Required<Pick<AudioManagerOptions, 'connectTimeoutMs'>> &
|
||||||
@@ -61,6 +62,13 @@ export default class AudioManager implements Disposable {
|
|||||||
assertValidTimerDelay(options.renewIntervalMs, 'renewIntervalMs');
|
assertValidTimerDelay(options.renewIntervalMs, 'renewIntervalMs');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.reconnectAttempts !== undefined &&
|
||||||
|
(!Number.isSafeInteger(options.reconnectAttempts) || options.reconnectAttempts < 0)
|
||||||
|
) {
|
||||||
|
throw new AudioManagerConfigError('reconnectAttempts must be a non-negative safe integer.');
|
||||||
|
}
|
||||||
|
|
||||||
if (options.volume?.initialPercent !== undefined) {
|
if (options.volume?.initialPercent !== undefined) {
|
||||||
if (options.volume.enabled !== true) {
|
if (options.volume.enabled !== true) {
|
||||||
throw new AudioManagerConfigError('volume.initialPercent requires volume.enabled to be true.');
|
throw new AudioManagerConfigError('volume.initialPercent requires volume.enabled to be true.');
|
||||||
@@ -254,6 +262,7 @@ export default class AudioManager implements Disposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.reconnectGeneration += 1;
|
||||||
this.cancelConnectAttempt();
|
this.cancelConnectAttempt();
|
||||||
this.clearRenewTimer();
|
this.clearRenewTimer();
|
||||||
this.stopCurrentPlayback();
|
this.stopCurrentPlayback();
|
||||||
@@ -285,6 +294,7 @@ export default class AudioManager implements Disposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.reconnectGeneration += 1;
|
||||||
this.cancelConnectAttempt();
|
this.cancelConnectAttempt();
|
||||||
this.clearRenewTimer();
|
this.clearRenewTimer();
|
||||||
this.stopCurrentPlayback();
|
this.stopCurrentPlayback();
|
||||||
@@ -307,8 +317,13 @@ export default class AudioManager implements Disposable {
|
|||||||
|
|
||||||
if (this.audioSource.type === 'url') {
|
if (this.audioSource.type === 'url') {
|
||||||
try {
|
try {
|
||||||
|
const url = new URL(this.audioSource.url);
|
||||||
|
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||||
|
throw new Error('Unsupported audio source protocol.');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input: new URL(this.audioSource.url).toString(),
|
input: url.toString(),
|
||||||
source: this.audioSource,
|
source: this.audioSource,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -361,6 +376,9 @@ export default class AudioManager implements Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleDisconnectedConnection(connection: VoiceConnection): Promise<void> {
|
private async handleDisconnectedConnection(connection: VoiceConnection): Promise<void> {
|
||||||
|
const shouldReconnect = this.resource !== undefined && (this.options.reconnectAttempts ?? 0) > 0;
|
||||||
|
const reconnectGeneration = this.reconnectGeneration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
entersState(connection, VoiceConnectionStatus.Signalling, DISCONNECT_RECOVERY_TIMEOUT_MS),
|
entersState(connection, VoiceConnectionStatus.Signalling, DISCONNECT_RECOVERY_TIMEOUT_MS),
|
||||||
@@ -371,6 +389,22 @@ export default class AudioManager implements Disposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldReconnect) {
|
||||||
|
this.clearRenewTimer();
|
||||||
|
for (let attempt = 0; attempt < (this.options.reconnectAttempts ?? 0); attempt += 1) {
|
||||||
|
try {
|
||||||
|
await this.start();
|
||||||
|
return;
|
||||||
|
} catch (reconnectError) {
|
||||||
|
error = reconnectError;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reconnectGeneration !== reconnectGeneration || this.playbackState === 'disposed') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.clearRenewTimer();
|
this.clearRenewTimer();
|
||||||
this.stopCurrentPlayback();
|
this.stopCurrentPlayback();
|
||||||
this.audioPlayer.stop(true);
|
this.audioPlayer.stop(true);
|
||||||
|
|||||||
+9
-1
@@ -112,7 +112,7 @@ export type VolumeOptions = {
|
|||||||
/**
|
/**
|
||||||
* Initial volume percentage applied when playback starts.
|
* Initial volume percentage applied when playback starts.
|
||||||
*
|
*
|
||||||
* Requires `enabled: true`.
|
* Must be a finite number from `0` through `100` and requires `enabled: true`.
|
||||||
*/
|
*/
|
||||||
initialPercent?: number;
|
initialPercent?: number;
|
||||||
};
|
};
|
||||||
@@ -144,13 +144,21 @@ export type AudioManagerOptions = {
|
|||||||
* Milliseconds after which the manager reconnects and restarts playback.
|
* Milliseconds after which the manager reconnects and restarts playback.
|
||||||
*
|
*
|
||||||
* Renewal is disabled unless an interval is provided.
|
* Renewal is disabled unless an interval is provided.
|
||||||
|
* Numeric values must be integers from `1` through `2_147_483_647`.
|
||||||
*
|
*
|
||||||
* @defaultValue `false`
|
* @defaultValue `false`
|
||||||
*/
|
*/
|
||||||
renewIntervalMs?: number | false;
|
renewIntervalMs?: number | false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of full reconnect and playback restart attempts after an unrecoverable disconnect.
|
||||||
|
* Disabled by default. Must be a non-negative safe integer.
|
||||||
|
*/
|
||||||
|
reconnectAttempts?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum milliseconds to wait for the Discord voice connection to become ready.
|
* Maximum milliseconds to wait for the Discord voice connection to become ready.
|
||||||
|
* Must be an integer from `1` through `2_147_483_647`.
|
||||||
*
|
*
|
||||||
* @defaultValue `20_000`
|
* @defaultValue `20_000`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -235,6 +235,28 @@ describe('AudioManager', () => {
|
|||||||
expect(manager.isConnected).toBe(false);
|
expect(manager.isConnected).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reconnects and restarts playback when configured', async () => {
|
||||||
|
const manager = new AudioManager({
|
||||||
|
connection: connectionOptions,
|
||||||
|
source: liveStreamSource,
|
||||||
|
renewIntervalMs: false,
|
||||||
|
reconnectAttempts: 1,
|
||||||
|
});
|
||||||
|
await manager.start();
|
||||||
|
jest.mocked(entersState)
|
||||||
|
.mockRejectedValueOnce(new Error('disconnect'))
|
||||||
|
.mockRejectedValueOnce(new Error('disconnect'));
|
||||||
|
jest.mocked(joinVoiceChannel).mockReturnValueOnce(mockSecondVoiceConnection);
|
||||||
|
mockConnection.state.status = VoiceConnectionStatus.Disconnected;
|
||||||
|
|
||||||
|
await listenerFor(connectionListeners, VoiceConnectionStatus.Disconnected)();
|
||||||
|
|
||||||
|
expect(joinVoiceChannel).toHaveBeenCalledTimes(2);
|
||||||
|
expect(startFfmpeg).toHaveBeenCalledTimes(2);
|
||||||
|
expect(manager.state).toBe('playing');
|
||||||
|
expect(manager.isConnected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('cleans up when connection startup fails', async () => {
|
it('cleans up when connection startup fails', async () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
|
||||||
@@ -599,6 +621,18 @@ describe('AudioManager', () => {
|
|||||||
await expect(manager.play()).rejects.toThrow(AudioManagerConfigError);
|
await expect(manager.play()).rejects.toThrow(AudioManagerConfigError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(['file:///etc/passwd', 'ftp://example.com/audio.mp3'])('rejects non-HTTP URL source %s', async (url) => {
|
||||||
|
const manager = new AudioManager({
|
||||||
|
connection: connectionOptions,
|
||||||
|
source: { type: 'url', url },
|
||||||
|
renewIntervalMs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.connect();
|
||||||
|
await expect(manager.play()).rejects.toThrow(AudioManagerConfigError);
|
||||||
|
expect(startFfmpeg).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('applies initial volume only when volume support is enabled', async () => {
|
it('applies initial volume only when volume support is enabled', async () => {
|
||||||
const manager = new AudioManager({
|
const manager = new AudioManager({
|
||||||
connection: connectionOptions,
|
connection: connectionOptions,
|
||||||
|
|||||||
Reference in new issue
Block a user