mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
Merge pull request #801 from markc1984/master
fix: support raw MPEG-TS (.ts) streams in all embedded video players
This commit is contained in:
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 }],
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+22
@@ -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
|
||||
|
||||
Reference in new issue
Block a user