chore: update Discord-video-stream subproject to latest commit
This commit is contained in:
@@ -21,6 +21,9 @@ export interface MediaControllerDependencies {
|
||||
musicPlayer?: MusicPlayer;
|
||||
screenController?: ScreenShareController;
|
||||
onStateChange?: (state: MediaState) => void;
|
||||
initialMusicVolume?: number;
|
||||
onMusicVolumeChange?: (volume: number) => void | Promise<void>;
|
||||
setMusicVolume?: (volume: number) => void;
|
||||
}
|
||||
|
||||
export class MediaController {
|
||||
@@ -31,9 +34,18 @@ export class MediaController {
|
||||
private skipInProgress = false;
|
||||
private screenPlayback: ScreenSharePlayback | null = null;
|
||||
private activeMode: MediaMode | null = null;
|
||||
private musicVolume: number;
|
||||
private readonly setPlayerMusicVolume: (volume: number) => void;
|
||||
|
||||
constructor(private readonly dependencies: MediaControllerDependencies = {}) {
|
||||
this.musicPlayer = dependencies.musicPlayer ?? createMusicPlayer();
|
||||
this.setPlayerMusicVolume =
|
||||
dependencies.setMusicVolume ??
|
||||
((volume) => {
|
||||
discordPlayer.setMusicVolume(volume);
|
||||
});
|
||||
this.musicVolume = normalizeVolume(dependencies.initialMusicVolume, 1);
|
||||
this.setPlayerMusicVolume(this.musicVolume);
|
||||
}
|
||||
|
||||
getState(): MediaState {
|
||||
@@ -42,10 +54,20 @@ export class MediaController {
|
||||
playing:
|
||||
this.activeMode === "screen" || snapshot.current?.status === "playing",
|
||||
activeMode: this.activeMode ?? snapshot.current?.mode ?? null,
|
||||
musicVolume: this.musicVolume,
|
||||
...snapshot,
|
||||
};
|
||||
}
|
||||
|
||||
async setMusicVolume(volume: number): Promise<MediaState> {
|
||||
const nextVolume = normalizeVolume(volume, this.musicVolume);
|
||||
if (this.musicVolume === nextVolume) return this.emitState();
|
||||
this.musicVolume = nextVolume;
|
||||
this.setPlayerMusicVolume(nextVolume);
|
||||
await this.dependencies.onMusicVolumeChange?.(nextVolume);
|
||||
return this.emitState();
|
||||
}
|
||||
|
||||
async queue(
|
||||
source: string,
|
||||
options: QueueMediaOptions = {},
|
||||
@@ -201,3 +223,8 @@ export class MediaController {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeVolume(value: number | undefined, fallback: number): number {
|
||||
if (!Number.isFinite(value)) return fallback;
|
||||
return Math.max(0, Math.min(1, value as number));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Readable } from "node:stream";
|
||||
import type { StreamType } from "@discordjs/voice";
|
||||
|
||||
export type MediaMode = "music" | "screen";
|
||||
export type MediaSourceKind =
|
||||
@@ -26,6 +27,7 @@ export interface MediaQueueItem extends ResolvedMediaSource {
|
||||
export interface MediaState {
|
||||
playing: boolean;
|
||||
activeMode: MediaMode | null;
|
||||
musicVolume: number;
|
||||
current: MediaQueueItem | null;
|
||||
queue: MediaQueueItem[];
|
||||
}
|
||||
@@ -56,11 +58,23 @@ export interface ScreenShareController {
|
||||
|
||||
export type DiscordPlayerOwner = "none" | "browser-bridge" | "music" | "screen";
|
||||
|
||||
export interface DiscordPlayOptions {
|
||||
inputType?: StreamType;
|
||||
inlineVolume?: boolean;
|
||||
volume?: number;
|
||||
}
|
||||
|
||||
export interface DiscordAudioPlayer {
|
||||
getOwner(): DiscordPlayerOwner;
|
||||
isConnected(): boolean;
|
||||
playStream(stream: Readable, owner: DiscordPlayerOwner): void;
|
||||
playStream(
|
||||
stream: Readable,
|
||||
owner: DiscordPlayerOwner,
|
||||
options?: DiscordPlayOptions,
|
||||
): void;
|
||||
pause(owner?: DiscordPlayerOwner): void;
|
||||
unpause(owner?: DiscordPlayerOwner): boolean;
|
||||
stop(owner?: DiscordPlayerOwner): void;
|
||||
getMusicVolume(): number;
|
||||
setMusicVolume(volume: number): void;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { spawn as nodeSpawn } from "node:child_process";
|
||||
import { StreamType } from "@discordjs/voice";
|
||||
import { discordPlayer } from "../player";
|
||||
import type {
|
||||
DiscordAudioPlayer,
|
||||
@@ -30,7 +31,10 @@ export function createMusicPlayer(
|
||||
}) as unknown as ChildProcessWithoutNullStreams;
|
||||
proc.stderr.resume();
|
||||
|
||||
audioPlayer.playStream(proc.stdout, "music");
|
||||
audioPlayer.playStream(proc.stdout, "music", {
|
||||
inputType: StreamType.Raw,
|
||||
inlineVolume: true,
|
||||
});
|
||||
|
||||
let stopped = false;
|
||||
let released = false;
|
||||
@@ -81,13 +85,13 @@ export function buildFfmpegArgs(source: string): string[] {
|
||||
source,
|
||||
"-vn",
|
||||
"-acodec",
|
||||
"libopus",
|
||||
"pcm_s16le",
|
||||
"-ar",
|
||||
"48000",
|
||||
"-ac",
|
||||
"2",
|
||||
"-f",
|
||||
"ogg",
|
||||
"s16le",
|
||||
"pipe:1",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ export interface MediaQueueItem {
|
||||
|
||||
export interface MediaState {
|
||||
playing: boolean;
|
||||
musicVolume: number;
|
||||
current: MediaQueueItem | null;
|
||||
queue: MediaQueueItem[];
|
||||
}
|
||||
|
||||
@@ -2,17 +2,23 @@ import { Readable } from "node:stream";
|
||||
import {
|
||||
AudioPlayer,
|
||||
AudioPlayerStatus,
|
||||
type AudioResource,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
StreamType,
|
||||
VoiceConnection,
|
||||
} from "@discordjs/voice";
|
||||
import type { DiscordPlayerOwner } from "./media/mediaTypes";
|
||||
import type {
|
||||
DiscordPlayOptions,
|
||||
DiscordPlayerOwner,
|
||||
} from "./media/mediaTypes";
|
||||
|
||||
export class DiscordPlayer {
|
||||
private player: AudioPlayer;
|
||||
private connection: VoiceConnection | null = null;
|
||||
private owner: DiscordPlayerOwner = "none";
|
||||
private resource: AudioResource | null = null;
|
||||
private musicVolume = 1;
|
||||
|
||||
constructor() {
|
||||
this.player = createAudioPlayer();
|
||||
@@ -24,6 +30,7 @@ export class DiscordPlayer {
|
||||
this.player.on("error", (error) => {
|
||||
console.error(`[player] Error: ${error.message}`);
|
||||
this.owner = "none";
|
||||
this.resource = null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,20 +47,34 @@ export class DiscordPlayer {
|
||||
return this.connection !== null;
|
||||
}
|
||||
|
||||
public playStream(stream: Readable, owner: DiscordPlayerOwner) {
|
||||
public playStream(
|
||||
stream: Readable,
|
||||
owner: DiscordPlayerOwner,
|
||||
options: DiscordPlayOptions = {},
|
||||
) {
|
||||
if (owner === "none") {
|
||||
throw new Error("Discord audio player owner is required");
|
||||
}
|
||||
this.assertOwnerAvailable(owner);
|
||||
|
||||
const resource = createAudioResource(stream, {
|
||||
inputType: StreamType.OggOpus,
|
||||
inputType: options.inputType ?? StreamType.OggOpus,
|
||||
inlineVolume: options.inlineVolume ?? false,
|
||||
});
|
||||
|
||||
if (this.owner === owner) {
|
||||
this.player.stop();
|
||||
}
|
||||
this.resource = resource;
|
||||
this.owner = owner;
|
||||
if (owner === "music") {
|
||||
const nextVolume =
|
||||
options.volume !== undefined
|
||||
? this.normalizeVolume(options.volume)
|
||||
: this.musicVolume;
|
||||
this.musicVolume = nextVolume;
|
||||
this.setResourceVolume(nextVolume);
|
||||
}
|
||||
this.player.play(resource);
|
||||
this.connection?.subscribe(this.player);
|
||||
}
|
||||
@@ -76,6 +97,19 @@ export class DiscordPlayer {
|
||||
if (!this.canControl(owner)) return;
|
||||
this.player.stop();
|
||||
this.owner = "none";
|
||||
this.resource = null;
|
||||
}
|
||||
|
||||
public getMusicVolume(): number {
|
||||
return this.musicVolume;
|
||||
}
|
||||
|
||||
public setMusicVolume(volume: number): void {
|
||||
const nextVolume = this.normalizeVolume(volume);
|
||||
this.musicVolume = nextVolume;
|
||||
if (this.owner === "music") {
|
||||
this.setResourceVolume(nextVolume);
|
||||
}
|
||||
}
|
||||
|
||||
private assertOwnerAvailable(owner: DiscordPlayerOwner): void {
|
||||
@@ -87,6 +121,16 @@ export class DiscordPlayer {
|
||||
private canControl(owner?: DiscordPlayerOwner): boolean {
|
||||
return !owner || this.owner === "none" || this.owner === owner;
|
||||
}
|
||||
|
||||
private normalizeVolume(volume: number): number {
|
||||
if (!Number.isFinite(volume)) return this.musicVolume;
|
||||
return Math.max(0, Math.min(1, volume));
|
||||
}
|
||||
|
||||
private setResourceVolume(volume: number): void {
|
||||
if (!this.resource?.volume) return;
|
||||
this.resource.volume.setVolume(volume);
|
||||
}
|
||||
}
|
||||
|
||||
export const discordPlayer = new DiscordPlayer();
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { MediaMode } from "../media/mediaTypes";
|
||||
|
||||
export type MediaRouteController = Pick<
|
||||
MediaController,
|
||||
"getState" | "queue" | "skip" | "stop"
|
||||
"getState" | "queue" | "skip" | "stop" | "setMusicVolume"
|
||||
>;
|
||||
|
||||
export interface MediaRouteOptions {
|
||||
@@ -91,5 +91,28 @@ export function createMediaRoutes(
|
||||
},
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/media/volume",
|
||||
adminAuth,
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { volume } = req.body as { volume?: number };
|
||||
if (typeof volume !== "number" || Number.isNaN(volume)) {
|
||||
throw new AppError("Volume is required", "INVALID_VOLUME", 400);
|
||||
}
|
||||
if (volume < 0 || volume > 1) {
|
||||
throw new AppError(
|
||||
"Volume must be between 0 and 1",
|
||||
"INVALID_VOLUME",
|
||||
400,
|
||||
);
|
||||
}
|
||||
res.json(await controller.setMusicVolume(volume));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@ interface SharedUIState {
|
||||
isStreaming: boolean;
|
||||
}
|
||||
|
||||
interface MediaSettings {
|
||||
musicVolume: number;
|
||||
}
|
||||
|
||||
type SharedUIStatePatch = Partial<SharedUIState> & {
|
||||
selectedGuild?: string;
|
||||
};
|
||||
@@ -74,6 +78,10 @@ const defaultSharedUIState: SharedUIState = {
|
||||
isStreaming: false,
|
||||
};
|
||||
|
||||
const defaultMediaSettings: MediaSettings = {
|
||||
musicVolume: 1,
|
||||
};
|
||||
|
||||
let sharedUIState: SharedUIState = { ...defaultSharedUIState };
|
||||
|
||||
export function normalizeSharedUIState(
|
||||
@@ -101,6 +109,17 @@ async function initializeSharedUIState() {
|
||||
);
|
||||
}
|
||||
|
||||
async function initializeMediaSettings(): Promise<MediaSettings> {
|
||||
const stored = await getPersistedValue(
|
||||
"media-settings",
|
||||
defaultMediaSettings,
|
||||
);
|
||||
return {
|
||||
...defaultMediaSettings,
|
||||
...(stored as MediaSettings),
|
||||
};
|
||||
}
|
||||
|
||||
function getSharedUIState(): SharedUIState {
|
||||
return { ...sharedUIState };
|
||||
}
|
||||
@@ -174,6 +193,7 @@ export async function startWebserver(
|
||||
voiceController: VoiceController,
|
||||
) {
|
||||
await initializeSharedUIState();
|
||||
let mediaSettings = await initializeMediaSettings();
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
@@ -200,6 +220,11 @@ export async function startWebserver(
|
||||
isBrowserStreaming: () => sharedUIState.isStreaming,
|
||||
screenController,
|
||||
onStateChange: (state) => broadcaster.mediaState(state),
|
||||
initialMusicVolume: mediaSettings.musicVolume,
|
||||
onMusicVolumeChange: async (volume) => {
|
||||
mediaSettings = { ...mediaSettings, musicVolume: volume };
|
||||
await setPersistedValue("media-settings", mediaSettings);
|
||||
},
|
||||
});
|
||||
|
||||
// Security headers. CSP disabled because the current static UI uses inline scripts/styles.
|
||||
|
||||
Reference in New Issue
Block a user