diff --git a/.changes/playback-external-launch-feedback.md b/.changes/playback-external-launch-feedback.md new file mode 100644 index 000000000..b25648a9e --- /dev/null +++ b/.changes/playback-external-launch-feedback.md @@ -0,0 +1,6 @@ +--- +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. diff --git a/AGENTS.md b/AGENTS.md index 9b0084ee3..e541a15a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -288,7 +288,13 @@ Key files: exception. PWA capability suppresses managed MPV/VLC, and ClearKey/KODIPROP DRM suppresses external targets because its payload is not transferable. Raw engine messages, arbitrary data, and credentials never enter recommendation - evidence or ownership state. + 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. - 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 diff --git a/CLAUDE.md b/CLAUDE.md index 62c2eb44f..0e3548911 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -824,7 +824,13 @@ app as a real argument, so it is not an option. exception. PWA capability suppresses managed MPV/VLC, and ClearKey/KODIPROP DRM suppresses external targets because its payload is not transferable. Raw engine messages, arbitrary data, and credentials never enter recommendation - evidence or ownership state. + 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. - 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.*` diff --git a/apps/electron-backend-e2e/src/dash-clearkey.e2e.ts b/apps/electron-backend-e2e/src/dash-clearkey.e2e.ts index c3ba12807..ab71c2132 100644 --- a/apps/electron-backend-e2e/src/dash-clearkey.e2e.ts +++ b/apps/electron-backend-e2e/src/dash-clearkey.e2e.ts @@ -3,6 +3,7 @@ import { readFileSync } from 'fs'; import { basename, join } from 'path'; import type { ExternalPlayerName, + ExternalPlayerSession, PlayerContentInfo, } from '@iptvnator/shared/interfaces'; import { @@ -189,6 +190,7 @@ type CapturedExternalPlayerLaunch = { }; type PlaybackRecommendationCaptureSnapshot = { + closed: string[]; completed: number; launches: CapturedExternalPlayerLaunch[]; released: boolean; @@ -197,6 +199,7 @@ type PlaybackRecommendationCaptureSnapshot = { type PlaybackRecommendationCaptureState = PlaybackRecommendationCaptureSnapshot & { releaseFirstResponse: (() => void) | undefined; + sessions: Record; waitForFirstResponse: Promise; }; @@ -209,10 +212,12 @@ async function installPlaybackRecommendationLaunchCapture( releaseFirstResponse = resolve; }); const state: PlaybackRecommendationCaptureState = { + closed: [], completed: 0, launches: [], released: false, releaseFirstResponse, + sessions: {}, waitForFirstResponse, }; const globalRef = globalThis as typeof globalThis & { @@ -223,7 +228,7 @@ async function installPlaybackRecommendationLaunchCapture( const captureLaunch = (player: ExternalPlayerName) => async ( - _event: unknown, + rawEvent: unknown, url: string, title: string, thumbnail?: string, @@ -248,30 +253,81 @@ async function installPlaybackRecommendationLaunchCapture( ], player, }); + const now = new Date().toISOString(); + const id = `e2e-recommended-${player}-${invocation}`; + const event = rawEvent as { + sender: { + send: ( + channel: string, + session: ExternalPlayerSession + ) => void; + }; + }; + const launching: ExternalPlayerSession = { + canClose: true, + id, + player, + startedAt: now, + status: 'launching', + streamUrl: url, + thumbnail: thumbnail ?? null, + title, + updatedAt: now, + }; + state.sessions[id] = launching; + event.sender.send('EXTERNAL_PLAYER_SESSION_UPDATE', launching); if (invocation === 1 && !state.released) { await state.waitForFirstResponse; } state.completed += 1; - const now = new Date().toISOString(); - return { - canClose: false, - id: `e2e-recommended-${player}-${invocation}`, + const completed: ExternalPlayerSession = { + ...launching, + canClose: player === 'mpv', + error: + player === 'vlc' + ? 'E2E player launch failed' + : undefined, player, - startedAt: now, - status: 'opened', - streamUrl: url, - thumbnail: thumbnail ?? null, - title, - updatedAt: now, + status: player === 'mpv' ? 'opened' : 'error', + updatedAt: new Date().toISOString(), }; + state.sessions[id] = completed; + event.sender.send('EXTERNAL_PLAYER_SESSION_UPDATE', completed); + return completed; }; ipcMain.removeHandler('OPEN_MPV_PLAYER'); ipcMain.removeHandler('OPEN_VLC_PLAYER'); ipcMain.handle('OPEN_MPV_PLAYER', captureLaunch('mpv')); ipcMain.handle('OPEN_VLC_PLAYER', captureLaunch('vlc')); + ipcMain.removeHandler('CLOSE_EXTERNAL_PLAYER_SESSION'); + ipcMain.handle( + 'CLOSE_EXTERNAL_PLAYER_SESSION', + (rawEvent: unknown, sessionId: string) => { + const session = state.sessions[sessionId]; + if (!session) return null; + const closed: ExternalPlayerSession = { + ...session, + canClose: false, + status: 'closed', + updatedAt: new Date().toISOString(), + }; + state.closed.push(sessionId); + state.sessions[sessionId] = closed; + const event = rawEvent as { + sender: { + send: ( + channel: string, + update: ExternalPlayerSession + ) => void; + }; + }; + event.sender.send('EXTERNAL_PLAYER_SESSION_UPDATE', closed); + return closed; + } + ); }); } @@ -289,6 +345,7 @@ async function getPlaybackRecommendationCapture( ); } return { + closed: capture.closed, completed: capture.completed, launches: capture.launches, released: capture.released, @@ -313,7 +370,7 @@ async function releasePlaybackRecommendationCapture( }); } -test('@electron @dash ClearKey DASH filters DRM fallback and eligible MKV opens in MPV', async ({ +test('@electron @dash ClearKey DASH filters DRM fallback and reports external launch states', async ({ dataDir, }) => { const fixtureServer = await startDashFixtureServer(); @@ -439,6 +496,7 @@ test('@electron @dash ClearKey DASH filters DRM fallback and eligible MKV opens }, ] satisfies CapturedExternalPlayerLaunch[]; expect(await getPlaybackRecommendationCapture(app)).toEqual({ + closed: [], completed: 0, launches: [], released: false, @@ -450,18 +508,33 @@ test('@electron @dash ClearKey DASH filters DRM fallback and eligible MKV opens timeout: 10_000, }) .toEqual({ + closed: [], completed: 0, launches: expectedLaunches, released: false, }); - expect( - (await getPlaybackRecommendationCapture(app)).launches.filter( - ({ player }) => player === 'vlc' - ) - ).toHaveLength(0); - await expect(mpvFallback).toHaveCount(0); - await expect(vlcFallback).toHaveClass( - /web-player-diagnostic__player-card--primary/ + await expect(mpvFallback).toBeVisible(); + await expect(vlcFallback).toBeVisible(); + await expect(mpvFallback).toContainText('Opening MPV'); + await expect(mpvFallback).toContainText('Opening player'); + await expect(mpvFallback).toHaveAttribute('aria-busy', 'true'); + await expect(vlcFallback).toHaveAttribute('aria-disabled', 'true'); + await vlcFallback.evaluate((button: HTMLButtonElement) => + button.click() + ); + await expect + .poll(() => getPlaybackRecommendationCapture(app)) + .toEqual({ + closed: [], + completed: 0, + launches: expectedLaunches, + released: false, + }); + const dock = app.mainWindow.locator('app-external-playback-dock'); + await expect(dock).toContainText('Opening player'); + await expect(dock.locator('.external-playback-dock')).toHaveAttribute( + 'aria-busy', + 'true' ); await releasePlaybackRecommendationCapture(app); @@ -470,6 +543,7 @@ test('@electron @dash ClearKey DASH filters DRM fallback and eligible MKV opens timeout: 10_000, }) .toEqual({ + closed: [], completed: 1, launches: expectedLaunches, released: true, @@ -483,14 +557,47 @@ test('@electron @dash ClearKey DASH filters DRM fallback and eligible MKV opens }) ); expect(await getPlaybackRecommendationCapture(app)).toEqual({ + closed: [], completed: 1, launches: expectedLaunches, released: true, }); - await expect(mpvFallback).toHaveCount(0); + await expect(mpvFallback).toBeVisible(); + await expect(mpvFallback).toContainText('Open MPV again'); + await expect(mpvFallback).toContainText('Player started'); await expect(vlcFallback).toHaveClass( /web-player-diagnostic__player-card--primary/ ); + await expect(dock).toContainText('Player started'); + + await vlcFallback.click(); + const expectedBothLaunches = [ + ...expectedLaunches, + { ...expectedLaunches[0], player: 'vlc' as const }, + ]; + await expect + .poll(() => getPlaybackRecommendationCapture(app), { + timeout: 10_000, + }) + .toEqual({ + closed: ['e2e-recommended-mpv-1'], + completed: 2, + launches: expectedBothLaunches, + released: true, + }); + await expect(mpvFallback).toBeVisible(); + await expect(vlcFallback).toBeVisible(); + await expect(vlcFallback).toContainText('Try VLC again'); + await expect(vlcFallback).toContainText('Player launch failed'); + await expect(mpvFallback).toHaveClass( + /web-player-diagnostic__player-card--primary/ + ); + await expect(dock).toContainText('E2E player launch failed'); + await expect( + dock.getByRole('button', { name: 'Dismiss' }) + ).toBeVisible(); + await dock.getByRole('button', { name: 'Dismiss' }).click(); + await expect(dock).toBeHidden(); } finally { await releasePlaybackRecommendationCapture(app).catch(() => undefined); await closeElectronApp(app); diff --git a/apps/web/src/app/services/external-playback.service.spec.ts b/apps/web/src/app/services/external-playback.service.spec.ts index f434c79bc..09e7624de 100644 --- a/apps/web/src/app/services/external-playback.service.spec.ts +++ b/apps/web/src/app/services/external-playback.service.spec.ts @@ -2,9 +2,7 @@ import { ExternalPlayerSession } from '@iptvnator/shared/interfaces'; import { ExternalPlaybackService } from './external-playback.service'; describe('ExternalPlaybackService', () => { - let listener: - | ((session: ExternalPlayerSession) => void) - | undefined; + let listener: ((session: ExternalPlayerSession) => void) | undefined; let closeExternalPlayerSession: jest.Mock; let service: ExternalPlaybackService; @@ -94,7 +92,9 @@ describe('ExternalPlaybackService', () => { contentType: 'vod', }) ).toBeNull(); - expect(service.visibleSession()).toBeNull(); + expect(service.visibleSession()).toEqual( + expect.objectContaining({ status: 'error' }) + ); }); it('delegates close requests for closable sessions', async () => { @@ -133,18 +133,22 @@ describe('ExternalPlaybackService', () => { expect(closeExternalPlayerSession).toHaveBeenCalledWith('session-2'); }); - it('hides terminal sessions from the dock', () => { + it('hides closed sessions but keeps errors visible until dismissed', () => { listener?.(createSession({ status: 'closed', canClose: false })); expect(service.visibleSession()).toBeNull(); - listener?.( - createSession({ - id: 'session-3', - status: 'error', - error: 'Launch failed', - canClose: false, - }) - ); + const failed = createSession({ + id: 'session-3', + status: 'error', + error: 'Launch failed', + canClose: false, + }); + listener?.(createSession({ id: failed.id })); + listener?.(failed); + + expect(service.visibleSession()).toEqual(failed); + + service.dismissActiveSession(); expect(service.visibleSession()).toBeNull(); }); }); diff --git a/apps/web/src/app/services/external-playback.service.ts b/apps/web/src/app/services/external-playback.service.ts index 100476c7d..2b6f07d58 100644 --- a/apps/web/src/app/services/external-playback.service.ts +++ b/apps/web/src/app/services/external-playback.service.ts @@ -1,5 +1,8 @@ import { computed, Injectable, signal } from '@angular/core'; -import { ExternalPlayerSession, PlayerContentInfo } from '@iptvnator/shared/interfaces'; +import { + ExternalPlayerSession, + PlayerContentInfo, +} from '@iptvnator/shared/interfaces'; @Injectable({ providedIn: 'root', @@ -10,11 +13,7 @@ export class ExternalPlaybackService { readonly visibleSession = computed(() => { const session = this.activeSession(); - if ( - !session || - session.status === 'closed' || - session.status === 'error' - ) { + if (!session || session.status === 'closed') { return null; } @@ -60,9 +59,8 @@ export class ExternalPlaybackService { this.dismissedSessionId.set(session.id); try { - const updatedSession = await window.electron.closeExternalPlayerSession( - session.id - ); + const updatedSession = + await window.electron.closeExternalPlayerSession(session.id); if (updatedSession) { this.handleSessionUpdate(updatedSession); return; @@ -106,7 +104,11 @@ export class ExternalPlaybackService { private handleSessionUpdate(session: ExternalPlayerSession): void { const current = this.activeSession(); - if (!current || current.id === session.id || session.status === 'launching') { + if ( + !current || + current.id === session.id || + session.status === 'launching' + ) { this.activeSession.set(session); } diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 6985c5e0b..db3b1494e 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "هذا الهدف متوافق مع بيانات التشفير المتاحة", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "المفضلة", "RAIL_DOWNLOADS": "التنزيلات", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "جميع القنوات", "RAIL_GROUPS": "المجموعات", "RAIL_CONTEXT_ACTIONS": "إجراءات القسم", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index a49b3ec9d..264a2a73c 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "هاد الهدف متوافق مع بيانات التشفير المتوفرة", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "المفضلة", "RAIL_DOWNLOADS": "التحميلات", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "جميع القنوات", "RAIL_GROUPS": "المجموعات", "RAIL_CONTEXT_ACTIONS": "إجراءات القسم", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index e1b95deaa..61a76161b 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Гэтая мэта сумяшчальная з даступнымі данымі шыфравання", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Выбранае", "RAIL_DOWNLOADS": "Спампаванні", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Усе каналы", "RAIL_GROUPS": "Групы", "RAIL_CONTEXT_ACTIONS": "Дзеянні раздзела", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index fedf12e47..929bd5e2f 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Dieses Ziel ist mit den verfügbaren Verschlüsselungsdaten kompatibel", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "MPV wird geöffnet…", + "ACTION_OPENING_VLC": "VLC wird geöffnet…", + "ACTION_REOPEN_MPV": "MPV erneut öffnen", + "ACTION_REOPEN_VLC": "VLC erneut öffnen", + "ACTION_RETRY_MPV": "MPV erneut versuchen", + "ACTION_RETRY_VLC": "VLC erneut versuchen", + "EXTERNAL_OPENING": "Player wird geöffnet…", + "EXTERNAL_STARTED": "Player gestartet", + "EXTERNAL_PLAYING": "Wiedergabe läuft", + "EXTERNAL_FAILED": "Player konnte nicht gestartet werden", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favoriten", "RAIL_DOWNLOADS": "Downloads", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Ausblenden", + "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_CLOSED": "Geschlossen", "RAIL_ALL_CHANNELS": "Alle Sender", "RAIL_GROUPS": "Gruppen", "RAIL_CONTEXT_ACTIONS": "Bereichsaktionen", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 04f06a43d..b6a864aba 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Αυτός ο στόχος είναι συμβατός με τα διαθέσιμα δεδομένα κρυπτογράφησης", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Αγαπημένα", "RAIL_DOWNLOADS": "Λήψεις", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Όλα τα κανάλια", "RAIL_GROUPS": "Ομάδες", "RAIL_CONTEXT_ACTIONS": "Ενέργειες ενότητας", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index c8f540f98..edb0867a8 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "This target is compatible with the available encryption data", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy stream URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favorites", "RAIL_DOWNLOADS": "Downloads", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "All channels", "RAIL_GROUPS": "Groups", "RAIL_CONTEXT_ACTIONS": "Section actions", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 4ab2372df..f73d1607e 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Este destino es compatible con los datos de cifrado disponibles", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favoritos", "RAIL_DOWNLOADS": "Descargas", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Todos los canales", "RAIL_GROUPS": "Grupos", "RAIL_CONTEXT_ACTIONS": "Acciones de la sección", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 0625168e5..6939bbac0 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Cette cible est compatible avec les données de chiffrement disponibles", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favoris", "RAIL_DOWNLOADS": "Téléchargements", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Toutes les chaînes", "RAIL_GROUPS": "Groupes", "RAIL_CONTEXT_ACTIONS": "Actions de section", diff --git a/apps/web/src/assets/i18n/hu.json b/apps/web/src/assets/i18n/hu.json index ba496ef2a..2d11bd679 100644 --- a/apps/web/src/assets/i18n/hu.json +++ b/apps/web/src/assets/i18n/hu.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Ez a cél kompatibilis az elérhető titkosítási adatokkal", "ACTION_OPEN_MPV": "Megnyitás MPV-ben", "ACTION_OPEN_VLC": "Megnyitás VLC-ben", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "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", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Kedvencek", "RAIL_DOWNLOADS": "Letöltések", "EXTERNAL_PLAYBACK_CLOSE": "Lejátszó bezárása", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Összes csatorna", "RAIL_GROUPS": "Csoportok", "RAIL_CONTEXT_ACTIONS": "A szakasz műveletei", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index f9b5739f4..82a1c95df 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Questa destinazione è compatibile con i dati di crittografia disponibili", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Preferiti", "RAIL_DOWNLOADS": "Download", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Tutti i canali", "RAIL_GROUPS": "Gruppi", "RAIL_CONTEXT_ACTIONS": "Azioni di sezione", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 5538244c3..663419b10 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "この対象は利用可能な暗号化データに対応しています", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "お気に入り", "RAIL_DOWNLOADS": "ダウンロード", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "すべてのチャンネル", "RAIL_GROUPS": "グループ", "RAIL_CONTEXT_ACTIONS": "セクションのアクション", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 747f036cd..ca62ec3c7 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "이 대상은 사용 가능한 암호화 데이터와 호환됩니다", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "즐겨찾기", "RAIL_DOWNLOADS": "다운로드", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "모든 채널", "RAIL_GROUPS": "그룹", "RAIL_CONTEXT_ACTIONS": "섹션 작업", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index e1c7b1287..5e8224146 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Dit doel is compatibel met de beschikbare versleutelingsgegevens", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favorieten", "RAIL_DOWNLOADS": "Downloads", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Alle kanalen", "RAIL_GROUPS": "Groepen", "RAIL_CONTEXT_ACTIONS": "Sectieacties", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index dcbf370a8..ed9ffe26b 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Ten cel jest zgodny z dostępnymi danymi szyfrowania", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Ulubione", "RAIL_DOWNLOADS": "Pobrane", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Wszystkie kanały", "RAIL_GROUPS": "Grupy", "RAIL_CONTEXT_ACTIONS": "Akcje sekcji", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index e2ca58bfd..0b83dea32 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Este destino é compatível com os dados de criptografia disponíveis", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favoritos", "RAIL_DOWNLOADS": "Downloads", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Todos os canais", "RAIL_GROUPS": "Grupos", "RAIL_CONTEXT_ACTIONS": "Ações da seção", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 569d5657c..bcf5d32dd 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Эта цель совместима с доступными данными шифрования", "ACTION_OPEN_MPV": "Открыть в MPV", "ACTION_OPEN_VLC": "Открыть в VLC", + "ACTION_OPENING_MPV": "MPV открывается…", + "ACTION_OPENING_VLC": "VLC открывается…", + "ACTION_REOPEN_MPV": "Открыть MPV снова", + "ACTION_REOPEN_VLC": "Открыть VLC снова", + "ACTION_RETRY_MPV": "Повторить запуск MPV", + "ACTION_RETRY_VLC": "Повторить запуск VLC", + "EXTERNAL_OPENING": "Плеер открывается…", + "EXTERNAL_STARTED": "Плеер запущен", + "EXTERNAL_PLAYING": "Воспроизведение", + "EXTERNAL_FAILED": "Не удалось запустить плеер", "ACTION_OPEN_MPV_HINT": "настроенный плеер", "ACTION_OPEN_VLC_HINT": "настроенный плеер", "ACTION_COPY_URL": "Скопировать URL потока", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Избранное", "RAIL_DOWNLOADS": "Загрузки", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Скрыть", + "EXTERNAL_PLAYBACK_OPENING": "Плеер открывается…", + "EXTERNAL_PLAYBACK_STARTED": "Плеер запущен", + "EXTERNAL_PLAYBACK_PLAYING": "Воспроизведение", + "EXTERNAL_PLAYBACK_FAILED": "Не удалось запустить плеер", + "EXTERNAL_PLAYBACK_CLOSED": "Закрыт", "RAIL_ALL_CHANNELS": "Все каналы", "RAIL_GROUPS": "Группы", "RAIL_CONTEXT_ACTIONS": "Действия раздела", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index bee89036e..07384c2c1 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "Bu hedef kullanılabilir şifreleme verileriyle uyumludur", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "Favoriler", "RAIL_DOWNLOADS": "İndirilenler", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "Tüm kanallar", "RAIL_GROUPS": "Gruplar", "RAIL_CONTEXT_ACTIONS": "Bölüm işlemleri", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 0829df4ac..0c1c9e6eb 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "此目标与可用的加密数据兼容", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "收藏", "RAIL_DOWNLOADS": "下载", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "所有频道", "RAIL_GROUPS": "分组", "RAIL_CONTEXT_ACTIONS": "章节操作", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 551895e4b..54998a2f0 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -583,6 +583,16 @@ "REASON_COMPATIBLE_DRM_PATH": "此目標與可用的加密資料相容", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPENING_MPV": "Opening MPV…", + "ACTION_OPENING_VLC": "Opening VLC…", + "ACTION_REOPEN_MPV": "Open MPV again", + "ACTION_REOPEN_VLC": "Open VLC again", + "ACTION_RETRY_MPV": "Try MPV again", + "ACTION_RETRY_VLC": "Try VLC again", + "EXTERNAL_OPENING": "Opening player…", + "EXTERNAL_STARTED": "Player started", + "EXTERNAL_PLAYING": "Playing", + "EXTERNAL_FAILED": "Player launch failed", "ACTION_OPEN_MPV_HINT": "configured player", "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", @@ -1298,6 +1308,12 @@ "RAIL_FAVORITES": "我的最愛", "RAIL_DOWNLOADS": "下載", "EXTERNAL_PLAYBACK_CLOSE": "Close player", + "EXTERNAL_PLAYBACK_DISMISS": "Dismiss", + "EXTERNAL_PLAYBACK_OPENING": "Opening player…", + "EXTERNAL_PLAYBACK_STARTED": "Player started", + "EXTERNAL_PLAYBACK_PLAYING": "Playing", + "EXTERNAL_PLAYBACK_FAILED": "Playback failed", + "EXTERNAL_PLAYBACK_CLOSED": "Closed", "RAIL_ALL_CHANNELS": "所有頻道", "RAIL_GROUPS": "群組", "RAIL_CONTEXT_ACTIONS": "區段操作", diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 44bd446dd..0544ae917 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -582,12 +582,13 @@ offer a same-family built-in alternative. Unknown source or engine-family facts also suppress built-in recommendations. The pure policy builds the following order, filters the current, unavailable, -incompatible, and already attempted targets, and then returns at most three -actions. It also projects attempted inline target IDs through the validated +incompatible, and already attempted inline targets, and then returns at most +three actions. It projects attempted inline target IDs through the validated canonical source/target capability matrix and filters every engine family that has already been attempted. HTML5 and ArtPlayer therefore cannot be offered as -separate hls.js recovery attempts. The first surviving action is primary and -later actions are secondary. +separate hls.js recovery attempts. External MPV/VLC attempts remain eligible; +the view reranks them by per-target attempt count instead of removing them. The +first surviving action is primary and later actions are secondary. | Sanitized evidence | Candidate order | | ----------------------------------------- | ------------------------------------------------------------ | @@ -654,8 +655,19 @@ override and saved player setting. The new engine receives the latest finite VOD position as a best-effort resume point; live playback starts at the live edge. Retry reloads the active target without clearing attempts. Selecting MPV or VLC records the external target before emitting the existing fallback -request. The system does not infer whether the external process ultimately -played the stream. +request. Both external actions remain mounted while their per-target state +moves through `launching`, `started`, `playing`, or `error`; an attempted idle +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. + +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; +the dock deliberately has no retry because it does not own the original headers +or credentials required to reconstruct a safe launch request. No recommendation mutates `Settings.player` or another persisted setting. Recovery recommendations never auto-switch a player or source and do not diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html index e9d55f521..53d171a78 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html @@ -2,6 +2,7 @@ class="external-playback-dock" [class.external-playback-dock--compact]="compact()" [class.external-playback-dock--error]="session().status === 'error'" + [attr.aria-busy]="session().status === 'launching' ? 'true' : null" > } diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts index 45a39d7a7..f4bc1ff27 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.spec.ts @@ -17,6 +17,11 @@ class FakeTranslateLoader implements TranslateLoader { WORKSPACE: { SHELL: { EXTERNAL_PLAYBACK_CLOSE: 'Close player', + EXTERNAL_PLAYBACK_DISMISS: 'Dismiss', + EXTERNAL_PLAYBACK_OPENING: 'Opening player…', + EXTERNAL_PLAYBACK_STARTED: 'Player started', + EXTERNAL_PLAYBACK_PLAYING: 'Playing', + EXTERNAL_PLAYBACK_FAILED: 'Playback failed', }, }, }); @@ -65,8 +70,18 @@ describe('ExternalPlaybackDockComponent', () => { const text = fixture.nativeElement.textContent; expect(text).toContain('Example Video'); expect(text).toContain('MPV'); - expect(text).toContain('Launching'); + expect(text).toContain('Opening player'); expect(text).toContain('Close player'); + expect( + fixture.nativeElement + .querySelector('.external-playback-dock') + .getAttribute('aria-busy') + ).toBe('true'); + expect( + fixture.nativeElement + .querySelector('.external-playback-dock__status') + .getAttribute('aria-live') + ).toBe('polite'); }); it('emits a single close action when the close button is clicked', () => { @@ -96,6 +111,44 @@ describe('ExternalPlaybackDockComponent', () => { ).toBe(true); }); + it.each([ + ['opened', 'Player started'], + ['playing', 'Playing'], + ] as const)('renders exact %s status copy', (status, expected) => { + fixture.componentRef.setInput('session', { ...session, status }); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain(expected); + expect( + fixture.nativeElement + .querySelector('.external-playback-dock') + .getAttribute('aria-busy') + ).toBeNull(); + }); + + it('keeps a failed session visible with a safe dismiss action and no retry', () => { + const dismissSpy = jest.fn(); + component.dismissClicked.subscribe(dismissSpy); + fixture.componentRef.setInput('session', { + ...session, + status: 'error', + error: '', + canClose: false, + }); + fixture.detectChanges(); + + const action = fixture.debugElement.query( + By.css('.external-playback-dock__button') + ); + expect(fixture.nativeElement.textContent).toContain('Playback failed'); + expect(action.nativeElement.textContent).toContain('Dismiss'); + expect(action.nativeElement.textContent).not.toContain('Retry'); + expect(action.nativeElement.getAttribute('aria-label')).toBe('Dismiss'); + + action.nativeElement.click(); + expect(dismissSpy).toHaveBeenCalledTimes(1); + }); + it('disables the artwork button when the session has no playlist target', () => { const artwork = fixture.debugElement.query( By.css('.external-playback-dock__artwork') diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts index 5e5713c83..acab37433 100644 --- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts +++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts @@ -35,6 +35,7 @@ export class ExternalPlaybackDockComponent { readonly compact = input(false); readonly closeClicked = output(); + readonly dismissClicked = output(); readonly artworkClicked = output(); private readonly artworkFailed = signal(false); @@ -55,25 +56,27 @@ export class ExternalPlaybackDockComponent { readonly artworkUrl = computed( () => this.session().thumbnail?.trim() ?? '' ); - readonly statusLabel = computed(() => { + readonly statusLabelKey = computed(() => { const session = this.session(); switch (session.status) { case 'launching': - return 'Launching…'; + return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_OPENING'; case 'opened': + return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_STARTED'; case 'playing': - return 'Opened'; + return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_PLAYING'; case 'error': - return session.error || 'Playback failed'; + return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_FAILED'; default: - return 'Closed'; + return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_CLOSED'; } }); readonly statusIcon = computed(() => { const status = this.session().status; if (status === 'error') return 'error_outline'; + if (status === 'playing') return 'play_circle'; return 'open_in_new'; }); @@ -101,8 +104,16 @@ export class ExternalPlaybackDockComponent { readonly artworkInteractive = computed( () => !!this.session().contentInfo?.playlistId ); - readonly showCloseButton = computed( - () => this.session().canClose && this.session().status !== 'error' + readonly showActionButton = computed( + () => this.session().status === 'error' || this.session().canClose + ); + readonly actionLabelKey = computed(() => + this.session().status === 'error' + ? 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_DISMISS' + : 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_CLOSE' + ); + readonly actionIcon = computed(() => + this.session().status === 'error' ? 'close' : 'stop_circle' ); constructor() { @@ -120,4 +131,12 @@ export class ExternalPlaybackDockComponent { if (!this.artworkInteractive()) return; this.artworkClicked.emit(); } + + onActionClick(): void { + if (this.session().status === 'error') { + this.dismissClicked.emit(); + return; + } + this.closeClicked.emit(); + } } diff --git a/libs/ui/playback/src/lib/playback-diagnostic-panel/playback-diagnostic-panel.component.html b/libs/ui/playback/src/lib/playback-diagnostic-panel/playback-diagnostic-panel.component.html index a3930a3c8..012bae4e5 100644 --- a/libs/ui/playback/src/lib/playback-diagnostic-panel/playback-diagnostic-panel.component.html +++ b/libs/ui/playback/src/lib/playback-diagnostic-panel/playback-diagnostic-panel.component.html @@ -101,12 +101,30 @@ [attr.data-test-id]=" getRecommendationTestId(recommendation) " - [disabled]="pending()" + [class.web-player-diagnostic__player-card--disabled]=" + pending() + " + [disabled]=" + pending() && + !isExternalPlayerRecommendation(recommendation) + " + [attr.aria-disabled]="pending() ? 'true' : null" + [attr.aria-busy]=" + isExternalLaunching(recommendation) ? 'true' : null + " (click)="activate(recommendation)" > - + @if (isExternalLaunching(recommendation)) { +