diff options
-rw-r--r-- | client/src/app/+videos/+video-watch/video-watch.component.ts | 50 | ||||
-rw-r--r-- | client/src/app/core/notification/peertube-socket.service.ts | 7 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 9 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 2 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 2 | ||||
-rw-r--r-- | server/lib/peertube-socket.ts | 13 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 51 | ||||
-rw-r--r-- | shared/models/videos/live/live-video-event-payload.model.ts | 3 | ||||
-rw-r--r-- | shared/models/videos/live/live-video-event.type.ts | 2 |
9 files changed, 114 insertions, 25 deletions
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 33de901c0..7eb56eb48 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -25,6 +25,7 @@ import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/sha | |||
25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 25 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
26 | import { MetaService } from '@ngx-meta/core' | 26 | import { MetaService } from '@ngx-meta/core' |
27 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' | 27 | import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' |
28 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
28 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' | 29 | import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' |
29 | import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' | 30 | import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' |
30 | import { | 31 | import { |
@@ -39,7 +40,6 @@ import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' | |||
39 | import { environment } from '../../../environments/environment' | 40 | import { environment } from '../../../environments/environment' |
40 | import { VideoSupportComponent } from './modal/video-support.component' | 41 | import { VideoSupportComponent } from './modal/video-support.component' |
41 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' | 42 | import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' |
42 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
43 | 43 | ||
44 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } | 44 | type URLOptions = CustomizationOptions & { playerMode: PlayerMode } |
45 | 45 | ||
@@ -866,21 +866,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
866 | 866 | ||
867 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { | 867 | private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { |
868 | if (!this.liveVideosSub) { | 868 | if (!this.liveVideosSub) { |
869 | this.liveVideosSub = this.peertubeSocket.getLiveVideosObservable() | 869 | this.liveVideosSub = this.buildLiveEventsSubscription() |
870 | .subscribe(({ payload }) => { | ||
871 | if (payload.state !== VideoState.PUBLISHED) return | ||
872 | |||
873 | const videoState = this.video.state.id | ||
874 | if (videoState !== VideoState.WAITING_FOR_LIVE && videoState !== VideoState.LIVE_ENDED) return | ||
875 | |||
876 | console.log('Loading video after live update.') | ||
877 | |||
878 | const videoUUID = this.video.uuid | ||
879 | |||
880 | // Reset to refetch the video | ||
881 | this.video = undefined | ||
882 | this.loadVideo(videoUUID) | ||
883 | }) | ||
884 | } | 870 | } |
885 | 871 | ||
886 | if (oldVideo && oldVideo.id !== newVideo.id) { | 872 | if (oldVideo && oldVideo.id !== newVideo.id) { |
@@ -892,6 +878,38 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
892 | await this.peertubeSocket.subscribeToLiveVideosSocket(newVideo.id) | 878 | await this.peertubeSocket.subscribeToLiveVideosSocket(newVideo.id) |
893 | } | 879 | } |
894 | 880 | ||
881 | private buildLiveEventsSubscription () { | ||
882 | return this.peertubeSocket.getLiveVideosObservable() | ||
883 | .subscribe(({ type, payload }) => { | ||
884 | if (type === 'state-change') return this.handleLiveStateChange(payload.state) | ||
885 | if (type === 'views-change') return this.handleLiveViewsChange(payload.views) | ||
886 | }) | ||
887 | } | ||
888 | |||
889 | private handleLiveStateChange (newState: VideoState) { | ||
890 | if (newState !== VideoState.PUBLISHED) return | ||
891 | |||
892 | const videoState = this.video.state.id | ||
893 | if (videoState !== VideoState.WAITING_FOR_LIVE && videoState !== VideoState.LIVE_ENDED) return | ||
894 | |||
895 | console.log('Loading video after live update.') | ||
896 | |||
897 | const videoUUID = this.video.uuid | ||
898 | |||
899 | // Reset to refetch the video | ||
900 | this.video = undefined | ||
901 | this.loadVideo(videoUUID) | ||
902 | } | ||
903 | |||
904 | private handleLiveViewsChange (newViews: number) { | ||
905 | if (!this.video) { | ||
906 | console.error('Cannot update video live views because video is no defined.') | ||
907 | return | ||
908 | } | ||
909 | |||
910 | this.video.views = newViews | ||
911 | } | ||
912 | |||
895 | private initHotkeys () { | 913 | private initHotkeys () { |
896 | this.hotkeys = [ | 914 | this.hotkeys = [ |
897 | // These hotkeys are managed by the player | 915 | // These hotkeys are managed by the player |
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts index 7e1c43364..089276cfc 100644 --- a/client/src/app/core/notification/peertube-socket.service.ts +++ b/client/src/app/core/notification/peertube-socket.service.ts | |||
@@ -73,8 +73,11 @@ export class PeerTubeSocket { | |||
73 | this.liveVideosSocket = this.io(environment.apiUrl + '/live-videos') | 73 | this.liveVideosSocket = this.io(environment.apiUrl + '/live-videos') |
74 | }) | 74 | }) |
75 | 75 | ||
76 | const type: LiveVideoEventType = 'state-change' | 76 | const types: LiveVideoEventType[] = [ 'views-change', 'state-change' ] |
77 | this.liveVideosSocket.on(type, (payload: LiveVideoEventPayload) => this.dispatchLiveVideoEvent(type, payload)) | 77 | |
78 | for (const type of types) { | ||
79 | this.liveVideosSocket.on(type, (payload: LiveVideoEventPayload) => this.dispatchLiveVideoEvent(type, payload)) | ||
80 | } | ||
78 | } | 81 | } |
79 | 82 | ||
80 | private async importIOIfNeeded () { | 83 | private async importIOIfNeeded () { |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index cb462e258..8545e5bad 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -461,8 +461,13 @@ async function updateVideoFromAP (options: { | |||
461 | transaction: undefined | 461 | transaction: undefined |
462 | }) | 462 | }) |
463 | 463 | ||
464 | if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users? | 464 | // Notify our users? |
465 | if (videoUpdated.isLive) PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) | 465 | if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) |
466 | |||
467 | if (videoUpdated.isLive) { | ||
468 | PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) | ||
469 | PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated) | ||
470 | } | ||
466 | 471 | ||
467 | logger.info('Remote video with uuid %s updated', videoObject.uuid) | 472 | logger.info('Remote video with uuid %s updated', videoObject.uuid) |
468 | 473 | ||
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 93d925830..8018e2277 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -91,7 +91,7 @@ async function saveLive (video: MVideo, live: MVideoLive) { | |||
91 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) | 91 | await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) |
92 | hlsPlaylist.VideoFiles = [] | 92 | hlsPlaylist.VideoFiles = [] |
93 | 93 | ||
94 | let durationDone: boolean | 94 | let durationDone = false |
95 | 95 | ||
96 | for (const playlistFile of playlistFiles) { | 96 | for (const playlistFile of playlistFiles) { |
97 | const concatenatedTsFile = LiveManager.Instance.buildConcatenatedName(playlistFile) | 97 | const concatenatedTsFile = LiveManager.Instance.buildConcatenatedName(playlistFile) |
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts index 5d9b68756..2fb4b774c 100644 --- a/server/lib/live-manager.ts +++ b/server/lib/live-manager.ts | |||
@@ -537,6 +537,8 @@ class LiveManager { | |||
537 | 537 | ||
538 | await federateVideoIfNeeded(video, false) | 538 | await federateVideoIfNeeded(video, false) |
539 | 539 | ||
540 | PeerTubeSocket.Instance.sendVideoViewsUpdate(video) | ||
541 | |||
540 | // Only keep not expired watchers | 542 | // Only keep not expired watchers |
541 | const newWatchers = watchers.filter(w => w > notBefore) | 543 | const newWatchers = watchers.filter(w => w > notBefore) |
542 | this.watchersPerVideo.set(videoId, newWatchers) | 544 | this.watchersPerVideo.set(videoId, newWatchers) |
diff --git a/server/lib/peertube-socket.ts b/server/lib/peertube-socket.ts index 5fc5bc20b..e27963e60 100644 --- a/server/lib/peertube-socket.ts +++ b/server/lib/peertube-socket.ts | |||
@@ -69,7 +69,18 @@ class PeerTubeSocket { | |||
69 | const data: LiveVideoEventPayload = { state: video.state } | 69 | const data: LiveVideoEventPayload = { state: video.state } |
70 | const type: LiveVideoEventType = 'state-change' | 70 | const type: LiveVideoEventType = 'state-change' |
71 | 71 | ||
72 | logger.debug('Sending video live new state notification of %s.', video.url) | 72 | logger.debug('Sending video live new state notification of %s.', video.url, { state: video.state }) |
73 | |||
74 | this.liveVideosNamespace | ||
75 | .in(video.id) | ||
76 | .emit(type, data) | ||
77 | } | ||
78 | |||
79 | sendVideoViewsUpdate (video: MVideo) { | ||
80 | const data: LiveVideoEventPayload = { views: video.views } | ||
81 | const type: LiveVideoEventType = 'views-change' | ||
82 | |||
83 | logger.debug('Sending video live views update notification of %s.', video.url, { views: video.views }) | ||
73 | 84 | ||
74 | this.liveVideosNamespace | 85 | this.liveVideosNamespace |
75 | .in(video.id) | 86 | .in(video.id) |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index e728fcce0..6d504f742 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -328,7 +328,7 @@ describe('Test live', function () { | |||
328 | await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions) | 328 | await checkResolutionsInMasterPlaylist(hlsPlaylist.playlistUrl, resolutions) |
329 | 329 | ||
330 | for (let i = 0; i < resolutions.length; i++) { | 330 | for (let i = 0; i < resolutions.length; i++) { |
331 | const segmentNum = 2 | 331 | const segmentNum = 3 |
332 | const segmentName = `${i}-00000${segmentNum}.ts` | 332 | const segmentName = `${i}-00000${segmentNum}.ts` |
333 | await waitUntilLiveSegmentGeneration(servers[0], video.uuid, i, segmentNum) | 333 | await waitUntilLiveSegmentGeneration(servers[0], video.uuid, i, segmentNum) |
334 | 334 | ||
@@ -608,6 +608,55 @@ describe('Test live', function () { | |||
608 | } | 608 | } |
609 | }) | 609 | }) |
610 | 610 | ||
611 | it('Should correctly send views change notification', async function () { | ||
612 | this.timeout(60000) | ||
613 | |||
614 | let localLastVideoViews = 0 | ||
615 | let remoteLastVideoViews = 0 | ||
616 | |||
617 | const liveVideoUUID = await createLiveWrapper() | ||
618 | await waitJobs(servers) | ||
619 | |||
620 | { | ||
621 | const videoId = await getVideoIdFromUUID(servers[0].url, liveVideoUUID) | ||
622 | |||
623 | const localSocket = getLiveNotificationSocket(servers[0].url) | ||
624 | localSocket.on('views-change', data => { localLastVideoViews = data.views }) | ||
625 | localSocket.emit('subscribe', { videoId }) | ||
626 | } | ||
627 | |||
628 | { | ||
629 | const videoId = await getVideoIdFromUUID(servers[1].url, liveVideoUUID) | ||
630 | |||
631 | const remoteSocket = getLiveNotificationSocket(servers[1].url) | ||
632 | remoteSocket.on('views-change', data => { remoteLastVideoViews = data.views }) | ||
633 | remoteSocket.emit('subscribe', { videoId }) | ||
634 | } | ||
635 | |||
636 | const command = await sendRTMPStreamInVideo(servers[0].url, servers[0].accessToken, liveVideoUUID) | ||
637 | |||
638 | for (const server of servers) { | ||
639 | await waitUntilLivePublished(server.url, server.accessToken, liveVideoUUID) | ||
640 | } | ||
641 | |||
642 | await waitJobs(servers) | ||
643 | |||
644 | expect(localLastVideoViews).to.equal(0) | ||
645 | expect(remoteLastVideoViews).to.equal(0) | ||
646 | |||
647 | await viewVideo(servers[0].url, liveVideoUUID) | ||
648 | await viewVideo(servers[1].url, liveVideoUUID) | ||
649 | |||
650 | await waitJobs(servers) | ||
651 | await wait(5000) | ||
652 | await waitJobs(servers) | ||
653 | |||
654 | expect(localLastVideoViews).to.equal(2) | ||
655 | expect(remoteLastVideoViews).to.equal(2) | ||
656 | |||
657 | await stopFfmpeg(command) | ||
658 | }) | ||
659 | |||
611 | it('Should not receive a notification after unsubscribe', async function () { | 660 | it('Should not receive a notification after unsubscribe', async function () { |
612 | this.timeout(60000) | 661 | this.timeout(60000) |
613 | 662 | ||
diff --git a/shared/models/videos/live/live-video-event-payload.model.ts b/shared/models/videos/live/live-video-event-payload.model.ts index f9038f4de..6cd7540e8 100644 --- a/shared/models/videos/live/live-video-event-payload.model.ts +++ b/shared/models/videos/live/live-video-event-payload.model.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { VideoState } from '../video-state.enum' | 1 | import { VideoState } from '../video-state.enum' |
2 | 2 | ||
3 | export interface LiveVideoEventPayload { | 3 | export interface LiveVideoEventPayload { |
4 | state: VideoState | 4 | state?: VideoState |
5 | views?: number | ||
5 | } | 6 | } |
diff --git a/shared/models/videos/live/live-video-event.type.ts b/shared/models/videos/live/live-video-event.type.ts index 4d15899da..50f794561 100644 --- a/shared/models/videos/live/live-video-event.type.ts +++ b/shared/models/videos/live/live-video-event.type.ts | |||
@@ -1 +1 @@ | |||
export type LiveVideoEventType = 'state-change' | export type LiveVideoEventType = 'state-change' | 'views-change' | ||