Improve mpv playback configuration
This commit is contained in:
@@ -117,15 +117,86 @@ export class MediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private envEnabled(name: string, defaultValue: boolean = false): boolean {
|
||||
const value = process.env[name];
|
||||
if (value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
switch (value.trim().toLowerCase()) {
|
||||
case "1":
|
||||
case "true":
|
||||
case "yes":
|
||||
case "on":
|
||||
return true;
|
||||
case "":
|
||||
case "0":
|
||||
case "false":
|
||||
case "no":
|
||||
case "off":
|
||||
return false;
|
||||
default:
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private addMpvEnvOption(args: string[], envName: string, optionName: string) {
|
||||
const value = process.env[envName];
|
||||
if (value) {
|
||||
args.push(`${optionName}=${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
private parseMpvExtraArgs(): string[] {
|
||||
const value = process.env.MPV_EXTRA_ARGS;
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const args: string[] = [];
|
||||
let current = "";
|
||||
let quote: string | null = null;
|
||||
let escaped = false;
|
||||
|
||||
for (const char of value) {
|
||||
if (escaped) {
|
||||
current += char;
|
||||
escaped = false;
|
||||
} else if (char === "\\") {
|
||||
escaped = true;
|
||||
} else if (quote) {
|
||||
if (char === quote) {
|
||||
quote = null;
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
} else if (char === "\"" || char === "'") {
|
||||
quote = char;
|
||||
} else if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
args.push(current);
|
||||
current = "";
|
||||
}
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
args.push(current);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private tryRespawnPlayerProcess(): Promise<Socket> {
|
||||
const socketFilename = Math.random().toString(36).substring(2, 10);
|
||||
const socketPath = `/tmp/mpv-${socketFilename}`;
|
||||
const enableVideo = process.env.ENABLE_VIDEO || false;
|
||||
const enableVideo = this.envEnabled("ENABLE_VIDEO");
|
||||
const ytdlFormat = process.env.MPV_YTDL_FORMAT;
|
||||
const logfilePath = `/tmp/mpv-logfile.txt`;
|
||||
const playerArgs = [
|
||||
"--video=" + (enableVideo ? "auto" : "no"),
|
||||
"--fullscreen",
|
||||
"--no-terminal",
|
||||
"--idle=yes",
|
||||
"--input-ipc-server=" + socketPath,
|
||||
@@ -133,10 +204,24 @@ export class MediaPlayer {
|
||||
"--msg-level=all=v"
|
||||
];
|
||||
|
||||
if (this.envEnabled("MPV_FULLSCREEN", true)) {
|
||||
playerArgs.push("--fullscreen");
|
||||
}
|
||||
|
||||
this.addMpvEnvOption(playerArgs, "MPV_WAYLAND_APP_ID", "--wayland-app-id");
|
||||
this.addMpvEnvOption(playerArgs, "MPV_TITLE", "--title");
|
||||
this.addMpvEnvOption(playerArgs, "MPV_BORDER", "--border");
|
||||
|
||||
if (this.envEnabled("MPV_ONTOP")) {
|
||||
playerArgs.push("--ontop");
|
||||
}
|
||||
|
||||
if (ytdlFormat) {
|
||||
playerArgs.push("--ytdl-format=" + ytdlFormat);
|
||||
}
|
||||
|
||||
playerArgs.push(...this.parseMpvExtraArgs());
|
||||
|
||||
console.log("Starting player process (video: " + (enableVideo ? "enabled" : "disabled") + ")");
|
||||
this.playerProcess = spawn("mpv", playerArgs);
|
||||
|
||||
@@ -381,9 +466,9 @@ export class MediaPlayer {
|
||||
|
||||
public async getFeatures(): Promise<Features> {
|
||||
return {
|
||||
video: !!process.env.ENABLE_VIDEO,
|
||||
screenshare: !!process.env.ENABLE_SCREENSHARE,
|
||||
browserPlayback: !!process.env.ENABLE_BROWSER_PLAYBACK
|
||||
video: this.envEnabled("ENABLE_VIDEO"),
|
||||
screenshare: this.envEnabled("ENABLE_SCREENSHARE"),
|
||||
browserPlayback: this.envEnabled("ENABLE_BROWSER_PLAYBACK")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -392,9 +477,9 @@ export class MediaPlayer {
|
||||
this.playbackErrors.delete(url);
|
||||
this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode, "-1", options.join(',')]));
|
||||
|
||||
if (fetchMetadata) {
|
||||
if (fetchMetadata && this.shouldFetchLinkPreview(url)) {
|
||||
this.fetchMetadataAndNotify(url).catch(error => {
|
||||
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
||||
console.warn(`Failed to fetch metadata for ${this.loggableUrl(url)}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -446,7 +531,7 @@ export class MediaPlayer {
|
||||
|
||||
private async fetchMetadataAndNotify(url: string) {
|
||||
try {
|
||||
console.log("Fetching metadata for " + url);
|
||||
console.log("Fetching metadata for " + this.loggableUrl(url));
|
||||
const metadata = await getLinkPreview(url);
|
||||
this.metadata.set(url, {
|
||||
title: (metadata as any)?.title,
|
||||
@@ -454,7 +539,7 @@ export class MediaPlayer {
|
||||
siteName: (metadata as any)?.siteName,
|
||||
});
|
||||
|
||||
console.log("Metadata fetched for " + url);
|
||||
console.log("Metadata fetched for " + this.loggableUrl(url));
|
||||
console.log(this.metadata.get(url));
|
||||
|
||||
// Notify clients that metadata has been updated
|
||||
@@ -467,6 +552,43 @@ export class MediaPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
private shouldFetchLinkPreview(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === "file:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pathname = parsed.pathname.toLowerCase();
|
||||
if (/\.(ts|m3u8|mpd|mp4|m4v|mkv|webm|avi|mov|mpeg|mpg|mp3|aac|flac|wav|ogg|opus)$/.test(pathname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\/download\/?$/.test(pathname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private loggableUrl(url: string): string {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
const basename = segments.length > 0 ? segments[segments.length - 1] : "";
|
||||
return `${parsed.protocol}//${parsed.host}/${basename ? ".../" + basename : ""}${parsed.search ? "?..." : ""}`;
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private connectToSocket(path: string): Promise<Socket> {
|
||||
const retryDelayMs = 100;
|
||||
const maxAttempts = 50;
|
||||
|
||||
@@ -31,6 +31,12 @@ const app = express();
|
||||
app.use(express.json());
|
||||
expressWs(app);
|
||||
|
||||
const frontendDistPath = [
|
||||
path.join(__dirname, "../dist/frontend"),
|
||||
path.join(__dirname, "../../frontend/dist"),
|
||||
].find((candidate) => fs.existsSync(path.join(candidate, "index.html")))
|
||||
|| path.join(__dirname, "../dist/frontend");
|
||||
|
||||
const apiRouter = express.Router();
|
||||
const mediaPlayer = new MediaPlayer();
|
||||
|
||||
@@ -300,14 +306,14 @@ apiRouter.get("/features", withErrorHandling(async (req, res) => {
|
||||
}));
|
||||
|
||||
// Serve static files for React app (after building)
|
||||
app.use(express.static(path.join(__dirname, "../dist/frontend")));
|
||||
app.use(express.static(frontendDistPath));
|
||||
|
||||
// Mount API routes under /api
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
// Serve React app for all other routes (client-side routing)
|
||||
app.get("*", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../dist/frontend/index.html"));
|
||||
res.sendFile(path.join(frontendDistPath, "index.html"));
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
Reference in New Issue
Block a user