import { forkJoin } from 'rxjs'
import { map } from 'rxjs/operators'
-import { SelectChannelItem } from 'src/types/select-options-item.model'
+import { SelectChannelItem, SelectOptionsItem } from 'src/types/select-options-item.model'
import { ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
-import { AbstractControl, FormArray, FormControl, FormGroup, ValidationErrors, Validators } from '@angular/forms'
+import { AbstractControl, FormArray, FormControl, FormGroup, Validators } from '@angular/forms'
import { HooksService, PluginService, ServerService } from '@app/core'
import { removeElementFromArray } from '@app/helpers'
import { BuildFormValidator } from '@app/shared/form-validators'
} from '@app/shared/form-validators/video-validators'
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
import { InstanceService } from '@app/shared/shared-instance'
-import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
+import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { logger } from '@root-helpers/logger'
import { PluginInfo } from '@root-helpers/plugins-manager'
import {
HTMLServerConfig,
LiveVideo,
+ LiveVideoLatencyMode,
RegisterClientFormFieldOptions,
RegisterClientVideoFieldOptions,
VideoConstant,
VideoDetails,
VideoPrivacy
} from '@shared/models'
+import { VideoSource } from '@shared/models/videos/video-source'
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
+import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
import { VideoEditType } from './video-edit.type'
type VideoLanguages = VideoConstant<string> & { group?: string }
@Input() videoToUpdate: VideoDetails
@Input() userVideoChannels: SelectChannelItem[] = []
- @Input() schedulePublicationPossible = true
+ @Input() forbidScheduledPublication = true
- @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = []
+ @Input() videoCaptions: VideoCaptionWithPathEdit[] = []
+ @Input() videoSource: VideoSource
+
+ @Input() hideWaitTranscoding = false
- @Input() waitTranscodingEnabled = true
@Input() type: VideoEditType
@Input() liveVideo: LiveVideo
videoCategories: VideoConstant<number>[] = []
videoLicences: VideoConstant<number>[] = []
videoLanguages: VideoLanguages[] = []
+ latencyModes: SelectOptionsItem[] = [
+ {
+ id: LiveVideoLatencyMode.SMALL_LATENCY,
+ label: $localize`Small latency`,
+ description: $localize`Reduce latency to ~15s disabling P2P`
+ },
+ {
+ id: LiveVideoLatencyMode.DEFAULT,
+ label: $localize`Default`,
+ description: $localize`Average latency of 30s`
+ },
+ {
+ id: LiveVideoLatencyMode.HIGH_LATENCY,
+ label: $localize`High latency`,
+ description: $localize`Average latency of 60s increasing P2P ratio`
+ }
+ ]
pluginDataFormGroup: FormGroup
private i18nPrimengCalendarService: I18nPrimengCalendarService,
private ngZone: NgZone,
private hooks: HooksService,
- private cd: ChangeDetectorRef
+ private cd: ChangeDetectorRef,
+ private modalService: NgbModal
) {
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
nsfw: 'false',
commentsEnabled: this.serverConfig.defaults.publish.commentsEnabled,
downloadEnabled: this.serverConfig.defaults.publish.downloadEnabled,
- waitTranscoding: 'true',
+ waitTranscoding: true,
licence: this.serverConfig.defaults.publish.licence,
tags: []
}
originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR,
liveStreamKey: null,
permanentLive: null,
+ latencyMode: null,
saveReplay: null
}
this.trackChannelChange()
this.trackPrivacyChange()
- this.trackLivePermanentFieldChange()
this.formBuilt.emit()
}
.subscribe(privacies => {
this.videoPrivacies = this.videoService.explainedPrivacyLabels(privacies).videoPrivacies
- if (this.schedulePublicationPossible) {
- this.videoPrivacies.push({
- id: this.SPECIAL_SCHEDULED_PRIVACY,
- label: $localize`Scheduled`,
- description: $localize`Hide the video until a specific date`
- })
- }
+ // Can't schedule publication if private privacy is not available (could be deleted by a plugin)
+ const hasPrivatePrivacy = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE)
+ if (this.forbidScheduledPublication || !hasPrivatePrivacy) return
+
+ this.videoPrivacies.push({
+ id: this.SPECIAL_SCHEDULED_PRIVACY,
+ label: $localize`Scheduled`,
+ description: $localize`Hide the video until a specific date`
+ })
})
this.initialVideoCaptions = this.videoCaptions.map(c => c.language.id)
this.schedulerInterval = setInterval(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
})
- this.hooks.runAction('action:video-edit.init', 'video-edit', { type: this.type })
+ const updateFormForPlugins = (values: any) => {
+ this.form.patchValue(values)
+ this.cd.detectChanges()
+ }
+ this.hooks.runAction('action:video-edit.init', 'video-edit', { type: this.type, updateForm: updateFormForPlugins })
+
+ this.form.valueChanges.subscribe(() => {
+ this.hooks.runAction('action:video-edit.form.updated', 'video-edit', { type: this.type, formValues: this.form.value })
+ })
}
ngOnDestroy () {
.map(c => c.language.id)
}
- onCaptionAdded (caption: VideoCaptionEdit) {
+ onCaptionEdited (caption: VideoCaptionEdit) {
const existingCaption = this.videoCaptions.find(c => c.language.id === caption.language.id)
// Replace existing caption?
if (existingCaption) {
- Object.assign(existingCaption, caption, { action: 'CREATE' as 'CREATE' })
+ Object.assign(existingCaption, caption)
} else {
this.videoCaptions.push(
Object.assign(caption, { action: 'CREATE' as 'CREATE' })
}
// This caption is not on the server, just remove it from our array
- if (caption.action === 'CREATE') {
+ if (caption.action === 'CREATE' || caption.action === 'UPDATE') {
removeElementFromArray(this.videoCaptions, caption)
return
}
this.videoCaptionAddModal.show()
}
+ openEditCaptionModal (videoCaption: VideoCaptionWithPathEdit) {
+ const modalRef = this.modalService.open(VideoCaptionEditModalContentComponent, { centered: true, keyboard: false })
+ modalRef.componentInstance.videoCaption = videoCaption
+ modalRef.componentInstance.serverConfig = this.serverConfig
+ modalRef.componentInstance.captionEdited.subscribe(this.onCaptionEdited.bind(this))
+ }
+
isSaveReplayEnabled () {
return this.serverConfig.live.allowReplay
}
return this.form.value['permanentLive'] === true
}
+ isLatencyModeEnabled () {
+ return this.serverConfig.live.latencySetting.enabled
+ }
+
isPluginFieldHidden (pluginField: PluginField) {
if (typeof pluginField.commonOptions.hidden !== 'function') return false
for (const setting of this.pluginFields) {
await this.pluginService.translateSetting(setting.pluginInfo.plugin.npmName, setting.commonOptions)
- const validator = (control: AbstractControl): ValidationErrors | null => {
+ // Not a form input, just a HTML tag
+ if (setting.commonOptions.type === 'html') continue
+
+ const validator = async (control: AbstractControl) => {
if (!setting.commonOptions.error) return null
- const error = setting.commonOptions.error({ formValues: this.form.value, value: control.value })
+ const error = await setting.commonOptions.error({ formValues: this.form.value, value: control.value })
return error?.error ? { [setting.commonOptions.name]: error.text } : null
}
const name = setting.commonOptions.name
pluginObj[name] = {
- VALIDATORS: [ validator ],
+ ASYNC_VALIDATORS: [ validator ],
+ VALIDATORS: [],
MESSAGES: {}
}
this.cd.detectChanges()
this.pluginFieldsAdded.emit()
+
+ // Plugins may need other control values to calculate potential errors
+ this.form.valueChanges.subscribe(() => this.formValidatorService.updateTreeValidity(this.pluginDataFormGroup))
}
private trackPrivacyChange () {
const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId)
if (!newChannel || !oldChannel) {
- console.error('Cannot find new or old channel.')
+ logger.error('Cannot find new or old channel.')
return
}
)
}
- private trackLivePermanentFieldChange () {
- // We will update the "support" field depending on the channel
- this.form.controls['permanentLive']
- .valueChanges
- .subscribe(
- permanentLive => {
- const saveReplayControl = this.form.controls['saveReplay']
-
- if (permanentLive === true) {
- saveReplayControl.setValue(false)
- saveReplayControl.disable()
- } else {
- saveReplayControl.enable()
- }
- }
- )
- }
-
private updateSupportField (support: string) {
return this.form.patchValue({ support: support || '' })
}