diff options
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 @@ | |||
1 | import { Component, ElementRef, ViewChild } from '@angular/core' | ||
2 | import { LiveVideoService, Video } from '@app/shared/shared-main' | ||
3 | import { 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 | }) | ||
10 | export 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 | |||
4 | p-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' | |||
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' | 5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core' |
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
7 | import { immutableAssign } from '@app/helpers' | 7 | import { immutableAssign } from '@app/helpers' |
8 | import { Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' | 9 | import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' |
10 | import { VideoSortField } from '@shared/models' | 10 | import { VideoSortField } from '@shared/models' |
11 | import { VideoChangeOwnershipComponent } from './video-change-ownership/video-change-ownership.component' | 11 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' |
12 | import { 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 | |||
18 | export class MyAccountVideosComponent implements OnInit, DisableForReuseHook { | 19 | export 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 | |||
34 | import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' | 34 | import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component' |
35 | import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' | 35 | import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component' |
36 | import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' | 36 | import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component' |
37 | import { VideoChangeOwnershipComponent } from './my-account-videos/video-change-ownership/video-change-ownership.component' | 37 | import { VideoChangeOwnershipComponent } from './my-account-videos/modals/video-change-ownership.component' |
38 | import { LiveStreamInformationComponent } from './my-account-videos/modals/live-stream-information.component' | ||
38 | import { MyAccountComponent } from './my-account.component' | 39 | import { 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 @@ | |||
4 | textarea { | 4 | textarea { |
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 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { readdir, remove } from 'fs-extra' | 2 | import { readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' | 4 | import { getDurationFromVideoFile, getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' |
5 | import { publishAndFederateIfNeeded } from '@server/lib/video' | ||
5 | import { getHLSDirectory } from '@server/lib/video-paths' | 6 | import { getHLSDirectory } from '@server/lib/video-paths' |
6 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' | 7 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' |
7 | import { VideoModel } from '@server/models/video/video' | 8 | import { 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 | ||
91 | async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) { | 101 | async 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 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { publishAndFederateIfNeeded } from '@server/lib/video' | ||
2 | import { getVideoFilePath } from '@server/lib/video-paths' | 3 | import { getVideoFilePath } from '@server/lib/video-paths' |
3 | import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' | 4 | import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models' |
4 | import { | 5 | import { |
@@ -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 | |||
178 | async 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 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
2 | import { TagModel } from '@server/models/video/tag' | 3 | import { TagModel } from '@server/models/video/tag' |
3 | import { VideoModel } from '@server/models/video/video' | 4 | import { VideoModel } from '@server/models/video/video' |
4 | import { FilteredModelAttributes } from '@server/types' | 5 | import { FilteredModelAttributes } from '@server/types' |
5 | import { MTag, MThumbnail, MVideoTag, MVideoThumbnail } from '@server/types/models' | 6 | import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
6 | import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models' | 7 | import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models' |
8 | import { federateVideoIfNeeded } from './activitypub/videos' | ||
9 | import { Notifier } from './notifier' | ||
7 | import { createVideoMiniatureFromExisting } from './thumbnail' | 10 | import { createVideoMiniatureFromExisting } from './thumbnail' |
8 | 11 | ||
9 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { | 12 | function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> { |
@@ -78,10 +81,33 @@ async function setVideoTags (options: { | |||
78 | } | 81 | } |
79 | } | 82 | } |
80 | 83 | ||
84 | async 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 | ||
83 | export { | 108 | export { |
84 | buildLocalVideoFromReq, | 109 | buildLocalVideoFromReq, |
110 | publishAndFederateIfNeeded, | ||
85 | buildVideoThumbnailsFromReq, | 111 | buildVideoThumbnailsFromReq, |
86 | setVideoTags | 112 | setVideoTags |
87 | } | 113 | } |