335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import { Component, OnInit } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule } from '@angular/forms';
|
|
import { VideoProcessingService } from './video-processing.service';
|
|
import { MomentModule } from 'ngx-moment';
|
|
import { OrderModule } from 'ngx-order-pipe';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
|
@Component({
|
|
selector: 'app-root',
|
|
standalone: true,
|
|
imports: [CommonModule, ReactiveFormsModule, MomentModule, OrderModule, FormsModule],
|
|
templateUrl: './app.component.html',
|
|
styles: []
|
|
})
|
|
export class AppComponent implements OnInit {
|
|
processingForm: FormGroup;
|
|
loadForm: FormGroup;
|
|
jobId: string | null = null;
|
|
jobStatus: string | null = null;
|
|
jobProgress: number = 0;
|
|
jobs: Array<any> = [];
|
|
lastJobSuccess: boolean = false;
|
|
welcomeHeader: any = null;
|
|
processRemux: boolean = true;
|
|
loaded: any = null;
|
|
Math: any = Math;
|
|
|
|
// Indicateur pour le préchargement depuis l'extension
|
|
isPreloadedFromExtension: boolean = false;
|
|
|
|
initForm() {
|
|
this.processingForm = this.fb.group({
|
|
keys: ['', Validators.required],
|
|
wantedResolution: ['1920x1080', Validators.required]
|
|
});
|
|
this.loadForm = this.fb.group({
|
|
mp4Filename: ['test', Validators.required],
|
|
mpdUrl: ['https://bakery.pplus.paramount.tech/l(de,it,no,fi,da,sv,es-MX,pt-BR,es-mx,pt-br)/paramountplus/2023/11/06/2279898691624/2416513_cenc_precon_dash/stream.mpd?CMCD=ot%3Dm%2Csf%3Dd%2Csid%3D%22295d7a15-c79d-4229-b593-7abdacd727c9%22%2Csu', Validators.required],
|
|
});
|
|
}
|
|
|
|
constructor(private fb: FormBuilder,
|
|
private videoProcessingService: VideoProcessingService,
|
|
private router: Router,
|
|
private route: ActivatedRoute) {
|
|
this.processingForm = this.fb.group({
|
|
keys: ['', Validators.required],
|
|
wantedResolution: ['1920x1080', Validators.required]
|
|
});
|
|
this.loadForm = this.fb.group({
|
|
mp4Filename: ['test', Validators.required],
|
|
mpdUrl: ['https://bakery.pplus.paramount.tech/l(de,it,no,fi,da,sv,es-MX,pt-BR,es-mx,pt-br)/paramountplus/2023/11/06/2279898691624/2416513_cenc_precon_dash/stream.mpd?CMCD=ot%3Dm%2Csf%3Dd%2Csid%3D%22295d7a15-c79d-4229-b593-7abdacd727c9%22%2Csu', Validators.required],
|
|
});
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.startPollingJobsStatus();
|
|
this.sayHello();
|
|
this.checkForExtensionPreload();
|
|
const keysControl = this.processingForm.get('keys');
|
|
|
|
if (keysControl) {
|
|
keysControl.valueChanges.subscribe(value => {
|
|
const keys = this.parseKeys(value);
|
|
const videoTracks = this.loaded?.videoTracks || [];
|
|
const decryptableResolutions = videoTracks.filter((res: any) => {
|
|
if (!res.defaultKID || res.defaultKID.length === 0)
|
|
return false;
|
|
return keys.find(k => k.key.toLowerCase() === res.defaultKID.toLowerCase());
|
|
});
|
|
if (decryptableResolutions.length > 0) {
|
|
this.processingForm.get('wantedResolution')?.enable();
|
|
const currentResolution = this.processingForm.get('wantedResolution')?.value;
|
|
if (!decryptableResolutions.find((res: any) => res.name === currentResolution)) {
|
|
this.processingForm.get('wantedResolution')?.setValue(decryptableResolutions[0].name);
|
|
}
|
|
} else {
|
|
this.processingForm.get('wantedResolution')?.disable();
|
|
this.processingForm.get('wantedResolution')?.setValue(null);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie si l'application a été ouverte avec des paramètres depuis l'extension
|
|
*/
|
|
private checkForExtensionPreload(): void {
|
|
this.route.queryParams.subscribe(params => {
|
|
if (params['mpdUrl'] && params['keys'] && params['autoLoad']) {
|
|
this.preloadFromExtension(params['mpdUrl'], params['keys']);
|
|
// this.router.navigate([], {
|
|
// queryParams: {
|
|
// mpdUrl: null,
|
|
// keys: null,
|
|
// autoLoad: null
|
|
// },
|
|
// queryParamsHandling: 'merge'
|
|
// });
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Précharge les données depuis l'extension Widevine
|
|
*/
|
|
private preloadFromExtension(mpdUrl: string, keys: string): void {
|
|
console.log('Preloading data from Widevine extension:', { mpdUrl, keys });
|
|
|
|
this.isPreloadedFromExtension = true;
|
|
|
|
const filename = this.generateFilenameFromMPD(mpdUrl);
|
|
|
|
this.loadForm.patchValue({
|
|
mpdUrl: mpdUrl,
|
|
mp4Filename: filename
|
|
});
|
|
|
|
this.processingForm.patchValue({
|
|
keys: keys
|
|
});
|
|
|
|
setTimeout(() => {
|
|
this.onSubmitLoad();
|
|
}, 500);
|
|
}
|
|
|
|
/**
|
|
* Génère un nom de fichier intelligent basé sur l'URL MPD
|
|
*/
|
|
private generateFilenameFromMPD(mpdUrl: string): string {
|
|
try {
|
|
const url = new URL(mpdUrl);
|
|
|
|
// Extraire des informations de l'URL
|
|
let filename = 'content';
|
|
|
|
// Patterns spécifiques pour différents CDNs
|
|
if (url.hostname.includes('canalplus') || url.hostname.includes('paramount')) {
|
|
// Exemple: paramplus.com_HDCTF601GLFR
|
|
const pathMatch = url.pathname.match(/([A-Z0-9_]+)\/\d+\//);
|
|
if (pathMatch) {
|
|
filename = pathMatch[1].toLowerCase().replace(/_/g, '-');
|
|
}
|
|
} else if (url.pathname.includes('/')) {
|
|
// Utiliser le dernier segment sans extension
|
|
const segments = url.pathname.split('/').filter(s => s);
|
|
const lastSegment = segments[segments.length - 1];
|
|
filename = lastSegment.replace('.mpd', '').replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
}
|
|
|
|
// Ajouter un timestamp pour éviter les collisions
|
|
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
return `${filename}_${timestamp}`;
|
|
|
|
} catch (error) {
|
|
console.warn('Could not parse MPD URL for filename generation:', error);
|
|
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
return `widevine_content_${timestamp}`;
|
|
}
|
|
}
|
|
|
|
private clearURLParams(): void {
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.delete('mpdUrl');
|
|
url.searchParams.delete('keys');
|
|
url.searchParams.delete('autoLoad');
|
|
|
|
window.history.replaceState({}, document.title, url.toString());
|
|
}
|
|
|
|
downloadFileFromAPI(filePath: string, filename: string, job: any) {
|
|
job.isDownloading = true;
|
|
this.videoProcessingService.downloadFile(filePath).subscribe({
|
|
next: (response) => {
|
|
const blob = new Blob([response], { type: 'video/mp4' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
job.isDownloading = false;
|
|
},
|
|
error: (error) => {
|
|
console.error('Error downloading file:', error);
|
|
job.isDownloading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
processUpdate(item: any, binType: string) {
|
|
item.isUpdating = true;
|
|
|
|
this.videoProcessingService.processUpdate(Object.assign(item, {binType})).subscribe({
|
|
next: (response) => {
|
|
item.isUpdating = false;
|
|
this.welcomeHeader = null;
|
|
this.sayHello();
|
|
},
|
|
error: (error) => {
|
|
console.error('Hello failed', error);
|
|
item.isUpdating = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
sayHello() {
|
|
this.videoProcessingService.hello().subscribe({
|
|
next: (response) => {
|
|
console.log('Hello success', response);
|
|
this.welcomeHeader = response;
|
|
},
|
|
error: (error) => {
|
|
console.error('Hello failed', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
flushQueue() {
|
|
this.videoProcessingService.flushQueue().subscribe({
|
|
next: (response) => {
|
|
},
|
|
error: (error) => {
|
|
}
|
|
});
|
|
}
|
|
|
|
humanFileSize(size: number) {
|
|
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
|
return +((size / Math.pow(1024, i)).toFixed(2)) * 1 + ' ' + ['bps', 'kbps', 'mbps', 'gbps', 'tbps'][i];
|
|
}
|
|
|
|
onSubmit() {
|
|
if (this.processingForm.valid) {
|
|
const formData = this.processingForm.value;
|
|
const data = Object.assign(formData, {
|
|
keys: this.parseKeys(formData.keys),
|
|
wantedAudioTracks: this.loaded.audioTracks.filter((track: any) => track.selected),
|
|
wantedSubtitles: this.loaded.subtitles.filter((sub: any) => sub.selected),
|
|
wantedResolution: this.loaded.videoTracks.find((res: any) => res.name === formData.wantedResolution),
|
|
mpdUrl: this.loadForm.get('mpdUrl')?.value,
|
|
mp4Filename: this.loadForm.get('mp4Filename')?.value,
|
|
wantedRemux: this.processRemux
|
|
});
|
|
console.log(data);
|
|
this.videoProcessingService.startProcess(data).subscribe({
|
|
next: (response) => {
|
|
// Nettoyer les paramètres URL après avoir démarré le processus
|
|
if (this.isPreloadedFromExtension) {
|
|
this.clearURLParams();
|
|
this.isPreloadedFromExtension = false;
|
|
}
|
|
},
|
|
error: (error) => {
|
|
console.error('Error starting process:', error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
getFirstDecryptableResolution() {
|
|
const decryptableResolutions = this.loaded.videoTracks.filter((res: any) => this.isDecryptable(res));
|
|
return decryptableResolutions.length > 0 ? decryptableResolutions[0].name : null;
|
|
}
|
|
|
|
isDecryptable(res: any) {
|
|
if (!res.defaultKID || res.defaultKID.length === 0)
|
|
return false;
|
|
const keys = this.parseKeys(this.processingForm.value.keys);
|
|
return keys.find(k => k.key.toLowerCase() === res.defaultKID.toLowerCase());
|
|
}
|
|
|
|
displayJobAudio(tracks: any) {
|
|
if (tracks.length === 0)
|
|
return 'None';
|
|
return tracks.map((elem: any) => `${elem.name}${elem.attributes?.CODECS ? ` (${elem.attributes.CODECS || 'N/A'})` : ''}`).join(' + ');
|
|
}
|
|
|
|
onSubmitLoad() {
|
|
if (this.loadForm.valid) {
|
|
this.loaded = null;
|
|
const formData = this.loadForm.value;
|
|
|
|
this.videoProcessingService.load(formData).subscribe({
|
|
next: (response) => {
|
|
this.loaded = response;
|
|
|
|
if (this.isPreloadedFromExtension) {
|
|
console.log('MPD loaded successfully from Widevine extension');
|
|
const firstDecryptable = this.getFirstDecryptableResolution();
|
|
if (firstDecryptable) {
|
|
this.processingForm.patchValue({
|
|
wantedResolution: firstDecryptable
|
|
});
|
|
this.processingForm.get('wantedResolution')?.enable();
|
|
}
|
|
} else {
|
|
console.log('MPD loaded successfully', response);
|
|
// disable formData wantedResolution
|
|
this.processingForm.get('wantedResolution')?.disable();
|
|
}
|
|
},
|
|
error: (error) => {
|
|
console.error('Error starting process:', error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
parseKeys(keysString: string): { key: string, value: string }[] {
|
|
return keysString.split('\n').map(line => line.trim())
|
|
.filter(line => line.includes(':'))
|
|
.map(line => {
|
|
const [key, value] = line.split(':').map(part => part.trim());
|
|
return { key, value };
|
|
});
|
|
}
|
|
|
|
startPollingJobsStatus() {
|
|
const interval = setInterval(() => {
|
|
this.videoProcessingService.getJobsStatus().subscribe({
|
|
next: (response) => {
|
|
this.lastJobSuccess = true;
|
|
this.jobs = response;
|
|
},
|
|
error: (error) => {
|
|
console.error('Error fetching job status:', error);
|
|
this.lastJobSuccess = false;
|
|
}
|
|
});
|
|
}, 1500);
|
|
}
|
|
} |