Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
8.8 kB · 310 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311import { execFile } from "child_process";import { access, cp, mkdir, mkdtemp, readFile, rename, rm, writeFile,} from "fs/promises";import os from "os";import { basename, delimiter, join, resolve } from "path";import { promisify } from "util";import getEnv from "./env";
const run = promisify(execFile);
export type ObsStatus = { obs: "absent" | "installed" | "flatpak"; plugin: "absent" | "outdated" | "current"; obsRunning: boolean; bundledVersion: string; installedVersion?: string;};
// Flatpak OBS only loads plugins packaged as Flatpak extensions.const FLATPAK_ERROR = "OBS is installed as a Flatpak, which can't load the Streamplace plugin. Install OBS from your distribution or obsproject.com instead.";
const exists = (path: string) => access(path).then( () => true, () => false, );
// Where OBS keeps its settings and profiles.const obsConfigDir = () => { const home = os.homedir(); if (process.platform === "win32") { return join( process.env.APPDATA ?? join(home, "AppData", "Roaming"), "obs-studio", ); } if (process.platform === "darwin") { return join(home, "Library", "Application Support", "obs-studio"); } return join( process.env.XDG_CONFIG_HOME || join(home, ".config"), "obs-studio", );};
// Where OBS 30.2+ loads third-party plugins from; machine-wide on Windows.const pluginRoot = () => { if (process.platform === "win32") { return join( process.env.ProgramData ?? "C:\\ProgramData", "obs-studio", "plugins", ); } return join(obsConfigDir(), "plugins");};
// The plugin forge.config.ts unpacks into the app's resources.const bundledPlugin = () => { const name = process.platform === "darwin" ? "obs-streamplace.plugin" : "obs-streamplace"; if (getEnv().isDev) { const platform = process.platform === "win32" ? "windows" : process.platform; const arch = process.arch === "x64" ? "amd64" : process.arch; return resolve( process.cwd(), "out", "obs-plugin", `${platform}-${arch}`, name, ); } return resolve(process.resourcesPath, name);};
// Written by obs-plugin/package.sh.const readManifest = async ( plugin: string,): Promise<{ version: string; build_time: number } | undefined> => { const file = plugin.endsWith(".plugin") ? join(plugin, "Contents", "Resources", "manifest.json") : join(plugin, "data", "manifest.json"); try { return JSON.parse(await readFile(file, "utf8")); } catch { return undefined; }};
// How the plugin installed under root compares to the bundled one. A copy// without a manifest predates them, so it's outdated.export const pluginState = async (bundled: string, root: string) => { const want = await readManifest(bundled); if (!want) { throw new Error(`no OBS plugin bundled at ${bundled}`); } const installed = join(root, basename(bundled)); if (!(await exists(installed))) { return { plugin: "absent" as const, bundledVersion: want.version }; } const have = await readManifest(installed); return { plugin: have && have.build_time >= want.build_time ? ("current" as const) : ("outdated" as const), bundledVersion: want.version, installedVersion: have?.version, };};
// Copies the bundled plugin into root unless it's already current, replacing// an older copy. Returns whether it installed anything.export const installPluginDir = async ( bundled: string, root: string,): Promise<boolean> => { if ((await pluginState(bundled, root)).plugin === "current") { return false; } await mkdir(root, { recursive: true }); // Staged inside root, so swapping it in is a rename on one filesystem. OBS // doesn't load from here: the module sits one directory too deep. const stage = await mkdtemp(join(root, ".obs-streamplace-")); const target = join(root, basename(bundled)); const old = join(stage, "old"); let movedOld = false; try { await cp(bundled, join(stage, "new"), { recursive: true }); // On Windows this fails while OBS has the DLL loaded, before anything changes. await rename(target, old).then( () => (movedOld = true), (e) => { if (e.code !== "ENOENT") throw e; }, ); await rename(join(stage, "new"), target).catch(async (e) => { // Put the working copy back rather than leave OBS without one. if (movedOld) await rename(old, target); throw e; }); } finally { await rm(stage, { recursive: true, force: true }); } return true;};
// The profile OBS opens with: user.ini since OBS 31, global.ini before.const currentProfileDir = async (configDir: string) => { for (const file of ["user.ini", "global.ini"]) { const text = await readFile(join(configDir, file), "utf8").catch(() => ""); let section = ""; for (const line of text.replace(/^\uFEFF/, "").split(/\r?\n/)) { const l = line.trim(); if (l.startsWith("[")) { section = l; } else if (section === "[Basic]" && l.startsWith("ProfileDir=")) { return l.slice("ProfileDir=".length); } } } return undefined;};
// Points OBS's current profile at server with this stream key, leaving the// profile's other settings alone.export const writeStreamConfig = async ( configDir: string, server: string, key: string,) => { const profile = await currentProfileDir(configDir); if (!profile) { throw new Error( "OBS has no profile yet. Open OBS once, close it, and try again.", ); } const service = { type: "rtmp_common", settings: { service: "Streamplace", server, key }, }; await writeFile( join(configDir, "basic", "profiles", profile, "service.json"), JSON.stringify(service, null, 4), );};
const detectObs = async (): Promise<ObsStatus["obs"]> => { if (process.platform === "win32") { if ( await exists( join(process.env.ProgramFiles ?? "C:\\Program Files", "obs-studio"), ) ) { return "installed"; } return run( "reg", [ "query", "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OBS Studio", ], { windowsHide: true }, ).then( () => "installed", () => "absent", ); } if (process.platform === "darwin") { return (await exists("/Applications/OBS.app")) ? "installed" : "absent"; } for (const dir of (process.env.PATH ?? "").split(delimiter)) { if (dir && (await exists(join(dir, "obs")))) { return "installed"; } } if ( await exists(join(os.homedir(), ".var", "app", "com.obsproject.Studio")) ) { return "flatpak"; } return (await exists(obsConfigDir())) ? "installed" : "absent";};
const isObsRunning = async () => { if (process.platform === "win32") { const { stdout } = await run( "tasklist", ["/FI", "IMAGENAME eq obs64.exe", "/NH"], { windowsHide: true }, ); return stdout.includes("obs64.exe"); } return run("pgrep", [ "-x", process.platform === "darwin" ? "OBS" : "obs", ]).then( () => true, () => false, );};
export const status = async (): Promise<ObsStatus> => { const [obs, obsRunning, state] = await Promise.all([ detectObs(), isObsRunning(), pluginState(bundledPlugin(), pluginRoot()), ]); return { obs, obsRunning, ...state };};
export const installPlugin = async (): Promise<{ ok: boolean; error?: string; restartObs: boolean;}> => { const [obs, obsRunning] = await Promise.all([detectObs(), isObsRunning()]); if (obs !== "installed") { const error = obs === "flatpak" ? FLATPAK_ERROR : "OBS isn't installed."; return { ok: false, error, restartObs: false }; } if (obsRunning && process.platform === "win32") { return { ok: false, error: "Close OBS, then install the plugin.", restartObs: false, }; } try { const installed = await installPluginDir(bundledPlugin(), pluginRoot()); return { ok: true, restartObs: installed && obsRunning }; } catch (e) { return { ok: false, error: e.message, restartObs: false }; }};
export const configureStream = async ( server: string, streamKey: string,): Promise<{ ok: boolean; error?: string }> => { const [obs, obsRunning] = await Promise.all([detectObs(), isObsRunning()]); if (obs !== "installed") { return { ok: false, error: obs === "flatpak" ? FLATPAK_ERROR : "OBS isn't installed.", }; } // OBS writes its settings back out when it exits, undoing ours. if (obsRunning) { return { ok: false, error: "Close OBS, then set up the stream." }; } try { await writeStreamConfig(obsConfigDir(), server, streamKey); return { ok: true }; } catch (e) { return { ok: false, error: e.message }; }};