mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
feat(playback): expose browser-player diagnostic details
Entire-Checkpoint: f957cd9849e0
This commit is contained in:
1 parent
96a010b166
commit
da04f4c9e9
29 files changed
+696
-65
No files matched your search
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "إخفاء قائمة القنوات",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "إخفاء قائمة القنوات",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Схаваць спіс каналаў",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Senderliste ausblenden",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Απόκρυψη λίστας καναλιών",
|
||||
|
||||
@@ -397,7 +397,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Hide channels list",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Ocultar lista de canales",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Masquer la liste des chaînes",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Nascondi elenco canali",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "チャンネル一覧を非表示",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "채널 목록 숨기기",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Zenderlijst verbergen",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Ukryj listę kanałów",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Ocultar lista de canais",
|
||||
|
||||
@@ -395,7 +395,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Открыть в MPV",
|
||||
"ACTION_OPEN_VLC": "Открыть в VLC",
|
||||
"ACTION_COPY_URL": "Скопировать URL"
|
||||
"ACTION_COPY_URL": "Скопировать URL",
|
||||
"DETAILS_SUMMARY": "Технические сведения",
|
||||
"CODEC_HINT": "Обнаружены {{codecs}}. Браузерные плееры Chromium/Electron часто имеют ограниченную поддержку этих кодеков.",
|
||||
"DETAIL_CODE": "Код диагностики",
|
||||
"DETAIL_PLAYER": "Плеер",
|
||||
"DETAIL_SOURCE": "Источник",
|
||||
"DETAIL_CONTAINER": "Контейнер",
|
||||
"DETAIL_MIME_TYPE": "MIME-тип",
|
||||
"DETAIL_VIDEO_CODECS": "Видео кодеки",
|
||||
"DETAIL_AUDIO_CODECS": "Аудио кодеки",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Код ошибки браузера",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Сообщение браузера",
|
||||
"DETAIL_ERROR_DETAILS": "Детали ошибки"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Скрыть список каналов",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "Kanal listesini gizle",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "隐藏频道列表",
|
||||
|
||||
@@ -391,7 +391,19 @@
|
||||
},
|
||||
"ACTION_OPEN_MPV": "Open in MPV",
|
||||
"ACTION_OPEN_VLC": "Open in VLC",
|
||||
"ACTION_COPY_URL": "Copy URL"
|
||||
"ACTION_COPY_URL": "Copy URL",
|
||||
"DETAILS_SUMMARY": "Technical details",
|
||||
"CODEC_HINT": "Detected {{codecs}}. Chromium/Electron browser players often have limited support for these codecs.",
|
||||
"DETAIL_CODE": "Diagnostic code",
|
||||
"DETAIL_PLAYER": "Player",
|
||||
"DETAIL_SOURCE": "Source",
|
||||
"DETAIL_CONTAINER": "Container",
|
||||
"DETAIL_MIME_TYPE": "MIME type",
|
||||
"DETAIL_VIDEO_CODECS": "Video codecs",
|
||||
"DETAIL_AUDIO_CODECS": "Audio codecs",
|
||||
"DETAIL_NATIVE_ERROR_CODE": "Native error code",
|
||||
"DETAIL_NATIVE_ERROR_MESSAGE": "Native error message",
|
||||
"DETAIL_ERROR_DETAILS": "Error details"
|
||||
},
|
||||
"LAYOUT": {
|
||||
"HIDE_CHANNELS_LIST": "隱藏頻道列表",
|
||||
|
||||
@@ -153,9 +153,16 @@ Supported diagnostic codes are:
|
||||
- `unsupported-codec`
|
||||
- `media-decode-error`
|
||||
- `network-error`
|
||||
- `browser-access-error`
|
||||
- `drm-or-encryption`
|
||||
- `unknown-playback-error`
|
||||
|
||||
`network-error` is reserved for provider/network loading failures. Browser security failures such as CORS, mixed content, Content Security Policy, and private-network-access blocks are classified as `browser-access-error` so the UI can explain that the browser player was blocked before playback reached decoding.
|
||||
|
||||
The banner keeps the primary message compact, then exposes technical details on demand: diagnostic code, reporting player/source, detected container/MIME, video/audio codecs, native browser error fields, and raw HLS/mpegts details. HLS manifest codec metadata also drives a concise browser-support hint for codecs that Chromium/Electron commonly cannot decode inline, such as HEVC, AC-3, E-AC-3, DTS, and MPEG-2 video.
|
||||
|
||||
URL extension metadata is filtered before diagnostics and player selection use it. Web script extensions such as `.php` are not shown as stream containers; explicit media query metadata such as `extension=ts` or `format=m3u8` is preferred when present.
|
||||
|
||||
When a diagnostic is actionable in Electron, the inline banner may offer `Open in MPV`, `Open in VLC`, and `Copy URL`. Web builds only expose copy/help text. MPV/VLC fallback requests carry the original `ResolvedPortalPlayback` payload so headers, referer, origin, user-agent, content metadata, and resume offset stay intact.
|
||||
|
||||
`PortalPlayer.openExternalPlayback(playback, player)` is the forced external launch API. It sends the playback payload to MPV or VLC regardless of the current saved player setting, so fallback buttons do not mutate preferences.
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from '@angular/core';
|
||||
import Artplayer from 'artplayer';
|
||||
import Hls, { type ErrorData, type ManifestParsedData } from 'hls.js';
|
||||
import { getStreamExtensionFromUrl } from 'm3u-utils';
|
||||
import mpegts from 'mpegts.js';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import {
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
classifyNativePlaybackIssue,
|
||||
classifyUnsupportedHlsManifestCodecs,
|
||||
createPlaybackSourceMetadata,
|
||||
getPlaybackMediaExtensionFromUrl,
|
||||
} from '../playback-diagnostics/playback-diagnostics.util';
|
||||
|
||||
type AudioTrackSelector = {
|
||||
@@ -131,7 +131,9 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
const el = this.elementRef.nativeElement.querySelector(
|
||||
'.artplayer-container'
|
||||
);
|
||||
const extension = getStreamExtensionFromUrl(this.channel?.url ?? '');
|
||||
const extension = getPlaybackMediaExtensionFromUrl(
|
||||
this.channel?.url ?? ''
|
||||
);
|
||||
const isLive = extension === 'm3u8' || extension === 'ts' || !extension;
|
||||
|
||||
this.player = new Artplayer({
|
||||
@@ -225,7 +227,10 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
'error',
|
||||
this.handleNativePlaybackError
|
||||
);
|
||||
this.player.video.addEventListener('loadeddata', this.clearPlaybackIssue);
|
||||
this.player.video.addEventListener(
|
||||
'loadeddata',
|
||||
this.clearPlaybackIssue
|
||||
);
|
||||
this.player.video.addEventListener('playing', this.clearPlaybackIssue);
|
||||
|
||||
if (this.startTime > 0) {
|
||||
@@ -308,7 +313,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
const selectedLabel =
|
||||
typeof item.html === 'string'
|
||||
? item.html
|
||||
: item.html.textContent ?? '';
|
||||
: (item.html.textContent ?? '');
|
||||
const selectedIndex = tracks.findIndex(
|
||||
(t, i) =>
|
||||
(t.name || t.lang || `Track ${i + 1}`) ===
|
||||
@@ -325,7 +330,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
}
|
||||
|
||||
private getVideoType(url: string): string {
|
||||
const extension = getStreamExtensionFromUrl(url);
|
||||
const extension = getPlaybackMediaExtensionFromUrl(url);
|
||||
switch (extension) {
|
||||
case 'mkv':
|
||||
return 'video/matroska';
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
classifyMpegTsPlaybackIssue,
|
||||
classifyNativePlaybackIssue,
|
||||
createPlaybackSourceMetadata,
|
||||
getLikelyBrowserUnsupportedCodecLabels,
|
||||
getPlaybackMediaExtensionFromUrl,
|
||||
} from './playback-diagnostics.util';
|
||||
|
||||
describe('playback diagnostics', () => {
|
||||
@@ -107,6 +109,24 @@ describe('playback diagnostics', () => {
|
||||
expect(issue.externalFallbackRecommended).toBe(true);
|
||||
});
|
||||
|
||||
it('classifies browser security policy blocks as browser access errors', () => {
|
||||
const issue = classifyHlsPlaybackIssue(
|
||||
{
|
||||
type: 'networkError',
|
||||
details:
|
||||
'manifestLoadError Refused to connect because it violates the following Content Security Policy directive: "connect-src"',
|
||||
fatal: true,
|
||||
},
|
||||
createPlaybackSourceMetadata({
|
||||
url: 'http://provider.example/live.m3u8',
|
||||
player: 'videojs',
|
||||
})
|
||||
);
|
||||
|
||||
expect(issue.code).toBe(PlaybackDiagnosticCode.BrowserAccessError);
|
||||
expect(issue.externalFallbackRecommended).toBe(true);
|
||||
});
|
||||
|
||||
it('classifies native CORS failures as browser access errors', () => {
|
||||
const issue = classifyNativePlaybackIssue(
|
||||
{
|
||||
@@ -178,4 +198,36 @@ describe('playback diagnostics', () => {
|
||||
expect(metadata.extension).toBe('');
|
||||
expect(metadata.container).toBe('');
|
||||
});
|
||||
|
||||
it('uses declared media query metadata without exposing web script extensions', () => {
|
||||
expect(
|
||||
getPlaybackMediaExtensionFromUrl(
|
||||
'http://portal.example/play/live.php?format=m3u8&stream=123'
|
||||
)
|
||||
).toBe('m3u8');
|
||||
expect(
|
||||
getPlaybackMediaExtensionFromUrl(
|
||||
'http://portal.example/play/live.php?type=m3u8&stream=123'
|
||||
)
|
||||
).toBe('m3u8');
|
||||
expect(
|
||||
getPlaybackMediaExtensionFromUrl(
|
||||
'http://portal.example/play/live.php?output=ts&stream=123'
|
||||
)
|
||||
).toBe('ts');
|
||||
expect(
|
||||
getPlaybackMediaExtensionFromUrl(
|
||||
'http://portal.example/play/live.php?stream=123'
|
||||
)
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('detects codecs with limited Chromium browser-player support', () => {
|
||||
expect(
|
||||
getLikelyBrowserUnsupportedCodecLabels({
|
||||
audioCodecs: ['mp4a.40.2', 'ac-3', 'ec-3'],
|
||||
videoCodecs: ['avc1.64001f', 'hvc1.1.6.L93.B0'],
|
||||
})
|
||||
).toEqual(['HEVC', 'AC-3', 'E-AC-3']);
|
||||
});
|
||||
});
|
||||
@@ -115,6 +115,38 @@ const UNSUPPORTED_CONTAINER_EXTENSIONS = new Set([
|
||||
'wmv',
|
||||
]);
|
||||
|
||||
const DECLARED_MEDIA_EXTENSION_QUERY_KEYS = [
|
||||
'extension',
|
||||
'ext',
|
||||
'format',
|
||||
'container',
|
||||
'type',
|
||||
'output',
|
||||
];
|
||||
|
||||
const DECLARED_MEDIA_EXTENSION_ALIASES = new Map([
|
||||
['hls', 'm3u8'],
|
||||
['mpegts', 'ts'],
|
||||
['mpeg-ts', 'ts'],
|
||||
]);
|
||||
|
||||
const DECLARED_MEDIA_EXTENSIONS = new Set([
|
||||
...UNSUPPORTED_CONTAINER_EXTENSIONS,
|
||||
'aac',
|
||||
'flac',
|
||||
'm3u',
|
||||
'm3u8',
|
||||
'm4s',
|
||||
'mp3',
|
||||
'mp4',
|
||||
'mpd',
|
||||
'mpv',
|
||||
'oga',
|
||||
'ogg',
|
||||
'ogv',
|
||||
'webm',
|
||||
]);
|
||||
|
||||
const NON_MEDIA_URL_EXTENSIONS = new Set([
|
||||
'asp',
|
||||
'aspx',
|
||||
@@ -128,10 +160,21 @@ const SOURCE_NOT_SUPPORTED_CODE = 4;
|
||||
const DECODE_ERROR_CODE = 3;
|
||||
const NETWORK_ERROR_CODE = 2;
|
||||
|
||||
const BROWSER_LIMITED_CODEC_PATTERNS: ReadonlyArray<{
|
||||
readonly label: string;
|
||||
readonly pattern: RegExp;
|
||||
}> = [
|
||||
{ label: 'HEVC', pattern: /^(hev1|hvc1|hevc|h265)/ },
|
||||
{ label: 'AC-3', pattern: /^(ac-?3|dac3)$/ },
|
||||
{ label: 'E-AC-3', pattern: /^(ec-?3|eac-?3|dec3)$/ },
|
||||
{ label: 'DTS', pattern: /^(dts|dtsc|dtse|dtsh|dtsl)/ },
|
||||
{ label: 'MPEG-2 Video', pattern: /^(mp2v|mpeg2video)/ },
|
||||
];
|
||||
|
||||
export function createPlaybackSourceMetadata(
|
||||
input: PlaybackSourceMetadataInput
|
||||
): PlaybackSourceMetadata {
|
||||
const extension = getMediaExtensionFromUrl(input.url);
|
||||
const extension = getPlaybackMediaExtensionFromUrl(input.url);
|
||||
const mimeType = input.mimeType?.trim() || undefined;
|
||||
|
||||
return {
|
||||
@@ -145,6 +188,32 @@ export function createPlaybackSourceMetadata(
|
||||
};
|
||||
}
|
||||
|
||||
export function getPlaybackMediaExtensionFromUrl(url: string): string {
|
||||
const queryExtension = getMediaExtensionFromQuery(url);
|
||||
if (queryExtension) {
|
||||
return queryExtension;
|
||||
}
|
||||
|
||||
const pathExtension = normalizeExtensionToken(getExtensionFromUrl(url));
|
||||
if (NON_MEDIA_URL_EXTENSIONS.has(pathExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return pathExtension;
|
||||
}
|
||||
|
||||
export function getLikelyBrowserUnsupportedCodecLabels(
|
||||
metadata: Pick<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 classifyNativePlaybackIssue(
|
||||
error: NativePlaybackErrorInput | MediaError | null | undefined,
|
||||
metadata: PlaybackSourceMetadata
|
||||
@@ -411,39 +480,36 @@ function normalizeExtensionToken(value: string | undefined): string {
|
||||
return normalizeToken(value).replace(/^\.+/, '');
|
||||
}
|
||||
|
||||
function getMediaExtensionFromUrl(url: string): string {
|
||||
const queryExtension = getMediaExtensionFromQuery(url);
|
||||
if (queryExtension) {
|
||||
return queryExtension;
|
||||
}
|
||||
|
||||
const pathExtension = normalizeExtensionToken(getExtensionFromUrl(url));
|
||||
if (NON_MEDIA_URL_EXTENSIONS.has(pathExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return pathExtension;
|
||||
}
|
||||
|
||||
function getMediaExtensionFromQuery(url: string): string {
|
||||
try {
|
||||
const parsedUrl = new URL(url, 'http://iptvnator.local');
|
||||
const declaredExtension = normalizeExtensionToken(
|
||||
parsedUrl.searchParams.get('extension') ?? undefined
|
||||
);
|
||||
|
||||
if (!declaredExtension) {
|
||||
return '';
|
||||
for (const key of DECLARED_MEDIA_EXTENSION_QUERY_KEYS) {
|
||||
const declaredExtension = normalizeDeclaredMediaExtension(
|
||||
parsedUrl.searchParams.get(key) ?? undefined
|
||||
);
|
||||
if (declaredExtension) {
|
||||
return declaredExtension;
|
||||
}
|
||||
}
|
||||
|
||||
return NON_MEDIA_URL_EXTENSIONS.has(declaredExtension)
|
||||
? ''
|
||||
: declaredExtension;
|
||||
return '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDeclaredMediaExtension(value: string | undefined): string {
|
||||
const extension = normalizeExtensionToken(value);
|
||||
const normalized =
|
||||
DECLARED_MEDIA_EXTENSION_ALIASES.get(extension) ?? extension;
|
||||
|
||||
if (!normalized || NON_MEDIA_URL_EXTENSIONS.has(normalized)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return DECLARED_MEDIA_EXTENSIONS.has(normalized) ? normalized : '';
|
||||
}
|
||||
|
||||
function inferContainerFromMimeType(mimeType: string | undefined): string {
|
||||
if (!mimeType) {
|
||||
return '';
|
||||
@@ -500,7 +566,9 @@ function isBrowserAccessFailure(details: string): boolean {
|
||||
details.includes('cross origin') ||
|
||||
details.includes('access-control') ||
|
||||
details.includes('access control') ||
|
||||
details.includes('content security policy') ||
|
||||
details.includes('mixed content') ||
|
||||
details.includes('private network access') ||
|
||||
details.includes('blocked by') ||
|
||||
details.includes('has been blocked') ||
|
||||
details.includes('not allowed to load local resource') ||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,19 @@ describe('WebPlayerViewComponent', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('treats web script playback URLs without declared media extension as MPEG-TS', () => {
|
||||
const streamUrl = 'https://example.com/live.php?stream=123&token=x';
|
||||
|
||||
component.setVjsOptions(streamUrl);
|
||||
|
||||
expect(component.vjsOptions.sources).toEqual([
|
||||
{
|
||||
src: streamUrl,
|
||||
type: 'video/mp2t',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses browser access diagnostic translation keys', () => {
|
||||
const issue = createBrowserAccessDiagnostic();
|
||||
|
||||
@@ -198,6 +211,49 @@ describe('WebPlayerViewComponent', () => {
|
||||
'PLAYBACK_DIAGNOSTICS.BROWSER_ACCESS_ERROR.DESCRIPTION'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders technical details and codec-specific hints in the diagnostic banner', () => {
|
||||
fixture.detectChanges();
|
||||
const issue = createUnsupportedCodecDiagnostic();
|
||||
|
||||
component.handlePlaybackIssue(issue);
|
||||
fixture.detectChanges();
|
||||
|
||||
const details = fixture.debugElement.query(
|
||||
By.css('[data-test-id="playback-diagnostic-details"]')
|
||||
);
|
||||
const codecHint = fixture.debugElement.query(
|
||||
By.css('[data-test-id="playback-codec-hint"]')
|
||||
);
|
||||
|
||||
expect(details.nativeElement.textContent).toContain(
|
||||
'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY'
|
||||
);
|
||||
expect(component.getDiagnosticCodecHint(issue)).toBe('HEVC, AC-3');
|
||||
expect(component.getDiagnosticDetails(issue)).toEqual(
|
||||
expect.arrayContaining([
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CODE',
|
||||
value: 'unsupported-codec',
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_PLAYER',
|
||||
value: 'Video.js',
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_VIDEO_CODECS',
|
||||
value: 'hvc1.1.6.L93.B0',
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_AUDIO_CODECS',
|
||||
value: 'ac-3',
|
||||
},
|
||||
])
|
||||
);
|
||||
expect(codecHint.nativeElement.textContent).toContain(
|
||||
'PLAYBACK_DIAGNOSTICS.CODEC_HINT'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function createUnsupportedContainerDiagnostic(): PlaybackDiagnostic {
|
||||
@@ -228,3 +284,18 @@ function createBrowserAccessDiagnostic(): PlaybackDiagnostic {
|
||||
externalFallbackRecommended: true,
|
||||
};
|
||||
}
|
||||
|
||||
function createUnsupportedCodecDiagnostic(): PlaybackDiagnostic {
|
||||
return {
|
||||
code: PlaybackDiagnosticCode.UnsupportedCodec,
|
||||
source: PlaybackDiagnosticSource.Hls,
|
||||
sourceUrl: 'https://example.com/live/index.m3u8',
|
||||
container: 'm3u8',
|
||||
mimeType: 'application/x-mpegURL',
|
||||
player: 'videojs',
|
||||
audioCodecs: ['ac-3'],
|
||||
videoCodecs: ['hvc1.1.6.L93.B0'],
|
||||
details: 'manifestIncompatibleCodecsError',
|
||||
externalFallbackRecommended: true,
|
||||
};
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { getStreamExtensionFromUrl } from 'm3u-utils';
|
||||
import {
|
||||
ResolvedPortalPlayback,
|
||||
Settings,
|
||||
@@ -32,9 +31,16 @@ import {
|
||||
type PlaybackDiagnostic,
|
||||
PlaybackDiagnosticCode,
|
||||
type PlaybackFallbackRequest,
|
||||
getLikelyBrowserUnsupportedCodecLabels,
|
||||
getPlaybackMediaExtensionFromUrl,
|
||||
} from '../playback-diagnostics/playback-diagnostics.util';
|
||||
import { VjsPlayerComponent } from '../vjs-player/vjs-player.component';
|
||||
|
||||
type PlaybackDiagnosticDetail = {
|
||||
readonly labelKey: string;
|
||||
readonly value: string;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-web-player-view',
|
||||
templateUrl: './web-player-view.component.html',
|
||||
@@ -118,7 +124,7 @@ export class WebPlayerViewComponent {
|
||||
}
|
||||
|
||||
setVjsOptions(streamUrl: string) {
|
||||
const extension = getStreamExtensionFromUrl(streamUrl);
|
||||
const extension = getPlaybackMediaExtensionFromUrl(streamUrl);
|
||||
const mimeType =
|
||||
extension === 'm3u' || extension === 'm3u8'
|
||||
? 'application/x-mpegURL'
|
||||
@@ -168,7 +174,58 @@ export class WebPlayerViewComponent {
|
||||
return codecs;
|
||||
}
|
||||
|
||||
return issue.container || issue.mimeType || issue.details || '';
|
||||
return issue.container || issue.mimeType || '';
|
||||
}
|
||||
|
||||
getDiagnosticCodecHint(issue: PlaybackDiagnostic): string {
|
||||
return getLikelyBrowserUnsupportedCodecLabels(issue).join(', ');
|
||||
}
|
||||
|
||||
getDiagnosticDetails(
|
||||
issue: PlaybackDiagnostic
|
||||
): readonly PlaybackDiagnosticDetail[] {
|
||||
return [
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CODE',
|
||||
value: issue.code,
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_PLAYER',
|
||||
value: this.formatPlayer(issue.player),
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_SOURCE',
|
||||
value: this.formatDiagnosticSource(issue.source),
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_CONTAINER',
|
||||
value: issue.container,
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_MIME_TYPE',
|
||||
value: issue.mimeType ?? '',
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_VIDEO_CODECS',
|
||||
value: issue.videoCodecs.join(', '),
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_AUDIO_CODECS',
|
||||
value: issue.audioCodecs.join(', '),
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_NATIVE_ERROR_CODE',
|
||||
value: issue.nativeErrorCode?.toString() ?? '',
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_NATIVE_ERROR_MESSAGE',
|
||||
value: issue.nativeErrorMessage ?? '',
|
||||
},
|
||||
{
|
||||
labelKey: 'PLAYBACK_DIAGNOSTICS.DETAIL_ERROR_DETAILS',
|
||||
value: issue.details ?? '',
|
||||
},
|
||||
].filter(({ value }) => value.trim().length > 0);
|
||||
}
|
||||
|
||||
private getDiagnosticTranslationBase(issue: PlaybackDiagnostic): string {
|
||||
@@ -194,4 +251,34 @@ export class WebPlayerViewComponent {
|
||||
private detectDesktop(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.electron;
|
||||
}
|
||||
|
||||
private formatPlayer(player: PlaybackDiagnostic['player']): string {
|
||||
switch (player) {
|
||||
case 'videojs':
|
||||
return 'Video.js';
|
||||
case 'html5':
|
||||
return 'HTML5';
|
||||
case 'artplayer':
|
||||
return 'ArtPlayer';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private formatDiagnosticSource(
|
||||
source: PlaybackDiagnostic['source']
|
||||
): string {
|
||||
switch (source) {
|
||||
case 'hls':
|
||||
return 'HLS.js';
|
||||
case 'mpegts':
|
||||
return 'mpegts.js';
|
||||
case 'native':
|
||||
return 'Native media element';
|
||||
case 'source':
|
||||
return 'Stream metadata';
|
||||
default:
|
||||
return source;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user