fix(player): clear reused mpv request headers

This commit is contained in:
4gray committed 2026-07-27 12:35:59 +02:00
1 parent edf47b19b7
commit 9aa93079bd
2 files changed
+196 -24

No files matched your search

@@ -45,6 +45,7 @@ export interface OpenExternalPlayerRequest {
let mpvProcess: ChildProcess | null = null;
let mpvSocketPath: string | null = null;
let positionPollingInterval: NodeJS.Timeout | null = null;
let mpvReuseOperationQueue: Promise<void> = Promise.resolve();
function getMpvPath(options: PlayerPathOptions = {}): string {
return (
@@ -182,6 +183,17 @@ function sendMpvCommand(
});
}
function enqueueMpvReuseOperation<T>(
operation: () => Promise<T>
): Promise<T> {
const queuedOperation = mpvReuseOperationQueue.then(operation);
mpvReuseOperationQueue = queuedOperation.then(
() => undefined,
() => undefined
);
return queuedOperation;
}
function killStoredMpvProcess(reason: string): void {
if (!mpvProcess || mpvProcess.killed) {
return;
@@ -283,36 +295,42 @@ export async function openMpvPlayer({
) {
traceExternalPlayer('reuse existing mpv instance');
try {
if (effectiveUserAgent) {
await enqueueMpvReuseOperation(async () => {
await sendMpvCommand('set_property', [
'user-agent',
effectiveUserAgent,
effectiveUserAgent ?? '',
]);
}
if (effectiveReferer) {
await sendMpvCommand('set_property', [
'referrer',
effectiveReferer,
effectiveReferer ?? '',
]);
}
if (headerFields.length > 0) {
await sendMpvCommand('set_property', [
'http-header-fields',
headerFields.join(','),
]);
}
const loadFileArgs: Array<string | number> = [url, 'replace'];
const loadFileOptions: string[] = [];
const loadFileArgs: Array<string | number> = [
url,
'replace',
];
const loadFileOptions: string[] = [];
if (title) {
loadFileOptions.push(`force-media-title=${title}`);
}
if (loadFileOptions.length > 0) {
loadFileArgs.push(-1, loadFileOptions.join(','));
}
if (title) {
loadFileOptions.push(`force-media-title=${title}`);
}
if (loadFileOptions.length > 0) {
loadFileArgs.push(-1, loadFileOptions.join(','));
}
await sendMpvCommand('loadfile', loadFileArgs);
await sendMpvCommand('loadfile', loadFileArgs);
if (startTime) {
await sendMpvCommand('seek', [
String(startTime),
'absolute',
]);
}
});
traceExternalPlayer('loaded new url in existing mpv instance');
externalPlayerSessions.attachCloser(session.id, async () => {
@@ -325,13 +343,6 @@ export async function openMpvPlayer({
}
});
if (startTime) {
await sendMpvCommand('seek', [
String(startTime),
'absolute',
]);
}
if (contentInfo) {
startPositionPolling(
mpvSocketPath,
@@ -8,6 +8,13 @@ jest.mock('child_process', () => ({
spawn: jest.fn(),
}));
const mockCreateConnection = jest.fn();
jest.mock('net', () => ({
...jest.requireActual('net'),
createConnection: mockCreateConnection,
}));
jest.mock('../app', () => ({
__esModule: true,
default: {
@@ -40,6 +47,7 @@ import { ipcMain } from 'electron';
import { spawn, type ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import {
MPV_REUSE_INSTANCE,
MPV_PLAYER_PATH,
store,
VLC_PLAYER_PATH,
@@ -59,6 +67,10 @@ import {
shouldUseMpvSocketBridge,
} from './player.events';
import { resolveEffectiveExternalPlaybackRequest } from './external-player-playback-request';
import {
openMpvPlayer,
shutdownMpvSession,
} from './mpv-session.service';
import { openVlcPlayer } from './vlc-session.service';
function createPathExists(existingPaths: string[]) {
@@ -532,6 +544,155 @@ describe('player.events Stalker playback contexts', () => {
});
});
describe('openMpvPlayer reusable request isolation', () => {
const ipcCommands: unknown[][] = [];
beforeEach(() => {
ipcCommands.length = 0;
mockGetStalkerPlaybackContextHeaders.mockReset();
mockGetStalkerPlaybackContextHeaders.mockReturnValue(undefined);
mockCreateConnection.mockReset();
mockCreateConnection.mockImplementation(() => {
const socket = Object.assign(new EventEmitter(), {
destroy: jest.fn(),
destroyed: false,
end: jest.fn(),
write: jest.fn((request: string) => {
const payload = JSON.parse(request) as {
command: unknown[];
};
ipcCommands.push(payload.command);
return true;
}),
});
setImmediate(() => socket.emit('connect'));
return socket;
});
(spawn as unknown as jest.Mock).mockReset();
(spawn as unknown as jest.Mock).mockReturnValue(
createMockChildProcess()
);
(store.get as unknown as jest.Mock).mockImplementation(
(key: string, fallback?: unknown) =>
key === MPV_REUSE_INSTANCE ? true : fallback
);
});
afterEach(() => {
shutdownMpvSession();
});
it('clears managed UA, referrer, Authorization, and Cookie before loading a plain source', async () => {
await openMpvPlayer({
title: '',
url: 'https://seed.example/stream.m3u8',
});
await openMpvPlayer({
mainOwnedHeaders: {
Authorization: 'Bearer managed-token',
Cookie: 'session=managed-cookie',
Origin: 'https://portal.example',
Referer: 'https://portal.example/player/',
'User-Agent': 'ManagedAgent/1.0',
},
title: '',
url: 'https://portal.example/managed.m3u8',
});
expect(ipcCommands).toEqual([
['set_property', 'user-agent', 'ManagedAgent/1.0'],
[
'set_property',
'referrer',
'https://portal.example/player/',
],
[
'set_property',
'http-header-fields',
'Authorization: Bearer managed-token,Cookie: session=managed-cookie,Origin: https://portal.example,Referer: https://portal.example/player/,User-Agent: ManagedAgent/1.0',
],
[
'loadfile',
'https://portal.example/managed.m3u8',
'replace',
],
]);
ipcCommands.length = 0;
await openMpvPlayer({
title: '',
url: 'https://plain.example/public.m3u8',
});
expect(ipcCommands).toEqual([
['set_property', 'user-agent', ''],
['set_property', 'referrer', ''],
['set_property', 'http-header-fields', ''],
[
'loadfile',
'https://plain.example/public.m3u8',
'replace',
],
]);
expect(JSON.stringify(ipcCommands)).not.toContain('managed-token');
expect(JSON.stringify(ipcCommands)).not.toContain('managed-cookie');
});
it('serializes concurrent managed and plain loads so their request properties cannot interleave', async () => {
await openMpvPlayer({
title: '',
url: 'https://seed.example/stream.m3u8',
});
const managedOpen = openMpvPlayer({
mainOwnedHeaders: {
Authorization: 'Bearer concurrent-token',
Cookie: 'session=concurrent-cookie',
Referer: 'https://portal.example/player/',
'User-Agent': 'ConcurrentAgent/1.0',
},
title: '',
url: 'https://portal.example/concurrent.m3u8',
});
const plainOpen = openMpvPlayer({
title: '',
url: 'https://plain.example/concurrent.m3u8',
});
await Promise.all([managedOpen, plainOpen]);
expect(ipcCommands).toEqual([
['set_property', 'user-agent', 'ConcurrentAgent/1.0'],
[
'set_property',
'referrer',
'https://portal.example/player/',
],
[
'set_property',
'http-header-fields',
'Authorization: Bearer concurrent-token,Cookie: session=concurrent-cookie,Referer: https://portal.example/player/,User-Agent: ConcurrentAgent/1.0',
],
[
'loadfile',
'https://portal.example/concurrent.m3u8',
'replace',
],
['set_property', 'user-agent', ''],
['set_property', 'referrer', ''],
['set_property', 'http-header-fields', ''],
[
'loadfile',
'https://plain.example/concurrent.m3u8',
'replace',
],
]);
});
});
describe('openVlcPlayer', () => {
beforeEach(() => {
(spawn as unknown as jest.Mock).mockReset();