Feature: adds screen sharing
This commit is contained in:
@@ -152,6 +152,40 @@ export class MediaPlayer {
|
||||
await this.loadFile(url, "replace");
|
||||
}
|
||||
|
||||
public async initiateScreenSharing(url: string) {
|
||||
console.log(`Initiating screen sharing with file: ${url}`);
|
||||
|
||||
this.metadata.set(url, {
|
||||
title: "Screen Sharing",
|
||||
description: "Screen Sharing",
|
||||
siteName: "Screen Sharing",
|
||||
});
|
||||
|
||||
// Special options for mpv to better handle screen sharing (AI recommended...)
|
||||
await this.loadFile(url, "replace", false, [
|
||||
"demuxer-lavf-o=fflags=+nobuffer+discardcorrupt", // Reduce buffering and discard corrupt frames
|
||||
"demuxer-lavf-o=analyzeduration=100000", // Reduce analyze duration
|
||||
"demuxer-lavf-o=probesize=1000000", // Reduce probe size
|
||||
"untimed=yes", // Ignore timing info
|
||||
"cache=no", // Disable cache
|
||||
"force-seekable=yes", // Force seekable
|
||||
"no-cache=yes", // Disable cache
|
||||
"demuxer-max-bytes=500K", // Limit demuxer buffer
|
||||
"demuxer-readahead-secs=0.1", // Reduce readahead
|
||||
"hr-seek=no", // Disable high-res seeking
|
||||
"video-sync=display-resample", // Better sync mode
|
||||
"video-latency-hacks=yes", // Enable latency hacks
|
||||
"audio-sync=yes", // Enable audio sync
|
||||
"audio-buffer=0.1", // Reduce audio buffer
|
||||
"audio-channels=stereo", // Force stereo audio
|
||||
"audio-samplerate=44100", // Match sample rate
|
||||
"audio-format=s16", // Use 16-bit audio
|
||||
]);
|
||||
|
||||
// Make sure it's playing
|
||||
setTimeout(() => this.play(), 100);
|
||||
}
|
||||
|
||||
public async play() {
|
||||
return this.modify(UserEvent.NowPlayingUpdate, () => this.writeCommand("set_property", ["pause", false]));
|
||||
}
|
||||
@@ -212,12 +246,14 @@ export class MediaPlayer {
|
||||
return this.modify(UserEvent.FavoritesUpdate, () => this.favoritesStore.updateFavoriteTitle(filename, title));
|
||||
}
|
||||
|
||||
private async loadFile(url: string, mode: string) {
|
||||
this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode]));
|
||||
private async loadFile(url: string, mode: string, fetchMetadata: boolean = true, options: string[] = []) {
|
||||
this.modify(UserEvent.PlaylistUpdate, () => this.writeCommand("loadfile", [url, mode, options.join(',')]));
|
||||
|
||||
this.fetchMetadataAndNotify(url).catch(error => {
|
||||
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
||||
});
|
||||
if (fetchMetadata) {
|
||||
this.fetchMetadataAndNotify(url).catch(error => {
|
||||
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async modify<T>(event: UserEvent, func: () => Promise<T>): Promise<T> {
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
import express from "express";
|
||||
import expressWs from "express-ws";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import { MediaPlayer } from "./MediaPlayer";
|
||||
import { searchInvidious, fetchThumbnail } from "./InvidiousAPI";
|
||||
import { PlaylistItem } from './types';
|
||||
import { PassThrough } from "stream";
|
||||
import { AddressInfo } from "net";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -30,6 +34,10 @@ expressWs(app);
|
||||
const apiRouter = express.Router();
|
||||
const mediaPlayer = new MediaPlayer();
|
||||
|
||||
// Create a shared stream that both endpoints can access
|
||||
let activeScreenshareStream: PassThrough | null = null;
|
||||
let activeScreenshareMimeType: string | null = null;
|
||||
|
||||
const withErrorHandling = (func: (req: any, res: any) => Promise<any>) => {
|
||||
return async (req: any, res: any) => {
|
||||
try {
|
||||
@@ -127,6 +135,82 @@ apiRouter.ws("/events", (ws, req) => {
|
||||
});
|
||||
});
|
||||
|
||||
// This is effectively a "private" endpoint that only the MPV instance accesses. We're
|
||||
// using the fact that QueueCube/MPV is based all around streaming URLs, so the active
|
||||
// screenshare stream manifests as just another URL to play.
|
||||
apiRouter.get("/screenshareStream", withErrorHandling(async (req, res) => {
|
||||
res.setHeader("Content-Type", activeScreenshareMimeType || "video/mp4");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
res.setHeader("Pragma", "no-cache");
|
||||
res.setHeader("Expires", "0");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("Transfer-Encoding", "chunked");
|
||||
|
||||
if (!activeScreenshareStream) {
|
||||
res.status(503).send("No active screen sharing session");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle client disconnection
|
||||
req.on('close', () => {
|
||||
console.log("Screenshare viewer disconnected");
|
||||
});
|
||||
|
||||
// Configure stream for low latency
|
||||
activeScreenshareStream.setMaxListeners(0);
|
||||
|
||||
// Pipe with immediate flush
|
||||
activeScreenshareStream.pipe(res, { end: false });
|
||||
}));
|
||||
|
||||
apiRouter.ws("/screenshare", (ws, req) => {
|
||||
const mimeType = req.query.mimeType as string;
|
||||
console.log("Screen sharing client connected with mimeType: " + mimeType);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
let firstChunk = false;
|
||||
|
||||
// Configure WebSocket for low latency
|
||||
ws.setMaxListeners(0);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
ws.on('message', (data: any) => {
|
||||
const buffer = data instanceof Buffer ? data : Buffer.from(data);
|
||||
|
||||
if (!firstChunk) {
|
||||
firstChunk = true;
|
||||
|
||||
const port = (server.address() as AddressInfo).port;
|
||||
const url = `http://localhost:${port}/api/screenshareStream`;
|
||||
console.log(`Starting screen share stream at ${url}`);
|
||||
|
||||
// Create new shared stream with immediate flush
|
||||
activeScreenshareStream = new PassThrough({
|
||||
highWaterMark: 1024 * 1024, // 1MB buffer
|
||||
allowHalfOpen: false
|
||||
});
|
||||
|
||||
activeScreenshareStream.write(buffer);
|
||||
mediaPlayer.initiateScreenSharing(url);
|
||||
} else if (activeScreenshareStream) {
|
||||
// Write with immediate flush
|
||||
activeScreenshareStream.write(buffer, () => {
|
||||
activeScreenshareStream?.cork();
|
||||
activeScreenshareStream?.uncork();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log("Screen sharing client disconnected");
|
||||
if (activeScreenshareStream) {
|
||||
activeScreenshareStream.end();
|
||||
activeScreenshareStream = null;
|
||||
}
|
||||
mediaPlayer.stop();
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.get("/search", withErrorHandling(async (req, res) => {
|
||||
const query = req.query.q as string;
|
||||
if (!query) {
|
||||
|
||||
Reference in New Issue
Block a user