diff options
author | Chocobozzz <me@florianbigard.com> | 2022-04-05 14:03:52 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2022-04-15 09:49:35 +0200 |
commit | 384ba8b77a8e4805c099f5ea12b41c2ca5776e26 (patch) | |
tree | 6b517033d9265d283677b85e0f57486e0e7fd8cf /client/src/assets | |
parent | b211106695bb82f6c32e53306081b5262c3d109d (diff) | |
download | PeerTube-384ba8b77a8e4805c099f5ea12b41c2ca5776e26.tar.gz PeerTube-384ba8b77a8e4805c099f5ea12b41c2ca5776e26.tar.zst PeerTube-384ba8b77a8e4805c099f5ea12b41c2ca5776e26.zip |
Support videos stats in client
Diffstat (limited to 'client/src/assets')
5 files changed, 75 insertions, 82 deletions
diff --git a/client/src/assets/images/feather/stats.svg b/client/src/assets/images/feather/stats.svg new file mode 100644 index 000000000..864167a6c --- /dev/null +++ b/client/src/assets/images/feather/stats.svg | |||
@@ -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-bar-chart-2"><line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line></svg> \ No newline at end of file | |||
diff --git a/client/src/assets/player/shared/manager-options/manager-options-builder.ts b/client/src/assets/player/shared/manager-options/manager-options-builder.ts index 29e851c1c..e454c719e 100644 --- a/client/src/assets/player/shared/manager-options/manager-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/manager-options-builder.ts | |||
@@ -32,14 +32,18 @@ export class ManagerOptionsBuilder { | |||
32 | peertube: { | 32 | peertube: { |
33 | mode: this.mode, | 33 | mode: this.mode, |
34 | autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent | 34 | autoplay, // Use peertube plugin autoplay because we could get the file by webtorrent |
35 | videoViewUrl: commonOptions.videoViewUrl, | 35 | |
36 | videoDuration: commonOptions.videoDuration, | 36 | ...pick(commonOptions, [ |
37 | userWatching: commonOptions.userWatching, | 37 | 'videoViewUrl', |
38 | subtitle: commonOptions.subtitle, | 38 | 'authorizationHeader', |
39 | videoCaptions: commonOptions.videoCaptions, | 39 | 'startTime', |
40 | stopTime: commonOptions.stopTime, | 40 | 'videoDuration', |
41 | isLive: commonOptions.isLive, | 41 | 'subtitle', |
42 | videoUUID: commonOptions.videoUUID | 42 | 'videoCaptions', |
43 | 'stopTime', | ||
44 | 'isLive', | ||
45 | 'videoUUID' | ||
46 | ]) | ||
43 | } | 47 | } |
44 | } | 48 | } |
45 | 49 | ||
diff --git a/client/src/assets/player/shared/peertube/peertube-plugin.ts b/client/src/assets/player/shared/peertube/peertube-plugin.ts index 1dc3e3de0..8b65903f9 100644 --- a/client/src/assets/player/shared/peertube/peertube-plugin.ts +++ b/client/src/assets/player/shared/peertube/peertube-plugin.ts | |||
@@ -2,6 +2,7 @@ import debug from 'debug' | |||
2 | import videojs from 'video.js' | 2 | import videojs from 'video.js' |
3 | import { isMobile } from '@root-helpers/web-browser' | 3 | import { isMobile } from '@root-helpers/web-browser' |
4 | import { timeToInt } from '@shared/core-utils' | 4 | import { timeToInt } from '@shared/core-utils' |
5 | import { VideoView, VideoViewEvent } from '@shared/models/videos' | ||
5 | import { | 6 | import { |
6 | getStoredLastSubtitle, | 7 | getStoredLastSubtitle, |
7 | getStoredMute, | 8 | getStoredMute, |
@@ -11,7 +12,7 @@ import { | |||
11 | saveVideoWatchHistory, | 12 | saveVideoWatchHistory, |
12 | saveVolumeInStore | 13 | saveVolumeInStore |
13 | } from '../../peertube-player-local-storage' | 14 | } from '../../peertube-player-local-storage' |
14 | import { PeerTubePluginOptions, UserWatching, VideoJSCaption } from '../../types' | 15 | import { PeerTubePluginOptions, VideoJSCaption } from '../../types' |
15 | import { SettingsButton } from '../settings/settings-menu-button' | 16 | import { SettingsButton } from '../settings/settings-menu-button' |
16 | 17 | ||
17 | const logger = debug('peertube:player:peertube') | 18 | const logger = debug('peertube:player:peertube') |
@@ -20,18 +21,19 @@ const Plugin = videojs.getPlugin('plugin') | |||
20 | 21 | ||
21 | class PeerTubePlugin extends Plugin { | 22 | class PeerTubePlugin extends Plugin { |
22 | private readonly videoViewUrl: string | 23 | private readonly videoViewUrl: string |
23 | private readonly videoDuration: number | 24 | private readonly authorizationHeader: string |
25 | |||
26 | private readonly videoUUID: string | ||
27 | private readonly startTime: number | ||
28 | |||
24 | private readonly CONSTANTS = { | 29 | private readonly CONSTANTS = { |
25 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | 30 | USER_VIEW_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video |
26 | } | 31 | } |
27 | 32 | ||
28 | private videoCaptions: VideoJSCaption[] | 33 | private videoCaptions: VideoJSCaption[] |
29 | private defaultSubtitle: string | 34 | private defaultSubtitle: string |
30 | 35 | ||
31 | private videoViewInterval: any | 36 | private videoViewInterval: any |
32 | private userWatchingVideoInterval: any | ||
33 | |||
34 | private isLive: boolean | ||
35 | 37 | ||
36 | private menuOpened = false | 38 | private menuOpened = false |
37 | private mouseInControlBar = false | 39 | private mouseInControlBar = false |
@@ -42,9 +44,11 @@ class PeerTubePlugin extends Plugin { | |||
42 | super(player) | 44 | super(player) |
43 | 45 | ||
44 | this.videoViewUrl = options.videoViewUrl | 46 | this.videoViewUrl = options.videoViewUrl |
45 | this.videoDuration = options.videoDuration | 47 | this.authorizationHeader = options.authorizationHeader |
48 | this.videoUUID = options.videoUUID | ||
49 | this.startTime = timeToInt(options.startTime) | ||
50 | |||
46 | this.videoCaptions = options.videoCaptions | 51 | this.videoCaptions = options.videoCaptions |
47 | this.isLive = options.isLive | ||
48 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout | 52 | this.initialInactivityTimeout = this.player.options_.inactivityTimeout |
49 | 53 | ||
50 | if (options.autoplay) this.player.addClass('vjs-has-autoplay') | 54 | if (options.autoplay) this.player.addClass('vjs-has-autoplay') |
@@ -101,15 +105,12 @@ class PeerTubePlugin extends Plugin { | |||
101 | this.player.duration(options.videoDuration) | 105 | this.player.duration(options.videoDuration) |
102 | 106 | ||
103 | this.initializePlayer() | 107 | this.initializePlayer() |
104 | this.runViewAdd() | 108 | this.runUserViewing() |
105 | |||
106 | this.runUserWatchVideo(options.userWatching, options.videoUUID) | ||
107 | }) | 109 | }) |
108 | } | 110 | } |
109 | 111 | ||
110 | dispose () { | 112 | dispose () { |
111 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) | 113 | if (this.videoViewInterval) clearInterval(this.videoViewInterval) |
112 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
113 | } | 114 | } |
114 | 115 | ||
115 | onMenuOpened () { | 116 | onMenuOpened () { |
@@ -142,74 +143,65 @@ class PeerTubePlugin extends Plugin { | |||
142 | this.listenFullScreenChange() | 143 | this.listenFullScreenChange() |
143 | } | 144 | } |
144 | 145 | ||
145 | private runViewAdd () { | 146 | private runUserViewing () { |
146 | this.clearVideoViewInterval() | 147 | let lastCurrentTime = this.startTime |
148 | let lastViewEvent: VideoViewEvent | ||
147 | 149 | ||
148 | // After 30 seconds (or 3/4 of the video), add a view to the video | 150 | this.player.one('play', () => { |
149 | let minSecondsToView = 30 | 151 | this.notifyUserIsWatching(this.startTime, lastViewEvent) |
152 | }) | ||
150 | 153 | ||
151 | if (!this.isLive && this.videoDuration < minSecondsToView) { | 154 | this.player.on('seeked', () => { |
152 | minSecondsToView = (this.videoDuration * 3) / 4 | 155 | // Don't take into account small seek events |
153 | } | 156 | if (Math.abs(this.player.currentTime() - lastCurrentTime) < 3) return |
154 | 157 | ||
155 | let secondsViewed = 0 | 158 | lastViewEvent = 'seek' |
156 | this.videoViewInterval = setInterval(() => { | 159 | }) |
157 | if (this.player && !this.player.paused()) { | ||
158 | secondsViewed += 1 | ||
159 | |||
160 | if (secondsViewed > minSecondsToView) { | ||
161 | // Restart the loop if this is a live | ||
162 | if (this.isLive) { | ||
163 | secondsViewed = 0 | ||
164 | } else { | ||
165 | this.clearVideoViewInterval() | ||
166 | } | ||
167 | 160 | ||
168 | this.addViewToVideo().catch(err => console.error(err)) | 161 | this.player.one('ended', () => { |
169 | } | 162 | const currentTime = Math.floor(this.player.duration()) |
170 | } | 163 | lastCurrentTime = currentTime |
171 | }, 1000) | ||
172 | } | ||
173 | 164 | ||
174 | private runUserWatchVideo (options: UserWatching, videoUUID: string) { | 165 | this.notifyUserIsWatching(currentTime, lastViewEvent) |
175 | let lastCurrentTime = 0 | ||
176 | 166 | ||
177 | this.userWatchingVideoInterval = setInterval(() => { | 167 | lastViewEvent = undefined |
168 | }) | ||
169 | |||
170 | this.videoViewInterval = setInterval(() => { | ||
178 | const currentTime = Math.floor(this.player.currentTime()) | 171 | const currentTime = Math.floor(this.player.currentTime()) |
179 | 172 | ||
180 | if (currentTime - lastCurrentTime >= 1) { | 173 | // No need to update |
181 | lastCurrentTime = currentTime | 174 | if (currentTime === lastCurrentTime) return |
182 | 175 | ||
183 | if (options) { | 176 | lastCurrentTime = currentTime |
184 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
185 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
186 | } else { | ||
187 | saveVideoWatchHistory(videoUUID, currentTime) | ||
188 | } | ||
189 | } | ||
190 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
191 | } | ||
192 | 177 | ||
193 | private clearVideoViewInterval () { | 178 | this.notifyUserIsWatching(currentTime, lastViewEvent) |
194 | if (this.videoViewInterval !== undefined) { | 179 | .catch(err => console.error('Cannot notify user is watching.', err)) |
195 | clearInterval(this.videoViewInterval) | 180 | |
196 | this.videoViewInterval = undefined | 181 | lastViewEvent = undefined |
197 | } | 182 | |
183 | // Server won't save history, so save the video position in local storage | ||
184 | if (!this.authorizationHeader) { | ||
185 | saveVideoWatchHistory(this.videoUUID, currentTime) | ||
186 | } | ||
187 | }, this.CONSTANTS.USER_VIEW_VIDEO_INTERVAL) | ||
198 | } | 188 | } |
199 | 189 | ||
200 | private addViewToVideo () { | 190 | private notifyUserIsWatching (currentTime: number, viewEvent: VideoViewEvent) { |
201 | if (!this.videoViewUrl) return Promise.resolve(undefined) | 191 | if (!this.videoViewUrl) return Promise.resolve(undefined) |
202 | 192 | ||
203 | return fetch(this.videoViewUrl, { method: 'POST' }) | 193 | const body: VideoView = { |
204 | } | 194 | currentTime, |
195 | viewEvent | ||
196 | } | ||
205 | 197 | ||
206 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | 198 | const headers = new Headers({ |
207 | const body = new URLSearchParams() | 199 | 'Content-type': 'application/json; charset=UTF-8' |
208 | body.append('currentTime', currentTime.toString()) | 200 | }) |
209 | 201 | ||
210 | const headers = new Headers({ Authorization: authorizationHeader }) | 202 | if (this.authorizationHeader) headers.set('Authorization', this.authorizationHeader) |
211 | 203 | ||
212 | return fetch(url, { method: 'PUT', body, headers }) | 204 | return fetch(this.videoViewUrl, { method: 'POST', body: JSON.stringify(body), headers }) |
213 | } | 205 | } |
214 | 206 | ||
215 | private listenFullScreenChange () { | 207 | private listenFullScreenChange () { |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index b3ad7e337..456ef115e 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { PluginsManager } from '@root-helpers/plugins-manager' | 1 | import { PluginsManager } from '@root-helpers/plugins-manager' |
2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' | 2 | import { LiveVideoLatencyMode, VideoFile } from '@shared/models' |
3 | import { PlaylistPluginOptions, UserWatching, VideoJSCaption } from './peertube-videojs-typings' | 3 | import { PlaylistPluginOptions, VideoJSCaption } from './peertube-videojs-typings' |
4 | 4 | ||
5 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | 5 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' |
6 | 6 | ||
@@ -53,6 +53,8 @@ export interface CommonOptions extends CustomizationOptions { | |||
53 | captions: boolean | 53 | captions: boolean |
54 | 54 | ||
55 | videoViewUrl: string | 55 | videoViewUrl: string |
56 | authorizationHeader?: string | ||
57 | |||
56 | embedUrl: string | 58 | embedUrl: string |
57 | embedTitle: string | 59 | embedTitle: string |
58 | 60 | ||
@@ -68,8 +70,6 @@ export interface CommonOptions extends CustomizationOptions { | |||
68 | videoUUID: string | 70 | videoUUID: string |
69 | videoShortUUID: string | 71 | videoShortUUID: string |
70 | 72 | ||
71 | userWatching?: UserWatching | ||
72 | |||
73 | serverUrl: string | 73 | serverUrl: string |
74 | 74 | ||
75 | errorNotifier: (message: string) => void | 75 | errorNotifier: (message: string) => void |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index d9a388681..ad284a671 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -88,23 +88,20 @@ type VideoJSCaption = { | |||
88 | src: string | 88 | src: string |
89 | } | 89 | } |
90 | 90 | ||
91 | type UserWatching = { | ||
92 | url: string | ||
93 | authorizationHeader: string | ||
94 | } | ||
95 | |||
96 | type PeerTubePluginOptions = { | 91 | type PeerTubePluginOptions = { |
97 | mode: PlayerMode | 92 | mode: PlayerMode |
98 | 93 | ||
99 | autoplay: boolean | 94 | autoplay: boolean |
100 | videoViewUrl: string | ||
101 | videoDuration: number | 95 | videoDuration: number |
102 | 96 | ||
103 | userWatching?: UserWatching | 97 | videoViewUrl: string |
98 | authorizationHeader?: string | ||
99 | |||
104 | subtitle?: string | 100 | subtitle?: string |
105 | 101 | ||
106 | videoCaptions: VideoJSCaption[] | 102 | videoCaptions: VideoJSCaption[] |
107 | 103 | ||
104 | startTime: number | string | ||
108 | stopTime: number | string | 105 | stopTime: number | string |
109 | 106 | ||
110 | isLive: boolean | 107 | isLive: boolean |
@@ -230,7 +227,6 @@ export { | |||
230 | AutoResolutionUpdateData, | 227 | AutoResolutionUpdateData, |
231 | PlaylistPluginOptions, | 228 | PlaylistPluginOptions, |
232 | VideoJSCaption, | 229 | VideoJSCaption, |
233 | UserWatching, | ||
234 | PeerTubePluginOptions, | 230 | PeerTubePluginOptions, |
235 | WebtorrentPluginOptions, | 231 | WebtorrentPluginOptions, |
236 | P2PMediaLoaderPluginOptions, | 232 | P2PMediaLoaderPluginOptions, |