diff options
author | Chocobozzz <me@florianbigard.com> | 2020-09-17 09:20:52 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-11-09 15:33:04 +0100 |
commit | c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e (patch) | |
tree | 79304b0152b0a38d33b26e65d4acdad0da4032a7 /client/src/app | |
parent | 110d463fece85e87a26aca48a6048ae0017a27b3 (diff) | |
download | PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.tar.gz PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.tar.zst PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.zip |
Live streaming implementation first step
Diffstat (limited to 'client/src/app')
28 files changed, 509 insertions, 109 deletions
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 @@ | |||
699 | </ng-template> | 699 | </ng-template> |
700 | </ng-container> | 700 | </ng-container> |
701 | 701 | ||
702 | <ng-container ngbNavItem="live"> | ||
703 | <a ngbNavLink i18n>Live streaming</a> | ||
704 | |||
705 | <ng-template ngbNavContent> | ||
706 | |||
707 | <div class="form-row mt-5"> | ||
708 | <div class="form-group col-12 col-lg-4 col-xl-3"> | ||
709 | <div i18n class="inner-form-title">LIVE</div> | ||
710 | <div i18n class="inner-form-description"> | ||
711 | Add ability for your users to do live streaming on your instance. | ||
712 | </div> | ||
713 | </div> | ||
714 | |||
715 | <div class="form-group form-group-right col-12 col-lg-8 col-xl-9"> | ||
716 | |||
717 | <ng-container formGroupName="live"> | ||
718 | |||
719 | <div class="form-group"> | ||
720 | <my-peertube-checkbox inputName="liveEnabled" formControlName="enabled"> | ||
721 | <ng-template ptTemplate="label"> | ||
722 | <ng-container i18n>Allow live streaming</ng-container> | ||
723 | </ng-template> | ||
724 | |||
725 | <ng-template ptTemplate="help"> | ||
726 | <ng-container i18n>Enabling live streaming requires trust in your users and extra moderation work</ng-container> | ||
727 | </ng-template> | ||
728 | |||
729 | <ng-container ngProjectAs="extra" formGroupName="transcoding"> | ||
730 | |||
731 | <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() }"> | ||
732 | <my-peertube-checkbox | ||
733 | inputName="liveTranscodingEnabled" formControlName="enabled" | ||
734 | i18n-labelText labelText="Enable live transcoding" | ||
735 | > | ||
736 | <ng-container ngProjectAs="description"> | ||
737 | Requires a lot of CPU! | ||
738 | </ng-container> | ||
739 | </my-peertube-checkbox> | ||
740 | </div> | ||
741 | |||
742 | <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }"> | ||
743 | <label i18n for="liveTranscodingThreads">Live transcoding threads</label> | ||
744 | <div class="peertube-select-container"> | ||
745 | <select id="liveTranscodingThreads" formControlName="threads" class="form-control"> | ||
746 | <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> | ||
747 | {{ transcodingThreadOption.label }} | ||
748 | </option> | ||
749 | </select> | ||
750 | </div> | ||
751 | <div *ngIf="formErrors.live.transcoding.threads" class="form-error">{{ formErrors.live.transcoding.threads }}</div> | ||
752 | </div> | ||
753 | |||
754 | <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isLiveEnabled() || !isLiveTranscodingEnabled() }"> | ||
755 | |||
756 | <label i18n for="liveTranscodingThreads">Live resolutions to generate</label> | ||
757 | |||
758 | <div class="ml-2 mt-2 d-flex flex-column"> | ||
759 | <ng-container formGroupName="resolutions"> | ||
760 | <div class="form-group" *ngFor="let resolution of liveResolutions"> | ||
761 | <my-peertube-checkbox | ||
762 | [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id" | ||
763 | labelText="{{resolution.label}}" | ||
764 | > | ||
765 | <ng-template *ngIf="resolution.description" ptTemplate="help"> | ||
766 | <div [innerHTML]="resolution.description"></div> | ||
767 | </ng-template> | ||
768 | </my-peertube-checkbox> | ||
769 | </div> | ||
770 | </ng-container> | ||
771 | </div> | ||
772 | </div> | ||
773 | </ng-container> | ||
774 | </my-peertube-checkbox> | ||
775 | </div> | ||
776 | </ng-container> | ||
777 | </div> | ||
778 | </div> | ||
779 | |||
780 | </ng-template> | ||
781 | </ng-container> | ||
782 | |||
702 | <ng-container ngbNavItem="advanced-configuration"> | 783 | <ng-container ngbNavItem="advanced-configuration"> |
703 | <a ngbNavLink i18n>Advanced configuration</a> | 784 | <a ngbNavLink i18n>Advanced configuration</a> |
704 | 785 | ||
@@ -814,7 +895,7 @@ | |||
814 | 895 | ||
815 | <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }"> | 896 | <div class="form-group" [ngClass]="{ 'disabled-checkbox-extra': !isTranscodingEnabled() }"> |
816 | 897 | ||
817 | <label i18n for="transcodingThreads">Resolutions to generate</label> | 898 | <label i18n>Resolutions to generate</label> |
818 | 899 | ||
819 | <div class="ml-2 mt-2 d-flex flex-column"> | 900 | <div class="ml-2 mt-2 d-flex flex-column"> |
820 | <ng-container formGroupName="resolutions"> | 901 | <ng-container formGroupName="resolutions"> |
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 | |||
34 | customConfig: CustomConfig | 34 | customConfig: CustomConfig |
35 | 35 | ||
36 | resolutions: { id: string, label: string, description?: string }[] = [] | 36 | resolutions: { id: string, label: string, description?: string }[] = [] |
37 | liveResolutions: { id: string, label: string, description?: string }[] = [] | ||
37 | transcodingThreadOptions: { label: string, value: number }[] = [] | 38 | transcodingThreadOptions: { label: string, value: number }[] = [] |
38 | 39 | ||
39 | languageItems: SelectOptionsItem[] = [] | 40 | languageItems: SelectOptionsItem[] = [] |
@@ -82,6 +83,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A | |||
82 | } | 83 | } |
83 | ] | 84 | ] |
84 | 85 | ||
86 | this.liveResolutions = this.resolutions.filter(r => r.id !== '0p') | ||
87 | |||
85 | this.transcodingThreadOptions = [ | 88 | this.transcodingThreadOptions = [ |
86 | { value: 0, label: $localize`Auto (via ffmpeg)` }, | 89 | { value: 0, label: $localize`Auto (via ffmpeg)` }, |
87 | { value: 1, label: '1' }, | 90 | { value: 1, label: '1' }, |
@@ -198,6 +201,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A | |||
198 | enabled: null | 201 | enabled: null |
199 | } | 202 | } |
200 | }, | 203 | }, |
204 | live: { | ||
205 | enabled: null, | ||
206 | |||
207 | transcoding: { | ||
208 | enabled: null, | ||
209 | threads: TRANSCODING_THREADS_VALIDATOR, | ||
210 | resolutions: {} | ||
211 | } | ||
212 | }, | ||
201 | autoBlacklist: { | 213 | autoBlacklist: { |
202 | videos: { | 214 | videos: { |
203 | ofUsers: { | 215 | ofUsers: { |
@@ -245,13 +257,24 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A | |||
245 | const defaultValues = { | 257 | const defaultValues = { |
246 | transcoding: { | 258 | transcoding: { |
247 | resolutions: {} | 259 | resolutions: {} |
260 | }, | ||
261 | live: { | ||
262 | transcoding: { | ||
263 | resolutions: {} | ||
264 | } | ||
248 | } | 265 | } |
249 | } | 266 | } |
267 | |||
250 | for (const resolution of this.resolutions) { | 268 | for (const resolution of this.resolutions) { |
251 | defaultValues.transcoding.resolutions[resolution.id] = 'false' | 269 | defaultValues.transcoding.resolutions[resolution.id] = 'false' |
252 | formGroupData.transcoding.resolutions[resolution.id] = null | 270 | formGroupData.transcoding.resolutions[resolution.id] = null |
253 | } | 271 | } |
254 | 272 | ||
273 | for (const resolution of this.liveResolutions) { | ||
274 | defaultValues.live.transcoding.resolutions[resolution.id] = 'false' | ||
275 | formGroupData.live.transcoding.resolutions[resolution.id] = null | ||
276 | } | ||
277 | |||
255 | this.buildForm(formGroupData) | 278 | this.buildForm(formGroupData) |
256 | this.loadForm() | 279 | this.loadForm() |
257 | this.checkTranscodingFields() | 280 | this.checkTranscodingFields() |
@@ -268,6 +291,14 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A | |||
268 | return this.form.value['transcoding']['enabled'] === true | 291 | return this.form.value['transcoding']['enabled'] === true |
269 | } | 292 | } |
270 | 293 | ||
294 | isLiveEnabled () { | ||
295 | return this.form.value['live']['enabled'] === true | ||
296 | } | ||
297 | |||
298 | isLiveTranscodingEnabled () { | ||
299 | return this.form.value['live']['transcoding']['enabled'] === true | ||
300 | } | ||
301 | |||
271 | isSignupEnabled () { | 302 | isSignupEnabled () { |
272 | return this.form.value['signup']['enabled'] === true | 303 | return this.form.value['signup']['enabled'] === true |
273 | } | 304 | } |
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 @@ | |||
1 | import { FormGroup } from '@angular/forms' | ||
2 | import { VideoEdit } from '@app/shared/shared-main' | ||
3 | |||
4 | function hydrateFormFromVideo (formGroup: FormGroup, video: VideoEdit, thumbnailFiles: boolean) { | ||
5 | formGroup.patchValue(video.toFormPatch()) | ||
6 | |||
7 | if (thumbnailFiles === false) return | ||
8 | |||
9 | const objects = [ | ||
10 | { | ||
11 | url: 'thumbnailUrl', | ||
12 | name: 'thumbnailfile' | ||
13 | }, | ||
14 | { | ||
15 | url: 'previewUrl', | ||
16 | name: 'previewfile' | ||
17 | } | ||
18 | ] | ||
19 | |||
20 | for (const obj of objects) { | ||
21 | if (!video[obj.url]) continue | ||
22 | |||
23 | fetch(video[obj.url]) | ||
24 | .then(response => response.blob()) | ||
25 | .then(data => { | ||
26 | formGroup.patchValue({ | ||
27 | [ obj.name ]: data | ||
28 | }) | ||
29 | }) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | export { | ||
34 | hydrateFormFromVideo | ||
35 | } | ||
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 @@ | |||
195 | </ng-template> | 195 | </ng-template> |
196 | </ng-container> | 196 | </ng-container> |
197 | 197 | ||
198 | <ng-container ngbNavItem *ngIf="videoLive"> | ||
199 | <a ngbNavLink i18n>Live settings</a> | ||
200 | |||
201 | <ng-template ngbNavContent> | ||
202 | <div class="row live-settings"> | ||
203 | <div class="col-md-12"> | ||
204 | |||
205 | <div class="form-group"> | ||
206 | <label for="videoLiveRTMPUrl" i18n>Live RTMP Url</label> | ||
207 | <my-input-readonly-copy id="videoLiveRTMPUrl" [value]="videoLive.rtmpUrl"></my-input-readonly-copy> | ||
208 | </div> | ||
209 | |||
210 | <div class="form-group"> | ||
211 | <label for="videoLiveStreamKey" i18n>Live stream key</label> | ||
212 | <my-input-readonly-copy id="videoLiveStreamKey" [value]="videoLive.streamKey"></my-input-readonly-copy> | ||
213 | </div> | ||
214 | </div> | ||
215 | </div> | ||
216 | </ng-template> | ||
217 | |||
218 | </ng-container> | ||
219 | |||
220 | |||
198 | <ng-container ngbNavItem> | 221 | <ng-container ngbNavItem> |
199 | <a ngbNavLink i18n>Advanced settings</a> | 222 | <a ngbNavLink i18n>Advanced settings</a> |
200 | 223 | ||
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 { | |||
20 | import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' | 20 | import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' |
21 | import { InstanceService } from '@app/shared/shared-instance' | 21 | import { InstanceService } from '@app/shared/shared-instance' |
22 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' | 22 | import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' |
23 | import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' | 23 | import { ServerConfig, VideoConstant, VideoLive, VideoPrivacy } from '@shared/models' |
24 | import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' | 24 | import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' |
25 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' | 25 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' |
26 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 26 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
27 | import { VideoEditType } from './video-edit.type' | ||
27 | 28 | ||
28 | type VideoLanguages = VideoConstant<string> & { group?: string } | 29 | type VideoLanguages = VideoConstant<string> & { group?: string } |
29 | 30 | ||
@@ -40,7 +41,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
40 | @Input() schedulePublicationPossible = true | 41 | @Input() schedulePublicationPossible = true |
41 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] | 42 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] |
42 | @Input() waitTranscodingEnabled = true | 43 | @Input() waitTranscodingEnabled = true |
43 | @Input() type: 'import-url' | 'import-torrent' | 'upload' | 'update' | 44 | @Input() type: VideoEditType |
45 | @Input() videoLive: VideoLive | ||
44 | 46 | ||
45 | @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent | 47 | @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent |
46 | 48 | ||
@@ -124,7 +126,8 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
124 | previewfile: null, | 126 | previewfile: null, |
125 | support: VIDEO_SUPPORT_VALIDATOR, | 127 | support: VIDEO_SUPPORT_VALIDATOR, |
126 | schedulePublicationAt: VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, | 128 | schedulePublicationAt: VIDEO_SCHEDULE_PUBLICATION_AT_VALIDATOR, |
127 | originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR | 129 | originallyPublishedAt: VIDEO_ORIGINALLY_PUBLISHED_AT_VALIDATOR, |
130 | liveStreamKey: null | ||
128 | } | 131 | } |
129 | 132 | ||
130 | this.formValidatorService.updateForm( | 133 | this.formValidatorService.updateForm( |
@@ -320,7 +323,12 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
320 | const currentSupport = this.form.value[ 'support' ] | 323 | const currentSupport = this.form.value[ 'support' ] |
321 | 324 | ||
322 | // First time we set the channel? | 325 | // First time we set the channel? |
323 | if (isNaN(oldChannelId) && !currentSupport) return this.updateSupportField(newChannel.support) | 326 | if (isNaN(oldChannelId)) { |
327 | // Fill support if it's empty | ||
328 | if (!currentSupport) this.updateSupportField(newChannel.support) | ||
329 | |||
330 | return | ||
331 | } | ||
324 | 332 | ||
325 | const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId) | 333 | const oldChannel = this.userVideoChannels.find(c => c.id === oldChannelId) |
326 | if (!newChannel || !oldChannel) { | 334 | 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 @@ | |||
1 | <div *ngIf="!isInUpdateForm" class="upload-video-container"> | ||
2 | <div class="first-step-block"> | ||
3 | <my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon> | ||
4 | |||
5 | <div class="form-group"> | ||
6 | <label i18n for="first-step-channel">Channel</label> | ||
7 | <my-select-channel | ||
8 | labelForId="first-step-channel" [items]="userVideoChannels" [(ngModel)]="firstStepChannelId" | ||
9 | ></my-select-channel> | ||
10 | </div> | ||
11 | |||
12 | <div class="form-group"> | ||
13 | <label i18n for="first-step-privacy">Privacy</label> | ||
14 | <my-select-options | ||
15 | labelForId="first-step-privacy" [items]="videoPrivacies" [(ngModel)]="firstStepPrivacyId" | ||
16 | ></my-select-options> | ||
17 | </div> | ||
18 | |||
19 | <input | ||
20 | type="button" i18n-value value="Go Live" (click)="goLive()" | ||
21 | /> | ||
22 | </div> | ||
23 | </div> | ||
24 | |||
25 | <div *ngIf="error" class="alert alert-danger"> | ||
26 | <div i18n>Sorry, but something went wrong</div> | ||
27 | {{ error }} | ||
28 | </div> | ||
29 | |||
30 | <!-- Hidden because we want to load the component --> | ||
31 | <form [hidden]="!isInUpdateForm" novalidate [formGroup]="form"> | ||
32 | <my-video-edit | ||
33 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" | ||
34 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [videoLive]="videoLive" | ||
35 | type="go-live" | ||
36 | ></my-video-edit> | ||
37 | |||
38 | <div class="submit-container"> | ||
39 | <div class="submit-button" | ||
40 | (click)="updateSecondStep()" | ||
41 | [ngClass]="{ disabled: !form.valid }" | ||
42 | > | ||
43 | <my-global-icon iconName="circle-tick" aria-hidden="true"></my-global-icon> | ||
44 | <input type="button" i18n-value value="Update" /> | ||
45 | </div> | ||
46 | </div> | ||
47 | </form> | ||
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 @@ | |||
1 | |||
2 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | ||
3 | import { Router } from '@angular/router' | ||
4 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | ||
5 | import { scrollToTop } from '@app/helpers' | ||
6 | import { FormValidatorService } from '@app/shared/shared-forms' | ||
7 | import { VideoCaptionService, VideoEdit, VideoService, VideoLiveService } from '@app/shared/shared-main' | ||
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | ||
9 | import { VideoCreate, VideoLive, VideoPrivacy } from '@shared/models' | ||
10 | import { VideoSend } from './video-send' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-video-go-live', | ||
14 | templateUrl: './video-go-live.component.html', | ||
15 | styleUrls: [ | ||
16 | '../shared/video-edit.component.scss', | ||
17 | './video-send.scss' | ||
18 | ] | ||
19 | }) | ||
20 | export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { | ||
21 | @Output() firstStepDone = new EventEmitter<string>() | ||
22 | @Output() firstStepError = new EventEmitter<void>() | ||
23 | |||
24 | isInUpdateForm = false | ||
25 | |||
26 | videoLive: VideoLive | ||
27 | videoId: number | ||
28 | videoUUID: string | ||
29 | error: string | ||
30 | |||
31 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | ||
32 | |||
33 | constructor ( | ||
34 | protected formValidatorService: FormValidatorService, | ||
35 | protected loadingBar: LoadingBarService, | ||
36 | protected notifier: Notifier, | ||
37 | protected authService: AuthService, | ||
38 | protected serverService: ServerService, | ||
39 | protected videoService: VideoService, | ||
40 | protected videoCaptionService: VideoCaptionService, | ||
41 | private videoLiveService: VideoLiveService, | ||
42 | private router: Router | ||
43 | ) { | ||
44 | super() | ||
45 | } | ||
46 | |||
47 | ngOnInit () { | ||
48 | super.ngOnInit() | ||
49 | } | ||
50 | |||
51 | canDeactivate () { | ||
52 | return { canDeactivate: true } | ||
53 | } | ||
54 | |||
55 | goLive () { | ||
56 | const video: VideoCreate = { | ||
57 | name: 'Live', | ||
58 | privacy: VideoPrivacy.PRIVATE, | ||
59 | nsfw: this.serverConfig.instance.isNSFW, | ||
60 | waitTranscoding: true, | ||
61 | commentsEnabled: true, | ||
62 | downloadEnabled: true, | ||
63 | channelId: this.firstStepChannelId | ||
64 | } | ||
65 | |||
66 | this.firstStepDone.emit(name) | ||
67 | |||
68 | // Go live in private mode, but correctly fill the update form with the first user choice | ||
69 | const toPatch = Object.assign({}, video, { privacy: this.firstStepPrivacyId }) | ||
70 | this.form.patchValue(toPatch) | ||
71 | |||
72 | this.videoLiveService.goLive(video).subscribe( | ||
73 | res => { | ||
74 | this.videoId = res.video.id | ||
75 | this.videoUUID = res.video.uuid | ||
76 | this.isInUpdateForm = true | ||
77 | |||
78 | this.fetchVideoLive() | ||
79 | }, | ||
80 | |||
81 | err => { | ||
82 | this.firstStepError.emit() | ||
83 | this.notifier.error(err.message) | ||
84 | } | ||
85 | ) | ||
86 | } | ||
87 | |||
88 | updateSecondStep () { | ||
89 | if (this.checkForm() === false) { | ||
90 | return | ||
91 | } | ||
92 | |||
93 | const video = new VideoEdit() | ||
94 | video.patch(this.form.value) | ||
95 | video.id = this.videoId | ||
96 | video.uuid = this.videoUUID | ||
97 | |||
98 | // Update the video | ||
99 | this.updateVideoAndCaptions(video) | ||
100 | .subscribe( | ||
101 | () => { | ||
102 | this.notifier.success($localize`Live published.`) | ||
103 | |||
104 | this.router.navigate([ '/videos/watch', video.uuid ]) | ||
105 | }, | ||
106 | |||
107 | err => { | ||
108 | this.error = err.message | ||
109 | scrollToTop() | ||
110 | console.error(err) | ||
111 | } | ||
112 | ) | ||
113 | |||
114 | } | ||
115 | |||
116 | private fetchVideoLive () { | ||
117 | this.videoLiveService.getVideoLive(this.videoId) | ||
118 | .subscribe( | ||
119 | videoLive => { | ||
120 | this.videoLive = videoLive | ||
121 | }, | ||
122 | |||
123 | err => { | ||
124 | this.firstStepError.emit() | ||
125 | this.notifier.error(err.message) | ||
126 | } | ||
127 | ) | ||
128 | } | ||
129 | } | ||
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' | |||
6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 6 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
7 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
8 | import { VideoPrivacy, VideoUpdate } from '@shared/models' | 8 | import { VideoPrivacy, VideoUpdate } from '@shared/models' |
9 | import { hydrateFormFromVideo } from '../shared/video-edit-utils' | ||
9 | import { VideoSend } from './video-send' | 10 | import { VideoSend } from './video-send' |
10 | 11 | ||
11 | @Component({ | 12 | @Component({ |
@@ -99,7 +100,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
99 | previewUrl: null | 100 | previewUrl: null |
100 | })) | 101 | })) |
101 | 102 | ||
102 | this.hydrateFormFromVideo() | 103 | hydrateFormFromVideo(this.form, this.video, false) |
103 | }, | 104 | }, |
104 | 105 | ||
105 | err => { | 106 | err => { |
@@ -136,10 +137,5 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
136 | console.error(err) | 137 | console.error(err) |
137 | } | 138 | } |
138 | ) | 139 | ) |
139 | |||
140 | } | ||
141 | |||
142 | private hydrateFormFromVideo () { | ||
143 | this.form.patchValue(this.video.toFormPatch()) | ||
144 | } | 140 | } |
145 | } | 141 | } |
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' | |||
7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 8 | import { LoadingBarService } from '@ngx-loading-bar/core' |
9 | import { VideoPrivacy, VideoUpdate } from '@shared/models' | 9 | import { VideoPrivacy, VideoUpdate } from '@shared/models' |
10 | import { hydrateFormFromVideo } from '../shared/video-edit-utils' | ||
10 | import { VideoSend } from './video-send' | 11 | import { VideoSend } from './video-send' |
11 | 12 | ||
12 | @Component({ | 13 | @Component({ |
@@ -109,7 +110,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
109 | 110 | ||
110 | this.videoCaptions = videoCaptions | 111 | this.videoCaptions = videoCaptions |
111 | 112 | ||
112 | this.hydrateFormFromVideo() | 113 | hydrateFormFromVideo(this.form, this.video, true) |
113 | }, | 114 | }, |
114 | 115 | ||
115 | err => { | 116 | err => { |
@@ -146,31 +147,5 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
146 | console.error(err) | 147 | console.error(err) |
147 | } | 148 | } |
148 | ) | 149 | ) |
149 | |||
150 | } | ||
151 | |||
152 | private hydrateFormFromVideo () { | ||
153 | this.form.patchValue(this.video.toFormPatch()) | ||
154 | |||
155 | const objects = [ | ||
156 | { | ||
157 | url: 'thumbnailUrl', | ||
158 | name: 'thumbnailfile' | ||
159 | }, | ||
160 | { | ||
161 | url: 'previewUrl', | ||
162 | name: 'previewfile' | ||
163 | } | ||
164 | ] | ||
165 | |||
166 | for (const obj of objects) { | ||
167 | fetch(this.video[obj.url]) | ||
168 | .then(response => response.blob()) | ||
169 | .then(data => { | ||
170 | this.form.patchValue({ | ||
171 | [ obj.name ]: data | ||
172 | }) | ||
173 | }) | ||
174 | } | ||
175 | } | 150 | } |
176 | } | 151 | } |
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 | |||
157 | this.waitTranscodingEnabled = false | 157 | this.waitTranscodingEnabled = false |
158 | } | 158 | } |
159 | 159 | ||
160 | const privacy = this.firstStepPrivacyId.toString() | ||
161 | const nsfw = this.serverConfig.instance.isNSFW | 160 | const nsfw = this.serverConfig.instance.isNSFW |
162 | const waitTranscoding = true | 161 | const waitTranscoding = true |
163 | const commentsEnabled = true | 162 | 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 @@ | |||
50 | <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent> | 50 | <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent> |
51 | </ng-template> | 51 | </ng-template> |
52 | </ng-container> | 52 | </ng-container> |
53 | |||
54 | <ng-container ngbNavItem *ngIf="isVideoLiveEnabled()"> | ||
55 | <a ngbNavLink> | ||
56 | <span i18n>Go live</span> | ||
57 | </a> | ||
58 | |||
59 | <ng-template ngbNavContent> | ||
60 | <my-video-go-live #videoGoLive (firstStepDone)="onFirstStepDone('go-live', $event)" (firstStepError)="onError()"></my-video-go-live> | ||
61 | </ng-template> | ||
62 | </ng-container> | ||
53 | </div> | 63 | </div> |
54 | 64 | ||
55 | <div [ngbNavOutlet]="nav"></div> | 65 | <div [ngbNavOutlet]="nav"></div> |
56 | </div> \ No newline at end of file | 66 | </div> |
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 @@ | |||
1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' | 2 | import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' |
3 | import { ServerConfig } from '@shared/models' | 3 | import { ServerConfig } from '@shared/models' |
4 | import { VideoEditType } from './shared/video-edit.type' | ||
5 | import { VideoGoLiveComponent } from './video-add-components/video-go-live.component' | ||
4 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' | 6 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' |
5 | import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' | 7 | import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' |
6 | import { VideoUploadComponent } from './video-add-components/video-upload.component' | 8 | import { VideoUploadComponent } from './video-add-components/video-upload.component' |
@@ -14,10 +16,11 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
14 | @ViewChild('videoUpload') videoUpload: VideoUploadComponent | 16 | @ViewChild('videoUpload') videoUpload: VideoUploadComponent |
15 | @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent | 17 | @ViewChild('videoImportUrl') videoImportUrl: VideoImportUrlComponent |
16 | @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent | 18 | @ViewChild('videoImportTorrent') videoImportTorrent: VideoImportTorrentComponent |
19 | @ViewChild('videoGoLive') videoGoLive: VideoGoLiveComponent | ||
17 | 20 | ||
18 | user: AuthUser = null | 21 | user: AuthUser = null |
19 | 22 | ||
20 | secondStepType: 'upload' | 'import-url' | 'import-torrent' | 23 | secondStepType: VideoEditType |
21 | videoName: string | 24 | videoName: string |
22 | serverConfig: ServerConfig | 25 | serverConfig: ServerConfig |
23 | 26 | ||
@@ -41,7 +44,7 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
41 | this.user = this.auth.getUser() | 44 | this.user = this.auth.getUser() |
42 | } | 45 | } |
43 | 46 | ||
44 | onFirstStepDone (type: 'upload' | 'import-url' | 'import-torrent', videoName: string) { | 47 | onFirstStepDone (type: VideoEditType, videoName: string) { |
45 | this.secondStepType = type | 48 | this.secondStepType = type |
46 | this.videoName = videoName | 49 | this.videoName = videoName |
47 | } | 50 | } |
@@ -62,9 +65,9 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
62 | } | 65 | } |
63 | 66 | ||
64 | canDeactivate (): { canDeactivate: boolean, text?: string} { | 67 | canDeactivate (): { canDeactivate: boolean, text?: string} { |
65 | if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() | ||
66 | if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() | 68 | if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() |
67 | if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() | 69 | if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() |
70 | if (this.secondStepType === 'go-live') return this.videoGoLive.canDeactivate() | ||
68 | 71 | ||
69 | return { canDeactivate: true } | 72 | return { canDeactivate: true } |
70 | } | 73 | } |
@@ -77,6 +80,10 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate { | |||
77 | return this.serverConfig.import.videos.torrent.enabled | 80 | return this.serverConfig.import.videos.torrent.enabled |
78 | } | 81 | } |
79 | 82 | ||
83 | isVideoLiveEnabled () { | ||
84 | return this.serverConfig.live.enabled | ||
85 | } | ||
86 | |||
80 | isInSecondStep () { | 87 | isInSecondStep () { |
81 | return !!this.secondStepType | 88 | return !!this.secondStepType |
82 | } | 89 | } |
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' | |||
4 | import { DragDropDirective } from './video-add-components/drag-drop.directive' | 4 | import { DragDropDirective } from './video-add-components/drag-drop.directive' |
5 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' | 5 | import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' |
6 | import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' | 6 | import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' |
7 | import { VideoGoLiveComponent } from './video-add-components/video-go-live.component' | ||
7 | import { VideoUploadComponent } from './video-add-components/video-upload.component' | 8 | import { VideoUploadComponent } from './video-add-components/video-upload.component' |
8 | import { VideoAddRoutingModule } from './video-add-routing.module' | 9 | import { VideoAddRoutingModule } from './video-add-routing.module' |
9 | import { VideoAddComponent } from './video-add.component' | 10 | import { VideoAddComponent } from './video-add.component' |
@@ -20,7 +21,8 @@ import { VideoAddComponent } from './video-add.component' | |||
20 | VideoUploadComponent, | 21 | VideoUploadComponent, |
21 | VideoImportUrlComponent, | 22 | VideoImportUrlComponent, |
22 | VideoImportTorrentComponent, | 23 | VideoImportTorrentComponent, |
23 | DragDropDirective | 24 | DragDropDirective, |
25 | VideoGoLiveComponent | ||
24 | ], | 26 | ], |
25 | 27 | ||
26 | exports: [ ], | 28 | 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 @@ | |||
11 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" | 11 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" |
12 | [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" | 12 | [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" |
13 | type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" | 13 | type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" |
14 | [videoLive]="videoLive" | ||
14 | ></my-video-edit> | 15 | ></my-video-edit> |
15 | 16 | ||
16 | <div class="submit-container"> | 17 | <div class="submit-container"> |
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' | |||
5 | import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' |
6 | import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' | 6 | import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' |
7 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
8 | import { VideoPrivacy } from '@shared/models' | 8 | import { VideoPrivacy, VideoLive } from '@shared/models' |
9 | import { hydrateFormFromVideo } from './shared/video-edit-utils' | ||
9 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-videos-update', | 12 | selector: 'my-videos-update', |
@@ -14,11 +15,12 @@ import { VideoPrivacy } from '@shared/models' | |||
14 | }) | 15 | }) |
15 | export class VideoUpdateComponent extends FormReactive implements OnInit { | 16 | export class VideoUpdateComponent extends FormReactive implements OnInit { |
16 | video: VideoEdit | 17 | video: VideoEdit |
18 | userVideoChannels: SelectChannelItem[] = [] | ||
19 | videoCaptions: VideoCaptionEdit[] = [] | ||
20 | videoLive: VideoLive | ||
17 | 21 | ||
18 | isUpdatingVideo = false | 22 | isUpdatingVideo = false |
19 | userVideoChannels: SelectChannelItem[] = [] | ||
20 | schedulePublicationPossible = false | 23 | schedulePublicationPossible = false |
21 | videoCaptions: VideoCaptionEdit[] = [] | ||
22 | waitTranscodingEnabled = true | 24 | waitTranscodingEnabled = true |
23 | 25 | ||
24 | private updateDone = false | 26 | private updateDone = false |
@@ -40,10 +42,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
40 | 42 | ||
41 | this.route.data | 43 | this.route.data |
42 | .pipe(map(data => data.videoData)) | 44 | .pipe(map(data => data.videoData)) |
43 | .subscribe(({ video, videoChannels, videoCaptions }) => { | 45 | .subscribe(({ video, videoChannels, videoCaptions, videoLive }) => { |
44 | this.video = new VideoEdit(video) | 46 | this.video = new VideoEdit(video) |
45 | this.userVideoChannels = videoChannels | 47 | this.userVideoChannels = videoChannels |
46 | this.videoCaptions = videoCaptions | 48 | this.videoCaptions = videoCaptions |
49 | this.videoLive = videoLive | ||
47 | 50 | ||
48 | this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE | 51 | this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE |
49 | 52 | ||
@@ -53,7 +56,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
53 | } | 56 | } |
54 | 57 | ||
55 | // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout | 58 | // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout |
56 | setTimeout(() => this.hydrateFormFromVideo()) | 59 | setTimeout(() => hydrateFormFromVideo(this.form, this.video, true)) |
57 | }, | 60 | }, |
58 | 61 | ||
59 | err => { | 62 | err => { |
@@ -133,29 +136,4 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
133 | pluginData: this.video.pluginData | 136 | pluginData: this.video.pluginData |
134 | }) | 137 | }) |
135 | } | 138 | } |
136 | |||
137 | private hydrateFormFromVideo () { | ||
138 | this.form.patchValue(this.video.toFormPatch()) | ||
139 | |||
140 | const objects = [ | ||
141 | { | ||
142 | url: 'thumbnailUrl', | ||
143 | name: 'thumbnailfile' | ||
144 | }, | ||
145 | { | ||
146 | url: 'previewUrl', | ||
147 | name: 'previewfile' | ||
148 | } | ||
149 | ] | ||
150 | |||
151 | for (const obj of objects) { | ||
152 | fetch(this.video[obj.url]) | ||
153 | .then(response => response.blob()) | ||
154 | .then(data => { | ||
155 | this.form.patchValue({ | ||
156 | [ obj.name ]: data | ||
157 | }) | ||
158 | }) | ||
159 | } | ||
160 | } | ||
161 | } | 139 | } |
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 @@ | |||
1 | import { forkJoin } from 'rxjs' | 1 | import { forkJoin, of } from 'rxjs' |
2 | import { map, switchMap } from 'rxjs/operators' | 2 | import { map, switchMap } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' | 4 | import { ActivatedRouteSnapshot, Resolve } from '@angular/router' |
5 | import { VideoCaptionService, VideoChannelService, VideoService } from '@app/shared/shared-main' | 5 | import { VideoCaptionService, VideoChannelService, VideoDetails, VideoLiveService, VideoService } from '@app/shared/shared-main' |
6 | 6 | ||
7 | @Injectable() | 7 | @Injectable() |
8 | export class VideoUpdateResolver implements Resolve<any> { | 8 | export class VideoUpdateResolver implements Resolve<any> { |
9 | constructor ( | 9 | constructor ( |
10 | private videoService: VideoService, | 10 | private videoService: VideoService, |
11 | private videoLiveService: VideoLiveService, | ||
11 | private videoChannelService: VideoChannelService, | 12 | private videoChannelService: VideoChannelService, |
12 | private videoCaptionService: VideoCaptionService | 13 | private videoCaptionService: VideoCaptionService |
13 | ) { | 14 | ) { |
@@ -18,32 +19,38 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
18 | 19 | ||
19 | return this.videoService.getVideo({ videoId: uuid }) | 20 | return this.videoService.getVideo({ videoId: uuid }) |
20 | .pipe( | 21 | .pipe( |
21 | switchMap(video => { | 22 | switchMap(video => forkJoin(this.buildVideoObservables(video))), |
22 | return forkJoin([ | 23 | map(([ video, videoChannels, videoCaptions, videoLive ]) => ({ video, videoChannels, videoCaptions, videoLive })) |
23 | this.videoService | 24 | ) |
24 | .loadCompleteDescription(video.descriptionPath) | 25 | } |
25 | .pipe(map(description => Object.assign(video, { description }))), | ||
26 | 26 | ||
27 | this.videoChannelService | 27 | private buildVideoObservables (video: VideoDetails) { |
28 | .listAccountVideoChannels(video.account) | 28 | return [ |
29 | .pipe( | 29 | this.videoService |
30 | map(result => result.data), | 30 | .loadCompleteDescription(video.descriptionPath) |
31 | map(videoChannels => videoChannels.map(c => ({ | 31 | .pipe(map(description => Object.assign(video, { description }))), |
32 | id: c.id, | ||
33 | label: c.displayName, | ||
34 | support: c.support, | ||
35 | avatarPath: c.avatar?.path | ||
36 | }))) | ||
37 | ), | ||
38 | 32 | ||
39 | this.videoCaptionService | 33 | this.videoChannelService |
40 | .listCaptions(video.id) | 34 | .listAccountVideoChannels(video.account) |
41 | .pipe( | 35 | .pipe( |
42 | map(result => result.data) | 36 | map(result => result.data), |
43 | ) | 37 | map(videoChannels => videoChannels.map(c => ({ |
44 | ]) | 38 | id: c.id, |
45 | }), | 39 | label: c.displayName, |
46 | map(([ video, videoChannels, videoCaptions ]) => ({ video, videoChannels, videoCaptions })) | 40 | support: c.support, |
47 | ) | 41 | avatarPath: c.avatar?.path |
42 | }))) | ||
43 | ), | ||
44 | |||
45 | this.videoCaptionService | ||
46 | .listCaptions(video.id) | ||
47 | .pipe( | ||
48 | map(result => result.data) | ||
49 | ), | ||
50 | |||
51 | video.isLive | ||
52 | ? this.videoLiveService.getVideoLive(video.id) | ||
53 | : of(undefined) | ||
54 | ] | ||
48 | } | 55 | } |
49 | } | 56 | } |
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' | |||
2 | import { catchError, first, map, shareReplay } from 'rxjs/operators' | 2 | import { catchError, first, map, shareReplay } from 'rxjs/operators' |
3 | import { HttpClient } from '@angular/common/http' | 3 | import { HttpClient } from '@angular/common/http' |
4 | import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' | 4 | import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' |
5 | import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type' | ||
5 | import { AuthService } from '@app/core/auth' | 6 | import { AuthService } from '@app/core/auth' |
6 | import { Notifier } from '@app/core/notification' | 7 | import { Notifier } from '@app/core/notification' |
7 | import { MarkdownService } from '@app/core/renderer' | 8 | import { MarkdownService } from '@app/core/renderer' |
@@ -192,7 +193,7 @@ export class PluginService implements ClientHook { | |||
192 | : PluginType.THEME | 193 | : PluginType.THEME |
193 | } | 194 | } |
194 | 195 | ||
195 | getRegisteredVideoFormFields (type: 'import-url' | 'import-torrent' | 'upload' | 'update') { | 196 | getRegisteredVideoFormFields (type: VideoEditType) { |
196 | return this.formFields.video.filter(f => f.videoFormOptions.type === type) | 197 | return this.formFields.video.filter(f => f.videoFormOptions.type === type) |
197 | } | 198 | } |
198 | 199 | ||
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 { | |||
74 | enabled: true | 74 | enabled: true |
75 | } | 75 | } |
76 | }, | 76 | }, |
77 | live: { | ||
78 | enabled: false, | ||
79 | transcoding: { | ||
80 | enabled: false, | ||
81 | enabledResolutions: [] | ||
82 | } | ||
83 | }, | ||
77 | avatar: { | 84 | avatar: { |
78 | file: { | 85 | file: { |
79 | size: { max: 0 }, | 86 | 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 @@ | |||
1 | <div class="input-group input-group-sm"> | 1 | <div class="input-group input-group-sm"> |
2 | <input #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="value" /> | 2 | <input [id]="id" #urlInput (click)="urlInput.select()" type="text" class="form-control readonly" readonly [value]="value" /> |
3 | 3 | ||
4 | <div class="input-group-append"> | 4 | <div class="input-group-append"> |
5 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> | 5 | <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> |
diff --git a/client/src/app/shared/shared-forms/input-readonly-copy.component.ts b/client/src/app/shared/shared-forms/input-readonly-copy.component.ts index a67b0c691..520827a53 100644 --- a/client/src/app/shared/shared-forms/input-readonly-copy.component.ts +++ b/client/src/app/shared/shared-forms/input-readonly-copy.component.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { FormGroup } from '@angular/forms' | ||
3 | 4 | ||
4 | @Component({ | 5 | @Component({ |
5 | selector: 'my-input-readonly-copy', | 6 | selector: 'my-input-readonly-copy', |
@@ -7,6 +8,7 @@ import { Notifier } from '@app/core' | |||
7 | styleUrls: [ './input-readonly-copy.component.scss' ] | 8 | styleUrls: [ './input-readonly-copy.component.scss' ] |
8 | }) | 9 | }) |
9 | export class InputReadonlyCopyComponent { | 10 | export class InputReadonlyCopyComponent { |
11 | @Input() id: string | ||
10 | @Input() value = '' | 12 | @Input() value = '' |
11 | 13 | ||
12 | constructor (private notifier: Notifier) { } | 14 | constructor (private notifier: Notifier) { } |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html index f6a3b7f0b..002695238 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.html +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -64,6 +64,24 @@ | |||
64 | </tr> | 64 | </tr> |
65 | 65 | ||
66 | <tr> | 66 | <tr> |
67 | <th i18n class="label" colspan="2">Live streaming</th> | ||
68 | </tr> | ||
69 | |||
70 | <tr> | ||
71 | <th i18n class="sub-label" scope="row">Live streaming enabled</th> | ||
72 | <td> | ||
73 | <my-feature-boolean [value]="serverConfig.live.enabled"></my-feature-boolean> | ||
74 | </td> | ||
75 | </tr> | ||
76 | |||
77 | <tr> | ||
78 | <th i18n class="sub-label" scope="row">Transcode live video in multiple resolutions</th> | ||
79 | <td> | ||
80 | <my-feature-boolean [value]="serverConfig.live.transcoding.enabled && serverConfig.live.transcoding.enabledResolutions.length > 1"></my-feature-boolean> | ||
81 | </td> | ||
82 | </tr> | ||
83 | |||
84 | <tr> | ||
67 | <th i18n class="label" colspan="2">Import</th> | 85 | <th i18n class="label" colspan="2">Import</th> |
68 | </tr> | 86 | </tr> |
69 | 87 | ||
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index 7f4676dd1..bca67b193 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -23,7 +23,7 @@ import { FeedComponent } from './feeds' | |||
23 | import { LoaderComponent, SmallLoaderComponent } from './loaders' | 23 | import { LoaderComponent, SmallLoaderComponent } from './loaders' |
24 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc' | 24 | import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc' |
25 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 25 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
26 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' | 26 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService, VideoLiveService } from './video' |
27 | import { VideoCaptionService } from './video-caption' | 27 | import { VideoCaptionService } from './video-caption' |
28 | import { VideoChannelService } from './video-channel' | 28 | import { VideoChannelService } from './video-channel' |
29 | 29 | ||
@@ -142,6 +142,7 @@ import { VideoChannelService } from './video-channel' | |||
142 | RedundancyService, | 142 | RedundancyService, |
143 | VideoImportService, | 143 | VideoImportService, |
144 | VideoOwnershipService, | 144 | VideoOwnershipService, |
145 | VideoLiveService, | ||
145 | VideoService, | 146 | VideoService, |
146 | 147 | ||
147 | VideoCaptionService, | 148 | VideoCaptionService, |
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 3053df4ef..121635a30 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -2,6 +2,7 @@ export * from './redundancy.service' | |||
2 | export * from './video-details.model' | 2 | export * from './video-details.model' |
3 | export * from './video-edit.model' | 3 | export * from './video-edit.model' |
4 | export * from './video-import.service' | 4 | export * from './video-import.service' |
5 | export * from './video-live.service' | ||
5 | export * from './video-ownership.service' | 6 | export * from './video-ownership.service' |
6 | export * from './video.model' | 7 | export * from './video.model' |
7 | export * from './video.service' | 8 | export * from './video.service' |
diff --git a/client/src/app/shared/shared-main/video/video-details.model.ts b/client/src/app/shared/shared-main/video/video-details.model.ts index a1cb051e9..f060d1dc9 100644 --- a/client/src/app/shared/shared-main/video/video-details.model.ts +++ b/client/src/app/shared/shared-main/video/video-details.model.ts | |||
@@ -62,8 +62,11 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
62 | } | 62 | } |
63 | 63 | ||
64 | getFiles () { | 64 | getFiles () { |
65 | if (this.files.length === 0) return this.getHlsPlaylist().files | 65 | if (this.files.length !== 0) return this.files |
66 | 66 | ||
67 | return this.files | 67 | const hls = this.getHlsPlaylist() |
68 | if (hls) return hls.files | ||
69 | |||
70 | return [] | ||
68 | } | 71 | } |
69 | } | 72 | } |
diff --git a/client/src/app/shared/shared-main/video/video-live.service.ts b/client/src/app/shared/shared-main/video/video-live.service.ts new file mode 100644 index 000000000..12daff756 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-live.service.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import { catchError } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { VideoCreate, VideoLive } from '@shared/models' | ||
6 | import { environment } from '../../../../environments/environment' | ||
7 | |||
8 | @Injectable() | ||
9 | export class VideoLiveService { | ||
10 | static BASE_VIDEO_LIVE_URL = environment.apiUrl + '/api/v1/videos/live/' | ||
11 | |||
12 | constructor ( | ||
13 | private authHttp: HttpClient, | ||
14 | private restExtractor: RestExtractor | ||
15 | ) {} | ||
16 | |||
17 | goLive (video: VideoCreate) { | ||
18 | return this.authHttp | ||
19 | .post<{ video: { id: number, uuid: string } }>(VideoLiveService.BASE_VIDEO_LIVE_URL, video) | ||
20 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
21 | } | ||
22 | |||
23 | getVideoLive (videoId: number | string) { | ||
24 | return this.authHttp | ||
25 | .get<VideoLive>(VideoLiveService.BASE_VIDEO_LIVE_URL + videoId) | ||
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
27 | } | ||
28 | } | ||
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts index 0dca3da0d..e3a52af3d 100644 --- a/client/src/app/shared/shared-main/video/video.model.ts +++ b/client/src/app/shared/shared-main/video/video.model.ts | |||
@@ -40,6 +40,8 @@ export class Video implements VideoServerModel { | |||
40 | thumbnailPath: string | 40 | thumbnailPath: string |
41 | thumbnailUrl: string | 41 | thumbnailUrl: string |
42 | 42 | ||
43 | isLive: boolean | ||
44 | |||
43 | previewPath: string | 45 | previewPath: string |
44 | previewUrl: string | 46 | previewUrl: string |
45 | 47 | ||
@@ -103,6 +105,8 @@ export class Video implements VideoServerModel { | |||
103 | this.state = hash.state | 105 | this.state = hash.state |
104 | this.description = hash.description | 106 | this.description = hash.description |
105 | 107 | ||
108 | this.isLive = hash.isLive | ||
109 | |||
106 | this.duration = hash.duration | 110 | this.duration = hash.duration |
107 | this.durationLabel = durationToString(hash.duration) | 111 | this.durationLabel = durationToString(hash.duration) |
108 | 112 | ||
@@ -113,10 +117,14 @@ export class Video implements VideoServerModel { | |||
113 | this.name = hash.name | 117 | this.name = hash.name |
114 | 118 | ||
115 | this.thumbnailPath = hash.thumbnailPath | 119 | this.thumbnailPath = hash.thumbnailPath |
116 | this.thumbnailUrl = hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath) | 120 | this.thumbnailUrl = this.thumbnailPath |
121 | ? hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath) | ||
122 | : null | ||
117 | 123 | ||
118 | this.previewPath = hash.previewPath | 124 | this.previewPath = hash.previewPath |
119 | this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath) | 125 | this.previewUrl = this.previewPath |
126 | ? hash.previewUrl || (absoluteAPIUrl + hash.previewPath) | ||
127 | : null | ||
120 | 128 | ||
121 | this.embedPath = hash.embedPath | 129 | this.embedPath = hash.embedPath |
122 | this.embedUrl = hash.embedUrl || (getAbsoluteEmbedUrl() + hash.embedPath) | 130 | this.embedUrl = hash.embedUrl || (getAbsoluteEmbedUrl() + hash.embedPath) |
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index 8a688c8ed..0e2d36081 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts | |||
@@ -18,7 +18,8 @@ import { | |||
18 | VideoFilter, | 18 | VideoFilter, |
19 | VideoPrivacy, | 19 | VideoPrivacy, |
20 | VideoSortField, | 20 | VideoSortField, |
21 | VideoUpdate | 21 | VideoUpdate, |
22 | VideoCreate | ||
22 | } from '@shared/models' | 23 | } from '@shared/models' |
23 | import { environment } from '../../../../environments/environment' | 24 | import { environment } from '../../../../environments/environment' |
24 | import { Account } from '../account/account.model' | 25 | import { Account } from '../account/account.model' |