mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-05-16 20:02:40 +00:00
5cb3e6a0b8
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)
456 lines
17 KiB
TypeScript
456 lines
17 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
|
|
|
import { wait } from '@peertube/peertube-core-utils'
|
|
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'
|
|
|
|
/**
|
|
*
|
|
* Simulate 5 sections of viewers
|
|
* * user0 started and ended before start date
|
|
* * user1 started before start date and ended in the interval
|
|
* * user2 started started in the interval and ended after end date
|
|
* * user3 started and ended in the interval
|
|
* * user4 started and ended after end date
|
|
*/
|
|
async function simulateComplexViewers (servers: PeerTubeServer[], videoUUID: string, useSessionId: boolean) {
|
|
const user0 = '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 user3 = '8.8.8.10,127.0.0.1'
|
|
const user4 = '8.8.8.11,127.0.0.1'
|
|
|
|
const sessionIdField = useSessionId
|
|
? 'sessionId'
|
|
: 'xForwardedFor'
|
|
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user0 }) // User 0 starts
|
|
await wait(500)
|
|
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user1 }) // User 1 starts
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 2, [sessionIdField]: user0 }) // User 0 ends
|
|
await wait(500)
|
|
|
|
const startDate = new Date().toISOString()
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user2 }) // User 2 starts
|
|
await wait(500)
|
|
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user3 }) // User 3 starts
|
|
await wait(500)
|
|
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 4, [sessionIdField]: user1 }) // User 1 ends
|
|
await wait(500)
|
|
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 3, [sessionIdField]: user3 }) // User 3 ends
|
|
await wait(500)
|
|
|
|
const endDate = new Date().toISOString()
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 0, [sessionIdField]: user4 }) // User 4 starts
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 5, [sessionIdField]: user2 }) // User 2 ends
|
|
await wait(500)
|
|
|
|
await servers[0].views.view({ id: videoUUID, currentTime: 1, [sessionIdField]: user4 }) // User 4 ends
|
|
|
|
await processViewersStats(servers)
|
|
|
|
return { startDate, endDate }
|
|
}
|
|
|
|
describe('Test views overall stats', function () {
|
|
let servers: PeerTubeServer[]
|
|
|
|
function runTests (options: { useSessionId: boolean }) {
|
|
const { useSessionId } = options
|
|
|
|
const generateSessionId = () => {
|
|
if (!options.useSessionId) return undefined
|
|
|
|
return buildUUID()
|
|
}
|
|
|
|
before(async function () {
|
|
this.timeout(120000)
|
|
|
|
servers = await prepareViewsServers()
|
|
})
|
|
|
|
describe('Test watch time stats of local videos on live and VOD', function () {
|
|
let vodVideoId: string
|
|
let liveVideoId: string
|
|
let command: FfmpegCommand
|
|
|
|
before(async function () {
|
|
this.timeout(240000);
|
|
|
|
({ vodVideoId, liveVideoId, ffmpegCommand: command } = await prepareViewsVideos({ servers, live: true, vod: true }))
|
|
})
|
|
|
|
it('Should display overall stats of a video with no viewers', async function () {
|
|
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId })
|
|
const video = await servers[0].videos.get({ id: videoId })
|
|
|
|
expect(video.views).to.equal(0)
|
|
expect(stats.averageWatchTime).to.equal(0)
|
|
expect(stats.totalWatchTime).to.equal(0)
|
|
expect(stats.totalViewers).to.equal(0)
|
|
}
|
|
})
|
|
|
|
it('Should display overall stats with 1 viewer below the watch time limit', async function () {
|
|
this.timeout(60000)
|
|
|
|
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
|
await servers[0].views.simulateViewer({ id: videoId, sessionId: generateSessionId(), currentTimes: [ 0, 1 ] })
|
|
}
|
|
|
|
await processViewersStats(servers)
|
|
|
|
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId })
|
|
const video = await servers[0].videos.get({ id: videoId })
|
|
|
|
expect(video.views).to.equal(0)
|
|
expect(stats.averageWatchTime).to.equal(1)
|
|
expect(stats.totalWatchTime).to.equal(1)
|
|
expect(stats.totalViewers).to.equal(1)
|
|
}
|
|
})
|
|
|
|
it('Should display overall stats with 2 viewers', async function () {
|
|
this.timeout(60000)
|
|
|
|
{
|
|
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 3 ] })
|
|
await servers[0].views.simulateViewer({ id: liveVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 35, 40 ] })
|
|
|
|
await processViewersStats(servers)
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
|
|
const video = await servers[0].videos.get({ id: vodVideoId })
|
|
|
|
expect(video.views).to.equal(1)
|
|
expect(stats.averageWatchTime).to.equal(2)
|
|
expect(stats.totalWatchTime).to.equal(4)
|
|
expect(stats.totalViewers).to.equal(2)
|
|
}
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
|
|
const video = await servers[0].videos.get({ id: liveVideoId })
|
|
|
|
expect(video.views).to.equal(1)
|
|
expect(stats.averageWatchTime).to.equal(21)
|
|
expect(stats.totalWatchTime).to.equal(41)
|
|
expect(stats.totalViewers).to.equal(2)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('Should display overall stats with a remote viewer below the watch time limit', async function () {
|
|
this.timeout(60000)
|
|
|
|
for (const videoId of [ liveVideoId, vodVideoId ]) {
|
|
await servers[1].views.simulateViewer({ id: videoId, sessionId: generateSessionId(), currentTimes: [ 0, 2 ] })
|
|
}
|
|
|
|
await processViewersStats(servers)
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
|
|
const video = await servers[0].videos.get({ id: vodVideoId })
|
|
|
|
expect(video.views).to.equal(1)
|
|
expect(stats.averageWatchTime).to.equal(2)
|
|
expect(stats.totalWatchTime).to.equal(6)
|
|
expect(stats.totalViewers).to.equal(3)
|
|
}
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
|
|
const video = await servers[0].videos.get({ id: liveVideoId })
|
|
|
|
expect(video.views).to.equal(1)
|
|
expect(stats.averageWatchTime).to.equal(14)
|
|
expect(stats.totalWatchTime).to.equal(43)
|
|
expect(stats.totalViewers).to.equal(3)
|
|
}
|
|
})
|
|
|
|
it('Should display overall stats with a remote viewer above the watch time limit', async function () {
|
|
this.timeout(60000)
|
|
|
|
await servers[1].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 5 ] })
|
|
await servers[1].views.simulateViewer({ id: liveVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 45 ] })
|
|
await processViewersStats(servers)
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId })
|
|
const video = await servers[0].videos.get({ id: vodVideoId })
|
|
|
|
expect(video.views).to.equal(2)
|
|
expect(stats.averageWatchTime).to.equal(3)
|
|
expect(stats.totalWatchTime).to.equal(11)
|
|
expect(stats.totalViewers).to.equal(4)
|
|
}
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId })
|
|
const video = await servers[0].videos.get({ id: liveVideoId })
|
|
|
|
expect(video.views).to.equal(2)
|
|
expect(stats.averageWatchTime).to.equal(22)
|
|
expect(stats.totalWatchTime).to.equal(88)
|
|
expect(stats.totalViewers).to.equal(4)
|
|
}
|
|
})
|
|
|
|
it('Should filter overall stats by date', async function () {
|
|
this.timeout(60000)
|
|
|
|
const beforeView = new Date()
|
|
|
|
await servers[0].views.simulateViewer({ id: vodVideoId, sessionId: generateSessionId(), currentTimes: [ 0, 3 ] })
|
|
await processViewersStats(servers)
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() })
|
|
expect(stats.averageWatchTime).to.equal(3)
|
|
expect(stats.totalWatchTime).to.equal(3)
|
|
expect(stats.totalViewers).to.equal(1)
|
|
}
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() })
|
|
expect(stats.averageWatchTime).to.equal(22)
|
|
expect(stats.totalWatchTime).to.equal(88)
|
|
expect(stats.totalViewers).to.equal(4)
|
|
}
|
|
})
|
|
|
|
after(async function () {
|
|
await stopFfmpeg(command)
|
|
})
|
|
})
|
|
|
|
describe('Test watchers peak stats of local videos on VOD', function () {
|
|
let videoUUID: string
|
|
let before2Watchers: Date
|
|
|
|
before(async function () {
|
|
this.timeout(240000);
|
|
|
|
({ vodVideoId: videoUUID } = await prepareViewsVideos({ servers, live: true, vod: true }))
|
|
})
|
|
|
|
it('Should not have watchers peak', async function () {
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
|
|
|
|
expect(stats.viewersPeak).to.equal(0)
|
|
expect(stats.viewersPeakDate).to.be.null
|
|
})
|
|
|
|
it('Should have watcher peak with 1 watcher', async function () {
|
|
this.timeout(60000)
|
|
|
|
const before = new Date()
|
|
await servers[0].views.simulateViewer({ id: videoUUID, sessionId: generateSessionId(), currentTimes: [ 0, 2 ] })
|
|
const after = new Date()
|
|
|
|
await processViewersStats(servers)
|
|
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
|
|
|
|
expect(stats.viewersPeak).to.equal(1)
|
|
expect(new Date(stats.viewersPeakDate)).to.be.above(before).and.below(after)
|
|
})
|
|
|
|
it('Should have watcher peak with 2 watchers', async function () {
|
|
this.timeout(60000)
|
|
|
|
const sessionId = generateSessionId()
|
|
|
|
before2Watchers = new Date()
|
|
await servers[0].views.view({ id: videoUUID, sessionId, currentTime: 0 })
|
|
await servers[1].views.view({ id: videoUUID, sessionId, currentTime: 0 })
|
|
await servers[0].views.view({ id: videoUUID, sessionId, currentTime: 2 })
|
|
await servers[1].views.view({ id: videoUUID, sessionId, currentTime: 2 })
|
|
const after = new Date()
|
|
|
|
await processViewersStats(servers)
|
|
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID })
|
|
|
|
expect(stats.viewersPeak).to.equal(2)
|
|
expect(new Date(stats.viewersPeakDate)).to.be.above(before2Watchers).and.below(after)
|
|
})
|
|
|
|
it('Should filter peak viewers stats by date', async function () {
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
|
|
expect(stats.viewersPeak).to.equal(0)
|
|
expect(stats.viewersPeakDate).to.not.exist
|
|
}
|
|
|
|
{
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate: before2Watchers.toISOString() })
|
|
expect(stats.viewersPeak).to.equal(1)
|
|
expect(new Date(stats.viewersPeakDate)).to.be.below(before2Watchers)
|
|
}
|
|
})
|
|
|
|
it('Should complex filter peak viewers by date', async function () {
|
|
this.timeout(60000)
|
|
|
|
const { startDate, endDate } = await simulateComplexViewers(servers, videoUUID, useSessionId)
|
|
|
|
const expectCorrect = (stats: VideoStatsOverall) => {
|
|
expect(stats.viewersPeak).to.equal(3)
|
|
expect(new Date(stats.viewersPeakDate)).to.be.above(new Date(startDate)).and.below(new Date(endDate))
|
|
}
|
|
|
|
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate, endDate }))
|
|
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate }))
|
|
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID, endDate }))
|
|
expectCorrect(await servers[0].videoStats.getOverallStats({ videoId: videoUUID }))
|
|
})
|
|
})
|
|
|
|
describe('Test countries/subdivisions', function () {
|
|
let videoUUID: string
|
|
|
|
it('Should not report countries/subdivisions if geoip is disabled', async function () {
|
|
this.timeout(120000)
|
|
|
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
|
await waitJobs(servers)
|
|
|
|
await servers[1].views.view({ id: uuid, sessionId: generateSessionId(), xForwardedFor: '8.8.8.8,127.0.0.1', currentTime: 1 })
|
|
|
|
await processViewersStats(servers)
|
|
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
|
|
expect(stats.countries).to.have.lengthOf(0)
|
|
expect(stats.subdivisions).to.have.lengthOf(0)
|
|
})
|
|
|
|
it('Should not report subdivisions if database URL is not provided in the configuration', async function () {
|
|
this.timeout(240000)
|
|
|
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video without subdivisions' })
|
|
await waitJobs(servers)
|
|
|
|
await Promise.all([ servers[0].kill(), servers[1].kill() ])
|
|
|
|
const config = { geo_ip: { enabled: true, city: { database_url: '' } } }
|
|
await Promise.all([ servers[0].run(config), servers[1].run(config) ])
|
|
|
|
await servers[0].views.simulateViewer({
|
|
id: uuid,
|
|
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)
|
|
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
|
|
|
|
expect(stats.countries).to.have.lengthOf(2)
|
|
expect(stats.subdivisions).to.have.lengthOf(0)
|
|
})
|
|
|
|
it('Should report countries/subdivisions if geoip is enabled', async function () {
|
|
this.timeout(240000)
|
|
|
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
|
videoUUID = uuid
|
|
await waitJobs(servers)
|
|
|
|
await Promise.all([
|
|
servers[0].kill(),
|
|
servers[1].kill()
|
|
])
|
|
|
|
const config = { geo_ip: { enabled: true } }
|
|
await Promise.all([
|
|
servers[0].run(config),
|
|
servers[1].run(config)
|
|
])
|
|
|
|
await servers[0].views.simulateViewer({
|
|
id: uuid,
|
|
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)
|
|
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: uuid })
|
|
|
|
expect(stats.countries).to.have.lengthOf(2)
|
|
|
|
expect(stats.countries[0].isoCode).to.equal('US')
|
|
expect(stats.countries[0].viewers).to.equal(2)
|
|
|
|
expect(stats.countries[1].isoCode).to.equal('FR')
|
|
expect(stats.countries[1].viewers).to.equal(1)
|
|
|
|
expect(stats.subdivisions[0].name).to.equal('California')
|
|
expect(stats.subdivisions[0].viewers).to.equal(2)
|
|
|
|
expect(stats.subdivisions[1].name).to.equal('Brittany')
|
|
expect(stats.subdivisions[1].viewers).to.equal(1)
|
|
})
|
|
|
|
it('Should filter countries/subdivisions stats by date', async function () {
|
|
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoUUID, startDate: new Date().toISOString() })
|
|
expect(stats.countries).to.have.lengthOf(0)
|
|
expect(stats.subdivisions).to.have.lengthOf(0)
|
|
})
|
|
})
|
|
|
|
after(async function () {
|
|
await cleanupTests(servers)
|
|
})
|
|
}
|
|
|
|
describe('Not using session id', function () {
|
|
runTests({ useSessionId: false })
|
|
})
|
|
|
|
describe('Using session id', function () {
|
|
runTests({ useSessionId: true })
|
|
})
|
|
})
|