Proyecto App Linux

This commit is contained in:
2026-05-03 23:43:09 +02:00
commit 7482733843
1248 changed files with 189750 additions and 0 deletions
@@ -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();
}
}
}