From da04f4c9e9c39df77f5ac3532e0c65c53fe6066d Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 14 May 2026 22:11:05 +0200 Subject: [PATCH] feat(playback): expose browser-player diagnostic details Entire-Checkpoint: f957cd9849e0 --- apps/web/src/assets/i18n/ar.json | 14 ++- apps/web/src/assets/i18n/ary.json | 14 ++- apps/web/src/assets/i18n/by.json | 14 ++- apps/web/src/assets/i18n/de.json | 14 ++- apps/web/src/assets/i18n/el.json | 14 ++- apps/web/src/assets/i18n/en.json | 14 ++- apps/web/src/assets/i18n/es.json | 14 ++- apps/web/src/assets/i18n/fr.json | 14 ++- apps/web/src/assets/i18n/it.json | 14 ++- apps/web/src/assets/i18n/ja.json | 14 ++- apps/web/src/assets/i18n/ko.json | 14 ++- apps/web/src/assets/i18n/nl.json | 14 ++- apps/web/src/assets/i18n/pl.json | 14 ++- apps/web/src/assets/i18n/pt.json | 14 ++- apps/web/src/assets/i18n/ru.json | 14 ++- apps/web/src/assets/i18n/tr.json | 14 ++- apps/web/src/assets/i18n/zh.json | 14 ++- apps/web/src/assets/i18n/zhtw.json | 14 ++- docs/architecture/embedded-inline-playback.md | 7 ++ .../lib/art-player/art-player.component.ts | 15 ++- .../html-video-player.component.ts | 27 ++-- .../playback-diagnostics.util.spec.ts | 52 ++++++++ .../playback-diagnostics.util.ts | 116 ++++++++++++++---- .../vjs-player/vjs-player.component.spec.ts | 3 + .../lib/vjs-player/vjs-player.component.ts | 4 +- .../web-player-view.component.html | 31 +++++ .../web-player-view.component.scss | 90 +++++++++++++- .../web-player-view.component.spec.ts | 71 +++++++++++ .../web-player-view.component.ts | 93 +++++++++++++- 29 files changed, 696 insertions(+), 65 deletions(-) diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index c712b9a50..f3ed480d5 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "إخفاء قائمة القنوات", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 2b7d4f321..43c57572c 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "إخفاء قائمة القنوات", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 77037ce1f..fa11ffdc3 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Схаваць спіс каналаў", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index dbcd57b82..56f98f36c 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Senderliste ausblenden", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 8ccb9a5d3..90a12934a 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Απόκρυψη λίστας καναλιών", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index f2a00f2fe..42accbaf2 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -397,7 +397,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Hide channels list", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index f24a62cb6..c3ed48941 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Ocultar lista de canales", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index fb59f39af..b69e90e6b 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Masquer la liste des chaînes", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index c0d475eb6..9763c199f 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Nascondi elenco canali", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index cc0fdabca..6a2bef802 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "チャンネル一覧を非表示", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 1683de803..4ee252d96 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "채널 목록 숨기기", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index da59ff376..701b1a6bc 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Zenderlijst verbergen", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 24bcb4618..24056e9ab 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Ukryj listę kanałów", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index b0e76969e..ccf19a571 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Ocultar lista de canais", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index da68eed5e..3133e9f5a 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -395,7 +395,19 @@ }, "ACTION_OPEN_MPV": "Открыть в MPV", "ACTION_OPEN_VLC": "Открыть в VLC", - "ACTION_COPY_URL": "Скопировать URL" + "ACTION_COPY_URL": "Скопировать URL", + "DETAILS_SUMMARY": "Технические сведения", + "CODEC_HINT": "Обнаружены {{codecs}}. Браузерные плееры Chromium/Electron часто имеют ограниченную поддержку этих кодеков.", + "DETAIL_CODE": "Код диагностики", + "DETAIL_PLAYER": "Плеер", + "DETAIL_SOURCE": "Источник", + "DETAIL_CONTAINER": "Контейнер", + "DETAIL_MIME_TYPE": "MIME-тип", + "DETAIL_VIDEO_CODECS": "Видео кодеки", + "DETAIL_AUDIO_CODECS": "Аудио кодеки", + "DETAIL_NATIVE_ERROR_CODE": "Код ошибки браузера", + "DETAIL_NATIVE_ERROR_MESSAGE": "Сообщение браузера", + "DETAIL_ERROR_DETAILS": "Детали ошибки" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Скрыть список каналов", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index b86da3b85..888d497b0 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "Kanal listesini gizle", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index cb3c029f2..5928a877b 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "隐藏频道列表", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index d19ead863..586bb50ba 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -391,7 +391,19 @@ }, "ACTION_OPEN_MPV": "Open in MPV", "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "ACTION_COPY_URL": "Copy URL", + "DETAILS_SUMMARY": "Technical details", + "CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.", + "DETAIL_CODE": "Diagnostic code", + "DETAIL_PLAYER": "Player", + "DETAIL_SOURCE": "Source", + "DETAIL_CONTAINER": "Container", + "DETAIL_MIME_TYPE": "MIME type", + "DETAIL_VIDEO_CODECS": "Video codecs", + "DETAIL_AUDIO_CODECS": "Audio codecs", + "DETAIL_NATIVE_ERROR_CODE": "Native error code", + "DETAIL_NATIVE_ERROR_MESSAGE": "Native error message", + "DETAIL_ERROR_DETAILS": "Error details" }, "LAYOUT": { "HIDE_CHANNELS_LIST": "隱藏頻道列表", diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index d06f52576..868a8776a 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -153,9 +153,16 @@ Supported diagnostic codes are: - `unsupported-codec` - `media-decode-error` - `network-error` +- `browser-access-error` - `drm-or-encryption` - `unknown-playback-error` +`network-error` is reserved for provider/network loading failures. Browser security failures such as CORS, mixed content, Content Security Policy, and private-network-access blocks are classified as `browser-access-error` so the UI can explain that the browser player was blocked before playback reached decoding. + +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. + +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. + 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. `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/ui/playback/src/lib/art-player/art-player.component.ts b/libs/ui/playback/src/lib/art-player/art-player.component.ts index b875a0609..ef9cbf359 100644 --- a/libs/ui/playback/src/lib/art-player/art-player.component.ts +++ b/libs/ui/playback/src/lib/art-player/art-player.component.ts @@ -12,7 +12,6 @@ import { } from '@angular/core'; import Artplayer from 'artplayer'; import Hls, { type ErrorData, type ManifestParsedData } from 'hls.js'; -import { getStreamExtensionFromUrl } from 'm3u-utils'; import mpegts from 'mpegts.js'; import { Channel } from 'shared-interfaces'; import { @@ -23,6 +22,7 @@ import { classifyNativePlaybackIssue, classifyUnsupportedHlsManifestCodecs, createPlaybackSourceMetadata, + getPlaybackMediaExtensionFromUrl, } from '../playback-diagnostics/playback-diagnostics.util'; type AudioTrackSelector = { @@ -131,7 +131,9 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { const el = this.elementRef.nativeElement.querySelector( '.artplayer-container' ); - const extension = getStreamExtensionFromUrl(this.channel?.url ?? ''); + const extension = getPlaybackMediaExtensionFromUrl( + this.channel?.url ?? '' + ); const isLive = extension === 'm3u8' || extension === 'ts' || !extension; this.player = new Artplayer({ @@ -225,7 +227,10 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { 'error', this.handleNativePlaybackError ); - this.player.video.addEventListener('loadeddata', this.clearPlaybackIssue); + this.player.video.addEventListener( + 'loadeddata', + this.clearPlaybackIssue + ); this.player.video.addEventListener('playing', this.clearPlaybackIssue); if (this.startTime > 0) { @@ -308,7 +313,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { const selectedLabel = typeof item.html === 'string' ? item.html - : item.html.textContent ?? ''; + : (item.html.textContent ?? ''); const selectedIndex = tracks.findIndex( (t, i) => (t.name || t.lang || `Track ${i + 1}`) === @@ -325,7 +330,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { } private getVideoType(url: string): string { - const extension = getStreamExtensionFromUrl(url); + const extension = getPlaybackMediaExtensionFromUrl(url); switch (extension) { case 'mkv': return 'video/matroska'; diff --git a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts index a4aa4ca8d..0ce93d9f1 100644 --- a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts +++ b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts @@ -13,7 +13,6 @@ import { } from '@angular/core'; import Hls, { type ErrorData, type ManifestParsedData } from 'hls.js'; import mpegts from 'mpegts.js'; -import { getStreamExtensionFromUrl } from 'm3u-utils'; import { DataService } from 'services'; import { Channel } from 'shared-interfaces'; import { @@ -24,6 +23,7 @@ import { classifyNativePlaybackIssue, classifyUnsupportedHlsManifestCodecs, createPlaybackSourceMetadata, + getPlaybackMediaExtensionFromUrl, } from '../playback-diagnostics/playback-diagnostics.util'; /** @@ -82,11 +82,14 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy { this.onVolumeChange(); }); - this.videoPlayer.nativeElement.addEventListener('loadedmetadata', () => { - if (this.startTime > 0) { - this.videoPlayer.nativeElement.currentTime = this.startTime; + this.videoPlayer.nativeElement.addEventListener( + 'loadedmetadata', + () => { + if (this.startTime > 0) { + this.videoPlayer.nativeElement.currentTime = this.startTime; + } } - }); + ); this.videoPlayer.nativeElement.addEventListener('timeupdate', () => { this.timeUpdate.emit({ @@ -143,7 +146,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy { if (channel.url) { this.playbackIssue.emit(null); const url = channel.url + (channel.epgParams ?? ''); - const extension = getStreamExtensionFromUrl(channel.url); + const extension = getPlaybackMediaExtensionFromUrl(channel.url); // Set user agent if specified on channel if (channel.http?.['user-agent']) { @@ -154,7 +157,11 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy { } if ((extension === 'ts' || !extension) && mpegts.isSupported()) { - console.log('Using mpegts.js for TS stream:', channel.name, url); + console.log( + 'Using mpegts.js for TS stream:', + channel.name, + url + ); this.mpegtsPlayer = mpegts.createPlayer({ type: 'mpegts', isLive: true, @@ -165,11 +172,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy { ); this.mpegtsPlayer.on( mpegts.Events.ERROR, - ( - type: string, - details: string, - info: unknown - ): void => { + (type: string, details: string, info: unknown): void => { this.playbackIssue.emit( classifyMpegTsPlaybackIssue( { diff --git a/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.spec.ts b/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.spec.ts index cd3c281c4..32022b40d 100644 --- a/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.spec.ts +++ b/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.spec.ts @@ -4,6 +4,8 @@ import { classifyMpegTsPlaybackIssue, classifyNativePlaybackIssue, createPlaybackSourceMetadata, + getLikelyBrowserUnsupportedCodecLabels, + getPlaybackMediaExtensionFromUrl, } from './playback-diagnostics.util'; describe('playback diagnostics', () => { @@ -107,6 +109,24 @@ describe('playback diagnostics', () => { expect(issue.externalFallbackRecommended).toBe(true); }); + it('classifies browser security policy blocks as browser access errors', () => { + const issue = classifyHlsPlaybackIssue( + { + type: 'networkError', + details: + 'manifestLoadError Refused to connect because it violates the following Content Security Policy directive: "connect-src"', + fatal: true, + }, + createPlaybackSourceMetadata({ + url: 'http://provider.example/live.m3u8', + player: 'videojs', + }) + ); + + expect(issue.code).toBe(PlaybackDiagnosticCode.BrowserAccessError); + expect(issue.externalFallbackRecommended).toBe(true); + }); + it('classifies native CORS failures as browser access errors', () => { const issue = classifyNativePlaybackIssue( { @@ -178,4 +198,36 @@ describe('playback diagnostics', () => { expect(metadata.extension).toBe(''); expect(metadata.container).toBe(''); }); + + it('uses declared media query metadata without exposing web script extensions', () => { + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?format=m3u8&stream=123' + ) + ).toBe('m3u8'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?type=m3u8&stream=123' + ) + ).toBe('m3u8'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?output=ts&stream=123' + ) + ).toBe('ts'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?stream=123' + ) + ).toBe(''); + }); + + it('detects codecs with limited Chromium browser-player support', () => { + expect( + getLikelyBrowserUnsupportedCodecLabels({ + audioCodecs: ['mp4a.40.2', 'ac-3', 'ec-3'], + videoCodecs: ['avc1.64001f', 'hvc1.1.6.L93.B0'], + }) + ).toEqual(['HEVC', 'AC-3', 'E-AC-3']); + }); }); diff --git a/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts b/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts index 87580a8fc..26a630adf 100644 --- a/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts +++ b/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.util.ts @@ -115,6 +115,38 @@ const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([ 'wmv', ]); +const DECLARED_MEDIA_EXTENSION_QUERY_KEYS = [ + 'extension', + 'ext', + 'format', + 'container', + 'type', + 'output', +]; + +const DECLARED_MEDIA_EXTENSION_ALIASES = new Map([ + ['hls', 'm3u8'], + ['mpegts', 'ts'], + ['mpeg-ts', 'ts'], +]); + +const DECLARED_MEDIA_EXTENSIONS = new Set([ + ...UNSUPPORTED_CONTAINER_EXTENSIONS, + 'aac', + 'flac', + 'm3u', + 'm3u8', + 'm4s', + 'mp3', + 'mp4', + 'mpd', + 'mpv', + 'oga', + 'ogg', + 'ogv', + 'webm', +]); + const NON_MEDIA_URL_EXTENSIONS = new Set([ 'asp', 'aspx', @@ -128,10 +160,21 @@ const SOURCE_NOT_SUPPORTED_CODE = 4; const DECODE_ERROR_CODE = 3; const NETWORK_ERROR_CODE = 2; +const BROWSER_LIMITED_CODEC_PATTERNS: ReadonlyArray<{ + readonly label: string; + readonly pattern: RegExp; +}> = [ + { label: 'HEVC', pattern: /^(hev1|hvc1|hevc|h265)/ }, + { label: 'AC-3', pattern: /^(ac-?3|dac3)$/ }, + { label: 'E-AC-3', pattern: /^(ec-?3|eac-?3|dec3)$/ }, + { label: 'DTS', pattern: /^(dts|dtsc|dtse|dtsh|dtsl)/ }, + { label: 'MPEG-2 Video', pattern: /^(mp2v|mpeg2video)/ }, +]; + export function createPlaybackSourceMetadata( input: PlaybackSourceMetadataInput ): PlaybackSourceMetadata { - const extension = getMediaExtensionFromUrl(input.url); + const extension = getPlaybackMediaExtensionFromUrl(input.url); const mimeType = input.mimeType?.trim() || undefined; return { @@ -145,6 +188,32 @@ export function createPlaybackSourceMetadata( }; } +export function getPlaybackMediaExtensionFromUrl(url: string): string { + const queryExtension = getMediaExtensionFromQuery(url); + if (queryExtension) { + return queryExtension; + } + + const pathExtension = normalizeExtensionToken(getExtensionFromUrl(url)); + if (NON_MEDIA_URL_EXTENSIONS.has(pathExtension)) { + return ''; + } + + return pathExtension; +} + +export function getLikelyBrowserUnsupportedCodecLabels( + metadata: Pick +): string[] { + const codecs = [...metadata.videoCodecs, ...metadata.audioCodecs].map( + normalizeToken + ); + + return BROWSER_LIMITED_CODEC_PATTERNS.filter(({ pattern }) => + codecs.some((codec) => pattern.test(codec)) + ).map(({ label }) => label); +} + export function classifyNativePlaybackIssue( error: NativePlaybackErrorInput | MediaError | null | undefined, metadata: PlaybackSourceMetadata @@ -411,39 +480,36 @@ function normalizeExtensionToken(value: string | undefined): string { return normalizeToken(value).replace(/^\.+/, ''); } -function getMediaExtensionFromUrl(url: string): string { - const queryExtension = getMediaExtensionFromQuery(url); - if (queryExtension) { - return queryExtension; - } - - const pathExtension = normalizeExtensionToken(getExtensionFromUrl(url)); - if (NON_MEDIA_URL_EXTENSIONS.has(pathExtension)) { - return ''; - } - - return pathExtension; -} - function getMediaExtensionFromQuery(url: string): string { try { const parsedUrl = new URL(url, 'http://iptvnator.local'); - const declaredExtension = normalizeExtensionToken( - parsedUrl.searchParams.get('extension') ?? undefined - ); - - if (!declaredExtension) { - return ''; + for (const key of DECLARED_MEDIA_EXTENSION_QUERY_KEYS) { + const declaredExtension = normalizeDeclaredMediaExtension( + parsedUrl.searchParams.get(key) ?? undefined + ); + if (declaredExtension) { + return declaredExtension; + } } - return NON_MEDIA_URL_EXTENSIONS.has(declaredExtension) - ? '' - : declaredExtension; + return ''; } catch { return ''; } } +function normalizeDeclaredMediaExtension(value: string | undefined): string { + const extension = normalizeExtensionToken(value); + const normalized = + DECLARED_MEDIA_EXTENSION_ALIASES.get(extension) ?? extension; + + if (!normalized || NON_MEDIA_URL_EXTENSIONS.has(normalized)) { + return ''; + } + + return DECLARED_MEDIA_EXTENSIONS.has(normalized) ? normalized : ''; +} + function inferContainerFromMimeType(mimeType: string | undefined): string { if (!mimeType) { return ''; @@ -500,7 +566,9 @@ function isBrowserAccessFailure(details: string): boolean { details.includes('cross origin') || details.includes('access-control') || details.includes('access control') || + details.includes('content security policy') || details.includes('mixed content') || + details.includes('private network access') || details.includes('blocked by') || details.includes('has been blocked') || details.includes('not allowed to load local resource') || diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts index b25298785..85da409f2 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.spec.ts @@ -163,6 +163,9 @@ describe('VjsPlayerComponent', () => { expect( isMpegTsSource('https://example.com/live.php?extension=ts') ).toBe(true); + expect(isMpegTsSource('https://example.com/live.php?stream=123')).toBe( + true + ); }); it('emits a playback issue when VideoJS reports an unsupported source', () => { diff --git a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts index d1b538aa8..02fd0e187 100644 --- a/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts +++ b/libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts @@ -11,7 +11,6 @@ import { viewChild, } from '@angular/core'; import '@yangkghjh/videojs-aspect-ratio-panel'; -import { getStreamExtensionFromUrl } from 'm3u-utils'; import mpegts from 'mpegts.js'; import videoJs from 'video.js'; import 'videojs-contrib-quality-levels'; @@ -22,6 +21,7 @@ import { classifyMpegTsPlaybackIssue, classifyNativePlaybackIssue, createPlaybackSourceMetadata, + getPlaybackMediaExtensionFromUrl, } from '../playback-diagnostics/playback-diagnostics.util'; /** @@ -254,7 +254,7 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { private isMpegTsSource(url?: string): boolean { if (!url) return false; - const extension = getStreamExtensionFromUrl(url); + const extension = getPlaybackMediaExtensionFromUrl(url); return (extension === 'ts' || !extension) && mpegts.isSupported(); } 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 cb5cb8474..361e3e6e3 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 @@ -61,6 +61,37 @@ {{ meta }} } + @if (getDiagnosticCodecHint(issue); as codecHint) { +
+ {{ + 'PLAYBACK_DIAGNOSTICS.CODEC_HINT' + | translate: { codecs: codecHint } + }} +
+ } +
+ + + {{ 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' | translate }} + +
+ @for ( + detail of getDiagnosticDetails(issue); + track detail.labelKey + ) { +
+
{{ detail.labelKey | translate }}
+
{{ detail.value }}
+
+ } +
+
@if (canShowExternalFallbackActions()) { 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 71daef965..27e24553d 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 @@ -59,7 +59,8 @@ app-html-video-player { } .web-player-diagnostic__description, -.web-player-diagnostic__meta { +.web-player-diagnostic__meta, +.web-player-diagnostic__codec-hint { margin-top: 3px; font-size: 0.82rem; line-height: 1.3; @@ -70,6 +71,80 @@ app-html-video-player { overflow-wrap: anywhere; } +.web-player-diagnostic__codec-hint { + display: inline-block; + max-width: 100%; + margin-top: 8px; + padding: 4px 8px; + border: 1px solid + color-mix(in srgb, var(--mat-sys-on-error-container) 18%, transparent); + border-radius: 6px; + background: color-mix( + in srgb, + var(--mat-sys-on-error-container) 7%, + transparent + ); + font-weight: 550; + overflow-wrap: anywhere; +} + +.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; +} + +.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; +} + +.web-player-diagnostic__details summary::-webkit-details-marker { + display: none; +} + +.web-player-diagnostic__details summary mat-icon { + width: 16px; + height: 16px; + font-size: 16px; +} + +.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; +} + +.web-player-diagnostic__detail-row { + display: contents; +} + +.web-player-diagnostic__detail-row dt { + color: color-mix( + in srgb, + var(--mat-sys-on-error-container) 76%, + transparent + ); + font-weight: 650; + white-space: nowrap; +} + +.web-player-diagnostic__detail-row dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; +} + .web-player-diagnostic__actions { grid-area: actions; display: flex; @@ -95,4 +170,17 @@ app-html-video-player { .web-player-diagnostic__actions { justify-content: flex-start; } + + .web-player-diagnostic__details dl { + grid-template-columns: minmax(0, 1fr); + } + + .web-player-diagnostic__detail-row { + display: grid; + gap: 2px; + } + + .web-player-diagnostic__detail-row dt { + white-space: normal; + } } 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 2a6d26213..701eeb0e6 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 @@ -188,6 +188,19 @@ describe('WebPlayerViewComponent', () => { ]); }); + it('treats web script playback URLs without declared media extension as MPEG-TS', () => { + const streamUrl = 'https://example.com/live.php?stream=123&token=x'; + + component.setVjsOptions(streamUrl); + + expect(component.vjsOptions.sources).toEqual([ + { + src: streamUrl, + type: 'video/mp2t', + }, + ]); + }); + it('uses browser access diagnostic translation keys', () => { const issue = createBrowserAccessDiagnostic(); @@ -198,6 +211,49 @@ describe('WebPlayerViewComponent', () => { 'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR.DESCRIPTION' ); }); + + it('renders technical details and codec-specific hints in the diagnostic banner', () => { + fixture.detectChanges(); + const issue = createUnsupportedCodecDiagnostic(); + + component.handlePlaybackIssue(issue); + fixture.detectChanges(); + + const details = fixture.debugElement.query( + By.css('[data-test-id="playback-diagnostic-details"]') + ); + const codecHint = fixture.debugElement.query( + By.css('[data-test-id="playback-codec-hint"]') + ); + + expect(details.nativeElement.textContent).toContain( + 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' + ); + expect(component.getDiagnosticCodecHint(issue)).toBe('HEVC, AC-3'); + expect(component.getDiagnosticDetails(issue)).toEqual( + expect.arrayContaining([ + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CODE', + value: 'unsupported-codec', + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_PLAYER', + value: 'Video.js', + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_VIDEO_CODECS', + value: 'hvc1.1.6.L93.B0', + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_AUDIO_CODECS', + value: 'ac-3', + }, + ]) + ); + expect(codecHint.nativeElement.textContent).toContain( + 'PLAYBACK_DIAGNOSTICS.CODEC_HINT' + ); + }); }); function createUnsupportedContainerDiagnostic(): PlaybackDiagnostic { @@ -228,3 +284,18 @@ function createBrowserAccessDiagnostic(): PlaybackDiagnostic { externalFallbackRecommended: true, }; } + +function createUnsupportedCodecDiagnostic(): PlaybackDiagnostic { + return { + code: PlaybackDiagnosticCode.UnsupportedCodec, + source: PlaybackDiagnosticSource.Hls, + sourceUrl: 'https://example.com/live/index.m3u8', + container: 'm3u8', + mimeType: 'application/x-mpegURL', + player: 'videojs', + audioCodecs: ['ac-3'], + videoCodecs: ['hvc1.1.6.L93.B0'], + details: 'manifestIncompatibleCodecsError', + externalFallbackRecommended: true, + }; +} 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 3beaf7557..cdc7de61f 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 @@ -17,7 +17,6 @@ import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import { StorageMap } from '@ngx-pwa/local-storage'; import { TranslatePipe } from '@ngx-translate/core'; -import { getStreamExtensionFromUrl } from 'm3u-utils'; import { ResolvedPortalPlayback, Settings, @@ -32,9 +31,16 @@ import { type PlaybackDiagnostic, PlaybackDiagnosticCode, type PlaybackFallbackRequest, + getLikelyBrowserUnsupportedCodecLabels, + getPlaybackMediaExtensionFromUrl, } from '../playback-diagnostics/playback-diagnostics.util'; import { VjsPlayerComponent } from '../vjs-player/vjs-player.component'; +type PlaybackDiagnosticDetail = { + readonly labelKey: string; + readonly value: string; +}; + @Component({ selector: 'app-web-player-view', templateUrl: './web-player-view.component.html', @@ -118,7 +124,7 @@ export class WebPlayerViewComponent { } setVjsOptions(streamUrl: string) { - const extension = getStreamExtensionFromUrl(streamUrl); + const extension = getPlaybackMediaExtensionFromUrl(streamUrl); const mimeType = extension === 'm3u' || extension === 'm3u8' ? 'application/x-mpegURL' @@ -168,7 +174,58 @@ export class WebPlayerViewComponent { return codecs; } - return issue.container || issue.mimeType || issue.details || ''; + return issue.container || issue.mimeType || ''; + } + + getDiagnosticCodecHint(issue: PlaybackDiagnostic): string { + return getLikelyBrowserUnsupportedCodecLabels(issue).join(', '); + } + + getDiagnosticDetails( + issue: PlaybackDiagnostic + ): readonly PlaybackDiagnosticDetail[] { + return [ + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CODE', + value: issue.code, + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_PLAYER', + value: this.formatPlayer(issue.player), + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_SOURCE', + value: this.formatDiagnosticSource(issue.source), + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CONTAINER', + value: issue.container, + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_MIME_TYPE', + value: issue.mimeType ?? '', + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_VIDEO_CODECS', + value: issue.videoCodecs.join(', '), + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_AUDIO_CODECS', + value: issue.audioCodecs.join(', '), + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_NATIVE_ERROR_CODE', + value: issue.nativeErrorCode?.toString() ?? '', + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_NATIVE_ERROR_MESSAGE', + value: issue.nativeErrorMessage ?? '', + }, + { + labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_ERROR_DETAILS', + value: issue.details ?? '', + }, + ].filter(({ value }) => value.trim().length > 0); } private getDiagnosticTranslationBase(issue: PlaybackDiagnostic): string { @@ -194,4 +251,34 @@ export class WebPlayerViewComponent { private detectDesktop(): boolean { return typeof window !== 'undefined' && !!window.electron; } + + private formatPlayer(player: PlaybackDiagnostic['player']): string { + switch (player) { + case 'videojs': + return 'Video.js'; + case 'html5': + return 'HTML5'; + case 'artplayer': + return 'ArtPlayer'; + default: + return ''; + } + } + + private formatDiagnosticSource( + source: PlaybackDiagnostic['source'] + ): string { + switch (source) { + case 'hls': + return 'HLS.js'; + case 'mpegts': + return 'mpegts.js'; + case 'native': + return 'Native media element'; + case 'source': + return 'Stream metadata'; + default: + return source; + } + } }