diff options
41 files changed, 468 insertions, 159 deletions
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts index 25d75aed2..602362fe9 100644 --- a/client/src/app/+admin/system/jobs/jobs.component.ts +++ b/client/src/app/+admin/system/jobs/jobs.component.ts | |||
@@ -32,6 +32,7 @@ export class JobsComponent extends RestTable implements OnInit { | |||
32 | 'video-import', | 32 | 'video-import', |
33 | 'videos-views', | 33 | 'videos-views', |
34 | 'activitypub-refresher', | 34 | 'activitypub-refresher', |
35 | 'video-live-ending', | ||
35 | 'video-redundancy' | 36 | 'video-redundancy' |
36 | ] | 37 | ] |
37 | 38 | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts index bcbea7fad..ad7497f45 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts | |||
@@ -86,7 +86,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit { | |||
86 | } | 86 | } |
87 | 87 | ||
88 | private savePreferencesImpl () { | 88 | private savePreferencesImpl () { |
89 | this.userNotificationService.updateNotificationSettings(this.user, this.user.notificationSettings) | 89 | this.userNotificationService.updateNotificationSettings(this.user.notificationSettings) |
90 | .subscribe( | 90 | .subscribe( |
91 | () => { | 91 | () => { |
92 | this.notifier.success($localize`Preferences saved`, undefined, 2000) | 92 | this.notifier.success($localize`Preferences saved`, undefined, 2000) |
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 c444dd8d3..0802e906d 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,7 +195,7 @@ | |||
195 | </ng-template> | 195 | </ng-template> |
196 | </ng-container> | 196 | </ng-container> |
197 | 197 | ||
198 | <ng-container ngbNavItem *ngIf="videoLive"> | 198 | <ng-container ngbNavItem *ngIf="liveVideo"> |
199 | <a ngbNavLink i18n>Live settings</a> | 199 | <a ngbNavLink i18n>Live settings</a> |
200 | 200 | ||
201 | <ng-template ngbNavContent> | 201 | <ng-template ngbNavContent> |
@@ -203,13 +203,13 @@ | |||
203 | <div class="col-md-12"> | 203 | <div class="col-md-12"> |
204 | 204 | ||
205 | <div class="form-group"> | 205 | <div class="form-group"> |
206 | <label for="videoLiveRTMPUrl" i18n>Live RTMP Url</label> | 206 | <label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label> |
207 | <my-input-readonly-copy id="videoLiveRTMPUrl" [value]="videoLive.rtmpUrl"></my-input-readonly-copy> | 207 | <my-input-readonly-copy id="liveVideoRTMPUrl" [value]="liveVideo.rtmpUrl"></my-input-readonly-copy> |
208 | </div> | 208 | </div> |
209 | 209 | ||
210 | <div class="form-group"> | 210 | <div class="form-group"> |
211 | <label for="videoLiveStreamKey" i18n>Live stream key</label> | 211 | <label for="liveVideoStreamKey" i18n>Live stream key</label> |
212 | <my-input-readonly-copy id="videoLiveStreamKey" [value]="videoLive.streamKey"></my-input-readonly-copy> | 212 | <my-input-readonly-copy id="liveVideoStreamKey" [value]="liveVideo.streamKey"></my-input-readonly-copy> |
213 | </div> | 213 | </div> |
214 | </div> | 214 | </div> |
215 | </div> | 215 | </div> |
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 bee65184b..304bf7ed0 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,7 +20,7 @@ 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, VideoLive, VideoPrivacy } from '@shared/models' | 23 | import { ServerConfig, VideoConstant, LiveVideo, 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' |
@@ -42,7 +42,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
42 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] | 42 | @Input() videoCaptions: (VideoCaptionEdit & { captionPath?: string })[] = [] |
43 | @Input() waitTranscodingEnabled = true | 43 | @Input() waitTranscodingEnabled = true |
44 | @Input() type: VideoEditType | 44 | @Input() type: VideoEditType |
45 | @Input() videoLive: VideoLive | 45 | @Input() liveVideo: LiveVideo |
46 | 46 | ||
47 | @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent | 47 | @ViewChild('videoCaptionAddModal', { static: true }) videoCaptionAddModal: VideoCaptionAddModalComponent |
48 | 48 | ||
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 index 6997f5388..8fae4044a 100644 --- 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 | |||
@@ -31,7 +31,7 @@ | |||
31 | <form [hidden]="!isInUpdateForm" novalidate [formGroup]="form"> | 31 | <form [hidden]="!isInUpdateForm" novalidate [formGroup]="form"> |
32 | <my-video-edit | 32 | <my-video-edit |
33 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" | 33 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" [schedulePublicationPossible]="false" |
34 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [videoLive]="videoLive" | 34 | [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [liveVideo]="liveVideo" |
35 | type="go-live" | 35 | type="go-live" |
36 | ></my-video-edit> | 36 | ></my-video-edit> |
37 | 37 | ||
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 index 64fd4c4d4..0a9efc693 100644 --- 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 | |||
@@ -4,9 +4,9 @@ import { Router } from '@angular/router' | |||
4 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' |
5 | import { scrollToTop } from '@app/helpers' | 5 | import { scrollToTop } from '@app/helpers' |
6 | import { FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormValidatorService } from '@app/shared/shared-forms' |
7 | import { VideoCaptionService, VideoEdit, VideoService, VideoLiveService } from '@app/shared/shared-main' | 7 | import { LiveVideoService, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 8 | import { LoadingBarService } from '@ngx-loading-bar/core' |
9 | import { VideoCreate, VideoLive, VideoPrivacy } from '@shared/models' | 9 | import { LiveVideo, VideoCreate, VideoPrivacy } from '@shared/models' |
10 | import { VideoSend } from './video-send' | 10 | import { VideoSend } from './video-send' |
11 | 11 | ||
12 | @Component({ | 12 | @Component({ |
@@ -23,7 +23,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon | |||
23 | 23 | ||
24 | isInUpdateForm = false | 24 | isInUpdateForm = false |
25 | 25 | ||
26 | videoLive: VideoLive | 26 | liveVideo: LiveVideo |
27 | videoId: number | 27 | videoId: number |
28 | videoUUID: string | 28 | videoUUID: string |
29 | error: string | 29 | error: string |
@@ -38,7 +38,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon | |||
38 | protected serverService: ServerService, | 38 | protected serverService: ServerService, |
39 | protected videoService: VideoService, | 39 | protected videoService: VideoService, |
40 | protected videoCaptionService: VideoCaptionService, | 40 | protected videoCaptionService: VideoCaptionService, |
41 | private videoLiveService: VideoLiveService, | 41 | private liveVideoService: LiveVideoService, |
42 | private router: Router | 42 | private router: Router |
43 | ) { | 43 | ) { |
44 | super() | 44 | super() |
@@ -69,7 +69,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon | |||
69 | const toPatch = Object.assign({}, video, { privacy: this.firstStepPrivacyId }) | 69 | const toPatch = Object.assign({}, video, { privacy: this.firstStepPrivacyId }) |
70 | this.form.patchValue(toPatch) | 70 | this.form.patchValue(toPatch) |
71 | 71 | ||
72 | this.videoLiveService.goLive(video).subscribe( | 72 | this.liveVideoService.goLive(video).subscribe( |
73 | res => { | 73 | res => { |
74 | this.videoId = res.video.id | 74 | this.videoId = res.video.id |
75 | this.videoUUID = res.video.uuid | 75 | this.videoUUID = res.video.uuid |
@@ -114,10 +114,10 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon | |||
114 | } | 114 | } |
115 | 115 | ||
116 | private fetchVideoLive () { | 116 | private fetchVideoLive () { |
117 | this.videoLiveService.getVideoLive(this.videoId) | 117 | this.liveVideoService.getVideoLive(this.videoId) |
118 | .subscribe( | 118 | .subscribe( |
119 | videoLive => { | 119 | liveVideo => { |
120 | this.videoLive = videoLive | 120 | this.liveVideo = liveVideo |
121 | }, | 121 | }, |
122 | 122 | ||
123 | err => { | 123 | err => { |
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 5f50ddc74..f290fd136 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.html +++ b/client/src/app/+videos/+video-edit/video-update.component.html | |||
@@ -11,7 +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 | [liveVideo]="liveVideo" |
15 | ></my-video-edit> | 15 | ></my-video-edit> |
16 | 16 | ||
17 | <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 c0f46acd2..ec1305a33 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,7 @@ 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, VideoLive } from '@shared/models' | 8 | import { LiveVideo, VideoPrivacy } from '@shared/models' |
9 | import { hydrateFormFromVideo } from './shared/video-edit-utils' | 9 | import { hydrateFormFromVideo } from './shared/video-edit-utils' |
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
@@ -17,7 +17,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
17 | video: VideoEdit | 17 | video: VideoEdit |
18 | userVideoChannels: SelectChannelItem[] = [] | 18 | userVideoChannels: SelectChannelItem[] = [] |
19 | videoCaptions: VideoCaptionEdit[] = [] | 19 | videoCaptions: VideoCaptionEdit[] = [] |
20 | videoLive: VideoLive | 20 | liveVideo: LiveVideo |
21 | 21 | ||
22 | isUpdatingVideo = false | 22 | isUpdatingVideo = false |
23 | schedulePublicationPossible = false | 23 | schedulePublicationPossible = false |
@@ -42,11 +42,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
42 | 42 | ||
43 | this.route.data | 43 | this.route.data |
44 | .pipe(map(data => data.videoData)) | 44 | .pipe(map(data => data.videoData)) |
45 | .subscribe(({ video, videoChannels, videoCaptions, videoLive }) => { | 45 | .subscribe(({ video, videoChannels, videoCaptions, liveVideo }) => { |
46 | this.video = new VideoEdit(video) | 46 | this.video = new VideoEdit(video) |
47 | this.userVideoChannels = videoChannels | 47 | this.userVideoChannels = videoChannels |
48 | this.videoCaptions = videoCaptions | 48 | this.videoCaptions = videoCaptions |
49 | this.videoLive = videoLive | 49 | this.liveVideo = liveVideo |
50 | 50 | ||
51 | this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE | 51 | this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE |
52 | 52 | ||
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 3a82324c3..b7ec22dd5 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts | |||
@@ -2,13 +2,13 @@ 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, VideoDetails, VideoLiveService, VideoService } from '@app/shared/shared-main' | 5 | import { VideoCaptionService, VideoChannelService, VideoDetails, LiveVideoService, 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 liveVideoService: LiveVideoService, |
12 | private videoChannelService: VideoChannelService, | 12 | private videoChannelService: VideoChannelService, |
13 | private videoCaptionService: VideoCaptionService | 13 | private videoCaptionService: VideoCaptionService |
14 | ) { | 14 | ) { |
@@ -49,7 +49,7 @@ export class VideoUpdateResolver implements Resolve<any> { | |||
49 | ), | 49 | ), |
50 | 50 | ||
51 | video.isLive | 51 | video.isLive |
52 | ? this.videoLiveService.getVideoLive(video.id) | 52 | ? this.liveVideoService.getVideoLive(video.id) |
53 | : of(undefined) | 53 | : of(undefined) |
54 | ] | 54 | ] |
55 | } | 55 | } |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html index 0d1768aa9..13242a2bc 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.html +++ b/client/src/app/+videos/+video-watch/video-watch.component.html | |||
@@ -29,6 +29,14 @@ | |||
29 | This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}. | 29 | This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}. |
30 | </div> | 30 | </div> |
31 | 31 | ||
32 | <div i18n class="col-md-12 alert alert-info" *ngIf="isWaitingForLive()"> | ||
33 | This live has not started yet. | ||
34 | </div> | ||
35 | |||
36 | <div i18n class="col-md-12 alert alert-info" *ngIf="isLiveEnded()"> | ||
37 | This live is finished. | ||
38 | </div> | ||
39 | |||
32 | <div class="col-md-12 alert alert-danger" *ngIf="video?.blacklisted"> | 40 | <div class="col-md-12 alert alert-danger" *ngIf="video?.blacklisted"> |
33 | <div class="blocked-label" i18n>This video is blocked.</div> | 41 | <div class="blocked-label" i18n>This video is blocked.</div> |
34 | {{ video.blockedReason }} | 42 | {{ video.blockedReason }} |
@@ -113,7 +121,7 @@ | |||
113 | </div> | 121 | </div> |
114 | </div> | 122 | </div> |
115 | 123 | ||
116 | <ng-container *ngIf="!isUserLoggedIn()"> | 124 | <ng-container *ngIf="!isUserLoggedIn() && !isLive()"> |
117 | <button | 125 | <button |
118 | *ngIf="isVideoDownloadable()" class="action-button action-button-save" | 126 | *ngIf="isVideoDownloadable()" class="action-button action-button-save" |
119 | (click)="showDownloadModal()" (keydown.enter)="showDownloadModal()" | 127 | (click)="showDownloadModal()" (keydown.enter)="showDownloadModal()" |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss index b2bd04cf3..4bf5ff808 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.scss +++ b/client/src/app/+videos/+video-watch/video-watch.component.scss | |||
@@ -50,6 +50,8 @@ $video-info-margin-left: 44px; | |||
50 | } | 50 | } |
51 | 51 | ||
52 | #video-wrapper { | 52 | #video-wrapper { |
53 | $video-height: 66vh; | ||
54 | |||
53 | background-color: #000; | 55 | background-color: #000; |
54 | display: flex; | 56 | display: flex; |
55 | justify-content: center; | 57 | justify-content: center; |
@@ -58,6 +60,7 @@ $video-info-margin-left: 44px; | |||
58 | display: flex; | 60 | display: flex; |
59 | justify-content: center; | 61 | justify-content: center; |
60 | flex-grow: 1; | 62 | flex-grow: 1; |
63 | height: $video-height; | ||
61 | } | 64 | } |
62 | 65 | ||
63 | .remote-server-down { | 66 | .remote-server-down { |
@@ -84,7 +87,7 @@ $video-info-margin-left: 44px; | |||
84 | ::ng-deep .video-js { | 87 | ::ng-deep .video-js { |
85 | width: 100%; | 88 | width: 100%; |
86 | max-width: getPlayerWidth(66vh); | 89 | max-width: getPlayerWidth(66vh); |
87 | height: 66vh; | 90 | height: $video-height; |
88 | 91 | ||
89 | // VideoJS create an inner video player | 92 | // VideoJS create an inner video player |
90 | video { | 93 | video { |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index fde32dc74..e4edb42fb 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -4,7 +4,17 @@ import { catchError } from 'rxjs/operators' | |||
4 | import { PlatformLocation } from '@angular/common' | 4 | import { PlatformLocation } from '@angular/common' |
5 | import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' | 5 | import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' |
6 | import { ActivatedRoute, Router } from '@angular/router' | 6 | import { ActivatedRoute, Router } from '@angular/router' |
7 | import { AuthService, AuthUser, ConfirmService, MarkdownService, Notifier, RestExtractor, ServerService, UserService } from '@app/core' | 7 | import { |
8 | AuthService, | ||
9 | AuthUser, | ||
10 | ConfirmService, | ||
11 | MarkdownService, | ||
12 | Notifier, | ||
13 | PeerTubeSocket, | ||
14 | RestExtractor, | ||
15 | ServerService, | ||
16 | UserService | ||
17 | } from '@app/core' | ||
8 | import { HooksService } from '@app/core/plugins/hooks.service' | 18 | import { HooksService } from '@app/core/plugins/hooks.service' |
9 | import { RedirectService } from '@app/core/routing/redirect.service' | 19 | import { RedirectService } from '@app/core/routing/redirect.service' |
10 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 20 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' |
@@ -30,6 +40,8 @@ import { environment } from '../../../environments/environment' | |||
30 | import { VideoSupportComponent } from './modal/video-support.component' | 40 | import { VideoSupportComponent } from './modal/video-support.component' |
31 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 41 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
32 | 42 | ||
43 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | ||
44 | |||
33 | @Component({ | 45 | @Component({ |
34 | selector: 'my-video-watch', | 46 | selector: 'my-video-watch', |
35 | templateUrl: './video-watch.component.html', | 47 | templateUrl: './video-watch.component.html', |
@@ -76,6 +88,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
76 | private paramsSub: Subscription | 88 | private paramsSub: Subscription |
77 | private queryParamsSub: Subscription | 89 | private queryParamsSub: Subscription |
78 | private configSub: Subscription | 90 | private configSub: Subscription |
91 | private liveVideosSub: Subscription | ||
79 | 92 | ||
80 | private serverConfig: ServerConfig | 93 | private serverConfig: ServerConfig |
81 | 94 | ||
@@ -99,6 +112,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
99 | private videoCaptionService: VideoCaptionService, | 112 | private videoCaptionService: VideoCaptionService, |
100 | private hotkeysService: HotkeysService, | 113 | private hotkeysService: HotkeysService, |
101 | private hooks: HooksService, | 114 | private hooks: HooksService, |
115 | private peertubeSocket: PeerTubeSocket, | ||
102 | private location: PlatformLocation, | 116 | private location: PlatformLocation, |
103 | @Inject(LOCALE_ID) private localeId: string | 117 | @Inject(LOCALE_ID) private localeId: string |
104 | ) { | 118 | ) { |
@@ -165,6 +179,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
165 | if (this.paramsSub) this.paramsSub.unsubscribe() | 179 | if (this.paramsSub) this.paramsSub.unsubscribe() |
166 | if (this.queryParamsSub) this.queryParamsSub.unsubscribe() | 180 | if (this.queryParamsSub) this.queryParamsSub.unsubscribe() |
167 | if (this.configSub) this.configSub.unsubscribe() | 181 | if (this.configSub) this.configSub.unsubscribe() |
182 | if (this.liveVideosSub) this.liveVideosSub.unsubscribe() | ||
168 | 183 | ||
169 | // Unbind hotkeys | 184 | // Unbind hotkeys |
170 | this.hotkeysService.remove(this.hotkeys) | 185 | this.hotkeysService.remove(this.hotkeys) |
@@ -306,6 +321,18 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
306 | return this.video && this.video.scheduledUpdate !== undefined | 321 | return this.video && this.video.scheduledUpdate !== undefined |
307 | } | 322 | } |
308 | 323 | ||
324 | isLive () { | ||
325 | return !!(this.video?.isLive) | ||
326 | } | ||
327 | |||
328 | isWaitingForLive () { | ||
329 | return this.video?.state.id === VideoState.WAITING_FOR_LIVE | ||
330 | } | ||
331 | |||
332 | isLiveEnded () { | ||
333 | return this.video?.state.id === VideoState.LIVE_ENDED | ||
334 | } | ||
335 | |||
309 | isVideoBlur (video: Video) { | 336 | isVideoBlur (video: Video) { |
310 | return video.isVideoNSFWForUser(this.user, this.serverConfig) | 337 | return video.isVideoNSFWForUser(this.user, this.serverConfig) |
311 | } | 338 | } |
@@ -470,8 +497,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
470 | private async onVideoFetched ( | 497 | private async onVideoFetched ( |
471 | video: VideoDetails, | 498 | video: VideoDetails, |
472 | videoCaptions: VideoCaption[], | 499 | videoCaptions: VideoCaption[], |
473 | urlOptions: CustomizationOptions & { playerMode: PlayerMode } | 500 | urlOptions: URLOptions |
474 | ) { | 501 | ) { |
502 | this.subscribeToLiveEventsIfNeeded(this.video, video) | ||
503 | |||
475 | this.video = video | 504 | this.video = video |
476 | this.videoCaptions = videoCaptions | 505 | this.videoCaptions = videoCaptions |
477 | 506 | ||
@@ -489,6 +518,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
489 | if (res === false) return this.location.back() | 518 | if (res === false) return this.location.back() |
490 | } | 519 | } |
491 | 520 | ||
521 | const videoState = this.video.state.id | ||
522 | if (videoState === VideoState.LIVE_ENDED || videoState === VideoState.WAITING_FOR_LIVE) return | ||
523 | |||
492 | // Flush old player if needed | 524 | // Flush old player if needed |
493 | this.flushPlayer() | 525 | this.flushPlayer() |
494 | 526 | ||
@@ -794,6 +826,29 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
794 | return !this.player.paused() | 826 | return !this.player.paused() |
795 | } | 827 | } |
796 | 828 | ||
829 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | ||
830 | if (!this.liveVideosSub) { | ||
831 | this.liveVideosSub = this.peertubeSocket.getLiveVideosObservable() | ||
832 | .subscribe(({ payload }) => { | ||
833 | if (payload.state !== VideoState.PUBLISHED || this.video.state.id !== VideoState.WAITING_FOR_LIVE) return | ||
834 | |||
835 | const videoUUID = this.video.uuid | ||
836 | |||
837 | // Reset to refetch the video | ||
838 | this.video = undefined | ||
839 | this.loadVideo(videoUUID) | ||
840 | }) | ||
841 | } | ||
842 | |||
843 | if (oldVideo && oldVideo.id !== newVideo.id) { | ||
844 | await this.peertubeSocket.unsubscribeLiveVideos(oldVideo.id) | ||
845 | } | ||
846 | |||
847 | if (!newVideo.isLive) return | ||
848 | |||
849 | await this.peertubeSocket.subscribeToLiveVideosSocket(newVideo.id) | ||
850 | } | ||
851 | |||
797 | private initHotkeys () { | 852 | private initHotkeys () { |
798 | this.hotkeys = [ | 853 | this.hotkeys = [ |
799 | // These hotkeys are managed by the player | 854 | // These hotkeys are managed by the player |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 22896e2e9..6c0a2245d 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -4,7 +4,7 @@ import { ToastModule } from 'primeng/toast' | |||
4 | import { CommonModule } from '@angular/common' | 4 | import { CommonModule } from '@angular/common' |
5 | import { NgModule, Optional, SkipSelf } from '@angular/core' | 5 | import { NgModule, Optional, SkipSelf } from '@angular/core' |
6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' | 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' |
7 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' | 7 | import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service' |
8 | import { HooksService } from '@app/core/plugins/hooks.service' | 8 | import { HooksService } from '@app/core/plugins/hooks.service' |
9 | import { PluginService } from '@app/core/plugins/plugin.service' | 9 | import { PluginService } from '@app/core/plugins/plugin.service' |
10 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' | 10 | import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' |
@@ -84,7 +84,7 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra | |||
84 | RedirectService, | 84 | RedirectService, |
85 | Notifier, | 85 | Notifier, |
86 | MessageService, | 86 | MessageService, |
87 | UserNotificationSocket, | 87 | PeerTubeSocket, |
88 | ServerConfigResolver, | 88 | ServerConfigResolver, |
89 | CanDeactivateGuard | 89 | CanDeactivateGuard |
90 | ] | 90 | ] |
diff --git a/client/src/app/core/notification/index.ts b/client/src/app/core/notification/index.ts index 3e8d9ea65..cd9634c8e 100644 --- a/client/src/app/core/notification/index.ts +++ b/client/src/app/core/notification/index.ts | |||
@@ -1,2 +1,2 @@ | |||
1 | export * from './notifier.service' | 1 | export * from './notifier.service' |
2 | export * from './user-notification-socket.service' | 2 | export * from './peertube-socket.service' |
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts new file mode 100644 index 000000000..8668c44a8 --- /dev/null +++ b/client/src/app/core/notification/peertube-socket.service.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { Injectable, NgZone } from '@angular/core' | ||
3 | import { LiveVideoEventPayload, LiveVideoEventType, UserNotification as UserNotificationServer } from '@shared/models' | ||
4 | import { environment } from '../../../environments/environment' | ||
5 | import { AuthService } from '../auth' | ||
6 | |||
7 | export type NotificationEvent = 'new' | 'read' | 'read-all' | ||
8 | |||
9 | @Injectable() | ||
10 | export class PeerTubeSocket { | ||
11 | private io: typeof import ('socket.io-client') | ||
12 | |||
13 | private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>() | ||
14 | private liveVideosSubject = new Subject<{ type: LiveVideoEventType, payload: LiveVideoEventPayload }>() | ||
15 | |||
16 | private notificationSocket: SocketIOClient.Socket | ||
17 | private liveVideosSocket: SocketIOClient.Socket | ||
18 | |||
19 | constructor ( | ||
20 | private auth: AuthService, | ||
21 | private ngZone: NgZone | ||
22 | ) {} | ||
23 | |||
24 | async getMyNotificationsSocket () { | ||
25 | await this.initNotificationSocket() | ||
26 | |||
27 | return this.notificationSubject.asObservable() | ||
28 | } | ||
29 | |||
30 | getLiveVideosObservable () { | ||
31 | return this.liveVideosSubject.asObservable() | ||
32 | } | ||
33 | |||
34 | async subscribeToLiveVideosSocket (videoId: number) { | ||
35 | await this.initLiveVideosSocket() | ||
36 | |||
37 | this.liveVideosSocket.emit('subscribe', { videoId }) | ||
38 | } | ||
39 | |||
40 | async unsubscribeLiveVideos (videoId: number) { | ||
41 | if (!this.liveVideosSocket) return | ||
42 | |||
43 | this.liveVideosSocket.emit('unsubscribe', { videoId }) | ||
44 | } | ||
45 | |||
46 | dispatchNotificationEvent (type: NotificationEvent, notification?: UserNotificationServer) { | ||
47 | this.notificationSubject.next({ type, notification }) | ||
48 | } | ||
49 | |||
50 | private async initNotificationSocket () { | ||
51 | if (this.notificationSocket) return | ||
52 | |||
53 | await this.importIOIfNeeded() | ||
54 | |||
55 | this.ngZone.runOutsideAngular(() => { | ||
56 | this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', { | ||
57 | query: { accessToken: this.auth.getAccessToken() } | ||
58 | }) | ||
59 | |||
60 | this.notificationSocket.on('new-notification', (n: UserNotificationServer) => this.dispatchNotificationEvent('new', n)) | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | private async initLiveVideosSocket () { | ||
65 | if (this.liveVideosSocket) return | ||
66 | |||
67 | await this.importIOIfNeeded() | ||
68 | |||
69 | this.ngZone.runOutsideAngular(() => { | ||
70 | this.liveVideosSocket = this.io(environment.apiUrl + '/live-videos') | ||
71 | |||
72 | const type: LiveVideoEventType = 'state-change' | ||
73 | this.liveVideosSocket.on(type, (payload: LiveVideoEventPayload) => this.dispatchLiveVideoEvent(type, payload)) | ||
74 | }) | ||
75 | } | ||
76 | |||
77 | private async importIOIfNeeded () { | ||
78 | if (this.io) return | ||
79 | |||
80 | this.io = (await import('socket.io-client') as any).default | ||
81 | } | ||
82 | |||
83 | private dispatchLiveVideoEvent (type: LiveVideoEventType, payload: LiveVideoEventPayload) { | ||
84 | this.liveVideosSubject.next({ type, payload }) | ||
85 | } | ||
86 | } | ||
diff --git a/client/src/app/core/notification/user-notification-socket.service.ts b/client/src/app/core/notification/user-notification-socket.service.ts deleted file mode 100644 index 37f0bc32c..000000000 --- a/client/src/app/core/notification/user-notification-socket.service.ts +++ /dev/null | |||
@@ -1,44 +0,0 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { Injectable, NgZone } from '@angular/core' | ||
3 | import { UserNotification as UserNotificationServer } from '@shared/models' | ||
4 | import { environment } from '../../../environments/environment' | ||
5 | import { AuthService } from '../auth' | ||
6 | |||
7 | export type NotificationEvent = 'new' | 'read' | 'read-all' | ||
8 | |||
9 | @Injectable() | ||
10 | export class UserNotificationSocket { | ||
11 | private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>() | ||
12 | |||
13 | private socket: SocketIOClient.Socket | ||
14 | |||
15 | constructor ( | ||
16 | private auth: AuthService, | ||
17 | private ngZone: NgZone | ||
18 | ) {} | ||
19 | |||
20 | dispatch (type: NotificationEvent, notification?: UserNotificationServer) { | ||
21 | this.notificationSubject.next({ type, notification }) | ||
22 | } | ||
23 | |||
24 | async getMyNotificationsSocket () { | ||
25 | await this.initSocket() | ||
26 | |||
27 | return this.notificationSubject.asObservable() | ||
28 | } | ||
29 | |||
30 | private async initSocket () { | ||
31 | if (this.socket) return | ||
32 | |||
33 | // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function | ||
34 | const io: typeof import ('socket.io-client') = (await import('socket.io-client') as any).default | ||
35 | |||
36 | this.ngZone.runOutsideAngular(() => { | ||
37 | this.socket = io(environment.apiUrl + '/user-notifications', { | ||
38 | query: { accessToken: this.auth.getAccessToken() } | ||
39 | }) | ||
40 | |||
41 | this.socket.on('new-notification', (n: UserNotificationServer) => this.dispatch('new', n)) | ||
42 | }) | ||
43 | } | ||
44 | } | ||
diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/avatar-notification.component.ts index 8b9955069..ed3ffc2d8 100644 --- a/client/src/app/menu/avatar-notification.component.ts +++ b/client/src/app/menu/avatar-notification.component.ts | |||
@@ -2,7 +2,7 @@ import { Subject, Subscription } from 'rxjs' | |||
2 | import { filter } from 'rxjs/operators' | 2 | import { filter } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' | 3 | import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' |
4 | import { NavigationEnd, Router } from '@angular/router' | 4 | import { NavigationEnd, Router } from '@angular/router' |
5 | import { Notifier, User, UserNotificationSocket } from '@app/core' | 5 | import { Notifier, User, PeerTubeSocket } from '@app/core' |
6 | import { UserNotificationService } from '@app/shared/shared-main' | 6 | import { UserNotificationService } from '@app/shared/shared-main' |
7 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' |
8 | 8 | ||
@@ -27,7 +27,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy { | |||
27 | 27 | ||
28 | constructor ( | 28 | constructor ( |
29 | private userNotificationService: UserNotificationService, | 29 | private userNotificationService: UserNotificationService, |
30 | private userNotificationSocket: UserNotificationSocket, | 30 | private peertubeSocket: PeerTubeSocket, |
31 | private notifier: Notifier, | 31 | private notifier: Notifier, |
32 | private router: Router | 32 | private router: Router |
33 | ) { | 33 | ) { |
@@ -75,7 +75,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy { | |||
75 | } | 75 | } |
76 | 76 | ||
77 | private async subscribeToNotifications () { | 77 | private async subscribeToNotifications () { |
78 | const obs = await this.userNotificationSocket.getMyNotificationsSocket() | 78 | const obs = await this.peertubeSocket.getMyNotificationsSocket() |
79 | 79 | ||
80 | this.notificationSub = obs.subscribe(data => { | 80 | this.notificationSub = obs.subscribe(data => { |
81 | if (data.type === 'new') return this.unreadNotifications++ | 81 | if (data.type === 'new') return this.unreadNotifications++ |
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 bca67b193..0580872f4 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, VideoLiveService } from './video' | 26 | import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService, LiveVideoService } 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,7 +142,7 @@ import { VideoChannelService } from './video-channel' | |||
142 | RedundancyService, | 142 | RedundancyService, |
143 | VideoImportService, | 143 | VideoImportService, |
144 | VideoOwnershipService, | 144 | VideoOwnershipService, |
145 | VideoLiveService, | 145 | LiveVideoService, |
146 | VideoService, | 146 | VideoService, |
147 | 147 | ||
148 | VideoCaptionService, | 148 | VideoCaptionService, |
diff --git a/client/src/app/shared/shared-main/users/user-notification.service.ts b/client/src/app/shared/shared-main/users/user-notification.service.ts index 7b9dc34be..9014b48a8 100644 --- a/client/src/app/shared/shared-main/users/user-notification.service.ts +++ b/client/src/app/shared/shared-main/users/user-notification.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { catchError, map, tap } from 'rxjs/operators' | 1 | import { catchError, map, tap } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket, AuthService } from '@app/core' | 4 | import { ComponentPaginationLight, RestExtractor, RestService, User, PeerTubeSocket, AuthService } from '@app/core' |
5 | import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' | 5 | import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' |
6 | import { environment } from '../../../../environments/environment' | 6 | import { environment } from '../../../../environments/environment' |
7 | import { UserNotification } from './user-notification.model' | 7 | import { UserNotification } from './user-notification.model' |
@@ -17,7 +17,7 @@ export class UserNotificationService { | |||
17 | private auth: AuthService, | 17 | private auth: AuthService, |
18 | private restExtractor: RestExtractor, | 18 | private restExtractor: RestExtractor, |
19 | private restService: RestService, | 19 | private restService: RestService, |
20 | private userNotificationSocket: UserNotificationSocket | 20 | private peertubeSocket: PeerTubeSocket |
21 | ) {} | 21 | ) {} |
22 | 22 | ||
23 | listMyNotifications (parameters: { | 23 | listMyNotifications (parameters: { |
@@ -57,7 +57,7 @@ export class UserNotificationService { | |||
57 | return this.authHttp.post(url, body, { headers }) | 57 | return this.authHttp.post(url, body, { headers }) |
58 | .pipe( | 58 | .pipe( |
59 | map(this.restExtractor.extractDataBool), | 59 | map(this.restExtractor.extractDataBool), |
60 | tap(() => this.userNotificationSocket.dispatch('read')), | 60 | tap(() => this.peertubeSocket.dispatchNotificationEvent('read')), |
61 | catchError(res => this.restExtractor.handleError(res)) | 61 | catchError(res => this.restExtractor.handleError(res)) |
62 | ) | 62 | ) |
63 | } | 63 | } |
@@ -69,12 +69,12 @@ export class UserNotificationService { | |||
69 | return this.authHttp.post(url, {}, { headers }) | 69 | return this.authHttp.post(url, {}, { headers }) |
70 | .pipe( | 70 | .pipe( |
71 | map(this.restExtractor.extractDataBool), | 71 | map(this.restExtractor.extractDataBool), |
72 | tap(() => this.userNotificationSocket.dispatch('read-all')), | 72 | tap(() => this.peertubeSocket.dispatchNotificationEvent('read-all')), |
73 | catchError(res => this.restExtractor.handleError(res)) | 73 | catchError(res => this.restExtractor.handleError(res)) |
74 | ) | 74 | ) |
75 | } | 75 | } |
76 | 76 | ||
77 | updateNotificationSettings (user: User, settings: UserNotificationSetting) { | 77 | updateNotificationSettings (settings: UserNotificationSetting) { |
78 | const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS | 78 | const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS |
79 | 79 | ||
80 | return this.authHttp.put(url, settings) | 80 | return this.authHttp.put(url, settings) |
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts index 121635a30..f69089517 100644 --- a/client/src/app/shared/shared-main/video/index.ts +++ b/client/src/app/shared/shared-main/video/index.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | export * from './live-video.service' | ||
1 | export * from './redundancy.service' | 2 | export * from './redundancy.service' |
2 | export * from './video-details.model' | 3 | export * from './video-details.model' |
3 | export * from './video-edit.model' | 4 | export * from './video-edit.model' |
4 | export * from './video-import.service' | 5 | export * from './video-import.service' |
5 | export * from './video-live.service' | ||
6 | export * from './video-ownership.service' | 6 | export * from './video-ownership.service' |
7 | export * from './video.model' | 7 | export * from './video.model' |
8 | export * from './video.service' | 8 | export * from './video.service' |
diff --git a/client/src/app/shared/shared-main/video/video-live.service.ts b/client/src/app/shared/shared-main/video/live-video.service.ts index 12daff756..2cd1c66a5 100644 --- a/client/src/app/shared/shared-main/video/video-live.service.ts +++ b/client/src/app/shared/shared-main/video/live-video.service.ts | |||
@@ -2,11 +2,11 @@ import { catchError } from 'rxjs/operators' | |||
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { RestExtractor } from '@app/core' | 4 | import { RestExtractor } from '@app/core' |
5 | import { VideoCreate, VideoLive } from '@shared/models' | 5 | import { VideoCreate, LiveVideo } from '@shared/models' |
6 | import { environment } from '../../../../environments/environment' | 6 | import { environment } from '../../../../environments/environment' |
7 | 7 | ||
8 | @Injectable() | 8 | @Injectable() |
9 | export class VideoLiveService { | 9 | export class LiveVideoService { |
10 | static BASE_VIDEO_LIVE_URL = environment.apiUrl + '/api/v1/videos/live/' | 10 | static BASE_VIDEO_LIVE_URL = environment.apiUrl + '/api/v1/videos/live/' |
11 | 11 | ||
12 | constructor ( | 12 | constructor ( |
@@ -16,13 +16,13 @@ export class VideoLiveService { | |||
16 | 16 | ||
17 | goLive (video: VideoCreate) { | 17 | goLive (video: VideoCreate) { |
18 | return this.authHttp | 18 | return this.authHttp |
19 | .post<{ video: { id: number, uuid: string } }>(VideoLiveService.BASE_VIDEO_LIVE_URL, video) | 19 | .post<{ video: { id: number, uuid: string } }>(LiveVideoService.BASE_VIDEO_LIVE_URL, video) |
20 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 20 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
21 | } | 21 | } |
22 | 22 | ||
23 | getVideoLive (videoId: number | string) { | 23 | getVideoLive (videoId: number | string) { |
24 | return this.authHttp | 24 | return this.authHttp |
25 | .get<VideoLive>(VideoLiveService.BASE_VIDEO_LIVE_URL + videoId) | 25 | .get<LiveVideo>(LiveVideoService.BASE_VIDEO_LIVE_URL + videoId) |
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 26 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
27 | } | 27 | } |
28 | } | 28 | } |
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.html b/client/src/app/shared/shared-share-modal/video-share.component.html index 3222dc5a6..80b4e446a 100644 --- a/client/src/app/shared/shared-share-modal/video-share.component.html +++ b/client/src/app/shared/shared-share-modal/video-share.component.html | |||
@@ -107,7 +107,7 @@ | |||
107 | 107 | ||
108 | <div class="filters"> | 108 | <div class="filters"> |
109 | <div> | 109 | <div> |
110 | <div class="form-group start-at"> | 110 | <div class="form-group start-at" *ngIf="!video.isLive"> |
111 | <my-peertube-checkbox | 111 | <my-peertube-checkbox |
112 | inputName="startAt" [(ngModel)]="customizations.startAtCheckbox" | 112 | inputName="startAt" [(ngModel)]="customizations.startAtCheckbox" |
113 | i18n-labelText labelText="Start at" | 113 | i18n-labelText labelText="Start at" |
@@ -138,7 +138,7 @@ | |||
138 | 138 | ||
139 | <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed"> | 139 | <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed"> |
140 | <div> | 140 | <div> |
141 | <div class="form-group stop-at"> | 141 | <div class="form-group stop-at" *ngIf="!video.isLive"> |
142 | <my-peertube-checkbox | 142 | <my-peertube-checkbox |
143 | inputName="stopAt" [(ngModel)]="customizations.stopAtCheckbox" | 143 | inputName="stopAt" [(ngModel)]="customizations.stopAtCheckbox" |
144 | i18n-labelText labelText="Stop at" | 144 | i18n-labelText labelText="Stop at" |
@@ -167,7 +167,7 @@ | |||
167 | ></my-peertube-checkbox> | 167 | ></my-peertube-checkbox> |
168 | </div> | 168 | </div> |
169 | 169 | ||
170 | <div class="form-group"> | 170 | <div class="form-group" *ngIf="!video.isLive"> |
171 | <my-peertube-checkbox | 171 | <my-peertube-checkbox |
172 | inputName="loop" [(ngModel)]="customizations.loop" | 172 | inputName="loop" [(ngModel)]="customizations.loop" |
173 | i18n-labelText labelText="Loop" | 173 | i18n-labelText labelText="Loop" |
diff --git a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts index 39358e08b..4ef17bfe3 100644 --- a/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts | |||
@@ -146,7 +146,10 @@ export class VideoActionsDropdownComponent implements OnChanges { | |||
146 | } | 146 | } |
147 | 147 | ||
148 | isVideoDownloadable () { | 148 | isVideoDownloadable () { |
149 | return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled | 149 | return this.video && |
150 | this.video.isLive !== true && | ||
151 | this.video instanceof VideoDetails && | ||
152 | this.video.downloadEnabled | ||
150 | } | 153 | } |
151 | 154 | ||
152 | canVideoBeDuplicated () { | 155 | canVideoBeDuplicated () { |
diff --git a/config/default.yaml b/config/default.yaml index 7efaeb5a2..d0937bfc8 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -246,9 +246,20 @@ transcoding: | |||
246 | live: | 246 | live: |
247 | enabled: false | 247 | enabled: false |
248 | 248 | ||
249 | # Limit lives duration | ||
250 | # Set null to disable duration limit | ||
251 | max_duration: 5 hours | ||
252 | |||
253 | # Allow your users to save a replay of their live | ||
254 | # PeerTube will transcode segments in a video file | ||
255 | # If the user daily/total quota is reached, PeerTube will stop the live | ||
256 | # /!\ transcoding.enabled (and not live.transcoding.enabled) has to be true to create a replay | ||
257 | allow_replay: true | ||
258 | |||
249 | rtmp: | 259 | rtmp: |
250 | port: 1935 | 260 | port: 1935 |
251 | 261 | ||
262 | # Allow to transcode the live streaming in multiple live resolutions | ||
252 | transcoding: | 263 | transcoding: |
253 | enabled: false | 264 | enabled: false |
254 | threads: 2 | 265 | threads: 2 |
diff --git a/config/test.yaml b/config/test.yaml index b9279b5e6..865ed5400 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -89,7 +89,7 @@ live: | |||
89 | port: 1935 | 89 | port: 1935 |
90 | 90 | ||
91 | transcoding: | 91 | transcoding: |
92 | enabled: false | 92 | enabled: true |
93 | threads: 2 | 93 | threads: 2 |
94 | 94 | ||
95 | resolutions: | 95 | resolutions: |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 606eeba2d..82d04a94e 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -139,7 +139,8 @@ const JOB_ATTEMPTS: { [id in JobType]: number } = { | |||
139 | 'email': 5, | 139 | 'email': 5, |
140 | 'videos-views': 1, | 140 | 'videos-views': 1, |
141 | 'activitypub-refresher': 1, | 141 | 'activitypub-refresher': 1, |
142 | 'video-redundancy': 1 | 142 | 'video-redundancy': 1, |
143 | 'video-live-ending': 1 | ||
143 | } | 144 | } |
144 | const JOB_CONCURRENCY: { [id in JobType]: number } = { | 145 | const JOB_CONCURRENCY: { [id in JobType]: number } = { |
145 | 'activitypub-http-broadcast': 1, | 146 | 'activitypub-http-broadcast': 1, |
@@ -152,7 +153,8 @@ const JOB_CONCURRENCY: { [id in JobType]: number } = { | |||
152 | 'email': 5, | 153 | 'email': 5, |
153 | 'videos-views': 1, | 154 | 'videos-views': 1, |
154 | 'activitypub-refresher': 1, | 155 | 'activitypub-refresher': 1, |
155 | 'video-redundancy': 1 | 156 | 'video-redundancy': 1, |
157 | 'video-live-ending': 1 | ||
156 | } | 158 | } |
157 | const JOB_TTL: { [id in JobType]: number } = { | 159 | const JOB_TTL: { [id in JobType]: number } = { |
158 | 'activitypub-http-broadcast': 60000 * 10, // 10 minutes | 160 | 'activitypub-http-broadcast': 60000 * 10, // 10 minutes |
@@ -165,7 +167,8 @@ const JOB_TTL: { [id in JobType]: number } = { | |||
165 | 'email': 60000 * 10, // 10 minutes | 167 | 'email': 60000 * 10, // 10 minutes |
166 | 'videos-views': undefined, // Unlimited | 168 | 'videos-views': undefined, // Unlimited |
167 | 'activitypub-refresher': 60000 * 10, // 10 minutes | 169 | 'activitypub-refresher': 60000 * 10, // 10 minutes |
168 | 'video-redundancy': 1000 * 3600 * 3 // 3 hours | 170 | 'video-redundancy': 1000 * 3600 * 3, // 3 hours |
171 | 'video-live-ending': 1000 * 60 * 10 // 10 minutes | ||
169 | } | 172 | } |
170 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { | 173 | const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { |
171 | 'videos-views': { | 174 | 'videos-views': { |
@@ -605,6 +608,7 @@ const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls') | |||
605 | 608 | ||
606 | const VIDEO_LIVE = { | 609 | const VIDEO_LIVE = { |
607 | EXTENSION: '.ts', | 610 | EXTENSION: '.ts', |
611 | CLEANUP_DELAY: 1000 * 60 * 5, // 5 mintues | ||
608 | RTMP: { | 612 | RTMP: { |
609 | CHUNK_SIZE: 60000, | 613 | CHUNK_SIZE: 60000, |
610 | GOP_CACHE: true, | 614 | GOP_CACHE: true, |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 049e06cff..ab23ff507 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -66,6 +66,7 @@ import { FilteredModelAttributes } from '../../types/sequelize' | |||
66 | import { ActorFollowScoreCache } from '../files-cache' | 66 | import { ActorFollowScoreCache } from '../files-cache' |
67 | import { JobQueue } from '../job-queue' | 67 | import { JobQueue } from '../job-queue' |
68 | import { Notifier } from '../notifier' | 68 | import { Notifier } from '../notifier' |
69 | import { PeerTubeSocket } from '../peertube-socket' | ||
69 | import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail' | 70 | import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail' |
70 | import { setVideoTags } from '../video' | 71 | import { setVideoTags } from '../video' |
71 | import { autoBlacklistVideoIfNeeded } from '../video-blacklist' | 72 | import { autoBlacklistVideoIfNeeded } from '../video-blacklist' |
@@ -348,6 +349,7 @@ async function updateVideoFromAP (options: { | |||
348 | video.privacy = videoData.privacy | 349 | video.privacy = videoData.privacy |
349 | video.channelId = videoData.channelId | 350 | video.channelId = videoData.channelId |
350 | video.views = videoData.views | 351 | video.views = videoData.views |
352 | video.isLive = videoData.isLive | ||
351 | 353 | ||
352 | const videoUpdated = await video.save(sequelizeOptions) as MVideoFullLight | 354 | const videoUpdated = await video.save(sequelizeOptions) as MVideoFullLight |
353 | 355 | ||
@@ -434,6 +436,7 @@ async function updateVideoFromAP (options: { | |||
434 | }) | 436 | }) |
435 | 437 | ||
436 | if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users? | 438 | if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users? |
439 | if (videoUpdated.isLive) PeerTubeSocket.Instance.sendVideoLiveNewState(video) | ||
437 | 440 | ||
438 | logger.info('Remote video with uuid %s updated', videoObject.uuid) | 441 | logger.info('Remote video with uuid %s updated', videoObject.uuid) |
439 | 442 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts new file mode 100644 index 000000000..1a58a9f7e --- /dev/null +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import * as Bull from 'bull' | ||
2 | import { readdir, remove } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { getHLSDirectory } from '@server/lib/video-paths' | ||
5 | import { VideoModel } from '@server/models/video/video' | ||
6 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
7 | import { VideoLiveEndingPayload } from '@shared/models' | ||
8 | import { logger } from '../../../helpers/logger' | ||
9 | |||
10 | async function processVideoLiveEnding (job: Bull.Job) { | ||
11 | const payload = job.data as VideoLiveEndingPayload | ||
12 | |||
13 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) | ||
14 | if (!video) { | ||
15 | logger.warn('Video live %d does not exist anymore. Cannot cleanup.', payload.videoId) | ||
16 | return | ||
17 | } | ||
18 | |||
19 | const streamingPlaylist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) | ||
20 | const hlsDirectory = getHLSDirectory(video, false) | ||
21 | |||
22 | const files = await readdir(hlsDirectory) | ||
23 | |||
24 | for (const filename of files) { | ||
25 | if ( | ||
26 | filename.endsWith('.ts') || | ||
27 | filename.endsWith('.m3u8') || | ||
28 | filename.endsWith('.mpd') || | ||
29 | filename.endsWith('.m4s') || | ||
30 | filename.endsWith('.tmp') | ||
31 | ) { | ||
32 | const p = join(hlsDirectory, filename) | ||
33 | |||
34 | remove(p) | ||
35 | .catch(err => logger.error('Cannot remove %s.', p, { err })) | ||
36 | } | ||
37 | } | ||
38 | |||
39 | streamingPlaylist.destroy() | ||
40 | .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) | ||
41 | } | ||
42 | |||
43 | // --------------------------------------------------------------------------- | ||
44 | |||
45 | export { | ||
46 | processVideoLiveEnding | ||
47 | } | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 14e181835..8d97434ac 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -10,6 +10,7 @@ import { | |||
10 | RefreshPayload, | 10 | RefreshPayload, |
11 | VideoFileImportPayload, | 11 | VideoFileImportPayload, |
12 | VideoImportPayload, | 12 | VideoImportPayload, |
13 | VideoLiveEndingPayload, | ||
13 | VideoRedundancyPayload, | 14 | VideoRedundancyPayload, |
14 | VideoTranscodingPayload | 15 | VideoTranscodingPayload |
15 | } from '../../../shared/models' | 16 | } from '../../../shared/models' |
@@ -27,6 +28,7 @@ import { processVideosViews } from './handlers/video-views' | |||
27 | import { refreshAPObject } from './handlers/activitypub-refresher' | 28 | import { refreshAPObject } from './handlers/activitypub-refresher' |
28 | import { processVideoFileImport } from './handlers/video-file-import' | 29 | import { processVideoFileImport } from './handlers/video-file-import' |
29 | import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' | 30 | import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy' |
31 | import { processVideoLiveEnding } from './handlers/video-live-ending' | ||
30 | 32 | ||
31 | type CreateJobArgument = | 33 | type CreateJobArgument = |
32 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | | 34 | { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | |
@@ -39,8 +41,13 @@ type CreateJobArgument = | |||
39 | { type: 'video-import', payload: VideoImportPayload } | | 41 | { type: 'video-import', payload: VideoImportPayload } | |
40 | { type: 'activitypub-refresher', payload: RefreshPayload } | | 42 | { type: 'activitypub-refresher', payload: RefreshPayload } | |
41 | { type: 'videos-views', payload: {} } | | 43 | { type: 'videos-views', payload: {} } | |
44 | { type: 'video-live-ending', payload: VideoLiveEndingPayload } | | ||
42 | { type: 'video-redundancy', payload: VideoRedundancyPayload } | 45 | { type: 'video-redundancy', payload: VideoRedundancyPayload } |
43 | 46 | ||
47 | type CreateJobOptions = { | ||
48 | delay?: number | ||
49 | } | ||
50 | |||
44 | const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = { | 51 | const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = { |
45 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, | 52 | 'activitypub-http-broadcast': processActivityPubHttpBroadcast, |
46 | 'activitypub-http-unicast': processActivityPubHttpUnicast, | 53 | 'activitypub-http-unicast': processActivityPubHttpUnicast, |
@@ -52,6 +59,7 @@ const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = { | |||
52 | 'video-import': processVideoImport, | 59 | 'video-import': processVideoImport, |
53 | 'videos-views': processVideosViews, | 60 | 'videos-views': processVideosViews, |
54 | 'activitypub-refresher': refreshAPObject, | 61 | 'activitypub-refresher': refreshAPObject, |
62 | 'video-live-ending': processVideoLiveEnding, | ||
55 | 'video-redundancy': processVideoRedundancy | 63 | 'video-redundancy': processVideoRedundancy |
56 | } | 64 | } |
57 | 65 | ||
@@ -66,7 +74,8 @@ const jobTypes: JobType[] = [ | |||
66 | 'video-import', | 74 | 'video-import', |
67 | 'videos-views', | 75 | 'videos-views', |
68 | 'activitypub-refresher', | 76 | 'activitypub-refresher', |
69 | 'video-redundancy' | 77 | 'video-redundancy', |
78 | 'video-live-ending' | ||
70 | ] | 79 | ] |
71 | 80 | ||
72 | class JobQueue { | 81 | class JobQueue { |
@@ -122,12 +131,12 @@ class JobQueue { | |||
122 | } | 131 | } |
123 | } | 132 | } |
124 | 133 | ||
125 | createJob (obj: CreateJobArgument): void { | 134 | createJob (obj: CreateJobArgument, options: CreateJobOptions = {}): void { |
126 | this.createJobWithPromise(obj) | 135 | this.createJobWithPromise(obj, options) |
127 | .catch(err => logger.error('Cannot create job.', { err, obj })) | 136 | .catch(err => logger.error('Cannot create job.', { err, obj })) |
128 | } | 137 | } |
129 | 138 | ||
130 | createJobWithPromise (obj: CreateJobArgument) { | 139 | createJobWithPromise (obj: CreateJobArgument, options: CreateJobOptions = {}) { |
131 | const queue = this.queues[obj.type] | 140 | const queue = this.queues[obj.type] |
132 | if (queue === undefined) { | 141 | if (queue === undefined) { |
133 | logger.error('Unknown queue %s: cannot create job.', obj.type) | 142 | logger.error('Unknown queue %s: cannot create job.', obj.type) |
@@ -137,7 +146,8 @@ class JobQueue { | |||
137 | const jobArgs: Bull.JobOptions = { | 146 | const jobArgs: Bull.JobOptions = { |
138 | backoff: { delay: 60 * 1000, type: 'exponential' }, | 147 | backoff: { delay: 60 * 1000, type: 'exponential' }, |
139 | attempts: JOB_ATTEMPTS[obj.type], | 148 | attempts: JOB_ATTEMPTS[obj.type], |
140 | timeout: JOB_TTL[obj.type] | 149 | timeout: JOB_TTL[obj.type], |
150 | delay: options.delay | ||
141 | } | 151 | } |
142 | 152 | ||
143 | return queue.add(obj.payload, jobArgs) | 153 | return queue.add(obj.payload, jobArgs) |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index f602bfb6d..41176d197 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -2,18 +2,22 @@ | |||
2 | import { AsyncQueue, queue } from 'async' | 2 | import { AsyncQueue, queue } from 'async' |
3 | import * as chokidar from 'chokidar' | 3 | import * as chokidar from 'chokidar' |
4 | import { FfmpegCommand } from 'fluent-ffmpeg' | 4 | import { FfmpegCommand } from 'fluent-ffmpeg' |
5 | import { ensureDir, readdir, remove } from 'fs-extra' | 5 | import { ensureDir } from 'fs-extra' |
6 | import { basename, join } from 'path' | 6 | import { basename } from 'path' |
7 | import { computeResolutionsToTranscode, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' | 7 | import { computeResolutionsToTranscode, runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' |
8 | import { logger } from '@server/helpers/logger' | 8 | import { logger } from '@server/helpers/logger' |
9 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 9 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
10 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' | 10 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, WEBSERVER } from '@server/initializers/constants' |
11 | import { VideoModel } from '@server/models/video/video' | ||
11 | import { VideoFileModel } from '@server/models/video/video-file' | 12 | import { VideoFileModel } from '@server/models/video/video-file' |
12 | import { VideoLiveModel } from '@server/models/video/video-live' | 13 | import { VideoLiveModel } from '@server/models/video/video-live' |
13 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
14 | import { MStreamingPlaylist, MVideo, MVideoLiveVideo } from '@server/types/models' | 15 | import { MStreamingPlaylist, MVideoLiveVideo } from '@server/types/models' |
15 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' | 16 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' |
17 | import { federateVideoIfNeeded } from './activitypub/videos' | ||
16 | import { buildSha256Segment } from './hls' | 18 | import { buildSha256Segment } from './hls' |
19 | import { JobQueue } from './job-queue' | ||
20 | import { PeerTubeSocket } from './peertube-socket' | ||
17 | import { getHLSDirectory } from './video-paths' | 21 | import { getHLSDirectory } from './video-paths' |
18 | 22 | ||
19 | const NodeRtmpServer = require('node-media-server/node_rtmp_server') | 23 | const NodeRtmpServer = require('node-media-server/node_rtmp_server') |
@@ -47,6 +51,7 @@ class LiveManager { | |||
47 | private static instance: LiveManager | 51 | private static instance: LiveManager |
48 | 52 | ||
49 | private readonly transSessions = new Map<string, FfmpegCommand>() | 53 | private readonly transSessions = new Map<string, FfmpegCommand>() |
54 | private readonly videoSessions = new Map<number, string>() | ||
50 | private readonly segmentsSha256 = new Map<string, Map<string, string>>() | 55 | private readonly segmentsSha256 = new Map<string, Map<string, string>>() |
51 | 56 | ||
52 | private segmentsSha256Queue: AsyncQueue<SegmentSha256QueueParam> | 57 | private segmentsSha256Queue: AsyncQueue<SegmentSha256QueueParam> |
@@ -56,7 +61,8 @@ class LiveManager { | |||
56 | } | 61 | } |
57 | 62 | ||
58 | init () { | 63 | init () { |
59 | this.getContext().nodeEvent.on('postPublish', (sessionId: string, streamPath: string) => { | 64 | const events = this.getContext().nodeEvent |
65 | events.on('postPublish', (sessionId: string, streamPath: string) => { | ||
60 | logger.debug('RTMP received stream', { id: sessionId, streamPath }) | 66 | logger.debug('RTMP received stream', { id: sessionId, streamPath }) |
61 | 67 | ||
62 | const splittedPath = streamPath.split('/') | 68 | const splittedPath = streamPath.split('/') |
@@ -69,7 +75,7 @@ class LiveManager { | |||
69 | .catch(err => logger.error('Cannot handle sessions.', { err })) | 75 | .catch(err => logger.error('Cannot handle sessions.', { err })) |
70 | }) | 76 | }) |
71 | 77 | ||
72 | this.getContext().nodeEvent.on('donePublish', sessionId => { | 78 | events.on('donePublish', sessionId => { |
73 | this.abortSession(sessionId) | 79 | this.abortSession(sessionId) |
74 | }) | 80 | }) |
75 | 81 | ||
@@ -115,6 +121,16 @@ class LiveManager { | |||
115 | return this.segmentsSha256.get(videoUUID) | 121 | return this.segmentsSha256.get(videoUUID) |
116 | } | 122 | } |
117 | 123 | ||
124 | stopSessionOf (videoId: number) { | ||
125 | const sessionId = this.videoSessions.get(videoId) | ||
126 | if (!sessionId) return | ||
127 | |||
128 | this.abortSession(sessionId) | ||
129 | |||
130 | this.onEndTransmuxing(videoId) | ||
131 | .catch(err => logger.error('Cannot end transmuxing of video %d.', videoId, { err })) | ||
132 | } | ||
133 | |||
118 | private getContext () { | 134 | private getContext () { |
119 | return context | 135 | return context |
120 | } | 136 | } |
@@ -135,6 +151,13 @@ class LiveManager { | |||
135 | } | 151 | } |
136 | 152 | ||
137 | const video = videoLive.Video | 153 | const video = videoLive.Video |
154 | if (video.isBlacklisted()) { | ||
155 | logger.warn('Video is blacklisted. Refusing stream %s.', streamKey) | ||
156 | return this.abortSession(sessionId) | ||
157 | } | ||
158 | |||
159 | this.videoSessions.set(video.id, sessionId) | ||
160 | |||
138 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) | 161 | const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) |
139 | 162 | ||
140 | const session = this.getContext().sessions.get(sessionId) | 163 | const session = this.getContext().sessions.get(sessionId) |
@@ -154,11 +177,6 @@ class LiveManager { | |||
154 | type: VideoStreamingPlaylistType.HLS | 177 | type: VideoStreamingPlaylistType.HLS |
155 | }, { returning: true }) as [ MStreamingPlaylist, boolean ] | 178 | }, { returning: true }) as [ MStreamingPlaylist, boolean ] |
156 | 179 | ||
157 | video.state = VideoState.PUBLISHED | ||
158 | await video.save() | ||
159 | |||
160 | // FIXME: federation? | ||
161 | |||
162 | return this.runMuxing({ | 180 | return this.runMuxing({ |
163 | sessionId, | 181 | sessionId, |
164 | videoLive, | 182 | videoLive, |
@@ -207,11 +225,46 @@ class LiveManager { | |||
207 | 225 | ||
208 | this.transSessions.set(sessionId, ffmpegExec) | 226 | this.transSessions.set(sessionId, ffmpegExec) |
209 | 227 | ||
228 | const videoUUID = videoLive.Video.uuid | ||
229 | const tsWatcher = chokidar.watch(outPath + '/*.ts') | ||
230 | |||
231 | const updateHandler = segmentPath => { | ||
232 | this.segmentsSha256Queue.push({ operation: 'update', segmentPath, videoUUID }) | ||
233 | } | ||
234 | |||
235 | const deleteHandler = segmentPath => this.segmentsSha256Queue.push({ operation: 'delete', segmentPath, videoUUID }) | ||
236 | |||
237 | tsWatcher.on('add', p => updateHandler(p)) | ||
238 | tsWatcher.on('change', p => updateHandler(p)) | ||
239 | tsWatcher.on('unlink', p => deleteHandler(p)) | ||
240 | |||
241 | const masterWatcher = chokidar.watch(outPath + '/master.m3u8') | ||
242 | masterWatcher.on('add', async () => { | ||
243 | try { | ||
244 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoLive.videoId) | ||
245 | |||
246 | video.state = VideoState.PUBLISHED | ||
247 | await video.save() | ||
248 | videoLive.Video = video | ||
249 | |||
250 | await federateVideoIfNeeded(video, false) | ||
251 | |||
252 | PeerTubeSocket.Instance.sendVideoLiveNewState(video) | ||
253 | } catch (err) { | ||
254 | logger.error('Cannot federate video %d.', videoLive.videoId, { err }) | ||
255 | } finally { | ||
256 | masterWatcher.close() | ||
257 | .catch(err => logger.error('Cannot close master watcher of %s.', outPath, { err })) | ||
258 | } | ||
259 | }) | ||
260 | |||
210 | const onFFmpegEnded = () => { | 261 | const onFFmpegEnded = () => { |
211 | watcher.close() | 262 | logger.info('RTMP transmuxing for video %s ended. Scheduling cleanup', streamPath) |
212 | .catch(err => logger.error('Cannot close watcher of %s.', outPath, { err })) | ||
213 | 263 | ||
214 | this.onEndTransmuxing(videoLive.Video, playlist, streamPath, outPath) | 264 | Promise.all([ tsWatcher.close(), masterWatcher.close() ]) |
265 | .catch(err => logger.error('Cannot close watchers of %s.', outPath, { err })) | ||
266 | |||
267 | this.onEndTransmuxing(videoLive.Video.id) | ||
215 | .catch(err => logger.error('Error in closed transmuxing.', { err })) | 268 | .catch(err => logger.error('Error in closed transmuxing.', { err })) |
216 | } | 269 | } |
217 | 270 | ||
@@ -225,44 +278,30 @@ class LiveManager { | |||
225 | }) | 278 | }) |
226 | 279 | ||
227 | ffmpegExec.on('end', () => onFFmpegEnded()) | 280 | ffmpegExec.on('end', () => onFFmpegEnded()) |
228 | |||
229 | const videoUUID = videoLive.Video.uuid | ||
230 | const watcher = chokidar.watch(outPath + '/*.ts') | ||
231 | |||
232 | const updateHandler = segmentPath => this.segmentsSha256Queue.push({ operation: 'update', segmentPath, videoUUID }) | ||
233 | const deleteHandler = segmentPath => this.segmentsSha256Queue.push({ operation: 'delete', segmentPath, videoUUID }) | ||
234 | |||
235 | watcher.on('add', p => updateHandler(p)) | ||
236 | watcher.on('change', p => updateHandler(p)) | ||
237 | watcher.on('unlink', p => deleteHandler(p)) | ||
238 | } | 281 | } |
239 | 282 | ||
240 | private async onEndTransmuxing (video: MVideo, playlist: MStreamingPlaylist, streamPath: string, outPath: string) { | 283 | private async onEndTransmuxing (videoId: number) { |
241 | logger.info('RTMP transmuxing for %s ended.', streamPath) | 284 | try { |
285 | const fullVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | ||
286 | if (!fullVideo) return | ||
242 | 287 | ||
243 | const files = await readdir(outPath) | 288 | JobQueue.Instance.createJob({ |
289 | type: 'video-live-ending', | ||
290 | payload: { | ||
291 | videoId: fullVideo.id | ||
292 | } | ||
293 | }, { delay: VIDEO_LIVE.CLEANUP_DELAY }) | ||
244 | 294 | ||
245 | for (const filename of files) { | 295 | // FIXME: use end |
246 | if ( | 296 | fullVideo.state = VideoState.WAITING_FOR_LIVE |
247 | filename.endsWith('.ts') || | 297 | await fullVideo.save() |
248 | filename.endsWith('.m3u8') || | ||
249 | filename.endsWith('.mpd') || | ||
250 | filename.endsWith('.m4s') || | ||
251 | filename.endsWith('.tmp') | ||
252 | ) { | ||
253 | const p = join(outPath, filename) | ||
254 | 298 | ||
255 | remove(p) | 299 | PeerTubeSocket.Instance.sendVideoLiveNewState(fullVideo) |
256 | .catch(err => logger.error('Cannot remove %s.', p, { err })) | ||
257 | } | ||
258 | } | ||
259 | 300 | ||
260 | playlist.destroy() | 301 | await federateVideoIfNeeded(fullVideo, false) |
261 | .catch(err => logger.error('Cannot remove live streaming playlist.', { err })) | 302 | } catch (err) { |
262 | 303 | logger.error('Cannot save/federate new video state of live streaming.', { err }) | |
263 | video.state = VideoState.LIVE_ENDED | 304 | } |
264 | video.save() | ||
265 | .catch(err => logger.error('Cannot save new video state of live streaming.', { err })) | ||
266 | } | 305 | } |
267 | 306 | ||
268 | private async addSegmentSha (options: SegmentSha256QueueParam) { | 307 | private async addSegmentSha (options: SegmentSha256QueueParam) { |
diff --git a/server/lib/peertube-socket.ts b/server/lib/peertube-socket.ts index 2e4b15b38..c918a8685 100644 --- a/server/lib/peertube-socket.ts +++ b/server/lib/peertube-socket.ts | |||
@@ -1,14 +1,18 @@ | |||
1 | import * as SocketIO from 'socket.io' | 1 | import { Socket } from 'dgram' |
2 | import { authenticateSocket } from '../middlewares' | ||
3 | import { logger } from '../helpers/logger' | ||
4 | import { Server } from 'http' | 2 | import { Server } from 'http' |
3 | import * as SocketIO from 'socket.io' | ||
4 | import { MVideo } from '@server/types/models' | ||
5 | import { UserNotificationModelForApi } from '@server/types/models/user' | 5 | import { UserNotificationModelForApi } from '@server/types/models/user' |
6 | import { LiveVideoEventPayload, LiveVideoEventType } from '@shared/models' | ||
7 | import { logger } from '../helpers/logger' | ||
8 | import { authenticateSocket } from '../middlewares' | ||
6 | 9 | ||
7 | class PeerTubeSocket { | 10 | class PeerTubeSocket { |
8 | 11 | ||
9 | private static instance: PeerTubeSocket | 12 | private static instance: PeerTubeSocket |
10 | 13 | ||
11 | private userNotificationSockets: { [ userId: number ]: SocketIO.Socket[] } = {} | 14 | private userNotificationSockets: { [ userId: number ]: SocketIO.Socket[] } = {} |
15 | private liveVideosNamespace: SocketIO.Namespace | ||
12 | 16 | ||
13 | private constructor () {} | 17 | private constructor () {} |
14 | 18 | ||
@@ -32,19 +36,37 @@ class PeerTubeSocket { | |||
32 | this.userNotificationSockets[userId] = this.userNotificationSockets[userId].filter(s => s !== socket) | 36 | this.userNotificationSockets[userId] = this.userNotificationSockets[userId].filter(s => s !== socket) |
33 | }) | 37 | }) |
34 | }) | 38 | }) |
39 | |||
40 | this.liveVideosNamespace = io.of('/live-videos') | ||
41 | .on('connection', socket => { | ||
42 | socket.on('subscribe', ({ videoId }) => socket.join(videoId)) | ||
43 | socket.on('unsubscribe', ({ videoId }) => socket.leave(videoId)) | ||
44 | }) | ||
35 | } | 45 | } |
36 | 46 | ||
37 | sendNotification (userId: number, notification: UserNotificationModelForApi) { | 47 | sendNotification (userId: number, notification: UserNotificationModelForApi) { |
38 | const sockets = this.userNotificationSockets[userId] | 48 | const sockets = this.userNotificationSockets[userId] |
39 | |||
40 | if (!sockets) return | 49 | if (!sockets) return |
41 | 50 | ||
51 | logger.debug('Sending user notification to user %d.', userId) | ||
52 | |||
42 | const notificationMessage = notification.toFormattedJSON() | 53 | const notificationMessage = notification.toFormattedJSON() |
43 | for (const socket of sockets) { | 54 | for (const socket of sockets) { |
44 | socket.emit('new-notification', notificationMessage) | 55 | socket.emit('new-notification', notificationMessage) |
45 | } | 56 | } |
46 | } | 57 | } |
47 | 58 | ||
59 | sendVideoLiveNewState (video: MVideo) { | ||
60 | const data: LiveVideoEventPayload = { state: video.state } | ||
61 | const type: LiveVideoEventType = 'state-change' | ||
62 | |||
63 | logger.debug('Sending video live new state notification of %s.', video.url) | ||
64 | |||
65 | this.liveVideosNamespace | ||
66 | .in(video.id) | ||
67 | .emit(type, data) | ||
68 | } | ||
69 | |||
48 | static get Instance () { | 70 | static get Instance () { |
49 | return this.instance || (this.instance = new this()) | 71 | return this.instance || (this.instance = new this()) |
50 | } | 72 | } |
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts index bdbcffda6..f6c66b6dd 100644 --- a/server/lib/video-blacklist.ts +++ b/server/lib/video-blacklist.ts | |||
@@ -17,6 +17,7 @@ import { sendDeleteVideo } from './activitypub/send' | |||
17 | import { federateVideoIfNeeded } from './activitypub/videos' | 17 | import { federateVideoIfNeeded } from './activitypub/videos' |
18 | import { Notifier } from './notifier' | 18 | import { Notifier } from './notifier' |
19 | import { Hooks } from './plugins/hooks' | 19 | import { Hooks } from './plugins/hooks' |
20 | import { LiveManager } from './live-manager' | ||
20 | 21 | ||
21 | async function autoBlacklistVideoIfNeeded (parameters: { | 22 | async function autoBlacklistVideoIfNeeded (parameters: { |
22 | video: MVideoWithBlacklistLight | 23 | video: MVideoWithBlacklistLight |
@@ -73,6 +74,10 @@ async function blacklistVideo (videoInstance: MVideoAccountLight, options: Video | |||
73 | await sendDeleteVideo(videoInstance, undefined) | 74 | await sendDeleteVideo(videoInstance, undefined) |
74 | } | 75 | } |
75 | 76 | ||
77 | if (videoInstance.isLive) { | ||
78 | LiveManager.Instance.stopSessionOf(videoInstance.id) | ||
79 | } | ||
80 | |||
76 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) | 81 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) |
77 | } | 82 | } |
78 | 83 | ||
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts index 6929b9688..8608bc84c 100644 --- a/server/models/video/video-live.ts +++ b/server/models/video/video-live.ts | |||
@@ -1,14 +1,21 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { WEBSERVER } from '@server/initializers/constants' | 2 | import { WEBSERVER } from '@server/initializers/constants' |
3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' | 3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' |
4 | import { VideoLive } from '@shared/models/videos/video-live.model' | 4 | import { LiveVideo, VideoState } from '@shared/models' |
5 | import { VideoModel } from './video' | 5 | import { VideoModel } from './video' |
6 | import { VideoBlacklistModel } from './video-blacklist' | ||
6 | 7 | ||
7 | @DefaultScope(() => ({ | 8 | @DefaultScope(() => ({ |
8 | include: [ | 9 | include: [ |
9 | { | 10 | { |
10 | model: VideoModel, | 11 | model: VideoModel, |
11 | required: true | 12 | required: true, |
13 | include: [ | ||
14 | { | ||
15 | model: VideoBlacklistModel, | ||
16 | required: false | ||
17 | } | ||
18 | ] | ||
12 | } | 19 | } |
13 | ] | 20 | ] |
14 | })) | 21 | })) |
@@ -49,7 +56,22 @@ export class VideoLiveModel extends Model<VideoLiveModel> { | |||
49 | const query = { | 56 | const query = { |
50 | where: { | 57 | where: { |
51 | streamKey | 58 | streamKey |
52 | } | 59 | }, |
60 | include: [ | ||
61 | { | ||
62 | model: VideoModel.unscoped(), | ||
63 | required: true, | ||
64 | where: { | ||
65 | state: VideoState.WAITING_FOR_LIVE | ||
66 | }, | ||
67 | include: [ | ||
68 | { | ||
69 | model: VideoBlacklistModel.unscoped(), | ||
70 | required: false | ||
71 | } | ||
72 | ] | ||
73 | } | ||
74 | ] | ||
53 | } | 75 | } |
54 | 76 | ||
55 | return VideoLiveModel.findOne<MVideoLiveVideo>(query) | 77 | return VideoLiveModel.findOne<MVideoLiveVideo>(query) |
@@ -65,7 +87,7 @@ export class VideoLiveModel extends Model<VideoLiveModel> { | |||
65 | return VideoLiveModel.findOne<MVideoLive>(query) | 87 | return VideoLiveModel.findOne<MVideoLive>(query) |
66 | } | 88 | } |
67 | 89 | ||
68 | toFormattedJSON (): VideoLive { | 90 | toFormattedJSON (): LiveVideo { |
69 | return { | 91 | return { |
70 | rtmpUrl: WEBSERVER.RTMP_URL, | 92 | rtmpUrl: WEBSERVER.RTMP_URL, |
71 | streamKey: this.streamKey | 93 | streamKey: this.streamKey |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index b8dc7c450..73bd89844 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -153,6 +153,17 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod | |||
153 | return VideoStreamingPlaylistModel.findByPk(id, options) | 153 | return VideoStreamingPlaylistModel.findByPk(id, options) |
154 | } | 154 | } |
155 | 155 | ||
156 | static loadHLSPlaylistByVideo (videoId: number) { | ||
157 | const options = { | ||
158 | where: { | ||
159 | type: VideoStreamingPlaylistType.HLS, | ||
160 | videoId | ||
161 | } | ||
162 | } | ||
163 | |||
164 | return VideoStreamingPlaylistModel.findOne(options) | ||
165 | } | ||
166 | |||
156 | static getHlsPlaylistFilename (resolution: number) { | 167 | static getHlsPlaylistFilename (resolution: number) { |
157 | return resolution + '.m3u8' | 168 | return resolution + '.m3u8' |
158 | } | 169 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index a3e3b6cfe..8493ab802 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -127,6 +127,7 @@ import { VideoShareModel } from './video-share' | |||
127 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 127 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
128 | import { VideoTagModel } from './video-tag' | 128 | import { VideoTagModel } from './video-tag' |
129 | import { VideoViewModel } from './video-view' | 129 | import { VideoViewModel } from './video-view' |
130 | import { LiveManager } from '@server/lib/live-manager' | ||
130 | 131 | ||
131 | export enum ScopeNames { | 132 | export enum ScopeNames { |
132 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 133 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
@@ -800,6 +801,13 @@ export class VideoModel extends Model<VideoModel> { | |||
800 | } | 801 | } |
801 | 802 | ||
802 | @BeforeDestroy | 803 | @BeforeDestroy |
804 | static stopLiveIfNeeded (instance: VideoModel) { | ||
805 | if (!instance.isLive) return | ||
806 | |||
807 | return LiveManager.Instance.stopSessionOf(instance.id) | ||
808 | } | ||
809 | |||
810 | @BeforeDestroy | ||
803 | static invalidateCache (instance: VideoModel) { | 811 | static invalidateCache (instance: VideoModel) { |
804 | ModelCache.Instance.invalidateCache('video', instance.id) | 812 | ModelCache.Instance.invalidateCache('video', instance.id) |
805 | } | 813 | } |
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index c643e6017..346b25607 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts | |||
@@ -16,6 +16,7 @@ export type JobType = | |||
16 | | 'videos-views' | 16 | | 'videos-views' |
17 | | 'activitypub-refresher' | 17 | | 'activitypub-refresher' |
18 | | 'video-redundancy' | 18 | | 'video-redundancy' |
19 | | 'video-live-ending' | ||
19 | 20 | ||
20 | export interface Job { | 21 | export interface Job { |
21 | id: number | 22 | id: number |
@@ -126,3 +127,7 @@ export type VideoTranscodingPayload = | |||
126 | | NewResolutionTranscodingPayload | 127 | | NewResolutionTranscodingPayload |
127 | | OptimizeTranscodingPayload | 128 | | OptimizeTranscodingPayload |
128 | | MergeAudioTranscodingPayload | 129 | | MergeAudioTranscodingPayload |
130 | |||
131 | export interface VideoLiveEndingPayload { | ||
132 | videoId: number | ||
133 | } | ||
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 2a032a456..abf144f23 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './blacklist' | 1 | export * from './blacklist' |
2 | export * from './caption' | 2 | export * from './caption' |
3 | export * from './channel' | 3 | export * from './channel' |
4 | export * from './live' | ||
4 | export * from './import' | 5 | export * from './import' |
5 | export * from './playlist' | 6 | export * from './playlist' |
6 | export * from './rate' | 7 | export * from './rate' |
@@ -19,7 +20,7 @@ export * from './video-create.model' | |||
19 | export * from './video-file-metadata' | 20 | export * from './video-file-metadata' |
20 | export * from './video-file.model' | 21 | export * from './video-file.model' |
21 | 22 | ||
22 | export * from './video-live.model' | 23 | export * from './live/live-video.model' |
23 | 24 | ||
24 | export * from './video-privacy.enum' | 25 | export * from './video-privacy.enum' |
25 | export * from './video-query.type' | 26 | export * from './video-query.type' |
diff --git a/shared/models/videos/live/index.ts b/shared/models/videos/live/index.ts new file mode 100644 index 000000000..4f331738b --- /dev/null +++ b/shared/models/videos/live/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './live-video-event-payload.model' | ||
2 | export * from './live-video-event.type' | ||
3 | export * from './live-video.model' | ||
diff --git a/shared/models/videos/live/live-video-event-payload.model.ts b/shared/models/videos/live/live-video-event-payload.model.ts new file mode 100644 index 000000000..f9038f4de --- /dev/null +++ b/shared/models/videos/live/live-video-event-payload.model.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | import { VideoState } from '../video-state.enum' | ||
2 | |||
3 | export interface LiveVideoEventPayload { | ||
4 | state: VideoState | ||
5 | } | ||
diff --git a/shared/models/videos/live/live-video-event.type.ts b/shared/models/videos/live/live-video-event.type.ts new file mode 100644 index 000000000..4d15899da --- /dev/null +++ b/shared/models/videos/live/live-video-event.type.ts | |||
@@ -0,0 +1 @@ | |||
export type LiveVideoEventType = 'state-change' | |||
diff --git a/shared/models/videos/video-live.model.ts b/shared/models/videos/live/live-video.model.ts index 2a834dc91..74abee96e 100644 --- a/shared/models/videos/video-live.model.ts +++ b/shared/models/videos/live/live-video.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export interface VideoLive { | 1 | export interface LiveVideo { |
2 | rtmpUrl: string | 2 | rtmpUrl: string |
3 | streamKey: string | 3 | streamKey: string |
4 | } | 4 | } |