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; 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 { 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, ); } }, }; }