feat(player): add VLC reuse-instance setting (#893)

VLC was unconditionally spawned per click — VLC's own single-instance
preference fails because the per-launch RC args defeat its D-Bus
forwarder. Mirror the existing MPV reuse pattern so users can opt in to
driving one tracked VLC via its RC interface (clear + add) instead of
opening a new window every stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: f957cd9849e0
This commit is contained in:
4grayandClaude Opus 4.7 committed 2026-05-05 00:55:04 +02:00
1 parent 6cbf2a1cfb
commit e192cba776
30 files changed
+387 -12

No files matched your search

@@ -15,6 +15,7 @@ jest.mock('../services/store.service', () => ({
MPV_PLAYER_PATH: 'MPV_PLAYER_PATH',
MPV_REUSE_INSTANCE: 'MPV_REUSE_INSTANCE',
VLC_PLAYER_PATH: 'VLC_PLAYER_PATH',
VLC_REUSE_INSTANCE: 'VLC_REUSE_INSTANCE',
store: {
get: jest.fn(),
set: jest.fn(),
@@ -30,14 +31,17 @@ import {
MPV_PLAYER_PATH,
store,
VLC_PLAYER_PATH,
VLC_REUSE_INSTANCE,
} from '../services/store.service';
import {
buildExternalPlayerSpawnSpec,
buildVlcEnqueueCommands,
isRunningInFlatpak,
parseVlcRcPlaybackState,
parseVlcRcNumericResponse,
resolveExternalPlayerLaunchContext,
shouldReuseMpvInstance,
shouldReuseVlcInstance,
shouldUseMpvSocketBridge,
} from './player.events';
@@ -150,6 +154,12 @@ describe('player.events Flatpak launch helpers', () => {
expect(shouldUseMpvSocketBridge(false)).toBe(true);
});
it('disables VLC reuse only in Flatpak', () => {
expect(shouldReuseVlcInstance(true, true)).toBe(false);
expect(shouldReuseVlcInstance(true, false)).toBe(true);
expect(shouldReuseVlcInstance(false, false)).toBe(false);
});
it('parses numeric VLC RC responses that include the prompt prefix', () => {
expect(
parseVlcRcNumericResponse(`VLC media player 3.0.20 Vetinari
@@ -215,4 +225,67 @@ describe('player.events external player path settings', () => {
'/Applications/mpv.app/Contents/MacOS/mpv'
);
});
it('persists the VLC reuse-instance preference', () => {
getIpcMainHandler('SET_VLC_REUSE_INSTANCE')({}, true);
expect(store.set).toHaveBeenCalledWith(VLC_REUSE_INSTANCE, true);
});
});
describe('buildVlcEnqueueCommands', () => {
it('clears the playlist and adds the URL with no extra options', () => {
expect(
buildVlcEnqueueCommands({ url: 'http://stream.example/a.m3u8' })
).toEqual(['clear', 'add http://stream.example/a.m3u8']);
});
it('attaches per-input HTTP options inline with the add command', () => {
const commands = buildVlcEnqueueCommands({
url: 'http://stream.example/a.m3u8',
title: 'Channel One',
userAgent: 'Custom/1.0',
referer: 'https://referer.example',
headers: { 'X-Token': 'abc' },
});
expect(commands[0]).toBe('clear');
expect(commands[1]).toBe(
'add http://stream.example/a.m3u8 :http-user-agent=Custom/1.0 :http-referrer=https://referer.example :http-header=X-Token: abc :meta-title=Channel One'
);
});
it('falls back to origin when referer is absent', () => {
const commands = buildVlcEnqueueCommands({
url: 'http://stream.example/a.m3u8',
origin: 'https://origin.example',
});
expect(commands[1]).toContain(
':http-referrer=https://origin.example'
);
});
it('appends a seek command when startTime is provided', () => {
const commands = buildVlcEnqueueCommands({
url: 'http://stream.example/a.m3u8',
startTime: 42.7,
});
expect(commands).toEqual([
'clear',
'add http://stream.example/a.m3u8',
'seek 42',
]);
});
it('skips empty header values', () => {
const commands = buildVlcEnqueueCommands({
url: 'http://stream.example/a.m3u8',
headers: { 'X-Empty': ' ', 'X-Real': 'value' },
});
expect(commands[1]).toContain(':http-header=X-Real: value');
expect(commands[1]).not.toContain('X-Empty');
});
});
@@ -11,6 +11,7 @@ import {
MPV_REUSE_INSTANCE,
store,
VLC_PLAYER_PATH,
VLC_REUSE_INSTANCE,
} from '../services/store.service';
import { ChildProcess, spawn } from 'child_process';
@@ -31,6 +32,10 @@ let mpvProcess: ChildProcess | null = null;
let mpvSocketPath: string | null = null;
let positionPollingInterval: NodeJS.Timeout | null = null;
// Keep track of the running VLC process for reuse
let vlcProcess: ChildProcess | null = null;
let vlcRcPort: number | null = null;
interface ExternalPlaybackSnapshot {
positionSeconds: number;
durationSeconds: number | null;
@@ -169,6 +174,96 @@ export function shouldUseMpvSocketBridge(
return !isFlatpak;
}
export function shouldReuseVlcInstance(
requestedReuseInstance: boolean,
isFlatpak: boolean = isRunningInFlatpak()
): boolean {
return !isFlatpak && requestedReuseInstance;
}
export function buildVlcEnqueueCommands(options: {
url: string;
title?: string;
userAgent?: string;
referer?: string;
origin?: string;
headers?: Record<string, string>;
startTime?: number;
}): string[] {
const inputOptions: string[] = [];
if (options.userAgent) {
inputOptions.push(`:http-user-agent=${options.userAgent}`);
}
if (options.referer) {
inputOptions.push(`:http-referrer=${options.referer}`);
} else if (options.origin) {
inputOptions.push(`:http-referrer=${options.origin}`);
}
Object.entries(options.headers ?? {}).forEach(([name, value]) => {
if (!name || value === undefined || value === null) return;
const trimmedValue = String(value).trim();
if (!trimmedValue) return;
inputOptions.push(`:http-header=${name}: ${trimmedValue}`);
});
if (options.title) {
inputOptions.push(`:meta-title=${options.title}`);
}
const inputLine =
inputOptions.length > 0
? `${options.url} ${inputOptions.join(' ')}`
: options.url;
const commands = ['clear', `add ${inputLine}`];
if (options.startTime && Number.isFinite(options.startTime)) {
commands.push(`seek ${Math.floor(options.startTime)}`);
}
return commands;
}
function sendVlcRcCommand(port: number, command: string): Promise<void> {
return new Promise((resolve, reject) => {
const client = createConnection({ port, host: '127.0.0.1' });
let settled = false;
const finish = (err?: Error) => {
if (settled) return;
settled = true;
clearTimeout(timeoutHandle);
if (!client.destroyed) client.destroy();
err ? reject(err) : resolve();
};
const timeoutHandle = setTimeout(
() => finish(new Error('VLC RC command timed out')),
2000
);
client.on('connect', () => {
client.write(`${command}\n`);
});
// The VLC RC interface emits its '> ' prompt after each command finishes.
client.on('data', (chunk) => {
if (chunk.toString().includes('>')) {
finish();
}
});
client.on('error', (err) => finish(err));
});
}
async function sendVlcRcCommands(
port: number,
commands: string[]
): Promise<void> {
for (const command of commands) {
await sendVlcRcCommand(port, command);
}
}
function buildPlayerStartError(
player: 'MPV' | 'VLC',
error: Error,
@@ -978,6 +1073,14 @@ ipcMain.handle(
getVlcPath({ isFlatpak }),
{ isFlatpak }
);
const requestedReuseInstance = store.get(
VLC_REUSE_INSTANCE,
false
);
const reuseInstance = shouldReuseVlcInstance(
requestedReuseInstance,
isFlatpak
);
const fallbackHeaders = getStalkerPlaybackContextHeaders(url) ?? {};
const mergedHeaders = isStalkerDirectStreamProfile(fallbackHeaders)
? fallbackHeaders
@@ -1000,17 +1103,118 @@ ipcMain.handle(
mergedHeaders['User-Agent'] ??
mergedHeaders['user-agent'] ??
undefined;
console.log('Opening VLC player with path:', vlcLaunchContext.playerPath);
console.log('VLC launch mode:', vlcLaunchContext.mode);
console.log('URL:', url);
// console.log('User-Agent:', userAgent);
// console.log('Referer:', referer);
// console.log('Origin:', origin);
// console.log('Start Time:', startTime);
console.log('[VLC] Opening player', {
path: vlcLaunchContext.playerPath,
launchMode: vlcLaunchContext.mode,
requestedReuseInstance,
reuseInstance,
stream: maskUrlForLogs(url),
hasUserAgent: Boolean(effectiveUserAgent),
hasReferer: Boolean(effectiveReferer),
hasOrigin: Boolean(effectiveOrigin),
hasContentInfo: Boolean(contentInfo),
startTime: startTime ?? null,
});
// Get free port for RC interface
// Try to drive an already-running VLC via its RC port instead of
// spawning a new instance.
if (
reuseInstance &&
vlcProcess &&
!vlcProcess.killed &&
vlcRcPort
) {
console.log(
'Reusing existing VLC instance on RC port',
vlcRcPort
);
try {
const enqueueCommands = buildVlcEnqueueCommands({
url,
title,
userAgent: effectiveUserAgent,
referer: effectiveReferer,
origin: effectiveOrigin,
headers: mergedHeaders,
startTime,
});
await sendVlcRcCommands(vlcRcPort, enqueueCommands);
console.log(
'Successfully loaded new URL in existing VLC instance'
);
const reusedRcPort = vlcRcPort;
let lastReusedSnapshot: ExternalPlaybackSnapshot | null =
null;
externalPlayerSessions.attachCloser(
session.id,
async () => {
try {
await sendVlcRcCommand(reusedRcPort, 'stop');
} catch {
if (vlcProcess && !vlcProcess.killed) {
vlcProcess.kill();
}
}
}
);
if (contentInfo) {
startVlcPositionPolling(
reusedRcPort,
contentInfo,
session.id,
(snapshot) => {
lastReusedSnapshot = snapshot;
},
() => {
if (
lastReusedSnapshot &&
contentInfo &&
externalPlayerSessions.getSession(
session.id
)?.status !== 'closed'
) {
sendPlaybackPositionUpdate(
session.id,
contentInfo,
lastReusedSnapshot
);
}
externalPlayerSessions.markClosed(session.id);
}
);
} else {
stopVlcPositionPolling();
}
return (
externalPlayerSessions.markOpened(session.id) ?? session
);
} catch (err) {
console.error(
'Failed to reuse existing VLC, spawning fresh:',
err
);
// Tracked process is not responsive; clear state and fall
// through to spawn a brand new VLC.
if (vlcProcess && !vlcProcess.killed) {
try {
vlcProcess.kill();
} catch {
/* ignore */
}
}
vlcProcess = null;
vlcRcPort = null;
stopVlcPositionPolling();
}
}
// Get free port for RC interface (always allocate when reuse is
// enabled so future calls can drive this instance).
let rcPort = 0;
if (contentInfo) {
if (contentInfo || reuseInstance) {
try {
rcPort = await getFreePort();
console.log('Using VLC RC port:', rcPort);
@@ -1073,12 +1277,24 @@ ipcMain.handle(
vlcLaunchContext,
playerArgs
);
const trackProcess = reuseInstance && !isRetry;
const proc = spawn(spawnSpec.command, spawnSpec.args, {
shell: false,
detached: true,
stdio: 'ignore', // Use 'ignore' for detached to allow clean shutdown
detached: !trackProcess,
stdio: trackProcess
? ['ignore', 'pipe', 'pipe']
: 'ignore',
});
if (trackProcess && rcPort > 0) {
vlcProcess = proc;
vlcRcPort = rcPort;
console.log(
'Tracking VLC process for reuse on RC port',
rcPort
);
}
const markVlcSessionClosed = () => {
if (
externalPlayerSessions.getSession(session.id)?.status ===
@@ -1162,6 +1378,10 @@ ipcMain.handle(
proc.on('error', (err) => {
console.error('Failed to start VLC player:', err);
if (vlcProcess === proc) {
vlcProcess = null;
vlcRcPort = null;
}
if (!isRetry && rcPort > 0) {
console.log('Retrying VLC without RC interface...');
// Retry without RC args
@@ -1189,6 +1409,10 @@ ipcMain.handle(
proc.on('exit', (code) => {
console.log(`VLC exited with code ${code}`);
if (vlcProcess === proc) {
vlcProcess = null;
vlcRcPort = null;
}
stopVlcPositionPolling();
if (
@@ -1238,7 +1462,9 @@ ipcMain.handle(
externalPlayerSessions.markClosed(session.id);
});
proc.unref();
if (!trackProcess) {
proc.unref();
}
};
spawnVlc(args);
@@ -1266,6 +1492,19 @@ ipcMain.handle(
}
);
ipcMain.handle('SET_VLC_REUSE_INSTANCE', (_event, reuseInstance: boolean) => {
console.log('... setting vlc reuse instance', reuseInstance);
store.set(VLC_REUSE_INSTANCE, reuseInstance);
// If disabling reuse, kill the existing tracked process
if (!reuseInstance && vlcProcess && !vlcProcess.killed) {
console.log('Disabling reuse, cleaning up existing VLC process');
vlcProcess.kill();
vlcProcess = null;
vlcRcPort = null;
}
});
ipcMain.handle(
CLOSE_EXTERNAL_PLAYER_SESSION,
async (_event, sessionId: string) => {
@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import {
MPV_REUSE_INSTANCE,
store,
VLC_REUSE_INSTANCE,
} from '../services/store.service';
import { httpServer } from '../server/http-server';
@@ -19,6 +20,10 @@ ipcMain.handle('SETTINGS_UPDATE', (_event, arg) => {
store.set(MPV_REUSE_INSTANCE, arg.mpvReuseInstance);
}
if (arg.vlcReuseInstance !== undefined) {
store.set(VLC_REUSE_INSTANCE, arg.vlcReuseInstance);
}
// Handle remote control settings
if (arg.remoteControl !== undefined || arg.remoteControlPort !== undefined) {
const enabled = arg.remoteControl ?? store.get('remoteControl', false);
@@ -5,12 +5,14 @@ export const WINDOW_BOUNDS = 'WINDOW_BOUNDS';
export const MPV_PLAYER_PATH = 'MPV_PLAYER_PATH';
export const VLC_PLAYER_PATH = 'VLC_PLAYER_PATH';
export const MPV_REUSE_INSTANCE = 'MPV_REUSE_INSTANCE';
export const VLC_REUSE_INSTANCE = 'VLC_REUSE_INSTANCE';
export type StoreType = {
[WINDOW_BOUNDS]: Electron.Rectangle;
[MPV_PLAYER_PATH]: string;
[VLC_PLAYER_PATH]: string;
[MPV_REUSE_INSTANCE]: boolean;
[VLC_REUSE_INSTANCE]: boolean;
};
// Export singleton store instance
+1
View File
@@ -45,6 +45,7 @@ const DEFAULT_SETTINGS: Settings = {
mpvPlayerPath: '',
mpvReuseInstance: false,
vlcPlayerPath: '',
vlcReuseInstance: false,
remoteControl: false,
remoteControlPort: 8765,
downloadFolder: '',
@@ -146,5 +146,17 @@
</mat-form-field>
</div>
</div>
<div class="setting-item">
<div class="setting-item__meta">
<h4>{{ 'SETTINGS.VLC_REUSE_INSTANCE_LABEL' | translate }}</h4>
<p>
{{ 'SETTINGS.VLC_REUSE_INSTANCE_DESCRIPTION' | translate }}
</p>
</div>
<div class="setting-item__control setting-item__toggle">
<mat-checkbox formControlName="vlcReuseInstance"></mat-checkbox>
</div>
</div>
}
</section>
@@ -77,6 +77,7 @@ const DEFAULT_SETTINGS = {
mpvPlayerPath: '',
mpvReuseInstance: false,
vlcPlayerPath: '',
vlcReuseInstance: false,
remoteControl: false,
remoteControlPort: 8765,
epgUrl: [],
@@ -188,6 +188,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
mpvPlayerPath: '',
mpvReuseInstance: false,
vlcPlayerPath: '',
vlcReuseInstance: false,
remoteControl: false,
remoteControlPort: [
8765,
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "مسار VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "حدد مسار مشغل VLC في نظامك.",
"VLC_PLAYER_PATH": "مسار مشغل VLC",
"VLC_REUSE_INSTANCE_LABEL": "إعادة استخدام نسخة VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "عند التفعيل، سيتم إعادة استخدام نفس نافذة VLC لتشغيل البثوث المختلفة. عند التعطيل، يفتح كل بث في نافذة VLC جديدة.",
"REMOTE_CONTROL": "التحكم عن بعد",
"REMOTE_CONTROL_DESCRIPTION": "تمكين التحكم عن بعد للتطبيق.",
"REMOTE_CONTROL_PORT": "منفذ التحكم عن بعد",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "مسار VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "حدد مسار مشغل VLC في السيستيم ديالك. غادي يستعمل المسار اللي عطيتيه لملف VLC بدل المسار الافتراضي ديال السيستيم ديالنا.",
"VLC_PLAYER_PATH": "مسار مشغل VLC",
"VLC_REUSE_INSTANCE_LABEL": "إعادة استخدام نسخة VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "إلى كانت مفعلة، نفس نافذة VLC غادي تستعمل لتشغيل بثوث مختلفة. إلى كانت معطلة، كل بث كيتحل في نافذة VLC جديدة.",
"REMOTE_CONTROL": "التحكم عن بعد",
"REMOTE_CONTROL_DESCRIPTION": "فعل التحكم عن بعد للتطبيق.",
"REMOTE_CONTROL_PORT": "منفذ التحكم عن بعد",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "Шлях да VLC прайгравальніка",
"VLC_PLAYER_PATH_DESCRIPTION": "Усталюйце шлях да прайгравальніка VLC у вашай сістэме. \nЁн будзе выкарыстоўваць прадастаўлены шлях да бінарнага файла VLC замест шляху па змаўчанні.",
"VLC_PLAYER_PATH": "Шлях да VLC прайгравальніка",
"VLC_REUSE_INSTANCE_LABEL": "Паўторна выкарыстоўваць асобнік VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Калі ўключана, тое ж акно VLC будзе паўторна выкарыстоўвацца для прайгравання розных патокаў. Калі адключана, кожны паток адкрываецца ў новым акне VLC.",
"REMOTE_CONTROL": "Аддаленае кіраванне",
"REMOTE_CONTROL_DESCRIPTION": "Уключыць аддаленае кіраванне праграмай.",
"REMOTE_CONTROL_PORT": "Порт аддаленага кіравання",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC-Pfad",
"VLC_PLAYER_PATH_DESCRIPTION": "Legen Sie den Pfad zum VLC-Player in Ihrem System fest. \nEs wird der bereitgestellte Pfad zu einer VLC-Binärdatei anstelle des Standardpfads für unser System verwendet.",
"VLC_PLAYER_PATH": "VLC-Playerpfad",
"VLC_REUSE_INSTANCE_LABEL": "VLC-Instanz wiederverwenden",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Wenn aktiviert, wird dasselbe VLC-Fenster zum Abspielen verschiedener Streams wiederverwendet. Wenn deaktiviert, wird jeder Stream in einem neuen VLC-Fenster geöffnet.",
"REMOTE_CONTROL": "Fernsteuerung",
"REMOTE_CONTROL_DESCRIPTION": "Fernsteuerung der Anwendung aktivieren.",
"REMOTE_CONTROL_PORT": "Fernsteuerungsport",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "διαδρομή VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "Ορίστε τη διαδρομή προς το VLC player στο σύστημά σας. Θα χρησιμοποιήσει την παρεχόμενη διαδρομή προς ένα δυαδικό αρχείο VLC αντί για την προεπιλεγμένη διαδρομή για το σύστημά μας.",
"VLC_PLAYER_PATH": "διαδρομή VLC player",
"VLC_REUSE_INSTANCE_LABEL": "Επαναχρησιμοποίηση της παρουσίας VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Όταν είναι ενεργοποιημένη, το ίδιο παράθυρο VLC θα επαναχρησιμοποιηθεί για την αναπαραγωγή διαφορετικών ροών. Όταν είναι απενεργοποιημένη, κάθε ροή ανοίγει σε ένα νέο παράθυρο VLC.",
"REMOTE_CONTROL": "Τηλεχειριστήριο",
"REMOTE_CONTROL_DESCRIPTION": "Ενεργοποιήστε τον τηλεχειρισμό της εφαρμογής.",
"REMOTE_CONTROL_PORT": "Θύρα τηλεχειριστηρίου",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC path",
"VLC_PLAYER_PATH_DESCRIPTION": "Set path to VLC player in your system. It will use the provided path to a VLC binary instead of the default path for our system.",
"VLC_PLAYER_PATH": "VLC player path",
"VLC_REUSE_INSTANCE_LABEL": "Reuse VLC instance",
"VLC_REUSE_INSTANCE_DESCRIPTION": "When enabled, the same VLC window will be reused for playing different streams. When disabled, each stream opens in a new VLC window.",
"REMOTE_CONTROL": "Remote control",
"REMOTE_CONTROL_DESCRIPTION": "Enable remote control of the application.",
"REMOTE_CONTROL_PORT": "Remote control port",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "ruta VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "Establezca la ruta al reproductor VLC en su sistema. \nUtilizará la ruta proporcionada a un binario VLC en lugar de la ruta predeterminada para nuestro sistema.",
"VLC_PLAYER_PATH": "Ruta del reproductor VLC",
"VLC_REUSE_INSTANCE_LABEL": "Reutilizar instancia de VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Cuando esta opción está activada, se reutiliza la misma ventana de VLC para reproducir distintos streams. Cuando está desactivada, cada stream se abre en una nueva ventana de VLC.",
"REMOTE_CONTROL": "Control remoto",
"REMOTE_CONTROL_DESCRIPTION": "Habilita el control remoto de la aplicación.",
"REMOTE_CONTROL_PORT": "Puerto de control remoto",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "Chemin VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "Définissez le chemin d'accès au lecteur VLC dans votre système. Il utilisera le chemin fourni vers un binaire VLC au lieu du chemin par défaut.",
"VLC_PLAYER_PATH": "Chemin du lecteur VLC",
"VLC_REUSE_INSTANCE_LABEL": "Réutiliser l'instance VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Lorsqu'elle est activée, la même fenêtre VLC sera réutilisée pour lire différents flux. Lorsqu'elle est désactivée, chaque flux s'ouvre dans une nouvelle fenêtre VLC.",
"REMOTE_CONTROL": "Télécommande",
"REMOTE_CONTROL_DESCRIPTION": "Activer la télécommande de l'application.",
"REMOTE_CONTROL_PORT": "Port de télécommande",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "Percorso VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "Imposta il percorso del lettore VLC nel sistema. Verrà usato il percorso fornito per il file exe VLC invece del percorso predefinito per il sistema.",
"VLC_PLAYER_PATH": "Percorso lettore VLC",
"VLC_REUSE_INSTANCE_LABEL": "Riutilizza istanza VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Quando attivo, la stessa finestra VLC viene riutilizzata per riprodurre stream diversi. Quando disattivo, ogni stream viene aperto in una nuova finestra VLC.",
"REMOTE_CONTROL": "Telecomando",
"REMOTE_CONTROL_DESCRIPTION": "Abilita controllo remoto nell'applicazione.",
"REMOTE_CONTROL_PORT": "Porta controllo remoto",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLCのパス",
"VLC_PLAYER_PATH_DESCRIPTION": "システム内のVLCプレーヤーのパスを設定します。システムのデフォルトパスの代わりに、指定されたVLCバイナリのパスを使用します。",
"VLC_PLAYER_PATH": "VLCプレーヤーのパス",
"VLC_REUSE_INSTANCE_LABEL": "VLCインスタンスを再利用",
"VLC_REUSE_INSTANCE_DESCRIPTION": "有効にすると、異なるストリームの再生に同じVLCウィンドウが再利用されます。無効にすると、各ストリームが新しいVLCウィンドウで開きます。",
"REMOTE_CONTROL": "リモートコントロール",
"REMOTE_CONTROL_DESCRIPTION": "アプリケーションのリモートコントロールを有効にします。",
"REMOTE_CONTROL_PORT": "リモートコントロールポート",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC 경로",
"VLC_PLAYER_PATH_DESCRIPTION": "시스템에서 VLC 플레이어의 경로를 설정하십시오. \n시스템의 기본 경로 대신 VLC 바이너리에 제공된 경로를 사용합니다.",
"VLC_PLAYER_PATH": "VLC 플레이어 경로",
"VLC_REUSE_INSTANCE_LABEL": "VLC 인스턴스 재사용",
"VLC_REUSE_INSTANCE_DESCRIPTION": "활성화하면 동일한 VLC 창에서 다른 스트림을 재생합니다. 비활성화하면 각 스트림이 새 VLC 창에서 열립니다.",
"REMOTE_CONTROL": "원격 제어",
"REMOTE_CONTROL_DESCRIPTION": "애플리케이션의 원격 제어를 활성화합니다.",
"REMOTE_CONTROL_PORT": "원격 제어 포트",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC-bestandspad",
"VLC_PLAYER_PATH_DESCRIPTION": "Stel het bestandspad in naar de VLC-speler op je systeem. Dit bestandspad wordt gebruikt in plaats van het standaardpad van het systeem.",
"VLC_PLAYER_PATH": "VLC-speler bestandspad",
"VLC_REUSE_INSTANCE_LABEL": "VLC-instantie hergebruiken",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Wanneer ingeschakeld, wordt hetzelfde VLC-venster hergebruikt voor het afspelen van verschillende streams. Wanneer uitgeschakeld, wordt elke stream geopend in een nieuw VLC-venster.",
"REMOTE_CONTROL": "Afstandsbediening",
"REMOTE_CONTROL_DESCRIPTION": "Schakel de afstandsbediening voor de applicatie in.",
"REMOTE_CONTROL_PORT": "Afstandsbedieningspoort",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "Ścieżka VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "Ustaw ścieżkę do odtwarzacza VLC w systemie. Będzie używać podanej ścieżki do pliku binarnego VLC zamiast domyślnej ścieżki dla naszego systemu.",
"VLC_PLAYER_PATH": "Ścieżka odtwarzacza VLC",
"VLC_REUSE_INSTANCE_LABEL": "Użyj ponownie instancji VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Po włączeniu to samo okno VLC będzie używane do odtwarzania różnych strumieni. Po wyłączeniu każdy strumień otwiera się w nowym oknie VLC.",
"REMOTE_CONTROL": "Zdalne sterowanie",
"REMOTE_CONTROL_DESCRIPTION": "Włącz zdalne sterowanie aplikacją.",
"REMOTE_CONTROL_PORT": "Port zdalnego sterowania",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "Caminho do VLC",
"VLC_PLAYER_PATH_DESCRIPTION": "Defina o caminho para o reprodutor VLC em seu sistema. Ele usará o caminho fornecido para um binário VLC em vez do caminho padrão para o nosso sistema.",
"VLC_PLAYER_PATH": "Caminho do reprodutor VLC",
"VLC_REUSE_INSTANCE_LABEL": "Reutilizar instância do VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Quando ativado, a mesma janela do VLC será reutilizada para reproduzir streams diferentes. Quando desativado, cada stream abre em uma nova janela do VLC.",
"REMOTE_CONTROL": "Controle remoto",
"REMOTE_CONTROL_DESCRIPTION": "Ativar controle remoto do aplicativo.",
"REMOTE_CONTROL_PORT": "Porta de controle remoto",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "Путь к VLC плееру",
"VLC_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю VLC в вашей системе. \nОн будет использовать предоставленный путь к binary файлу VLC вместо пути по умолчанию.",
"VLC_PLAYER_PATH": "Путь к VLC плееру",
"VLC_REUSE_INSTANCE_LABEL": "Переиспользовать экземпляр VLC",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Когда включено, одно и то же окно VLC будет использоваться для воспроизведения разных потоков. Когда отключено, каждый поток открывается в новом окне VLC.",
"REMOTE_CONTROL": "Удалённое управление",
"REMOTE_CONTROL_DESCRIPTION": "Включить удалённое управление приложением.",
"REMOTE_CONTROL_PORT": "Порт удалённого управления",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC yolu",
"VLC_PLAYER_PATH_DESCRIPTION": "Sisteminizdeki VLC oynatıcı yolunu ayarlayın. Sistemimiz için varsayılan yolu kullanmak yerine, sağlanan VLC ikili dosyası yolunu kullanacaktır.",
"VLC_PLAYER_PATH": "VLC oynatıcı yolu",
"VLC_REUSE_INSTANCE_LABEL": "VLC örneğini yeniden kullan",
"VLC_REUSE_INSTANCE_DESCRIPTION": "Etkinleştirildiğinde, farklı yayınları oynatmak için aynı VLC penceresi yeniden kullanılır. Devre dışı bırakıldığında, her yayın yeni bir VLC penceresinde açılır.",
"REMOTE_CONTROL": "Uzaktan kontrol",
"REMOTE_CONTROL_DESCRIPTION": "Uygulamanın uzaktan kontrolünü etkinleştirin.",
"REMOTE_CONTROL_PORT": "Uzaktan kontrol portu",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC路径",
"VLC_PLAYER_PATH_DESCRIPTION": "设置系统中 VLC 播放器的路径。\n它将使用提供的 VLC 二进制文件路径,而不是我们系统的默认路径。",
"VLC_PLAYER_PATH": "VLC播放器路径",
"VLC_REUSE_INSTANCE_LABEL": "复用 VLC 实例",
"VLC_REUSE_INSTANCE_DESCRIPTION": "启用后,将复用同一个 VLC 窗口播放不同的视频流。禁用后,每个视频流都会在新的 VLC 窗口中打开。",
"REMOTE_CONTROL": "远程控制",
"REMOTE_CONTROL_DESCRIPTION": "启用对应用程序的远程控制。",
"REMOTE_CONTROL_PORT": "远程控制端口",
+2
View File
@@ -323,6 +323,8 @@
"VLC_PLAYER_PATH_LABEL": "VLC 路徑",
"VLC_PLAYER_PATH_DESCRIPTION": "設定您系統中 VLC 播放器的路徑。將使用提供的 VLC 二進位檔路徑,而非系統預設路徑。",
"VLC_PLAYER_PATH": "VLC 播放器路徑",
"VLC_REUSE_INSTANCE_LABEL": "重複使用 VLC 實體",
"VLC_REUSE_INSTANCE_DESCRIPTION": "啟用後,相同的 VLC 視窗將重複用於播放不同串流。停用時,每個串流會在新的 VLC 視窗中開啟。",
"REMOTE_CONTROL": "遠端控制",
"REMOTE_CONTROL_DESCRIPTION": "啟用應用程式遠端控制功能。",
"REMOTE_CONTROL_PORT": "遠端控制連接埠",
@@ -105,6 +105,7 @@ Current contract:
- AppImage, deb/rpm, snap, macOS, and Windows keep the existing direct process spawn flow.
- VLC keeps the current external-session flow in Flatpak, including the RC port used for progress polling.
- MPV is intentionally reduced in Flatpak: the app does not reuse an existing MPV instance there and does not open the Unix socket bridge used for non-Flatpak progress polling.
- VLC instance reuse is also gated off in Flatpak. Outside Flatpak the user can opt in via the "Reuse VLC instance" setting; the app then keeps a single tracked VLC process and drives subsequent stream loads through its RC interface (`clear` + `add <url> :http-*`) instead of spawning a new window per click.
This keeps non-Flatpak behavior unchanged while allowing Flatpak builds to open host-installed external players.
@@ -30,6 +30,7 @@ const DEFAULT_SETTINGS: Settings = {
mpvPlayerPath: '',
mpvReuseInstance: false,
vlcPlayerPath: '',
vlcReuseInstance: false,
remoteControl: false,
remoteControlPort: 8765,
epgUrl: [],
@@ -133,6 +134,7 @@ export const SettingsStore = signalStore(
mpvPlayerPath: store.mpvPlayerPath(),
mpvReuseInstance: store.mpvReuseInstance(),
vlcPlayerPath: store.vlcPlayerPath(),
vlcReuseInstance: store.vlcReuseInstance(),
remoteControl: store.remoteControl(),
remoteControlPort: store.remoteControlPort(),
epgUrl: store.epgUrl(),
@@ -34,6 +34,7 @@ export const SET_MPV_PLAYER_PATH = 'SET_MPV_PLAYER_PATH';
export const SET_MPV_REUSE_INSTANCE = 'SET_MPV_REUSE_INSTANCE';
export const OPEN_VLC_PLAYER = 'OPEN_VLC_PLAYER';
export const SET_VLC_PLAYER_PATH = 'SET_VLC_PLAYER_PATH';
export const SET_VLC_REUSE_INSTANCE = 'SET_VLC_REUSE_INSTANCE';
export const CLOSE_EXTERNAL_PLAYER_SESSION = 'CLOSE_EXTERNAL_PLAYER_SESSION';
export const EXTERNAL_PLAYER_SESSION_UPDATE =
'EXTERNAL_PLAYER_SESSION_UPDATE';
@@ -38,6 +38,7 @@ export interface Settings {
mpvPlayerPath: string;
mpvReuseInstance: boolean;
vlcPlayerPath: string;
vlcReuseInstance: boolean;
remoteControl: boolean;
remoteControlPort: number;
/** Custom download folder path (uses system Downloads folder if not set) */