Files
GMW/src/media/screenShareController.ts
T

124 lines
3.4 KiB
TypeScript
Raw Normal View History

import type { Readable } from "node:stream";
import {
playStream as defaultPlayStream,
prepareStream as defaultPrepareStream,
Encoders,
Utils,
} from "@dank074/discord-video-stream";
import { AppError } from "../errors";
import { discordPlayer } from "../player";
import type { DiscordPlayerOwner, ScreenSharePlayback } from "./mediaTypes";
import { createYtDlp } from "./ytdlp";
export interface ScreenShareVoiceStatus {
connected: boolean;
activeGuildId: string | null;
activeChannelId: string | null;
}
interface PreparedScreenStream {
command: { kill?: (signal: NodeJS.Signals) => unknown };
output: Readable;
}
type PrepareScreenStream = (
source: string,
options: object,
) => PreparedScreenStream;
type PlayScreenStream = (
output: Readable,
streamer: unknown,
options: { type: "go-live" },
) => Promise<void>;
export interface ScreenShareControllerDependencies {
getVoiceStatus: () => ScreenShareVoiceStatus;
getPlayerOwner?: () => DiscordPlayerOwner;
getDirectVideoUrl?: (source: string) => Promise<string>;
prepareStream?: PrepareScreenStream;
playStream?: PlayScreenStream;
streamer: unknown;
}
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));
const prepareStream =
dependencies.prepareStream ??
(defaultPrepareStream as unknown as PrepareScreenStream);
const playStream =
dependencies.playStream ??
(defaultPlayStream as unknown as PlayScreenStream);
return {
isActive(): boolean {
return active !== null;
},
async start(source: string): Promise<ScreenSharePlayback> {
const status = dependencies.getVoiceStatus();
if (
!status.connected ||
!status.activeGuildId ||
!status.activeChannelId
) {
throw new AppError(
"Connect to a voice channel before sharing screen",
"VOICE_NOT_CONNECTED",
409,
);
}
if (active || getPlayerOwner() !== "none") {
throw new AppError("Another media mode is active", "MEDIA_BUSY", 409);
}
try {
const directUrl = await getDirectVideoUrl(source);
const { command, output } = prepareStream(directUrl, {
encoder: Encoders.software({ x264: { preset: "superfast" } }),
height: 720,
frameRate: 30,
bitrateVideo: 2500,
bitrateVideoMax: 4000,
includeAudio: true,
videoCodec: Utils.normalizeVideoCodec("H264"),
});
let stopped = false;
const done = playStream(output, dependencies.streamer, {
type: "go-live",
}).finally(() => {
active = null;
});
active = {
done,
stop() {
if (stopped) return;
stopped = true;
command.kill?.("SIGTERM");
active = null;
},
};
return active;
} catch (error) {
active = null;
throw new AppError(
error instanceof Error ? error.message : "Screen stream failed",
"SCREEN_STREAM_FAILED",
500,
);
}
},
};
}