chore: update Discord-video-stream subproject to latest commit

This commit is contained in:
MythEclipse
2026-05-17 04:52:20 +07:00
parent 518577d79d
commit 71889ab689
15 changed files with 268 additions and 19 deletions
+1
View File
@@ -234,6 +234,7 @@ export default function App() {
onStartScreen={(source) => media.enqueue(source, "screen")} onStartScreen={(source) => media.enqueue(source, "screen")}
onSkip={media.skip} onSkip={media.skip}
onStop={media.stop} onStop={media.stop}
onVolumeChange={media.setVolume}
/> />
)} )}
</TabsContent> </TabsContent>
+7
View File
@@ -19,3 +19,10 @@ export function skipMedia(): Promise<MediaState> {
export function stopMedia(): Promise<MediaState> { export function stopMedia(): Promise<MediaState> {
return request<MediaState>('/api/media/stop', { method: 'POST' }); return request<MediaState>('/api/media/stop', { method: 'POST' });
} }
export function setMediaVolume(volume: number): Promise<MediaState> {
return request<MediaState>('/api/media/volume', {
method: 'POST',
body: JSON.stringify({ volume }),
});
}
+18 -2
View File
@@ -11,9 +11,18 @@ interface MediaPanelProps {
onStartScreen: (source: string) => void; onStartScreen: (source: string) => void;
onSkip: () => void; onSkip: () => void;
onStop: () => void; onStop: () => void;
onVolumeChange: (volume: number) => void;
} }
export function MediaPanel({ state, loading, onQueueMusic, onStartScreen, onSkip, onStop }: MediaPanelProps) { export function MediaPanel({
state,
loading,
onQueueMusic,
onStartScreen,
onSkip,
onStop,
onVolumeChange,
}: MediaPanelProps) {
return ( return (
<div className="grid gap-6 xl:grid-cols-[1fr_380px]"> <div className="grid gap-6 xl:grid-cols-[1fr_380px]">
<Tabs defaultValue="music" className="min-w-0"> <Tabs defaultValue="music" className="min-w-0">
@@ -22,7 +31,14 @@ export function MediaPanel({ state, loading, onQueueMusic, onStartScreen, onSkip
<TabsTrigger value="screen">Screen Share</TabsTrigger> <TabsTrigger value="screen">Screen Share</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="music"> <TabsContent value="music">
<MusicPlayer loading={loading} onQueue={onQueueMusic} onSkip={onSkip} onStop={onStop} /> <MusicPlayer
loading={loading}
volume={state.musicVolume}
onVolumeChange={onVolumeChange}
onQueue={onQueueMusic}
onSkip={onSkip}
onStop={onStop}
/>
</TabsContent> </TabsContent>
<TabsContent value="screen"> <TabsContent value="screen">
<ScreenShare loading={loading} onStart={onStartScreen} onSkip={onSkip} onStop={onStop} /> <ScreenShare loading={loading} onStart={onStartScreen} onSkip={onSkip} onStop={onStop} />
+41 -2
View File
@@ -1,18 +1,42 @@
import { Music2 } from "lucide-react"; import { Music2 } from "lucide-react";
import { useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
interface MusicPlayerProps { interface MusicPlayerProps {
loading: boolean; loading: boolean;
volume: number;
onVolumeChange: (volume: number) => void;
onQueue: (source: string) => void; onQueue: (source: string) => void;
onSkip: () => void; onSkip: () => void;
onStop: () => void; onStop: () => void;
} }
export function MusicPlayer({ loading, onQueue, onSkip, onStop }: MusicPlayerProps) { export function MusicPlayer({
loading,
volume,
onVolumeChange,
onQueue,
onSkip,
onStop,
}: MusicPlayerProps) {
const [source, setSource] = useState(""); const [source, setSource] = useState("");
const safeVolume = Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : 1;
const [draftVolume, setDraftVolume] = useState(Math.round(safeVolume * 100));
useEffect(() => {
setDraftVolume(Math.round(safeVolume * 100));
}, [safeVolume]);
useEffect(() => {
const normalized = draftVolume / 100;
if (Math.abs(normalized - safeVolume) < 0.001) return;
const timer = window.setTimeout(() => {
onVolumeChange(normalized);
}, 150);
return () => window.clearTimeout(timer);
}, [draftVolume, onVolumeChange, safeVolume]);
const submit = () => { const submit = () => {
const trimmed = source.trim(); const trimmed = source.trim();
@@ -34,6 +58,21 @@ export function MusicPlayer({ loading, onQueue, onSkip, onStop }: MusicPlayerPro
onKeyDown={(event) => event.key === "Enter" && submit()} onKeyDown={(event) => event.key === "Enter" && submit()}
placeholder="YouTube URL, Spotify track, or search terms" placeholder="YouTube URL, Spotify track, or search terms"
/> />
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="font-medium">Volume</span>
<span className="text-muted-foreground">{draftVolume}%</span>
</div>
<input
type="range"
min={0}
max={100}
step={1}
value={draftVolume}
onChange={(event) => setDraftVolume(Number(event.target.value))}
className="h-2 w-full cursor-pointer accent-primary"
/>
</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button disabled={loading || !source.trim()} onClick={submit}>Queue / Play</Button> <Button disabled={loading || !source.trim()} onClick={submit}>Queue / Play</Button>
<Button variant="secondary" disabled={loading} onClick={onSkip}>Skip</Button> <Button variant="secondary" disabled={loading} onClick={onSkip}>Skip</Button>
+37 -3
View File
@@ -1,8 +1,19 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { getMediaStatus, queueMedia, skipMedia, stopMedia } from "../api/media"; import {
getMediaStatus,
queueMedia,
setMediaVolume,
skipMedia,
stopMedia,
} from "../api/media";
import type { MediaMode, MediaState } from "../types/media"; import type { MediaMode, MediaState } from "../types/media";
const emptyMediaState: MediaState = { playing: false, current: null, queue: [] }; const emptyMediaState: MediaState = {
playing: false,
musicVolume: 1,
current: null,
queue: [],
};
export function useMediaControl() { export function useMediaControl() {
const [mediaState, setMediaState] = useState<MediaState>(emptyMediaState); const [mediaState, setMediaState] = useState<MediaState>(emptyMediaState);
@@ -55,9 +66,32 @@ export function useMediaControl() {
} }
}, []); }, []);
const setVolume = useCallback(async (volume: number) => {
setError(null);
try {
const state = await setMediaVolume(volume);
setMediaState(state);
return state;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
setError(message);
throw err;
}
}, []);
useEffect(() => { useEffect(() => {
refreshMedia().catch((err) => setError(err instanceof Error ? err.message : String(err))); refreshMedia().catch((err) => setError(err instanceof Error ? err.message : String(err)));
}, [refreshMedia]); }, [refreshMedia]);
return { mediaState, setMediaState, loading, error, refreshMedia, enqueue, skip, stop }; return {
mediaState,
setMediaState,
loading,
error,
refreshMedia,
enqueue,
skip,
stop,
setVolume,
};
} }
+1
View File
@@ -11,6 +11,7 @@ export interface MediaItem {
export interface MediaState { export interface MediaState {
playing: boolean; playing: boolean;
musicVolume: number;
current: MediaItem | null; current: MediaItem | null;
queue: MediaItem[]; queue: MediaItem[];
} }
+27
View File
@@ -21,6 +21,9 @@ export interface MediaControllerDependencies {
musicPlayer?: MusicPlayer; musicPlayer?: MusicPlayer;
screenController?: ScreenShareController; screenController?: ScreenShareController;
onStateChange?: (state: MediaState) => void; onStateChange?: (state: MediaState) => void;
initialMusicVolume?: number;
onMusicVolumeChange?: (volume: number) => void | Promise<void>;
setMusicVolume?: (volume: number) => void;
} }
export class MediaController { export class MediaController {
@@ -31,9 +34,18 @@ export class MediaController {
private skipInProgress = false; private skipInProgress = false;
private screenPlayback: ScreenSharePlayback | null = null; private screenPlayback: ScreenSharePlayback | null = null;
private activeMode: MediaMode | null = null; private activeMode: MediaMode | null = null;
private musicVolume: number;
private readonly setPlayerMusicVolume: (volume: number) => void;
constructor(private readonly dependencies: MediaControllerDependencies = {}) { constructor(private readonly dependencies: MediaControllerDependencies = {}) {
this.musicPlayer = dependencies.musicPlayer ?? createMusicPlayer(); 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 { getState(): MediaState {
@@ -42,10 +54,20 @@ export class MediaController {
playing: playing:
this.activeMode === "screen" || snapshot.current?.status === "playing", this.activeMode === "screen" || snapshot.current?.status === "playing",
activeMode: this.activeMode ?? snapshot.current?.mode ?? null, activeMode: this.activeMode ?? snapshot.current?.mode ?? null,
musicVolume: this.musicVolume,
...snapshot, ...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( async queue(
source: string, source: string,
options: QueueMediaOptions = {}, options: QueueMediaOptions = {},
@@ -201,3 +223,8 @@ export class MediaController {
return state; 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));
}
+15 -1
View File
@@ -1,4 +1,5 @@
import type { Readable } from "node:stream"; import type { Readable } from "node:stream";
import type { StreamType } from "@discordjs/voice";
export type MediaMode = "music" | "screen"; export type MediaMode = "music" | "screen";
export type MediaSourceKind = export type MediaSourceKind =
@@ -26,6 +27,7 @@ export interface MediaQueueItem extends ResolvedMediaSource {
export interface MediaState { export interface MediaState {
playing: boolean; playing: boolean;
activeMode: MediaMode | null; activeMode: MediaMode | null;
musicVolume: number;
current: MediaQueueItem | null; current: MediaQueueItem | null;
queue: MediaQueueItem[]; queue: MediaQueueItem[];
} }
@@ -56,11 +58,23 @@ export interface ScreenShareController {
export type DiscordPlayerOwner = "none" | "browser-bridge" | "music" | "screen"; export type DiscordPlayerOwner = "none" | "browser-bridge" | "music" | "screen";
export interface DiscordPlayOptions {
inputType?: StreamType;
inlineVolume?: boolean;
volume?: number;
}
export interface DiscordAudioPlayer { export interface DiscordAudioPlayer {
getOwner(): DiscordPlayerOwner; getOwner(): DiscordPlayerOwner;
isConnected(): boolean; isConnected(): boolean;
playStream(stream: Readable, owner: DiscordPlayerOwner): void; playStream(
stream: Readable,
owner: DiscordPlayerOwner,
options?: DiscordPlayOptions,
): void;
pause(owner?: DiscordPlayerOwner): void; pause(owner?: DiscordPlayerOwner): void;
unpause(owner?: DiscordPlayerOwner): boolean; unpause(owner?: DiscordPlayerOwner): boolean;
stop(owner?: DiscordPlayerOwner): void; stop(owner?: DiscordPlayerOwner): void;
getMusicVolume(): number;
setMusicVolume(volume: number): void;
} }
+7 -3
View File
@@ -1,5 +1,6 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process"; import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { spawn as nodeSpawn } from "node:child_process"; import { spawn as nodeSpawn } from "node:child_process";
import { StreamType } from "@discordjs/voice";
import { discordPlayer } from "../player"; import { discordPlayer } from "../player";
import type { import type {
DiscordAudioPlayer, DiscordAudioPlayer,
@@ -30,7 +31,10 @@ export function createMusicPlayer(
}) as unknown as ChildProcessWithoutNullStreams; }) as unknown as ChildProcessWithoutNullStreams;
proc.stderr.resume(); proc.stderr.resume();
audioPlayer.playStream(proc.stdout, "music"); audioPlayer.playStream(proc.stdout, "music", {
inputType: StreamType.Raw,
inlineVolume: true,
});
let stopped = false; let stopped = false;
let released = false; let released = false;
@@ -81,13 +85,13 @@ export function buildFfmpegArgs(source: string): string[] {
source, source,
"-vn", "-vn",
"-acodec", "-acodec",
"libopus", "pcm_s16le",
"-ar", "-ar",
"48000", "48000",
"-ac", "-ac",
"2", "2",
"-f", "-f",
"ogg", "s16le",
"pipe:1", "pipe:1",
]; ];
} }
+1
View File
@@ -114,6 +114,7 @@ export interface MediaQueueItem {
export interface MediaState { export interface MediaState {
playing: boolean; playing: boolean;
musicVolume: number;
current: MediaQueueItem | null; current: MediaQueueItem | null;
queue: MediaQueueItem[]; queue: MediaQueueItem[];
} }
+47 -3
View File
@@ -2,17 +2,23 @@ import { Readable } from "node:stream";
import { import {
AudioPlayer, AudioPlayer,
AudioPlayerStatus, AudioPlayerStatus,
type AudioResource,
createAudioPlayer, createAudioPlayer,
createAudioResource, createAudioResource,
StreamType, StreamType,
VoiceConnection, VoiceConnection,
} from "@discordjs/voice"; } from "@discordjs/voice";
import type { DiscordPlayerOwner } from "./media/mediaTypes"; import type {
DiscordPlayOptions,
DiscordPlayerOwner,
} from "./media/mediaTypes";
export class DiscordPlayer { export class DiscordPlayer {
private player: AudioPlayer; private player: AudioPlayer;
private connection: VoiceConnection | null = null; private connection: VoiceConnection | null = null;
private owner: DiscordPlayerOwner = "none"; private owner: DiscordPlayerOwner = "none";
private resource: AudioResource | null = null;
private musicVolume = 1;
constructor() { constructor() {
this.player = createAudioPlayer(); this.player = createAudioPlayer();
@@ -24,6 +30,7 @@ export class DiscordPlayer {
this.player.on("error", (error) => { this.player.on("error", (error) => {
console.error(`[player] Error: ${error.message}`); console.error(`[player] Error: ${error.message}`);
this.owner = "none"; this.owner = "none";
this.resource = null;
}); });
} }
@@ -40,20 +47,34 @@ export class DiscordPlayer {
return this.connection !== null; return this.connection !== null;
} }
public playStream(stream: Readable, owner: DiscordPlayerOwner) { public playStream(
stream: Readable,
owner: DiscordPlayerOwner,
options: DiscordPlayOptions = {},
) {
if (owner === "none") { if (owner === "none") {
throw new Error("Discord audio player owner is required"); throw new Error("Discord audio player owner is required");
} }
this.assertOwnerAvailable(owner); this.assertOwnerAvailable(owner);
const resource = createAudioResource(stream, { const resource = createAudioResource(stream, {
inputType: StreamType.OggOpus, inputType: options.inputType ?? StreamType.OggOpus,
inlineVolume: options.inlineVolume ?? false,
}); });
if (this.owner === owner) { if (this.owner === owner) {
this.player.stop(); this.player.stop();
} }
this.resource = resource;
this.owner = owner; 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.player.play(resource);
this.connection?.subscribe(this.player); this.connection?.subscribe(this.player);
} }
@@ -76,6 +97,19 @@ export class DiscordPlayer {
if (!this.canControl(owner)) return; if (!this.canControl(owner)) return;
this.player.stop(); this.player.stop();
this.owner = "none"; 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 { private assertOwnerAvailable(owner: DiscordPlayerOwner): void {
@@ -87,6 +121,16 @@ export class DiscordPlayer {
private canControl(owner?: DiscordPlayerOwner): boolean { private canControl(owner?: DiscordPlayerOwner): boolean {
return !owner || this.owner === "none" || this.owner === owner; 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(); export const discordPlayer = new DiscordPlayer();
+24 -1
View File
@@ -6,7 +6,7 @@ import type { MediaMode } from "../media/mediaTypes";
export type MediaRouteController = Pick< export type MediaRouteController = Pick<
MediaController, MediaController,
"getState" | "queue" | "skip" | "stop" "getState" | "queue" | "skip" | "stop" | "setMusicVolume"
>; >;
export interface MediaRouteOptions { 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; return router;
} }
+25
View File
@@ -60,6 +60,10 @@ interface SharedUIState {
isStreaming: boolean; isStreaming: boolean;
} }
interface MediaSettings {
musicVolume: number;
}
type SharedUIStatePatch = Partial<SharedUIState> & { type SharedUIStatePatch = Partial<SharedUIState> & {
selectedGuild?: string; selectedGuild?: string;
}; };
@@ -74,6 +78,10 @@ const defaultSharedUIState: SharedUIState = {
isStreaming: false, isStreaming: false,
}; };
const defaultMediaSettings: MediaSettings = {
musicVolume: 1,
};
let sharedUIState: SharedUIState = { ...defaultSharedUIState }; let sharedUIState: SharedUIState = { ...defaultSharedUIState };
export function normalizeSharedUIState( 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 { function getSharedUIState(): SharedUIState {
return { ...sharedUIState }; return { ...sharedUIState };
} }
@@ -174,6 +193,7 @@ export async function startWebserver(
voiceController: VoiceController, voiceController: VoiceController,
) { ) {
await initializeSharedUIState(); await initializeSharedUIState();
let mediaSettings = await initializeMediaSettings();
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
@@ -200,6 +220,11 @@ export async function startWebserver(
isBrowserStreaming: () => sharedUIState.isStreaming, isBrowserStreaming: () => sharedUIState.isStreaming,
screenController, screenController,
onStateChange: (state) => broadcaster.mediaState(state), 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. // Security headers. CSP disabled because the current static UI uses inline scripts/styles.
+1
View File
@@ -194,6 +194,7 @@ describe("MediaController", () => {
expect(state).toEqual({ expect(state).toEqual({
playing: false, playing: false,
activeMode: null, activeMode: null,
musicVolume: 1,
current: null, current: null,
queue: [], queue: [],
}); });
+16 -4
View File
@@ -5,6 +5,7 @@ type Spawn = typeof nodeSpawn;
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import { PassThrough } from "node:stream"; import { PassThrough } from "node:stream";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { StreamType } from "@discordjs/voice";
import type { import type {
DiscordAudioPlayer, DiscordAudioPlayer,
DiscordPlayerOwner, DiscordPlayerOwner,
@@ -23,13 +24,15 @@ class FakeProcess extends EventEmitter {
} }
describe("createMusicPlayer", () => { describe("createMusicPlayer", () => {
it("spawns ffmpeg as Ogg Opus and passes stdout to Discord", async () => { it("spawns ffmpeg as raw PCM and passes stdout to Discord", async () => {
const proc = new FakeProcess(); const proc = new FakeProcess();
const spawn = vi.fn(() => proc); const spawn = vi.fn(() => proc);
const discordPlayer: DiscordAudioPlayer = { const discordPlayer: DiscordAudioPlayer = {
isConnected: () => true, isConnected: () => true,
playStream: vi.fn(), playStream: vi.fn(),
getOwner: vi.fn((): DiscordPlayerOwner => "none"), getOwner: vi.fn((): DiscordPlayerOwner => "none"),
getMusicVolume: vi.fn(() => 1),
setMusicVolume: vi.fn(),
pause: vi.fn(), pause: vi.fn(),
unpause: vi.fn(() => true), unpause: vi.fn(() => true),
stop: vi.fn(), stop: vi.fn(),
@@ -57,18 +60,21 @@ describe("createMusicPlayer", () => {
"https://example.com/song.mp3", "https://example.com/song.mp3",
"-vn", "-vn",
"-acodec", "-acodec",
"libopus", "pcm_s16le",
"-ar", "-ar",
"48000", "48000",
"-ac", "-ac",
"2", "2",
"-f", "-f",
"ogg", "s16le",
"pipe:1", "pipe:1",
], ],
{ stdio: ["ignore", "pipe", "pipe"] }, { stdio: ["ignore", "pipe", "pipe"] },
); );
expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout, "music"); expect(discordPlayer.playStream).toHaveBeenCalledWith(proc.stdout, "music", {
inputType: StreamType.Raw,
inlineVolume: true,
});
}); });
it("rejects playback when Discord is not connected", () => { it("rejects playback when Discord is not connected", () => {
@@ -77,6 +83,8 @@ describe("createMusicPlayer", () => {
isConnected: () => false, isConnected: () => false,
playStream: vi.fn(), playStream: vi.fn(),
getOwner: vi.fn((): DiscordPlayerOwner => "none"), getOwner: vi.fn((): DiscordPlayerOwner => "none"),
getMusicVolume: vi.fn(() => 1),
setMusicVolume: vi.fn(),
pause: vi.fn(), pause: vi.fn(),
unpause: vi.fn(() => true), unpause: vi.fn(() => true),
stop: vi.fn(), stop: vi.fn(),
@@ -102,6 +110,8 @@ describe("createMusicPlayer", () => {
isConnected: () => true, isConnected: () => true,
playStream: vi.fn(), playStream: vi.fn(),
getOwner: vi.fn((): DiscordPlayerOwner => "none"), getOwner: vi.fn((): DiscordPlayerOwner => "none"),
getMusicVolume: vi.fn(() => 1),
setMusicVolume: vi.fn(),
pause: vi.fn(), pause: vi.fn(),
unpause: vi.fn(() => true), unpause: vi.fn(() => true),
stop: vi.fn(), stop: vi.fn(),
@@ -128,6 +138,8 @@ describe("createMusicPlayer", () => {
isConnected: () => true, isConnected: () => true,
playStream: vi.fn(), playStream: vi.fn(),
getOwner: vi.fn((): DiscordPlayerOwner => "none"), getOwner: vi.fn((): DiscordPlayerOwner => "none"),
getMusicVolume: vi.fn(() => 1),
setMusicVolume: vi.fn(),
pause: vi.fn(), pause: vi.fn(),
unpause: vi.fn(() => true), unpause: vi.fn(() => true),
stop: vi.fn(), stop: vi.fn(),