--- /dev/null
+<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>
--- /dev/null
+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
+ })
+ }
+}
</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"
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+p-autocomplete {
+ display: block;
+}
+
+.form-group {
+ margin: 20px 0;
+}
\ No newline at end of file
<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>
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',
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 = {}
}
ownerDisplayType: OwnerDisplayType = 'videoChannel'
+ videoActions: DropdownAction<{ video: Video }>[] = []
+
videos: Video[] = []
videosSearch: string
videosSearchChanged = new Subject<string>()
}
ngOnInit () {
+ this.buildActions()
+
this.videosSearchChanged
.pipe(debounceTime(500))
.subscribe(() => {
)
}
- 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'
+ }
+ ]
+ }
}
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({
MyAccountVideosComponent,
VideoChangeOwnershipComponent,
+ LiveStreamInformationComponent,
+
MyAccountOwnershipComponent,
MyAccountAcceptOwnershipComponent,
MyAccountVideoImportsComponent,
}
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 () {
'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
}
<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>
</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"
textarea {
@include peertube-textarea(100%, 100px);
}
+
+.live-info {
+ font-size: 15px;
+ margin: 40px 0 20px 0;
+}
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)
--- /dev/null
+<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
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'
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)
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)
}
video.isLive = false
video.state = VideoState.TO_TRANSCODE
+ video.duration = duration
+
await video.save()
const videoWithFiles = await VideoModel.loadWithFiles(video.id)
video.state = VideoState.PUBLISHED
await video.save()
+
+ await publishAndFederateIfNeeded(video)
}
async function cleanupLive (video: MVideo, streamingPlaylist: MStreamingPlaylist) {
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 {
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)
- }
-}
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> {
}
}
+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
}