mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-02-25 18:55:32 -06:00
Use sessionId instead of IP to identify viewer
Breaking: YAML config `ip_view_expiration` is renamed `view_expiration` Breaking: Views are taken into account after 10 seconds instead of 30 seconds (can be changed in YAML config) Purpose of this commit is to get closer to other video platforms where some platforms count views on play (mux, vimeo) or others use a very low delay (instagram, tiktok) We also want to improve the viewer identification, where we no longer use the IP but the `sessionId` generated by the web browser. Multiple viewers behind a NAT can now be able to be identified as independent viewers (this method is also used by vimeo or mux)
This commit is contained in:
parent
6f6abcabfb
commit
5cb3e6a0b8
@ -1,6 +1,8 @@
|
|||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
|
import { peertubeLocalStorage, peertubeSessionStorage } from '@root-helpers/peertube-web-storage'
|
||||||
|
import { randomString } from '@root-helpers/string'
|
||||||
|
|
||||||
function getStoredVolume () {
|
export function getStoredVolume () {
|
||||||
const value = getLocalStorage('volume')
|
const value = getLocalStorage('volume')
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
const valueNumber = parseFloat(value)
|
const valueNumber = parseFloat(value)
|
||||||
@ -12,38 +14,38 @@ function getStoredVolume () {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredMute () {
|
export function getStoredMute () {
|
||||||
const value = getLocalStorage('mute')
|
const value = getLocalStorage('mute')
|
||||||
if (value !== null && value !== undefined) return value === 'true'
|
if (value !== null && value !== undefined) return value === 'true'
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredTheater () {
|
export function getStoredTheater () {
|
||||||
const value = getLocalStorage('theater-enabled')
|
const value = getLocalStorage('theater-enabled')
|
||||||
if (value !== null && value !== undefined) return value === 'true'
|
if (value !== null && value !== undefined) return value === 'true'
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveVolumeInStore (value: number) {
|
export function saveVolumeInStore (value: number) {
|
||||||
return setLocalStorage('volume', value.toString())
|
return setLocalStorage('volume', value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMuteInStore (value: boolean) {
|
export function saveMuteInStore (value: boolean) {
|
||||||
return setLocalStorage('mute', value.toString())
|
return setLocalStorage('mute', value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTheaterInStore (enabled: boolean) {
|
export function saveTheaterInStore (enabled: boolean) {
|
||||||
return setLocalStorage('theater-enabled', enabled.toString())
|
return setLocalStorage('theater-enabled', enabled.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveAverageBandwidth (value: number) {
|
export function saveAverageBandwidth (value: number) {
|
||||||
/** used to choose the most fitting resolution */
|
/** used to choose the most fitting resolution */
|
||||||
return setLocalStorage('average-bandwidth', value.toString())
|
return setLocalStorage('average-bandwidth', value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAverageBandwidthInStore () {
|
export function getAverageBandwidthInStore () {
|
||||||
const value = getLocalStorage('average-bandwidth')
|
const value = getLocalStorage('average-bandwidth')
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
const valueNumber = parseInt(value, 10)
|
const valueNumber = parseInt(value, 10)
|
||||||
@ -57,25 +59,25 @@ function getAverageBandwidthInStore () {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function saveLastSubtitle (language: string) {
|
export function saveLastSubtitle (language: string) {
|
||||||
return setLocalStorage('last-subtitle', language)
|
return setLocalStorage('last-subtitle', language)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredLastSubtitle () {
|
export function getStoredLastSubtitle () {
|
||||||
return getLocalStorage('last-subtitle')
|
return getLocalStorage('last-subtitle')
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePreferredSubtitle (language: string) {
|
export function savePreferredSubtitle (language: string) {
|
||||||
return setLocalStorage('preferred-subtitle', language)
|
return setLocalStorage('preferred-subtitle', language)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredPreferredSubtitle () {
|
export function getStoredPreferredSubtitle () {
|
||||||
return getLocalStorage('preferred-subtitle')
|
return getLocalStorage('preferred-subtitle')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function saveVideoWatchHistory (videoUUID: string, duration: number) {
|
export function saveVideoWatchHistory (videoUUID: string, duration: number) {
|
||||||
return setLocalStorage(`video-watch-history`, JSON.stringify({
|
return setLocalStorage(`video-watch-history`, JSON.stringify({
|
||||||
...getStoredVideoWatchHistory(),
|
...getStoredVideoWatchHistory(),
|
||||||
|
|
||||||
@ -86,7 +88,7 @@ function saveVideoWatchHistory (videoUUID: string, duration: number) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredVideoWatchHistory (videoUUID?: string) {
|
export function getStoredVideoWatchHistory (videoUUID?: string) {
|
||||||
let data
|
let data
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -105,7 +107,7 @@ function getStoredVideoWatchHistory (videoUUID?: string) {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupVideoWatch () {
|
export function cleanupVideoWatch () {
|
||||||
const data = getStoredVideoWatchHistory()
|
const data = getStoredVideoWatchHistory()
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
@ -127,39 +129,36 @@ function cleanupVideoWatch () {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export function getPlayerSessionId () {
|
||||||
getStoredVolume,
|
const key = 'session-id'
|
||||||
getStoredMute,
|
|
||||||
getStoredTheater,
|
let sessionId = getSessionStorage(key)
|
||||||
saveVolumeInStore,
|
if (sessionId) return sessionId
|
||||||
saveMuteInStore,
|
|
||||||
saveTheaterInStore,
|
sessionId = randomString(32)
|
||||||
saveAverageBandwidth,
|
setSessionStorage(key, sessionId)
|
||||||
getAverageBandwidthInStore,
|
|
||||||
saveLastSubtitle,
|
return sessionId
|
||||||
getStoredLastSubtitle,
|
|
||||||
saveVideoWatchHistory,
|
|
||||||
getStoredVideoWatchHistory,
|
|
||||||
cleanupVideoWatch,
|
|
||||||
savePreferredSubtitle,
|
|
||||||
getStoredPreferredSubtitle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const KEY_PREFIX = 'peertube-videojs-'
|
const KEY_PREFIX = 'peertube-videojs-'
|
||||||
|
|
||||||
function getLocalStorage (key: string) {
|
function getLocalStorage (key: string) {
|
||||||
try {
|
return peertubeLocalStorage.getItem(KEY_PREFIX + key)
|
||||||
return localStorage.getItem(KEY_PREFIX + key)
|
|
||||||
} catch {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLocalStorage (key: string, value: string) {
|
function setLocalStorage (key: string, value: string) {
|
||||||
try {
|
peertubeLocalStorage.setItem(KEY_PREFIX + key, value)
|
||||||
localStorage.setItem(KEY_PREFIX + key, value)
|
|
||||||
} catch { /* empty */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSessionStorage (key: string) {
|
||||||
|
return peertubeSessionStorage.getItem(KEY_PREFIX + key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSessionStorage (key: string, value: string) {
|
||||||
|
peertubeSessionStorage.setItem(KEY_PREFIX + key, value)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import debug from 'debug'
|
|
||||||
import videojs from 'video.js'
|
|
||||||
import { timeToInt } from '@peertube/peertube-core-utils'
|
import { timeToInt } from '@peertube/peertube-core-utils'
|
||||||
import { VideoView, VideoViewEvent } from '@peertube/peertube-models'
|
import { VideoView, VideoViewEvent } from '@peertube/peertube-models'
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
import { isIOS, isMobile, isSafari } from '@root-helpers/web-browser'
|
import { isIOS, isMobile, isSafari } from '@root-helpers/web-browser'
|
||||||
|
import debug from 'debug'
|
||||||
|
import videojs from 'video.js'
|
||||||
import {
|
import {
|
||||||
|
getPlayerSessionId,
|
||||||
getStoredLastSubtitle,
|
getStoredLastSubtitle,
|
||||||
getStoredMute,
|
getStoredMute,
|
||||||
getStoredVolume,
|
getStoredVolume,
|
||||||
@ -371,7 +372,9 @@ class PeerTubePlugin extends Plugin {
|
|||||||
|
|
||||||
if (!this.videoViewUrl()) return Promise.resolve(true)
|
if (!this.videoViewUrl()) return Promise.resolve(true)
|
||||||
|
|
||||||
const body: VideoView = { currentTime, viewEvent }
|
const sessionId = getPlayerSessionId()
|
||||||
|
|
||||||
|
const body: VideoView = { currentTime, viewEvent, sessionId }
|
||||||
|
|
||||||
const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
|
const headers = new Headers({ 'Content-type': 'application/json; charset=UTF-8' })
|
||||||
if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
|
if (this.authorizationHeader()) headers.set('Authorization', this.authorizationHeader())
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
function capitalizeFirstLetter (str: string) {
|
export function capitalizeFirstLetter (str: string) {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export function randomString (length: number) {
|
||||||
capitalizeFirstLetter
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
const charsLength = chars.length
|
||||||
|
const randomArray = new Uint8Array(length)
|
||||||
|
|
||||||
|
let result = ''
|
||||||
|
|
||||||
|
for (const v of crypto.getRandomValues(randomArray)) {
|
||||||
|
result += chars[v % charsLength]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
@ -381,7 +381,16 @@ views:
|
|||||||
# PeerTube buffers local video views before updating and federating the video
|
# PeerTube buffers local video views before updating and federating the video
|
||||||
local_buffer_update_interval: '30 minutes'
|
local_buffer_update_interval: '30 minutes'
|
||||||
|
|
||||||
ip_view_expiration: '1 hour'
|
# How long does it take to count again a view from the same user
|
||||||
|
view_expiration: '1 hour'
|
||||||
|
|
||||||
|
# Minimum amount of time the viewer has to watch the video before PeerTube adds a view
|
||||||
|
count_view_after: '10 seconds'
|
||||||
|
|
||||||
|
# Player can send a session id string to track the user
|
||||||
|
# Since this can be spoofed by users to create fake views, you have the option to disable this feature
|
||||||
|
# If disabled, PeerTube will use the IP address to track the same user (default behavior before PeerTube 6.1)
|
||||||
|
trust_viewer_session_id: true
|
||||||
|
|
||||||
# How often the web browser sends "is watching" information to the server
|
# How often the web browser sends "is watching" information to the server
|
||||||
# Increase the value or set null to disable it if you plan to have many viewers
|
# Increase the value or set null to disable it if you plan to have many viewers
|
||||||
|
@ -379,7 +379,16 @@ views:
|
|||||||
# PeerTube buffers local video views before updating and federating the video
|
# PeerTube buffers local video views before updating and federating the video
|
||||||
local_buffer_update_interval: '30 minutes'
|
local_buffer_update_interval: '30 minutes'
|
||||||
|
|
||||||
ip_view_expiration: '1 hour'
|
# How long does it take to count again a view from the same user
|
||||||
|
view_expiration: '1 hour'
|
||||||
|
|
||||||
|
# Minimum amount of time the viewer has to watch the video before PeerTube adds a view
|
||||||
|
count_view_after: '10 seconds'
|
||||||
|
|
||||||
|
# Player can send a session id string to track the user
|
||||||
|
# Since this can be spoofed by users to create fake views, you have the option to disable this feature
|
||||||
|
# If disabled, PeerTube will use the IP address to track the same user (default behavior before PeerTube 6.1)
|
||||||
|
trust_viewer_session_id: true
|
||||||
|
|
||||||
# How often the web browser sends "is watching" information to the server
|
# How often the web browser sends "is watching" information to the server
|
||||||
# Increase the value or set null to disable it if you plan to have many viewers
|
# Increase the value or set null to disable it if you plan to have many viewers
|
||||||
|
@ -150,7 +150,7 @@ views:
|
|||||||
max_age: -1
|
max_age: -1
|
||||||
|
|
||||||
local_buffer_update_interval: '5 seconds'
|
local_buffer_update_interval: '5 seconds'
|
||||||
ip_view_expiration: '1 second'
|
view_expiration: '1 second'
|
||||||
|
|
||||||
geo_ip:
|
geo_ip:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
// high excluded
|
// high excluded
|
||||||
function randomInt (low: number, high: number) {
|
export function randomInt (low: number, high: number) {
|
||||||
return Math.floor(Math.random() * (high - low) + low)
|
return Math.floor(Math.random() * (high - low) + low)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
randomInt
|
|
||||||
}
|
|
||||||
|
@ -3,4 +3,5 @@ export type VideoViewEvent = 'seek'
|
|||||||
export interface VideoView {
|
export interface VideoView {
|
||||||
currentTime: number
|
currentTime: number
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
|
sessionId?: string
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,9 @@ export class ViewsCommand extends AbstractCommand {
|
|||||||
currentTime: number
|
currentTime: number
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
xForwardedFor?: string
|
xForwardedFor?: string
|
||||||
|
sessionId?: string
|
||||||
}) {
|
}) {
|
||||||
const { id, xForwardedFor, viewEvent, currentTime } = options
|
const { id, xForwardedFor, viewEvent, currentTime, sessionId } = options
|
||||||
const path = '/api/v1/videos/' + id + '/views'
|
const path = '/api/v1/videos/' + id + '/views'
|
||||||
|
|
||||||
return this.postBodyRequest({
|
return this.postBodyRequest({
|
||||||
@ -20,7 +21,8 @@ export class ViewsCommand extends AbstractCommand {
|
|||||||
xForwardedFor,
|
xForwardedFor,
|
||||||
fields: {
|
fields: {
|
||||||
currentTime,
|
currentTime,
|
||||||
viewEvent
|
viewEvent,
|
||||||
|
sessionId
|
||||||
},
|
},
|
||||||
implicitToken: false,
|
implicitToken: false,
|
||||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
@ -30,6 +32,7 @@ export class ViewsCommand extends AbstractCommand {
|
|||||||
async simulateView (options: OverrideCommandOptions & {
|
async simulateView (options: OverrideCommandOptions & {
|
||||||
id: number | string
|
id: number | string
|
||||||
xForwardedFor?: string
|
xForwardedFor?: string
|
||||||
|
sessionId?: string
|
||||||
}) {
|
}) {
|
||||||
await this.view({ ...options, currentTime: 0 })
|
await this.view({ ...options, currentTime: 0 })
|
||||||
await this.view({ ...options, currentTime: 5 })
|
await this.view({ ...options, currentTime: 5 })
|
||||||
@ -39,6 +42,7 @@ export class ViewsCommand extends AbstractCommand {
|
|||||||
id: number | string
|
id: number | string
|
||||||
currentTimes: number[]
|
currentTimes: number[]
|
||||||
xForwardedFor?: string
|
xForwardedFor?: string
|
||||||
|
sessionId?: string
|
||||||
}) {
|
}) {
|
||||||
let viewEvent: VideoViewEvent = 'seek'
|
let viewEvent: VideoViewEvent = 'seek'
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
|
import { PeerTubeServer, cleanupTests, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
|
import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { FfmpegCommand } from 'fluent-ffmpeg'
|
import { FfmpegCommand } from 'fluent-ffmpeg'
|
||||||
import { prepareViewsServers, prepareViewsVideos, processViewsBuffer } from '@tests/shared/views.js'
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
|
||||||
import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
|
||||||
|
|
||||||
describe('Test video views/viewers counters', function () {
|
describe('Test video views/viewers counters', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
@ -21,7 +22,14 @@ describe('Test video views/viewers counters', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runTests () {
|
function runTests (options: { useSessionId: boolean }) {
|
||||||
|
|
||||||
|
const generateSession = () => {
|
||||||
|
if (!options.useSessionId) return undefined
|
||||||
|
|
||||||
|
return buildUUID()
|
||||||
|
}
|
||||||
|
|
||||||
describe('Test views counter on VOD', function () {
|
describe('Test views counter on VOD', function () {
|
||||||
let videoUUID: string
|
let videoUUID: string
|
||||||
|
|
||||||
@ -35,29 +43,35 @@ describe('Test video views/viewers counters', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should not view a video if watch time is below the threshold', async function () {
|
it('Should not view a video if watch time is below the threshold', async function () {
|
||||||
await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 2 ] })
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: generateSession(), currentTimes: [ 1, 2 ] })
|
||||||
await processViewsBuffer(servers)
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
await checkCounter('views', videoUUID, 0)
|
await checkCounter('views', videoUUID, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should view a video if watch time is above the threshold', async function () {
|
it('Should view a video if watch time is above the threshold', async function () {
|
||||||
await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: generateSession(), currentTimes: [ 1, 4 ] })
|
||||||
await processViewsBuffer(servers)
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
await checkCounter('views', videoUUID, 1)
|
await checkCounter('views', videoUUID, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not view again this video with the same IP', async function () {
|
it('Should not view again this video with the same IP/session Id', async function () {
|
||||||
await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] })
|
const sessionId = generateSession()
|
||||||
await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 1, 4 ] })
|
const xForwardedFor = '0.0.0.1,127.0.0.1'
|
||||||
|
|
||||||
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId, xForwardedFor, currentTimes: [ 1, 4 ] })
|
||||||
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId, xForwardedFor, currentTimes: [ 1, 4 ] })
|
||||||
await processViewsBuffer(servers)
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
await checkCounter('views', videoUUID, 2)
|
await checkCounter('views', videoUUID, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should view the video from server 2 and send the event', async function () {
|
it('Should view the video from server 2 and send the event', async function () {
|
||||||
await servers[1].views.simulateViewer({ id: videoUUID, currentTimes: [ 1, 4 ] })
|
const sessionId = generateSession()
|
||||||
|
|
||||||
|
await servers[1].views.simulateViewer({ id: videoUUID, sessionId, currentTimes: [ 1, 4 ] })
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
await processViewsBuffer(servers)
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
@ -87,20 +101,29 @@ describe('Test video views/viewers counters', function () {
|
|||||||
it('Should view twice and display 1 view/viewer', async function () {
|
it('Should view twice and display 1 view/viewer', async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
const sessionId = generateSession()
|
||||||
await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
|
|
||||||
await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
|
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
|
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
|
|
||||||
|
|
||||||
await wait(1000)
|
for (let i = 0; i < 3; i++) {
|
||||||
|
await servers[0].views.simulateViewer({ id: liveVideoId, sessionId, currentTimes: [ 0, 35 ] })
|
||||||
|
await servers[0].views.simulateViewer({ id: liveVideoId, sessionId, currentTimes: [ 0, 35 ] })
|
||||||
|
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId, currentTimes: [ 0, 5 ] })
|
||||||
|
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId, currentTimes: [ 0, 5 ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
await waitJobs(servers)
|
let doWhile = true
|
||||||
|
while (doWhile) {
|
||||||
|
try {
|
||||||
await checkCounter('viewers', liveVideoId, 1)
|
await checkCounter('viewers', liveVideoId, 1)
|
||||||
await checkCounter('viewers', vodVideoId, 1)
|
await checkCounter('viewers', vodVideoId, 1)
|
||||||
|
|
||||||
|
doWhile = false
|
||||||
|
} catch {
|
||||||
|
await wait(1000)
|
||||||
|
|
||||||
|
doWhile = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await processViewsBuffer(servers)
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
await checkCounter('views', liveVideoId, 1)
|
await checkCounter('views', liveVideoId, 1)
|
||||||
@ -121,7 +144,7 @@ describe('Test video views/viewers counters', function () {
|
|||||||
await checkCounter('viewers', vodVideoId, 0)
|
await checkCounter('viewers', vodVideoId, 0)
|
||||||
|
|
||||||
error = false
|
error = false
|
||||||
await wait(2500)
|
await wait(1000)
|
||||||
} catch {
|
} catch {
|
||||||
error = true
|
error = true
|
||||||
}
|
}
|
||||||
@ -131,22 +154,43 @@ describe('Test video views/viewers counters', function () {
|
|||||||
it('Should view on a remote and on local and display appropriate views/viewers', async function () {
|
it('Should view on a remote and on local and display appropriate views/viewers', async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 0, 5 ] })
|
const xForwardedFor = '0.0.0.1,127.0.0.1'
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, xForwardedFor: '0.0.0.1,127.0.0.1', currentTimes: [ 0, 5 ] })
|
const sessionId = generateSession()
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
|
const xForwardedFor2 = '0.0.0.2,127.0.0.1'
|
||||||
await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
|
const sessionId2 = generateSession()
|
||||||
await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
|
|
||||||
|
|
||||||
await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
|
{
|
||||||
await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
|
const currentTimes = [ 0, 5 ]
|
||||||
await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35 ] })
|
|
||||||
|
|
||||||
await wait(3000) // Throttled federation
|
await servers[0].views.simulateViewer({ id: vodVideoId, xForwardedFor, sessionId, currentTimes })
|
||||||
await waitJobs(servers)
|
await servers[0].views.simulateViewer({ id: vodVideoId, xForwardedFor, sessionId, currentTimes })
|
||||||
|
await servers[0].views.simulateViewer({ id: vodVideoId, xForwardedFor: xForwardedFor2, sessionId: sessionId2, currentTimes })
|
||||||
|
await servers[1].views.simulateViewer({ id: vodVideoId, xForwardedFor, sessionId, currentTimes })
|
||||||
|
await servers[1].views.simulateViewer({ id: vodVideoId, xForwardedFor, sessionId, currentTimes })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const currentTimes = [ 0, 35 ]
|
||||||
|
|
||||||
|
await servers[0].views.simulateViewer({ id: liveVideoId, xForwardedFor: xForwardedFor2, sessionId: sessionId2, currentTimes })
|
||||||
|
await servers[1].views.simulateViewer({ id: liveVideoId, xForwardedFor, sessionId, currentTimes })
|
||||||
|
await servers[1].views.simulateViewer({ id: liveVideoId, xForwardedFor, sessionId, currentTimes })
|
||||||
|
}
|
||||||
|
|
||||||
|
let doWhile = true
|
||||||
|
while (doWhile) {
|
||||||
|
try {
|
||||||
await checkCounter('viewers', liveVideoId, 2)
|
await checkCounter('viewers', liveVideoId, 2)
|
||||||
await checkCounter('viewers', vodVideoId, 3)
|
await checkCounter('viewers', vodVideoId, 3)
|
||||||
|
|
||||||
|
doWhile = false
|
||||||
|
} catch {
|
||||||
|
await wait(1000)
|
||||||
|
|
||||||
|
doWhile = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await processViewsBuffer(servers)
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
await checkCounter('views', liveVideoId, 3)
|
await checkCounter('views', liveVideoId, 3)
|
||||||
@ -167,7 +211,13 @@ describe('Test video views/viewers counters', function () {
|
|||||||
servers = await prepareViewsServers({ viewExpiration: '5 seconds', viewersFederationV2: false })
|
servers = await prepareViewsServers({ viewExpiration: '5 seconds', viewersFederationV2: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
runTests()
|
describe('Not using session id', function () {
|
||||||
|
runTests({ useSessionId: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Using session id', function () {
|
||||||
|
runTests({ useSessionId: true })
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
@ -182,10 +232,74 @@ describe('Test video views/viewers counters', function () {
|
|||||||
servers = await prepareViewsServers({ viewExpiration: '5 seconds', viewersFederationV2: true })
|
servers = await prepareViewsServers({ viewExpiration: '5 seconds', viewersFederationV2: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
runTests()
|
describe('Not using session id', function () {
|
||||||
|
runTests({ useSessionId: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Using session id', function () {
|
||||||
|
runTests({ useSessionId: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('View minimum duration config', function () {
|
||||||
|
|
||||||
|
it('Should update "count_view_after" config', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||||
|
|
||||||
|
{
|
||||||
|
await servers[0].views.simulateViewer({ id: uuid, sessionId: buildUUID(), currentTimes: [ 1, 2 ] })
|
||||||
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
|
await checkCounter('views', uuid, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
await servers[0].kill()
|
||||||
|
await servers[0].run({ views: { videos: { count_view_after: '1 second' } } })
|
||||||
|
|
||||||
|
{
|
||||||
|
await servers[0].views.simulateViewer({ id: uuid, sessionId: buildUUID(), currentTimes: [ 1, 2 ] })
|
||||||
|
await processViewsBuffer(servers)
|
||||||
|
|
||||||
|
await checkCounter('views', uuid, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Disabling session id trusting', function () {
|
||||||
|
let videoUUID: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
servers = await prepareViewsServers({ viewExpiration: '5 seconds', viewersFederationV2: true, trustViewerSessionId: false });
|
||||||
|
|
||||||
|
({ uuid: videoUUID } = await servers[0].videos.quickUpload({ name: 'video' }))
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not take into account session id if the server does not trust it', async function () {
|
||||||
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: buildUUID(), currentTimes: [ 1, 4 ] })
|
||||||
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: buildUUID(), currentTimes: [ 1, 4 ] })
|
||||||
|
|
||||||
|
await processViewsBuffer(servers)
|
||||||
|
await checkCounter('views', videoUUID, 1)
|
||||||
|
|
||||||
|
const xForwardedFor = '0.0.0.1,127.0.0.1'
|
||||||
|
await servers[0].views.simulateViewer({ id: videoUUID, xForwardedFor, sessionId: buildUUID(), currentTimes: [ 1, 4 ] })
|
||||||
|
|
||||||
|
await processViewsBuffer(servers)
|
||||||
|
await checkCounter('views', videoUUID, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { FfmpegCommand } from 'fluent-ffmpeg'
|
|
||||||
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
|
|
||||||
import { cleanupTests, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import { VideoStatsOverall } from '@peertube/peertube-models'
|
import { VideoStatsOverall } from '@peertube/peertube-models'
|
||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
|
import { PeerTubeServer, cleanupTests, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
|
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { FfmpegCommand } from 'fluent-ffmpeg'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -16,39 +17,43 @@ import { VideoStatsOverall } from '@peertube/peertube-models'
|
|||||||
* * user3 started and ended in the interval
|
* * user3 started and ended in the interval
|
||||||
* * user4 started and ended after end date
|
* * user4 started and ended after end date
|
||||||
*/
|
*/
|
||||||
async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string) {
|
async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string, useSessionId: boolean) {
|
||||||
const user0 = '8.8.8.8,127.0.0.1'
|
const user0 = '8.8.8.8,127.0.0.1'
|
||||||
const user1 = '8.8.8.8,127.0.0.1'
|
const user1 = '8.8.8.8,127.0.0.1'
|
||||||
const user2 = '8.8.8.9,127.0.0.1'
|
const user2 = '8.8.8.9,127.0.0.1'
|
||||||
const user3 = '8.8.8.10,127.0.0.1'
|
const user3 = '8.8.8.10,127.0.0.1'
|
||||||
const user4 = '8.8.8.11,127.0.0.1'
|
const user4 = '8.8.8.11,127.0.0.1'
|
||||||
|
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user0 }) // User 0 starts
|
const sessionIdField = useSessionId
|
||||||
|
? 'sessionId'
|
||||||
|
: 'xForwardedFor'
|
||||||
|
|
||||||
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user0 }) // User 0 starts
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user1 }) // User 1 starts
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user1 }) // User 1 starts
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 2, xForwardedFor: user0 }) // User 0 ends
|
await servers[0].views.view({ id: videoUUID, currentTime: 2, [sessionIdField]: user0 }) // User 0 ends
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
const startDate = new Date().toISOString()
|
const startDate = new Date().toISOString()
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user2 }) // User 2 starts
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user2 }) // User 2 starts
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user3 }) // User 3 starts
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user3 }) // User 3 starts
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 4, xForwardedFor: user1 }) // User 1 ends
|
await servers[0].views.view({ id: videoUUID, currentTime: 4, [sessionIdField]: user1 }) // User 1 ends
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 3, xForwardedFor: user3 }) // User 3 ends
|
await servers[0].views.view({ id: videoUUID, currentTime: 3, [sessionIdField]: user3 }) // User 3 ends
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
const endDate = new Date().toISOString()
|
const endDate = new Date().toISOString()
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 0, xForwardedFor: user4 }) // User 4 starts
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user4 }) // User 4 starts
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 5, xForwardedFor: user2 }) // User 2 ends
|
await servers[0].views.view({ id: videoUUID, currentTime: 5, [sessionIdField]: user2 }) // User 2 ends
|
||||||
await wait(500)
|
await wait(500)
|
||||||
|
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 1, xForwardedFor: user4 }) // User 4 ends
|
await servers[0].views.view({ id: videoUUID, currentTime: 1, [sessionIdField]: user4 }) // User 4 ends
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
@ -58,6 +63,15 @@ async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: str
|
|||||||
describe('Test views overall stats', function () {
|
describe('Test views overall stats', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
|
|
||||||
|
function runTests (options: { useSessionId: boolean }) {
|
||||||
|
const { useSessionId } = options
|
||||||
|
|
||||||
|
const generateSessionId = () => {
|
||||||
|
if (!options.useSessionId) return undefined
|
||||||
|
|
||||||
|
return buildUUID()
|
||||||
|
}
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
|
||||||
@ -91,7 +105,7 @@ describe('Test views overall stats', function () {
|
|||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
||||||
await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
|
await servers[0].views.simulateViewer({ id: videoId, sessionId: generateSessionId(), currentTimes: [ 0, 1 ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
@ -111,8 +125,8 @@ describe('Test views overall stats', function () {
|
|||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
{
|
{
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
|
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 3 ] })
|
||||||
await servers[0].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 35, 40 ] })
|
await servers[0].views.simulateViewer({ id: liveVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 35, 40 ] })
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
@ -142,7 +156,7 @@ describe('Test views overall stats', function () {
|
|||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
||||||
await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 2 ] })
|
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSessionId(), currentTimes: [ 0, 2 ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
@ -171,8 +185,8 @@ describe('Test views overall stats', function () {
|
|||||||
it('Should display overall stats with a remote viewer above the watch time limit', async function () {
|
it('Should display overall stats with a remote viewer above the watch time limit', async function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
await servers[1].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 5 ] })
|
await servers[1].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 5 ] })
|
||||||
await servers[1].views.simulateViewer({ id: liveVideoId, currentTimes: [ 0, 45 ] })
|
await servers[1].views.simulateViewer({ id: liveVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 45 ] })
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -201,7 +215,7 @@ describe('Test views overall stats', function () {
|
|||||||
|
|
||||||
const beforeView = new Date()
|
const beforeView = new Date()
|
||||||
|
|
||||||
await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
|
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 3 ] })
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -245,7 +259,7 @@ describe('Test views overall stats', function () {
|
|||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
const before = new Date()
|
const before = new Date()
|
||||||
await servers[0].views.simulateViewer({ id: videoUUID, currentTimes: [ 0, 2 ] })
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: generateSessionId(), currentTimes: [ 0, 2 ] })
|
||||||
const after = new Date()
|
const after = new Date()
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
@ -259,11 +273,13 @@ describe('Test views overall stats', function () {
|
|||||||
it('Should have watcher peak with 2 watchers', async function () {
|
it('Should have watcher peak with 2 watchers', async function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
|
const sessionId = generateSessionId()
|
||||||
|
|
||||||
before2Watchers = new Date()
|
before2Watchers = new Date()
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 0 })
|
await servers[0].views.view({ id: videoUUID, sessionId, currentTime: 0 })
|
||||||
await servers[1].views.view({ id: videoUUID, currentTime: 0 })
|
await servers[1].views.view({ id: videoUUID, sessionId, currentTime: 0 })
|
||||||
await servers[0].views.view({ id: videoUUID, currentTime: 2 })
|
await servers[0].views.view({ id: videoUUID, sessionId, currentTime: 2 })
|
||||||
await servers[1].views.view({ id: videoUUID, currentTime: 2 })
|
await servers[1].views.view({ id: videoUUID, sessionId, currentTime: 2 })
|
||||||
const after = new Date()
|
const after = new Date()
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
@ -291,7 +307,7 @@ describe('Test views overall stats', function () {
|
|||||||
it('Should complex filter peak viewers by date', async function () {
|
it('Should complex filter peak viewers by date', async function () {
|
||||||
this.timeout(60000)
|
this.timeout(60000)
|
||||||
|
|
||||||
const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID)
|
const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID, useSessionId)
|
||||||
|
|
||||||
const expectCorrect = (stats: VideoStatsOverall) => {
|
const expectCorrect = (stats: VideoStatsOverall) => {
|
||||||
expect(stats.viewersPeak).to.equal(3)
|
expect(stats.viewersPeak).to.equal(3)
|
||||||
@ -314,7 +330,7 @@ describe('Test views overall stats', function () {
|
|||||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
await servers[1].views.view({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
|
await servers[1].views.view({ id: uuid, sessionId: generateSessionId(), xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
@ -334,9 +350,24 @@ describe('Test views overall stats', function () {
|
|||||||
const config = { geo_ip: { enabled: true, city: { database_url: '' } } }
|
const config = { geo_ip: { enabled: true, city: { database_url: '' } } }
|
||||||
await Promise.all([ servers[0].run(config), servers[1].run(config) ])
|
await Promise.all([ servers[0].run(config), servers[1].run(config) ])
|
||||||
|
|
||||||
await servers[0].views.simulateViewer({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTimes: [ 1, 2 ] })
|
await servers[0].views.simulateViewer({
|
||||||
await servers[1].views.simulateViewer({ id: uuid, xForwardedFor: '8.8.8.4,127.0.0.1', currentTimes: [ 3, 4 ] })
|
id: uuid,
|
||||||
await servers[1].views.simulateViewer({ id: uuid, xForwardedFor: '80.67.169.12,127.0.0.1', currentTimes: [ 2, 3 ] })
|
sessionId: generateSessionId(),
|
||||||
|
xForwardedFor: '8.8.8.8,127.0.0.1',
|
||||||
|
currentTimes: [ 1, 2 ]
|
||||||
|
})
|
||||||
|
await servers[1].views.simulateViewer({
|
||||||
|
id: uuid,
|
||||||
|
sessionId: generateSessionId(),
|
||||||
|
xForwardedFor: '8.8.8.4,127.0.0.1',
|
||||||
|
currentTimes: [ 3, 4 ]
|
||||||
|
})
|
||||||
|
await servers[1].views.simulateViewer({
|
||||||
|
id: uuid,
|
||||||
|
sessionId: generateSessionId(),
|
||||||
|
xForwardedFor: '80.67.169.12,127.0.0.1',
|
||||||
|
currentTimes: [ 2, 3 ]
|
||||||
|
})
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
@ -364,9 +395,24 @@ describe('Test views overall stats', function () {
|
|||||||
servers[1].run(config)
|
servers[1].run(config)
|
||||||
])
|
])
|
||||||
|
|
||||||
await servers[0].views.simulateViewer({ id: uuid, xForwardedFor: '8.8.8.8,127.0.0.1', currentTimes: [ 1, 2 ] })
|
await servers[0].views.simulateViewer({
|
||||||
await servers[1].views.simulateViewer({ id: uuid, xForwardedFor: '8.8.8.4,127.0.0.1', currentTimes: [ 3, 4 ] })
|
id: uuid,
|
||||||
await servers[1].views.simulateViewer({ id: uuid, xForwardedFor: '80.67.169.12,127.0.0.1', currentTimes: [ 2, 3 ] })
|
sessionId: generateSessionId(),
|
||||||
|
xForwardedFor: '8.8.8.8,127.0.0.1',
|
||||||
|
currentTimes: [ 1, 2 ]
|
||||||
|
})
|
||||||
|
await servers[1].views.simulateViewer({
|
||||||
|
id: uuid,
|
||||||
|
sessionId: generateSessionId(),
|
||||||
|
xForwardedFor: '8.8.8.4,127.0.0.1',
|
||||||
|
currentTimes: [ 3, 4 ]
|
||||||
|
})
|
||||||
|
await servers[1].views.simulateViewer({
|
||||||
|
id: uuid,
|
||||||
|
sessionId: generateSessionId(),
|
||||||
|
xForwardedFor: '80.67.169.12,127.0.0.1',
|
||||||
|
currentTimes: [ 2, 3 ]
|
||||||
|
})
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
@ -397,4 +443,13 @@ describe('Test views overall stats', function () {
|
|||||||
after(async function () {
|
after(async function () {
|
||||||
await cleanupTests(servers)
|
await cleanupTests(servers)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Not using session id', function () {
|
||||||
|
runTests({ useSessionId: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Using session id', function () {
|
||||||
|
runTests({ useSessionId: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import { PeerTubeServer, cleanupTests } from '@peertube/peertube-server-commands'
|
import { PeerTubeServer, cleanupTests } from '@peertube/peertube-server-commands'
|
||||||
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
|
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
@ -17,6 +18,12 @@ describe('Test views retention stats', function () {
|
|||||||
describe('Test retention stats on VOD', function () {
|
describe('Test retention stats on VOD', function () {
|
||||||
let vodVideoId: string
|
let vodVideoId: string
|
||||||
|
|
||||||
|
function runTests (options: { useSessionId: boolean }) {
|
||||||
|
|
||||||
|
const sessionField = options.useSessionId
|
||||||
|
? 'sessionId'
|
||||||
|
: 'xForwardedFor'
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(240000);
|
this.timeout(240000);
|
||||||
|
|
||||||
@ -34,15 +41,15 @@ describe('Test views retention stats', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Should display appropriate retention metrics', async function () {
|
it('Should display appropriate retention metrics', async function () {
|
||||||
await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
|
await servers[0].views.simulateViewer({ [sessionField]: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
|
||||||
await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 3 ] })
|
await servers[0].views.simulateViewer({ [sessionField]: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 3 ] })
|
||||||
await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 3, 4 ] })
|
await servers[1].views.simulateViewer({ [sessionField]: '127.0.0.2,127.0.0.1', id: vodVideoId, currentTimes: [ 3, 4 ] })
|
||||||
await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
|
await servers[1].views.simulateViewer({ [sessionField]: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 0, 1 ] })
|
||||||
|
|
||||||
// Do not take into account empty section
|
// Do not take into account empty section
|
||||||
await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 5, 5 ] })
|
await servers[1].views.simulateViewer({ [sessionField]: '127.0.0.3,127.0.0.1', id: vodVideoId, currentTimes: [ 5, 5 ] })
|
||||||
await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.4,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 1 ] })
|
await servers[0].views.simulateViewer({ [sessionField]: '127.0.0.4,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 1 ] })
|
||||||
await servers[1].views.simulateViewer({ xForwardedFor: '127.0.0.4,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 1 ] })
|
await servers[1].views.simulateViewer({ [sessionField]: '127.0.0.4,127.0.0.1', id: vodVideoId, currentTimes: [ 1, 1 ] })
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
|
|
||||||
@ -57,8 +64,8 @@ describe('Test views retention stats', function () {
|
|||||||
|
|
||||||
const newVideo = await servers[0].videos.quickUpload({ name: 'video 2' })
|
const newVideo = await servers[0].videos.quickUpload({ name: 'video 2' })
|
||||||
|
|
||||||
await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.2,127.0.0.1', id: newVideo.id, currentTimes: [ 0, 1 ] })
|
await servers[0].views.simulateViewer({ [sessionField]: '127.0.0.2,127.0.0.1', id: newVideo.id, currentTimes: [ 0, 1 ] })
|
||||||
await servers[0].views.simulateViewer({ xForwardedFor: '127.0.0.3,127.0.0.1', id: newVideo.id, currentTimes: [ 1, 3 ] })
|
await servers[0].views.simulateViewer({ [sessionField]: '127.0.0.3,127.0.0.1', id: newVideo.id, currentTimes: [ 1, 3 ] })
|
||||||
|
|
||||||
await wait(2500)
|
await wait(2500)
|
||||||
|
|
||||||
@ -73,6 +80,15 @@ describe('Test views retention stats', function () {
|
|||||||
|
|
||||||
expect(data.map(d => d.retentionPercent)).to.deep.equal([ 50, 100, 50, 50, 0, 0 ])
|
expect(data.map(d => d.retentionPercent)).to.deep.equal([ 50, 100, 50, 50, 0, 0 ])
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Not using session id', function () {
|
||||||
|
runTests({ useSessionId: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Using session id', function () {
|
||||||
|
runTests({ useSessionId: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models'
|
||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
|
import { PeerTubeServer, cleanupTests, stopFfmpeg } from '@peertube/peertube-server-commands'
|
||||||
|
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { FfmpegCommand } from 'fluent-ffmpeg'
|
import { FfmpegCommand } from 'fluent-ffmpeg'
|
||||||
import { prepareViewsServers, prepareViewsVideos, processViewersStats } from '@tests/shared/views.js'
|
|
||||||
import { VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@peertube/peertube-models'
|
|
||||||
import { cleanupTests, PeerTubeServer, stopFfmpeg } from '@peertube/peertube-server-commands'
|
|
||||||
|
|
||||||
function buildOneMonthAgo () {
|
function buildOneMonthAgo () {
|
||||||
const monthAgo = new Date()
|
const monthAgo = new Date()
|
||||||
@ -80,6 +81,14 @@ describe('Test views timeserie stats', function () {
|
|||||||
expect(new Date(second.date).getTime() - new Date(first.date).getTime()).to.equal(intervalMs)
|
expect(new Date(second.date).getTime() - new Date(first.date).getTime()).to.equal(intervalMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runTests (options: { useSessionId: boolean }) {
|
||||||
|
|
||||||
|
const generateSession = () => {
|
||||||
|
if (!options.useSessionId) return undefined
|
||||||
|
|
||||||
|
return buildUUID()
|
||||||
|
}
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(240000);
|
this.timeout(240000);
|
||||||
|
|
||||||
@ -88,8 +97,8 @@ describe('Test views timeserie stats', function () {
|
|||||||
|
|
||||||
it('Should display appropriate viewers metrics', async function () {
|
it('Should display appropriate viewers metrics', async function () {
|
||||||
for (const videoId of [ vodVideoId, liveVideoId ]) {
|
for (const videoId of [ vodVideoId, liveVideoId ]) {
|
||||||
await servers[0].views.simulateViewer({ id: videoId, currentTimes: [ 0, 3 ] })
|
await servers[0].views.simulateViewer({ id: videoId, sessionId: generateSession(), currentTimes: [ 0, 3 ] })
|
||||||
await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 5 ] })
|
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSession(), currentTimes: [ 0, 5 ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
@ -115,7 +124,7 @@ describe('Test views timeserie stats', function () {
|
|||||||
})
|
})
|
||||||
expectTimeserieData(result, 8)
|
expectTimeserieData(result, 8)
|
||||||
|
|
||||||
await servers[1].views.simulateViewer({ id: videoId, currentTimes: [ 0, 1 ] })
|
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSession(), currentTimes: [ 0, 1 ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
await processViewersStats(servers)
|
await processViewersStats(servers)
|
||||||
@ -245,6 +254,15 @@ describe('Test views timeserie stats', function () {
|
|||||||
after(async function () {
|
after(async function () {
|
||||||
await stopFfmpeg(command)
|
await stopFfmpeg(command)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Not using session id', function () {
|
||||||
|
runTests({ useSessionId: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Using session id', function () {
|
||||||
|
runTests({ useSessionId: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createMultipleServers,
|
createMultipleServers,
|
||||||
@ -12,6 +11,8 @@ import {
|
|||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
waitJobs
|
waitJobs
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Test video views cleaner', function () {
|
describe('Test video views cleaner', function () {
|
||||||
let servers: PeerTubeServer[]
|
let servers: PeerTubeServer[]
|
||||||
@ -33,10 +34,12 @@ describe('Test video views cleaner', function () {
|
|||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
await servers[0].views.simulateView({ id: videoIdServer1 })
|
const sessionId = buildUUID()
|
||||||
await servers[1].views.simulateView({ id: videoIdServer1 })
|
|
||||||
await servers[0].views.simulateView({ id: videoIdServer2 })
|
await servers[0].views.simulateView({ id: videoIdServer1, sessionId })
|
||||||
await servers[1].views.simulateView({ id: videoIdServer2 })
|
await servers[1].views.simulateView({ id: videoIdServer1, sessionId })
|
||||||
|
await servers[0].views.simulateView({ id: videoIdServer2, sessionId })
|
||||||
|
await servers[1].views.simulateView({ id: videoIdServer2, sessionId })
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
@ -33,14 +33,25 @@ async function processViewsBuffer (servers: PeerTubeServer[]) {
|
|||||||
async function prepareViewsServers (options: {
|
async function prepareViewsServers (options: {
|
||||||
viewersFederationV2?: boolean
|
viewersFederationV2?: boolean
|
||||||
viewExpiration?: string // default 1 second
|
viewExpiration?: string // default 1 second
|
||||||
|
trustViewerSessionId?: boolean // default true
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { viewExpiration = '1 second' } = options
|
const { viewExpiration = '1 second', trustViewerSessionId = true } = options
|
||||||
|
|
||||||
const env = options?.viewersFederationV2 === true
|
const env = options?.viewersFederationV2 === true
|
||||||
? { USE_VIEWERS_FEDERATION_V2: 'true' }
|
? { USE_VIEWERS_FEDERATION_V2: 'true' }
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const servers = await createMultipleServers(2, { views: { videos: { ip_view_expiration: viewExpiration } } }, { env })
|
const config = {
|
||||||
|
views: {
|
||||||
|
videos: {
|
||||||
|
view_expiration: viewExpiration,
|
||||||
|
trust_viewer_session_id: trustViewerSessionId,
|
||||||
|
count_view_after: '10 seconds'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = await createMultipleServers(2, config, { env })
|
||||||
await setAccessTokensToServers(servers)
|
await setAccessTokensToServers(servers)
|
||||||
await setDefaultVideoChannel(servers)
|
await setDefaultVideoChannel(servers)
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ async function prepare () {
|
|||||||
views: {
|
views: {
|
||||||
videos: {
|
videos: {
|
||||||
local_buffer_update_interval: '30 minutes',
|
local_buffer_update_interval: '30 minutes',
|
||||||
ip_view_expiration: '1 hour'
|
view_expiration: '1 hour'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,8 @@ async function viewVideo (req: express.Request, res: express.Response) {
|
|||||||
video,
|
video,
|
||||||
ip,
|
ip,
|
||||||
currentTime: body.currentTime,
|
currentTime: body.currentTime,
|
||||||
viewEvent: body.viewEvent
|
viewEvent: body.viewEvent,
|
||||||
|
sessionId: body.sessionId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (successView) {
|
if (successView) {
|
||||||
|
@ -54,7 +54,7 @@ function checkMissedConfig () {
|
|||||||
'services.twitter.username',
|
'services.twitter.username',
|
||||||
'followers.instance.enabled', 'followers.instance.manual_approval',
|
'followers.instance.enabled', 'followers.instance.manual_approval',
|
||||||
'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
|
'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
|
||||||
'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration',
|
'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.view_expiration',
|
||||||
'views.videos.watching_interval.anonymous', 'views.videos.watching_interval.users',
|
'views.videos.watching_interval.anonymous', 'views.videos.watching_interval.users',
|
||||||
'rates_limit.api.window', 'rates_limit.api.max', 'rates_limit.login.window', 'rates_limit.login.max',
|
'rates_limit.api.window', 'rates_limit.api.max', 'rates_limit.login.window', 'rates_limit.login.max',
|
||||||
'rates_limit.signup.window', 'rates_limit.signup.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max',
|
'rates_limit.signup.window', 'rates_limit.signup.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max',
|
||||||
|
@ -308,7 +308,9 @@ const CONFIG = {
|
|||||||
MAX_AGE: parseDurationToMs(config.get('views.videos.remote.max_age'))
|
MAX_AGE: parseDurationToMs(config.get('views.videos.remote.max_age'))
|
||||||
},
|
},
|
||||||
LOCAL_BUFFER_UPDATE_INTERVAL: parseDurationToMs(config.get('views.videos.local_buffer_update_interval')),
|
LOCAL_BUFFER_UPDATE_INTERVAL: parseDurationToMs(config.get('views.videos.local_buffer_update_interval')),
|
||||||
IP_VIEW_EXPIRATION: parseDurationToMs(config.get('views.videos.ip_view_expiration')),
|
VIEW_EXPIRATION: parseDurationToMs(config.get('views.videos.view_expiration')),
|
||||||
|
COUNT_VIEW_AFTER: parseDurationToMs(config.get<number>('views.videos.count_view_after')),
|
||||||
|
TRUST_VIEWER_SESSION_ID: config.get<boolean>('views.videos.trust_viewer_session_id'),
|
||||||
WATCHING_INTERVAL: {
|
WATCHING_INTERVAL: {
|
||||||
ANONYMOUS: parseDurationToMs(config.get<string>('views.videos.watching_interval.anonymous')),
|
ANONYMOUS: parseDurationToMs(config.get<string>('views.videos.watching_interval.anonymous')),
|
||||||
USERS: parseDurationToMs(config.get<string>('views.videos.watching_interval.users'))
|
USERS: parseDurationToMs(config.get<string>('views.videos.watching_interval.users'))
|
||||||
|
@ -502,7 +502,7 @@ const CONSTRAINTS_FIELDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VIEW_LIFETIME = {
|
const VIEW_LIFETIME = {
|
||||||
VIEW: CONFIG.VIEWS.VIDEOS.IP_VIEW_EXPIRATION,
|
VIEW: CONFIG.VIEWS.VIDEOS.VIEW_EXPIRATION,
|
||||||
VIEWER_COUNTER: 60000 * 2, // 2 minutes
|
VIEWER_COUNTER: 60000 * 2, // 2 minutes
|
||||||
VIEWER_STATS: 60000 * 60 // 1 hour
|
VIEWER_STATS: 60000 * 60 // 1 hour
|
||||||
}
|
}
|
||||||
|
@ -176,12 +176,12 @@ class Redis {
|
|||||||
|
|
||||||
/* ************ Views per IP ************ */
|
/* ************ Views per IP ************ */
|
||||||
|
|
||||||
setIPVideoView (ip: string, videoUUID: string) {
|
setSessionIdVideoView (ip: string, videoUUID: string) {
|
||||||
return this.setValue(this.generateIPViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
|
return this.setValue(this.generateSessionIdViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
|
||||||
}
|
}
|
||||||
|
|
||||||
async doesVideoIPViewExist (ip: string, videoUUID: string) {
|
async doesVideoSessionIdViewExist (sessionId: string, videoUUID: string) {
|
||||||
return this.exists(this.generateIPViewKey(ip, videoUUID))
|
return this.exists(this.generateSessionIdViewKey(sessionId, videoUUID))
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ************ Video views stats ************ */
|
/* ************ Video views stats ************ */
|
||||||
@ -281,8 +281,8 @@ class Redis {
|
|||||||
return this.getObject(viewerKey)
|
return this.getObject(viewerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalVideoViewer (ip: string, videoId: number, object: any) {
|
setLocalVideoViewer (sessionId: string, videoId: number, object: any) {
|
||||||
const { setKey, viewerKey } = this.generateLocalVideoViewerKeys(ip, videoId)
|
const { setKey, viewerKey } = this.generateLocalVideoViewerKeys(sessionId, videoId)
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.addToSet(setKey, viewerKey),
|
this.addToSet(setKey, viewerKey),
|
||||||
@ -338,12 +338,16 @@ class Redis {
|
|||||||
return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
|
return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
|
||||||
}
|
}
|
||||||
|
|
||||||
generateLocalVideoViewerKeys (ip: string, videoId: number): { setKey: string, viewerKey: string }
|
generateLocalVideoViewerKeys (sessionId: string, videoId: number): { setKey: string, viewerKey: string }
|
||||||
generateLocalVideoViewerKeys (): { setKey: string }
|
generateLocalVideoViewerKeys (): { setKey: string }
|
||||||
generateLocalVideoViewerKeys (ip?: string, videoId?: number) {
|
generateLocalVideoViewerKeys (sessionId?: string, videoId?: number) {
|
||||||
const anonymousIP = sha256(CONFIG.SECRETS + '-' + ip)
|
return {
|
||||||
|
setKey: `local-video-viewer-stats-keys`,
|
||||||
|
|
||||||
return { setKey: `local-video-viewer-stats-keys`, viewerKey: `local-video-viewer-stats-${anonymousIP}-${videoId}` }
|
viewerKey: sessionId && videoId
|
||||||
|
? `local-video-viewer-stats-${sessionId}-${videoId}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
|
private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
|
||||||
@ -370,8 +374,8 @@ class Redis {
|
|||||||
return 'verify-email-registration-' + registrationId
|
return 'verify-email-registration-' + registrationId
|
||||||
}
|
}
|
||||||
|
|
||||||
generateIPViewKey (ip: string, videoUUID: string) {
|
generateSessionIdViewKey (sessionId: string, videoUUID: string) {
|
||||||
return `views-${videoUUID}-${sha256(CONFIG.SECRETS.PEERTUBE + '-' + ip)}`
|
return `views-${videoUUID}-${sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateContactFormKey (ip: string) {
|
private generateContactFormKey (ip: string) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { buildUUID, isTestOrDevInstance, isUsingViewersFederationV2, sha256 } from '@peertube/peertube-node-utils'
|
import { isTestOrDevInstance, isUsingViewersFederationV2 } from '@peertube/peertube-node-utils'
|
||||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { VIEW_LIFETIME } from '@server/initializers/constants.js'
|
import { VIEW_LIFETIME } from '@server/initializers/constants.js'
|
||||||
@ -28,8 +28,6 @@ export class VideoViewerCounters {
|
|||||||
private readonly viewersPerVideo = new Map<number, Viewer[]>()
|
private readonly viewersPerVideo = new Map<number, Viewer[]>()
|
||||||
private readonly idToViewer = new Map<string, Viewer>()
|
private readonly idToViewer = new Map<string, Viewer>()
|
||||||
|
|
||||||
private readonly salt = buildUUID()
|
|
||||||
|
|
||||||
private processingViewerCounters = false
|
private processingViewerCounters = false
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -40,13 +38,13 @@ export class VideoViewerCounters {
|
|||||||
|
|
||||||
async addLocalViewer (options: {
|
async addLocalViewer (options: {
|
||||||
video: MVideoImmutable
|
video: MVideoImmutable
|
||||||
ip: string
|
sessionId: string
|
||||||
}) {
|
}) {
|
||||||
const { video, ip } = options
|
const { video, sessionId } = options
|
||||||
|
|
||||||
logger.debug('Adding local viewer to video viewers counter %s.', video.uuid, { ...lTags(video.uuid) })
|
logger.debug('Adding local viewer to video viewers counter %s.', video.uuid, { ...lTags(video.uuid) })
|
||||||
|
|
||||||
const viewerId = this.generateViewerId(ip, video.uuid)
|
const viewerId = sessionId + '-' + video.uuid
|
||||||
const viewer = this.idToViewer.get(viewerId)
|
const viewer = this.idToViewer.get(viewerId)
|
||||||
|
|
||||||
if (viewer) {
|
if (viewer) {
|
||||||
@ -217,10 +215,6 @@ export class VideoViewerCounters {
|
|||||||
logger.debug('Video viewers update for %s is %d.', video.url, totalViewers, lTags())
|
logger.debug('Video viewers update for %s is %d.', video.url, totalViewers, lTags())
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateViewerId (ip: string, videoUUID: string) {
|
|
||||||
return sha256(this.salt + '-' + ip + '-' + videoUUID)
|
|
||||||
}
|
|
||||||
|
|
||||||
private async federateViewerIfNeeded (video: MVideoImmutable, viewer: Viewer) {
|
private async federateViewerIfNeeded (video: MVideoImmutable, viewer: Viewer) {
|
||||||
// Federate the viewer if it's been a "long" time we did not
|
// Federate the viewer if it's been a "long" time we did not
|
||||||
const now = new Date().getTime()
|
const now = new Date().getTime()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Transaction } from 'sequelize'
|
|
||||||
import { VideoViewEvent } from '@peertube/peertube-models'
|
import { VideoViewEvent } from '@peertube/peertube-models'
|
||||||
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
|
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
|
||||||
import { GeoIP } from '@server/helpers/geo-ip.js'
|
import { GeoIP } from '@server/helpers/geo-ip.js'
|
||||||
@ -12,6 +11,7 @@ import { VideoModel } from '@server/models/video/video.js'
|
|||||||
import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section.js'
|
import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section.js'
|
||||||
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer.js'
|
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer.js'
|
||||||
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
|
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
|
||||||
|
import { Transaction } from 'sequelize'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('views')
|
const lTags = loggerTagsFactory('views')
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ export class VideoViewerStats {
|
|||||||
private processingRedisWrites = false
|
private processingRedisWrites = false
|
||||||
|
|
||||||
private readonly viewerCache = new Map<string, LocalViewerStats>()
|
private readonly viewerCache = new Map<string, LocalViewerStats>()
|
||||||
private readonly redisPendingWrites = new Map<string, { ip: string, videoId: number, stats: LocalViewerStats }>()
|
private readonly redisPendingWrites = new Map<string, { sessionId: string, videoId: number, stats: LocalViewerStats }>()
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
setInterval(() => this.processViewerStats(), VIEW_LIFETIME.VIEWER_STATS)
|
setInterval(() => this.processViewerStats(), VIEW_LIFETIME.VIEWER_STATS)
|
||||||
@ -50,35 +50,19 @@ export class VideoViewerStats {
|
|||||||
video: MVideoImmutable
|
video: MVideoImmutable
|
||||||
currentTime: number
|
currentTime: number
|
||||||
ip: string
|
ip: string
|
||||||
|
sessionId: string
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
}) {
|
}) {
|
||||||
const { video, ip, viewEvent, currentTime } = options
|
const { video, ip, viewEvent, currentTime, sessionId } = options
|
||||||
|
|
||||||
logger.debug('Adding local viewer to video stats %s.', video.uuid, { currentTime, viewEvent, ...lTags(video.uuid) })
|
logger.debug(
|
||||||
|
'Adding local viewer to video stats %s.', video.uuid,
|
||||||
|
{ currentTime, viewEvent, sessionId, ...lTags(video.uuid) }
|
||||||
|
)
|
||||||
|
|
||||||
return this.updateLocalViewerStats({ video, viewEvent, currentTime, ip })
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async getWatchTime (videoId: number, ip: string) {
|
|
||||||
const stats: LocalViewerStats = await this.getLocalVideoViewerByIP({ ip, videoId })
|
|
||||||
|
|
||||||
return stats?.watchTime || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private async updateLocalViewerStats (options: {
|
|
||||||
video: MVideoImmutable
|
|
||||||
ip: string
|
|
||||||
currentTime: number
|
|
||||||
viewEvent?: VideoViewEvent
|
|
||||||
}) {
|
|
||||||
const { video, ip, viewEvent, currentTime } = options
|
|
||||||
const nowMs = new Date().getTime()
|
const nowMs = new Date().getTime()
|
||||||
|
|
||||||
let stats: LocalViewerStats = await this.getLocalVideoViewerByIP({ ip, videoId: video.id })
|
let stats: LocalViewerStats = await this.getLocalVideoViewer({ sessionId, videoId: video.id })
|
||||||
|
|
||||||
if (stats && stats.watchSections.length >= MAX_LOCAL_VIEWER_WATCH_SECTIONS) {
|
if (stats && stats.watchSections.length >= MAX_LOCAL_VIEWER_WATCH_SECTIONS) {
|
||||||
logger.warn('Too much watch section to store for a viewer, skipping this one', { currentTime, viewEvent, ...lTags(video.uuid) })
|
logger.warn('Too much watch section to store for a viewer, skipping this one', { currentTime, viewEvent, ...lTags(video.uuid) })
|
||||||
@ -129,9 +113,19 @@ export class VideoViewerStats {
|
|||||||
|
|
||||||
logger.debug('Set local video viewer stats for video %s.', video.uuid, { stats, ...lTags(video.uuid) })
|
logger.debug('Set local video viewer stats for video %s.', video.uuid, { stats, ...lTags(video.uuid) })
|
||||||
|
|
||||||
this.setLocalVideoViewer(ip, video.id, stats)
|
this.setLocalVideoViewer(sessionId, video.id, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async getWatchTime (videoId: number, sessionId: string) {
|
||||||
|
const stats: LocalViewerStats = await this.getLocalVideoViewer({ sessionId, videoId })
|
||||||
|
|
||||||
|
return stats?.watchTime || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async processViewerStats () {
|
async processViewerStats () {
|
||||||
if (this.processingViewersStats) return
|
if (this.processingViewersStats) return
|
||||||
this.processingViewersStats = true
|
this.processingViewersStats = true
|
||||||
@ -213,11 +207,11 @@ export class VideoViewerStats {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private getLocalVideoViewerByIP (options: {
|
private getLocalVideoViewer (options: {
|
||||||
ip: string
|
sessionId: string
|
||||||
videoId: number
|
videoId: number
|
||||||
}): Promise<LocalViewerStats> {
|
}): Promise<LocalViewerStats> {
|
||||||
const { viewerKey } = Redis.Instance.generateLocalVideoViewerKeys(options.ip, options.videoId)
|
const { viewerKey } = Redis.Instance.generateLocalVideoViewerKeys(options.sessionId, options.videoId)
|
||||||
|
|
||||||
return this.getLocalVideoViewerByKey(viewerKey)
|
return this.getLocalVideoViewerByKey(viewerKey)
|
||||||
}
|
}
|
||||||
@ -229,11 +223,11 @@ export class VideoViewerStats {
|
|||||||
return Redis.Instance.getLocalVideoViewer({ key })
|
return Redis.Instance.getLocalVideoViewer({ key })
|
||||||
}
|
}
|
||||||
|
|
||||||
private setLocalVideoViewer (ip: string, videoId: number, stats: LocalViewerStats) {
|
private setLocalVideoViewer (sessionId: string, videoId: number, stats: LocalViewerStats) {
|
||||||
const { viewerKey } = Redis.Instance.generateLocalVideoViewerKeys(ip, videoId)
|
const { viewerKey } = Redis.Instance.generateLocalVideoViewerKeys(sessionId, videoId)
|
||||||
this.viewerCache.set(viewerKey, stats)
|
this.viewerCache.set(viewerKey, stats)
|
||||||
|
|
||||||
this.redisPendingWrites.set(viewerKey, { ip, videoId, stats })
|
this.redisPendingWrites.set(viewerKey, { sessionId, videoId, stats })
|
||||||
}
|
}
|
||||||
|
|
||||||
private deleteLocalVideoViewersKeys (key: string) {
|
private deleteLocalVideoViewersKeys (key: string) {
|
||||||
@ -248,13 +242,13 @@ export class VideoViewerStats {
|
|||||||
this.processingRedisWrites = true
|
this.processingRedisWrites = true
|
||||||
|
|
||||||
for (const [ key, pendingWrite ] of this.redisPendingWrites) {
|
for (const [ key, pendingWrite ] of this.redisPendingWrites) {
|
||||||
const { ip, videoId, stats } = pendingWrite
|
const { sessionId, videoId, stats } = pendingWrite
|
||||||
this.redisPendingWrites.delete(key)
|
this.redisPendingWrites.delete(key)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Redis.Instance.setLocalVideoViewer(ip, videoId, stats)
|
await Redis.Instance.setLocalVideoViewer(sessionId, videoId, stats)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Cannot write viewer into redis', { ip, videoId, stats, err })
|
logger.error('Cannot write viewer into redis', { sessionId, videoId, stats, err })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
|
import { VIEW_LIFETIME } from '@server/initializers/constants.js'
|
||||||
import { sendView } from '@server/lib/activitypub/send/send-view.js'
|
import { sendView } from '@server/lib/activitypub/send/send-view.js'
|
||||||
import { getCachedVideoDuration } from '@server/lib/video.js'
|
import { getCachedVideoDuration } from '@server/lib/video.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
|
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
|
||||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
|
||||||
import { Redis } from '../../redis.js'
|
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { VIEW_LIFETIME } from '@server/initializers/constants.js'
|
import { Redis } from '../../redis.js'
|
||||||
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('views')
|
const lTags = loggerTagsFactory('views')
|
||||||
|
|
||||||
@ -19,19 +20,19 @@ export class VideoViews {
|
|||||||
|
|
||||||
async addLocalView (options: {
|
async addLocalView (options: {
|
||||||
video: MVideoImmutable
|
video: MVideoImmutable
|
||||||
ip: string
|
sessionId: string
|
||||||
watchTime: number
|
watchTime: number
|
||||||
}) {
|
}) {
|
||||||
const { video, ip, watchTime } = options
|
const { video, sessionId, watchTime } = options
|
||||||
|
|
||||||
logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) })
|
logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) })
|
||||||
|
|
||||||
if (!await this.hasEnoughWatchTime(video, watchTime)) return false
|
if (!await this.hasEnoughWatchTime(video, watchTime)) return false
|
||||||
|
|
||||||
const viewExists = await this.doesVideoIPViewExist(ip, video.uuid)
|
const viewExists = await this.doesVideoSessionIdViewExist(sessionId, video.uuid)
|
||||||
if (viewExists) return false
|
if (viewExists) return false
|
||||||
|
|
||||||
await this.setIPVideoView(ip, video.uuid)
|
await this.setSessionIdVideoView(sessionId, video.uuid)
|
||||||
|
|
||||||
await this.addView(video)
|
await this.addView(video)
|
||||||
|
|
||||||
@ -69,24 +70,25 @@ export class VideoViews {
|
|||||||
private async hasEnoughWatchTime (video: MVideoImmutable, watchTime: number) {
|
private async hasEnoughWatchTime (video: MVideoImmutable, watchTime: number) {
|
||||||
const { duration, isLive } = await getCachedVideoDuration(video.id)
|
const { duration, isLive } = await getCachedVideoDuration(video.id)
|
||||||
|
|
||||||
if (isLive || duration >= 30) return watchTime >= 30
|
const countViewAfterSeconds = CONFIG.VIEWS.VIDEOS.COUNT_VIEW_AFTER / 1000 // Config is in ms
|
||||||
|
if (isLive || duration >= countViewAfterSeconds) return watchTime >= countViewAfterSeconds
|
||||||
|
|
||||||
// Check more than 50% of the video is watched
|
// Check more than 50% of the video is watched
|
||||||
return duration / watchTime < 2
|
return duration / watchTime < 2
|
||||||
}
|
}
|
||||||
|
|
||||||
private doesVideoIPViewExist (ip: string, videoUUID: string) {
|
private doesVideoSessionIdViewExist (sessionId: string, videoUUID: string) {
|
||||||
const key = Redis.Instance.generateIPViewKey(ip, videoUUID)
|
const key = Redis.Instance.generateSessionIdViewKey(sessionId, videoUUID)
|
||||||
const value = this.viewsCache.has(key)
|
const value = this.viewsCache.has(key)
|
||||||
if (value === true) return Promise.resolve(true)
|
if (value === true) return Promise.resolve(true)
|
||||||
|
|
||||||
return Redis.Instance.doesVideoIPViewExist(ip, videoUUID)
|
return Redis.Instance.doesVideoSessionIdViewExist(sessionId, videoUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private setIPVideoView (ip: string, videoUUID: string) {
|
private setSessionIdVideoView (sessionId: string, videoUUID: string) {
|
||||||
const key = Redis.Instance.generateIPViewKey(ip, videoUUID)
|
const key = Redis.Instance.generateSessionIdViewKey(sessionId, videoUUID)
|
||||||
this.viewsCache.set(key, true)
|
this.viewsCache.set(key, true)
|
||||||
|
|
||||||
return Redis.Instance.setIPVideoView(ip, videoUUID)
|
return Redis.Instance.setSessionIdVideoView(sessionId, videoUUID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
|
||||||
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
|
|
||||||
import { VideoViewEvent } from '@peertube/peertube-models'
|
import { VideoViewEvent } from '@peertube/peertube-models'
|
||||||
|
import { sha256 } from '@peertube/peertube-node-utils'
|
||||||
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
|
||||||
import { VideoScope, VideoViewerCounters, VideoViewerStats, VideoViews, ViewerScope } from './shared/index.js'
|
import { VideoScope, VideoViewerCounters, VideoViewerStats, VideoViews, ViewerScope } from './shared/index.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,20 +46,26 @@ export class VideoViewsManager {
|
|||||||
video: MVideoImmutable
|
video: MVideoImmutable
|
||||||
currentTime: number
|
currentTime: number
|
||||||
ip: string | null
|
ip: string | null
|
||||||
|
sessionId?: string
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
}) {
|
}) {
|
||||||
const { video, ip, viewEvent, currentTime } = options
|
const { video, ip, viewEvent, currentTime } = options
|
||||||
|
|
||||||
logger.debug('Processing local view for %s and ip %s.', video.url, ip, lTags())
|
let sessionId = options.sessionId
|
||||||
|
if (!sessionId || CONFIG.VIEWS.VIDEOS.TRUST_VIEWER_SESSION_ID !== true) {
|
||||||
|
sessionId = sha256(CONFIG.SECRETS + '-' + ip)
|
||||||
|
}
|
||||||
|
|
||||||
await this.videoViewerStats.addLocalViewer({ video, ip, viewEvent, currentTime })
|
logger.debug(`Processing local view for ${video.url}, ip ${ip} and session id ${sessionId}.`, lTags())
|
||||||
|
|
||||||
const successViewer = await this.videoViewerCounters.addLocalViewer({ video, ip })
|
await this.videoViewerStats.addLocalViewer({ video, ip, sessionId, viewEvent, currentTime })
|
||||||
|
|
||||||
|
const successViewer = await this.videoViewerCounters.addLocalViewer({ video, sessionId })
|
||||||
|
|
||||||
// Do it after added local viewer to fetch updated information
|
// Do it after added local viewer to fetch updated information
|
||||||
const watchTime = await this.videoViewerStats.getWatchTime(video.id, ip)
|
const watchTime = await this.videoViewerStats.getWatchTime(video.id, sessionId)
|
||||||
|
|
||||||
const successView = await this.videoViews.addLocalView({ video, watchTime, ip })
|
const successView = await this.videoViews.addLocalView({ video, watchTime, sessionId })
|
||||||
|
|
||||||
return { successView, successViewer }
|
return { successView, successViewer }
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user