From 4a4334cb64a737d8bcaf844853906a8ae101a184 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Fri, 15 May 2026 00:55:23 +0200 Subject: [PATCH] feat(playback): improve browser-player diagnostics (#939) * feat(playback): diagnose browser stream access errors Entire-Checkpoint: f957cd9849e0 * feat(playback): expose browser-player diagnostic details Entire-Checkpoint: f957cd9849e0 * fix(playback): keep media path extensions authoritative Entire-Checkpoint: f957cd9849e0 * fix(playback): tighten browser access diagnostics Entire-Checkpoint: f957cd9849e0 * fix(playback): align diagnostic followups Entire-Checkpoint: f957cd9849e0 * fix(playback): narrow declared stream metadata Entire-Checkpoint: f957cd9849e0 * fix(playback): preserve explicit extension overrides Entire-Checkpoint: f957cd9849e0 * fix(playback): keep mpeg-ts out of unsupported containers Entire-Checkpoint: f957cd9849e0 * refactor(playback): split diagnostics utilities Entire-Checkpoint: f957cd9849e0 * fix(playback): treat mpegts early eof as media fallback Entire-Checkpoint: f957cd9849e0 --- apps/web/src/assets/i18n/ar.json | 72 ++-- apps/web/src/assets/i18n/ary.json | 72 ++-- apps/web/src/assets/i18n/by.json | 72 ++-- apps/web/src/assets/i18n/de.json | 72 ++-- apps/web/src/assets/i18n/el.json | 72 ++-- apps/web/src/assets/i18n/en.json | 72 ++-- apps/web/src/assets/i18n/es.json | 72 ++-- apps/web/src/assets/i18n/fr.json | 72 ++-- apps/web/src/assets/i18n/it.json | 72 ++-- apps/web/src/assets/i18n/ja.json | 72 ++-- apps/web/src/assets/i18n/ko.json | 72 ++-- apps/web/src/assets/i18n/nl.json | 72 ++-- apps/web/src/assets/i18n/pl.json | 72 ++-- apps/web/src/assets/i18n/pt.json | 72 ++-- apps/web/src/assets/i18n/ru.json | 72 ++-- apps/web/src/assets/i18n/tr.json | 72 ++-- apps/web/src/assets/i18n/zh.json | 72 ++-- apps/web/src/assets/i18n/zhtw.json | 72 ++-- docs/architecture/embedded-inline-playback.md | 9 + .../lib/art-player/art-player.component.ts | 16 +- .../html-video-player.component.spec.ts | 32 ++ .../html-video-player.component.ts | 28 +- .../playback-diagnostics.model.ts | 97 +++++ .../playback-diagnostics.util.spec.ts | 255 ++++++++++++ .../playback-diagnostics.util.ts | 394 ++++-------------- .../playback-error-patterns.util.ts | 96 +++++ .../playback-media-source.util.ts | 227 ++++++++++ .../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 | 97 +++++ .../web-player-view.component.ts | 103 ++++- 33 files changed, 1936 insertions(+), 842 deletions(-) create mode 100644 libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts create mode 100644 libs/ui/playback/src/lib/playback-diagnostics/playback-error-patterns.util.ts create mode 100644 libs/ui/playback/src/lib/playback-diagnostics/playback-media-source.util.ts diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index ebf0adda4..c60b6a34d 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "السمة الفاتحة", "SYSTEM_THEME": "سمة النظام" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "إخفاء قائمة القنوات", "SHOW_CHANNELS_LIST": "إظهار قائمة القنوات", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index e3ff4dd52..3440e2823 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "المظهر الفاتح", "SYSTEM_THEME": "مظهر النظام" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "إخفاء قائمة القنوات", "SHOW_CHANNELS_LIST": "إظهار قائمة القنوات", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 30fa9963a..136647f34 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Светлая тэма", "SYSTEM_THEME": "Сістэмная тэма" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "Схаваць спіс каналаў", "SHOW_CHANNELS_LIST": "Паказаць спіс каналаў", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 71f1b91b9..16980bfd7 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Hell", "SYSTEM_THEME": "System" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Senderliste einblenden", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 00dfb314f..1fe997e15 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Ανοιχτόχρωμο θέμα", "SYSTEM_THEME": "Θέμα συστήματος" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "Απόκρυψη λίστας καναλιών", "SHOW_CHANNELS_LIST": "Εμφάνιση λίστας καναλιών", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 5670dbdbe..ac94947de 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -366,35 +366,51 @@ "LIGHT_THEME": "Light theme", "SYSTEM_THEME": "System theme" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Show channels list", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 8586fe1e2..ba934cc30 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Tema claro", "SYSTEM_THEME": "Tema del sistema" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Mostrar lista de canales", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index a0afad51c..f5979d846 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Thème clair", "SYSTEM_THEME": "Thème du système" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Afficher la liste des chaînes", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 382246bd3..4a2eec0e4 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Tema chiaro", "SYSTEM_THEME": "Tema di sistema" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Mostra elenco canali", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 3cd55a128..b79b2017d 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "ライトテーマ", "SYSTEM_THEME": "システムテーマ" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "チャンネル一覧を非表示", "SHOW_CHANNELS_LIST": "チャンネル一覧を表示", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 27a42a643..d093647e0 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "밝은 테마", "SYSTEM_THEME": "시스템 테마" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "채널 목록 숨기기", "SHOW_CHANNELS_LIST": "채널 목록 표시", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index baa6d432d..4fbdaffc2 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Licht thema", "SYSTEM_THEME": "Systeemthema" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Zenderlijst tonen", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 78d897df3..00ade7d51 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Jasny motyw", "SYSTEM_THEME": "Motyw systemowy" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Pokaż listę kanałów", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 2d9f91a0f..b1898a703 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Tema claro", "SYSTEM_THEME": "Tema do sistema" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Mostrar lista de canais", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index f840edb02..08fcc32e1 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -364,35 +364,51 @@ "LIGHT_THEME": "Светлая тема", "SYSTEM_THEME": "Системная тема" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "Контейнер потока, вероятно, не поддерживается встроенным браузерным плеером", - "DESCRIPTION": "Chromium/Electron может не воспроизводить этот контейнер внутри приложения. Попробуйте MPV или VLC с более широкой поддержкой кодеков." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "Кодек потока, вероятно, не поддерживается встроенным браузерным плеером", - "DESCRIPTION": "Поток сообщает кодеки, которые встроенный Chromium-плеер может не декодировать. MPV или VLC часто справляются с такими потоками." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "Браузерный плеер не смог декодировать этот поток", - "DESCRIPTION": "Воспроизведение началось, но медиапайплайн завершился ошибкой декодирования. Внешний плеер может обработать этот поток лучше." - }, - "NETWORK_ERROR": { - "TITLE": "Не удалось загрузить поток", - "DESCRIPTION": "Плеер сообщил сетевую ошибку или ошибку загрузки у провайдера. Проверьте URL потока, заголовки или доступность провайдера." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "Поток может быть зашифрован или защищен DRM", - "DESCRIPTION": "Браузерный плеер не может продолжить из-за ошибки ключа или шифрования. Внешний плеер тоже может не помочь, если требуется DRM провайдера." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "TITLE": "Браузерный плеер сообщил ошибку воспроизведения", - "DESCRIPTION": "Поток не воспроизвелся во встроенном плеере, но ошибка не указала конкретную причину в кодеке, контейнере или сети." - }, - "ACTION_OPEN_MPV": "Открыть в MPV", - "ACTION_OPEN_VLC": "Открыть в VLC", - "ACTION_COPY_URL": "Скопировать URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "Контейнер потока, вероятно, не поддерживается встроенным браузерным плеером", + "DESCRIPTION": "Chromium/Electron может не воспроизводить этот контейнер внутри приложения. Попробуйте MPV или VLC с более широкой поддержкой кодеков." }, + "UNSUPPORTED_CODEC": { + "TITLE": "Кодек потока, вероятно, не поддерживается встроенным браузерным плеером", + "DESCRIPTION": "Поток сообщает кодеки, которые встроенный Chromium-плеер может не декодировать. MPV или VLC часто справляются с такими потоками." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "Браузерный плеер не смог декодировать этот поток", + "DESCRIPTION": "Воспроизведение началось, но медиапайплайн завершился ошибкой декодирования. Внешний плеер может обработать этот поток лучше." + }, + "NETWORK_ERROR": { + "TITLE": "Не удалось загрузить поток", + "DESCRIPTION": "Плеер сообщил сетевую ошибку или ошибку загрузки у провайдера. Проверьте URL потока, заголовки или доступность провайдера." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "Браузерный плеер не получил доступ к потоку", + "DESCRIPTION": "Запрос может быть заблокирован CORS, mixed content или требованиями провайдера/прокси к заголовкам. MPV или VLC часто обходят такие ограничения браузерного плеера." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "Поток может быть зашифрован или защищен DRM", + "DESCRIPTION": "Браузерный плеер не может продолжить из-за ошибки ключа или шифрования. Внешний плеер тоже может не помочь, если требуется DRM провайдера." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "TITLE": "Браузерный плеер сообщил ошибку воспроизведения", + "DESCRIPTION": "Поток не воспроизвелся во встроенном плеере, но ошибка не указала конкретную причину в кодеке, контейнере или сети." + }, + "ACTION_OPEN_MPV": "Открыть в MPV", + "ACTION_OPEN_VLC": "Открыть в VLC", + "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": "Скрыть список каналов", "SHOW_CHANNELS_LIST": "Показать список каналов", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 20a1f36f1..de5ed271f 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "Açık tema", "SYSTEM_THEME": "Sistem teması" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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", "SHOW_CHANNELS_LIST": "Kanal listesini göster", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 02da1e7a5..c8e595224 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "明亮模式", "SYSTEM_THEME": "跟随系统" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "隐藏频道列表", "SHOW_CHANNELS_LIST": "显示频道列表", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 9115b80c1..692eabdff 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -360,35 +360,51 @@ "LIGHT_THEME": "亮色主題", "SYSTEM_THEME": "跟隨系統" }, - "PLAYBACK_DIAGNOSTICS": { - "UNSUPPORTED_CONTAINER": { - "TITLE": "This stream container is likely unsupported by the browser player", - "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." - }, - "UNSUPPORTED_CODEC": { - "TITLE": "This stream codec is likely unsupported by the browser player", - "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." - }, - "MEDIA_DECODE_ERROR": { - "TITLE": "The browser player could not decode this stream", - "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." - }, - "NETWORK_ERROR": { - "TITLE": "The stream could not be loaded", - "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." - }, - "DRM_OR_ENCRYPTION": { - "TITLE": "This stream may be encrypted or DRM-protected", - "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." - }, - "UNKNOWN_PLAYBACK_ERROR": { - "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." - }, - "ACTION_OPEN_MPV": "Open in MPV", - "ACTION_OPEN_VLC": "Open in VLC", - "ACTION_COPY_URL": "Copy URL" + "PLAYBACK_DIAGNOSTICS": { + "UNSUPPORTED_CONTAINER": { + "TITLE": "This stream container is likely unsupported by the browser player", + "DESCRIPTION": "Chromium/Electron may not decode this container inline. Try MPV or VLC for broader codec support." }, + "UNSUPPORTED_CODEC": { + "TITLE": "This stream codec is likely unsupported by the browser player", + "DESCRIPTION": "The stream advertises codecs that the built-in Chromium player may not decode. MPV or VLC can often play it." + }, + "MEDIA_DECODE_ERROR": { + "TITLE": "The browser player could not decode this stream", + "DESCRIPTION": "Playback started but the media pipeline failed while decoding. An external player may handle this stream better." + }, + "NETWORK_ERROR": { + "TITLE": "The stream could not be loaded", + "DESCRIPTION": "The player reported a network or provider loading error. Check the stream URL, headers, or provider availability." + }, + "BROWSER_ACCESS_ERROR": { + "TITLE": "The browser player could not access this stream", + "DESCRIPTION": "The request may be blocked by CORS, mixed content, or provider header/proxy restrictions. MPV or VLC can often bypass browser-player access limits." + }, + "DRM_OR_ENCRYPTION": { + "TITLE": "This stream may be encrypted or DRM-protected", + "DESCRIPTION": "The browser player cannot continue with the reported encryption/key error. External playback may still fail if provider DRM is required." + }, + "UNKNOWN_PLAYBACK_ERROR": { + "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." + }, + "ACTION_OPEN_MPV": "Open in MPV", + "ACTION_OPEN_VLC": "Open in VLC", + "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": "隱藏頻道列表", "SHOW_CHANNELS_LIST": "顯示頻道列表", diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index d06f52576..a2e376864 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -153,9 +153,18 @@ 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. + +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. + +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..5168e27b4 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) { @@ -274,6 +279,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { details: data.details, fatal: data.fatal, message: data.error?.message, + error: data.error, }, this.createSourceMetadata(url, 'application/x-mpegURL') ) @@ -308,7 +314,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 +331,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.spec.ts b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.spec.ts index b95cbcc27..da1d25328 100644 --- a/libs/ui/playback/src/lib/html-video-player/html-video-player.component.spec.ts +++ b/libs/ui/playback/src/lib/html-video-player/html-video-player.component.spec.ts @@ -109,4 +109,36 @@ describe('HtmlVideoPlayerComponent', () => { expect(issues).toEqual([]); }); + + it('keeps raw HLS error object context in emitted playback issue details', () => { + const issues: Array<{ details?: string }> = []; + component.playbackIssue.subscribe((issue) => { + if (issue) issues.push(issue); + }); + + ( + component as unknown as { + handleHlsError: ( + url: string, + data: { + type: string; + details: string; + fatal: boolean; + error?: unknown; + } + ) => void; + } + ).handleHlsError('https://example.com/live/playlist.m3u8', { + type: 'networkError', + details: 'manifestLoadError', + fatal: true, + error: { + context: 'xhr setup failed', + status: 0, + }, + }); + + expect(issues[0].details).toContain('xhr setup failed'); + expect(issues[0].details).toContain('"status":0'); + }); }); 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..099bd8e1e 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( { @@ -285,6 +288,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy { details: data.details, fatal: data.fatal, message: data.error?.message, + error: data.error, }, this.createSourceMetadata(url, 'application/x-mpegURL') ) diff --git a/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts b/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts new file mode 100644 index 000000000..bcbb2c9ed --- /dev/null +++ b/libs/ui/playback/src/lib/playback-diagnostics/playback-diagnostics.model.ts @@ -0,0 +1,97 @@ +import type { + ExternalPlayerName, + ResolvedPortalPlayback, +} from 'shared-interfaces'; + +export const PlaybackDiagnosticCode = { + UnsupportedContainer: 'unsupported-container', + UnsupportedCodec: 'unsupported-codec', + MediaDecodeError: 'media-decode-error', + NetworkError: 'network-error', + BrowserAccessError: 'browser-access-error', + DrmOrEncryption: 'drm-or-encryption', + UnknownPlaybackError: 'unknown-playback-error', +} as const; + +export type PlaybackDiagnosticCode = + (typeof PlaybackDiagnosticCode)[keyof typeof PlaybackDiagnosticCode]; + +export const PlaybackDiagnosticSource = { + Source: 'source', + Native: 'native', + Hls: 'hls', + MpegTs: 'mpegts', +} as const; + +export type PlaybackDiagnosticSource = + (typeof PlaybackDiagnosticSource)[keyof typeof PlaybackDiagnosticSource]; + +export const InlinePlaybackPlayer = { + VideoJs: 'videojs', + Html5: 'html5', + ArtPlayer: 'artplayer', +} as const; + +export type InlinePlaybackPlayer = + (typeof InlinePlaybackPlayer)[keyof typeof InlinePlaybackPlayer]; + +export interface PlaybackSourceMetadataInput { + readonly url: string; + readonly mimeType?: string; + readonly player?: InlinePlaybackPlayer; + readonly audioCodecs?: readonly string[]; + readonly videoCodecs?: readonly string[]; +} + +export interface PlaybackSourceMetadata { + readonly url: string; + readonly extension: string; + readonly container: string; + readonly mimeType?: string; + readonly player?: InlinePlaybackPlayer; + readonly audioCodecs: readonly string[]; + readonly videoCodecs: readonly string[]; +} + +export interface NativePlaybackErrorInput { + readonly code?: number; + readonly message?: string; +} + +export interface HlsPlaybackErrorInput { + readonly type?: string; + readonly details?: string; + readonly fatal?: boolean; + readonly message?: string; + readonly error?: unknown; + readonly audioCodecs?: readonly string[]; + readonly videoCodecs?: readonly string[]; +} + +export interface MpegTsPlaybackErrorInput { + readonly type?: string; + readonly details?: string; + readonly message?: string; + readonly info?: unknown; +} + +export interface PlaybackDiagnostic { + readonly code: PlaybackDiagnosticCode; + readonly source: PlaybackDiagnosticSource; + readonly sourceUrl: string; + readonly container: string; + readonly mimeType?: string; + readonly player?: InlinePlaybackPlayer; + readonly audioCodecs: readonly string[]; + readonly videoCodecs: readonly string[]; + readonly details?: string; + readonly nativeErrorCode?: number; + readonly nativeErrorMessage?: string; + readonly externalFallbackRecommended: boolean; +} + +export interface PlaybackFallbackRequest { + readonly player: ExternalPlayerName; + readonly playback: ResolvedPortalPlayback; + readonly diagnostic: PlaybackDiagnostic; +} 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 423d49c75..0dbf6413f 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', () => { @@ -72,6 +74,34 @@ describe('playback diagnostics', () => { expect(unsupportedSourceIssue.externalFallbackRecommended).toBe(true); }); + it('classifies MIME-only unsupported containers as container diagnostics', () => { + const issue = classifyNativePlaybackIssue( + { code: 4, message: 'source not supported' }, + createPlaybackSourceMetadata({ + url: 'https://example.com/archive/stream', + mimeType: 'video/x-msvideo', + player: 'html5', + }) + ); + + expect(issue.code).toBe(PlaybackDiagnosticCode.UnsupportedContainer); + expect(issue.container).toBe('x-msvideo'); + }); + + it('does not classify MPEG-TS MIME-only failures as unsupported containers', () => { + const issue = classifyNativePlaybackIssue( + { code: 4, message: 'source not supported' }, + createPlaybackSourceMetadata({ + url: 'https://example.com/live/stream', + mimeType: 'video/mp2t', + player: 'videojs', + }) + ); + + expect(issue.code).toBe(PlaybackDiagnosticCode.UnsupportedCodec); + expect(issue.container).toBe('mp2t'); + }); + it('classifies HLS network errors without claiming codec incompatibility', () => { const issue = classifyHlsPlaybackIssue( { @@ -89,6 +119,150 @@ describe('playback diagnostics', () => { expect(issue.externalFallbackRecommended).toBe(false); }); + it('does not treat provider-side blocked messages as browser access errors', () => { + const issue = classifyHlsPlaybackIssue( + { + type: 'networkError', + details: 'manifestLoadError Request blocked by rate limiter', + fatal: true, + }, + createPlaybackSourceMetadata({ + url: 'https://provider.example/live/index.m3u8', + player: 'videojs', + }) + ); + + expect(issue.code).toBe(PlaybackDiagnosticCode.NetworkError); + expect(issue.externalFallbackRecommended).toBe(false); + }); + + it('keeps raw HLS error object context in diagnostic details', () => { + const issue = classifyHlsPlaybackIssue( + { + type: 'networkError', + details: 'manifestLoadError', + fatal: true, + error: { + context: 'xhr setup failed', + status: 0, + }, + }, + createPlaybackSourceMetadata({ + url: 'https://provider.example/live/index.m3u8', + player: 'videojs', + }) + ); + + expect(issue.details).toContain('xhr setup failed'); + expect(issue.details).toContain('"status":0'); + }); + + it('classifies HLS browser access blocks separately from provider network failures', () => { + const issue = classifyHlsPlaybackIssue( + { + type: 'networkError', + details: + 'manifestLoadError Mixed Content: The page at https://app.example was loaded over HTTPS, but requested an insecure stream http://provider.example/live.m3u8. This request has been blocked.', + fatal: true, + }, + createPlaybackSourceMetadata({ + url: 'http://provider.example/live.m3u8', + player: 'videojs', + }) + ); + + expect(issue.code).toBe('browser-access-error'); + 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( + { + code: 2, + message: + 'Access to media at https://provider.example/live.m3u8 from origin app://iptvnator has been blocked by CORS policy: No Access-Control-Allow-Origin header is present.', + }, + createPlaybackSourceMetadata({ + url: 'https://provider.example/live.m3u8', + player: 'html5', + }) + ); + + expect(issue.code).toBe('browser-access-error'); + expect(issue.externalFallbackRecommended).toBe(true); + }); + + it('keeps opaque native network failures generic when the browser hides access details', () => { + const issue = classifyNativePlaybackIssue( + { + code: 2, + message: '', + }, + createPlaybackSourceMetadata({ + url: 'https://provider.example/live.m3u8', + player: 'html5', + }) + ); + + expect(issue.code).toBe(PlaybackDiagnosticCode.NetworkError); + expect(issue.externalFallbackRecommended).toBe(false); + }); + + it('classifies mpegts browser fetch restrictions separately from generic network errors', () => { + const issue = classifyMpegTsPlaybackIssue( + { + type: 'NetworkError', + details: + 'Fetch blocked by access-control policy while loading segment', + }, + createPlaybackSourceMetadata({ + url: 'https://provider.example/live/channel.ts', + mimeType: 'video/mp2t', + player: 'videojs', + }) + ); + + expect(issue.code).toBe('browser-access-error'); + expect(issue.externalFallbackRecommended).toBe(true); + }); + + it('classifies mpegts early EOF failures as fallback-actionable media errors', () => { + const issue = classifyMpegTsPlaybackIssue( + { + type: 'NetworkError', + details: 'UnrecoverableEarlyEof', + info: { msg: 'Fetch stream meet Early-EOF' }, + }, + createPlaybackSourceMetadata({ + url: 'https://provider.example/movie/123.ts', + mimeType: 'video/mp2t', + player: 'videojs', + }) + ); + + expect(issue.code).toBe(PlaybackDiagnosticCode.MediaDecodeError); + expect(issue.externalFallbackRecommended).toBe(true); + expect(issue.details).toContain('Early-EOF'); + }); + it('classifies mpegts codec errors as unsupported codec fallbacks', () => { const issue = classifyMpegTsPlaybackIssue( { @@ -125,4 +299,85 @@ 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?container=m3u8&stream=123' + ) + ).toBe('m3u8'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?ext=ts&stream=123' + ) + ).toBe('ts'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?stream=123' + ) + ).toBe(''); + }); + + it('keeps trusted media path extensions ahead of generic query metadata', () => { + expect( + getPlaybackMediaExtensionFromUrl( + 'http://cdn.example/archive/movie.mp4?container=m3u8' + ) + ).toBe('mp4'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://cdn.example/live/channel.m3u8?format=mp4' + ) + ).toBe('m3u8'); + }); + + it('keeps explicit extension query metadata ahead of trusted media path extensions', () => { + expect( + getPlaybackMediaExtensionFromUrl( + 'http://cdn.example/live/channel.ts?extension=m3u8' + ) + ).toBe('m3u8'); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://cdn.example/live/channel.m3u8?ext=ts' + ) + ).toBe('ts'); + }); + + it('does not infer media extensions from generic type/output query metadata', () => { + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?type=m3u8&stream=123' + ) + ).toBe(''); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?output=ts&stream=123' + ) + ).toBe(''); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.php?format=mpv&stream=123' + ) + ).toBe(''); + expect( + getPlaybackMediaExtensionFromUrl( + 'http://portal.example/play/live.mpv?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 d30008a03..8cf24499c 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 @@ -1,157 +1,55 @@ -import { getExtensionFromUrl } from 'm3u-utils'; -import type { ExternalPlayerName, ResolvedPortalPlayback } from 'shared-interfaces'; +import type { + HlsPlaybackErrorInput, + MpegTsPlaybackErrorInput, + NativePlaybackErrorInput, + PlaybackDiagnostic, + PlaybackDiagnosticCode, + PlaybackDiagnosticSource, + PlaybackSourceMetadata, +} from './playback-diagnostics.model'; +import { PlaybackDiagnosticCode as DiagnosticCode } from './playback-diagnostics.model'; +import { PlaybackDiagnosticSource as DiagnosticSource } from './playback-diagnostics.model'; +import { + isBrowserAccessFailure, + isCodecFailure, + isDrmOrEncryptionFailure, + isEarlyEofFailure, + isNetworkFailure, + normalizeErrorDetails, +} from './playback-error-patterns.util'; +import { + isLikelyContainerIssue, + mergeCodecMetadata, +} from './playback-media-source.util'; -export const PlaybackDiagnosticCode = { - UnsupportedContainer: 'unsupported-container', - UnsupportedCodec: 'unsupported-codec', - MediaDecodeError: 'media-decode-error', - NetworkError: 'network-error', - DrmOrEncryption: 'drm-or-encryption', - UnknownPlaybackError: 'unknown-playback-error', -} as const; - -export type PlaybackDiagnosticCode = - (typeof PlaybackDiagnosticCode)[keyof typeof PlaybackDiagnosticCode]; - -export const PlaybackDiagnosticSource = { - Source: 'source', - Native: 'native', - Hls: 'hls', - MpegTs: 'mpegts', -} as const; - -export type PlaybackDiagnosticSource = - (typeof PlaybackDiagnosticSource)[keyof typeof PlaybackDiagnosticSource]; - -export const InlinePlaybackPlayer = { - VideoJs: 'videojs', - Html5: 'html5', - ArtPlayer: 'artplayer', -} as const; - -export type InlinePlaybackPlayer = - (typeof InlinePlaybackPlayer)[keyof typeof InlinePlaybackPlayer]; - -export interface PlaybackSourceMetadataInput { - readonly url: string; - readonly mimeType?: string; - readonly player?: InlinePlaybackPlayer; - readonly audioCodecs?: readonly string[]; - readonly videoCodecs?: readonly string[]; -} - -export interface PlaybackSourceMetadata { - readonly url: string; - readonly extension: string; - readonly container: string; - readonly mimeType?: string; - readonly player?: InlinePlaybackPlayer; - readonly audioCodecs: readonly string[]; - readonly videoCodecs: readonly string[]; -} - -export interface NativePlaybackErrorInput { - readonly code?: number; - readonly message?: string; -} - -export interface HlsPlaybackErrorInput { - readonly type?: string; - readonly details?: string; - readonly fatal?: boolean; - readonly message?: string; - readonly error?: unknown; - readonly audioCodecs?: readonly string[]; - readonly videoCodecs?: readonly string[]; -} - -export interface MpegTsPlaybackErrorInput { - readonly type?: string; - readonly details?: string; - readonly message?: string; - readonly info?: unknown; -} - -export interface PlaybackDiagnostic { - readonly code: PlaybackDiagnosticCode; - readonly source: PlaybackDiagnosticSource; - readonly sourceUrl: string; - readonly container: string; - readonly mimeType?: string; - readonly player?: InlinePlaybackPlayer; - readonly audioCodecs: readonly string[]; - readonly videoCodecs: readonly string[]; - readonly details?: string; - readonly nativeErrorCode?: number; - readonly nativeErrorMessage?: string; - readonly externalFallbackRecommended: boolean; -} - -export interface PlaybackFallbackRequest { - readonly player: ExternalPlayerName; - readonly playback: ResolvedPortalPlayback; - readonly diagnostic: PlaybackDiagnostic; -} - -const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([ - 'avi', - 'asf', - 'divx', - 'flv', - 'm2ts', - 'm4v', - 'mkv', - 'mov', - 'mpeg', - 'mpg', - 'rm', - 'rmvb', - 'ts', - 'vob', - 'wmv', -]); - -const NON_MEDIA_URL_EXTENSIONS = new Set([ - 'asp', - 'aspx', - 'cgi', - 'jsp', - 'php', - 'pl', -]); +export * from './playback-diagnostics.model'; +export { + createPlaybackSourceMetadata, + getLikelyBrowserUnsupportedCodecLabels, + getPlaybackMediaExtensionFromUrl, +} from './playback-media-source.util'; const SOURCE_NOT_SUPPORTED_CODE = 4; const DECODE_ERROR_CODE = 3; const NETWORK_ERROR_CODE = 2; -export function createPlaybackSourceMetadata( - input: PlaybackSourceMetadataInput -): PlaybackSourceMetadata { - const extension = getMediaExtensionFromUrl(input.url); - const mimeType = input.mimeType?.trim() || undefined; - - return { - url: input.url, - extension, - container: extension || inferContainerFromMimeType(mimeType), - mimeType, - player: input.player, - audioCodecs: normalizeCodecs(input.audioCodecs), - videoCodecs: normalizeCodecs(input.videoCodecs), - }; -} - export function classifyNativePlaybackIssue( error: NativePlaybackErrorInput | MediaError | null | undefined, metadata: PlaybackSourceMetadata ): PlaybackDiagnostic { const nativeErrorCode = error?.code; const nativeErrorMessage = error?.message || undefined; + const lowerNativeErrorMessage = nativeErrorMessage?.toLowerCase() ?? ''; if (nativeErrorCode === NETWORK_ERROR_CODE) { + // Native MediaError details are often opaque for browser security + // failures. Only classify browser access when the runtime exposes a + // concrete CORS/mixed-content/CSP-style message. return createDiagnostic({ - code: PlaybackDiagnosticCode.NetworkError, - source: PlaybackDiagnosticSource.Native, + code: isBrowserAccessFailure(lowerNativeErrorMessage) + ? DiagnosticCode.BrowserAccessError + : DiagnosticCode.NetworkError, + source: DiagnosticSource.Native, metadata, nativeErrorCode, nativeErrorMessage, @@ -160,8 +58,8 @@ export function classifyNativePlaybackIssue( if (nativeErrorCode === DECODE_ERROR_CODE) { return createDiagnostic({ - code: PlaybackDiagnosticCode.MediaDecodeError, - source: PlaybackDiagnosticSource.Native, + code: DiagnosticCode.MediaDecodeError, + source: DiagnosticSource.Native, metadata, nativeErrorCode, nativeErrorMessage, @@ -171,9 +69,9 @@ export function classifyNativePlaybackIssue( if (nativeErrorCode === SOURCE_NOT_SUPPORTED_CODE) { return createDiagnostic({ code: isLikelyContainerIssue(metadata) - ? PlaybackDiagnosticCode.UnsupportedContainer - : PlaybackDiagnosticCode.UnsupportedCodec, - source: PlaybackDiagnosticSource.Native, + ? DiagnosticCode.UnsupportedContainer + : DiagnosticCode.UnsupportedCodec, + source: DiagnosticSource.Native, metadata, nativeErrorCode, nativeErrorMessage, @@ -181,8 +79,8 @@ export function classifyNativePlaybackIssue( } return createDiagnostic({ - code: PlaybackDiagnosticCode.UnknownPlaybackError, - source: PlaybackDiagnosticSource.Native, + code: DiagnosticCode.UnknownPlaybackError, + source: DiagnosticSource.Native, metadata, nativeErrorCode, nativeErrorMessage, @@ -203,8 +101,10 @@ export function classifyHlsPlaybackIssue( if (isNetworkFailure(lowerType, lowerDetails)) { return createDiagnostic({ - code: PlaybackDiagnosticCode.NetworkError, - source: PlaybackDiagnosticSource.Hls, + code: isBrowserAccessFailure(lowerDetails) + ? DiagnosticCode.BrowserAccessError + : DiagnosticCode.NetworkError, + source: DiagnosticSource.Hls, metadata: mergedMetadata, details, }); @@ -212,8 +112,8 @@ export function classifyHlsPlaybackIssue( if (isDrmOrEncryptionFailure(lowerDetails)) { return createDiagnostic({ - code: PlaybackDiagnosticCode.DrmOrEncryption, - source: PlaybackDiagnosticSource.Hls, + code: DiagnosticCode.DrmOrEncryption, + source: DiagnosticSource.Hls, metadata: mergedMetadata, details, }); @@ -221,8 +121,8 @@ export function classifyHlsPlaybackIssue( if (isCodecFailure(lowerDetails)) { return createDiagnostic({ - code: PlaybackDiagnosticCode.UnsupportedCodec, - source: PlaybackDiagnosticSource.Hls, + code: DiagnosticCode.UnsupportedCodec, + source: DiagnosticSource.Hls, metadata: mergedMetadata, details, }); @@ -230,16 +130,16 @@ export function classifyHlsPlaybackIssue( if (lowerType.includes('media') || lowerType.includes('mux')) { return createDiagnostic({ - code: PlaybackDiagnosticCode.MediaDecodeError, - source: PlaybackDiagnosticSource.Hls, + code: DiagnosticCode.MediaDecodeError, + source: DiagnosticSource.Hls, metadata: mergedMetadata, details, }); } return createDiagnostic({ - code: PlaybackDiagnosticCode.UnknownPlaybackError, - source: PlaybackDiagnosticSource.Hls, + code: DiagnosticCode.UnknownPlaybackError, + source: DiagnosticSource.Hls, metadata: mergedMetadata, details, }); @@ -253,10 +153,21 @@ export function classifyMpegTsPlaybackIssue( const lowerDetails = details.toLowerCase(); const lowerType = (error.type ?? '').toLowerCase(); + if (isEarlyEofFailure(lowerDetails)) { + return createDiagnostic({ + code: DiagnosticCode.MediaDecodeError, + source: DiagnosticSource.MpegTs, + metadata, + details, + }); + } + if (isNetworkFailure(lowerType, lowerDetails)) { return createDiagnostic({ - code: PlaybackDiagnosticCode.NetworkError, - source: PlaybackDiagnosticSource.MpegTs, + code: isBrowserAccessFailure(lowerDetails) + ? DiagnosticCode.BrowserAccessError + : DiagnosticCode.NetworkError, + source: DiagnosticSource.MpegTs, metadata, details, }); @@ -264,8 +175,8 @@ export function classifyMpegTsPlaybackIssue( if (lowerDetails.includes('codec')) { return createDiagnostic({ - code: PlaybackDiagnosticCode.UnsupportedCodec, - source: PlaybackDiagnosticSource.MpegTs, + code: DiagnosticCode.UnsupportedCodec, + source: DiagnosticSource.MpegTs, metadata, details, }); @@ -273,8 +184,8 @@ export function classifyMpegTsPlaybackIssue( if (lowerDetails.includes('format') || lowerDetails.includes('mse')) { return createDiagnostic({ - code: PlaybackDiagnosticCode.UnsupportedContainer, - source: PlaybackDiagnosticSource.MpegTs, + code: DiagnosticCode.UnsupportedContainer, + source: DiagnosticSource.MpegTs, metadata, details, }); @@ -282,16 +193,16 @@ export function classifyMpegTsPlaybackIssue( if (lowerType.includes('media')) { return createDiagnostic({ - code: PlaybackDiagnosticCode.MediaDecodeError, - source: PlaybackDiagnosticSource.MpegTs, + code: DiagnosticCode.MediaDecodeError, + source: DiagnosticSource.MpegTs, metadata, details, }); } return createDiagnostic({ - code: PlaybackDiagnosticCode.UnknownPlaybackError, - source: PlaybackDiagnosticSource.MpegTs, + code: DiagnosticCode.UnknownPlaybackError, + source: DiagnosticSource.MpegTs, metadata, details, }); @@ -312,8 +223,8 @@ export function classifyUnsupportedHlsManifestCodecs( } return createDiagnostic({ - code: PlaybackDiagnosticCode.UnsupportedCodec, - source: PlaybackDiagnosticSource.Source, + code: DiagnosticCode.UnsupportedCodec, + source: DiagnosticSource.Source, metadata, details: mimeType, }); @@ -354,148 +265,11 @@ function createDiagnostic(options: { function isExternalFallbackRecommended(code: PlaybackDiagnosticCode): boolean { return ( - code === PlaybackDiagnosticCode.UnsupportedContainer || - code === PlaybackDiagnosticCode.UnsupportedCodec || - code === PlaybackDiagnosticCode.MediaDecodeError || - code === PlaybackDiagnosticCode.DrmOrEncryption - ); -} - -function mergeCodecMetadata( - metadata: PlaybackSourceMetadata, - codecs: Pick -): PlaybackSourceMetadata { - const audioCodecs = normalizeCodecs([ - ...metadata.audioCodecs, - ...(codecs.audioCodecs ?? []), - ]); - const videoCodecs = normalizeCodecs([ - ...metadata.videoCodecs, - ...(codecs.videoCodecs ?? []), - ]); - - return { - ...metadata, - audioCodecs, - videoCodecs, - }; -} - -function normalizeCodecs(codecs: readonly string[] | undefined): string[] { - return Array.from( - new Set( - (codecs ?? []) - .map((codec) => codec.trim()) - .filter((codec) => codec.length > 0) - ) - ); -} - -function normalizeToken(value: string | undefined): string { - return value?.trim().toLowerCase() ?? ''; -} - -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 ''; - } - - return NON_MEDIA_URL_EXTENSIONS.has(declaredExtension) - ? '' - : declaredExtension; - } catch { - return ''; - } -} - -function inferContainerFromMimeType(mimeType: string | undefined): string { - if (!mimeType) { - return ''; - } - - const subtype = mimeType.split(';')[0]?.split('/')[1] ?? ''; - return subtype.toLowerCase(); -} - -function isLikelyContainerIssue(metadata: PlaybackSourceMetadata): boolean { - return ( - UNSUPPORTED_CONTAINER_EXTENSIONS.has(metadata.extension) || - metadata.mimeType === 'video/matroska' - ); -} - -function normalizeErrorDetails( - error: HlsPlaybackErrorInput | MpegTsPlaybackErrorInput -): string { - const hlsError = 'error' in error ? error.error : undefined; - const mpegTsInfo = 'info' in error ? error.info : undefined; - const errorMessage = - hlsError instanceof Error - ? hlsError.message - : typeof hlsError === 'string' - ? hlsError - : ''; - const info = - typeof mpegTsInfo === 'string' - ? mpegTsInfo - : mpegTsInfo - ? JSON.stringify(mpegTsInfo) - : ''; - - return [error.details, error.message, errorMessage, info] - .filter((part): part is string => Boolean(part)) - .join(' '); -} - -function isNetworkFailure(type: string, details: string): boolean { - return ( - type.includes('network') || - details.includes('network') || - details.includes('loaderror') || - details.includes('timeout') || - details.includes('status') - ); -} - -function isCodecFailure(details: string): boolean { - return ( - details.includes('codec') || - details.includes('incompatiblecodecs') || - details.includes('addcodec') - ); -} - -function isDrmOrEncryptionFailure(details: string): boolean { - return ( - details.includes('decrypt') || - details.includes('keysystem') || - details.includes('keyload') || - details.includes('license') || - details.includes('drm') + code === DiagnosticCode.UnsupportedContainer || + code === DiagnosticCode.UnsupportedCodec || + code === DiagnosticCode.MediaDecodeError || + code === DiagnosticCode.BrowserAccessError || + code === DiagnosticCode.DrmOrEncryption ); } diff --git a/libs/ui/playback/src/lib/playback-diagnostics/playback-error-patterns.util.ts b/libs/ui/playback/src/lib/playback-diagnostics/playback-error-patterns.util.ts new file mode 100644 index 000000000..22f3b9724 --- /dev/null +++ b/libs/ui/playback/src/lib/playback-diagnostics/playback-error-patterns.util.ts @@ -0,0 +1,96 @@ +import type { + HlsPlaybackErrorInput, + MpegTsPlaybackErrorInput, +} from './playback-diagnostics.model'; + +export function normalizeErrorDetails( + error: HlsPlaybackErrorInput | MpegTsPlaybackErrorInput +): string { + const hlsError = 'error' in error ? error.error : undefined; + const mpegTsInfo = 'info' in error ? error.info : undefined; + const errorMessage = normalizeErrorPayload(hlsError); + const info = normalizeErrorPayload(mpegTsInfo); + + return [error.details, error.message, errorMessage, info] + .filter((part): part is string => Boolean(part)) + .join(' '); +} + +export function isNetworkFailure(type: string, details: string): boolean { + return ( + type.includes('network') || + details.includes('network') || + details.includes('loaderror') || + details.includes('timeout') || + details.includes('status') + ); +} + +export function isBrowserAccessFailure(details: string): boolean { + return ( + details.includes('cors') || + details.includes('cross-origin') || + 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 content security') || + details.includes('blocked by cors') || + details.includes('blocked by mixed content') || + details.includes('not allowed to load local resource') || + details.includes('err_blocked') || + details.includes('err_cleartext') + ); +} + +export function isEarlyEofFailure(details: string): boolean { + const compactDetails = details.replace(/[^a-z0-9]/g, ''); + return compactDetails.includes('earlyeof'); +} + +export function isCodecFailure(details: string): boolean { + return ( + details.includes('codec') || + details.includes('incompatiblecodecs') || + details.includes('addcodec') + ); +} + +export function isDrmOrEncryptionFailure(details: string): boolean { + return ( + details.includes('decrypt') || + details.includes('keysystem') || + details.includes('keyload') || + details.includes('license') || + details.includes('drm') + ); +} + +function normalizeErrorPayload(payload: unknown): string { + if (!payload) { + return ''; + } + + if (typeof payload === 'string') { + return payload; + } + + if (payload instanceof Error) { + const extraDetails = stringifyUnknown(payload); + return [payload.message, extraDetails === '{}' ? '' : extraDetails] + .filter(Boolean) + .join(' '); + } + + return stringifyUnknown(payload); +} + +function stringifyUnknown(value: unknown): string { + try { + return JSON.stringify(value) || ''; + } catch { + return String(value); + } +} diff --git a/libs/ui/playback/src/lib/playback-diagnostics/playback-media-source.util.ts b/libs/ui/playback/src/lib/playback-diagnostics/playback-media-source.util.ts new file mode 100644 index 000000000..3915ac822 --- /dev/null +++ b/libs/ui/playback/src/lib/playback-diagnostics/playback-media-source.util.ts @@ -0,0 +1,227 @@ +import { getExtensionFromUrl } from 'm3u-utils'; + +import type { + PlaybackSourceMetadata, + PlaybackSourceMetadataInput, +} from './playback-diagnostics.model'; + +const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([ + 'avi', + 'asf', + 'divx', + 'flv', + 'm2ts', + 'm4v', + 'mkv', + 'mov', + 'mpeg', + 'mpg', + 'rm', + 'rmvb', + 'ts', + 'vob', + 'wmv', +]); + +const UNSUPPORTED_CONTAINER_NAMES = new Set([ + ...UNSUPPORTED_CONTAINER_EXTENSIONS, + 'matroska', + 'quicktime', + 'x-matroska', + 'x-msvideo', +]); + +const EXPLICIT_MEDIA_EXTENSION_QUERY_KEYS = ['extension', 'ext']; + +const DECLARED_MEDIA_FORMAT_QUERY_KEYS = ['format', 'container']; + +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', + 'oga', + 'ogg', + 'ogv', + 'webm', +]); + +const NON_MEDIA_URL_EXTENSIONS = new Set([ + 'asp', + 'aspx', + 'cgi', + 'jsp', + 'mpv', + 'php', + 'pl', +]); + +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 = getPlaybackMediaExtensionFromUrl(input.url); + const mimeType = input.mimeType?.trim() || undefined; + + return { + url: input.url, + extension, + container: extension || inferContainerFromMimeType(mimeType), + mimeType, + player: input.player, + audioCodecs: normalizeCodecs(input.audioCodecs), + videoCodecs: normalizeCodecs(input.videoCodecs), + }; +} + +export function getPlaybackMediaExtensionFromUrl(url: string): string { + const explicitQueryExtension = getMediaExtensionFromQuery( + url, + EXPLICIT_MEDIA_EXTENSION_QUERY_KEYS + ); + if (explicitQueryExtension) { + return explicitQueryExtension; + } + + const pathExtension = normalizeExtensionToken(getExtensionFromUrl(url)); + if (DECLARED_MEDIA_EXTENSIONS.has(pathExtension)) { + return pathExtension; + } + + const formatQueryExtension = getMediaExtensionFromQuery( + url, + DECLARED_MEDIA_FORMAT_QUERY_KEYS + ); + if (formatQueryExtension) { + return formatQueryExtension; + } + + 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 isLikelyContainerIssue( + metadata: PlaybackSourceMetadata +): boolean { + return ( + UNSUPPORTED_CONTAINER_NAMES.has(metadata.extension) || + UNSUPPORTED_CONTAINER_NAMES.has(metadata.container) || + metadata.mimeType === 'video/matroska' + ); +} + +export function mergeCodecMetadata( + metadata: PlaybackSourceMetadata, + codecs: Pick +): PlaybackSourceMetadata { + const audioCodecs = normalizeCodecs([ + ...metadata.audioCodecs, + ...(codecs.audioCodecs ?? []), + ]); + const videoCodecs = normalizeCodecs([ + ...metadata.videoCodecs, + ...(codecs.videoCodecs ?? []), + ]); + + return { + ...metadata, + audioCodecs, + videoCodecs, + }; +} + +function inferContainerFromMimeType(mimeType: string | undefined): string { + if (!mimeType) { + return ''; + } + + const subtype = mimeType.split(';')[0]?.split('/')[1] ?? ''; + return subtype.toLowerCase(); +} + +function normalizeCodecs(codecs: readonly string[] | undefined): string[] { + return Array.from( + new Set( + (codecs ?? []) + .map((codec) => codec.trim()) + .filter((codec) => codec.length > 0) + ) + ); +} + +function normalizeToken(value: string | undefined): string { + return value?.trim().toLowerCase() ?? ''; +} + +function normalizeExtensionToken(value: string | undefined): string { + return normalizeToken(value).replace(/^\.+/, ''); +} + +function getMediaExtensionFromQuery( + url: string, + queryKeys: readonly string[] +): string { + try { + const parsedUrl = new URL(url, 'http://iptvnator.local'); + for (const key of queryKeys) { + const declaredExtension = normalizeDeclaredMediaExtension( + parsedUrl.searchParams.get(key) ?? undefined + ); + if (declaredExtension) { + return 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 : ''; +} 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 d37a4c1be..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 @@ -187,6 +187,73 @@ 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(); + + expect(component.getDiagnosticTitleKey(issue)).toBe( + 'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR.TITLE' + ); + expect(component.getDiagnosticDescriptionKey(issue)).toBe( + '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 { @@ -202,3 +269,33 @@ function createUnsupportedContainerDiagnostic(): PlaybackDiagnostic { externalFallbackRecommended: true, }; } + +function createBrowserAccessDiagnostic(): PlaybackDiagnostic { + return { + code: PlaybackDiagnosticCode.BrowserAccessError, + source: PlaybackDiagnosticSource.Hls, + sourceUrl: 'https://provider.example/live.m3u8', + container: 'm3u8', + mimeType: 'application/x-mpegURL', + player: 'videojs', + audioCodecs: [], + videoCodecs: [], + details: 'blocked by CORS policy', + 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 45b4982c7..98b5e82ec 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 @@ -1,13 +1,12 @@ import { Component, - EventEmitter, - Output, Signal, ViewEncapsulation, computed, effect, inject, input, + output, signal, } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; @@ -17,7 +16,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 +30,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', @@ -65,12 +70,11 @@ export class WebPlayerViewComponent { volume = input(1); showCaptions = input(false); playerOverride = input(null); - @Output() timeUpdate = new EventEmitter<{ + readonly timeUpdate = output<{ currentTime: number; duration: number; }>(); - @Output() externalFallbackRequested = - new EventEmitter(); + readonly externalFallbackRequested = output(); settings = toSignal( this.storage.get(STORE_KEY.Settings) @@ -118,7 +122,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 +172,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 { @@ -181,6 +236,8 @@ export class WebPlayerViewComponent { return 'PLAYBACK_DIAGNOSTICS.MEDIA_DECODE_ERROR'; case PlaybackDiagnosticCode.NetworkError: return 'PLAYBACK_DIAGNOSTICS.NETWORK_ERROR'; + case PlaybackDiagnosticCode.BrowserAccessError: + return 'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR'; case PlaybackDiagnosticCode.DrmOrEncryption: return 'PLAYBACK_DIAGNOSTICS.DRM_OR_ENCRYPTION'; case PlaybackDiagnosticCode.UnknownPlaybackError: @@ -192,4 +249,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; + } + } }