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