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
This commit is contained in:
4gray authored and GitHub committed 2026-05-15 00:55:23 +02:00
1 parent 8232a86a1e
commit 4a4334cb64
33 files changed
+1936 -842

No files matched your search

+44 -28
View File
@@ -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": "إظهار قائمة القنوات",
+44 -28
View File
@@ -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": "إظهار قائمة القنوات",
+44 -28
View File
@@ -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": "Паказаць спіс каналаў",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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": "Εμφάνιση λίστας καναλιών",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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": "チャンネル一覧を表示",
+44 -28
View File
@@ -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": "채널 목록 표시",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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": "Показать список каналов",
+44 -28
View File
@@ -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",
+44 -28
View File
@@ -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": "显示频道列表",
+44 -28
View File
@@ -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": "顯示頻道列表",
@@ -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.
@@ -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';
@@ -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');
});
});
@@ -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')
)
@@ -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;
}
@@ -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']);
});
});
@@ -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<PlaybackSourceMetadataInput, 'audioCodecs' | 'videoCodecs'>
): 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
);
}
@@ -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);
}
}
@@ -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<PlaybackSourceMetadata, 'audioCodecs' | 'videoCodecs'>
): 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<PlaybackSourceMetadataInput, 'audioCodecs' | 'videoCodecs'>
): 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 : '';
}
@@ -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', () => {
@@ -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();
}
@@ -61,6 +61,37 @@
{{ meta }}
</div>
}
@if (getDiagnosticCodecHint(issue); as codecHint) {
<div
class="web-player-diagnostic__codec-hint"
data-test-id="playback-codec-hint"
>
{{
'PLAYBACK_DIAGNOSTICS.CODEC_HINT'
| translate: { codecs: codecHint }
}}
</div>
}
<details
class="web-player-diagnostic__details"
data-test-id="playback-diagnostic-details"
>
<summary>
<mat-icon aria-hidden="true">info</mat-icon>
{{ 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' | translate }}
</summary>
<dl>
@for (
detail of getDiagnosticDetails(issue);
track detail.labelKey
) {
<div class="web-player-diagnostic__detail-row">
<dt>{{ detail.labelKey | translate }}</dt>
<dd>{{ detail.value }}</dd>
</div>
}
</dl>
</details>
</div>
<div class="web-player-diagnostic__actions">
@if (canShowExternalFallbackActions()) {
@@ -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;
}
}
@@ -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,
};
}
@@ -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<number>(1);
showCaptions = input<boolean>(false);
playerOverride = input<VideoPlayer | null>(null);
@Output() timeUpdate = new EventEmitter<{
readonly timeUpdate = output<{
currentTime: number;
duration: number;
}>();
@Output() externalFallbackRequested =
new EventEmitter<PlaybackFallbackRequest>();
readonly externalFallbackRequested = output<PlaybackFallbackRequest>();
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;
}
}
}