Proyecto App Linux
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<video #videoPlayer id="video-player" autoplay="true" controls="true"></video>
|
||||
@@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DataService } from 'services';
|
||||
import { HtmlVideoPlayerComponent } from './html-video-player.component';
|
||||
|
||||
describe('HtmlVideoPlayerComponent', () => {
|
||||
let component: HtmlVideoPlayerComponent;
|
||||
let fixture: ComponentFixture<HtmlVideoPlayerComponent>;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
let dataService: DataService;
|
||||
|
||||
const TEST_CHANNEL = {
|
||||
id: '1234',
|
||||
url: 'http://test.ts',
|
||||
name: 'Test channel',
|
||||
group: {
|
||||
title: 'News group',
|
||||
},
|
||||
http: {
|
||||
'user-agent': 'localhost',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
const dataServiceMock = {
|
||||
sendIpcEvent: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HtmlVideoPlayerComponent, TranslateModule.forRoot()],
|
||||
providers: [{ provide: DataService, useValue: dataServiceMock }],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HtmlVideoPlayerComponent);
|
||||
component = fixture.componentInstance;
|
||||
dataService = TestBed.inject(DataService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call play channel function after input changes', () => {
|
||||
jest.spyOn(component, 'playChannel');
|
||||
jest.spyOn(global.console, 'error').mockImplementation(() => {
|
||||
/* empty */
|
||||
});
|
||||
component.ngOnChanges({
|
||||
channel: new SimpleChange(null, TEST_CHANNEL, true),
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.playChannel).toHaveBeenCalledWith(TEST_CHANNEL);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,218 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
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';
|
||||
|
||||
/**
|
||||
* This component contains the implementation of HTML5 based video player
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-html-video-player',
|
||||
templateUrl: './html-video-player.component.html',
|
||||
styleUrls: ['./html-video-player.component.scss'],
|
||||
standalone: true,
|
||||
})
|
||||
export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
|
||||
/** Channel to play */
|
||||
@Input() channel!: Channel;
|
||||
@Input() volume = 1;
|
||||
@Input() startTime = 0;
|
||||
@Output() timeUpdate = new EventEmitter<{
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}>();
|
||||
|
||||
private readonly dataService = inject(DataService);
|
||||
|
||||
/** Video player DOM element */
|
||||
@ViewChild('videoPlayer', { static: true })
|
||||
videoPlayer!: ElementRef<HTMLVideoElement>;
|
||||
|
||||
/** HLS object */
|
||||
hls!: Hls;
|
||||
/** mpegts.js player for raw MPEG-TS streams */
|
||||
private mpegtsPlayer: mpegts.Player | null = null;
|
||||
|
||||
/** Captions/subtitles indicator */
|
||||
@Input() showCaptions!: boolean;
|
||||
|
||||
ngOnInit() {
|
||||
this.videoPlayer.nativeElement.addEventListener('volumechange', () => {
|
||||
this.onVolumeChange();
|
||||
});
|
||||
|
||||
this.videoPlayer.nativeElement.addEventListener('loadedmetadata', () => {
|
||||
if (this.startTime > 0) {
|
||||
this.videoPlayer.nativeElement.currentTime = this.startTime;
|
||||
}
|
||||
});
|
||||
|
||||
this.videoPlayer.nativeElement.addEventListener('timeupdate', () => {
|
||||
this.timeUpdate.emit({
|
||||
currentTime: this.videoPlayer.nativeElement.currentTime,
|
||||
duration: this.videoPlayer.nativeElement.duration,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for component input changes
|
||||
* @param changes component changes
|
||||
*/
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['channel'] && changes['channel'].currentValue) {
|
||||
this.playChannel(changes['channel'].currentValue);
|
||||
}
|
||||
if (changes['volume']?.currentValue !== undefined) {
|
||||
console.log(
|
||||
'Setting HTML5 player volume to:',
|
||||
changes['volume'].currentValue
|
||||
);
|
||||
this.videoPlayer.nativeElement.volume =
|
||||
changes['volume'].currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts to play the given channel
|
||||
* @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 ?? '');
|
||||
const extension = getExtensionFromUrl(channel.url);
|
||||
|
||||
// Set user agent if specified on channel
|
||||
if (channel.http?.['user-agent']) {
|
||||
window.electron?.setUserAgent(
|
||||
channel.http['user-agent'],
|
||||
channel.http.referrer
|
||||
);
|
||||
}
|
||||
|
||||
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 &&
|
||||
Hls.isSupported()
|
||||
) {
|
||||
console.log('... switching channel to ', channel.name, url);
|
||||
this.hls = new Hls();
|
||||
this.hls.attachMedia(this.videoPlayer.nativeElement);
|
||||
this.hls.loadSource(url);
|
||||
this.handlePlayOperation();
|
||||
} else {
|
||||
console.log('Using native video player...');
|
||||
this.addSourceToVideo(
|
||||
this.videoPlayer.nativeElement,
|
||||
url,
|
||||
'video/mp4'
|
||||
);
|
||||
this.videoPlayer.nativeElement.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSourceToVideo(element: HTMLVideoElement, url: string, type: string) {
|
||||
const source = document.createElement('source');
|
||||
source.src = url;
|
||||
source.type = type;
|
||||
element.appendChild(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables text based captions based on the global settings
|
||||
*/
|
||||
disableCaptions(): void {
|
||||
for (
|
||||
let i = 0;
|
||||
i < this.videoPlayer.nativeElement.textTracks.length;
|
||||
i++
|
||||
) {
|
||||
this.videoPlayer.nativeElement.textTracks[i].mode = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles promise based play operation
|
||||
*/
|
||||
handlePlayOperation(): void {
|
||||
const playPromise = this.videoPlayer.nativeElement.play();
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(() => {
|
||||
// Automatic playback started!
|
||||
if (!this.showCaptions) {
|
||||
this.disableCaptions();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Do nothing
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save volume when user changes it
|
||||
*/
|
||||
onVolumeChange(): void {
|
||||
const currentVolume = this.videoPlayer.nativeElement.volume;
|
||||
console.log('Volume changed to:', currentVolume);
|
||||
localStorage.setItem('volume', currentVolume.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy hls instance on component destroy and clean up event listener
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.videoPlayer.nativeElement.removeEventListener(
|
||||
'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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user