diff options
author | Chocobozzz <me@florianbigard.com> | 2021-11-09 10:11:20 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-11-09 15:00:31 +0100 |
commit | 51353d9a035fb6b81f903a8b5f391292841649fd (patch) | |
tree | 75acb6eea5e043bf2e15a6a5a92e9a3c5967b156 /server/lib/video-views.ts | |
parent | 221ee1adc916684d4881d2a9c4c01954dcde986e (diff) | |
download | PeerTube-51353d9a035fb6b81f903a8b5f391292841649fd.tar.gz PeerTube-51353d9a035fb6b81f903a8b5f391292841649fd.tar.zst PeerTube-51353d9a035fb6b81f903a8b5f391292841649fd.zip |
Refactor video views
Introduce viewers attribute for live videos
Count views for live videos
Reduce delay to see the viewer update for lives
Add ability to configure video views buffer interval and view ip
expiration
Diffstat (limited to 'server/lib/video-views.ts')
-rw-r--r-- | server/lib/video-views.ts | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/server/lib/video-views.ts b/server/lib/video-views.ts new file mode 100644 index 000000000..220b509c2 --- /dev/null +++ b/server/lib/video-views.ts | |||
@@ -0,0 +1,130 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
2 | import { VIEW_LIFETIME } from '@server/initializers/constants' | ||
3 | import { VideoModel } from '@server/models/video/video' | ||
4 | import { MVideo } from '@server/types/models' | ||
5 | import { PeerTubeSocket } from './peertube-socket' | ||
6 | import { Redis } from './redis' | ||
7 | |||
8 | const lTags = loggerTagsFactory('views') | ||
9 | |||
10 | export class VideoViews { | ||
11 | |||
12 | // Values are Date().getTime() | ||
13 | private readonly viewersPerVideo = new Map<number, number[]>() | ||
14 | |||
15 | private static instance: VideoViews | ||
16 | |||
17 | private constructor () { | ||
18 | } | ||
19 | |||
20 | init () { | ||
21 | setInterval(() => this.cleanViewers(), VIEW_LIFETIME.VIEWER) | ||
22 | } | ||
23 | |||
24 | async processView (options: { | ||
25 | video: MVideo | ||
26 | ip: string | null | ||
27 | viewerExpires?: Date | ||
28 | }) { | ||
29 | const { video, ip, viewerExpires } = options | ||
30 | |||
31 | logger.debug('Processing view for %s and ip %s.', video.url, ip, lTags()) | ||
32 | |||
33 | let success = await this.addView(video, ip) | ||
34 | |||
35 | if (video.isLive) { | ||
36 | const successViewer = await this.addViewer(video, ip, viewerExpires) | ||
37 | success ||= successViewer | ||
38 | } | ||
39 | |||
40 | return success | ||
41 | } | ||
42 | |||
43 | getViewers (video: MVideo) { | ||
44 | const viewers = this.viewersPerVideo.get(video.id) | ||
45 | if (!viewers) return 0 | ||
46 | |||
47 | return viewers.length | ||
48 | } | ||
49 | |||
50 | buildViewerExpireTime () { | ||
51 | return new Date().getTime() + VIEW_LIFETIME.VIEWER | ||
52 | } | ||
53 | |||
54 | private async addView (video: MVideo, ip: string | null) { | ||
55 | const promises: Promise<any>[] = [] | ||
56 | |||
57 | if (ip !== null) { | ||
58 | const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid) | ||
59 | if (viewExists) return false | ||
60 | |||
61 | promises.push(Redis.Instance.setIPVideoView(ip, video.uuid)) | ||
62 | } | ||
63 | |||
64 | if (video.isOwned()) { | ||
65 | promises.push(Redis.Instance.addLocalVideoView(video.id)) | ||
66 | } | ||
67 | |||
68 | promises.push(Redis.Instance.addVideoViewStats(video.id)) | ||
69 | |||
70 | await Promise.all(promises) | ||
71 | |||
72 | return true | ||
73 | } | ||
74 | |||
75 | private async addViewer (video: MVideo, ip: string | null, viewerExpires?: Date) { | ||
76 | if (ip !== null) { | ||
77 | const viewExists = await Redis.Instance.doesVideoIPViewerExist(ip, video.uuid) | ||
78 | if (viewExists) return false | ||
79 | |||
80 | await Redis.Instance.setIPVideoViewer(ip, video.uuid) | ||
81 | } | ||
82 | |||
83 | let watchers = this.viewersPerVideo.get(video.id) | ||
84 | |||
85 | if (!watchers) { | ||
86 | watchers = [] | ||
87 | this.viewersPerVideo.set(video.id, watchers) | ||
88 | } | ||
89 | |||
90 | const expiration = viewerExpires | ||
91 | ? viewerExpires.getTime() | ||
92 | : this.buildViewerExpireTime() | ||
93 | |||
94 | watchers.push(expiration) | ||
95 | await this.notifyClients(video.id, watchers.length) | ||
96 | |||
97 | return true | ||
98 | } | ||
99 | |||
100 | private async cleanViewers () { | ||
101 | logger.info('Cleaning video viewers.', lTags()) | ||
102 | |||
103 | for (const videoId of this.viewersPerVideo.keys()) { | ||
104 | const notBefore = new Date().getTime() | ||
105 | |||
106 | const viewers = this.viewersPerVideo.get(videoId) | ||
107 | |||
108 | // Only keep not expired viewers | ||
109 | const newViewers = viewers.filter(w => w > notBefore) | ||
110 | |||
111 | if (newViewers.length === 0) this.viewersPerVideo.delete(videoId) | ||
112 | else this.viewersPerVideo.set(videoId, newViewers) | ||
113 | |||
114 | await this.notifyClients(videoId, newViewers.length) | ||
115 | } | ||
116 | } | ||
117 | |||
118 | private async notifyClients (videoId: string | number, viewersLength: number) { | ||
119 | const video = await VideoModel.loadImmutableAttributes(videoId) | ||
120 | if (!video) return | ||
121 | |||
122 | PeerTubeSocket.Instance.sendVideoViewsUpdate(video, viewersLength) | ||
123 | |||
124 | logger.debug('Live video views update for %s is %d.', video.url, viewersLength, lTags()) | ||
125 | } | ||
126 | |||
127 | static get Instance () { | ||
128 | return this.instance || (this.instance = new this()) | ||
129 | } | ||
130 | } | ||