aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts1
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html10
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts16
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts8
-rw-r--r--client/src/app/+videos/+video-edit/video-update.resolver.ts6
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html10
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.scss5
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts59
-rw-r--r--client/src/app/core/core.module.ts4
-rw-r--r--client/src/app/core/notification/index.ts2
-rw-r--r--client/src/app/core/notification/peertube-socket.service.ts86
-rw-r--r--client/src/app/core/notification/user-notification-socket.service.ts44
-rw-r--r--client/src/app/menu/avatar-notification.component.ts6
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts4
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.service.ts10
-rw-r--r--client/src/app/shared/shared-main/video/index.ts2
-rw-r--r--client/src/app/shared/shared-main/video/live-video.service.ts (renamed from client/src/app/shared/shared-main/video/video-live.service.ts)8
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts5
23 files changed, 207 insertions, 95 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 {
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, VideoLive, VideoPrivacy } from '@shared/models' 23import { ServerConfig, VideoConstant, LiveVideo, 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'
@@ -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'
4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core'
5import { scrollToTop } from '@app/helpers' 5import { scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms' 6import { FormValidatorService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoService, VideoLiveService } from '@app/shared/shared-main' 7import { LiveVideoService, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
8import { LoadingBarService } from '@ngx-loading-bar/core' 8import { LoadingBarService } from '@ngx-loading-bar/core'
9import { VideoCreate, VideoLive, VideoPrivacy } from '@shared/models' 9import { LiveVideo, VideoCreate, VideoPrivacy } from '@shared/models'
10import { VideoSend } from './video-send' 10import { 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'
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, VideoLive } from '@shared/models' 8import { LiveVideo, VideoPrivacy } from '@shared/models'
9import { hydrateFormFromVideo } from './shared/video-edit-utils' 9import { 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'
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, VideoDetails, VideoLiveService, VideoService } from '@app/shared/shared-main' 5import { VideoCaptionService, VideoChannelService, VideoDetails, LiveVideoService, 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 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'
4import { PlatformLocation } from '@angular/common' 4import { PlatformLocation } from '@angular/common'
5import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core' 5import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
6import { ActivatedRoute, Router } from '@angular/router' 6import { ActivatedRoute, Router } from '@angular/router'
7import { AuthService, AuthUser, ConfirmService, MarkdownService, Notifier, RestExtractor, ServerService, UserService } from '@app/core' 7import {
8 AuthService,
9 AuthUser,
10 ConfirmService,
11 MarkdownService,
12 Notifier,
13 PeerTubeSocket,
14 RestExtractor,
15 ServerService,
16 UserService
17} from '@app/core'
8import { HooksService } from '@app/core/plugins/hooks.service' 18import { HooksService } from '@app/core/plugins/hooks.service'
9import { RedirectService } from '@app/core/routing/redirect.service' 19import { RedirectService } from '@app/core/routing/redirect.service'
10import { isXPercentInViewport, scrollToTop } from '@app/helpers' 20import { isXPercentInViewport, scrollToTop } from '@app/helpers'
@@ -30,6 +40,8 @@ import { environment } from '../../../environments/environment'
30import { VideoSupportComponent } from './modal/video-support.component' 40import { VideoSupportComponent } from './modal/video-support.component'
31import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' 41import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
32 42
43type 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'
4import { CommonModule } from '@angular/common' 4import { CommonModule } from '@angular/common'
5import { NgModule, Optional, SkipSelf } from '@angular/core' 5import { NgModule, Optional, SkipSelf } from '@angular/core'
6import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 6import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
7import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' 7import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service'
8import { HooksService } from '@app/core/plugins/hooks.service' 8import { HooksService } from '@app/core/plugins/hooks.service'
9import { PluginService } from '@app/core/plugins/plugin.service' 9import { PluginService } from '@app/core/plugins/plugin.service'
10import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service' 10import { 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 @@
1export * from './notifier.service' 1export * from './notifier.service'
2export * from './user-notification-socket.service' 2export * 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 @@
1import { Subject } from 'rxjs'
2import { Injectable, NgZone } from '@angular/core'
3import { LiveVideoEventPayload, LiveVideoEventType, UserNotification as UserNotificationServer } from '@shared/models'
4import { environment } from '../../../environments/environment'
5import { AuthService } from '../auth'
6
7export type NotificationEvent = 'new' | 'read' | 'read-all'
8
9@Injectable()
10export 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 @@
1import { Subject } from 'rxjs'
2import { Injectable, NgZone } from '@angular/core'
3import { UserNotification as UserNotificationServer } from '@shared/models'
4import { environment } from '../../../environments/environment'
5import { AuthService } from '../auth'
6
7export type NotificationEvent = 'new' | 'read' | 'read-all'
8
9@Injectable()
10export 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'
2import { filter } from 'rxjs/operators' 2import { filter } from 'rxjs/operators'
3import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core' 3import { Component, EventEmitter, Input, Output, OnDestroy, OnInit, ViewChild } from '@angular/core'
4import { NavigationEnd, Router } from '@angular/router' 4import { NavigationEnd, Router } from '@angular/router'
5import { Notifier, User, UserNotificationSocket } from '@app/core' 5import { Notifier, User, PeerTubeSocket } from '@app/core'
6import { UserNotificationService } from '@app/shared/shared-main' 6import { UserNotificationService } from '@app/shared/shared-main'
7import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' 7import { 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'
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, VideoLiveService } from './video' 26import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService, LiveVideoService } 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,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 @@
1import { catchError, map, tap } from 'rxjs/operators' 1import { catchError, map, tap } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http' 2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { ComponentPaginationLight, RestExtractor, RestService, User, UserNotificationSocket, AuthService } from '@app/core' 4import { ComponentPaginationLight, RestExtractor, RestService, User, PeerTubeSocket, AuthService } from '@app/core'
5import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models' 5import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '@shared/models'
6import { environment } from '../../../../environments/environment' 6import { environment } from '../../../../environments/environment'
7import { UserNotification } from './user-notification.model' 7import { 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 @@
1export * from './live-video.service'
1export * from './redundancy.service' 2export * from './redundancy.service'
2export * from './video-details.model' 3export * from './video-details.model'
3export * from './video-edit.model' 4export * from './video-edit.model'
4export * from './video-import.service' 5export * from './video-import.service'
5export * from './video-live.service'
6export * from './video-ownership.service' 6export * from './video-ownership.service'
7export * from './video.model' 7export * from './video.model'
8export * from './video.service' 8export * 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'
2import { HttpClient } from '@angular/common/http' 2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core' 4import { RestExtractor } from '@app/core'
5import { VideoCreate, VideoLive } from '@shared/models' 5import { VideoCreate, LiveVideo } from '@shared/models'
6import { environment } from '../../../../environments/environment' 6import { environment } from '../../../../environments/environment'
7 7
8@Injectable() 8@Injectable()
9export class VideoLiveService { 9export 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 () {