mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(playback): harden external player lifecycle
This commit is contained in:
1 parent
7ed1ccfd8e
commit
40bfa8412a
33 files changed
+307
-103
No files matched your search
@@ -3,4 +3,4 @@ type: fix
|
||||
area: playback
|
||||
---
|
||||
|
||||
MPV and VLC recovery actions now stay available after launch attempts, show precise opening, started, playing, and failure feedback, and prevent overlapping external-player launches. Failed launches remain visible in the playback dock until dismissed.
|
||||
MPV and VLC recovery actions now stay available after launch attempts, show precise opening, started, playing, and failure feedback, and prevent overlapping external-player launches. External-player errors remain visible in the playback dock until dismissed.
|
||||
@@ -291,10 +291,12 @@ Key files:
|
||||
evidence or ownership state. MPV/VLC actions remain mounted after an attempt
|
||||
and expose credential-free per-target launching/started/playing/error state;
|
||||
only an exact Electron `playing` update is labelled Playing. One handshake is
|
||||
allowed at a time, switching players first closes the tracked live external
|
||||
session, and stale updates cannot take ownership. The global dock mirrors
|
||||
those statuses, keeps errors visible until dismissal, and intentionally has
|
||||
no retry because it does not own the original launch headers or credentials.
|
||||
allowed at a time, a replacement waits for confirmed teardown of the tracked
|
||||
external process, and stale updates cannot take ownership. If the local
|
||||
handshake times out after an exact Electron session is known, that ID remains
|
||||
correlated so a later exact update can recover the UI. The global dock mirrors
|
||||
those statuses, keeps errors visible until dismissal, and intentionally has no
|
||||
retry because it does not own the original launch headers or credentials.
|
||||
- The built-in HTML5/hls.js player is the second guarded consumer.
|
||||
`HtmlVideoPlayerComponent` provides a component-scoped
|
||||
`WebVideoControlsAdapter`; its neutral `web-video-support` bridge is shared
|
||||
|
||||
@@ -827,10 +827,12 @@ app as a real argument, so it is not an option.
|
||||
evidence or ownership state. MPV/VLC actions remain mounted after an attempt
|
||||
and expose credential-free per-target launching/started/playing/error state;
|
||||
only an exact Electron `playing` update is labelled Playing. One handshake is
|
||||
allowed at a time, switching players first closes the tracked live external
|
||||
session, and stale updates cannot take ownership. The global dock mirrors
|
||||
those statuses, keeps errors visible until dismissal, and intentionally has
|
||||
no retry because it does not own the original launch headers or credentials.
|
||||
allowed at a time, a replacement waits for confirmed teardown of the tracked
|
||||
external process, and stale updates cannot take ownership. If the local
|
||||
handshake times out after an exact Electron session is known, that ID remains
|
||||
correlated so a later exact update can recover the UI. The global dock mirrors
|
||||
those statuses, keeps errors visible until dismissal, and intentionally has no
|
||||
retry because it does not own the original launch headers or credentials.
|
||||
- DASH + ClearKey (M3U module): `.mpd` channels play through a lazily loaded
|
||||
Shaka Player source engine inside the HTML5 and ArtPlayer components (no new
|
||||
player in settings). ClearKey keys come from `#KODIPROP:inputstream.adaptive.*`
|
||||
|
||||
@@ -569,6 +569,36 @@ test('@electron @dash ClearKey DASH filters DRM fallback and reports external la
|
||||
/web-player-diagnostic__player-card--primary/
|
||||
);
|
||||
await expect(dock).toContainText('Player started');
|
||||
const dockThemeStyles = await dock
|
||||
.locator('.external-playback-dock')
|
||||
.evaluate((element) => {
|
||||
const body = element.ownerDocument.body;
|
||||
const wasDark = body.classList.contains('dark-theme');
|
||||
const read = () => {
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
borderColor: style.borderColor,
|
||||
color: style.color,
|
||||
onSurface: style
|
||||
.getPropertyValue('--app-heading-color')
|
||||
.trim(),
|
||||
surface: style
|
||||
.getPropertyValue('--app-widget-bg')
|
||||
.trim(),
|
||||
};
|
||||
};
|
||||
body.classList.remove('dark-theme');
|
||||
const light = read();
|
||||
body.classList.add('dark-theme');
|
||||
const dark = read();
|
||||
body.classList.toggle('dark-theme', wasDark);
|
||||
return { dark, light };
|
||||
});
|
||||
expect(dockThemeStyles.light.surface).not.toBe('');
|
||||
expect(dockThemeStyles.dark.surface).not.toBe('');
|
||||
expect(dockThemeStyles.light.onSurface).not.toBe('');
|
||||
expect(dockThemeStyles.dark.onSurface).not.toBe('');
|
||||
expect(dockThemeStyles.light).not.toEqual(dockThemeStyles.dark);
|
||||
|
||||
await vlcFallback.click();
|
||||
const expectedBothLaunches = [
|
||||
@@ -588,7 +618,7 @@ test('@electron @dash ClearKey DASH filters DRM fallback and reports external la
|
||||
await expect(mpvFallback).toBeVisible();
|
||||
await expect(vlcFallback).toBeVisible();
|
||||
await expect(vlcFallback).toContainText('Try VLC again');
|
||||
await expect(vlcFallback).toContainText('Player launch failed');
|
||||
await expect(vlcFallback).toContainText('External player error');
|
||||
await expect(mpvFallback).toHaveClass(
|
||||
/web-player-diagnostic__player-card--primary/
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { ChildProcess } from 'child_process';
|
||||
|
||||
/**
|
||||
* Resolves only after the exact child has stopped. A sent termination signal
|
||||
* is not itself proof that a replacement can be started safely.
|
||||
*/
|
||||
export function waitForExternalPlayerProcessExit(
|
||||
child: ChildProcess
|
||||
): Promise<void> {
|
||||
if (hasExited(child)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const complete = () => {
|
||||
child.off('exit', complete);
|
||||
child.off('error', complete);
|
||||
resolve();
|
||||
};
|
||||
child.once('exit', complete);
|
||||
child.once('error', complete);
|
||||
});
|
||||
}
|
||||
|
||||
export function terminateExternalPlayerProcess(
|
||||
child: ChildProcess
|
||||
): Promise<void> {
|
||||
const exited = waitForExternalPlayerProcessExit(child);
|
||||
if (!hasExited(child) && !child.killed) {
|
||||
try {
|
||||
child.kill();
|
||||
} catch {
|
||||
// Keep waiting: failing closed prevents a replacement launch from
|
||||
// overlapping a child whose teardown could not be confirmed.
|
||||
}
|
||||
}
|
||||
return exited;
|
||||
}
|
||||
|
||||
function hasExited(child: ChildProcess): boolean {
|
||||
return child.exitCode !== null || child.signalCode !== null;
|
||||
}
|
||||
@@ -49,13 +49,16 @@ import {
|
||||
VLC_REUSE_INSTANCE,
|
||||
store,
|
||||
} from '../services/store.service';
|
||||
import { externalPlayerSessions } from './external-player-runtime';
|
||||
import { openMpvPlayer, shutdownMpvSession } from './mpv-session.service';
|
||||
import { openVlcPlayer, shutdownVlcSession } from './vlc-session.service';
|
||||
|
||||
function createMockChildProcess(): ChildProcess {
|
||||
return Object.assign(new EventEmitter(), {
|
||||
exitCode: null,
|
||||
killed: false,
|
||||
kill: jest.fn(() => true),
|
||||
signalCode: null,
|
||||
stderr: null,
|
||||
stdout: null,
|
||||
unref: jest.fn(),
|
||||
@@ -222,6 +225,39 @@ describe('external player shutdown on app quit', () => {
|
||||
expect(proc.kill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('waits for a detached MPV process to exit before closing its session', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
(spawn as unknown as jest.Mock).mockReturnValue(proc);
|
||||
mockStoreValues({
|
||||
[MPV_PLAYER_PATH]: '/usr/bin/mpv',
|
||||
[MPV_REUSE_INSTANCE]: false,
|
||||
});
|
||||
const session = await openMpvPlayer({
|
||||
title: 'Detached MPV stream',
|
||||
url: 'https://example.com/live.m3u8',
|
||||
});
|
||||
|
||||
let closeSettled = false;
|
||||
const closePromise = externalPlayerSessions
|
||||
.closeSession(session.id)
|
||||
.then((closed) => {
|
||||
closeSettled = true;
|
||||
return closed;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(proc.kill).toHaveBeenCalledTimes(1);
|
||||
expect(closeSettled).toBe(false);
|
||||
|
||||
Object.defineProperty(proc, 'exitCode', { value: 0 });
|
||||
proc.emit('exit', 0);
|
||||
|
||||
await expect(closePromise).resolves.toMatchObject({
|
||||
id: session.id,
|
||||
status: 'closed',
|
||||
});
|
||||
});
|
||||
|
||||
it('kills the stored reusable VLC process on shutdown', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
(spawn as unknown as jest.Mock).mockReturnValue(proc);
|
||||
|
||||
@@ -29,6 +29,10 @@ import {
|
||||
sendPlayerErrorNotification,
|
||||
traceExternalPlayer,
|
||||
} from './external-player-runtime';
|
||||
import {
|
||||
terminateExternalPlayerProcess,
|
||||
waitForExternalPlayerProcessExit,
|
||||
} from './external-player-process';
|
||||
|
||||
export interface OpenExternalPlayerRequest {
|
||||
url: string;
|
||||
@@ -280,6 +284,7 @@ export async function openMpvPlayer({
|
||||
mpvSocketPath
|
||||
) {
|
||||
traceExternalPlayer('reuse existing mpv instance');
|
||||
const reusedProcess = mpvProcess;
|
||||
try {
|
||||
if (effectiveUserAgent) {
|
||||
await sendMpvCommand('set_property', [
|
||||
@@ -317,10 +322,10 @@ export async function openMpvPlayer({
|
||||
try {
|
||||
await sendMpvCommand('quit', []);
|
||||
} catch {
|
||||
if (mpvProcess && !mpvProcess.killed) {
|
||||
mpvProcess.kill();
|
||||
}
|
||||
await terminateExternalPlayerProcess(reusedProcess);
|
||||
return;
|
||||
}
|
||||
await waitForExternalPlayerProcessExit(reusedProcess);
|
||||
});
|
||||
|
||||
if (startTime) {
|
||||
@@ -373,7 +378,9 @@ export async function openMpvPlayer({
|
||||
}
|
||||
|
||||
if (headerFields.length > 0) {
|
||||
args.push(`--http-header-fields=${joinMpvHeaderFields(headerFields)}`);
|
||||
args.push(
|
||||
`--http-header-fields=${joinMpvHeaderFields(headerFields)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (title) {
|
||||
@@ -500,9 +507,7 @@ export async function openMpvPlayer({
|
||||
}
|
||||
|
||||
externalPlayerSessions.attachCloser(session.id, async () => {
|
||||
if (!proc.killed) {
|
||||
proc.kill();
|
||||
}
|
||||
await terminateExternalPlayerProcess(proc);
|
||||
});
|
||||
|
||||
if (useMpvSocketBridge && contentInfo && socketPath) {
|
||||
|
||||
@@ -52,8 +52,10 @@ const rcWrites: string[] = [];
|
||||
|
||||
function createMockChildProcess(): ChildProcess {
|
||||
return Object.assign(new EventEmitter(), {
|
||||
exitCode: null,
|
||||
killed: false,
|
||||
kill: jest.fn(() => true),
|
||||
signalCode: null,
|
||||
stderr: null,
|
||||
stdout: null,
|
||||
unref: jest.fn(),
|
||||
@@ -163,6 +165,37 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
expect(session.status).toBe('opened');
|
||||
});
|
||||
|
||||
it('quits a reused VLC process and waits for its exact exit on close', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
await openTrackedVlcInstance(proc);
|
||||
installRcSocketMock('ack');
|
||||
const session = await openVlcPlayer({
|
||||
title: 'Second',
|
||||
url: 'https://example.com/two.m3u8',
|
||||
});
|
||||
rcWrites.length = 0;
|
||||
|
||||
let closeSettled = false;
|
||||
const closePromise = externalPlayerSessions
|
||||
.closeSession(session.id)
|
||||
.then((closed) => {
|
||||
closeSettled = true;
|
||||
return closed;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(rcWrites).toEqual(['quit\n']);
|
||||
expect(closeSettled).toBe(false);
|
||||
|
||||
Object.defineProperty(proc, 'exitCode', { value: 0 });
|
||||
proc.emit('exit', 0);
|
||||
|
||||
await expect(closePromise).resolves.toMatchObject({
|
||||
id: session.id,
|
||||
status: 'closed',
|
||||
});
|
||||
});
|
||||
|
||||
it('kills the stale instance and spawns fresh when RC reuse fails', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
await openTrackedVlcInstance(proc);
|
||||
@@ -184,6 +217,35 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
});
|
||||
|
||||
describe('process exit handling', () => {
|
||||
it('waits for a detached VLC process to exit before closing its session', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(proc);
|
||||
const openPromise = openVlcPlayer({ title: 'S', url: streamUrl });
|
||||
await waitForSpawnCallCount(1);
|
||||
proc.emit('spawn');
|
||||
const session = await openPromise;
|
||||
|
||||
let closeSettled = false;
|
||||
const closePromise = externalPlayerSessions
|
||||
.closeSession(session.id)
|
||||
.then((closed) => {
|
||||
closeSettled = true;
|
||||
return closed;
|
||||
});
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
|
||||
expect(proc.kill).toHaveBeenCalledTimes(1);
|
||||
expect(closeSettled).toBe(false);
|
||||
|
||||
Object.defineProperty(proc, 'exitCode', { value: 0 });
|
||||
proc.emit('exit', 0);
|
||||
|
||||
await expect(closePromise).resolves.toMatchObject({
|
||||
id: session.id,
|
||||
status: 'closed',
|
||||
});
|
||||
});
|
||||
|
||||
it('marks the session closed on a clean exit', async () => {
|
||||
const proc = createMockChildProcess();
|
||||
spawnMock.mockReturnValueOnce(proc);
|
||||
@@ -262,9 +324,7 @@ describe('vlc-session.service process lifecycle', () => {
|
||||
});
|
||||
const proc = createMockChildProcess();
|
||||
const retryProc = createMockChildProcess();
|
||||
spawnMock
|
||||
.mockReturnValueOnce(proc)
|
||||
.mockReturnValueOnce(retryProc);
|
||||
spawnMock.mockReturnValueOnce(proc).mockReturnValueOnce(retryProc);
|
||||
|
||||
const openPromise = openVlcPlayer({ title: 'S', url: streamUrl });
|
||||
await waitForSpawnCallCount(1);
|
||||
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
sendPlayerErrorNotification,
|
||||
traceExternalPlayer,
|
||||
} from './external-player-runtime';
|
||||
import {
|
||||
terminateExternalPlayerProcess,
|
||||
waitForExternalPlayerProcessExit,
|
||||
} from './external-player-process';
|
||||
|
||||
export interface OpenVlcPlayerRequest {
|
||||
url: string;
|
||||
@@ -362,6 +366,7 @@ export async function openVlcPlayer({
|
||||
traceExternalPlayer('reuse existing vlc instance', {
|
||||
rcPort: vlcRcPort,
|
||||
});
|
||||
const reusedProcess = vlcProcess;
|
||||
try {
|
||||
const enqueueCommands = buildVlcEnqueueCommands({
|
||||
url,
|
||||
@@ -379,12 +384,12 @@ export async function openVlcPlayer({
|
||||
let lastReusedSnapshot: ExternalPlaybackSnapshot | null = null;
|
||||
externalPlayerSessions.attachCloser(session.id, async () => {
|
||||
try {
|
||||
await sendVlcRcCommand(reusedRcPort, 'stop');
|
||||
await sendVlcRcCommand(reusedRcPort, 'quit');
|
||||
} catch {
|
||||
if (vlcProcess && !vlcProcess.killed) {
|
||||
vlcProcess.kill();
|
||||
}
|
||||
await terminateExternalPlayerProcess(reusedProcess);
|
||||
return;
|
||||
}
|
||||
await waitForExternalPlayerProcessExit(reusedProcess);
|
||||
});
|
||||
|
||||
if (contentInfo) {
|
||||
@@ -573,9 +578,7 @@ export async function openVlcPlayer({
|
||||
|
||||
externalPlayerSessions.attachCloser(session.id, async () => {
|
||||
await flushVlcPlaybackPosition();
|
||||
if (!proc.killed) {
|
||||
proc.kill();
|
||||
}
|
||||
await terminateExternalPlayerProcess(proc);
|
||||
});
|
||||
|
||||
if (!isRetry && rcPort > 0 && contentInfo) {
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "جميع القنوات",
|
||||
"RAIL_GROUPS": "المجموعات",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "جميع القنوات",
|
||||
"RAIL_GROUPS": "المجموعات",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Усе каналы",
|
||||
"RAIL_GROUPS": "Групы",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Player wird geöffnet…",
|
||||
"EXTERNAL_STARTED": "Player gestartet",
|
||||
"EXTERNAL_PLAYING": "Wiedergabe läuft",
|
||||
"EXTERNAL_FAILED": "Player konnte nicht gestartet werden",
|
||||
"EXTERNAL_FAILED": "Fehler im externen Player",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Player wird geöffnet…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player gestartet",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Wiedergabe läuft",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Wiedergabe fehlgeschlagen",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Fehler im externen Player",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Geschlossen",
|
||||
"RAIL_ALL_CHANNELS": "Alle Sender",
|
||||
"RAIL_GROUPS": "Gruppen",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Όλα τα κανάλια",
|
||||
"RAIL_GROUPS": "Ομάδες",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy stream URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "All channels",
|
||||
"RAIL_GROUPS": "Groups",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Todos los canales",
|
||||
"RAIL_GROUPS": "Grupos",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Toutes les chaînes",
|
||||
"RAIL_GROUPS": "Groupes",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "beállított lejátszó",
|
||||
"ACTION_OPEN_VLC_HINT": "beállított lejátszó",
|
||||
"ACTION_COPY_URL": "Adatfolyam URL-címének másolása",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Összes csatorna",
|
||||
"RAIL_GROUPS": "Csoportok",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Tutti i canali",
|
||||
"RAIL_GROUPS": "Gruppi",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "すべてのチャンネル",
|
||||
"RAIL_GROUPS": "グループ",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "모든 채널",
|
||||
"RAIL_GROUPS": "그룹",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Alle kanalen",
|
||||
"RAIL_GROUPS": "Groepen",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Wszystkie kanały",
|
||||
"RAIL_GROUPS": "Grupy",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Todos os canais",
|
||||
"RAIL_GROUPS": "Grupos",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Плеер открывается…",
|
||||
"EXTERNAL_STARTED": "Плеер запущен",
|
||||
"EXTERNAL_PLAYING": "Воспроизведение",
|
||||
"EXTERNAL_FAILED": "Не удалось запустить плеер",
|
||||
"EXTERNAL_FAILED": "Ошибка внешнего плеера",
|
||||
"ACTION_OPEN_MPV_HINT": "настроенный плеер",
|
||||
"ACTION_OPEN_VLC_HINT": "настроенный плеер",
|
||||
"ACTION_COPY_URL": "Скопировать URL потока",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Плеер открывается…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Плеер запущен",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Воспроизведение",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Не удалось запустить плеер",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Ошибка внешнего плеера",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Закрыт",
|
||||
"RAIL_ALL_CHANNELS": "Все каналы",
|
||||
"RAIL_GROUPS": "Группы",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "Tüm kanallar",
|
||||
"RAIL_GROUPS": "Gruplar",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "所有频道",
|
||||
"RAIL_GROUPS": "分组",
|
||||
|
||||
@@ -592,7 +592,7 @@
|
||||
"EXTERNAL_OPENING": "Opening player…",
|
||||
"EXTERNAL_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYING": "Playing",
|
||||
"EXTERNAL_FAILED": "Player launch failed",
|
||||
"EXTERNAL_FAILED": "External player error",
|
||||
"ACTION_OPEN_MPV_HINT": "configured player",
|
||||
"ACTION_OPEN_VLC_HINT": "configured player",
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
@@ -1312,7 +1312,7 @@
|
||||
"EXTERNAL_PLAYBACK_OPENING": "Opening player…",
|
||||
"EXTERNAL_PLAYBACK_STARTED": "Player started",
|
||||
"EXTERNAL_PLAYBACK_PLAYING": "Playing",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "Playback failed",
|
||||
"EXTERNAL_PLAYBACK_FAILED": "External player error",
|
||||
"EXTERNAL_PLAYBACK_CLOSED": "Closed",
|
||||
"RAIL_ALL_CHANNELS": "所有頻道",
|
||||
"RAIL_GROUPS": "群組",
|
||||
|
||||
@@ -661,11 +661,16 @@ target becomes an explicit reopen action and a failed target becomes Try again.
|
||||
Only an exact correlated Electron `playing` session update earns the Playing
|
||||
label. A single external launch handshake owns the session: duplicate actions
|
||||
are ignored, other external actions wait, and an existing live external session
|
||||
must close before a different player can start.
|
||||
must close before a different player can start. A replacement does not launch
|
||||
until teardown of the exact spawned process is confirmed. If the local
|
||||
handshake timeout fires after Electron has supplied an exact session ID, the ID
|
||||
stays correlated so a later `opened`, `playing`, or `error` update can reconcile
|
||||
the action with the global dock.
|
||||
|
||||
The global external-playback dock uses the same Electron session status. It
|
||||
shows Opening player with progress during launch, Player started for `opened`,
|
||||
and Playing only for `playing`. Launch errors remain visible until dismissed;
|
||||
and Playing only for `playing`. External player errors remain visible until
|
||||
dismissed, whether startup failed or a started player exited unexpectedly;
|
||||
the dock deliberately has no retry because it does not own the original headers
|
||||
or credentials required to reconstruct a safe launch request.
|
||||
|
||||
|
||||
+26
-33
@@ -9,27 +9,27 @@
|
||||
gap: 14px;
|
||||
min-height: 72px;
|
||||
padding: 2px 14px;
|
||||
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 60%, transparent);
|
||||
border: 1px solid var(--app-widget-header-border, rgba(0, 0, 0, 0.08));
|
||||
border-radius: 15px 15px 0 0;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--mat-sys-surface-container-high) 88%, white 12%),
|
||||
color-mix(in srgb, var(--mat-sys-surface-container) 92%, transparent)
|
||||
);
|
||||
box-shadow:
|
||||
0 18px 42px rgba(15, 23, 42, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--app-widget-bg, #fff) 88%, white 12%),
|
||||
var(--app-widget-header-bg, #fafbfc)
|
||||
);
|
||||
box-shadow: var(--app-widget-shadow, 0 18px 42px rgba(15, 23, 42, 0.12));
|
||||
}
|
||||
|
||||
.external-playback-dock--error {
|
||||
border-color: color-mix(in srgb, var(--mat-sys-error) 50%, transparent);
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--mat-sys-error-container) 72%, transparent),
|
||||
color-mix(in srgb, var(--mat-sys-surface-container) 90%, transparent)
|
||||
);
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--app-expiry-expired-text, #b3261e) 50%,
|
||||
transparent
|
||||
);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--app-expiry-expired-surface, rgba(244, 67, 54, 0.14)),
|
||||
var(--app-widget-bg, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
.external-playback-dock--compact {
|
||||
@@ -37,9 +37,7 @@
|
||||
padding: 8px 10px;
|
||||
gap: 10px;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 12px 28px rgba(15, 23, 42, 0.16),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||||
box-shadow: var(--app-widget-shadow, 0 12px 28px rgba(15, 23, 42, 0.16));
|
||||
|
||||
.external-playback-dock__artwork {
|
||||
width: 38px;
|
||||
@@ -63,7 +61,7 @@
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: color-mix(in srgb, var(--mat-sys-surface-container-highest) 92%, transparent);
|
||||
background: var(--app-widget-header-bg, #fafbfc);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
@@ -93,7 +91,7 @@
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--mat-sys-primary);
|
||||
outline: 2px solid var(--app-selection-color, #2f7bff);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -102,7 +100,7 @@
|
||||
.external-playback-dock__placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
color: var(--app-body-color, #4a5060);
|
||||
}
|
||||
|
||||
.external-playback-dock__meta {
|
||||
@@ -114,14 +112,9 @@
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--mat-sys-surface-container-highest) 80%,
|
||||
transparent
|
||||
);
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
border: 1px solid
|
||||
color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent);
|
||||
background: var(--app-card-hover-bg, #f0f2f5);
|
||||
color: var(--app-body-color, #4a5060);
|
||||
border: 1px solid var(--app-widget-header-border, rgba(0, 0, 0, 0.08));
|
||||
font-size: 0.66rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
@@ -133,7 +126,7 @@
|
||||
white-space: nowrap;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--mat-sys-on-surface);
|
||||
color: var(--app-heading-color, #1a1d23);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
@@ -141,7 +134,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
color: var(--app-body-color, #4a5060);
|
||||
font-size: 0.78rem;
|
||||
min-width: 0;
|
||||
|
||||
|
||||
+4
-2
@@ -21,7 +21,7 @@ class FakeTranslateLoader implements TranslateLoader {
|
||||
EXTERNAL_PLAYBACK_OPENING: 'Opening player…',
|
||||
EXTERNAL_PLAYBACK_STARTED: 'Player started',
|
||||
EXTERNAL_PLAYBACK_PLAYING: 'Playing',
|
||||
EXTERNAL_PLAYBACK_FAILED: 'Playback failed',
|
||||
EXTERNAL_PLAYBACK_FAILED: 'External player error',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -140,7 +140,9 @@ describe('ExternalPlaybackDockComponent', () => {
|
||||
const action = fixture.debugElement.query(
|
||||
By.css('.external-playback-dock__button')
|
||||
);
|
||||
expect(fixture.nativeElement.textContent).toContain('Playback failed');
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'External player error'
|
||||
);
|
||||
expect(action.nativeElement.textContent).toContain('Dismiss');
|
||||
expect(action.nativeElement.textContent).not.toContain('Retry');
|
||||
expect(action.nativeElement.getAttribute('aria-label')).toBe('Dismiss');
|
||||
|
||||
@@ -134,6 +134,30 @@ describe('ExternalPlaybackRecovery', () => {
|
||||
recovery.destroy();
|
||||
});
|
||||
|
||||
it('keeps an exact launching session correlated after the local timeout', () => {
|
||||
const recovery = new ExternalPlaybackRecovery();
|
||||
recovery.syncSession('content-a');
|
||||
recovery.begin('mpv', null);
|
||||
recovery.observe(
|
||||
session({ id: 'slow-mpv', player: 'mpv', status: 'launching' })
|
||||
);
|
||||
|
||||
jest.advanceTimersByTime(EXTERNAL_RECOVERY_LAUNCH_TIMEOUT_MS);
|
||||
|
||||
expect(recovery.target('mpv')).toEqual({
|
||||
attempts: 1,
|
||||
sessionId: 'slow-mpv',
|
||||
status: 'error',
|
||||
});
|
||||
expect(
|
||||
recovery.observe(
|
||||
session({ id: 'slow-mpv', player: 'mpv', status: 'playing' })
|
||||
)
|
||||
).toBe(true);
|
||||
expect(recovery.target('mpv').status).toBe('playing');
|
||||
recovery.destroy();
|
||||
});
|
||||
|
||||
it('invalidates an old intent and timer when the content session changes', () => {
|
||||
const recovery = new ExternalPlaybackRecovery();
|
||||
recovery.syncSession('content-a');
|
||||
|
||||
@@ -138,7 +138,7 @@ export class ExternalPlaybackRecovery {
|
||||
const current = this.target(intent.target);
|
||||
this.setTarget(intent.target, {
|
||||
attempts: current.attempts,
|
||||
sessionId: null,
|
||||
sessionId: current.sessionId,
|
||||
status: 'error',
|
||||
});
|
||||
this.clearActiveIntent();
|
||||
|
||||
Reference in new issue
Block a user