mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
perf(playback): make the web players and M3U player OnPush
Plan item C6 step 3 for playback: the eight Eager components in libs/ui/playback (video.js, ArtPlayer, HTML5/hls/mpegts, audio player, web player view, VOD details, sidebar, external-player dialog) and the M3U video player and VOD detail switch to OnPush. The player libraries' events already reach the UI through the signal-backed controls adapter or outputs, and the players' DOM belongs to the libraries. The M3U video player rendered three plain fields written outside template events: the channel-number overlay, cleared by a 2 s debounce timer, and the player choice, written from an IndexedDB read and a settings effect. They are signals now, and a test checks that the overlay leaves the DOM when the timer fires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2738bc28a1
commit
f7cd0d6e48
13 files changed
+76
-45
No files matched your search
@@ -97,14 +97,14 @@ files that still contain `ChangeDetectionStrategy.Eager`.
|
||||
- [x] `libs/ui/epg/src/lib/epg-progress-panel/epg-progress-panel.component.ts` (idle audit root; also `EpgTrustConfirmDialogComponent`)
|
||||
- [x] `libs/ui/epg/src/lib/epg-source-status/epg-source-status.component.ts`
|
||||
- [ ] `libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts` (`apps/remote-control-web` only)
|
||||
- [ ] `libs/ui/playback/src/lib/art-player/art-player.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/external-player-info-dialog/external-player-info-dialog.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/vod-details/vod-details.component.ts`
|
||||
- [ ] `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/art-player/art-player.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/audio-player/audio-player.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/external-player-info-dialog/external-player-info-dialog.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/html-video-player/html-video-player.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/vjs-player/vjs-player.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/vod-details/vod-details.component.ts`
|
||||
- [x] `libs/ui/playback/src/lib/web-player-view/web-player-view.component.ts`
|
||||
|
||||
`libs/ui/playback` (8) goes with the playback PR, not the `libs/ui` one.
|
||||
|
||||
@@ -131,8 +131,8 @@ files that still contain `ChangeDetectionStrategy.Eager`.
|
||||
- [ ] `libs/playlist/import/feature/src/lib/text-import/text-import.component.ts`
|
||||
- [ ] `libs/playlist/import/feature/src/lib/url-upload/url-upload.component.ts`
|
||||
- [ ] `libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts`
|
||||
- [ ] `libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts`
|
||||
- [ ] `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts`
|
||||
- [x] `libs/playlist/m3u/feature-player/src/lib/m3u-vod-detail/m3u-vod-detail.component.ts`
|
||||
- [x] `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts`
|
||||
- [ ] `libs/playlist/shared/ui/src/lib/recent-playlists/empty-state/empty-state.component.ts`
|
||||
- [ ] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts`
|
||||
- [ ] `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts`
|
||||
@@ -165,8 +165,8 @@ the field a signal (or a `computed`), or writes it through one.
|
||||
|
||||
| Done | Site | What depends on the zone | Owning PR |
|
||||
| --- | --- | --- | --- |
|
||||
| [ ] | `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts` `onChannelNumberInput`/`clearChannelNumberInput` | 2 s `window.setTimeout` hides the channel-number overlay through plain `showChannelNumberOverlay`/`channelNumberInput` | playback |
|
||||
| [ ] | same file, `applySettings` and the settings `effect()` | IndexedDB `storage.get(...).subscribe` and an effect assign plain `playerSettings`, which picks the player in the template | playback |
|
||||
| [x] | `libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts` `onChannelNumberInput`/`clearChannelNumberInput` | 2 s `window.setTimeout` hides the channel-number overlay through plain `showChannelNumberOverlay`/`channelNumberInput` | playback |
|
||||
| [x] | same file, `applySettings` and the settings `effect()` | IndexedDB `storage.get(...).subscribe` and an effect assign plain `playerSettings`, which picks the player in the template | playback |
|
||||
| [ ] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts` `checkPortalStatus` | plain `portalStatus` assigned after `await` in `ngOnInit` (PWA only: skipped when source health is supported) | playlist |
|
||||
| [ ] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` (EPG clear and EPG file pick handlers) | plain `playlist` reassigned after `await` | playlist |
|
||||
| [ ] | `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` (device-id derivation) | `form.patchValue` after `await`; template getters read `control.value`, which is not signal-backed | playlist |
|
||||
@@ -185,7 +185,7 @@ before: with zone.js on they still matter.
|
||||
- [ ] `apps/web/src/app/settings/settings-unload-guard.service.ts`: two
|
||||
`zone.run` calls around the window-close dialog (IPC
|
||||
`onWindowCloseRequested` and `beforeunload`).
|
||||
- [ ] `libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts`:
|
||||
- [x] `libs/ui/playback/src/lib/embedded-mpv-player/embedded-mpv-session-controller.ts`:
|
||||
`runOutsideAngular(() => setInterval(...))` for the position poll;
|
||||
`embedded-mpv-session-controller.position.spec.ts` asserts the call and
|
||||
changes with it.
|
||||
|
||||
@@ -71,7 +71,7 @@ import {
|
||||
TranslatePipe,
|
||||
],
|
||||
templateUrl: './m3u-vod-detail.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrls: ['./m3u-vod-detail.component.scss'],
|
||||
})
|
||||
export class M3uVodDetailComponent {
|
||||
|
||||
+32
-3
@@ -231,8 +231,12 @@ describe('VideoPlayerComponent fullscreen channel panel + zapping', () => {
|
||||
.overrideComponent(VideoPlayerComponent, {
|
||||
set: {
|
||||
imports: [],
|
||||
template:
|
||||
'<ng-template #fullscreenChannelPanel></ng-template>',
|
||||
// The real template's channel-number overlay, so the
|
||||
// OnPush timer test below can read the rendered state.
|
||||
template: `<ng-template #fullscreenChannelPanel></ng-template>
|
||||
@if (showChannelNumberOverlay()) {
|
||||
<div class="channel-number-overlay">{{ channelNumberInput() }}</div>
|
||||
}`,
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
@@ -247,6 +251,31 @@ describe('VideoPlayerComponent fullscreen channel panel + zapping', () => {
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
// OnPush: the overlay hides from a 2 s timer, outside any template
|
||||
// event, so it must leave the DOM without a zone-triggered tick.
|
||||
it('hides the channel-number overlay when its debounce fires', () => {
|
||||
jest.useFakeTimers();
|
||||
try {
|
||||
const overlay = () =>
|
||||
(fixture.nativeElement as HTMLElement).querySelector(
|
||||
'.channel-number-overlay'
|
||||
);
|
||||
component.handleChannelNumberInput('2');
|
||||
fixture.detectChanges();
|
||||
expect(overlay()?.textContent).toBe('2');
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(overlay()).toBeNull();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
setActiveChannelDispatch(nextChannel)
|
||||
);
|
||||
} finally {
|
||||
jest.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
describe('FULLSCREEN_CHANNEL_PANEL host', () => {
|
||||
it.each([VideoPlayer.MPV, VideoPlayer.VLC])(
|
||||
'withholds rows that would leave forced-inline DASH for %s',
|
||||
@@ -260,7 +289,7 @@ describe('VideoPlayerComponent fullscreen channel panel + zapping', () => {
|
||||
url: 'http://localhost/next.mpd',
|
||||
};
|
||||
player.set(externalPlayer);
|
||||
component.playerSettings.player = externalPlayer;
|
||||
component.playerSettings.set({ player: externalPlayer });
|
||||
setActive(dashChannel);
|
||||
channels.set([dashChannel, sampleChannel, nextDashChannel]);
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
[playbackSessionKey]="playbackSessionKey()"
|
||||
[inlinePlayerAvailable]="shouldShowInlinePlayer(activeChannel)"
|
||||
[volume]="volume()"
|
||||
[playerOverride]="playerSettings.player ?? null"
|
||||
[playerOverride]="playerSettings().player ?? null"
|
||||
(playbackStarted)="refreshVolumeFromBus()"
|
||||
(externalFallbackRequested)="
|
||||
handleExternalFallbackRequest($event)
|
||||
@@ -124,7 +124,7 @@
|
||||
[playerOverride]="
|
||||
activeChannelIsDash()
|
||||
? dashPlayerOverride()
|
||||
: (playerSettings.player ?? null)
|
||||
: (playerSettings().player ?? null)
|
||||
"
|
||||
[volume]="volume()"
|
||||
[timelineSegments]="catchupTimelineSegments()"
|
||||
@@ -255,10 +255,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
@if (showChannelNumberOverlay) {
|
||||
@if (showChannelNumberOverlay()) {
|
||||
<div class="channel-number-overlay">
|
||||
<div class="channel-number-display">
|
||||
{{ channelNumberInput }}
|
||||
{{ channelNumberInput() }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ function isInsideScrollableRegion(
|
||||
{ provide: EPG_GUIDE_SOURCE, useExisting: M3uEpgGuideSourceService },
|
||||
],
|
||||
templateUrl: './video-player.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrl: './video-player.component.scss',
|
||||
})
|
||||
export class VideoPlayerComponent
|
||||
@@ -713,9 +713,9 @@ export class VideoPlayerComponent
|
||||
);
|
||||
|
||||
/** Selected video player options */
|
||||
playerSettings: Partial<Settings> = {
|
||||
readonly playerSettings = signal<Partial<Settings>>({
|
||||
player: VideoPlayer.VideoJs,
|
||||
};
|
||||
});
|
||||
|
||||
readonly isDesktop = this.runtime.isElectron;
|
||||
readonly supportsEpg = this.runtime.supportsEpg;
|
||||
@@ -731,8 +731,8 @@ export class VideoPlayerComponent
|
||||
);
|
||||
|
||||
/** Channel number input state */
|
||||
channelNumberInput = '';
|
||||
showChannelNumberOverlay = false;
|
||||
readonly channelNumberInput = signal('');
|
||||
readonly showChannelNumberOverlay = signal(false);
|
||||
private channelNumberTimeout?: number;
|
||||
|
||||
/**
|
||||
@@ -795,9 +795,9 @@ export class VideoPlayerComponent
|
||||
|
||||
// React to settings changes
|
||||
effect(() => {
|
||||
this.playerSettings = {
|
||||
this.playerSettings.set({
|
||||
player: this.settingsStore.player(),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// Keep "now" fresh so EPG state re-evaluates over time.
|
||||
@@ -1191,10 +1191,10 @@ export class VideoPlayerComponent
|
||||
applySettings(): void {
|
||||
this.storage.get(STORE_KEY.Settings).subscribe((settings: unknown) => {
|
||||
if (settings && Object.keys(settings as Settings).length > 0) {
|
||||
this.playerSettings = {
|
||||
this.playerSettings.set({
|
||||
player:
|
||||
(settings as Settings).player || VideoPlayer.VideoJs,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1472,12 +1472,14 @@ export class VideoPlayerComponent
|
||||
}
|
||||
|
||||
// Add digit to current input
|
||||
this.channelNumberInput += digit;
|
||||
this.showChannelNumberOverlay = true;
|
||||
this.channelNumberInput.update((input) => input + digit);
|
||||
this.showChannelNumberOverlay.set(true);
|
||||
|
||||
// Set timeout to switch channel after 2 seconds of no input
|
||||
this.channelNumberTimeout = window.setTimeout(() => {
|
||||
this.switchToChannelByNumber(parseInt(this.channelNumberInput, 10));
|
||||
this.switchToChannelByNumber(
|
||||
parseInt(this.channelNumberInput(), 10)
|
||||
);
|
||||
this.clearChannelNumberInput();
|
||||
}, 2000);
|
||||
}
|
||||
@@ -1547,8 +1549,8 @@ export class VideoPlayerComponent
|
||||
* Clear channel number input and hide overlay
|
||||
*/
|
||||
clearChannelNumberInput(): void {
|
||||
this.channelNumberInput = '';
|
||||
this.showChannelNumberOverlay = false;
|
||||
this.channelNumberInput.set('');
|
||||
this.showChannelNumberOverlay.set(false);
|
||||
if (this.channelNumberTimeout) {
|
||||
clearTimeout(this.channelNumberTimeout);
|
||||
this.channelNumberTimeout = undefined;
|
||||
@@ -1663,7 +1665,7 @@ export class VideoPlayerComponent
|
||||
return true;
|
||||
}
|
||||
|
||||
const player = this.playerSettings.player;
|
||||
const player = this.playerSettings().player;
|
||||
return (
|
||||
!this.isExternalPlayer(player) && player !== VideoPlayer.EmbeddedMpv
|
||||
);
|
||||
@@ -1685,7 +1687,7 @@ export class VideoPlayerComponent
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.isExternalPlayer(this.playerSettings.player);
|
||||
return !this.isExternalPlayer(this.playerSettings().player);
|
||||
}
|
||||
|
||||
handleExternalFallbackRequest(request: PlaybackFallbackRequest): void {
|
||||
|
||||
@@ -51,7 +51,7 @@ Artplayer.AUTO_PLAYBACK_TIMEOUT = 10000;
|
||||
],
|
||||
providers: [WebVideoControlsAdapter],
|
||||
templateUrl: './art-player.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styleUrls: ['./art-player.component.scss'],
|
||||
})
|
||||
export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
@@ -167,7 +167,7 @@ import { PlaybackHistoryConfirmation } from '../playback-history/playback-histor
|
||||
</div>
|
||||
`,
|
||||
styleUrls: ['./audio-player.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
|
||||
@@ -71,7 +71,7 @@ const debugHtmlPlayer = createDevLogger('HtmlVideoPlayer');
|
||||
SeriesPlaybackNavigationControlsComponent,
|
||||
],
|
||||
providers: [WebVideoControlsAdapter],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ChannelListContainerComponent } from '@iptvnator/ui/components';
|
||||
selector: 'app-sidebar',
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrls: ['./sidebar.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ChannelListContainerComponent,
|
||||
MatIcon,
|
||||
|
||||
@@ -66,7 +66,7 @@ const debugVjsPlayer = createDevLogger('VjsPlayer');
|
||||
SeriesPlaybackNavigationControlsComponent,
|
||||
],
|
||||
providers: [WebVideoControlsAdapter],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
|
||||
@@ -82,7 +82,7 @@ import { createVodSimilarInPortals } from './vod-similar-in-portals.state';
|
||||
selector: 'app-vod-details',
|
||||
templateUrl: './vod-details.component.html',
|
||||
styleUrls: ['../styles/detail-view.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CastCrewRowComponent,
|
||||
DetailActionButtonComponent,
|
||||
|
||||
@@ -91,7 +91,7 @@ import { resolveWebPlayerSharedControls } from './web-player-shared-controls';
|
||||
useFactory: resolveWebPlayerSharedControls,
|
||||
},
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class WebPlayerViewComponent implements OnDestroy {
|
||||
|
||||
Reference in new issue
Block a user