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

View File

@@ -66,7 +66,7 @@
<i class="fas fa-download text-primary me-2"></i>
<h6 class="mb-0 fw-semibold">Downloader</h6>
</div>
<div class="mb-3">
<small class="text-muted d-block mb-1">Installed binary</small>
<span *ngIf="welcomeHeader.downloader.filsIsPresent" class="badge bg-success bg-opacity-10 text-success border border-success">
@@ -76,7 +76,7 @@
<i class="fas fa-exclamation-triangle me-1"></i>No
</span>
</div>
<div>
<small class="text-muted d-block mb-1">Update available</small>
<div class="d-flex align-items-center gap-2">
@@ -86,7 +86,7 @@
<span *ngIf="!welcomeHeader.downloader.newReleaseAvailable" class="badge bg-success bg-opacity-10 text-success border border-success">
<i class="fas fa-circle-check me-1"></i>No
</span>
<button *ngIf="welcomeHeader.downloader.newReleaseAvailable" [disabled]="welcomeHeader.downloader.isUpdating" (click)="processUpdate(welcomeHeader.downloader, 'downloader')" class="btn btn-sm btn-primary">
<i *ngIf="welcomeHeader.downloader.isUpdating" class="fas fa-circle-notch fa-spin me-1"></i>
<i *ngIf="!welcomeHeader.downloader.isUpdating" class="fas fa-arrow-up me-1"></i>
@@ -96,14 +96,14 @@
</div>
</div>
</div>
<div class="col-md-6">
<div class="border-start border-4 border-info ps-3 h-100">
<div class="d-flex align-items-center mb-3">
<i class="fas fa-key text-info me-2"></i>
<h6 class="mb-0 fw-semibold">Content Decrypter</h6>
</div>
<div class="mb-3">
<small class="text-muted d-block mb-1">Installed binary</small>
<span *ngIf="welcomeHeader.mp4decrypt.filsIsPresent" class="badge bg-success bg-opacity-10 text-success border border-success">
@@ -113,7 +113,7 @@
<i class="fas fa-exclamation-triangle me-1"></i>No
</span>
</div>
<div>
<small class="text-muted d-block mb-1">Update available</small>
<div class="d-flex align-items-center gap-2">
@@ -123,7 +123,7 @@
<span *ngIf="!welcomeHeader.mp4decrypt.newReleaseAvailable" class="badge bg-success bg-opacity-10 text-success border border-success">
<i class="fas fa-circle-check me-1"></i>No
</span>
<button *ngIf="welcomeHeader.mp4decrypt.newReleaseAvailable" [disabled]="welcomeHeader.mp4decrypt.isUpdating" (click)="processUpdate(welcomeHeader.mp4decrypt, 'mp4decrypt')" class="btn btn-sm btn-primary">
<i *ngIf="welcomeHeader.mp4decrypt.isUpdating" class="fas fa-circle-notch fa-spin me-1"></i>
<i *ngIf="!welcomeHeader.mp4decrypt.isUpdating" class="fas fa-arrow-up me-1"></i>
@@ -134,7 +134,7 @@
</div>
</div>
</div>
<div class="row" *ngIf="!welcomeHeader">
<div class="col-12 text-center py-4">
<i class="fas fa-circle-notch fa-2x fa-spin text-primary"></i>
@@ -166,7 +166,7 @@
<div class="flex flex-col mt-2">
<label>Wanted Resolution</label>
<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>
</div>
</div>

View File

@@ -4,6 +4,7 @@ import { ReactiveFormsModule, FormBuilder, FormGroup, Validators, FormsModule }
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',
@@ -24,6 +25,9 @@ export class AppComponent implements OnInit {
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({
@@ -37,7 +41,9 @@ export class AppComponent implements OnInit {
}
constructor(private fb: FormBuilder,
private videoProcessingService: VideoProcessingService) {
private videoProcessingService: VideoProcessingService,
private router: Router,
private route: ActivatedRoute) {
this.processingForm = this.fb.group({
keys: ['', Validators.required],
wantedResolution: ['1920x1080', Validators.required]
@@ -51,6 +57,117 @@ export class AppComponent implements OnInit {
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) {
@@ -130,16 +247,31 @@ export class AppComponent implements OnInit {
console.log(data);
this.videoProcessingService.startProcess(data).subscribe({
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) => {
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) {
if (tracks.length === 0)
return 'None';
@@ -153,9 +285,22 @@ export class AppComponent implements OnInit {
this.videoProcessingService.load(formData).subscribe({
next: (response) => {
console.log(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) => {
console.error('Error starting process:', error);
@@ -165,7 +310,6 @@ export class AppComponent implements OnInit {
}
parseKeys(keysString: string): { key: string, value: string }[] {
console.log('KS =', keysString);
return keysString.split('\n').map(line => line.trim())
.filter(line => line.includes(':'))
.map(line => {
@@ -180,14 +324,10 @@ export class AppComponent implements OnInit {
next: (response) => {
this.lastJobSuccess = true;
this.jobs = response;
// if (['completed', 'failed'].includes(response.state)) {
// clearInterval(interval);
// }
},
error: (error) => {
console.error('Error fetching job status:', error);
this.lastJobSuccess = false;
//clearInterval(interval);
}
});
}, 1500);