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 1b8ba77e7..4e28a83a0 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 @@ -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'; } diff --git a/libs/ui/components/src/lib/html-video-player/html-video-player.component.ts b/libs/ui/components/src/lib/html-video-player/html-video-player.component.ts index e74621d77..dd7391e24 100644 --- a/libs/ui/components/src/lib/html-video-player/html-video-player.component.ts +++ b/libs/ui/components/src/lib/html-video-player/html-video-player.component.ts @@ -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(); } 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 2528b0a99..c2716d5c6 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 @@ -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. */ diff --git a/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.ts b/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.ts index 8d36c9404..420e599b6 100644 --- a/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.ts +++ b/libs/ui/shared-portals/src/lib/web-player-view/web-player-view.component.ts @@ -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 }], diff --git a/package.json b/package.json index 289c327f8..3077b6cc5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 440667b90..c0fb1fa1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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