From c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 17 Sep 2020 09:20:52 +0200 Subject: [PATCH] Live streaming implementation first step --- .../edit-custom-config.component.html | 83 +- .../edit-custom-config.component.ts | 31 + .../+video-edit/shared/video-edit-utils.ts | 35 + .../shared/video-edit.component.html | 23 + .../shared/video-edit.component.ts | 16 +- .../+video-edit/shared/video-edit.type.ts | 1 + .../video-go-live.component.html | 47 + .../video-go-live.component.ts | 129 + .../video-import-torrent.component.ts | 8 +- .../video-import-url.component.ts | 29 +- .../video-upload.component.ts | 1 - .../+video-edit/video-add.component.html | 12 +- .../+video-edit/video-add.component.ts | 13 +- .../+videos/+video-edit/video-add.module.ts | 4 +- .../+video-edit/video-update.component.html | 1 + .../+video-edit/video-update.component.ts | 38 +- .../+video-edit/video-update.resolver.ts | 61 +- client/src/app/core/plugins/plugin.service.ts | 3 +- client/src/app/core/server/server.service.ts | 7 + .../input-readonly-copy.component.html | 2 +- .../input-readonly-copy.component.ts | 2 + .../instance-features-table.component.html | 18 + .../shared/shared-main/shared-main.module.ts | 3 +- .../src/app/shared/shared-main/video/index.ts | 1 + .../shared-main/video/video-details.model.ts | 7 +- .../shared-main/video/video-live.service.ts | 28 + .../shared/shared-main/video/video.model.ts | 12 +- .../shared/shared-main/video/video.service.ts | 3 +- .../p2p-media-loader/segment-validator.ts | 37 +- .../assets/player/peertube-player-manager.ts | 4 +- client/src/standalone/videos/embed.ts | 4 +- config/default.yaml | 18 + config/test.yaml | 54 +- package.json | 2 + scripts/create-transcoding-job.ts | 2 +- scripts/update-host.ts | 2 +- server.ts | 15 +- server/assets/default-live-background.jpg | Bin 0 -> 93634 bytes server/controllers/api/config.ts | 39 +- server/controllers/api/videos/index.ts | 4 +- server/controllers/api/videos/live.ts | 116 + server/controllers/index.ts | 1 + server/controllers/live.ts | 29 + server/controllers/static.ts | 9 +- server/helpers/core-utils.ts | 11 + server/helpers/custom-validators/videos.ts | 5 +- server/helpers/ffmpeg-utils.ts | 138 +- server/initializers/config.ts | 21 + server/initializers/constants.ts | 50 +- server/initializers/database.ts | 10 +- .../migrations/0535-video-live.ts | 39 + .../migrations/0540-video-file-infohash.ts | 26 + server/lib/hls.ts | 10 +- .../job-queue/handlers/video-transcoding.ts | 2 +- server/lib/live-manager.ts | 310 +++ server/lib/video-paths.ts | 3 +- server/lib/video-transcoding.ts | 7 +- server/lib/video.ts | 31 + .../validators/videos/video-live.ts | 66 + server/models/video/video-file.ts | 4 +- server/models/video/video-format-utils.ts | 2 + server/models/video/video-live.ts | 74 + .../models/video/video-streaming-playlist.ts | 4 +- server/models/video/video.ts | 5 + server/tests/api/check-params/config.ts | 16 + server/tests/api/server/config.ts | 36 + server/tests/api/videos/video-transcoder.ts | 2 +- server/types/models/video/index.ts | 1 + server/types/models/video/video-live.ts | 15 + server/typings/express/index.d.ts | 5 +- shared/extra-utils/server/config.ts | 15 + shared/models/server/custom-config.model.ts | 29 +- shared/models/server/server-config.model.ts | 10 + shared/models/videos/index.ts | 2 + shared/models/videos/video-create.model.ts | 2 +- shared/models/videos/video-live.model.ts | 4 + shared/models/videos/video-state.enum.ts | 4 +- shared/models/videos/video-update.model.ts | 1 - shared/models/videos/video.model.ts | 2 + yarn.lock | 2139 ++++++++--------- 80 files changed, 2752 insertions(+), 1303 deletions(-) create mode 100644 client/src/app/+videos/+video-edit/shared/video-edit-utils.ts create mode 100644 client/src/app/+videos/+video-edit/shared/video-edit.type.ts create mode 100644 client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html create mode 100644 client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts create mode 100644 client/src/app/shared/shared-main/video/video-live.service.ts create mode 100644 server/assets/default-live-background.jpg create mode 100644 server/controllers/api/videos/live.ts create mode 100644 server/controllers/live.ts create mode 100644 server/initializers/migrations/0535-video-live.ts create mode 100644 server/initializers/migrations/0540-video-file-infohash.ts create mode 100644 server/lib/live-manager.ts create mode 100644 server/lib/video.ts create mode 100644 server/middlewares/validators/videos/video-live.ts create mode 100644 server/models/video/video-live.ts create mode 100644 server/types/models/video/video-live.ts create mode 100644 shared/models/videos/video-live.model.ts diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 227137f48..8000f471f 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -699,6 +699,87 @@ + + Live streaming + + + +
+
+
LIVE
+
+ Add ability for your users to do live streaming on your instance. +
+
+ +
+ + + +
+ + + Allow live streaming + + + + Enabling live streaming requires trust in your users and extra moderation work + + + + +
+ + + Requires a lot of CPU! + + +
+ +
+ +
+ +
+
{{ formErrors.live.transcoding.threads }}
+
+ +
+ + + +
+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ Advanced configuration @@ -814,7 +895,7 @@
- +
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 78e9dd5e5..de800c87e 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -34,6 +34,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A customConfig: CustomConfig resolutions: { id: string, label: string, description?: string }[] = [] + liveResolutions: { id: string, label: string, description?: string }[] = [] transcodingThreadOptions: { label: string, value: number }[] = [] languageItems: SelectOptionsItem[] = [] @@ -82,6 +83,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A } ] + this.liveResolutions = this.resolutions.filter(r => r.id !== '0p') + this.transcodingThreadOptions = [ { value: 0, label: $localize`Auto (via ffmpeg)` }, { value: 1, label: '1' }, @@ -198,6 +201,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A enabled: null } }, + live: { + enabled: null, + + transcoding: { + enabled: null, + threads: TRANSCODING_THREADS_VALIDATOR, + resolutions: {} + } + }, autoBlacklist: { videos: { ofUsers: { @@ -245,13 +257,24 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A const defaultValues = { transcoding: { resolutions: {} + }, + live: { + transcoding: { + resolutions: {} + } } } + for (const resolution of this.resolutions) { defaultValues.transcoding.resolutions[resolution.id] = 'false' formGroupData.transcoding.resolutions[resolution.id] = null } + for (const resolution of this.liveResolutions) { + defaultValues.live.transcoding.resolutions[resolution.id] = 'false' + formGroupData.live.transcoding.resolutions[resolution.id] = null + } + this.buildForm(formGroupData) this.loadForm() this.checkTranscodingFields() @@ -268,6 +291,14 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A return this.form.value['transcoding']['enabled'] === true } + isLiveEnabled () { + return this.form.value['live']['enabled'] === true + } + + isLiveTranscodingEnabled () { + return this.form.value['live']['transcoding']['enabled'] === true + } + isSignupEnabled () { return this.form.value['signup']['enabled'] === true } diff --git a/client/src/app/+videos/+video-edit/shared/video-edit-utils.ts b/client/src/app/+videos/+video-edit/shared/video-edit-utils.ts new file mode 100644 index 000000000..3a7dbed36 --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-edit-utils.ts @@ -0,0 +1,35 @@ +import { FormGroup } from '@angular/forms' +import { VideoEdit } from '@app/shared/shared-main' + +function hydrateFormFromVideo (formGroup: FormGroup, video: VideoEdit, thumbnailFiles: boolean) { + formGroup.patchValue(video.toFormPatch()) + + if (thumbnailFiles === false) return + + const objects = [ + { + url: 'thumbnailUrl', + name: 'thumbnailfile' + }, + { + url: 'previewUrl', + name: 'previewfile' + } + ] + + for (const obj of objects) { + if (!video[obj.url]) continue + + fetch(video[obj.url]) + .then(response => response.blob()) + .then(data => { + formGroup.patchValue({ + [ obj.name ]: data + }) + }) + } +} + +export { + hydrateFormFromVideo +} diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 842997b20..c444dd8d3 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html @@ -195,6 +195,29 @@ + + Live settings + + +
+
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+ + Advanced settings diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index f04111e69..bee65184b 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts @@ -20,10 +20,11 @@ import { import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' import { InstanceService } from '@app/shared/shared-instance' import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' -import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' +import { ServerConfig, VideoConstant, VideoLive, VideoPrivacy } from '@shared/models' import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' +import { VideoEditType } from './video-edit.type' type VideoLanguages = VideoConstant & { group?: string } @@ -40,7 +41,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { @Input() schedulePublicationPossible = true @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] @Input() waitTranscodingEnabled = true - @Input() type: 'import-url' | 'import-torrent' | 'upload' | 'update' + @Input() type: VideoEditType + @Input() videoLive: VideoLive @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent @@ -124,7 +126,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { previewfile: null, support: VIDEO_SUPPORT_VALIDATOR, schedulePublicationAt: VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, - originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR + originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, + liveStreamKey: null } this.formValidatorService.updateForm( @@ -320,7 +323,12 @@ export class VideoEditComponent implements OnInit, OnDestroy { const currentSupport = this.form.value[ 'support' ] // First time we set the channel? - if (isNaN(oldChannelId) && !currentSupport) return this.updateSupportField(newChannel.support) + if (isNaN(oldChannelId)) { + // Fill support if it's empty + if (!currentSupport) this.updateSupportField(newChannel.support) + + return + } const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId) if (!newChannel || !oldChannel) { diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.type.ts b/client/src/app/+videos/+video-edit/shared/video-edit.type.ts new file mode 100644 index 000000000..fdbe9505c --- /dev/null +++ b/client/src/app/+videos/+video-edit/shared/video-edit.type.ts @@ -0,0 +1 @@ +export type VideoEditType = 'update' | 'upload' | 'import-url' | 'import-torrent' | 'go-live' diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html new file mode 100644 index 000000000..6997f5388 --- /dev/null +++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html @@ -0,0 +1,47 @@ +
+
+ + +
+ + +
+ +
+ + +
+ + +
+
+ +
+
Sorry, but something went wrong
+ {{ error }} +
+ + +
+ + +
+
+ + +
+
+
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts new file mode 100644 index 000000000..64fd4c4d4 --- /dev/null +++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts @@ -0,0 +1,129 @@ + +import { Component, EventEmitter, OnInit, Output } from '@angular/core' +import { Router } from '@angular/router' +import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' +import { scrollToTop } from '@app/helpers' +import { FormValidatorService } from '@app/shared/shared-forms' +import { VideoCaptionService, VideoEdit, VideoService, VideoLiveService } from '@app/shared/shared-main' +import { LoadingBarService } from '@ngx-loading-bar/core' +import { VideoCreate, VideoLive, VideoPrivacy } from '@shared/models' +import { VideoSend } from './video-send' + +@Component({ + selector: 'my-video-go-live', + templateUrl: './video-go-live.component.html', + styleUrls: [ + '../shared/video-edit.component.scss', + './video-send.scss' + ] +}) +export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { + @Output() firstStepDone = new EventEmitter() + @Output() firstStepError = new EventEmitter() + + isInUpdateForm = false + + videoLive: VideoLive + videoId: number + videoUUID: string + error: string + + protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC + + constructor ( + protected formValidatorService: FormValidatorService, + protected loadingBar: LoadingBarService, + protected notifier: Notifier, + protected authService: AuthService, + protected serverService: ServerService, + protected videoService: VideoService, + protected videoCaptionService: VideoCaptionService, + private videoLiveService: VideoLiveService, + private router: Router + ) { + super() + } + + ngOnInit () { + super.ngOnInit() + } + + canDeactivate () { + return { canDeactivate: true } + } + + goLive () { + const video: VideoCreate = { + name: 'Live', + privacy: VideoPrivacy.PRIVATE, + nsfw: this.serverConfig.instance.isNSFW, + waitTranscoding: true, + commentsEnabled: true, + downloadEnabled: true, + channelId: this.firstStepChannelId + } + + this.firstStepDone.emit(name) + + // Go live in private mode, but correctly fill the update form with the first user choice + const toPatch = Object.assign({}, video, { privacy: this.firstStepPrivacyId }) + this.form.patchValue(toPatch) + + this.videoLiveService.goLive(video).subscribe( + res => { + this.videoId = res.video.id + this.videoUUID = res.video.uuid + this.isInUpdateForm = true + + this.fetchVideoLive() + }, + + err => { + this.firstStepError.emit() + this.notifier.error(err.message) + } + ) + } + + updateSecondStep () { + if (this.checkForm() === false) { + return + } + + const video = new VideoEdit() + video.patch(this.form.value) + video.id = this.videoId + video.uuid = this.videoUUID + + // Update the video + this.updateVideoAndCaptions(video) + .subscribe( + () => { + this.notifier.success($localize`Live published.`) + + this.router.navigate([ '/videos/watch', video.uuid ]) + }, + + err => { + this.error = err.message + scrollToTop() + console.error(err) + } + ) + + } + + private fetchVideoLive () { + this.videoLiveService.getVideoLive(this.videoId) + .subscribe( + videoLive => { + this.videoLive = videoLive + }, + + err => { + this.firstStepError.emit() + this.notifier.error(err.message) + } + ) + } +} diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts index e9ad8af7a..64e887987 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts @@ -6,6 +6,7 @@ import { FormValidatorService } from '@app/shared/shared-forms' import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' import { VideoPrivacy, VideoUpdate } from '@shared/models' +import { hydrateFormFromVideo } from '../shared/video-edit-utils' import { VideoSend } from './video-send' @Component({ @@ -99,7 +100,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca previewUrl: null })) - this.hydrateFormFromVideo() + hydrateFormFromVideo(this.form, this.video, false) }, err => { @@ -136,10 +137,5 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca console.error(err) } ) - - } - - private hydrateFormFromVideo () { - this.form.patchValue(this.video.toFormPatch()) } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts index 8bad81097..47f59a5d0 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts @@ -7,6 +7,7 @@ import { FormValidatorService } from '@app/shared/shared-forms' import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' import { VideoPrivacy, VideoUpdate } from '@shared/models' +import { hydrateFormFromVideo } from '../shared/video-edit-utils' import { VideoSend } from './video-send' @Component({ @@ -109,7 +110,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom this.videoCaptions = videoCaptions - this.hydrateFormFromVideo() + hydrateFormFromVideo(this.form, this.video, true) }, err => { @@ -146,31 +147,5 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom console.error(err) } ) - - } - - private hydrateFormFromVideo () { - this.form.patchValue(this.video.toFormPatch()) - - const objects = [ - { - url: 'thumbnailUrl', - name: 'thumbnailfile' - }, - { - url: 'previewUrl', - name: 'previewfile' - } - ] - - for (const obj of objects) { - fetch(this.video[obj.url]) - .then(response => response.blob()) - .then(data => { - this.form.patchValue({ - [ obj.name ]: data - }) - }) - } } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index 32a17052a..258f5c7a0 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -157,7 +157,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy this.waitTranscodingEnabled = false } - const privacy = this.firstStepPrivacyId.toString() const nsfw = this.serverConfig.instance.isNSFW const waitTranscoding = true const commentsEnabled = true diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html index 14d41f95b..bf2cc9c83 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html @@ -50,7 +50,17 @@
+ + + + Go live + + + + + +
-
\ No newline at end of file + diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts index 94e85efc1..441d5a3db 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.ts +++ b/client/src/app/+videos/+video-edit/video-add.component.ts @@ -1,6 +1,8 @@ import { Component, HostListener, OnInit, ViewChild } from '@angular/core' import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' import { ServerConfig } from '@shared/models' +import { VideoEditType } from './shared/video-edit.type' +import { VideoGoLiveComponent } from './video-add-components/video-go-live.component' import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' import { VideoUploadComponent } from './video-add-components/video-upload.component' @@ -14,10 +16,11 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { @ViewChild('videoUpload') videoUpload: VideoUploadComponent @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent + @ViewChild('videoGoLive') videoGoLive: VideoGoLiveComponent user: AuthUser = null - secondStepType: 'upload' | 'import-url' | 'import-torrent' + secondStepType: VideoEditType videoName: string serverConfig: ServerConfig @@ -41,7 +44,7 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { this.user = this.auth.getUser() } - onFirstStepDone (type: 'upload' | 'import-url' | 'import-torrent', videoName: string) { + onFirstStepDone (type: VideoEditType, videoName: string) { this.secondStepType = type this.videoName = videoName } @@ -62,9 +65,9 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { } canDeactivate (): { canDeactivate: boolean, text?: string} { - if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() + if (this.secondStepType === 'go-live') return this.videoGoLive.canDeactivate() return { canDeactivate: true } } @@ -77,6 +80,10 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { return this.serverConfig.import.videos.torrent.enabled } + isVideoLiveEnabled () { + return this.serverConfig.live.enabled + } + isInSecondStep () { return !!this.secondStepType } diff --git a/client/src/app/+videos/+video-edit/video-add.module.ts b/client/src/app/+videos/+video-edit/video-add.module.ts index 477c1cf5e..da651119b 100644 --- a/client/src/app/+videos/+video-edit/video-add.module.ts +++ b/client/src/app/+videos/+video-edit/video-add.module.ts @@ -4,6 +4,7 @@ import { VideoEditModule } from './shared/video-edit.module' import { DragDropDirective } from './video-add-components/drag-drop.directive' import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' +import { VideoGoLiveComponent } from './video-add-components/video-go-live.component' import { VideoUploadComponent } from './video-add-components/video-upload.component' import { VideoAddRoutingModule } from './video-add-routing.module' import { VideoAddComponent } from './video-add.component' @@ -20,7 +21,8 @@ import { VideoAddComponent } from './video-add.component' VideoUploadComponent, VideoImportUrlComponent, VideoImportTorrentComponent, - DragDropDirective + DragDropDirective, + VideoGoLiveComponent ], exports: [ ], diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html index b37596399..5f50ddc74 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.html +++ b/client/src/app/+videos/+video-edit/video-update.component.html @@ -11,6 +11,7 @@ [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" + [videoLive]="videoLive" >
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts index 20438a2d3..c0f46acd2 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.ts +++ b/client/src/app/+videos/+video-edit/video-update.component.ts @@ -5,7 +5,8 @@ import { Notifier } from '@app/core' import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' -import { VideoPrivacy } from '@shared/models' +import { VideoPrivacy, VideoLive } from '@shared/models' +import { hydrateFormFromVideo } from './shared/video-edit-utils' @Component({ selector: 'my-videos-update', @@ -14,11 +15,12 @@ import { VideoPrivacy } from '@shared/models' }) export class VideoUpdateComponent extends FormReactive implements OnInit { video: VideoEdit + userVideoChannels: SelectChannelItem[] = [] + videoCaptions: VideoCaptionEdit[] = [] + videoLive: VideoLive isUpdatingVideo = false - userVideoChannels: SelectChannelItem[] = [] schedulePublicationPossible = false - videoCaptions: VideoCaptionEdit[] = [] waitTranscodingEnabled = true private updateDone = false @@ -40,10 +42,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { this.route.data .pipe(map(data => data.videoData)) - .subscribe(({ video, videoChannels, videoCaptions }) => { + .subscribe(({ video, videoChannels, videoCaptions, videoLive }) => { this.video = new VideoEdit(video) this.userVideoChannels = videoChannels this.videoCaptions = videoCaptions + this.videoLive = videoLive this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE @@ -53,7 +56,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { } // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout - setTimeout(() => this.hydrateFormFromVideo()) + setTimeout(() => hydrateFormFromVideo(this.form, this.video, true)) }, err => { @@ -133,29 +136,4 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { pluginData: this.video.pluginData }) } - - private hydrateFormFromVideo () { - this.form.patchValue(this.video.toFormPatch()) - - const objects = [ - { - url: 'thumbnailUrl', - name: 'thumbnailfile' - }, - { - url: 'previewUrl', - name: 'previewfile' - } - ] - - for (const obj of objects) { - fetch(this.video[obj.url]) - .then(response => response.blob()) - .then(data => { - this.form.patchValue({ - [ obj.name ]: data - }) - }) - } - } } diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index a391913d8..3a82324c3 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts @@ -1,13 +1,14 @@ -import { forkJoin } from 'rxjs' +import { forkJoin, of } from 'rxjs' import { map, switchMap } from 'rxjs/operators' import { Injectable } from '@angular/core' import { ActivatedRouteSnapshot, Resolve } from '@angular/router' -import { VideoCaptionService, VideoChannelService, VideoService } from '@app/shared/shared-main' +import { VideoCaptionService, VideoChannelService, VideoDetails, VideoLiveService, VideoService } from '@app/shared/shared-main' @Injectable() export class VideoUpdateResolver implements Resolve { constructor ( private videoService: VideoService, + private videoLiveService: VideoLiveService, private videoChannelService: VideoChannelService, private videoCaptionService: VideoCaptionService ) { @@ -18,32 +19,38 @@ export class VideoUpdateResolver implements Resolve { return this.videoService.getVideo({ videoId: uuid }) .pipe( - switchMap(video => { - return forkJoin([ - this.videoService - .loadCompleteDescription(video.descriptionPath) - .pipe(map(description => Object.assign(video, { description }))), + switchMap(video => forkJoin(this.buildVideoObservables(video))), + map(([ video, videoChannels, videoCaptions, videoLive ]) => ({ video, videoChannels, videoCaptions, videoLive })) + ) + } - this.videoChannelService - .listAccountVideoChannels(video.account) - .pipe( - map(result => result.data), - map(videoChannels => videoChannels.map(c => ({ - id: c.id, - label: c.displayName, - support: c.support, - avatarPath: c.avatar?.path - }))) - ), + private buildVideoObservables (video: VideoDetails) { + return [ + this.videoService + .loadCompleteDescription(video.descriptionPath) + .pipe(map(description => Object.assign(video, { description }))), - this.videoCaptionService - .listCaptions(video.id) - .pipe( - map(result => result.data) - ) - ]) - }), - map(([ video, videoChannels, videoCaptions ]) => ({ video, videoChannels, videoCaptions })) - ) + this.videoChannelService + .listAccountVideoChannels(video.account) + .pipe( + map(result => result.data), + map(videoChannels => videoChannels.map(c => ({ + id: c.id, + label: c.displayName, + support: c.support, + avatarPath: c.avatar?.path + }))) + ), + + this.videoCaptionService + .listCaptions(video.id) + .pipe( + map(result => result.data) + ), + + video.isLive + ? this.videoLiveService.getVideoLive(video.id) + : of(undefined) + ] } } diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts index 4e44a1865..b755fda2c 100644 --- a/client/src/app/core/plugins/plugin.service.ts +++ b/client/src/app/core/plugins/plugin.service.ts @@ -2,6 +2,7 @@ import { Observable, of, ReplaySubject } from 'rxjs' import { catchError, first, map, shareReplay } from 'rxjs/operators' import { HttpClient } from '@angular/common/http' import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' +import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type' import { AuthService } from '@app/core/auth' import { Notifier } from '@app/core/notification' import { MarkdownService } from '@app/core/renderer' @@ -192,7 +193,7 @@ export class PluginService implements ClientHook { : PluginType.THEME } - getRegisteredVideoFormFields (type: 'import-url' | 'import-torrent' | 'upload' | 'update') { + getRegisteredVideoFormFields (type: VideoEditType) { return this.formFields.video.filter(f => f.videoFormOptions.type === type) } diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 5bcf33c1b..bc76bacfc 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -74,6 +74,13 @@ export class ServerService { enabled: true } }, + live: { + enabled: false, + transcoding: { + enabled: false, + enabledResolutions: [] + } + }, avatar: { file: { size: { max: 0 }, diff --git a/client/src/app/shared/shared-forms/input-readonly-copy.component.html b/client/src/app/shared/shared-forms/input-readonly-copy.component.html index 9566e9741..7a75bd70b 100644 --- a/client/src/app/shared/shared-forms/input-readonly-copy.component.html +++ b/client/src/app/shared/shared-forms/input-readonly-copy.component.html @@ -1,5 +1,5 @@
- +