From 538649feff6730b47e091aa2497b01cee5736895 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 21 Feb 2026 11:56:21 +0100 Subject: [PATCH] feat(players): enhance video player with audio track management and UI adjustments Entire-Checkpoint: f35656857e95 --- .../video-player/video-player.component.ts | 10 +- .../lib/art-player/art-player.component.ts | 43 +++++++- .../channel-list-container.component.ts | 1 + .../lib/vjs-player/vjs-player.component.scss | 5 +- .../lib/vjs-player/vjs-player.component.ts | 100 ++++++++++++++++++ 5 files changed, 150 insertions(+), 9 deletions(-) diff --git a/apps/web/src/app/home/video-player/video-player.component.ts b/apps/web/src/app/home/video-player/video-player.component.ts index 27af6ba26..a0bd82866 100644 --- a/apps/web/src/app/home/video-player/video-player.component.ts +++ b/apps/web/src/app/home/video-player/video-player.component.ts @@ -93,9 +93,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { private readonly store = inject(Store); /** Active selected channel */ - readonly activeChannel$ = this.store - .select(selectActive) - .pipe(filter((channel) => Boolean((channel as Channel)?.url))); + readonly activeChannel$ = this.store.select(selectActive); /** Channels list */ channels$!: Observable; @@ -179,6 +177,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { combineLatestWith(this.activatedRoute.queryParams), switchMap(([params, queryParams]) => { if (params['id']) { + this.store.dispatch(ChannelActions.resetActiveChannel()); this.store.dispatch( PlaylistActions.setActivePlaylist({ playlistId: params['id'], @@ -402,8 +401,9 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { openSettings() { this.dialog.open(SettingsComponent, { - width: '1000px', - height: '90%', + width: '1200px', + maxWidth: '96vw', + maxHeight: '92vh', data: { isDialog: true }, }); } diff --git a/libs/ui/components/src/lib/art-player/art-player.component.ts b/libs/ui/components/src/lib/art-player/art-player.component.ts index 780d86f03..1b8ba77e7 100644 --- a/libs/ui/components/src/lib/art-player/art-player.component.ts +++ b/libs/ui/components/src/lib/art-player/art-player.component.ts @@ -111,6 +111,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { this.hls = new Hls(); this.hls.loadSource(url); this.hls.attachMedia(video); + this.setupHlsAudioTracks(); } else if ( video.canPlayType('application/vnd.apple.mpegurl') ) { @@ -119,10 +120,8 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { }, mkv: function (video: HTMLVideoElement, url: string) { video.src = url; - // Add error handling video.onerror = () => { console.error('Error loading MKV file:', video.error); - // Fallback to treating it as a regular video video.src = url; }; }, @@ -143,11 +142,49 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { }); } + /** + * Listens for HLS.js audio tracks and adds a settings menu entry + * to ArtPlayer for switching between available audio tracks. + */ + private setupHlsAudioTracks(): void { + if (!this.hls) return; + + const hls = this.hls; + + hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => { + const tracks = hls.audioTracks; + if (!tracks || tracks.length <= 1) return; + + this.player.setting.add({ + html: 'Audio', + icon: ` + + `, + width: 220, + tooltip: tracks[hls.audioTrack]?.name || '', + selector: tracks.map((track, index) => ({ + html: track.name || track.lang || `Track ${index + 1}`, + default: index === hls.audioTrack, + })), + onSelect: function (this: Artplayer, item: any) { + const selectedIndex = tracks.findIndex( + (t, i) => + (t.name || t.lang || `Track ${i + 1}`) === item.html + ); + if (selectedIndex >= 0) { + hls.audioTrack = selectedIndex; + } + return item.html; + }, + } as any); + }); + } + private getVideoType(url: string): string { const extension = url.split('.').pop()?.toLowerCase(); switch (extension) { case 'mkv': - return 'video/matroska'; // Changed from 'mkv' + return 'video/matroska'; case 'm3u8': return 'm3u8'; case 'mp4': diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index d5cfd2e62..23b1e18be 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -176,6 +176,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { + this.store.dispatch(ChannelActions.resetActiveChannel()); this.store.dispatch(ChannelActions.setChannels({ channels: [] })); if (this.epgRefreshInterval) { diff --git a/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss b/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss index bea148a1d..89e7e58b5 100644 --- a/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss +++ b/libs/ui/components/src/lib/vjs-player/vjs-player.component.scss @@ -13,9 +13,12 @@ } } +.video-js .vjs-menu-button-popup .vjs-menu { + z-index: 10; +} @media only screen and (max-width: 599px) { .video-js { height: calc(50vh - 65px) !important; } -} \ No newline at end of file +} diff --git a/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts b/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts index 191f95a37..2528b0a99 100644 --- a/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts +++ b/libs/ui/components/src/lib/vjs-player/vjs-player.component.ts @@ -61,8 +61,34 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { if (this.startTime > 0) { this.player.currentTime(this.startTime); } + this.logAudioTracks(); + this.setupAudioTrackMenu(); }); + // Audio tracks may be added after loadedmetadata (e.g. HLS alternate audio) + const audioTracks = this.player.audioTracks(); + if (audioTracks) { + audioTracks.addEventListener('addtrack', () => { + console.log( + '[AudioTrack] addtrack event fired, total tracks:', + this.player.audioTracks().length + ); + this.logAudioTracks(); + this.setupAudioTrackMenu(); + }); + audioTracks.addEventListener('removetrack', () => { + console.log( + '[AudioTrack] removetrack event fired, total tracks:', + this.player.audioTracks().length + ); + this.logAudioTracks(); + this.setupAudioTrackMenu(); + }); + audioTracks.addEventListener('change', () => { + this.logAudioTracks(); + }); + } + this.player.on('volumechange', () => { const currentVolume = this.player.volume(); localStorage.setItem('volume', currentVolume.toString()); @@ -119,4 +145,78 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy { this.player.dispose(); } } + + /** + * Logs all available audio tracks for debugging. + */ + private logAudioTracks(): void { + const audioTracks = this.player.audioTracks(); + console.log('[AudioTrack] Audio tracks count:', audioTracks?.length ?? 0); + for (let i = 0; i < (audioTracks?.length ?? 0); i++) { + const t = audioTracks[i]; + console.log( + `[AudioTrack] Track ${i}: label="${t.label}", language="${t.language}", enabled=${t.enabled}, kind="${t.kind}"` + ); + } + + // Also check the underlying tech for HLS audio tracks + const tech = + typeof this.player?.tech === 'function' + ? this.player.tech({ IWillNotUseThisInPlugins: true }) + : null; + const audioMediaGroups = + tech?.vhs?.playlists?.main?.mediaGroups?.AUDIO ?? + tech?.vhs?.playlists?.master?.mediaGroups?.AUDIO; + + if (audioMediaGroups) { + console.log( + '[AudioTrack] HLS AUDIO media groups:', + JSON.stringify(Object.keys(audioMediaGroups)) + ); + } else { + console.log( + '[AudioTrack] HLS AUDIO media groups: none found in playlist metadata' + ); + } + } + + /** + * Sets up the audio track selection menu in the control bar. + * Uses the Video.js audioTracks() API which works with both + * native multi-audio streams and HLS.js alternate audio tracks. + */ + private setupAudioTrackMenu(): void { + const audioTracks = this.player.audioTracks(); + console.log( + '[AudioTrack] setupAudioTrackMenu called, tracks:', + audioTracks?.length ?? 0 + ); + if (!audioTracks || audioTracks.length <= 1) { + console.log( + '[AudioTrack] Skipping menu: need >1 tracks, have', + audioTracks?.length ?? 0 + ); + console.log( + '[AudioTrack] If VLC/MPV show more tracks, the HLS manifest likely does not expose alternate audio via EXT-X-MEDIA' + ); + return; + } + + const controlBar = this.player.getChild('controlBar'); + if (!controlBar) { + return; + } + + let audioButton = + controlBar.getChild('audioTrackButton') ?? + controlBar.getChild('AudioTrackButton'); + if (!audioButton) { + audioButton = controlBar.addChild('audioTrackButton', {}); + } + + if (audioButton) { + audioButton.show?.(); + audioButton.update?.(); + } + } }