Add modal to display live information
This commit is contained in:
parent
31c82cd914
commit
d846d99c6c
|
@ -0,0 +1,33 @@
|
|||
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Live information</h4>
|
||||
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="liveVideoRTMPUrl" i18n>Live RTMP Url</label>
|
||||
<my-input-readonly-copy id="liveVideoRTMPUrl" [value]="rtmpUrl"></my-input-readonly-copy>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="liveVideoStreamKey" i18n>Live stream key</label>
|
||||
<my-input-readonly-copy id="liveVideoStreamKey" [value]="streamKey"></my-input-readonly-copy>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="form-group inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Close" class="action-button action-button-cancel"
|
||||
(click)="dismiss()"
|
||||
>
|
||||
|
||||
<my-edit-button
|
||||
i18n-label label="Update live settings"
|
||||
[routerLink]="[ '/videos', 'update', video.uuid ]" (click)="dismiss()"
|
||||
></my-edit-button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,40 @@
|
|||
import { Component, ElementRef, ViewChild } from '@angular/core'
|
||||
import { LiveVideoService, Video } from '@app/shared/shared-main'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
selector: 'my-live-stream-information',
|
||||
templateUrl: './live-stream-information.component.html',
|
||||
styleUrls: [ './live-stream-information.component.scss' ]
|
||||
})
|
||||
export class LiveStreamInformationComponent {
|
||||
@ViewChild('modal', { static: true }) modal: ElementRef
|
||||
|
||||
video: Video
|
||||
rtmpUrl = ''
|
||||
streamKey = ''
|
||||
|
||||
constructor (
|
||||
private modalService: NgbModal,
|
||||
private liveVideoService: LiveVideoService
|
||||
) { }
|
||||
|
||||
show (video: Video) {
|
||||
this.video = video
|
||||
this.rtmpUrl = ''
|
||||
this.streamKey = ''
|
||||
|
||||
this.loadLiveInfo(video)
|
||||
|
||||
this.modalService
|
||||
.open(this.modal, { centered: true })
|
||||
}
|
||||
|
||||
private loadLiveInfo (video: Video) {
|
||||
this.liveVideoService.getVideoLive(video.id)
|
||||
.subscribe(live => {
|
||||
this.rtmpUrl = live.rtmpUrl
|
||||
this.streamKey = live.streamKey
|
||||
})
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer inputs">
|
||||
<div class="modal-footer">
|
||||
<div class="form-group inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
|
|
@ -0,0 +1,10 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
p-autocomplete {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin: 20px 0;
|
||||
}
|
|
@ -34,18 +34,13 @@
|
|||
|
||||
<ng-template ptTemplate="rowButtons" let-video>
|
||||
<div class="action-button">
|
||||
<my-delete-button label (click)="deleteVideo(video)"></my-delete-button>
|
||||
|
||||
<my-edit-button label [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
|
||||
|
||||
<my-button i18n-label label="Change ownership"
|
||||
className="action-button-change-ownership grey-button"
|
||||
icon="ownership-change"
|
||||
(click)="changeOwnership($event, video)"
|
||||
></my-button>
|
||||
<my-action-dropdown [actions]="videoActions" [entry]="{ video: video }"></my-action-dropdown>
|
||||
</div>
|
||||
</ng-template>
|
||||
</my-videos-selection>
|
||||
|
||||
|
||||
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
|
||||
<my-live-stream-information #liveStreamInformationModal></my-live-stream-information>
|
||||
|
|
|
@ -5,10 +5,11 @@ import { ActivatedRoute, Router } from '@angular/router'
|
|||
import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService } from '@app/core'
|
||||
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
|
||||
import { immutableAssign } from '@app/helpers'
|
||||
import { Video, VideoService } from '@app/shared/shared-main'
|
||||
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
||||
import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
|
||||
import { VideoSortField } from '@shared/models'
|
||||
import { VideoChangeOwnershipComponent } from './video-change-ownership/video-change-ownership.component'
|
||||
import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
|
||||
import { LiveStreamInformationComponent } from './modals/live-stream-information.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-videos',
|
||||
|
@ -18,6 +19,7 @@ import { VideoChangeOwnershipComponent } from './video-change-ownership/video-ch
|
|||
export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
|
||||
@ViewChild('videosSelection', { static: true }) videosSelection: VideosSelectionComponent
|
||||
@ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
|
||||
@ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent
|
||||
|
||||
titlePage: string
|
||||
selection: SelectionType = {}
|
||||
|
@ -37,6 +39,8 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
|
|||
}
|
||||
ownerDisplayType: OwnerDisplayType = 'videoChannel'
|
||||
|
||||
videoActions: DropdownAction<{ video: Video }>[] = []
|
||||
|
||||
videos: Video[] = []
|
||||
videosSearch: string
|
||||
videosSearchChanged = new Subject<string>()
|
||||
|
@ -56,6 +60,8 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildActions()
|
||||
|
||||
this.videosSearchChanged
|
||||
.pipe(debounceTime(500))
|
||||
.subscribe(() => {
|
||||
|
@ -138,12 +144,36 @@ export class MyAccountVideosComponent implements OnInit, DisableForReuseHook {
|
|||
)
|
||||
}
|
||||
|
||||
changeOwnership (event: Event, video: Video) {
|
||||
event.preventDefault()
|
||||
changeOwnership (video: Video) {
|
||||
this.videoChangeOwnershipModal.show(video)
|
||||
}
|
||||
|
||||
displayLiveInformation (video: Video) {
|
||||
this.liveStreamInformationModal.show(video)
|
||||
}
|
||||
|
||||
private removeVideoFromArray (id: number) {
|
||||
this.videos = this.videos.filter(v => v.id !== id)
|
||||
}
|
||||
|
||||
private buildActions () {
|
||||
this.videoActions = [
|
||||
{
|
||||
label: $localize`Display live information`,
|
||||
handler: ({ video }) => this.displayLiveInformation(video),
|
||||
isDisplayed: ({ video }) => video.isLive,
|
||||
iconName: 'live'
|
||||
},
|
||||
{
|
||||
label: $localize`Change ownership`,
|
||||
handler: ({ video }) => this.changeOwnership(video),
|
||||
iconName: 'ownership-change'
|
||||
},
|
||||
{
|
||||
label: $localize`Delete`,
|
||||
handler: ({ video }) => this.deleteVideo(video),
|
||||
iconName: 'delete'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,8 @@ import { MyAccountVideoPlaylistElementsComponent } from './my-account-video-play
|
|||
import { MyAccountVideoPlaylistUpdateComponent } from './my-account-video-playlists/my-account-video-playlist-update.component'
|
||||
import { MyAccountVideoPlaylistsComponent } from './my-account-video-playlists/my-account-video-playlists.component'
|
||||
import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
|
||||
import { VideoChangeOwnershipComponent } from './my-account-videos/video-change-ownership/video-change-ownership.component'
|
||||
import { VideoChangeOwnershipComponent } from './my-account-videos/modals/video-change-ownership.component'
|
||||
import { LiveStreamInformationComponent } from './my-account-videos/modals/live-stream-information.component'
|
||||
import { MyAccountComponent } from './my-account.component'
|
||||
|
||||
@NgModule({
|
||||
|
@ -68,6 +69,8 @@ import { MyAccountComponent } from './my-account.component'
|
|||
MyAccountVideosComponent,
|
||||
|
||||
VideoChangeOwnershipComponent,
|
||||
LiveStreamInformationComponent,
|
||||
|
||||
MyAccountOwnershipComponent,
|
||||
MyAccountAcceptOwnershipComponent,
|
||||
MyAccountVideoImportsComponent,
|
||||
|
|
|
@ -226,7 +226,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
isVideoDownloadable () {
|
||||
return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled
|
||||
return this.video && this.video instanceof VideoDetails && this.video.downloadEnabled && !this.video.isLive
|
||||
}
|
||||
|
||||
loadCompleteDescription () {
|
||||
|
|
|
@ -66,6 +66,7 @@ const icons = {
|
|||
'cross': require('!!raw-loader?!../../../assets/images/feather/x.svg').default,
|
||||
'tick': require('!!raw-loader?!../../../assets/images/feather/check.svg').default,
|
||||
'columns': require('!!raw-loader?!../../../assets/images/feather/columns.svg').default,
|
||||
'live': require('!!raw-loader?!../../../assets/images/feather/live.svg').default,
|
||||
'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
|
||||
'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Block video "{{ video.name }}"</h4>
|
||||
<h4 i18n class="modal-title" *ngIf="!video.isLive">Block video "{{ video.name }}"</h4>
|
||||
<h4 i18n class="modal-title" *ngIf="video.isLive">Block live "{{ video.name }}"</h4>
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
|
@ -28,6 +29,10 @@
|
|||
</my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<strong class="live-info" *ngIf="video.isLive" i18n>
|
||||
Blocking this live will automatically terminate the live stream.
|
||||
</strong>
|
||||
|
||||
<div class="form-group inputs">
|
||||
<input
|
||||
type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
|
||||
|
|
|
@ -4,3 +4,8 @@
|
|||
textarea {
|
||||
@include peertube-textarea(100%, 100px);
|
||||
}
|
||||
|
||||
.live-info {
|
||||
font-size: 15px;
|
||||
margin: 40px 0 20px 0;
|
||||
}
|
||||
|
|
|
@ -186,7 +186,12 @@ export class VideoActionsDropdownComponent implements OnChanges {
|
|||
async removeVideo () {
|
||||
this.modalOpened.emit()
|
||||
|
||||
const res = await this.confirmService.confirm($localize`Do you really want to delete this video?`, $localize`Delete`)
|
||||
let message = $localize`Do you really want to delete this video?`
|
||||
if (this.video.isLive) {
|
||||
message += ' ' + $localize`The live stream will be automatically terminated.`
|
||||
}
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Delete`)
|
||||
if (res === false) return
|
||||
|
||||
this.videoService.removeVideo(this.video.id)
|
||||
|
|
|
@ -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>
|
After Width: | Height: | Size: 389 B |
|
@ -1,7 +1,8 @@
|
|||
import * as Bull from 'bull'
|
||||
import { readdir, remove } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
import { getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
|
||||
import { getDurationFromVideoFile, getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
|
||||
import { publishAndFederateIfNeeded } from '@server/lib/video'
|
||||
import { getHLSDirectory } from '@server/lib/video-paths'
|
||||
import { generateHlsPlaylist } from '@server/lib/video-transcoding'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
|
@ -44,6 +45,7 @@ async function saveLive (video: MVideo, live: MVideoLive) {
|
|||
|
||||
const playlistFiles = files.filter(f => f.endsWith('.m3u8') && f !== 'master.m3u8')
|
||||
const resolutions: number[] = []
|
||||
let duration: number
|
||||
|
||||
for (const playlistFile of playlistFiles) {
|
||||
const playlistPath = join(hlsDirectory, playlistFile)
|
||||
|
@ -58,6 +60,10 @@ async function saveLive (video: MVideo, live: MVideoLive) {
|
|||
const segmentFiles = files.filter(f => f.startsWith(shouldStartWith) && f.endsWith('.ts'))
|
||||
await hlsPlaylistToFragmentedMP4(hlsDirectory, segmentFiles, mp4TmpName)
|
||||
|
||||
if (!duration) {
|
||||
duration = await getDurationFromVideoFile(mp4TmpName)
|
||||
}
|
||||
|
||||
resolutions.push(videoFileResolution)
|
||||
}
|
||||
|
||||
|
@ -67,6 +73,8 @@ async function saveLive (video: MVideo, live: MVideoLive) {
|
|||
|
||||
video.isLive = false
|
||||
video.state = VideoState.TO_TRANSCODE
|
||||
video.duration = duration
|
||||
|
||||
await video.save()
|
||||
|
||||
const videoWithFiles = await VideoModel.loadWithFiles(video.id)
|
||||
|
@ -86,6 +94,8 @@ async function saveLive (video: MVideo, live: MVideoLive) {
|
|||
|
||||
video.state = VideoState.PUBLISHED
|
||||
await video.save()
|
||||
|
||||
await publishAndFederateIfNeeded(video)
|
||||
}
|
||||
|
||||
async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as Bull from 'bull'
|
||||
import { publishAndFederateIfNeeded } from '@server/lib/video'
|
||||
import { getVideoFilePath } from '@server/lib/video-paths'
|
||||
import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
|
||||
import {
|
||||
|
@ -174,25 +175,3 @@ function createHlsJobIfEnabled (payload?: { videoUUID: string, resolution: numbe
|
|||
return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
|
||||
}
|
||||
}
|
||||
|
||||
async function publishAndFederateIfNeeded (video: MVideoUUID) {
|
||||
const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
|
||||
// Maybe the video changed in database, refresh it
|
||||
const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
|
||||
// Video does not exist anymore
|
||||
if (!videoDatabase) return undefined
|
||||
|
||||
// We transcoded the video file in another format, now we can publish it
|
||||
const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
|
||||
|
||||
// If the video was not published, we consider it is a new one for other instances
|
||||
await federateVideoIfNeeded(videoDatabase, videoPublished, t)
|
||||
|
||||
return { videoDatabase, videoPublished }
|
||||
})
|
||||
|
||||
if (videoPublished) {
|
||||
Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import { Transaction } from 'sequelize/types'
|
||||
import { sequelizeTypescript } from '@server/initializers/database'
|
||||
import { TagModel } from '@server/models/video/tag'
|
||||
import { VideoModel } from '@server/models/video/video'
|
||||
import { FilteredModelAttributes } from '@server/types'
|
||||
import { MTag, MThumbnail, MVideoTag, MVideoThumbnail } from '@server/types/models'
|
||||
import { MTag, MThumbnail, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
|
||||
import { ThumbnailType, VideoCreate, VideoPrivacy } from '@shared/models'
|
||||
import { federateVideoIfNeeded } from './activitypub/videos'
|
||||
import { Notifier } from './notifier'
|
||||
import { createVideoMiniatureFromExisting } from './thumbnail'
|
||||
|
||||
function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> {
|
||||
|
@ -78,10 +81,33 @@ async function setVideoTags (options: {
|
|||
}
|
||||
}
|
||||
|
||||
async function publishAndFederateIfNeeded (video: MVideoUUID) {
|
||||
const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
|
||||
// Maybe the video changed in database, refresh it
|
||||
const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
|
||||
// Video does not exist anymore
|
||||
if (!videoDatabase) return undefined
|
||||
|
||||
// We transcoded the video file in another format, now we can publish it
|
||||
const videoPublished = await videoDatabase.publishIfNeededAndSave(t)
|
||||
|
||||
// If the video was not published, we consider it is a new one for other instances
|
||||
await federateVideoIfNeeded(videoDatabase, videoPublished, t)
|
||||
|
||||
return { videoDatabase, videoPublished }
|
||||
})
|
||||
|
||||
if (videoPublished) {
|
||||
Notifier.Instance.notifyOnNewVideoIfNeeded(videoDatabase)
|
||||
Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
buildLocalVideoFromReq,
|
||||
publishAndFederateIfNeeded,
|
||||
buildVideoThumbnailsFromReq,
|
||||
setVideoTags
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue