From d7f914bc57b7b6e93a55a4c4c0ea3e549c671fcf Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 18 May 2026 22:02:58 +0200 Subject: [PATCH] fix(playback): improve inline stream diagnostics --- apps/electron-backend/project.json | 4 + apps/web/src/assets/i18n/ar.json | 5 + apps/web/src/assets/i18n/ary.json | 5 + apps/web/src/assets/i18n/by.json | 5 + apps/web/src/assets/i18n/de.json | 5 + apps/web/src/assets/i18n/el.json | 5 + apps/web/src/assets/i18n/en.json | 7 +- apps/web/src/assets/i18n/es.json | 5 + apps/web/src/assets/i18n/fr.json | 5 + apps/web/src/assets/i18n/it.json | 5 + apps/web/src/assets/i18n/ja.json | 5 + apps/web/src/assets/i18n/ko.json | 5 + apps/web/src/assets/i18n/nl.json | 5 + apps/web/src/assets/i18n/pl.json | 5 + apps/web/src/assets/i18n/pt.json | 5 + apps/web/src/assets/i18n/ru.json | 5 + apps/web/src/assets/i18n/tr.json | 5 + apps/web/src/assets/i18n/zh.json | 5 + apps/web/src/assets/i18n/zhtw.json | 5 + docs/architecture/embedded-inline-playback.md | 6 +- .../unified-live-tab.component.html | 43 +- .../unified-live-tab.component.spec.ts | 153 ++++-- .../unified-live-tab.component.ts | 63 +-- .../web-player-view.component.html | 170 ++++--- .../web-player-view.component.scss | 447 ++++++++++++++---- .../web-player-view.component.spec.ts | 64 +++ .../web-player-view.component.ts | 14 + libs/ui/styles/_portal-layout.scss | 8 +- 28 files changed, 772 insertions(+), 292 deletions(-) rename libs/portal/shared/{util/src/lib/collection => ui/src/lib/components/unified-collection}/unified-live-tab.component.spec.ts (88%) diff --git a/apps/electron-backend/project.json b/apps/electron-backend/project.json index a1c984353..ec1cff48d 100644 --- a/apps/electron-backend/project.json +++ b/apps/electron-backend/project.json @@ -118,6 +118,10 @@ } }, "serve-electron": { + "dependsOn": [ + "electron-backend:build-worker", + "electron-backend:build-embedded-mpv" + ], "executor": "nx-electron:execute", "options": { "buildTarget": "electron-backend:build", diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 9cb061cf2..8a658b05c 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index b7f10113e..3faa6a9ca 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index e2e7f7b5f..a5d4b725e 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index a49f9857c..d6f72b073 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index d4aecf808..4e0769554 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index ac94947de..ee8a4a470 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", + "ACTION_COPY_URL": "Copy stream URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 4954162bf..29b095996 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 55735bbf5..42d3ad995 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 623961154..3d9b4fc9d 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index dfc276188..d8c77a915 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 29cfe4746..635855e55 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 8522610f7..40c46f5d4 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 38a2f44a5..1726294b4 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 1d8b8b47c..79d31dbc2 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index f1d858f05..80dcdaf6c 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -395,9 +395,14 @@ "TITLE": "Браузерный плеер сообщил ошибку воспроизведения", "DESCRIPTION": "Поток не воспроизвелся во встроенном плеере, но ошибка не указала конкретную причину в кодеке, контейнере или сети." }, + "NATIVE_FALLBACK_TITLE": "Откройте поток во внешнем плеере", + "INLINE_FAILURE_TITLE": "Проверьте поток или повторите попытку", "ACTION_OPEN_MPV": "Открыть в MPV", "ACTION_OPEN_VLC": "Открыть в VLC", + "ACTION_OPEN_MPV_HINT": "настроенный плеер", + "ACTION_OPEN_VLC_HINT": "настроенный плеер", "ACTION_COPY_URL": "Скопировать URL", + "ACTION_RETRY": "Повторить", "DETAILS_SUMMARY": "Технические сведения", "CODEC_HINT": "Обнаружены {{codecs}}. Браузерные плееры Chromium/Electron часто имеют ограниченную поддержку этих кодеков.", "DETAIL_CODE": "Код диагностики", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 803089552..6911a1c88 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 7a8da6975..7016ca2eb 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 4738de9ca..4f362b502 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -395,9 +395,14 @@ "TITLE": "The browser player reported a playback error", "DESCRIPTION": "The stream failed in the built-in player, but the error did not identify a specific codec, container, or network cause." }, + "NATIVE_FALLBACK_TITLE": "Open it in a native player instead", + "INLINE_FAILURE_TITLE": "Check the stream or try again", "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", + "ACTION_OPEN_MPV_HINT": "configured player", + "ACTION_OPEN_VLC_HINT": "configured player", "ACTION_COPY_URL": "Copy URL", + "ACTION_RETRY": "Retry", "DETAILS_SUMMARY": "Technical details", "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", "DETAIL_CODE": "Diagnostic code", diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index cbf176915..28b55b73d 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -138,7 +138,7 @@ and playback-position saving keep the same behavior as manual episode clicks. ## Codec And Container Diagnostics -The shared `WebPlayerViewComponent` is the central browser-player viewport for M3U, Xtream, and Stalker inline playback. Video.js, HTML5, and ArtPlayer report native media errors, HLS.js errors, mpegts.js errors, and HLS manifest codec metadata into the shared diagnostics classifier. +The shared `WebPlayerViewComponent` is the central browser-player viewport for M3U, Xtream, and Stalker inline playback, including live streams opened from favorites and recently viewed collections. Video.js, HTML5, and ArtPlayer report native media errors, HLS.js errors, mpegts.js errors, and HLS manifest codec metadata into the shared diagnostics classifier. The diagnostics remain client-only: @@ -161,13 +161,13 @@ Supported diagnostic codes are: mpegts.js `Early-EOF` failures on MPEG-TS streams are classified as `media-decode-error` instead of generic `network-error`. These failures usually mean the fetch stream ended before mpegts.js expected a complete transport stream, and external players may still handle the same URL more tolerant of short reads or malformed TS boundaries. -The banner keeps the primary message compact, then exposes technical details on demand: diagnostic code, reporting player/source, detected container/MIME, video/audio codecs, native browser error fields, and raw HLS/mpegts details. HLS manifest codec metadata also drives a concise browser-support hint for codecs that Chromium/Electron commonly cannot decode inline, such as HEVC, AC-3, E-AC-3, DTS, and MPEG-2 video. +The diagnostic surface covers the inline player viewport when playback fails, with a compact warning badge, a native-player fallback headline, and player-card actions for configured external players. It exposes technical details on demand: diagnostic code, reporting player/source, detected container/MIME, video/audio codecs, native browser error fields, and raw HLS/mpegts details. HLS manifest codec metadata also drives a concise browser-support hint for codecs that Chromium/Electron commonly cannot decode inline, such as HEVC, AC-3, E-AC-3, DTS, and MPEG-2 video. URL extension metadata is filtered before diagnostics and player selection use it. Web script extensions such as `.php` are not shown as stream containers; explicit media query metadata such as `extension=ts` or `format=m3u8` is preferred when present. Portal VOD and episode payloads with `contentInfo` are treated as non-live by the Video.js MPEG-TS path unless `isLive` is explicitly set. If Chromium leaves the underlying MediaSource duration at `Infinity` for a finite TS VOD, the Video.js wrapper normalizes its UI duration from the finite `seekable` or `buffered` range. This removes the misleading `LIVE` control state without changing stream decoding, diagnostics, or external fallback behavior. -When a diagnostic is actionable in Electron, the inline banner may offer `Open in MPV`, `Open in VLC`, and `Copy URL`. Web builds only expose copy/help text. MPV/VLC fallback requests carry the original `ResolvedPortalPlayback` payload so headers, referer, origin, user-agent, content metadata, and resume offset stay intact. +When a diagnostic is actionable in Electron, the diagnostic surface may offer `Open in MPV`, `Open in VLC`, `Copy URL`, technical details, and `Retry`. Web builds only expose copy/help text and retry. MPV/VLC fallback requests carry the original `ResolvedPortalPlayback` payload so headers, referer, origin, user-agent, content metadata, and resume offset stay intact. Retry clears the current diagnostic and rebuilds the active inline player inputs; it does not change the saved player setting. `PortalPlayer.openExternalPlayback(playback, player)` is the forced external launch API. It sends the playback payload to MPV or VLC regardless of the current saved player setting, so fallback buttons do not mutate preferences. diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.html b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.html index 3d8c42fe7..ea23637ed 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.html +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.html @@ -56,40 +56,15 @@ " /> } @else { - @if (player() === 'videojs') { - - } - @if (player() === 'html5') { - - } - @if (player() === 'artplayer') { - - } - @if (player() === 'embedded-mpv') { - - } + } } diff --git a/libs/portal/shared/util/src/lib/collection/unified-live-tab.component.spec.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.spec.ts similarity index 88% rename from libs/portal/shared/util/src/lib/collection/unified-live-tab.component.spec.ts rename to libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.spec.ts index f6e41c276..f4e8b3296 100644 --- a/libs/portal/shared/util/src/lib/collection/unified-live-tab.component.spec.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.spec.ts @@ -9,16 +9,9 @@ import { import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; -import { - GlobalFavoritesListComponent, - UnifiedLiveTabComponent, -} from '@iptvnator/portal/shared/ui'; -import { - AudioPlayerComponent, - ArtPlayerComponent, - HtmlVideoPlayerComponent, - VjsPlayerComponent, -} from '@iptvnator/ui/playback'; +import { GlobalFavoritesListComponent } from '../global-favorites-list/global-favorites-list.component'; +import { UnifiedLiveTabComponent } from './unified-live-tab.component'; +import { AudioPlayerComponent } from '@iptvnator/ui/playback'; import { EpgDateNavigationDirection, EpgListComponent, @@ -27,11 +20,19 @@ import { } from '@iptvnator/ui/epg'; import { ResizableDirective } from '@iptvnator/ui/components'; import { SettingsStore } from '@iptvnator/services'; -import { Channel, EpgItem, EpgProgram } from '@iptvnator/shared/interfaces'; +import { + Channel, + EpgItem, + EpgProgram, + ResolvedPortalPlayback, + VideoPlayer, +} from '@iptvnator/shared/interfaces'; import { EpgViewComponent, LiveEpgPanelComponent, LiveEpgPanelSummary, + type PlaybackFallbackRequest, + WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; import { DEFAULT_FAVORITES_CHANNEL_SORT_MODE, @@ -115,38 +116,6 @@ class StubLiveEpgPanelComponent { readonly dateNavigation = output(); } -@Component({ - selector: 'app-vjs-player', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class StubVjsPlayerComponent { - readonly options = input(); - readonly volume = input(1); -} - -@Component({ - selector: 'app-html-video-player', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class StubHtmlVideoPlayerComponent { - readonly channel = input(null); - readonly volume = input(1); - readonly showCaptions = input(false); -} - -@Component({ - selector: 'app-art-player', - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -class StubArtPlayerComponent { - readonly channel = input(null); - readonly volume = input(1); - readonly showCaptions = input(false); -} - @Component({ selector: 'app-audio-player', template: '
', @@ -158,9 +127,23 @@ class StubAudioPlayerComponent { readonly channelName = input(''); } +@Component({ + selector: 'app-web-player-view', + template: '
', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class StubWebPlayerViewComponent { + readonly streamUrl = input.required(); + readonly title = input(''); + readonly playback = input(null); + readonly playerOverride = input(null); + readonly externalFallbackRequested = output(); +} + describe('UnifiedLiveTabComponent', () => { let fixture: ComponentFixture; let component: UnifiedLiveTabComponent; + let player: ReturnType>; let streamResolver: { resolveLiveDetail: jest.Mock; resolveM3uPlaybackDetail: jest.Mock; @@ -173,6 +156,7 @@ describe('UnifiedLiveTabComponent', () => { let portalPlayer: { isEmbeddedPlayer: jest.Mock; openResolvedPlayback: jest.Mock; + openExternalPlayback: jest.Mock; }; beforeEach(async () => { @@ -187,9 +171,11 @@ describe('UnifiedLiveTabComponent', () => { recentData = { recordLivePlayback: jest.fn(), }; + player = signal(VideoPlayer.VideoJs); portalPlayer = { isEmbeddedPlayer: jest.fn().mockReturnValue(false), openResolvedPlayback: jest.fn(), + openExternalPlayback: jest.fn(), }; await TestBed.configureTestingModule({ @@ -201,7 +187,7 @@ describe('UnifiedLiveTabComponent', () => { provide: SettingsStore, useValue: { openStreamOnDoubleClick: signal(false), - player: signal('videojs'), + player, }, }, { provide: PORTAL_PLAYER, useValue: portalPlayer }, @@ -211,27 +197,23 @@ describe('UnifiedLiveTabComponent', () => { remove: { imports: [ AudioPlayerComponent, - ArtPlayerComponent, EpgListComponent, EpgViewComponent, GlobalFavoritesListComponent, - HtmlVideoPlayerComponent, LiveEpgPanelComponent, ResizableDirective, - VjsPlayerComponent, + WebPlayerViewComponent, ], }, add: { imports: [ StubAudioPlayerComponent, - StubArtPlayerComponent, StubEpgListComponent, StubEpgViewComponent, StubGlobalFavoritesListComponent, - StubHtmlVideoPlayerComponent, StubLiveEpgPanelComponent, StubResizableDirective, - StubVjsPlayerComponent, + StubWebPlayerViewComponent, ], }, }) @@ -455,6 +437,77 @@ describe('UnifiedLiveTabComponent', () => { expect(localStorage.getItem('live-epg-panel-state')).toBe('collapsed'); }); + it('uses the shared web player wrapper for inline live playback diagnostics', async () => { + portalPlayer.isEmbeddedPlayer.mockReturnValue(true); + player.set(VideoPlayer.VideoJs); + const item = buildLiveItem('xtream'); + streamResolver.resolveLiveDetail.mockResolvedValue({ + epgMode: 'portal', + playback: { + streamUrl: 'https://example.com/xtream.m3u8', + title: 'Xtream Live', + }, + epgItems: [], + }); + recentData.recordLivePlayback.mockResolvedValue({ + ...item, + viewedAt: '2026-03-26T12:00:00.000Z', + }); + + fixture.componentRef.setInput('items', [item]); + fixture.detectChanges(); + await fixture.whenStable(); + + await component.onChannelSelected(component.channelsForList()[0]); + fixture.detectChanges(); + await fixture.whenStable(); + + const webPlayer = fixture.debugElement.query( + By.directive(StubWebPlayerViewComponent) + ).componentInstance as StubWebPlayerViewComponent; + + expect(webPlayer.streamUrl()).toBe('https://example.com/xtream.m3u8'); + expect(webPlayer.title()).toBe('Xtream Live'); + const playback = webPlayer.playback(); + expect(playback).toEqual( + expect.objectContaining({ + streamUrl: 'https://example.com/xtream.m3u8', + title: 'Xtream Live', + }) + ); + if (!playback) { + throw new Error('Expected wrapper playback to be set'); + } + expect(webPlayer.playerOverride()).toBe(VideoPlayer.VideoJs); + expect(fixture.nativeElement.querySelector('app-vjs-player')).toBeNull(); + expect( + fixture.nativeElement.querySelector('app-html-video-player') + ).toBeNull(); + expect(fixture.nativeElement.querySelector('app-art-player')).toBeNull(); + + webPlayer.externalFallbackRequested.emit({ + player: 'mpv', + playback, + diagnostic: { + code: 'network-error', + player: 'videojs', + source: 'hls', + container: '', + mimeType: '', + videoCodecs: [], + audioCodecs: [], + externalFallbackRecommended: false, + }, + }); + + expect(portalPlayer.openExternalPlayback).toHaveBeenCalledWith( + expect.objectContaining({ + streamUrl: 'https://example.com/xtream.m3u8', + }), + 'mpv' + ); + }); + it('does not wait for M3U program lookup before opening playback', async () => { const item = buildLiveItem('m3u'); const pendingPrograms = createDeferred(); diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts index 1969c56a1..373cf0c0a 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.ts @@ -37,20 +37,16 @@ import { } from '@iptvnator/ui/epg'; import { GlobalFavoritesListComponent } from '../global-favorites-list/global-favorites-list.component'; import { PortalEmptyStateComponent } from '../portal-empty-state/portal-empty-state.component'; -import { - AudioPlayerComponent, - ArtPlayerComponent, - HtmlVideoPlayerComponent, - VjsPlayerComponent, - WebPlayerViewComponent, -} from '@iptvnator/ui/playback'; +import { AudioPlayerComponent } from '@iptvnator/ui/playback'; import { ResizableDirective } from '@iptvnator/ui/components'; import { SettingsStore } from '@iptvnator/services'; -import { Channel, EpgItem, EpgProgram } from '@iptvnator/shared/interfaces'; +import { EpgItem, EpgProgram } from '@iptvnator/shared/interfaces'; import { EpgViewComponent, LiveEpgPanelComponent, LiveEpgPanelSummary, + type PlaybackFallbackRequest, + WebPlayerViewComponent, } from '@iptvnator/ui/shared-portals'; @Component({ @@ -60,11 +56,9 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AudioPlayerComponent, - ArtPlayerComponent, EpgListComponent, EpgViewComponent, GlobalFavoritesListComponent, - HtmlVideoPlayerComponent, LiveEpgPanelComponent, MatButtonModule, MatIconModule, @@ -72,7 +66,6 @@ import { PortalEmptyStateComponent, ResizableDirective, TranslatePipe, - VjsPlayerComponent, WebPlayerViewComponent, ], }) @@ -163,38 +156,6 @@ export class UnifiedLiveTabComponent { return this.getLiveEpgPanelSummary(this.activeDetail()); }); - readonly activeChannelForOverlay = computed((): Channel | undefined => { - const detail = this.activeDetail(); - if (!detail) { - return undefined; - } - - if (detail.channel) { - return detail.channel; - } - - return { - id: this.activeUid() ?? '', - name: detail.playback.title ?? '', - url: detail.playback.streamUrl, - tvg: { - logo: detail.playback.thumbnail ?? '', - id: '', - name: '', - rec: '', - url: '', - }, - group: { title: '' }, - http: { - referrer: detail.playback.referer ?? '', - 'user-agent': detail.playback.userAgent ?? '', - origin: detail.playback.origin ?? '', - }, - radio: 'false', - epgParams: '', - } satisfies Channel; - }); - readonly channelsForList = computed((): UnifiedFavoriteChannel[] => this.items().map((item) => ({ uid: item.uid, @@ -331,6 +292,13 @@ export class UnifiedLiveTabComponent { this.selectedLiveEpgDate.set(selectedDate); } + handleExternalFallbackRequest(request: PlaybackFallbackRequest): void { + void this.portalPlayer.openExternalPlayback( + request.playback, + request.player + ); + } + onClose(): void { this.selectionRequestId += 1; this.isSelecting.set(false); @@ -348,14 +316,13 @@ export class UnifiedLiveTabComponent { isAutoOpen = false, startPlayback = false ): Promise { - if (this.activeUid() === item.uid && this.activeDetail()) { + const activeDetail = this.activeDetail(); + if (this.activeUid() === item.uid && activeDetail) { if ( startPlayback && - this.shouldOpenExternalPlayback(this.activeDetail()!, true) + this.shouldOpenExternalPlayback(activeDetail, true) ) { - void this.portalPlayer.openResolvedPlayback( - this.activeDetail()!.playback - ); + void this.portalPlayer.openResolvedPlayback(activeDetail.playback); } if (isAutoOpen) { this.autoOpenHandled.emit(); diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.html b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.html index 361e3e6e3..198e394fb 100644 --- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.html +++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.html @@ -46,21 +46,26 @@ data-test-id="playback-diagnostic-banner" role="status" > - -
-
- {{ getDiagnosticTitleKey(issue) | translate }} +
+
+ + {{ getDiagnosticTitleKey(issue) | translate }}
+ +

+ {{ getDiagnosticHeadlineKey() | translate }} +

+
{{ getDiagnosticDescriptionKey(issue) | translate }}
+ @if (getDiagnosticMeta(issue); as meta) {
{{ meta }}
} + @if (getDiagnosticCodecHint(issue); as codecHint) {
} -
- - - {{ 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' | translate }} - -
- @for ( - detail of getDiagnosticDetails(issue); - track detail.labelKey - ) { -
-
{{ detail.labelKey | translate }}
-
{{ detail.value }}
-
- } -
-
-
-
+ @if (canShowExternalFallbackActions()) { - - + + + +
} - + +
+ + + + + + + + +
+ + + {{ 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' | translate }} + +
+ @for ( + detail of getDiagnosticDetails(issue); + track detail.labelKey + ) { +
+
{{ detail.labelKey | translate }}
+
{{ detail.value }}
+
+ } +
+
+
} diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss index 27e24553d..996c6b19a 100644 --- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss +++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss @@ -1,4 +1,5 @@ .web-player-view { + container-type: inline-size; display: block; height: 100%; position: relative; @@ -15,94 +16,235 @@ app-html-video-player { .web-player-diagnostic { position: absolute; - inset-inline: 16px; - bottom: var(--web-player-diagnostic-bottom, 16px); + inset: 0; z-index: 5; - display: grid; - grid-template-areas: - 'icon body' - 'icon actions'; - grid-template-columns: auto minmax(0, 1fr); - gap: 8px 12px; - align-items: start; - padding: 14px 16px; - max-block-size: calc(100% - 32px); - overflow: auto; - border: 1px solid color-mix(in srgb, var(--mat-sys-error) 36%, transparent); - border-radius: 8px; - background: color-mix( - in srgb, - var(--mat-sys-error-container) 92%, - transparent - ); - color: var(--mat-sys-on-error-container); - box-shadow: 0 16px 42px rgb(0 0 0 / 28%); -} - -.web-player-diagnostic__icon { - grid-area: icon; - display: inline-flex; + display: flex; align-items: center; - justify-content: center; - padding-top: 2px; + padding: clamp(24px, 9vh, 92px) clamp(20px, 18vw, 216px); + overflow: auto; + background: + radial-gradient( + circle at 18% 18%, + rgb(255 176 76 / 6%), + transparent 28% + ), + rgb(0 0 0 / 96%); + color: #f6f6f6; } -.web-player-diagnostic__body { - grid-area: body; - max-width: 640px; +.web-player-diagnostic__content { + width: min(100%, 720px); } -.web-player-diagnostic__title { - font-size: 0.92rem; - font-weight: 650; - line-height: 1.2; +.web-player-diagnostic__badge { + display: inline-flex; + max-width: 100%; + align-items: center; + gap: 8px; + padding: 5px 11px; + border: 1px solid rgb(255 174 66 / 42%); + border-radius: 6px; + background: rgb(255 148 35 / 15%); + color: #ffb24c; + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0; + line-height: 1.1; + text-transform: uppercase; } -.web-player-diagnostic__description, -.web-player-diagnostic__meta, -.web-player-diagnostic__codec-hint { - margin-top: 3px; - font-size: 0.82rem; - line-height: 1.3; +.web-player-diagnostic__badge mat-icon { + width: 16px; + height: 16px; + flex: 0 0 16px; + font-size: 16px; +} + +.web-player-diagnostic__badge span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.web-player-diagnostic__headline { + max-width: 660px; + margin: 20px 0 0; + color: #fff; + font-size: 1.65rem; + font-weight: 760; + letter-spacing: 0; + line-height: 1.05; +} + +.web-player-diagnostic__description { + max-width: 760px; + margin-top: 12px; + color: rgb(255 255 255 / 73%); + font-size: 1rem; + line-height: 1.48; } .web-player-diagnostic__meta { - opacity: 0.78; + margin-top: 9px; + color: rgb(255 255 255 / 48%); + font-family: var(--app-monospace-font, monospace); + font-size: 0.82rem; + line-height: 1.35; overflow-wrap: anywhere; } .web-player-diagnostic__codec-hint { display: inline-block; max-width: 100%; - margin-top: 8px; + margin-top: 12px; padding: 4px 8px; - border: 1px solid - color-mix(in srgb, var(--mat-sys-on-error-container) 18%, transparent); + border: 1px solid rgb(255 255 255 / 12%); border-radius: 6px; - background: color-mix( - in srgb, - var(--mat-sys-on-error-container) 7%, - transparent - ); - font-weight: 550; + background: rgb(255 255 255 / 5%); + color: rgb(255 255 255 / 68%); + font-size: 0.78rem; + font-weight: 620; + line-height: 1.3; overflow-wrap: anywhere; } +.web-player-diagnostic__player-actions { + display: grid; + grid-template-columns: repeat(2, minmax(168px, 192px)); + gap: 12px; + margin-top: 28px; +} + +.web-player-diagnostic__player-card { + display: grid; + min-height: 60px; + grid-template-columns: 22px minmax(0, 1fr); + align-items: center; + gap: 12px; + padding: 12px 16px; + border: 1px solid rgb(255 255 255 / 13%); + border-radius: 8px; + background: rgb(0 0 0 / 22%); + color: #f5f5f5; + cursor: pointer; + text-align: left; + transition: + background-color 140ms ease, + border-color 140ms ease, + color 140ms ease, + transform 140ms ease; +} + +.web-player-diagnostic__player-card:hover { + border-color: rgb(255 255 255 / 28%); + background: rgb(255 255 255 / 6%); +} + +.web-player-diagnostic__player-card:active { + transform: translateY(1px); +} + +.web-player-diagnostic__player-card:focus-visible, +.web-player-diagnostic__utility-button:focus-visible, +.web-player-diagnostic__details summary:focus-visible { + outline: 2px solid #ffb24c; + outline-offset: 3px; +} + +.web-player-diagnostic__player-card--primary { + border-color: transparent; + background: #fff; + color: #151515; +} + +.web-player-diagnostic__player-card--primary:hover { + border-color: transparent; + background: rgb(255 255 255 / 88%); +} + +.web-player-diagnostic__player-card mat-icon { + width: 20px; + height: 20px; + font-size: 20px; +} + +.web-player-diagnostic__player-copy { + display: grid; + min-width: 0; + gap: 2px; +} + +.web-player-diagnostic__player-label { + overflow: hidden; + font-size: 0.95rem; + font-weight: 760; + line-height: 1.05; + text-overflow: ellipsis; + white-space: nowrap; +} + +.web-player-diagnostic__player-hint { + overflow: hidden; + color: currentColor; + font-size: 0.78rem; + line-height: 1.1; + opacity: 0.62; + text-overflow: ellipsis; + white-space: nowrap; +} + +.web-player-diagnostic__utilities { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 8px 12px; + margin-top: 22px; + color: rgb(255 255 255 / 45%); +} + +.web-player-diagnostic__utility-button, +.web-player-diagnostic__details summary { + display: inline-flex; + min-height: 24px; + align-items: center; + gap: 6px; + padding: 0; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + font: inherit; + font-size: 0.86rem; + font-weight: 640; + line-height: 1.2; +} + +.web-player-diagnostic__utility-button:hover, +.web-player-diagnostic__details summary:hover { + color: rgb(255 255 255 / 76%); +} + +.web-player-diagnostic__utility-button mat-icon, +.web-player-diagnostic__details summary mat-icon { + width: 16px; + height: 16px; + font-size: 16px; +} + +.web-player-diagnostic__divider { + width: 1px; + height: 18px; + margin-top: 3px; + background: rgb(255 255 255 / 16%); +} + .web-player-diagnostic__details { - margin-top: 10px; - border-top: 1px solid - color-mix(in srgb, var(--mat-sys-on-error-container) 16%, transparent); - padding-top: 8px; + min-width: 0; + max-width: 100%; } .web-player-diagnostic__details summary { - display: inline-flex; - align-items: center; - gap: 5px; - cursor: pointer; - font-size: 0.78rem; - font-weight: 650; - opacity: 0.84; list-style: none; } @@ -110,19 +252,23 @@ app-html-video-player { display: none; } -.web-player-diagnostic__details summary mat-icon { - width: 16px; - height: 16px; - font-size: 16px; +.web-player-diagnostic__details[open] { + flex-basis: 100%; } .web-player-diagnostic__details dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); - gap: 5px 12px; - margin: 8px 0 0; - font-size: 0.76rem; - line-height: 1.25; + gap: 7px 14px; + max-width: min(100%, 640px); + margin: 12px 0 0; + padding: 12px; + border: 1px solid rgb(255 255 255 / 10%); + border-radius: 8px; + background: rgb(255 255 255 / 5%); + color: rgb(255 255 255 / 68%); + font-size: 0.78rem; + line-height: 1.3; } .web-player-diagnostic__detail-row { @@ -130,12 +276,8 @@ app-html-video-player { } .web-player-diagnostic__detail-row dt { - color: color-mix( - in srgb, - var(--mat-sys-on-error-container) 76%, - transparent - ); - font-weight: 650; + color: rgb(255 255 255 / 48%); + font-weight: 700; white-space: nowrap; } @@ -145,34 +287,32 @@ app-html-video-player { overflow-wrap: anywhere; } -.web-player-diagnostic__actions { - grid-area: actions; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - gap: 8px; -} - -.web-player-diagnostic__actions button { - white-space: nowrap; -} - -@media (max-width: 760px) { +@container (max-width: 760px) { .web-player-diagnostic { - inset-inline: 8px; - bottom: var(--web-player-diagnostic-bottom, 8px); - max-block-size: calc(100% - 16px); - grid-template-areas: - 'icon body' - 'actions actions'; + align-items: flex-start; + padding: 22px 16px; } - .web-player-diagnostic__actions { - justify-content: flex-start; + .web-player-diagnostic__badge { + white-space: normal; + } + + .web-player-diagnostic__badge span { + white-space: normal; + } + + .web-player-diagnostic__headline { + margin-top: 16px; + font-size: 1.48rem; + } + + .web-player-diagnostic__utilities { + gap: 10px 12px; } .web-player-diagnostic__details dl { grid-template-columns: minmax(0, 1fr); + max-width: 100%; } .web-player-diagnostic__detail-row { @@ -184,3 +324,116 @@ app-html-video-player { white-space: normal; } } + +@container (max-width: 520px) { + .web-player-diagnostic { + align-items: flex-start; + padding: clamp(14px, 5cqi, 22px); + } + + .web-player-diagnostic__content { + width: 100%; + } + + .web-player-diagnostic__badge { + gap: 6px; + padding: 4px 8px; + font-size: 0.7rem; + white-space: normal; + } + + .web-player-diagnostic__badge span { + white-space: nowrap; + } + + .web-player-diagnostic__headline { + margin-top: 14px; + font-size: 1.35rem; + line-height: 1.08; + } + + .web-player-diagnostic__description { + margin-top: 10px; + font-size: 0.9rem; + line-height: 1.38; + } + + .web-player-diagnostic__meta { + margin-top: 8px; + font-size: 0.74rem; + } + + .web-player-diagnostic__codec-hint { + margin-top: 10px; + font-size: 0.72rem; + } + + .web-player-diagnostic__player-actions { + grid-template-columns: minmax(0, 1fr); + gap: 8px; + margin-top: 16px; + } + + .web-player-diagnostic__player-card { + min-height: 52px; + padding: 10px 12px; + } + + .web-player-diagnostic__utilities { + display: grid; + gap: 7px; + margin-top: 16px; + } + + .web-player-diagnostic__divider { + display: none; + } + + .web-player-diagnostic__utility-button, + .web-player-diagnostic__details summary { + width: fit-content; + min-height: 22px; + font-size: 0.82rem; + } + + .web-player-diagnostic__details[open] { + flex-basis: auto; + } + + .web-player-diagnostic__details dl { + max-height: 260px; + overflow: auto; + grid-template-columns: minmax(0, 1fr); + gap: 6px; + margin-top: 8px; + padding: 10px; + font-size: 0.72rem; + } + + .web-player-diagnostic__detail-row { + display: grid; + gap: 2px; + } + + .web-player-diagnostic__detail-row dt { + white-space: normal; + } +} + +@container (max-width: 340px) { + .web-player-diagnostic { + padding: 12px; + } + + .web-player-diagnostic__headline { + font-size: 1.22rem; + } + + .web-player-diagnostic__description { + font-size: 0.84rem; + } + + .web-player-diagnostic__player-label { + font-size: 0.88rem; + } +} diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts index 558016329..7f9ef4565 100644 --- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts +++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts @@ -158,6 +158,9 @@ describe('WebPlayerViewComponent', () => { expect(banner.nativeElement.textContent).toContain( 'PLAYBACK_DIAGNOSTICS.UNSUPPORTED_CONTAINER.TITLE' ); + expect(banner.nativeElement.textContent).toContain( + 'PLAYBACK_DIAGNOSTICS.NATIVE_FALLBACK_TITLE' + ); mpvButton.nativeElement.click(); expect(requests).toEqual([ @@ -242,6 +245,27 @@ describe('WebPlayerViewComponent', () => { ); }); + it('uses an inline recovery headline when external fallback actions are unavailable', () => { + fixture.detectChanges(); + component.handlePlaybackIssue(createNetworkDiagnostic()); + fixture.detectChanges(); + + const banner = fixture.debugElement.query( + By.css('[data-test-id="playback-diagnostic-banner"]') + ); + const mpvButton = fixture.debugElement.query( + By.css('[data-test-id="playback-fallback-mpv"]') + ); + + expect(mpvButton).toBeNull(); + expect(banner.nativeElement.textContent).toContain( + 'PLAYBACK_DIAGNOSTICS.NETWORK_ERROR.TITLE' + ); + expect(banner.nativeElement.textContent).toContain( + 'PLAYBACK_DIAGNOSTICS.INLINE_FAILURE_TITLE' + ); + }); + it('renders technical details and codec-specific hints in the diagnostic banner', () => { fixture.detectChanges(); const issue = createUnsupportedCodecDiagnostic(); @@ -284,6 +308,31 @@ describe('WebPlayerViewComponent', () => { 'PLAYBACK_DIAGNOSTICS.CODEC_HINT' ); }); + + it('clears playback diagnostics when retrying inline playback', () => { + fixture.detectChanges(); + component.handlePlaybackIssue(createUnsupportedCodecDiagnostic()); + fixture.detectChanges(); + + const retryButton = fixture.debugElement.query( + By.css('[data-test-id="playback-retry"]') + ); + const utilityControls = fixture.nativeElement.querySelectorAll( + '[data-test-id="playback-retry"], [data-test-id="playback-diagnostic-details"]' + ); + + expect(utilityControls[0]).toBe(retryButton.nativeElement); + + retryButton.nativeElement.click(); + fixture.detectChanges(); + + expect(component.playbackDiagnostic()).toBeNull(); + expect( + fixture.debugElement.query( + By.css('[data-test-id="playback-diagnostic-banner"]') + ) + ).toBeNull(); + }); }); function createUnsupportedContainerDiagnostic(): PlaybackDiagnostic { @@ -329,3 +378,18 @@ function createUnsupportedCodecDiagnostic(): PlaybackDiagnostic { externalFallbackRecommended: true, }; } + +function createNetworkDiagnostic(): PlaybackDiagnostic { + return { + code: PlaybackDiagnosticCode.NetworkError, + source: PlaybackDiagnosticSource.MpegTs, + sourceUrl: 'https://example.com/live/channel.ts', + container: 'ts', + mimeType: 'video/mp2t', + player: 'videojs', + audioCodecs: [], + videoCodecs: [], + details: 'HttpStatusCodeInvalid {"code":456,"msg":""}', + externalFallbackRecommended: false, + }; +} diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts index f216de195..04a92a59b 100644 --- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts +++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts @@ -165,6 +165,14 @@ export class WebPlayerViewComponent { }); } + retryPlayback(): void { + const playback = this.resolvedPlayback(); + + this.playbackDiagnostic.set(null); + this.setChannel(playback.streamUrl); + this.setVjsOptions(playback.streamUrl, this.isLivePlayback(playback)); + } + getDiagnosticTitleKey(issue: PlaybackDiagnostic): string { return `${this.getDiagnosticTranslationBase(issue)}.TITLE`; } @@ -173,6 +181,12 @@ export class WebPlayerViewComponent { return `${this.getDiagnosticTranslationBase(issue)}.DESCRIPTION`; } + getDiagnosticHeadlineKey(): string { + return this.canShowExternalFallbackActions() + ? 'PLAYBACK_DIAGNOSTICS.NATIVE_FALLBACK_TITLE' + : 'PLAYBACK_DIAGNOSTICS.INLINE_FAILURE_TITLE'; + } + getDiagnosticMeta(issue: PlaybackDiagnostic): string { const codecs = [...issue.videoCodecs, ...issue.audioCodecs].join(', '); if (codecs) { diff --git a/libs/ui/styles/_portal-layout.scss b/libs/ui/styles/_portal-layout.scss index aef73c0e4..b18f4ec71 100644 --- a/libs/ui/styles/_portal-layout.scss +++ b/libs/ui/styles/_portal-layout.scss @@ -73,10 +73,10 @@ position: absolute; top: 6px; left: 6px; - // Player internals, including the radio audio stage, may use z-index - // 2 inside the content area. Keep the restore affordance above them - // for both paint order and hit testing. - z-index: 5; + // Player internals, including diagnostic overlays, may use z-index 5 + // inside the content area. Keep the restore affordance above them for + // both paint order and hit testing. + z-index: 6; background: rgba(128, 128, 128, 0.18); color: var(--mat-sys-on-surface, rgba(255, 255, 255, 0.75)); box-shadow: none;