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.
*/
@@ -56,9 +56,11 @@ export class WebPlayerViewComponent {
setVjsOptions(streamUrl: string) {
const extension = getExtensionFromUrl(streamUrl);
const mimeType =
extension === 'm3u' || extension === 'm3u8' || extension === 'ts'
extension === 'm3u' || extension === 'm3u8'
? 'application/x-mpegURL'
: 'video/mp4';
: extension === 'ts'
? 'video/mp2t'
: 'video/mp4';
this.vjsOptions = {
sources: [{ src: streamUrl, type: mimeType }],
+1
View File
@@ -73,6 +73,7 @@
"iptv-playlist-parser": "github:4gray/iptv-playlist-parser",
"lodash": "4.17.21",
"moment": "2.30.1",
"mpegts.js": "1.8.0",
"ngx-indexed-db": "21.0.0",
"ngx-skeleton-loader": "11.3.0",
"rxjs": "7.8.2",
+22
View File
@@ -110,6 +110,9 @@ importers:
moment:
specifier: 2.30.1
version: 2.30.1
mpegts.js:
specifier: 1.8.0
version: 1.8.0
ngx-indexed-db:
specifier: 21.0.0
version: 21.0.0(@angular/common@21.1.5(@angular/core@21.1.5(@angular/compiler@21.1.5)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.1.5(@angular/compiler@21.1.5)(rxjs@7.8.2)(zone.js@0.15.1))
@@ -6273,6 +6276,9 @@ packages:
es6-error@4.1.1:
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
es6-promise@4.2.8:
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
esast-util-from-estree@2.0.0:
resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==}
@@ -8569,6 +8575,9 @@ packages:
resolution: {integrity: sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==}
hasBin: true
mpegts.js@1.8.0:
resolution: {integrity: sha512-ZtujqtmTjWgcDDkoOnLvrOKUTO/MKgLHM432zGDI8oPaJ0S+ebPxg1nEpDpLw6I7KmV/GZgUIrfbWi3qqEircg==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -11519,6 +11528,10 @@ packages:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
webworkify-webpack@https://codeload.github.com/xqq/webworkify-webpack/tar.gz/24d1e719b4a6cac37a518b2bb10fe124527ef4ef:
resolution: {tarball: https://codeload.github.com/xqq/webworkify-webpack/tar.gz/24d1e719b4a6cac37a518b2bb10fe124527ef4ef}
version: 2.1.5
whatwg-encoding@2.0.0:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}
@@ -18616,6 +18629,8 @@ snapshots:
es6-error@4.1.1:
optional: true
es6-promise@4.2.8: {}
esast-util-from-estree@2.0.0:
dependencies:
'@types/estree-jsx': 1.0.5
@@ -21821,6 +21836,11 @@ snapshots:
'@xmldom/xmldom': 0.8.11
global: 4.4.0
mpegts.js@1.8.0:
dependencies:
es6-promise: 4.2.8
webworkify-webpack: https://codeload.github.com/xqq/webworkify-webpack/tar.gz/24d1e719b4a6cac37a518b2bb10fe124527ef4ef
mrmime@2.0.1: {}
ms@2.0.0: {}
@@ -25104,6 +25124,8 @@ snapshots:
websocket-extensions@0.1.4: {}
webworkify-webpack@https://codeload.github.com/xqq/webworkify-webpack/tar.gz/24d1e719b4a6cac37a518b2bb10fe124527ef4ef: {}
whatwg-encoding@2.0.0:
dependencies:
iconv-lite: 0.6.3