feat(embedded-mpv): configurable extra libmpv options and network auto-reconnect (#1515)

Extra libmpv options (Settings > Playback) reach every embedded engine off the command line (createSession array on Windows/macOS, a 0600 --include file on Linux native-view, a stdin preamble for the frame-copy helper); the keys the embed depends on are refused, and keys libmpv rejects are reported once per session. Dropped streams reload automatically (error, or ended on live) with 2 s -> 30 s backoff, six attempts per outage and a 30 s stability reset, only for a load that already played; engine failures stay terminal, a running recording is filed as interrupted and restarted after the reload, and an external subtitle file is re-added. Settings.embeddedMpvAutoReconnect (default on) opts out; the player shows 'Reconnecting... attempt N of M'.

Started by Bpl5966 in #1515 and finished by the maintainers in the same PR.

Co-authored-by: Bpl5966 <amine.b1959@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ZoultandClaude Fable 5.1 authored and GitHub committed 2026-09-04 16:36:14 +02:00
1 parent 6cfdaf5900
commit 5a11b82eaf
66 files changed
+3740 -101

No files matched your search

@@ -355,3 +355,17 @@ describe('redactSensitiveData', () => {
});
});
});
describe('redactSensitiveData embedded MPV options', () => {
it('redacts the whole extra-options text, which can carry request headers', () => {
const secret = 'embedded-mpv-header-secret';
const redacted = redactSensitiveData({
player: 'embedded-mpv',
embeddedMpvExtraOptions: `cache-secs=5\nhttp-header-fields=X-Playback-Key: ${secret}`,
}) as Record<string, unknown>;
expect(JSON.stringify(redacted)).not.toContain(secret);
expect(redacted['player']).toBe('embedded-mpv');
});
});
@@ -15,6 +15,8 @@ const SENSITIVE_KEY_NAMES = new Set([
'credentials',
'deviceid',
'deviceid2',
// Free-form libmpv lines can carry request headers with credentials.
'embeddedmpvextraoptions',
'login',
'mac',
'macaddress',
@@ -33,11 +35,23 @@ const SENSITIVE_KEY_NAMES = new Set([
]);
const SENSITIVE_KEY_SUFFIXES = [
'apikey', 'authorization', 'cookie',
'deviceid', 'deviceid1', 'deviceid2',
'macaddress', 'passwd', 'password', 'prehash',
'serialnumber', 'signature', 'signature1', 'signature2',
'secret', 'token', 'username',
'apikey',
'authorization',
'cookie',
'deviceid',
'deviceid1',
'deviceid2',
'macaddress',
'passwd',
'password',
'prehash',
'serialnumber',
'signature',
'signature1',
'signature2',
'secret',
'token',
'username',
];
const XTREAM_CREDENTIAL_PATH_SEGMENTS = new Set([
@@ -160,25 +174,20 @@ function redactUrlStrings(
value: string,
sanitizeValue: (value: string) => string
): string {
return value.replace(
/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/giu,
(candidate) => {
try {
return redactUrl(new URL(candidate), sanitizeValue);
} catch {
return candidate;
}
return value.replace(/[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/giu, (candidate) => {
try {
return redactUrl(new URL(candidate), sanitizeValue);
} catch {
return candidate;
}
);
});
}
function redactEmbeddedSensitivePairs(value: string): string {
const redactedAssignments = value.replace(
/\b([a-z][a-z0-9_.-]*)(\s*=\s*)((?:Bearer\s+)?[^&\s,;]+)/giu,
(match, key: string, separator: string) =>
isSensitiveKey(key)
? `${key}${separator}${REDACTED_VALUE}`
: match
isSensitiveKey(key) ? `${key}${separator}${REDACTED_VALUE}` : match
);
return redactedAssignments.replace(
/\b([a-z0-9_.-]*(?:api[-_.]?key|auth(?:orization)?|cookie|credentials|device[-_.]?id[12]?|login|mac(?:[-_.]?address)?|passwd|password|prehash|pwd|secret|serial[-_.]?number|set[-_.]?cookie|signature[12]?|sn|token|username))(\s*:\s*)(?:Bearer\s+)?[^;,\r\n]+/giu,
@@ -286,9 +295,10 @@ export function redactSensitiveData(
if (error.stack) {
const [, ...stackFrames] = error.stack.split('\n');
output['stack'] = visitString(
[`${output['name']}: ${output['message']}`, ...stackFrames].join(
'\n'
),
[
`${output['name']}: ${output['message']}`,
...stackFrames,
].join('\n'),
depth + 1
);
}
@@ -370,8 +380,7 @@ export function redactSensitiveData(
const stringKey = String(key);
const redactedKey = visitString(stringKey, depth + 1);
entries[redactedKey] =
isSensitiveKey(stringKey) &&
!/[/:?=&]/u.test(stringKey)
isSensitiveKey(stringKey) && !/[/:?=&]/u.test(stringKey)
? REDACTED_VALUE
: visit(entry, depth + 1);
}