Add Widevine Plugin support
All checks were successful
ci / Image build (push) Successful in 2m13s
ci / Deployment (push) Successful in 7s

This commit is contained in:
Joris Bertomeu
2025-08-26 10:42:30 +02:00
parent fedc2250db
commit 847c30b112
3 changed files with 159 additions and 19 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -166,7 +166,7 @@
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label>Wanted Resolution</label> <label>Wanted Resolution</label>
<select formControlName="wantedResolution" class="form-select"> <select formControlName="wantedResolution" class="form-select">
<option [value]="res.name" *ngFor="let res of loaded.videoTracks">{{res.name}} ({{res.codec}} - {{humanFileSize(res.bandwidth)}})</option> <option [disabled]="!isDecryptable(res)" [value]="res.name" *ngFor="let res of loaded.videoTracks">{{res.name}} ({{res.codec}} - {{humanFileSize(res.bandwidth)}})</option>
</select> </select>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,7 @@ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule }
import { VideoProcessingService } from './video-processing.service'; import { VideoProcessingService } from './video-processing.service';
import { MomentModule } from 'ngx-moment'; import { MomentModule } from 'ngx-moment';
import { OrderModule } from 'ngx-order-pipe'; import { OrderModule } from 'ngx-order-pipe';
import { ActivatedRoute, Router } from '@angular/router';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -25,6 +26,9 @@ export class AppComponent implements OnInit {
loaded: any = null; loaded: any = null;
Math: any = Math; Math: any = Math;
// Indicateur pour le préchargement depuis l'extension
isPreloadedFromExtension: boolean = false;
initForm() { initForm() {
this.processingForm = this.fb.group({ this.processingForm = this.fb.group({
keys: ['', Validators.required], keys: ['', Validators.required],
@@ -37,7 +41,9 @@ export class AppComponent implements OnInit {
} }
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private videoProcessingService: VideoProcessingService) { private videoProcessingService: VideoProcessingService,
private router: Router,
private route: ActivatedRoute) {
this.processingForm = this.fb.group({ this.processingForm = this.fb.group({
keys: ['', Validators.required], keys: ['', Validators.required],
wantedResolution: ['1920x1080', Validators.required] wantedResolution: ['1920x1080', Validators.required]
@@ -51,6 +57,117 @@ export class AppComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.startPollingJobsStatus(); this.startPollingJobsStatus();
this.sayHello(); 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) { downloadFileFromAPI(filePath: string, filename: string, job: any) {
@@ -130,16 +247,31 @@ export class AppComponent implements OnInit {
console.log(data); console.log(data);
this.videoProcessingService.startProcess(data).subscribe({ this.videoProcessingService.startProcess(data).subscribe({
next: (response) => { next: (response) => {
//this.jobId = response.jobId; // Nettoyer les paramètres URL après avoir démarré le processus
if (this.isPreloadedFromExtension) {
this.clearURLParams();
this.isPreloadedFromExtension = false;
}
}, },
error: (error) => { error: (error) => {
console.error('Error starting process:', error); console.error('Error starting process:', error);
} }
}); });
//this.initForm();
} }
} }
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) { displayJobAudio(tracks: any) {
if (tracks.length === 0) if (tracks.length === 0)
return 'None'; return 'None';
@@ -153,9 +285,22 @@ export class AppComponent implements OnInit {
this.videoProcessingService.load(formData).subscribe({ this.videoProcessingService.load(formData).subscribe({
next: (response) => { next: (response) => {
console.log(response);
this.loaded = response; this.loaded = response;
//this.jobId = response.jobId;
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) => { error: (error) => {
console.error('Error starting process:', error); console.error('Error starting process:', error);
@@ -165,7 +310,6 @@ export class AppComponent implements OnInit {
} }
parseKeys(keysString: string): { key: string, value: string }[] { parseKeys(keysString: string): { key: string, value: string }[] {
console.log('KS =', keysString);
return keysString.split('\n').map(line => line.trim()) return keysString.split('\n').map(line => line.trim())
.filter(line => line.includes(':')) .filter(line => line.includes(':'))
.map(line => { .map(line => {
@@ -180,14 +324,10 @@ export class AppComponent implements OnInit {
next: (response) => { next: (response) => {
this.lastJobSuccess = true; this.lastJobSuccess = true;
this.jobs = response; this.jobs = response;
// if (['completed', 'failed'].includes(response.state)) {
// clearInterval(interval);
// }
}, },
error: (error) => { error: (error) => {
console.error('Error fetching job status:', error); console.error('Error fetching job status:', error);
this.lastJobSuccess = false; this.lastJobSuccess = false;
//clearInterval(interval);
} }
}); });
}, 1500); }, 1500);