- {{ getDiagnosticTitleKey(issue) | translate }}
+
+
+ warning
+ {{ getDiagnosticTitleKey(issue) | translate }}
+
+
+ {{ diagnosticHeadlineKey() | translate }}
+
+
{{ getDiagnosticDescriptionKey(issue) | translate }}
+
@if (getDiagnosticMeta(issue); as meta) {
{{ meta }}
}
+
@if (getDiagnosticCodecHint(issue); as codecHint) {
}
-
-
- info
- {{ 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' | translate }}
-
-
- @for (
- detail of getDiagnosticDetails(issue);
- track detail.labelKey
- ) {
-
-
- {{ detail.labelKey | translate }}
- - {{ detail.value }}
-
- }
-
-
-
-
+
@if (canShowExternalFallbackActions()) {
-
-
+
+
+
+
}
-
+
+
+
+
+
+
+
+
+
+
+
+
+ expand_more
+ {{ 'PLAYBACK_DIAGNOSTICS.DETAILS_SUMMARY' | translate }}
+
+
+ @for (
+ detail of getDiagnosticDetails(issue);
+ track detail.labelKey
+ ) {
+
+
- {{ detail.labelKey | translate }}
+ - {{ detail.value }}
+
+ }
+
+
+
}
diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss
index 27e24553d..996c6b19a 100644
--- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss
+++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.scss
@@ -1,4 +1,5 @@
.web-player-view {
+ container-type: inline-size;
display: block;
height: 100%;
position: relative;
@@ -15,94 +16,235 @@ app-html-video-player {
.web-player-diagnostic {
position: absolute;
- inset-inline: 16px;
- bottom: var(--web-player-diagnostic-bottom, 16px);
+ inset: 0;
z-index: 5;
- display: grid;
- grid-template-areas:
- 'icon body'
- 'icon actions';
- grid-template-columns: auto minmax(0, 1fr);
- gap: 8px 12px;
- align-items: start;
- padding: 14px 16px;
- max-block-size: calc(100% - 32px);
- overflow: auto;
- border: 1px solid color-mix(in srgb, var(--mat-sys-error) 36%, transparent);
- border-radius: 8px;
- background: color-mix(
- in srgb,
- var(--mat-sys-error-container) 92%,
- transparent
- );
- color: var(--mat-sys-on-error-container);
- box-shadow: 0 16px 42px rgb(0 0 0 / 28%);
-}
-
-.web-player-diagnostic__icon {
- grid-area: icon;
- display: inline-flex;
+ display: flex;
align-items: center;
- justify-content: center;
- padding-top: 2px;
+ padding: clamp(24px, 9vh, 92px) clamp(20px, 18vw, 216px);
+ overflow: auto;
+ background:
+ radial-gradient(
+ circle at 18% 18%,
+ rgb(255 176 76 / 6%),
+ transparent 28%
+ ),
+ rgb(0 0 0 / 96%);
+ color: #f6f6f6;
}
-.web-player-diagnostic__body {
- grid-area: body;
- max-width: 640px;
+.web-player-diagnostic__content {
+ width: min(100%, 720px);
}
-.web-player-diagnostic__title {
- font-size: 0.92rem;
- font-weight: 650;
- line-height: 1.2;
+.web-player-diagnostic__badge {
+ display: inline-flex;
+ max-width: 100%;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 11px;
+ border: 1px solid rgb(255 174 66 / 42%);
+ border-radius: 6px;
+ background: rgb(255 148 35 / 15%);
+ color: #ffb24c;
+ font-size: 0.78rem;
+ font-weight: 800;
+ letter-spacing: 0;
+ line-height: 1.1;
+ text-transform: uppercase;
}
-.web-player-diagnostic__description,
-.web-player-diagnostic__meta,
-.web-player-diagnostic__codec-hint {
- margin-top: 3px;
- font-size: 0.82rem;
- line-height: 1.3;
+.web-player-diagnostic__badge mat-icon {
+ width: 16px;
+ height: 16px;
+ flex: 0 0 16px;
+ font-size: 16px;
+}
+
+.web-player-diagnostic__badge span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.web-player-diagnostic__headline {
+ max-width: 660px;
+ margin: 20px 0 0;
+ color: #fff;
+ font-size: 1.65rem;
+ font-weight: 760;
+ letter-spacing: 0;
+ line-height: 1.05;
+}
+
+.web-player-diagnostic__description {
+ max-width: 760px;
+ margin-top: 12px;
+ color: rgb(255 255 255 / 73%);
+ font-size: 1rem;
+ line-height: 1.48;
}
.web-player-diagnostic__meta {
- opacity: 0.78;
+ margin-top: 9px;
+ color: rgb(255 255 255 / 48%);
+ font-family: var(--app-monospace-font, monospace);
+ font-size: 0.82rem;
+ line-height: 1.35;
overflow-wrap: anywhere;
}
.web-player-diagnostic__codec-hint {
display: inline-block;
max-width: 100%;
- margin-top: 8px;
+ margin-top: 12px;
padding: 4px 8px;
- border: 1px solid
- color-mix(in srgb, var(--mat-sys-on-error-container) 18%, transparent);
+ border: 1px solid rgb(255 255 255 / 12%);
border-radius: 6px;
- background: color-mix(
- in srgb,
- var(--mat-sys-on-error-container) 7%,
- transparent
- );
- font-weight: 550;
+ background: rgb(255 255 255 / 5%);
+ color: rgb(255 255 255 / 68%);
+ font-size: 0.78rem;
+ font-weight: 620;
+ line-height: 1.3;
overflow-wrap: anywhere;
}
+.web-player-diagnostic__player-actions {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(168px, 192px));
+ gap: 12px;
+ margin-top: 28px;
+}
+
+.web-player-diagnostic__player-card {
+ display: grid;
+ min-height: 60px;
+ grid-template-columns: 22px minmax(0, 1fr);
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ border: 1px solid rgb(255 255 255 / 13%);
+ border-radius: 8px;
+ background: rgb(0 0 0 / 22%);
+ color: #f5f5f5;
+ cursor: pointer;
+ text-align: left;
+ transition:
+ background-color 140ms ease,
+ border-color 140ms ease,
+ color 140ms ease,
+ transform 140ms ease;
+}
+
+.web-player-diagnostic__player-card:hover {
+ border-color: rgb(255 255 255 / 28%);
+ background: rgb(255 255 255 / 6%);
+}
+
+.web-player-diagnostic__player-card:active {
+ transform: translateY(1px);
+}
+
+.web-player-diagnostic__player-card:focus-visible,
+.web-player-diagnostic__utility-button:focus-visible,
+.web-player-diagnostic__details summary:focus-visible {
+ outline: 2px solid #ffb24c;
+ outline-offset: 3px;
+}
+
+.web-player-diagnostic__player-card--primary {
+ border-color: transparent;
+ background: #fff;
+ color: #151515;
+}
+
+.web-player-diagnostic__player-card--primary:hover {
+ border-color: transparent;
+ background: rgb(255 255 255 / 88%);
+}
+
+.web-player-diagnostic__player-card mat-icon {
+ width: 20px;
+ height: 20px;
+ font-size: 20px;
+}
+
+.web-player-diagnostic__player-copy {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.web-player-diagnostic__player-label {
+ overflow: hidden;
+ font-size: 0.95rem;
+ font-weight: 760;
+ line-height: 1.05;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.web-player-diagnostic__player-hint {
+ overflow: hidden;
+ color: currentColor;
+ font-size: 0.78rem;
+ line-height: 1.1;
+ opacity: 0.62;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.web-player-diagnostic__utilities {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ gap: 8px 12px;
+ margin-top: 22px;
+ color: rgb(255 255 255 / 45%);
+}
+
+.web-player-diagnostic__utility-button,
+.web-player-diagnostic__details summary {
+ display: inline-flex;
+ min-height: 24px;
+ align-items: center;
+ gap: 6px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.86rem;
+ font-weight: 640;
+ line-height: 1.2;
+}
+
+.web-player-diagnostic__utility-button:hover,
+.web-player-diagnostic__details summary:hover {
+ color: rgb(255 255 255 / 76%);
+}
+
+.web-player-diagnostic__utility-button mat-icon,
+.web-player-diagnostic__details summary mat-icon {
+ width: 16px;
+ height: 16px;
+ font-size: 16px;
+}
+
+.web-player-diagnostic__divider {
+ width: 1px;
+ height: 18px;
+ margin-top: 3px;
+ background: rgb(255 255 255 / 16%);
+}
+
.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;
+ min-width: 0;
+ max-width: 100%;
}
.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;
}
@@ -110,19 +252,23 @@ app-html-video-player {
display: none;
}
-.web-player-diagnostic__details summary mat-icon {
- width: 16px;
- height: 16px;
- font-size: 16px;
+.web-player-diagnostic__details[open] {
+ flex-basis: 100%;
}
.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;
+ gap: 7px 14px;
+ max-width: min(100%, 640px);
+ margin: 12px 0 0;
+ padding: 12px;
+ border: 1px solid rgb(255 255 255 / 10%);
+ border-radius: 8px;
+ background: rgb(255 255 255 / 5%);
+ color: rgb(255 255 255 / 68%);
+ font-size: 0.78rem;
+ line-height: 1.3;
}
.web-player-diagnostic__detail-row {
@@ -130,12 +276,8 @@ app-html-video-player {
}
.web-player-diagnostic__detail-row dt {
- color: color-mix(
- in srgb,
- var(--mat-sys-on-error-container) 76%,
- transparent
- );
- font-weight: 650;
+ color: rgb(255 255 255 / 48%);
+ font-weight: 700;
white-space: nowrap;
}
@@ -145,34 +287,32 @@ app-html-video-player {
overflow-wrap: anywhere;
}
-.web-player-diagnostic__actions {
- grid-area: actions;
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-start;
- gap: 8px;
-}
-
-.web-player-diagnostic__actions button {
- white-space: nowrap;
-}
-
-@media (max-width: 760px) {
+@container (max-width: 760px) {
.web-player-diagnostic {
- inset-inline: 8px;
- bottom: var(--web-player-diagnostic-bottom, 8px);
- max-block-size: calc(100% - 16px);
- grid-template-areas:
- 'icon body'
- 'actions actions';
+ align-items: flex-start;
+ padding: 22px 16px;
}
- .web-player-diagnostic__actions {
- justify-content: flex-start;
+ .web-player-diagnostic__badge {
+ white-space: normal;
+ }
+
+ .web-player-diagnostic__badge span {
+ white-space: normal;
+ }
+
+ .web-player-diagnostic__headline {
+ margin-top: 16px;
+ font-size: 1.48rem;
+ }
+
+ .web-player-diagnostic__utilities {
+ gap: 10px 12px;
}
.web-player-diagnostic__details dl {
grid-template-columns: minmax(0, 1fr);
+ max-width: 100%;
}
.web-player-diagnostic__detail-row {
@@ -184,3 +324,116 @@ app-html-video-player {
white-space: normal;
}
}
+
+@container (max-width: 520px) {
+ .web-player-diagnostic {
+ align-items: flex-start;
+ padding: clamp(14px, 5cqi, 22px);
+ }
+
+ .web-player-diagnostic__content {
+ width: 100%;
+ }
+
+ .web-player-diagnostic__badge {
+ gap: 6px;
+ padding: 4px 8px;
+ font-size: 0.7rem;
+ white-space: normal;
+ }
+
+ .web-player-diagnostic__badge span {
+ white-space: nowrap;
+ }
+
+ .web-player-diagnostic__headline {
+ margin-top: 14px;
+ font-size: 1.35rem;
+ line-height: 1.08;
+ }
+
+ .web-player-diagnostic__description {
+ margin-top: 10px;
+ font-size: 0.9rem;
+ line-height: 1.38;
+ }
+
+ .web-player-diagnostic__meta {
+ margin-top: 8px;
+ font-size: 0.74rem;
+ }
+
+ .web-player-diagnostic__codec-hint {
+ margin-top: 10px;
+ font-size: 0.72rem;
+ }
+
+ .web-player-diagnostic__player-actions {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 8px;
+ margin-top: 16px;
+ }
+
+ .web-player-diagnostic__player-card {
+ min-height: 52px;
+ padding: 10px 12px;
+ }
+
+ .web-player-diagnostic__utilities {
+ display: grid;
+ gap: 7px;
+ margin-top: 16px;
+ }
+
+ .web-player-diagnostic__divider {
+ display: none;
+ }
+
+ .web-player-diagnostic__utility-button,
+ .web-player-diagnostic__details summary {
+ width: fit-content;
+ min-height: 22px;
+ font-size: 0.82rem;
+ }
+
+ .web-player-diagnostic__details[open] {
+ flex-basis: auto;
+ }
+
+ .web-player-diagnostic__details dl {
+ max-height: 260px;
+ overflow: auto;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 6px;
+ margin-top: 8px;
+ padding: 10px;
+ font-size: 0.72rem;
+ }
+
+ .web-player-diagnostic__detail-row {
+ display: grid;
+ gap: 2px;
+ }
+
+ .web-player-diagnostic__detail-row dt {
+ white-space: normal;
+ }
+}
+
+@container (max-width: 340px) {
+ .web-player-diagnostic {
+ padding: 12px;
+ }
+
+ .web-player-diagnostic__headline {
+ font-size: 1.22rem;
+ }
+
+ .web-player-diagnostic__description {
+ font-size: 0.84rem;
+ }
+
+ .web-player-diagnostic__player-label {
+ font-size: 0.88rem;
+ }
+}
diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts
index 558016329..7e70e8642 100644
--- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts
+++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.spec.ts
@@ -158,6 +158,9 @@ describe('WebPlayerViewComponent', () => {
expect(banner.nativeElement.textContent).toContain(
'PLAYBACK_DIAGNOSTICS.UNSUPPORTED_CONTAINER.TITLE'
);
+ expect(banner.nativeElement.textContent).toContain(
+ 'PLAYBACK_DIAGNOSTICS.NATIVE_FALLBACK_TITLE'
+ );
mpvButton.nativeElement.click();
expect(requests).toEqual([
@@ -231,6 +234,69 @@ describe('WebPlayerViewComponent', () => {
);
});
+ it('preserves playback HTTP metadata for channel-based players', () => {
+ const streamUrl = 'https://example.com/live/channel.m3u8';
+ fixture.componentRef.setInput('playerOverride', VideoPlayer.Html5Player);
+ fixture.componentRef.setInput('playback', {
+ streamUrl,
+ title: 'Header Locked Channel',
+ userAgent: 'ProviderAgent/1.0',
+ referer: 'https://provider.example/ref',
+ origin: 'https://provider.example',
+ headers: {
+ 'User-Agent': 'IgnoredFallbackAgent/1.0',
+ Referer: 'https://ignored.example/ref',
+ Origin: 'https://ignored.example',
+ },
+ });
+
+ fixture.detectChanges();
+
+ const player = fixture.debugElement.query(
+ By.directive(StubHtmlVideoPlayerComponent)
+ ).componentInstance as StubHtmlVideoPlayerComponent;
+ expect(player.channel()).toEqual(
+ expect.objectContaining({
+ url: streamUrl,
+ name: 'Header Locked Channel',
+ http: {
+ 'user-agent': 'ProviderAgent/1.0',
+ referrer: 'https://provider.example/ref',
+ origin: 'https://provider.example',
+ },
+ })
+ );
+ });
+
+ it('falls back to playback headers when explicit HTTP metadata is absent', () => {
+ const streamUrl = 'https://example.com/live/channel.m3u8';
+ fixture.componentRef.setInput('playerOverride', VideoPlayer.Html5Player);
+ fixture.componentRef.setInput('playback', {
+ streamUrl,
+ title: 'Header Fallback Channel',
+ headers: {
+ 'user-agent': 'HeaderAgent/1.0',
+ referer: 'https://headers.example/ref',
+ origin: 'https://headers.example',
+ },
+ });
+
+ fixture.detectChanges();
+
+ const player = fixture.debugElement.query(
+ By.directive(StubHtmlVideoPlayerComponent)
+ ).componentInstance as StubHtmlVideoPlayerComponent;
+ expect(player.channel()).toEqual(
+ expect.objectContaining({
+ http: {
+ 'user-agent': 'HeaderAgent/1.0',
+ referrer: 'https://headers.example/ref',
+ origin: 'https://headers.example',
+ },
+ })
+ );
+ });
+
it('uses browser access diagnostic translation keys', () => {
const issue = createBrowserAccessDiagnostic();
@@ -242,6 +308,27 @@ describe('WebPlayerViewComponent', () => {
);
});
+ it('uses an inline recovery headline when external fallback actions are unavailable', () => {
+ fixture.detectChanges();
+ component.handlePlaybackIssue(createNetworkDiagnostic());
+ fixture.detectChanges();
+
+ const banner = fixture.debugElement.query(
+ By.css('[data-test-id="playback-diagnostic-banner"]')
+ );
+ const mpvButton = fixture.debugElement.query(
+ By.css('[data-test-id="playback-fallback-mpv"]')
+ );
+
+ expect(mpvButton).toBeNull();
+ expect(banner.nativeElement.textContent).toContain(
+ 'PLAYBACK_DIAGNOSTICS.NETWORK_ERROR.TITLE'
+ );
+ expect(banner.nativeElement.textContent).toContain(
+ 'PLAYBACK_DIAGNOSTICS.INLINE_FAILURE_TITLE'
+ );
+ });
+
it('renders technical details and codec-specific hints in the diagnostic banner', () => {
fixture.detectChanges();
const issue = createUnsupportedCodecDiagnostic();
@@ -284,6 +371,41 @@ describe('WebPlayerViewComponent', () => {
'PLAYBACK_DIAGNOSTICS.CODEC_HINT'
);
});
+
+ it('clears playback diagnostics when retrying inline playback', () => {
+ fixture.detectChanges();
+ const player = fixture.debugElement.query(
+ By.directive(StubVjsPlayerComponent)
+ ).componentInstance as StubVjsPlayerComponent;
+ expect(player.options()).toEqual(
+ expect.objectContaining({ reloadToken: 0 })
+ );
+
+ component.handlePlaybackIssue(createUnsupportedCodecDiagnostic());
+ fixture.detectChanges();
+
+ const retryButton = fixture.debugElement.query(
+ By.css('[data-test-id="playback-retry"]')
+ );
+ const utilityControls = fixture.nativeElement.querySelectorAll(
+ '[data-test-id="playback-retry"], [data-test-id="playback-diagnostic-details"]'
+ );
+
+ expect(utilityControls[0]).toBe(retryButton.nativeElement);
+
+ retryButton.nativeElement.click();
+ fixture.detectChanges();
+
+ expect(component.playbackDiagnostic()).toBeNull();
+ expect(player.options()).toEqual(
+ expect.objectContaining({ reloadToken: 1 })
+ );
+ expect(
+ fixture.debugElement.query(
+ By.css('[data-test-id="playback-diagnostic-banner"]')
+ )
+ ).toBeNull();
+ });
});
function createUnsupportedContainerDiagnostic(): PlaybackDiagnostic {
@@ -329,3 +451,18 @@ function createUnsupportedCodecDiagnostic(): PlaybackDiagnostic {
externalFallbackRecommended: true,
};
}
+
+function createNetworkDiagnostic(): PlaybackDiagnostic {
+ return {
+ code: PlaybackDiagnosticCode.NetworkError,
+ source: PlaybackDiagnosticSource.MpegTs,
+ sourceUrl: 'https://example.com/live/channel.ts',
+ container: 'ts',
+ mimeType: 'video/mp2t',
+ player: 'videojs',
+ audioCodecs: [],
+ videoCodecs: [],
+ details: 'HttpStatusCodeInvalid {"code":456,"msg":"
"}',
+ externalFallbackRecommended: false,
+ };
+}
diff --git a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts
index f216de195..2477f5403 100644
--- a/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts
+++ b/libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts
@@ -8,6 +8,7 @@ import {
input,
output,
signal,
+ untracked,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ClipboardModule } from '@angular/cdk/clipboard';
@@ -17,6 +18,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslatePipe } from '@ngx-translate/core';
import {
+ Channel,
ResolvedPortalPlayback,
Settings,
STORE_KEY,
@@ -80,19 +82,26 @@ export class WebPlayerViewComponent {
this.storage.get(STORE_KEY.Settings)
) as Signal;
- channel!: { url: string };
+ channel!: Channel;
player!: VideoPlayer;
vjsOptions!: {
isLive: boolean;
+ reloadToken: number;
sources: { src: string; type: string }[];
};
readonly isDesktop = signal(this.detectDesktop());
+ readonly reloadToken = signal(0);
readonly playbackDiagnostic = signal(null);
readonly canShowExternalFallbackActions = computed(
() =>
this.isDesktop() &&
!!this.playbackDiagnostic()?.externalFallbackRecommended
);
+ readonly diagnosticHeadlineKey = computed(() =>
+ this.canShowExternalFallbackActions()
+ ? 'PLAYBACK_DIAGNOSTICS.NATIVE_FALLBACK_TITLE'
+ : 'PLAYBACK_DIAGNOSTICS.INLINE_FAILURE_TITLE'
+ );
readonly resolvedPlayback = computed(() => {
const playback = this.playback();
@@ -119,7 +128,7 @@ export class WebPlayerViewComponent {
const playback = this.resolvedPlayback();
this.playbackDiagnostic.set(null);
- this.setChannel(playback.streamUrl);
+ this.setChannel(playback);
this.setVjsOptions(
playback.streamUrl,
this.isLivePlayback(playback)
@@ -138,13 +147,47 @@ export class WebPlayerViewComponent {
this.vjsOptions = {
isLive,
+ reloadToken: untracked(() => this.reloadToken()),
sources: [{ src: streamUrl, type: mimeType }],
};
}
- setChannel(streamUrl: string) {
+ setChannel(playbackOrUrl: ResolvedPortalPlayback | string) {
+ const playback =
+ typeof playbackOrUrl === 'string'
+ ? {
+ streamUrl: playbackOrUrl,
+ title: playbackOrUrl,
+ }
+ : playbackOrUrl;
+
this.channel = {
- url: streamUrl,
+ id: playback.streamUrl,
+ url: playback.streamUrl,
+ name: playback.title || playback.streamUrl,
+ group: { title: '' },
+ tvg: {
+ id: '',
+ name: playback.title || playback.streamUrl,
+ url: '',
+ logo: playback.thumbnail ?? '',
+ rec: '',
+ },
+ http: {
+ referrer:
+ playback.referer ??
+ this.getHeaderValue(playback.headers, 'Referer') ??
+ '',
+ 'user-agent':
+ playback.userAgent ??
+ this.getHeaderValue(playback.headers, 'User-Agent') ??
+ '',
+ origin:
+ playback.origin ??
+ this.getHeaderValue(playback.headers, 'Origin') ??
+ '',
+ },
+ radio: 'false',
};
}
@@ -165,6 +208,15 @@ export class WebPlayerViewComponent {
});
}
+ retryPlayback(): void {
+ const playback = this.resolvedPlayback();
+
+ this.playbackDiagnostic.set(null);
+ this.reloadToken.update((value) => value + 1);
+ this.setChannel(playback);
+ this.setVjsOptions(playback.streamUrl, this.isLivePlayback(playback));
+ }
+
getDiagnosticTitleKey(issue: PlaybackDiagnostic): string {
return `${this.getDiagnosticTranslationBase(issue)}.TITLE`;
}
@@ -265,6 +317,20 @@ export class WebPlayerViewComponent {
return !playback.contentInfo;
}
+ private getHeaderValue(
+ headers: ResolvedPortalPlayback['headers'] | undefined,
+ name: string
+ ): string | undefined {
+ if (!headers) {
+ return undefined;
+ }
+
+ const matchingKey = Object.keys(headers).find(
+ (key) => key.toLowerCase() === name.toLowerCase()
+ );
+ return matchingKey ? headers[matchingKey] : undefined;
+ }
+
private formatPlayer(player: PlaybackDiagnostic['player']): string {
switch (player) {
case 'videojs':
diff --git a/libs/ui/styles/_portal-layout.scss b/libs/ui/styles/_portal-layout.scss
index aef73c0e4..b18f4ec71 100644
--- a/libs/ui/styles/_portal-layout.scss
+++ b/libs/ui/styles/_portal-layout.scss
@@ -73,10 +73,10 @@
position: absolute;
top: 6px;
left: 6px;
- // Player internals, including the radio audio stage, may use z-index
- // 2 inside the content area. Keep the restore affordance above them
- // for both paint order and hit testing.
- z-index: 5;
+ // Player internals, including diagnostic overlays, may use z-index 5
+ // inside the content area. Keep the restore affordance above them for
+ // both paint order and hit testing.
+ z-index: 6;
background: rgba(128, 128, 128, 0.18);
color: var(--mat-sys-on-surface, rgba(255, 255, 255, 0.75));
box-shadow: none;