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 = []; lastJobSuccess: boolean = false; welcomeHeader: any = null; processRemux: boolean = true; wantToUpscale: 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, wantToUpscale: this.wantToUpscale }); 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 true; 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 }[] { if (keysString == null || keysString.length === 0) return []; if (Array.isArray(keysString)) return keysString; 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); } }