Files
iptvnator/apps/electron-backend/src/main.ts
T
4grayandClaude Fable 5 8eb0fe3261 feat(playback): embedded mpv pre-0.22 hardening, Linux parity, and localization (#1122)
* fix(playback): harden embedded mpv session handling and support detection

- guard the session controller against late startup rejections clobbering
  a newer session during fast channel zapping
- exclude the refresh timestamp from the session-update dedup key so idle
  sessions stop re-emitting IPC updates every 500 ms
- macOS: reconcile async loadfile replies by request id so a rejected
  seek/aid/speed on a live stream no longer flips the session to error
- append --ozone-platform=x11 on Linux in main.ts so direct binary and
  AppImage launches match the packaged .desktop launcher behavior
- return a sandbox-specific unsupported reason in Flatpak/Snap instead of
  asking the user to install mpv inside the sandbox
- update the stale "macOS only" embedded MPV claim in CLAUDE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): populate Linux audio tracks and fix ARM Linux packaging

- Linux poller now reads track-list/count each tick and walks the scalar
  track-list/N/* sub-properties when the count changes, so the audio-track
  menu is no longer empty; selection reconciles from the aid property
- afterPack replaces the x64 embedded_mpv.node with an
  embedded-mpv-unavailable.txt marker in arm64/armv7l Linux packages, and
  package-layout verification rejects foreign-architecture addons while
  requiring the marker
- extend native source invariants for the non-fatal async-reply rule
  (macOS) and the Linux track-list polling contract
- document the Linux track-list mechanics and ARM packaging behavior in
  docs/architecture/embedded-mpv-native.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(playback): embedded mpv player UX polish and full localization

- click on the video toggles pause (same action as Space) with a 250 ms
  grace period so double-click fullscreen cancels the pending pause; no
  DOM overlay is drawn — the dock transport icon is the feedback
- timeline scrubbing previews the drag position locally and commits a
  single seek on release instead of one IPC seek per drag pixel
- translate all player UI strings (controls, tooltips, aria-labels,
  status and recording messages) via new EMBEDDED_MPV.PLAYER.* keys,
  synced across en + 17 locales through the i18n-fill workflow
- replace the legacy @Output() EventEmitter with the signal output() API

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(playback): react to language changes and respect ozone platform hint

Address review feedback on #1122:
- add a translationsTick signal (onLangChange/onTranslationChange/
  onDefaultLangChange) read by every computed() and template helper that
  calls translate.instant(), so labels re-evaluate on a runtime language
  switch and when the translation file finishes loading after mount
- suppress the Linux --ozone-platform=x11 fallback when the user set
  ELECTRON_OZONE_PLATFORM_HINT, matching the existing respect for an
  explicit --ozone-platform switch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 14:22:48 +02:00

188 lines
6.8 KiB
TypeScript

import { app, BrowserWindow } from 'electron';
import { getElectronUserDataPath } from '@iptvnator/shared/database';
import { autoUpdater } from 'electron-updater';
import fixPath from 'fix-path';
import App from './app/app';
import { initDatabase } from './app/database/connection';
import DatabaseEvents from './app/events/database.events';
import {
resetStaleDownloads,
setMainWindow as setDownloadsMainWindow,
} from './app/events/database/downloads.events';
import ElectronEvents from './app/events/electron.events';
import EmbeddedMpvEvents, {
shutdownEmbeddedMpv,
} from './app/events/embedded-mpv.events';
import EpgEvents from './app/events/epg.events';
import AppUpdateEvents from './app/events/app-update.events';
import { shutdownMpvSession } from './app/events/mpv-session.service';
import PlayerEvents from './app/events/player.events';
import { shutdownVlcSession } from './app/events/vlc-session.service';
import PlaylistEvents from './app/events/playlist.events';
import RemoteControlEvents from './app/events/remote-control.events';
import SettingsEvents from './app/events/settings.events';
import SharedEvents from './app/events/shared.events';
import SquirrelEvents from './app/events/squirrel.events';
import StalkerEvents from './app/events/stalker.events';
import { isStartupTraceEnabled, trace } from './app/services/debug-trace';
import { AppUpdateService } from './app/services/app-update.service';
import { databaseWorkerClient } from './app/services/database-worker-client';
import WindowEvents from './app/events/window.events';
import XtreamEvents from './app/events/xtream.events';
import { environment } from './environments/environment';
app.setName('iptvnator');
// Packaged Linux launchers force X11 via the .desktop entry
// (electron-builder `executableArgs`), but direct binary/AppImage launches
// from a terminal bypass that entry. Embedded MPV supports X11/XWayland
// only, and its support probe requires the ozone switch to be present, so
// mirror the launcher behavior here. Explicit user intent wins: both an
// --ozone-platform switch and the ELECTRON_OZONE_PLATFORM_HINT env var
// suppress the fallback.
if (
process.platform === 'linux' &&
!app.commandLine.hasSwitch('ozone-platform') &&
!process.env.ELECTRON_OZONE_PLATFORM_HINT
) {
app.commandLine.appendSwitch('ozone-platform', 'x11');
}
const electronUserDataPath = getElectronUserDataPath();
if (electronUserDataPath) {
app.setPath('userData', electronUserDataPath);
}
let fixPathScheduled = false;
/**
* Update process.env.PATH from the user's interactive login shell so that
* spawned external players (MPV/VLC) can be resolved by binary name.
*
* Runs after window creation + IPC handler registration so the 50-300 ms
* shell-spawn cost (bash/zsh -ilc env) doesn't block startup. Idempotent:
* subsequent calls are no-ops.
*/
function scheduleDeferredFixPath(): void {
if (fixPathScheduled || process.platform === 'win32') {
return;
}
fixPathScheduled = true;
setImmediate(() => {
try {
fixPath();
if (isStartupTraceEnabled()) {
trace('startup', 'fix-path:done');
}
} catch (error) {
console.warn('fix-path failed:', error);
}
});
}
export default class Main {
static initialize() {
if (SquirrelEvents.handleEvents()) {
// squirrel event handled (except first run event) and app will exit in 1000ms, so don't do anything else
app.quit();
}
}
static bootstrapApp() {
if (isStartupTraceEnabled()) {
trace('startup', 'bootstrap-app');
}
App.main(app, BrowserWindow);
}
static async bootstrapAppEvents() {
if (isStartupTraceEnabled()) {
trace('startup', 'bootstrap-events:start');
}
const appUpdateService = new AppUpdateService({
app,
appVersion: environment.version,
getMainWindow: () => App.mainWindow,
updater: () => autoUpdater,
});
AppUpdateEvents.bootstrapAppUpdateEvents(appUpdateService);
ElectronEvents.bootstrapElectronEvents();
WindowEvents.bootstrapWindowEvents();
EmbeddedMpvEvents.bootstrapEmbeddedMpvEvents();
PlaylistEvents.bootstrapPlaylistEvents();
SharedEvents.bootstrapSharedEvents();
PlayerEvents.bootstrapPlayerEvents();
SettingsEvents.bootstrapSettingsEvents();
StalkerEvents.bootstrapStalkerEvents();
XtreamEvents.bootstrapXtreamEvents();
DatabaseEvents.bootstrapDatabaseEvents();
EpgEvents.bootstrapEpgEvents();
RemoteControlEvents.bootstrapRemoteControlEvents();
// Set main window for downloads and reset stale downloads
if (App.mainWindow) {
setDownloadsMainWindow(App.mainWindow);
}
// Load the renderer only after IPC handlers are registered. On slower
// Linux CI hosts the renderer can otherwise invoke Electron bridge IPC
// before the main process has installed handlers.
await App.loadMainWindow();
void appUpdateService.checkForUpdatesOnStartup();
// Initialize the database after the first renderer load is underway so
// Linux Electron E2E can observe a BrowserWindow even when SQLite
// startup or download recovery is slow. IPC handlers call getDatabase()
// lazily and share the same initialization promise.
await initDatabase();
if (isStartupTraceEnabled()) {
trace('startup', 'init-database:done');
}
await resetStaleDownloads();
if (isStartupTraceEnabled()) {
trace('startup', 'reset-stale-downloads:done');
}
if (isStartupTraceEnabled()) {
trace('startup', 'bootstrap-events:done');
}
// Hydrate process.env.PATH from the user's login shell now — after
// the window has loaded and IPC handlers are live. Fire-and-forget
// (setImmediate) so it doesn't gate any user-visible work. Worst
// case: the user clicks an external player within the ~100 ms it
// takes to complete; the spawn would still find MPV/VLC at any of
// the well-known paths checked by getDefault*Path before falling
// back to bare-name PATH lookup.
scheduleDeferredFixPath();
}
}
// handle setup events as quickly as possible
Main.initialize();
// bootstrap app
Main.bootstrapApp();
// Bootstrap app events after Electron app is ready
app.whenReady().then(async () => {
if (isStartupTraceEnabled()) {
trace('startup', 'app.whenReady');
}
await Main.bootstrapAppEvents();
});
app.on('before-quit', () => {
shutdownEmbeddedMpv();
shutdownMpvSession();
shutdownVlcSession();
void databaseWorkerClient.shutdown();
});