mirror of
https://github.com/xuthus83/pigallery2.git
synced 2024-11-03 21:04:03 +08:00
implementing video transcoding settings
This commit is contained in:
parent
f8a361cb9e
commit
c8a4a6429d
@ -77,13 +77,17 @@ export interface TaskConfig {
|
||||
scheduled: TaskScheduleDTO[];
|
||||
}
|
||||
|
||||
export type codecType = 'libvpx-vp9' | 'libx264' | 'libvpx' | 'libx265';
|
||||
export type resolutionType = 240 | 360 | 480 | 720 | 1080 | 1440 | 2160 | 4320;
|
||||
export type formatType = 'mp4' | 'webm';
|
||||
|
||||
export interface VideoConfig {
|
||||
transcoding: {
|
||||
bitRate: number,
|
||||
resolution: 240 | 360 | 480 | 720 | 1080 | 1440 | 2160 | 4320,
|
||||
resolution: resolutionType,
|
||||
fps: number,
|
||||
codec: 'libvpx-vp9' | 'libx264' | 'libvpx',
|
||||
format: 'mp4' | 'webm'
|
||||
codec: codecType,
|
||||
format: formatType
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,6 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
|
||||
.switch-wrapper {
|
||||
display: inline-block;
|
||||
|
@ -102,8 +102,8 @@
|
||||
|
||||
<div *ngIf="Progress != null">
|
||||
<span class="progress-details" i18n>indexing</span>: {{Progress.comment}} <br/>
|
||||
<span class="progress-details" i18n>elapsed</span>: {{TimeElapsed | duration}}<br/>
|
||||
<span class="progress-details" i18n>left</span>: {{TimeLeft | duration}}
|
||||
<span class="progress-details" i18n>elapsed</span>: {{tasksService.calcTimeElapsed(Progress) | duration}}<br/>
|
||||
<span class="progress-details" i18n>left</span>: {{tasksService.calcTimeLeft(Progress) | duration}}
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success"
|
||||
role="progressbar"
|
||||
|
@ -45,18 +45,6 @@ export class IndexingSettingsComponent extends SettingsComponent<IndexingConfig,
|
||||
return this.tasksService.progress.value[DefaultsTasks[DefaultsTasks.Indexing]];
|
||||
}
|
||||
|
||||
get TimeLeft(): number {
|
||||
if (this.Progress) {
|
||||
return (this.Progress.time.current - this.Progress.time.start) / this.Progress.progress * this.Progress.left;
|
||||
}
|
||||
}
|
||||
|
||||
get TimeElapsed() {
|
||||
if (this.Progress) {
|
||||
return (this.Progress.time.current - this.Progress.time.start);
|
||||
}
|
||||
}
|
||||
|
||||
get excludeFolderList(): string {
|
||||
return this.settings.excludeFolderList.join(';');
|
||||
}
|
||||
|
@ -42,6 +42,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
<p class="title" i18n>Misc:</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="enableOnScrollThumbnailPrioritising" i18n>Scroll based thumbnail
|
||||
@ -128,6 +129,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
<p class="title" i18n>Navigation bar:</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="showItemCount" [hidden]="simplifiedMode" i18n>Show item count</label>
|
||||
|
@ -7,70 +7,81 @@ import {NetworkService} from '../../model/network/network.service';
|
||||
export class ScheduledTasksService {
|
||||
|
||||
|
||||
public progress: BehaviorSubject<{ [key: string]: TaskProgressDTO }>;
|
||||
public onTaskFinish: EventEmitter<string> = new EventEmitter<string>();
|
||||
timer: number = null;
|
||||
private subscribers = 0;
|
||||
public progress: BehaviorSubject<{ [key: string]: TaskProgressDTO }>;
|
||||
public onTaskFinish: EventEmitter<string> = new EventEmitter<string>();
|
||||
timer: number = null;
|
||||
private subscribers = 0;
|
||||
|
||||
constructor(private _networkService: NetworkService) {
|
||||
this.progress = new BehaviorSubject({});
|
||||
}
|
||||
|
||||
|
||||
subscribeToProgress(): void {
|
||||
this.incSubscribers();
|
||||
}
|
||||
|
||||
unsubscribeFromProgress(): void {
|
||||
this.decSubscribers();
|
||||
}
|
||||
|
||||
public forceUpdate() {
|
||||
return this.getProgress();
|
||||
}
|
||||
|
||||
public async start(id: string, config?: any): Promise<void> {
|
||||
await this._networkService.postJson('/admin/tasks/scheduled/' + id + '/start', {config: config});
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
public async stop(id: string): Promise<void> {
|
||||
await this._networkService.postJson('/admin/tasks/scheduled/' + id + '/stop');
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
protected async getProgress() {
|
||||
const prevPrg = this.progress.value;
|
||||
this.progress.next(await this._networkService.getJson<{ [key: string]: TaskProgressDTO }>('/admin/tasks/scheduled/progress'));
|
||||
for (const prg in prevPrg) {
|
||||
if (!this.progress.value.hasOwnProperty(prg)) {
|
||||
this.onTaskFinish.emit(prg);
|
||||
}
|
||||
constructor(private _networkService: NetworkService) {
|
||||
this.progress = new BehaviorSubject({});
|
||||
}
|
||||
}
|
||||
|
||||
protected getProgressPeriodically() {
|
||||
if (this.timer != null || this.subscribers === 0) {
|
||||
return;
|
||||
public calcTimeElapsed(progress: TaskProgressDTO) {
|
||||
if (progress) {
|
||||
return (progress.time.current - progress.time.start);
|
||||
}
|
||||
}
|
||||
let repeatTime = 5000;
|
||||
if (Object.values(this.progress.value).length === 0) {
|
||||
repeatTime = 10000;
|
||||
|
||||
public calcTimeLeft(progress: TaskProgressDTO) {
|
||||
if (progress) {
|
||||
return (progress.time.current - progress.time.start) / progress.progress * progress.left;
|
||||
}
|
||||
}
|
||||
this.timer = window.setTimeout(async () => {
|
||||
await this.getProgress();
|
||||
this.timer = null;
|
||||
this.getProgressPeriodically();
|
||||
}, repeatTime);
|
||||
}
|
||||
|
||||
private incSubscribers() {
|
||||
this.subscribers++;
|
||||
this.getProgressPeriodically();
|
||||
}
|
||||
subscribeToProgress(): void {
|
||||
this.incSubscribers();
|
||||
}
|
||||
|
||||
private decSubscribers() {
|
||||
this.subscribers--;
|
||||
}
|
||||
unsubscribeFromProgress(): void {
|
||||
this.decSubscribers();
|
||||
}
|
||||
|
||||
public forceUpdate() {
|
||||
return this.getProgress();
|
||||
}
|
||||
|
||||
public async start(id: string, config?: any): Promise<void> {
|
||||
await this._networkService.postJson('/admin/tasks/scheduled/' + id + '/start', {config: config});
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
public async stop(id: string): Promise<void> {
|
||||
await this._networkService.postJson('/admin/tasks/scheduled/' + id + '/stop');
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
protected async getProgress() {
|
||||
const prevPrg = this.progress.value;
|
||||
this.progress.next(await this._networkService.getJson<{ [key: string]: TaskProgressDTO }>('/admin/tasks/scheduled/progress'));
|
||||
for (const prg in prevPrg) {
|
||||
if (!this.progress.value.hasOwnProperty(prg)) {
|
||||
this.onTaskFinish.emit(prg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getProgressPeriodically() {
|
||||
if (this.timer != null || this.subscribers === 0) {
|
||||
return;
|
||||
}
|
||||
let repeatTime = 5000;
|
||||
if (Object.values(this.progress.value).length === 0) {
|
||||
repeatTime = 10000;
|
||||
}
|
||||
this.timer = window.setTimeout(async () => {
|
||||
await this.getProgress();
|
||||
this.timer = null;
|
||||
this.getProgressPeriodically();
|
||||
}, repeatTime);
|
||||
}
|
||||
|
||||
private incSubscribers() {
|
||||
this.subscribers++;
|
||||
this.getProgressPeriodically();
|
||||
}
|
||||
|
||||
private decSubscribers() {
|
||||
this.subscribers--;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
.buttons-row {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
<form #settingsForm="ngForm" class="form-horizontal">
|
||||
<div class="card mb-4">
|
||||
<h5 class="card-header">
|
||||
<ng-container i18n>Video settings</ng-container><ng-container *ngIf="changed">*</ng-container>
|
||||
<ng-container i18n>Video settings</ng-container>
|
||||
<ng-container *ngIf="changed">*</ng-container>
|
||||
<div class="switch-wrapper">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
@ -13,14 +14,88 @@
|
||||
[switch-disabled]="inProgress"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
[(ngModel)]="settings.client.enabled">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
<ng-container i18n>Video support uses ffmpeg. ffmpeg and ffprobe binaries need to be available in the PATH or the @ffmpeg-installer/ffmpeg and @ffprobe-installer/ffprobe optional node packages need to be installed.</ng-container>
|
||||
<ng-container i18n>Video support uses ffmpeg. ffmpeg and ffprobe binaries need to be available in the PATH or the
|
||||
@ffmpeg-installer/ffmpeg and @ffprobe-installer/ffprobe optional node packages need to be installed.
|
||||
</ng-container>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p class="title" i18n>Video transcoding:</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="format" i18n>Format</label>
|
||||
<div class="col-md-10">
|
||||
<select id="format" class="form-control" [(ngModel)]="settings.server.transcoding.format"
|
||||
(ngModelChange)="formatChanged($event)"
|
||||
name="format" required>
|
||||
<option *ngFor="let format of formats" [ngValue]="format">{{format}}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>Select the format to encode your videos to.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="codec" i18n>Codec</label>
|
||||
<div class="col-md-10">
|
||||
<select id="codec" class="form-control" [(ngModel)]="settings.server.transcoding.codec"
|
||||
name="codec" required>
|
||||
<option *ngFor="let codec of codecs[settings.server.transcoding.format]" [ngValue]="codec">{{codec}}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>This codec will be used for the transcoding. </small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="resolution" i18n>Resolution</label>
|
||||
<div class="col-md-10">
|
||||
<select id="resolution" class="form-control" [(ngModel)]="settings.server.transcoding.resolution"
|
||||
(ngModelChange)="updateBitRate()"
|
||||
name="resolution" required>
|
||||
<option *ngFor="let resolution of resolutions" [ngValue]="resolution">{{resolution}}p
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>Resolution of the output video.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="fps" i18n>FPS</label>
|
||||
<div class="col-md-10">
|
||||
<select id="fps" class="form-control" [(ngModel)]="settings.server.transcoding.fps"
|
||||
(ngModelChange)="updateBitRate()"
|
||||
name="fps" required>
|
||||
<option *ngFor="let fps of fps" [ngValue]="fps">{{fps}}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>Target frame per second (fps) of the output video.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="bitRate" i18n>Bit rate</label>
|
||||
<div class="input-group col-md-10">
|
||||
<input type="number" class="form-control" placeholder="2"
|
||||
id="bitRate"
|
||||
min="0"
|
||||
step="0.1"
|
||||
max="1000"
|
||||
[(ngModel)]="bitRate"
|
||||
name="bitRate" required>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">mbps</span>
|
||||
</div>
|
||||
<small class="form-text text-muted" i18n>Target bit rate for the output video. This should be less than the
|
||||
upload rate of your home server.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
@ -31,6 +106,56 @@
|
||||
[disabled]=" !changed || inProgress"
|
||||
(click)="reset()" i18n>Reset
|
||||
</button>
|
||||
|
||||
<br/>
|
||||
<hr/>
|
||||
|
||||
<ng-container i18n>To ensure smooth video playback, video transcoding is recommended to a lower bit rate than the
|
||||
server's upload rate.
|
||||
</ng-container>
|
||||
<ng-container i18n>The transcoded videos will be save to the thumbnail folder.</ng-container>
|
||||
<ng-container i18n>You can trigger the transcoding manually, but you can also create an automatic encoding task in
|
||||
advanced settings mode.
|
||||
</ng-container>
|
||||
|
||||
<br/>
|
||||
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
*ngIf="Progress == null"
|
||||
[disabled]="inProgress"
|
||||
title="Indexes the folders"
|
||||
i18n-title
|
||||
(click)="transcode()" i18n>Transcode videos now
|
||||
</button>
|
||||
<button class="btn btn-secondary float-right"
|
||||
*ngIf="Progress != null"
|
||||
[disabled]="inProgress"
|
||||
(click)="cancelTranscoding()" i18n>Cancel transcoding
|
||||
</button>
|
||||
|
||||
|
||||
<ng-container *ngIf="Progress != null">
|
||||
<br/>
|
||||
<hr/>
|
||||
<span class="progress-details" i18n>indexing</span>: {{Progress.comment}} <br/>
|
||||
<span class="progress-details" i18n>elapsed</span>: {{tasksService.calcTimeElapsed(Progress) | duration}}<br/>
|
||||
<span class="progress-details" i18n>left</span>: {{tasksService.calcTimeLeft(Progress) | duration}}
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success"
|
||||
role="progressbar"
|
||||
aria-valuenow="2"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style="min-width: 2em;"
|
||||
[style.width.%]="(Progress.progress/(Progress.left+Progress.progress))*100">
|
||||
{{Progress.progress}}
|
||||
/{{Progress.progress + Progress.left}}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -6,6 +6,10 @@ import {NavigationService} from '../../../model/navigation.service';
|
||||
import {NotificationService} from '../../../model/notification.service';
|
||||
import {ClientConfig} from '../../../../../common/config/public/ConfigClass';
|
||||
import {I18n} from '@ngx-translate/i18n-polyfill';
|
||||
import {codecType, formatType, resolutionType, VideoConfig} from '../../../../../common/config/private/IPrivateConfig';
|
||||
import {ScheduledTasksService} from '../scheduled-tasks.service';
|
||||
import {DefaultsTasks} from '../../../../../common/entities/task/TaskDTO';
|
||||
import {ErrorDTO} from '../../../../../common/entities/Error';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -15,17 +19,108 @@ import {I18n} from '@ngx-translate/i18n-polyfill';
|
||||
'./../_abstract/abstract.settings.component.css'],
|
||||
providers: [VideoSettingsService],
|
||||
})
|
||||
export class VideoSettingsComponent extends SettingsComponent<ClientConfig.VideoConfig> {
|
||||
export class VideoSettingsComponent extends SettingsComponent<{ server: VideoConfig, client: ClientConfig.VideoConfig }> {
|
||||
|
||||
resolutions: resolutionType[] = [360, 480, 720, 1080, 1440, 2160, 4320];
|
||||
codecs: { [key: string]: codecType[] } = {webm: ['libvpx', 'libvpx-vp9'], mp4: ['libx264', 'libx265']};
|
||||
formats: formatType[] = ['mp4', 'webm'];
|
||||
fps = [24, 25, 30, 48, 50, 60];
|
||||
|
||||
constructor(_authService: AuthenticationService,
|
||||
_navigation: NavigationService,
|
||||
_settingsService: VideoSettingsService,
|
||||
public tasksService: ScheduledTasksService,
|
||||
notification: NotificationService,
|
||||
i18n: I18n) {
|
||||
super(i18n('Video'), _authService, _navigation, <any>_settingsService, notification, i18n, s => s.Client.Video);
|
||||
super(i18n('Video'), _authService, _navigation, <any>_settingsService, notification, i18n, s => ({
|
||||
client: s.Client.Video,
|
||||
server: s.Server.Video
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
get Progress() {
|
||||
return this.tasksService.progress.value[DefaultsTasks[DefaultsTasks['Video Converting']]];
|
||||
}
|
||||
|
||||
get bitRate(): number {
|
||||
return this.settings.server.transcoding.bitRate / 1024 / 1024;
|
||||
}
|
||||
|
||||
set bitRate(value: number) {
|
||||
this.settings.server.transcoding.bitRate = Math.round(value * 1024 * 1024);
|
||||
}
|
||||
|
||||
getRecommendedBitRate(resolution: number, fps: number) {
|
||||
let bitRate = 1024 * 1024;
|
||||
if (resolution <= 360) {
|
||||
bitRate = 1024 * 1024;
|
||||
} else if (resolution <= 480) {
|
||||
bitRate = 2.5 * 1024 * 1024;
|
||||
} else if (resolution <= 720) {
|
||||
bitRate = 5 * 1024 * 1024;
|
||||
} else if (resolution <= 1080) {
|
||||
bitRate = 8 * 1024 * 1024;
|
||||
} else if (resolution <= 1440) {
|
||||
bitRate = 16 * 1024 * 1024;
|
||||
} else if (resolution <= 2016) {
|
||||
bitRate = 40 * 1024 * 1024;
|
||||
}
|
||||
|
||||
if (fps > 30) {
|
||||
bitRate *= 1.5;
|
||||
}
|
||||
|
||||
return bitRate;
|
||||
}
|
||||
|
||||
updateBitRate() {
|
||||
this.settings.server.transcoding.bitRate = this.getRecommendedBitRate(this.settings.server.transcoding.resolution,
|
||||
this.settings.server.transcoding.fps);
|
||||
}
|
||||
|
||||
formatChanged(format: formatType) {
|
||||
this.settings.server.transcoding.codec = this.codecs[format][0];
|
||||
}
|
||||
|
||||
|
||||
async transcode() {
|
||||
this.inProgress = true;
|
||||
this.error = '';
|
||||
try {
|
||||
await this.tasksService.start(DefaultsTasks[DefaultsTasks['Video Converting']]);
|
||||
this.notification.info(this.i18n('Video transcoding started'));
|
||||
this.inProgress = false;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err.message) {
|
||||
this.error = (<ErrorDTO>err).message;
|
||||
}
|
||||
}
|
||||
|
||||
this.inProgress = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
async cancelTranscoding() {
|
||||
this.inProgress = true;
|
||||
this.error = '';
|
||||
try {
|
||||
await this.tasksService.stop(DefaultsTasks[DefaultsTasks['Video Converting']]);
|
||||
this.notification.info(this.i18n('Video transcoding interrupted'));
|
||||
this.inProgress = false;
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (err.message) {
|
||||
this.error = (<ErrorDTO>err).message;
|
||||
}
|
||||
}
|
||||
|
||||
this.inProgress = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user