aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-10-28 10:49:20 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-09 15:33:04 +0100
commitd846d99c6c81028bb7bd3cb20abd433cbf396a22 (patch)
tree1090abd56e9b2df82dd92783f499ac9f10fbd501
parent31c82cd914e13dbf53280d0aad0740d70c414441 (diff)
downloadPeerTube-d846d99c6c81028bb7bd3cb20abd433cbf396a22.tar.gz
PeerTube-d846d99c6c81028bb7bd3cb20abd433cbf396a22.tar.zst
PeerTube-d846d99c6c81028bb7bd3cb20abd433cbf396a22.zip
Add modal to display live information
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.html33
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.scss (renamed from client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.scss)0
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.ts40
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html (renamed from client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html)2
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss10
-rw-r--r--client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts (renamed from client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts)0
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html9
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts38
-rw-r--r--client/src/app/+my-account/my-account.module.ts5
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts2
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts1
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.html7
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.scss5
-rw-r--r--client/src/app/shared/shared-video-miniature/video-actions-dropdown.component.ts7
-rw-r--r--client/src/assets/images/feather/live.svg1
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts12
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts23
-rw-r--r--server/lib/video.ts28
18 files changed, 183 insertions, 40 deletions
diff --git a/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.html b/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.html
new file mode 100644
index 000000000..5e2323b91
--- /dev/null
+++ b/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.html
@@ -0,0 +1,33 @@
1<ng-template #modal let-close="close" let-dismiss="dismiss">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Live information</h4>
4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
6 </div>
7
8 <div class="modal-body">
9 <div class="form-group">
10 <label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label>
11 <my-input-readonly-copy id="liveVideoRTMPUrl" [value]="rtmpUrl"></my-input-readonly-copy>
12 </div>
13
14 <div class="form-group">
15 <label for="liveVideoStreamKey" i18n>Live stream key</label>
16 <my-input-readonly-copy id="liveVideoStreamKey" [value]="streamKey"></my-input-readonly-copy>
17 </div>
18 </div>
19
20 <div class="modal-footer">
21 <div class="form-group inputs">
22 <input
23 type="button" role="button" i18n-value value="Close" class="action-button action-button-cancel"
24 (click)="dismiss()"
25 >
26
27 <my-edit-button
28 i18n-label label="Update live settings"
29 [routerLink]="[ '/videos', 'update', video.uuid ]" (click)="dismiss()"
30 ></my-edit-button>
31 </div>
32 </div>
33</ng-template>
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.scss b/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.scss
index a79fec179..a79fec179 100644
--- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.scss
+++ b/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.scss
diff --git a/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.ts b/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.ts
new file mode 100644
index 000000000..a5885a8e7
--- /dev/null
+++ b/client/src/app/+my-account/my-account-videos/modals/live-stream-information.component.ts
@@ -0,0 +1,40 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { LiveVideoService, Video } from '@app/shared/shared-main'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4
5@Component({
6 selector: 'my-live-stream-information',
7 templateUrl: './live-stream-information.component.html',
8 styleUrls: [ './live-stream-information.component.scss' ]
9})
10export class LiveStreamInformationComponent {
11 @ViewChild('modal', { static: true }) modal: ElementRef
12
13 video: Video
14 rtmpUrl = ''
15 streamKey = ''
16
17 constructor (
18 private modalService: NgbModal,
19 private liveVideoService: LiveVideoService
20 ) { }
21
22 show (video: Video) {
23 this.video = video
24 this.rtmpUrl = ''
25 this.streamKey = ''
26
27 this.loadLiveInfo(video)
28
29 this.modalService
30 .open(this.modal, { centered: true })
31 }
32
33 private loadLiveInfo (video: Video) {
34 this.liveVideoService.getVideoLive(video.id)
35 .subscribe(live => {
36 this.rtmpUrl = live.rtmpUrl
37 this.streamKey = live.streamKey
38 })
39 }
40}
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html
index 9d809d2bf..c7c5a0b69 100644
--- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html
+++ b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.html
@@ -16,7 +16,7 @@
16 </div> 16 </div>
17 </div> 17 </div>
18 18
19 <div class="modal-footer inputs"> 19 <div class="modal-footer">
20 <div class="form-group inputs"> 20 <div class="form-group inputs">
21 <input 21 <input
22 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 22 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
diff --git a/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss
new file mode 100644
index 000000000..a79fec179
--- /dev/null
+++ b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4p-autocomplete {
5 display: block;
6}
7
8.form-group {
9 margin: 20px 0;
10} \ No newline at end of file
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts
index 84237dee1..84237dee1 100644
--- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts
+++ b/client/src/app/+my-account/my-account-videos/modals/video-change-ownership.component.ts
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
index f2ed0ac99..aa5b284e7 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
@@ -34,18 +34,13 @@
34 34
35 <ng-template ptTemplate="rowButtons" let-video> 35 <ng-template ptTemplate="rowButtons" let-video>
36 <div class="action-button"> 36 <div class="action-button">
37 <my-delete-button label (click)="deleteVideo(video)"></my-delete-button>
38
39 <my-edit-button label [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button> 37 <my-edit-button label [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
40 38
41 <my-button i18n-label label="Change ownership" 39 <my-action-dropdown [actions]="videoActions" [entry]="{ video: video }"></my-action-dropdown>
42 className="action-button-change-ownership grey-button"
43 icon="ownership-change"
44 (click)="changeOwnership($event, video)"
45 ></my-button>
46 </div> 40 </div>
47 </ng-template> 41 </ng-template>
48</my-videos-selection> 42</my-videos-selection>
49 43
50 44
51<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> 45<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
46<my-live-stream-information #liveStreamInformationModal></my-live-stream-information>
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
index 46a02a41a..7a3019239 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
@@ -5,10 +5,11 @@ import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' 5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { immutableAssign } from '@app/helpers' 7import { immutableAssign } from '@app/helpers'
8import { Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' 9import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
10import { VideoSortField } from '@shared/models' 10import { VideoSortField } from '@shared/models'
11import { VideoChangeOwnershipComponent } from './video-change-ownership/video-change-ownership.component' 11import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
12import { LiveStreamInformationComponent } from './modals/live-stream-information.component'
12 13
13@Component({ 14@Component({
14 selector: 'my-account-videos', 15 selector: 'my-account-videos',
@@ -18,6 +19,7 @@ import { VideoChangeOwnershipComponent } from './video-change-ownership/video-ch
18export class MyAccountVideosComponent implements OnInit, DisableForReuseHook { 19export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
19 @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent 20 @ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
20 @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent 21 @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
22 @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent
21 23
22 titlePage: string 24 titlePage: string
23 selection: SelectionType = {} 25 selection: SelectionType = {}
@@ -37,6 +39,8 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
37 } 39 }
38 ownerDisplayType: OwnerDisplayType = 'videoChannel' 40 ownerDisplayType: OwnerDisplayType = 'videoChannel'
39 41
42 videoActions: DropdownAction<{ video: Video }>[] = []
43
40 videos: Video[] = [] 44 videos: Video[] = []
41 videosSearch: string 45 videosSearch: string
42 videosSearchChanged = new Subject<string>() 46 videosSearchChanged = new Subject<string>()
@@ -56,6 +60,8 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
56 } 60 }
57 61
58 ngOnInit () { 62 ngOnInit () {
63 this.buildActions()
64
59 this.videosSearchChanged 65 this.videosSearchChanged
60 .pipe(debounceTime(500)) 66 .pipe(debounceTime(500))
61 .subscribe(() => { 67 .subscribe(() => {
@@ -138,12 +144,36 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
138 ) 144 )
139 } 145 }
140 146
141 changeOwnership (event: Event, video: Video) { 147 changeOwnership (video: Video) {
142 event.preventDefault()
143 this.videoChangeOwnershipModal.show(video) 148 this.videoChangeOwnershipModal.show(video)
144 } 149 }
145 150
151 displayLiveInformation (video: Video) {
152 this.liveStreamInformationModal.show(video)
153 }
154
146 private removeVideoFromArray (id: number) { 155 private removeVideoFromArray (id: number) {
147 this.videos = this.videos.filter(v => v.id !== id) 156 this.videos = this.videos.filter(v => v.id !== id)
148 } 157 }
158
159 private buildActions () {
160 this.videoActions = [
161 {
162 label: $localize`Display live information`,
163 handler: ({ video }) => this.displayLiveInformation(video),
164 isDisplayed: ({ video }) => video.isLive,
165 iconName: 'live'
166 },
167 {
168 label: $localize`Change ownership`,
169 handler: ({ video }) => this.changeOwnership(video),
170 iconName: 'ownership-change'
171 },
172 {
173 label: $localize`Delete`,
174 handler: ({ video }) => this.deleteVideo(video),
175 iconName: 'delete'
176 }
177 ]
178 }
149} 179}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 5f7ed4d2f..6b8baff52 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -34,7 +34,8 @@ import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-play
34import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' 34import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component'
35import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' 35import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
36import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' 36import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
37import { VideoChangeOwnershipComponent } from './my-account-videos/video-change-ownership/video-change-ownership.component' 37import { VideoChangeOwnershipComponent } from './my-account-videos/modals/video-change-ownership.component'
38import { LiveStreamInformationComponent } from './my-account-videos/modals/live-stream-information.component'
38import { MyAccountComponent } from './my-account.component' 39import { MyAccountComponent } from './my-account.component'
39 40
40@NgModule({ 41@NgModule({
@@ -68,6 +69,8 @@ import { MyAccountComponent } from './my-account.component'
68 MyAccountVideosComponent, 69 MyAccountVideosComponent,
69 70
70 VideoChangeOwnershipComponent, 71 VideoChangeOwnershipComponent,
72 LiveStreamInformationComponent,
73
71 MyAccountOwnershipComponent, 74 MyAccountOwnershipComponent,
72 MyAccountAcceptOwnershipComponent, 75 MyAccountAcceptOwnershipComponent,
73 MyAccountVideoImportsComponent, 76 MyAccountVideoImportsComponent,
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 e4edb42fb..9a3439731 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -226,7 +226,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
226 } 226 }
227 227
228 isVideoDownloadable () { 228 isVideoDownloadable () {
229 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled 229 return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled && !this.video.isLive
230 } 230 }
231 231
232 loadCompleteDescription () { 232 loadCompleteDescription () {
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index 99efcd599..ab71bc3e7 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -66,6 +66,7 @@ const icons = {
66 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default, 66 'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default,
67 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default, 67 'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default,
68 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default, 68 'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default,
69 'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default,
69 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, 70 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
70 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default 71 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default
71} 72}
diff --git a/client/src/app/shared/shared-moderation/video-block.component.html b/client/src/app/shared/shared-moderation/video-block.component.html
index 5e73d66c5..e982c4d77 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.html
+++ b/client/src/app/shared/shared-moderation/video-block.component.html
@@ -1,6 +1,7 @@
1<ng-template #modal> 1<ng-template #modal>
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Block video "{{ video.name }}"</h4> 3 <h4 i18n class="modal-title" *ngIf="!video.isLive">Block video "{{ video.name }}"</h4>
4 <h4 i18n class="modal-title" *ngIf="video.isLive">Block live "{{ video.name }}"</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> 5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 6 </div>
6 7
@@ -28,6 +29,10 @@
28 </my-peertube-checkbox> 29 </my-peertube-checkbox>
29 </div> 30 </div>
30 31
32 <strong class="live-info" *ngIf="video.isLive" i18n>
33 Blocking this live will automatically terminate the live stream.
34 </strong>
35
31 <div class="form-group inputs"> 36 <div class="form-group inputs">
32 <input 37 <input
33 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel" 38 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
diff --git a/client/src/app/shared/shared-moderation/video-block.component.scss b/client/src/app/shared/shared-moderation/video-block.component.scss
index afcdb9a16..afa0d96f7 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.scss
+++ b/client/src/app/shared/shared-moderation/video-block.component.scss
@@ -4,3 +4,8 @@
4textarea { 4textarea {
5 @include peertube-textarea(100%, 100px); 5 @include peertube-textarea(100%, 100px);
6} 6}
7
8.live-info {
9 font-size: 15px;
10 margin: 40px 0 20px 0;
11}
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 4ef17bfe3..8f4c129a5 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
@@ -186,7 +186,12 @@ export class VideoActionsDropdownComponent implements OnChanges {
186 async removeVideo () { 186 async removeVideo () {
187 this.modalOpened.emit() 187 this.modalOpened.emit()
188 188
189 const res = await this.confirmService.confirm($localize`Do you really want to delete this video?`, $localize`Delete`) 189 let message = $localize`Do you really want to delete this video?`
190 if (this.video.isLive) {
191 message += ' ' + $localize`The live stream will be automatically terminated.`
192 }
193
194 const res = await this.confirmService.confirm(message, $localize`Delete`)
190 if (res === false) return 195 if (res === false) return
191 196
192 this.videoService.removeVideo(this.video.id) 197 this.videoService.removeVideo(this.video.id)
diff --git a/client/src/assets/images/feather/live.svg b/client/src/assets/images/feather/live.svg
new file mode 100644
index 000000000..5abfcd13c
--- /dev/null
+++ b/client/src/assets/images/feather/live.svg
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-radio"><circle cx="12" cy="12" r="2"></circle><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"></path></svg> \ No newline at end of file
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index cd5bb1d1c..32eeff4d1 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,7 +1,8 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { readdir, remove } from 'fs-extra' 2import { readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' 4import { getDurationFromVideoFile, getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
5import { publishAndFederateIfNeeded } from '@server/lib/video'
5import { getHLSDirectory } from '@server/lib/video-paths' 6import { getHLSDirectory } from '@server/lib/video-paths'
6import { generateHlsPlaylist } from '@server/lib/video-transcoding' 7import { generateHlsPlaylist } from '@server/lib/video-transcoding'
7import { VideoModel } from '@server/models/video/video' 8import { VideoModel } from '@server/models/video/video'
@@ -44,6 +45,7 @@ async function saveLive (video: MVideo, live: MVideoLive) {
44 45
45 const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8') 46 const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8')
46 const resolutions: number[] = [] 47 const resolutions: number[] = []
48 let duration: number
47 49
48 for (const playlistFile of playlistFiles) { 50 for (const playlistFile of playlistFiles) {
49 const playlistPath = join(hlsDirectory, playlistFile) 51 const playlistPath = join(hlsDirectory, playlistFile)
@@ -58,6 +60,10 @@ async function saveLive (video: MVideo, live: MVideoLive) {
58 const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts')) 60 const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts'))
59 await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName) 61 await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName)
60 62
63 if (!duration) {
64 duration = await getDurationFromVideoFile(mp4TmpName)
65 }
66
61 resolutions.push(videoFileResolution) 67 resolutions.push(videoFileResolution)
62 } 68 }
63 69
@@ -67,6 +73,8 @@ async function saveLive (video: MVideo, live: MVideoLive) {
67 73
68 video.isLive = false 74 video.isLive = false
69 video.state = VideoState.TO_TRANSCODE 75 video.state = VideoState.TO_TRANSCODE
76 video.duration = duration
77
70 await video.save() 78 await video.save()
71 79
72 const videoWithFiles = await VideoModel.loadWithFiles(video.id) 80 const videoWithFiles = await VideoModel.loadWithFiles(video.id)
@@ -86,6 +94,8 @@ async function saveLive (video: MVideo, live: MVideoLive) {
86 94
87 video.state = VideoState.PUBLISHED 95 video.state = VideoState.PUBLISHED
88 await video.save() 96 await video.save()
97
98 await publishAndFederateIfNeeded(video)
89} 99}
90 100
91async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { 101async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 2aebc29f7..843a9f1b5 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -1,4 +1,5 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { publishAndFederateIfNeeded } from '@server/lib/video'
2import { getVideoFilePath } from '@server/lib/video-paths' 3import { getVideoFilePath } from '@server/lib/video-paths'
3import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' 4import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
4import { 5import {
@@ -174,25 +175,3 @@ function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: numbe
174 return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload }) 175 return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
175 } 176 }
176} 177}
177
178async function publishAndFederateIfNeeded (video: MVideoUUID) {
179 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
180 // Maybe the video changed in database, refresh it
181 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
182 // Video does not exist anymore
183 if (!videoDatabase) return undefined
184
185 // We transcoded the video file in another format, now we can publish it
186 const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
187
188 // If the video was not published, we consider it is a new one for other instances
189 await federateVideoIfNeeded(videoDatabase, videoPublished, t)
190
191 return { videoDatabase, videoPublished }
192 })
193
194 if (videoPublished) {
195 Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
196 Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
197 }
198}
diff --git a/server/lib/video.ts b/server/lib/video.ts
index 6df41e6cd..81b7c4159 100644
--- a/server/lib/video.ts
+++ b/server/lib/video.ts
@@ -1,9 +1,12 @@
1import { Transaction } from 'sequelize/types' 1import { Transaction } from 'sequelize/types'
2import { sequelizeTypescript } from '@server/initializers/database'
2import { TagModel } from '@server/models/video/tag' 3import { TagModel } from '@server/models/video/tag'
3import { VideoModel } from '@server/models/video/video' 4import { VideoModel } from '@server/models/video/video'
4import { FilteredModelAttributes } from '@server/types' 5import { FilteredModelAttributes } from '@server/types'
5import { MTag, MThumbnail, MVideoTag, MVideoThumbnail } from '@server/types/models' 6import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
6import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models' 7import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models'
8import { federateVideoIfNeeded } from './activitypub/videos'
9import { Notifier } from './notifier'
7import { createVideoMiniatureFromExisting } from './thumbnail' 10import { createVideoMiniatureFromExisting } from './thumbnail'
8 11
9function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { 12function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> {
@@ -78,10 +81,33 @@ async function setVideoTags (options: {
78 } 81 }
79} 82}
80 83
84async function publishAndFederateIfNeeded (video: MVideoUUID) {
85 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
86 // Maybe the video changed in database, refresh it
87 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
88 // Video does not exist anymore
89 if (!videoDatabase) return undefined
90
91 // We transcoded the video file in another format, now we can publish it
92 const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
93
94 // If the video was not published, we consider it is a new one for other instances
95 await federateVideoIfNeeded(videoDatabase, videoPublished, t)
96
97 return { videoDatabase, videoPublished }
98 })
99
100 if (videoPublished) {
101 Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
102 Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
103 }
104}
105
81// --------------------------------------------------------------------------- 106// ---------------------------------------------------------------------------
82 107
83export { 108export {
84 buildLocalVideoFromReq, 109 buildLocalVideoFromReq,
110 publishAndFederateIfNeeded,
85 buildVideoThumbnailsFromReq, 111 buildVideoThumbnailsFromReq,
86 setVideoTags 112 setVideoTags
87} 113}