mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(playback): hide VLC RC console on Windows (#1533)
* fix(playback): hide VLC RC console on Windows * fix(playback): preserve VLC retry arguments containing quiet flag
This commit is contained in:
1 parent
0245d73d78
commit
07ccf45f32
5 files changed
+185
-5
No files matched your search
@@ -0,0 +1,7 @@
|
||||
---
|
||||
type: fix
|
||||
area: playback
|
||||
issues: [871]
|
||||
---
|
||||
|
||||
VLC now opens without an extra console window on Windows when playback progress tracking or Reuse VLC instance is enabled.
|
||||
@@ -47,6 +47,7 @@ import { externalPlayerSessions } from './external-player-runtime';
|
||||
import { externalPlayerProcessTeardownGate } from './external-player-process';
|
||||
import { openVlcPlayer, shutdownVlcSession } from './vlc-session.service';
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
const spawnMock = spawn as unknown as jest.Mock;
|
||||
const streamUrl = 'https://example.com/stream.m3u8';
|
||||
const rcWrites: string[] = [];
|
||||
@@ -136,6 +137,7 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
// Drop any process tracked for reuse so tests stay isolated.
|
||||
shutdownVlcSession();
|
||||
consoleErrorSpy.mockRestore();
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
describe('instance reuse', () => {
|
||||
@@ -1219,12 +1221,14 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
});
|
||||
|
||||
it('retries without the RC interface when VLC exits with code 1', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
mockStoreValues({
|
||||
[VLC_PLAYER_PATH]: '/usr/bin/vlc',
|
||||
[VLC_REUSE_INSTANCE]: true,
|
||||
});
|
||||
const proc = createMockChildProcess();
|
||||
await openTrackedVlcInstance(proc);
|
||||
expect(spawnMock.mock.calls[0][1]).toContain('--rc-quiet');
|
||||
|
||||
const retryProc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(retryProc);
|
||||
@@ -1234,6 +1238,7 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
const retryArgs = spawnMock.mock.calls[1][1] as string[];
|
||||
expect(retryArgs.join(' ')).not.toContain('--extraintf');
|
||||
expect(retryArgs.join(' ')).not.toContain('--rc-host');
|
||||
expect(retryArgs).not.toContain('--rc-quiet');
|
||||
// Retry processes are never tracked for reuse.
|
||||
expect(spawnMock.mock.calls[1][2]).toMatchObject({
|
||||
detached: true,
|
||||
@@ -1262,6 +1267,7 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
});
|
||||
|
||||
it('retries without the RC interface after a start error', async () => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
mockStoreValues({
|
||||
[VLC_PLAYER_PATH]: '/usr/bin/vlc',
|
||||
[VLC_REUSE_INSTANCE]: true,
|
||||
@@ -1272,11 +1278,14 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
|
||||
const openPromise = openVlcPlayer({ title: 'S', url: streamUrl });
|
||||
await waitForSpawnCallCount(1);
|
||||
expect(spawnMock.mock.calls[0][1]).toContain('--rc-quiet');
|
||||
proc.emit('error', new Error('rc unsupported'));
|
||||
await waitForSpawnCallCount(2);
|
||||
|
||||
const retryArgs = spawnMock.mock.calls[1][1] as string[];
|
||||
expect(retryArgs.join(' ')).not.toContain('--extraintf');
|
||||
expect(retryArgs.join(' ')).not.toContain('--rc-host');
|
||||
expect(retryArgs).not.toContain('--rc-quiet');
|
||||
retryProc.emit('spawn');
|
||||
const session = await openPromise;
|
||||
expect(session.status).toBe('opened');
|
||||
|
||||
@@ -39,6 +39,7 @@ import { spawn, type ChildProcess } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import { createServer } from 'net';
|
||||
import {
|
||||
VLC_PLAYER_ARGUMENTS,
|
||||
VLC_PLAYER_PATH,
|
||||
VLC_REUSE_INSTANCE,
|
||||
store,
|
||||
@@ -51,6 +52,7 @@ import {
|
||||
shutdownVlcSession,
|
||||
} from './vlc-session.service';
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
const spawnMock = spawn as unknown as jest.Mock;
|
||||
const streamUrl = 'https://example.com/stream.m3u8';
|
||||
|
||||
@@ -86,6 +88,8 @@ describe('vlc-session.service helpers and launch args', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
jest.useFakeTimers({ doNotFake: ['setImmediate'] });
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
(createServer as unknown as jest.Mock).mockImplementation(() => ({
|
||||
unref: jest.fn(),
|
||||
@@ -103,7 +107,10 @@ describe('vlc-session.service helpers and launch args', () => {
|
||||
afterEach(() => {
|
||||
// Drop any process tracked for reuse so tests stay isolated.
|
||||
shutdownVlcSession();
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
consoleErrorSpy.mockRestore();
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
describe('buildVlcEnqueueCommands', () => {
|
||||
@@ -177,6 +184,152 @@ describe('vlc-session.service helpers and launch args', () => {
|
||||
});
|
||||
|
||||
describe('openVlcPlayer launch args', () => {
|
||||
it.each(['error', 'exit'])(
|
||||
'preserves data containing --rc-quiet during an RC retry (%s)',
|
||||
async (event) => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
mockStoreValues({
|
||||
[VLC_PLAYER_PATH]: '/usr/bin/vlc',
|
||||
[VLC_REUSE_INSTANCE]: true,
|
||||
[VLC_PLAYER_ARGUMENTS]: '--fullscreen\n--rc-quiet',
|
||||
});
|
||||
const proc = createMockChildProcess();
|
||||
const retryProc = createMockChildProcess();
|
||||
spawnMock
|
||||
.mockReturnValueOnce(proc)
|
||||
.mockReturnValueOnce(retryProc);
|
||||
const url = `${streamUrl}?option=--rc-quiet`;
|
||||
const openPromise = openVlcPlayer({
|
||||
title: 'Movie --rc-quiet',
|
||||
url,
|
||||
userAgent: 'Agent --rc-quiet',
|
||||
});
|
||||
await waitForSpawnCallCount(1);
|
||||
proc.emit('spawn');
|
||||
await openPromise;
|
||||
proc.emit(event, event === 'exit' ? 1 : new Error('RC failed'));
|
||||
await waitForSpawnCallCount(2);
|
||||
|
||||
expect(spawnMock.mock.lastCall[1]).toEqual([
|
||||
'--fullscreen',
|
||||
// Custom arguments are preserved; only the managed flag goes.
|
||||
'--rc-quiet',
|
||||
':http-user-agent=Agent --rc-quiet',
|
||||
url,
|
||||
':meta-title=Movie --rc-quiet',
|
||||
]);
|
||||
retryProc.emit('spawn');
|
||||
retryProc.emit('exit', 0);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['win32', 'darwin', 'linux'] as const)(
|
||||
'adds quiet mode only for managed Windows RC launches (%s)',
|
||||
async (platform) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform });
|
||||
for (const reuseInstance of [false, true]) {
|
||||
mockStoreValues({
|
||||
[VLC_PLAYER_PATH]: '/usr/bin/vlc',
|
||||
[VLC_REUSE_INSTANCE]: reuseInstance,
|
||||
});
|
||||
const proc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(proc);
|
||||
const openPromise = openVlcPlayer({
|
||||
title: 'RC Stream',
|
||||
url: streamUrl,
|
||||
// Exercise progress-only and reuse-only RC separately.
|
||||
...(!reuseInstance && {
|
||||
contentInfo: {
|
||||
playlistId: 'playlist',
|
||||
contentXtreamId: 1,
|
||||
contentType: 'vod' as const,
|
||||
},
|
||||
}),
|
||||
});
|
||||
await waitForSpawnCallCount(reuseInstance ? 2 : 1);
|
||||
proc.emit('spawn');
|
||||
await openPromise;
|
||||
|
||||
expect(spawnMock.mock.lastCall[1]).toEqual([
|
||||
'--extraintf=rc',
|
||||
'--rc-host=127.0.0.1:43210',
|
||||
...(platform === 'win32' ? ['--rc-quiet'] : []),
|
||||
streamUrl,
|
||||
':meta-title=RC Stream',
|
||||
]);
|
||||
expect(spawnMock.mock.lastCall[2].detached).toBe(
|
||||
!reuseInstance
|
||||
);
|
||||
proc.emit('exit', 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['--rc-quiet', '--no-rc-quiet'])(
|
||||
'keeps custom arguments before managed quiet mode (%s)',
|
||||
async (customQuiet) => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
mockStoreValues({
|
||||
[VLC_PLAYER_PATH]: '/usr/bin/vlc',
|
||||
[VLC_REUSE_INSTANCE]: true,
|
||||
[VLC_PLAYER_ARGUMENTS]: `--fullscreen\n${customQuiet}`,
|
||||
});
|
||||
const proc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(proc);
|
||||
const openPromise = openVlcPlayer({
|
||||
title: 'Custom',
|
||||
url: streamUrl,
|
||||
});
|
||||
await waitForSpawnCallCount(1);
|
||||
proc.emit('spawn');
|
||||
await openPromise;
|
||||
|
||||
expect(spawnMock.mock.lastCall[1]).toEqual([
|
||||
'--fullscreen',
|
||||
customQuiet,
|
||||
'--extraintf=rc',
|
||||
'--rc-host=127.0.0.1:43210',
|
||||
'--rc-quiet',
|
||||
streamUrl,
|
||||
':meta-title=Custom',
|
||||
]);
|
||||
proc.emit('exit', 0);
|
||||
}
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'does not add quiet mode without an RC port (reuse: %s)',
|
||||
async (reuseInstance) => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32' });
|
||||
mockStoreValues({
|
||||
[VLC_PLAYER_PATH]: '/usr/bin/vlc',
|
||||
[VLC_REUSE_INSTANCE]: reuseInstance,
|
||||
});
|
||||
(createServer as unknown as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
throw new Error('No free port');
|
||||
}
|
||||
);
|
||||
const proc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(proc);
|
||||
const openPromise = openVlcPlayer({
|
||||
title: 'No RC',
|
||||
url: streamUrl,
|
||||
});
|
||||
await waitForSpawnCallCount(1);
|
||||
proc.emit('spawn');
|
||||
await openPromise;
|
||||
|
||||
expect(spawnMock.mock.lastCall[1]).toEqual([
|
||||
streamUrl,
|
||||
':meta-title=No RC',
|
||||
]);
|
||||
expect(createServer).toHaveBeenCalledTimes(
|
||||
reuseInstance ? 1 : 0
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
it('spawns a detached VLC process with http options and start time', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(proc);
|
||||
|
||||
@@ -249,6 +249,10 @@ export async function openVlcPlayer({
|
||||
if (rcPort > 0) {
|
||||
args.push('--extraintf=rc');
|
||||
args.push(`--rc-host=127.0.0.1:${rcPort}`);
|
||||
if (process.platform === 'win32') {
|
||||
// Keep TCP control without VLC's separate DOS console.
|
||||
args.push('--rc-quiet');
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveUserAgent) {
|
||||
@@ -521,7 +525,7 @@ export async function openVlcPlayer({
|
||||
(arg) =>
|
||||
!arg.includes('--extraintf') &&
|
||||
!arg.includes('--rc-host') &&
|
||||
!arg.includes('--rc-quiet')
|
||||
arg !== '--rc-quiet'
|
||||
);
|
||||
spawnVlc(retryArgs, true);
|
||||
} else {
|
||||
@@ -572,7 +576,7 @@ export async function openVlcPlayer({
|
||||
(arg) =>
|
||||
!arg.includes('--extraintf') &&
|
||||
!arg.includes('--rc-host') &&
|
||||
!arg.includes('--rc-quiet')
|
||||
arg !== '--rc-quiet'
|
||||
);
|
||||
spawnVlc(retryArgs, true);
|
||||
return;
|
||||
|
||||
@@ -867,15 +867,22 @@ path-only setting; extra flags are stored separately as `mpvPlayerArguments` and
|
||||
|
||||
Argument fields are line-oriented: one non-empty trimmed line becomes one argv
|
||||
entry. IPTVnator prepends those custom entries before its stream-specific runtime
|
||||
arguments, then keeps the stream URL last. This avoids shell parsing, keeps paths
|
||||
with spaces safe, and preserves existing settings for users who never configured
|
||||
extra arguments.
|
||||
arguments. This avoids shell parsing, keeps paths with spaces safe, and preserves
|
||||
existing settings for users who never configured extra arguments.
|
||||
|
||||
The arguments apply only when IPTVnator spawns a new external player process. If
|
||||
MPV or VLC instance reuse is active and an existing process is reused, subsequent
|
||||
streams are loaded through MPV IPC or VLC RC commands and new process arguments
|
||||
are not re-applied until a fresh process starts.
|
||||
|
||||
VLC enables its TCP RC interface when content metadata requires progress
|
||||
tracking or instance reuse is enabled. On Windows, these managed RC launches
|
||||
also append `--rc-quiet` after custom arguments to suppress VLC's DOS console
|
||||
while retaining TCP control. macOS/Linux and launches without an allocated RC
|
||||
port receive no automatic quiet flag. Both launch-error and exit-code-1 retries
|
||||
remove the app-generated RC flags, including `--rc-quiet`; custom arguments
|
||||
continue to be prepended unchanged.
|
||||
|
||||
## Electron External Player Ownership
|
||||
|
||||
External MPV/VLC integration is split across focused main-process modules:
|
||||
|
||||
Reference in new issue
Block a user