PeerTube/server/core/lib/views/shared/video-views.ts
Chocobozzz 5cb3e6a0b8
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)
2024-04-04 16:27:40 +02:00

95 lines
3 KiB
TypeScript

import { buildUUID } from '@peertube/peertube-node-utils'
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 { getCachedVideoDuration } from '@server/lib/video.js'
import { getServerActor } from '@server/models/application/application.js'
import { MVideo, MVideoImmutable } from '@server/types/models/index.js'
import { LRUCache } from 'lru-cache'
import { Redis } from '../../redis.js'
import { CONFIG } from '@server/initializers/config.js'
const lTags = loggerTagsFactory('views')
export class VideoViews {
private readonly viewsCache = new LRUCache<string, boolean>({
max: 10_000,
ttl: VIEW_LIFETIME.VIEW
})
async addLocalView (options: {
video: MVideoImmutable
sessionId: string
watchTime: number
}) {
const { video, sessionId, watchTime } = options
logger.debug('Adding local view to video %s.', video.uuid, { watchTime, ...lTags(video.uuid) })
if (!await this.hasEnoughWatchTime(video, watchTime)) return false
const viewExists = await this.doesVideoSessionIdViewExist(sessionId, video.uuid)
if (viewExists) return false
await this.setSessionIdVideoView(sessionId, video.uuid)
await this.addView(video)
await sendView({ byActor: await getServerActor(), video, viewerIdentifier: buildUUID() })
return true
}
async addRemoteView (options: {
video: MVideo
}) {
const { video } = options
logger.debug('Adding remote view to video %s.', video.uuid, { ...lTags(video.uuid) })
await this.addView(video)
return true
}
// ---------------------------------------------------------------------------
private async addView (video: MVideoImmutable) {
const promises: Promise<any>[] = []
if (video.isOwned()) {
promises.push(Redis.Instance.addLocalVideoView(video.id))
}
promises.push(Redis.Instance.addVideoViewStats(video.id))
await Promise.all(promises)
}
private async hasEnoughWatchTime (video: MVideoImmutable, watchTime: number) {
const { duration, isLive } = await getCachedVideoDuration(video.id)
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
return duration / watchTime < 2
}
private doesVideoSessionIdViewExist (sessionId: string, videoUUID: string) {
const key = Redis.Instance.generateSessionIdViewKey(sessionId, videoUUID)
const value = this.viewsCache.has(key)
if (value === true) return Promise.resolve(true)
return Redis.Instance.doesVideoSessionIdViewExist(sessionId, videoUUID)
}
private setSessionIdVideoView (sessionId: string, videoUUID: string) {
const key = Redis.Instance.generateSessionIdViewKey(sessionId, videoUUID)
this.viewsCache.set(key, true)
return Redis.Instance.setSessionIdVideoView(sessionId, videoUUID)
}
}