feat(player): detect codec errors and support mpegts streams

Add codec error detection and reporting to the Video.js player component
and propagate codec error events to parent components. Introduce a
codecError event emitter with a CodecError interface and logic to avoid
duplicated or noisy error reporting. Improve player initialization to
handle raw MPEG-TS streams via mpegts.js: detect mpegts URLs, prevent
autoplay for those streams, and initialize a dedicated mpegts player
instance. Wire codec error output up in template so host components can
handle codec failures.

Also:
- Replace hardcoded HLS MIME type with runtime getMimeType() in templates.
- Swap to videojs-quality-selector-hls plugin and add safe init of plugins.
- Add safeguards for player destruction and more robust error handling
  (track fatal errors, prevent races).
- Persist volume changes and listen to video element errors to surface
  codec problems.

These changes enable better handling of unsupported audio/video codecs,
support raw .ts streams, and allow UI-level responses to codec failures.
This commit is contained in:
4gray committed 2026-01-07 23:52:46 +01:00
1 parent 80a2649a8b
commit aaabe6023a
5 files changed
+667 -44

No files matched your search

@@ -36,12 +36,13 @@
src:
activeChannel.url +
activeChannel.epgParams,
type: 'application/x-mpegURL',
type: getMimeType(activeChannel.url),
},
],
}"
[volume]="volume"
[class.hide-captions]="!playerSettings.showCaptions"
(codecError)="handleCodecError($event)"
/>
}
<!-- default html player component -->
@@ -58,6 +59,7 @@
[channel]="activeChannel"
[volume]="volume"
[showCaptions]="playerSettings.showCaptions!"
(codecError)="handleCodecError($event)"
/>
}
@if (playerSettings.player === 'mpv') {
@@ -12,12 +12,15 @@ import {
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Store } from '@ngrx/store';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslateService } from '@ngx-translate/core';
import {
ArtPlayerComponent,
AudioPlayerComponent,
CodecError,
COMPONENT_OVERLAY_REF,
EpgListComponent,
HtmlVideoPlayerComponent,
@@ -80,8 +83,10 @@ export class VideoPlayerComponent implements OnInit {
private readonly playlistsService = inject(PlaylistsService);
private readonly router = inject(Router);
private readonly settingsStore = inject(SettingsStore);
private readonly snackBar = inject(MatSnackBar);
private readonly storage = inject(StorageMap);
private readonly store = inject(Store);
private readonly translateService = inject(TranslateService);
/** Active selected channel */
readonly activeChannel$ = this.store
@@ -474,4 +479,65 @@ export class VideoPlayerComponent implements OnInit {
target instanceof HTMLTextAreaElement
);
}
/**
* Get the MIME type for a stream URL based on its extension
* @param url Stream URL
* @returns MIME type string for Video.js
*/
getMimeType(url: string): string {
const extension = url.split(/[#?]/)[0].split('.').pop()?.toLowerCase();
switch (extension) {
case 'ts':
return 'video/mp2t';
case 'm3u8':
return 'application/x-mpegURL';
case 'mp4':
return 'video/mp4';
case 'webm':
return 'video/webm';
case 'mkv':
return 'video/x-matroska';
case 'flv':
return 'video/x-flv';
default:
return 'application/x-mpegURL';
}
}
/**
* Handle codec error from video players
* Shows a snackbar suggesting to use VLC or MPV for unsupported codecs
*/
handleCodecError(error: CodecError): void {
console.log('handleCodecError received:', error);
const codecName = error.codec || 'unknown';
const isAudio = error.type === 'unsupported_audio';
const codecType = isAudio ? 'audio' : 'video';
// Show snackbar with helpful message
const message = this.translateService.instant('PLAYER.UNSUPPORTED_CODEC', {
codecType: codecType,
codec: codecName
}) || `Unsupported ${codecType} codec: ${codecName}. Use VLC or MPV for full codec support.`;
const actionText = this.isDesktop
? this.translateService.instant('PLAYER.SWITCH_TO_VLC') || 'Switch to VLC'
: this.translateService.instant('CLOSE') || 'Close';
const snackBarRef = this.snackBar.open(message, actionText, {
duration: 10000,
horizontalPosition: 'center',
verticalPosition: 'bottom',
panelClass: ['codec-error-snackbar']
});
// If on desktop, offer to switch to VLC
if (this.isDesktop) {
snackBarRef.onAction().subscribe(() => {
// Update player setting to VLC
this.settingsStore.updateSettings({ player: VideoPlayer.VLC });
});
}
}
}