aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html22
-rw-r--r--client/src/app/shared/shared-thumbnail/video-thumbnail.component.html5
-rw-r--r--client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss7
-rw-r--r--client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts5
-rw-r--r--server/lib/live-manager.ts12
-rw-r--r--server/models/video/video.ts13
-rw-r--r--server/tests/api/live/live.ts82
-rw-r--r--shared/extra-utils/videos/video-streaming-playlists.ts15
8 files changed, 151 insertions, 10 deletions
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 daee4be2a..9e469f0b0 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 35
36 <div i18n class="col-md-12 alert alert-info" *ngIf="isLiveEnded()"> 36 <div i18n class="col-md-12 alert alert-info" *ngIf="isLiveEnded()">
37 This live is finished. 37 This live has ended.
38 </div> 38 </div>
39 39
40 <div class="col-md-12 alert alert-danger" *ngIf="video?.blacklisted"> 40 <div class="col-md-12 alert alert-danger" *ngIf="video?.blacklisted">
@@ -51,8 +51,14 @@
51 <div class="d-block d-md-none"> <!-- only shown on medium devices, has its counterpart for larger viewports below --> 51 <div class="d-block d-md-none"> <!-- only shown on medium devices, has its counterpart for larger viewports below -->
52 <h1 class="video-info-name">{{ video.name }}</h1> 52 <h1 class="video-info-name">{{ video.name }}</h1>
53 53
54 <div i18n class="video-info-date-views"> 54 <div class="video-info-date-views">
55 Published <my-date-toggle [date]="video.publishedAt"></my-date-toggle> <span class="views"> • {{ video.views | myNumberFormatter }} views</span> 55 <ng-container i18n>Published <my-date-toggle [date]="video.publishedAt"></my-date-toggle></ng-container>
56
57 <span i18n class="views">
58 • {{ video.views | myNumberFormatter }}
59 <ng-container *ngIf="!video.isLive">views</ng-container>
60 <ng-container *ngIf="video.isLive">viewers</ng-container>
61 </span>
56 </div> 62 </div>
57 </div> 63 </div>
58 64
@@ -62,8 +68,14 @@
62 </div> 68 </div>
63 69
64 <div class="video-info-first-row-bottom"> 70 <div class="video-info-first-row-bottom">
65 <div i18n class="d-none d-md-block video-info-date-views"> 71 <div class="d-none d-md-block video-info-date-views">
66 Published <my-date-toggle [date]="video.publishedAt"></my-date-toggle> <span class="views"> • {{ video.views | myNumberFormatter }} views</span> 72 <ng-container i18n>Published <my-date-toggle [date]="video.publishedAt"></my-date-toggle></ng-container>
73
74 <span i18n class="views">
75 • {{ video.views | myNumberFormatter }}
76 <ng-container *ngIf="!video.isLive">views</ng-container>
77 <ng-container *ngIf="video.isLive">viewers</ng-container>
78 </span>
67 </div> 79 </div>
68 80
69 <div class="video-actions-rates"> 81 <div class="video-actions-rates">
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html
index be07844ab..4fea0cc1c 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.html
@@ -27,7 +27,10 @@
27 <div class="video-thumbnail-label-overlay danger"><ng-content select="label-danger"></ng-content></div> 27 <div class="video-thumbnail-label-overlay danger"><ng-content select="label-danger"></ng-content></div>
28 28
29 <div class="video-thumbnail-duration-overlay" *ngIf="!video.isLive">{{ video.durationLabel }}</div> 29 <div class="video-thumbnail-duration-overlay" *ngIf="!video.isLive">{{ video.durationLabel }}</div>
30 <div i18n class="video-thumbnail-live-overlay" *ngIf="video.isLive">LIVE</div> 30 <div class="video-thumbnail-live-overlay" [ngClass]="{ 'live-ended': isLiveEnded() }" *ngIf="video.isLive">
31 <ng-container i18n *ngIf="!isLiveEnded()">LIVE</ng-container>
32 <ng-container i18n *ngIf="isLiveEnded()">LIVE ENDED</ng-container>
33 </div>
31 34
32 <div class="play-overlay"> 35 <div class="play-overlay">
33 <div class="icon"></div> 36 <div class="icon"></div>
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
index 1b6151c89..4f53ffaf6 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.scss
@@ -51,9 +51,12 @@
51 } 51 }
52 52
53 .video-thumbnail-live-overlay { 53 .video-thumbnail-live-overlay {
54 background-color: rgba(224, 8, 8, 0.7);
55 color: #fff;
56 font-weight: $font-semibold; 54 font-weight: $font-semibold;
55 color: #fff;
56
57 &:not(.live-ended) {
58 background-color: rgba(224, 8, 8, 0.7);
59 }
57 } 60 }
58 61
59 .video-thumbnail-actions-overlay { 62 .video-thumbnail-actions-overlay {
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
index b2a2cf240..67a9b0028 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
@@ -1,5 +1,6 @@
1import { Component, EventEmitter, Input, Output } from '@angular/core' 1import { Component, EventEmitter, Input, Output } from '@angular/core'
2import { ScreenService } from '@app/core' 2import { ScreenService } from '@app/core'
3import { VideoState } from '@shared/models'
3import { Video } from '../shared-main' 4import { Video } from '../shared-main'
4 5
5@Component({ 6@Component({
@@ -29,6 +30,10 @@ export class VideoThumbnailComponent {
29 this.addedToWatchLaterText = $localize`Remove from watch later` 30 this.addedToWatchLaterText = $localize`Remove from watch later`
30 } 31 }
31 32
33 isLiveEnded () {
34 return this.video.state.id === VideoState.LIVE_ENDED
35 }
36
32 getImageUrl () { 37 getImageUrl () {
33 if (!this.video) return '' 38 if (!this.video) return ''
34 39
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index 2702437c4..fe5b33322 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -99,6 +99,10 @@ class LiveManager {
99 } 99 }
100 }) 100 })
101 101
102 // Cleanup broken lives, that were terminated by a server restart for example
103 this.handleBrokenLives()
104 .catch(err => logger.error('Cannot handle broken lives.', { err }))
105
102 setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE) 106 setInterval(() => this.updateLiveViews(), VIEW_LIFETIME.LIVE)
103 } 107 }
104 108
@@ -468,6 +472,14 @@ class LiveManager {
468 } 472 }
469 } 473 }
470 474
475 private async handleBrokenLives () {
476 const videoIds = await VideoModel.listPublishedLiveIds()
477
478 for (const id of videoIds) {
479 await this.onEndTransmuxing(id, true)
480 }
481 }
482
471 static get Instance () { 483 static get Instance () {
472 return this.instance || (this.instance = new this()) 484 return this.instance || (this.instance = new this())
473 } 485 }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 70839aa89..f3055a494 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -988,6 +988,19 @@ export class VideoModel extends Model<VideoModel> {
988 }) 988 })
989 } 989 }
990 990
991 static listPublishedLiveIds () {
992 const options = {
993 attributes: [ 'id' ],
994 where: {
995 isLive: true,
996 state: VideoState.PUBLISHED
997 }
998 }
999
1000 return VideoModel.findAll(options)
1001 .map(v => v.id)
1002 }
1003
991 static listUserVideosForApi ( 1004 static listUserVideosForApi (
992 accountId: number, 1005 accountId: number,
993 start: number, 1006 start: number,
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index 29081c6cc..aa2e1318a 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -2,22 +2,28 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { FfmpegCommand } from 'fluent-ffmpeg'
5import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io' 6import { getLiveNotificationSocket } from '@shared/extra-utils/socket/socket-io'
6import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' 7import { LiveVideo, LiveVideoCreate, Video, VideoDetails, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
7import { 8import {
8 addVideoToBlacklist, 9 addVideoToBlacklist,
9 checkLiveCleanup, 10 checkLiveCleanup,
11 checkLiveSegmentHash,
10 checkResolutionsInMasterPlaylist, 12 checkResolutionsInMasterPlaylist,
13 checkSegmentHash,
11 cleanupTests, 14 cleanupTests,
12 createLive, 15 createLive,
13 doubleFollow, 16 doubleFollow,
14 flushAndRunMultipleServers, 17 flushAndRunMultipleServers,
15 getLive, 18 getLive,
19 getPlaylist,
16 getVideo, 20 getVideo,
17 getVideoIdFromUUID, 21 getVideoIdFromUUID,
18 getVideosList, 22 getVideosList,
23 killallServers,
19 makeRawRequest, 24 makeRawRequest,
20 removeVideo, 25 removeVideo,
26 reRunServer,
21 sendRTMPStream, 27 sendRTMPStream,
22 sendRTMPStreamInVideo, 28 sendRTMPStreamInVideo,
23 ServerInfo, 29 ServerInfo,
@@ -31,9 +37,9 @@ import {
31 viewVideo, 37 viewVideo,
32 wait, 38 wait,
33 waitJobs, 39 waitJobs,
34 waitUntilLiveStarts 40 waitUntilLiveStarts,
41 waitUntilLog
35} from '../../../../shared/extra-utils' 42} from '../../../../shared/extra-utils'
36import { FfmpegCommand } from 'fluent-ffmpeg'
37 43
38const expect = chai.expect 44const expect = chai.expect
39 45
@@ -316,6 +322,19 @@ describe('Test live', function () {
316 expect(hlsPlaylist.files).to.have.lengthOf(0) 322 expect(hlsPlaylist.files).to.have.lengthOf(0)
317 323
318 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions) 324 await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions)
325
326 for (let i = 0; i < resolutions.length; i++) {
327 const segmentName = `${i}-000001.ts`
328 await waitUntilLog(servers[0], `${video.uuid}/${segmentName}`, 1, false)
329
330 const res = await getPlaylist(`${servers[0].url}/static/streaming-playlists/hls/${video.uuid}/${i}.m3u8`)
331 const subPlaylist = res.text
332
333 expect(subPlaylist).to.contain(segmentName)
334
335 const baseUrlAndPath = servers[0].url + '/static/streaming-playlists/hls'
336 await checkLiveSegmentHash(baseUrlAndPath, video.uuid, segmentName, hlsPlaylist)
337 }
319 } 338 }
320 } 339 }
321 340
@@ -580,6 +599,65 @@ describe('Test live', function () {
580 }) 599 })
581 }) 600 })
582 601
602 describe('After a server restart', function () {
603 let liveVideoId: string
604 let liveVideoReplayId: string
605
606 async function createLiveWrapper (saveReplay: boolean) {
607 const liveAttributes = {
608 name: 'live video',
609 channelId: servers[0].videoChannel.id,
610 privacy: VideoPrivacy.PUBLIC,
611 saveReplay
612 }
613
614 const res = await createLive(servers[0].url, servers[0].accessToken, liveAttributes)
615 return res.body.video.uuid
616 }
617
618 before(async function () {
619 this.timeout(60000)
620
621 liveVideoId = await createLiveWrapper(false)
622 liveVideoReplayId = await createLiveWrapper(true)
623
624 await Promise.all([
625 sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoId),
626 sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoReplayId)
627 ])
628
629 await Promise.all([
630 waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoId),
631 waitUntilLiveStarts(servers[0].url, servers[0].accessToken, liveVideoReplayId)
632 ])
633
634 await killallServers([ servers[0] ])
635 await reRunServer(servers[0])
636
637 await wait(5000)
638 })
639
640 it('Should cleanup lives', async function () {
641 this.timeout(60000)
642
643 const res = await getVideo(servers[0].url, liveVideoId)
644 const video: VideoDetails = res.body
645
646 expect(video.state.id).to.equal(VideoState.LIVE_ENDED)
647 })
648
649 it('Should save a live replay', async function () {
650 this.timeout(60000)
651
652 await waitJobs(servers)
653
654 const res = await getVideo(servers[0].url, liveVideoReplayId)
655 const video: VideoDetails = res.body
656
657 expect(video.state.id).to.equal(VideoState.PUBLISHED)
658 })
659 })
660
583 after(async function () { 661 after(async function () {
584 await cleanupTests(servers) 662 await cleanupTests(servers)
585 }) 663 })
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
index 8cf0e4930..b386e77c3 100644
--- a/shared/extra-utils/videos/video-streaming-playlists.ts
+++ b/shared/extra-utils/videos/video-streaming-playlists.ts
@@ -41,6 +41,20 @@ async function checkSegmentHash (
41 expect(sha256(res2.body)).to.equal(sha256Server) 41 expect(sha256(res2.body)).to.equal(sha256Server)
42} 42}
43 43
44async function checkLiveSegmentHash (
45 baseUrlSegment: string,
46 videoUUID: string,
47 segmentName: string,
48 hlsPlaylist: VideoStreamingPlaylist
49) {
50 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
51
52 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
53
54 const sha256Server = resSha.body[segmentName]
55 expect(sha256(res2.body)).to.equal(sha256Server)
56}
57
44async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) { 58async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
45 const res = await getPlaylist(playlistUrl) 59 const res = await getPlaylist(playlistUrl)
46 60
@@ -62,5 +76,6 @@ export {
62 getSegment, 76 getSegment,
63 checkResolutionsInMasterPlaylist, 77 checkResolutionsInMasterPlaylist,
64 getSegmentSha256, 78 getSegmentSha256,
79 checkLiveSegmentHash,
65 checkSegmentHash 80 checkSegmentHash
66} 81}