aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-09-17 09:20:52 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-11-09 15:33:04 +0100
commitc6c0fa6cd8fe8f752463d8982c3dbcd448739c4e (patch)
tree79304b0152b0a38d33b26e65d4acdad0da4032a7 /client/src
parent110d463fece85e87a26aca48a6048ae0017a27b3 (diff)
downloadPeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.tar.gz
PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.tar.zst
PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.zip
Live streaming implementation first step
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html83
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts31
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit-utils.ts35
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html23
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts16
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.type.ts1
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html47
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts129
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts8
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts29
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts1
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.html12
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add.module.ts4
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html1
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts38
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts61
-rw-r--r--client/src/app/core/plugins/plugin.service.ts3
-rw-r--r--client/src/app/core/server/server.service.ts7
-rw-r--r--client/src/app/shared/shared-forms/input-readonly-copy.component.html2
-rw-r--r--client/src/app/shared/shared-forms/input-readonly-copy.component.ts2
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.html18
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts3
-rw-r--r--client/src/app/shared/shared-main/video/index.ts1
-rw-r--r--client/src/app/shared/shared-main/video/video-details.model.ts7
-rw-r--r--client/src/app/shared/shared-main/video/video-live.service.ts28
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts12
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts3
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-validator.ts37
-rw-r--r--client/src/assets/player/peertube-player-manager.ts4
-rw-r--r--client/src/standalone/videos/embed.ts4
31 files changed, 544 insertions, 119 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 @@
1import { FormGroup } from '@angular/forms'
2import { VideoEdit } from '@app/shared/shared-main'
3
4function 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
33export {
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 {
20import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' 20import { FormReactiveValidationMessages, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms'
21import { InstanceService } from '@app/shared/shared-instance' 21import { InstanceService } from '@app/shared/shared-instance'
22import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 22import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
23import { ServerConfig, VideoConstant, VideoPrivacy } from '@shared/models' 23import { ServerConfig, VideoConstant, VideoLive, VideoPrivacy } from '@shared/models'
24import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' 24import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
25import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 25import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
26import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 26import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
27import { VideoEditType } from './video-edit.type'
27 28
28type VideoLanguages = VideoConstant<string> & { group?: string } 29type 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
2import { Component, EventEmitter, OnInit, Output } from '@angular/core'
3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
5import { scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoService, VideoLiveService } from '@app/shared/shared-main'
8import { LoadingBarService } from '@ngx-loading-bar/core'
9import { VideoCreate, VideoLive, VideoPrivacy } from '@shared/models'
10import { 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})
20export 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'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { VideoPrivacy, VideoUpdate } from '@shared/models' 8import { VideoPrivacy, VideoUpdate } from '@shared/models'
9import { hydrateFormFromVideo } from '../shared/video-edit-utils'
9import { VideoSend } from './video-send' 10import { 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'
7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
8import { LoadingBarService } from '@ngx-loading-bar/core' 8import { LoadingBarService } from '@ngx-loading-bar/core'
9import { VideoPrivacy, VideoUpdate } from '@shared/models' 9import { VideoPrivacy, VideoUpdate } from '@shared/models'
10import { hydrateFormFromVideo } from '../shared/video-edit-utils'
10import { VideoSend } from './video-send' 11import { 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 @@
1import { Component, HostListener, OnInit, ViewChild } from '@angular/core' 1import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
2import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' 2import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models' 3import { ServerConfig } from '@shared/models'
4import { VideoEditType } from './shared/video-edit.type'
5import { VideoGoLiveComponent } from './video-add-components/video-go-live.component'
4import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' 6import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
5import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' 7import { VideoImportUrlComponent } from './video-add-components/video-import-url.component'
6import { VideoUploadComponent } from './video-add-components/video-upload.component' 8import { 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'
4import { DragDropDirective } from './video-add-components/drag-drop.directive' 4import { DragDropDirective } from './video-add-components/drag-drop.directive'
5import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' 5import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component'
6import { VideoImportUrlComponent } from './video-add-components/video-import-url.component' 6import { VideoImportUrlComponent } from './video-add-components/video-import-url.component'
7import { VideoGoLiveComponent } from './video-add-components/video-go-live.component'
7import { VideoUploadComponent } from './video-add-components/video-upload.component' 8import { VideoUploadComponent } from './video-add-components/video-upload.component'
8import { VideoAddRoutingModule } from './video-add-routing.module' 9import { VideoAddRoutingModule } from './video-add-routing.module'
9import { VideoAddComponent } from './video-add.component' 10import { 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'
5import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms' 5import { FormReactive, FormValidatorService, SelectChannelItem } from '@app/shared/shared-forms'
6import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { VideoPrivacy } from '@shared/models' 8import { VideoPrivacy, VideoLive } from '@shared/models'
9import { 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})
15export class VideoUpdateComponent extends FormReactive implements OnInit { 16export 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 @@
1import { forkJoin } from 'rxjs' 1import { forkJoin, of } from 'rxjs'
2import { map, switchMap } from 'rxjs/operators' 2import { map, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { ActivatedRouteSnapshot, Resolve } from '@angular/router' 4import { ActivatedRouteSnapshot, Resolve } from '@angular/router'
5import { VideoCaptionService, VideoChannelService, VideoService } from '@app/shared/shared-main' 5import { VideoCaptionService, VideoChannelService, VideoDetails, VideoLiveService, VideoService } from '@app/shared/shared-main'
6 6
7@Injectable() 7@Injectable()
8export class VideoUpdateResolver implements Resolve<any> { 8export 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'
2import { catchError, first, map, shareReplay } from 'rxjs/operators' 2import { catchError, first, map, shareReplay } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http' 3import { HttpClient } from '@angular/common/http'
4import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core' 4import { Inject, Injectable, LOCALE_ID, NgZone } from '@angular/core'
5import { VideoEditType } from '@app/+videos/+video-edit/shared/video-edit.type'
5import { AuthService } from '@app/core/auth' 6import { AuthService } from '@app/core/auth'
6import { Notifier } from '@app/core/notification' 7import { Notifier } from '@app/core/notification'
7import { MarkdownService } from '@app/core/renderer' 8import { 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 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { 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})
9export class InputReadonlyCopyComponent { 10export 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'
23import { LoaderComponent, SmallLoaderComponent } from './loaders' 23import { LoaderComponent, SmallLoaderComponent } from './loaders'
24import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc' 24import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent } from './misc'
25import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 25import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
26import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 26import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService, VideoLiveService } from './video'
27import { VideoCaptionService } from './video-caption' 27import { VideoCaptionService } from './video-caption'
28import { VideoChannelService } from './video-channel' 28import { 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'
2export * from './video-details.model' 2export * from './video-details.model'
3export * from './video-edit.model' 3export * from './video-edit.model'
4export * from './video-import.service' 4export * from './video-import.service'
5export * from './video-live.service'
5export * from './video-ownership.service' 6export * from './video-ownership.service'
6export * from './video.model' 7export * from './video.model'
7export * from './video.service' 8export * 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 @@
1import { catchError } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core'
5import { VideoCreate, VideoLive } from '@shared/models'
6import { environment } from '../../../../environments/environment'
7
8@Injectable()
9export 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'
23import { environment } from '../../../../environments/environment' 24import { environment } from '../../../../environments/environment'
24import { Account } from '../account/account.model' 25import { Account } from '../account/account.model'
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts
index 72c32f9e0..0614f73d2 100644
--- a/client/src/assets/player/p2p-media-loader/segment-validator.ts
+++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts
@@ -1,17 +1,42 @@
1import { Segment } from 'p2p-media-loader-core' 1import { Segment } from 'p2p-media-loader-core'
2import { basename } from 'path' 2import { basename } from 'path'
3 3
4type SegmentsJSON = { [filename: string]: string | { [byterange: string]: string } }
5
4function segmentValidatorFactory (segmentsSha256Url: string) { 6function segmentValidatorFactory (segmentsSha256Url: string) {
5 const segmentsJSON = fetchSha256Segments(segmentsSha256Url) 7 let segmentsJSON = fetchSha256Segments(segmentsSha256Url)
6 const regex = /bytes=(\d+)-(\d+)/ 8 const regex = /bytes=(\d+)-(\d+)/
7 9
8 return async function segmentValidator (segment: Segment) { 10 return async function segmentValidator (segment: Segment, canRefetchSegmentHashes = true) {
9 const filename = basename(segment.url) 11 const filename = basename(segment.url)
10 const captured = regex.exec(segment.range)
11 12
12 const range = captured[1] + '-' + captured[2] 13 const segmentValue = (await segmentsJSON)[filename]
14
15 if (!segmentValue && !canRefetchSegmentHashes) {
16 throw new Error(`Unknown segment name ${filename} in segment validator`)
17 }
18
19 if (!segmentValue) {
20 console.log('Refetching sha segments.')
21
22 // Refetch
23 segmentsJSON = fetchSha256Segments(segmentsSha256Url)
24 segmentValidator(segment, false)
25 return
26 }
27
28 let hashShouldBe: string
29 let range = ''
30
31 if (typeof segmentValue === 'string') {
32 hashShouldBe = segmentValue
33 } else {
34 const captured = regex.exec(segment.range)
35 range = captured[1] + '-' + captured[2]
36
37 hashShouldBe = segmentValue[range]
38 }
13 39
14 const hashShouldBe = (await segmentsJSON)[filename][range]
15 if (hashShouldBe === undefined) { 40 if (hashShouldBe === undefined) {
16 throw new Error(`Unknown segment name ${filename}/${range} in segment validator`) 41 throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
17 } 42 }
@@ -36,7 +61,7 @@ export {
36 61
37function fetchSha256Segments (url: string) { 62function fetchSha256Segments (url: string) {
38 return fetch(url) 63 return fetch(url)
39 .then(res => res.json()) 64 .then(res => res.json() as Promise<SegmentsJSON>)
40 .catch(err => { 65 .catch(err => {
41 console.error('Cannot get sha256 segments', err) 66 console.error('Cannot get sha256 segments', err)
42 return {} 67 return {}
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index af044c864..3d72d4609 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -325,7 +325,7 @@ export class PeertubePlayerManager {
325 trackerAnnounce, 325 trackerAnnounce,
326 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url), 326 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url),
327 rtcConfig: getRtcConfig(), 327 rtcConfig: getRtcConfig(),
328 requiredSegmentsPriority: 5, 328 requiredSegmentsPriority: 1,
329 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager), 329 segmentUrlBuilder: segmentUrlBuilderFactory(redundancyUrlManager),
330 useP2P: getStoredP2PEnabled(), 330 useP2P: getStoredP2PEnabled(),
331 consumeOnly 331 consumeOnly
@@ -353,7 +353,7 @@ export class PeertubePlayerManager {
353 hlsjsConfig: { 353 hlsjsConfig: {
354 capLevelToPlayerSize: true, 354 capLevelToPlayerSize: true,
355 autoStartLoad: false, 355 autoStartLoad: false,
356 liveSyncDurationCount: 7, 356 liveSyncDurationCount: 5,
357 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() 357 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
358 } 358 }
359 } 359 }
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 995e8c277..9b07ef5e6 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -556,9 +556,9 @@ export class PeerTubeEmbed {
556 556
557 Object.assign(options, { 557 Object.assign(options, {
558 p2pMediaLoader: { 558 p2pMediaLoader: {
559 playlistUrl: hlsPlaylist.playlistUrl, 559 playlistUrl: 'http://localhost:9000/live/toto/master.m3u8',
560 segmentsSha256Url: hlsPlaylist.segmentsSha256Url, 560 segmentsSha256Url: hlsPlaylist.segmentsSha256Url,
561 redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), 561 redundancyBaseUrls: [],
562 trackerAnnounce: videoInfo.trackerUrls, 562 trackerAnnounce: videoInfo.trackerUrls,
563 videoFiles: hlsPlaylist.files 563 videoFiles: hlsPlaylist.files
564 } as P2PMediaLoaderOptions 564 } as P2PMediaLoaderOptions