Merge pull request #801 from markc1984/master

fix: support raw MPEG-TS (.ts) streams in all embedded video players
This commit is contained in:
4gray authored and GitHub committed 2026-03-05 23:42:38 +01:00
commit ea55c485a7
6 files changed
+146 -11

No files matched your search

@@ -12,6 +12,8 @@ import {
} from '@angular/core';
import Artplayer from 'artplayer';
import Hls from 'hls.js';
import { getExtensionFromUrl } from 'm3u-utils';
import mpegts from 'mpegts.js';
import { Channel } from 'shared-interfaces';
Artplayer.AUTO_PLAYBACK_TIMEOUT = 10000;
@@ -46,6 +48,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
private player!: Artplayer;
private hls: Hls | null = null;
private mpegtsPlayer: mpegts.Player | null = null;
private readonly elementRef = inject(ElementRef);
@@ -65,6 +68,13 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
}
private destroyPlayer(): void {
if (this.mpegtsPlayer) {
this.mpegtsPlayer.pause();
this.mpegtsPlayer.unload();
this.mpegtsPlayer.detachMediaElement();
this.mpegtsPlayer.destroy();
this.mpegtsPlayer = null;
}
if (this.hls) {
this.hls.destroy();
this.hls = null;
@@ -78,7 +88,8 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
const el = this.elementRef.nativeElement.querySelector(
'.artplayer-container'
);
const isLive = this.channel?.url?.toLowerCase().includes('m3u8');
const extension = getExtensionFromUrl(this.channel?.url ?? '');
const isLive = extension === 'm3u8' || extension === 'ts';
this.player = new Artplayer({
container: el,
@@ -118,6 +129,21 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
video.src = url;
}
},
ts: (video: HTMLVideoElement, url: string) => {
if (mpegts.isSupported()) {
if (this.mpegtsPlayer) {
this.mpegtsPlayer.destroy();
}
this.mpegtsPlayer = mpegts.createPlayer({
type: 'mpegts',
isLive: true,
url: url,
});
this.mpegtsPlayer.attachMediaElement(video);
this.mpegtsPlayer.load();
this.mpegtsPlayer.play();
}
},
mkv: function (video: HTMLVideoElement, url: string) {
video.src = url;
video.onerror = () => {
@@ -181,7 +207,7 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
}
private getVideoType(url: string): string {
const extension = url.split('.').pop()?.toLowerCase();
const extension = getExtensionFromUrl(url);
switch (extension) {
case 'mkv':
return 'video/matroska';
@@ -189,6 +215,8 @@ export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges {
return 'm3u8';
case 'mp4':
return 'mp4';
case 'ts':
return 'ts';
default:
return 'auto';
}
@@ -12,6 +12,7 @@ import {
ViewChild,
} from '@angular/core';
import Hls from 'hls.js';
import mpegts from 'mpegts.js';
import { getExtensionFromUrl } from 'm3u-utils';
import { DataService } from 'services';
import { Channel } from 'shared-interfaces';
@@ -43,6 +44,8 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
/** HLS object */
hls!: Hls;
/** mpegts.js player for raw MPEG-TS streams */
private mpegtsPlayer: mpegts.Player | null = null;
/** Captions/subtitles indicator */
@Input() showCaptions!: boolean;
@@ -89,6 +92,13 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
* @param channel given channel object
*/
playChannel(channel: Channel): void {
if (this.mpegtsPlayer) {
this.mpegtsPlayer.pause();
this.mpegtsPlayer.unload();
this.mpegtsPlayer.detachMediaElement();
this.mpegtsPlayer.destroy();
this.mpegtsPlayer = null;
}
if (this.hls) this.hls.destroy();
if (channel.url) {
const url = channel.url + (channel.epgParams ?? '');
@@ -102,7 +112,19 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
);
}
if (
if (extension === 'ts' && mpegts.isSupported()) {
console.log('Using mpegts.js for TS stream:', channel.name, url);
this.mpegtsPlayer = mpegts.createPlayer({
type: 'mpegts',
isLive: true,
url: url,
});
this.mpegtsPlayer.attachMediaElement(
this.videoPlayer.nativeElement
);
this.mpegtsPlayer.load();
this.handlePlayOperation();
} else if (
extension !== 'mp4' &&
extension !== 'mpv' &&
Hls &&
@@ -182,6 +204,13 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
'volumechange',
this.onVolumeChange
);
if (this.mpegtsPlayer) {
this.mpegtsPlayer.pause();
this.mpegtsPlayer.unload();
this.mpegtsPlayer.detachMediaElement();
this.mpegtsPlayer.destroy();
this.mpegtsPlayer = null;
}
if (this.hls) {
this.hls.destroy();
}
@@ -12,6 +12,8 @@ import {
ViewEncapsulation,
} from '@angular/core';
import '@yangkghjh/videojs-aspect-ratio-panel';
import { getExtensionFromUrl } from 'm3u-utils';
import mpegts from 'mpegts.js';
import videoJs from 'video.js';
import 'videojs-contrib-quality-levels';
import 'videojs-quality-selector-hls';
@@ -33,6 +35,8 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy {
@Input() options!: any;
/** VideoJs object */
player!: any;
/** mpegts.js player for raw MPEG-TS streams */
private mpegtsPlayer: mpegts.Player | null = null;
@Input() volume = 1;
@Input() startTime = 0;
@Output() timeUpdate = new EventEmitter<{
@@ -44,12 +48,17 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy {
* Instantiate Video.js on component init
*/
ngOnInit(): void {
const source = this.options?.sources?.[0];
const isMpegTs = this.isMpegTsSource(source?.src);
// For raw MPEG-TS streams, init Video.js without a source (UI/controls only)
const vjsOptions = isMpegTs
? { ...this.options, sources: [], autoplay: false }
: { ...this.options, autoplay: true };
this.player = videoJs(
this.target.nativeElement,
{
...this.options,
autoplay: true,
},
vjsOptions,
() => {
console.log(
'Setting VideoJS player initial volume to:',
@@ -100,6 +109,11 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy {
duration: this.player.duration(),
});
});
// Attach mpegts.js after Video.js is ready
if (isMpegTs) {
this.initMpegTs(source.src);
}
}
);
try {
@@ -125,8 +139,16 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy {
* @param changes contains changed channel object
*/
ngOnChanges(changes: SimpleChanges): void {
if (changes['options'].previousValue) {
this.player.src(changes['options'].currentValue.sources[0]);
if (changes['options']?.previousValue) {
const newSource = changes['options'].currentValue.sources[0];
if (this.isMpegTsSource(newSource?.src)) {
this.destroyMpegTs();
this.player.reset();
this.initMpegTs(newSource.src);
} else {
this.destroyMpegTs();
this.player.src(newSource);
}
}
if (changes['volume']?.currentValue !== undefined && this.player) {
console.log(
@@ -141,11 +163,42 @@ export class VjsPlayerComponent implements OnInit, OnChanges, OnDestroy {
* Removes the players HTML reference on destroy
*/
ngOnDestroy(): void {
this.destroyMpegTs();
if (this.player) {
this.player.dispose();
}
}
private isMpegTsSource(url?: string): boolean {
if (!url) return false;
return getExtensionFromUrl(url) === 'ts' && mpegts.isSupported();
}
private initMpegTs(url: string): void {
const videoEl = this.player.tech({ IWillNotUseThisInPlugins: true })?.el();
if (!videoEl) return;
console.log('Using mpegts.js for TS stream:', url);
this.mpegtsPlayer = mpegts.createPlayer({
type: 'mpegts',
isLive: true,
url: url,
});
this.mpegtsPlayer.attachMediaElement(videoEl as HTMLVideoElement);
this.mpegtsPlayer.load();
this.mpegtsPlayer.play();
}
private destroyMpegTs(): void {
if (this.mpegtsPlayer) {
this.mpegtsPlayer.pause();
this.mpegtsPlayer.unload();
this.mpegtsPlayer.detachMediaElement();
this.mpegtsPlayer.destroy();
this.mpegtsPlayer = null;
}
}
/**
* Logs all available audio tracks for debugging.
*/