Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
395b505
Implement native external streamer MVP and integrate into Electron ma…
zortos293 Apr 1, 2026
e27365a
fix streamer startup/input threading issues
zortos293 Apr 1, 2026
2d4cfe5
add extra dev streamer binary lookup paths
zortos293 Apr 1, 2026
fc7eeaa
fix native startup settings and sdl runtime mismatch
zortos293 Apr 1, 2026
9d236bb
update streamer lockfile for bundled sdl
zortos293 Apr 1, 2026
9de6e37
fix bundled sdl build on newer cmake
zortos293 Apr 1, 2026
2cf9005
update bundled sdl to fix linux build
zortos293 Apr 1, 2026
8c02b30
use system sdl on linux
zortos293 Apr 1, 2026
6e147a6
fix external streamer startup ordering
zortos293 Apr 1, 2026
5d7fb10
instrument native offer handling
zortos293 Apr 1, 2026
83311b4
fix native answer local description flow
zortos293 Apr 1, 2026
8347839
fix native IPC field casing
zortos293 Apr 1, 2026
7581bae
improve native nvst and ice parity
zortos293 Apr 1, 2026
ed67b93
log and normalize native ice candidates
zortos293 Apr 1, 2026
c3b69d5
add native transport and track diagnostics
zortos293 Apr 1, 2026
19128d6
force answering dtls role in native streamer
zortos293 Apr 1, 2026
09e0e3f
fix native answer transport SDP
zortos293 Apr 1, 2026
e5de9a5
Improve native streamer protocol parity
zortos293 Apr 1, 2026
b4decca
Improve native mouse capture behavior
zortos293 Apr 1, 2026
a863058
Reduce native decode latency on Linux
zortos293 Apr 1, 2026
98248be
Add native decoder backend fallback ladder
zortos293 Apr 2, 2026
78f984a
Fix macOS SDL bundled build config
zortos293 Apr 2, 2026
382a20f
Add macOS native streamer repo paths
zortos293 Apr 2, 2026
86dd1c8
Run macOS streamer window on main thread
zortos293 Apr 2, 2026
e4a4e28
fix(streamer): improve macos dev launcher
zortos293 Apr 2, 2026
bef9b84
fix(streamer): avoid decoder log runtime panic
zortos293 Apr 2, 2026
8ba861d
fix(streamer): handle dynamic frame sizes on macos
zortos293 Apr 2, 2026
5d72789
perf(streamer): reduce native playback latency
zortos293 Apr 2, 2026
36ff4f8
perf(streamer): optimize mac decode upload path
zortos293 Apr 2, 2026
bf0c8de
fix(streamer): probe mac videotoolbox decoder support
zortos293 Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions opennow-stable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"main": "dist-electron/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build",
"build:native-runtime": "cargo build --manifest-path ../opennow-streamer/Cargo.toml --release && node ./scripts/bundle-native-runtime.mjs",
"build": "npm run build:native-runtime && electron-vite build",
"preview": "electron-vite preview",
"dist": "npm run build && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder",
"dist:signed": "npm run build && electron-builder",
Expand Down Expand Up @@ -61,7 +62,11 @@
"files": [
"dist/**",
"dist-electron/**",
"package.json"
"package.json",
"resources/bin/**"
],
"extraResources": [
{ "from": "resources/bin", "to": "bin" }
],
"asar": true,
"win": {
Expand Down
43 changes: 43 additions & 0 deletions opennow-stable/scripts/bundle-native-runtime.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { cpSync, existsSync, mkdirSync, realpathSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const repoRoot = resolve(__dirname, '../..');
const outDir = resolve(repoRoot, 'opennow-stable/resources/bin');
mkdirSync(outDir, { recursive: true });

const exeSuffix = process.platform === 'win32' ? '.exe' : '';
const streamerCandidates = [
resolve(repoRoot, `opennow-streamer/target/release/opennow-streamer${exeSuffix}`),
resolve(repoRoot, `opennow-streamer/target/debug/opennow-streamer${exeSuffix}`),
];
const streamer = streamerCandidates.find((p) => existsSync(p));
if (!streamer) {
throw new Error(`Missing opennow-streamer binary. Build it first: ${streamerCandidates.join(', ')}`);
}
cpSync(streamer, join(outDir, `opennow-streamer${exeSuffix}`));

if (process.platform === 'linux') {
const sdlLibDir = execFileSync('pkg-config', ['--variable=libdir', 'sdl2'], { encoding: 'utf8' }).trim();
const sdlCandidates = [
join(sdlLibDir, 'libSDL2-2.0.so.0'),
'/lib/x86_64-linux-gnu/libSDL2-2.0.so.0',
'/usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0',
];
const sdlLib = sdlCandidates.find((candidate) => existsSync(candidate));
if (!sdlLib) {
throw new Error(`Missing libSDL2-2.0.so.0. Checked: ${sdlCandidates.join(', ')}`);
}
cpSync(realpathSync(sdlLib), join(outDir, 'libSDL2-2.0.so.0'));
}

const ffmpegEnv = process.env.OPENNOW_FFMPEG_BIN;
const ffmpeg = ffmpegEnv || execFileSync(process.platform === 'win32' ? 'where' : 'which', ['ffmpeg'], { encoding: 'utf8' }).split(/\r?\n/).find(Boolean);
if (!ffmpeg || !existsSync(ffmpeg)) {
throw new Error('Missing ffmpeg binary. Set OPENNOW_FFMPEG_BIN or ensure ffmpeg is on PATH.');
}
cpSync(ffmpeg, join(outDir, `ffmpeg${exeSuffix}`));
console.log(`Bundled native runtime into ${outDir}`);
37 changes: 34 additions & 3 deletions opennow-stable/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
} from "./gfn/games";
import { fetchSubscription, fetchDynamicRegions } from "./gfn/subscription";
import { GfnSignalingClient } from "./gfn/signaling";
import { StreamerManager } from "./services/streamerManager";
import { isSessionError, SessionError, GfnErrorCode } from "./gfn/errorCodes";

const __filename = fileURLToPath(import.meta.url);
Expand Down Expand Up @@ -187,6 +188,7 @@ let signalingClient: GfnSignalingClient | null = null;
let signalingClientKey: string | null = null;
let authService: AuthService;
let settingsManager: SettingsManager;
let streamerManager: StreamerManager;
const SCREENSHOT_LIMIT = 60;

function getScreenshotDirectory(): string {
Expand Down Expand Up @@ -481,9 +483,16 @@ async function listRecordings(): Promise<RecordingEntry[]> {
}

function emitToRenderer(event: MainToRendererSignalingEvent): void {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IPC_CHANNELS.SIGNALING_EVENT, event);
}
void streamerManager?.forwardSignalingEvent(event).then((handled) => {
if (!handled && mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IPC_CHANNELS.SIGNALING_EVENT, event);
}
}).catch((error) => {
console.error("[Streamer] Failed to forward signaling event:", error);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(IPC_CHANNELS.SIGNALING_EVENT, event);
}
});
}

async function createMainWindow(): Promise<void> {
Expand Down Expand Up @@ -769,6 +778,14 @@ function registerIpcHandlers(): void {
return signalingClient.requestKeyframe(payload);
});

ipcMain.handle(IPC_CHANNELS.STREAMER_START, async (_event, payload) => {
await streamerManager.start(payload);
});

ipcMain.handle(IPC_CHANNELS.STREAMER_STOP, async (): Promise<void> => {
await streamerManager.stop();
});

// Toggle fullscreen via IPC (for completeness)
ipcMain.handle(IPC_CHANNELS.TOGGLE_FULLSCREEN, async () => {
if (mainWindow && !mainWindow.isDestroyed()) {
Expand Down Expand Up @@ -1153,6 +1170,19 @@ app.whenReady().then(async () => {
await authService.initialize();

settingsManager = getSettingsManager();
streamerManager = new StreamerManager(
() => mainWindow,
{
sendAnswer: async (payload) => {
if (!signalingClient) throw new Error("Signaling is not connected");
await signalingClient.sendAnswer(payload);
},
sendIceCandidate: async (payload) => {
if (!signalingClient) throw new Error("Signaling is not connected");
await signalingClient.sendIceCandidate(payload);
},
},
);

// Request microphone permission on macOS at startup
if (process.platform === "darwin") {
Expand Down Expand Up @@ -1247,6 +1277,7 @@ app.on("window-all-closed", () => {

app.on("before-quit", () => {
refreshScheduler.stop();
void streamerManager?.stop();
signalingClient?.disconnect();
signalingClient = null;
signalingClientKey = null;
Expand Down
Loading