Feature: adds screen sharing

This commit is contained in:
2025-05-12 20:27:09 -07:00
parent db3b10d324
commit 035d74d412
6 changed files with 345 additions and 23 deletions

View File

@@ -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) {