mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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:
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": "منفذ التحكم عن بعد",
|
||||
|
||||
@@ -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": "منفذ التحكم عن بعد",
|
||||
|
||||
@@ -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": "Порт аддаленага кіравання",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Θύρα τηλεχειριστηρίου",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "リモートコントロールポート",
|
||||
|
||||
@@ -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": "원격 제어 포트",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Порт удалённого управления",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "远程控制端口",
|
||||
|
||||
@@ -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) */
|
||||
|
||||
Reference in new issue
Block a user