113 lines
3.0 KiB
TypeScript
113 lines
3.0 KiB
TypeScript
import {
|
|
Streamer,
|
|
playPreparedStream,
|
|
} from "../streaming";
|
|
import { AppError } from "../errors";
|
|
import { createChildLogger } from "../logger";
|
|
import { discordPlayer } from "../player";
|
|
|
|
const logger = createChildLogger("screen-share");
|
|
|
|
import type { DiscordPlayerOwner, ScreenSharePlayback } from "./mediaTypes";
|
|
import { createYtDlp } from "./ytdlp";
|
|
|
|
export interface ScreenShareVoiceStatus {
|
|
connected: boolean;
|
|
activeGuildId: string | null;
|
|
activeChannelId: string | null;
|
|
}
|
|
|
|
export interface ScreenShareControllerDependencies {
|
|
getVoiceStatus: () => ScreenShareVoiceStatus;
|
|
getPlayerOwner?: () => DiscordPlayerOwner;
|
|
getDirectVideoUrl?: (source: string) => Promise<string>;
|
|
streamer: Streamer;
|
|
onStreamStart?: () => void;
|
|
onStreamEnd?: () => void;
|
|
}
|
|
|
|
export function createScreenShareController(
|
|
dependencies: ScreenShareControllerDependencies,
|
|
) {
|
|
let active: ScreenSharePlayback | null = null;
|
|
const ytdlp = createYtDlp();
|
|
const getPlayerOwner =
|
|
dependencies.getPlayerOwner ?? (() => discordPlayer.getOwner());
|
|
const getDirectVideoUrl =
|
|
dependencies.getDirectVideoUrl ??
|
|
((source) => ytdlp.getDirectVideoUrl(source));
|
|
|
|
return {
|
|
isActive(): boolean {
|
|
return active !== null;
|
|
},
|
|
|
|
async start(source: string): Promise<ScreenSharePlayback> {
|
|
const status = dependencies.getVoiceStatus();
|
|
|
|
if (active) {
|
|
active.stop();
|
|
}
|
|
|
|
// Ensure bot is in the voice channel and owns the screen-share stream
|
|
if (
|
|
!status.connected ||
|
|
!status.activeGuildId ||
|
|
!status.activeChannelId
|
|
) {
|
|
throw new AppError(
|
|
"Connect to a voice channel before sharing screen",
|
|
"VOICE_NOT_CONNECTED",
|
|
409,
|
|
);
|
|
}
|
|
|
|
// If another media owner (e.g. music) holds the shared player, reject
|
|
const owner = getPlayerOwner();
|
|
if (owner === "music") {
|
|
throw new AppError("Another media mode is active", "MEDIA_BUSY", 409);
|
|
}
|
|
|
|
try {
|
|
const directUrl = await getDirectVideoUrl(source);
|
|
const session = await dependencies.streamer.createSession(
|
|
status.activeGuildId,
|
|
status.activeChannelId,
|
|
);
|
|
|
|
dependencies.onStreamStart?.();
|
|
|
|
let stopped = false;
|
|
const done = playPreparedStream(directUrl, session, {
|
|
fps: 30,
|
|
bitrate: 2500,
|
|
includeAudio: true,
|
|
presetH26x: "superfast",
|
|
}).finally(() => {
|
|
active = null;
|
|
dependencies.onStreamEnd?.();
|
|
});
|
|
done.catch(() => undefined);
|
|
|
|
active = {
|
|
done,
|
|
stop() {
|
|
if (stopped) return;
|
|
stopped = true;
|
|
session.stop();
|
|
active = null;
|
|
},
|
|
};
|
|
return active;
|
|
} catch (error) {
|
|
active = null;
|
|
throw new AppError(
|
|
error instanceof Error ? error.message : "Screen stream failed",
|
|
"SCREEN_STREAM_FAILED",
|
|
500,
|
|
);
|
|
}
|
|
},
|
|
};
|
|
}
|