Optimize view endpoint
This commit is contained in:
parent
fba911e2c8
commit
aa2ce188d1
9 changed files with 58 additions and 32 deletions
|
@ -46,10 +46,12 @@ async function prepare () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const env = { PRODUCTION_CONSTANTS: 'true' }
|
||||||
|
|
||||||
servers = await Promise.all([
|
servers = await Promise.all([
|
||||||
createSingleServer(1, config, { nodeArgs: [ '--inspect' ] }),
|
createSingleServer(1, config, { env, nodeArgs: [ '--inspect' ] }),
|
||||||
createSingleServer(2, config),
|
createSingleServer(2, config, { env }),
|
||||||
createSingleServer(3, config)
|
createSingleServer(3, config, { env })
|
||||||
])
|
])
|
||||||
|
|
||||||
await setAccessTokensToServers(servers)
|
await setAccessTokensToServers(servers)
|
||||||
|
@ -81,7 +83,7 @@ async function runViewers () {
|
||||||
|
|
||||||
await Bluebird.map(viewers, viewer => {
|
await Bluebird.map(viewers, viewer => {
|
||||||
return servers[0].views.simulateView({ id: videoId, xForwardedFor: viewer.xForwardedFor })
|
return servers[0].views.simulateView({ id: videoId, xForwardedFor: viewer.xForwardedFor })
|
||||||
}, { concurrency: 100 })
|
}, { concurrency: 500 })
|
||||||
|
|
||||||
console.log('Finished to run views in %d seconds.', (new Date().getTime() - before) / 1000)
|
console.log('Finished to run views in %d seconds.', (new Date().getTime() - before) / 1000)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ export {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function viewVideo (req: express.Request, res: express.Response) {
|
async function viewVideo (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.onlyVideo
|
const video = res.locals.onlyImmutableVideo
|
||||||
|
|
||||||
const body = req.body as VideoView
|
const body = req.body as VideoView
|
||||||
|
|
||||||
|
|
|
@ -734,12 +734,14 @@ const VIDEO_LIVE = {
|
||||||
const MEMOIZE_TTL = {
|
const MEMOIZE_TTL = {
|
||||||
OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
|
OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
|
||||||
INFO_HASH_EXISTS: 1000 * 3600 * 12, // 12 hours
|
INFO_HASH_EXISTS: 1000 * 3600 * 12, // 12 hours
|
||||||
|
VIDEO_DURATION: 1000 * 10, // 10 seconds
|
||||||
LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute
|
LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute
|
||||||
LIVE_CHECK_SOCKET_HEALTH: 1000 * 60 // 1 minute
|
LIVE_CHECK_SOCKET_HEALTH: 1000 * 60 // 1 minute
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEMOIZE_LENGTH = {
|
const MEMOIZE_LENGTH = {
|
||||||
INFO_HASH_EXISTS: 200
|
INFO_HASH_EXISTS: 200,
|
||||||
|
VIDEO_DURATION: 200
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUEUE_CONCURRENCY = {
|
const QUEUE_CONCURRENCY = {
|
||||||
|
@ -812,7 +814,7 @@ const STATS_TIMESERIE = {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Special constants for a test instance
|
// Special constants for a test instance
|
||||||
if (isTestInstance() === true) {
|
if (isTestInstance() === true && process.env.PRODUCTION_CONSTANTS !== 'true') {
|
||||||
PRIVATE_RSA_KEY_SIZE = 1024
|
PRIVATE_RSA_KEY_SIZE = 1024
|
||||||
|
|
||||||
ACTOR_FOLLOW_SCORE.BASE = 20
|
ACTOR_FOLLOW_SCORE.BASE = 20
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { UploadFiles } from 'express'
|
import { UploadFiles } from 'express'
|
||||||
import { Transaction } from 'sequelize/types'
|
import { Transaction } from 'sequelize/types'
|
||||||
import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY } from '@server/initializers/constants'
|
import { DEFAULT_AUDIO_RESOLUTION, JOB_PRIORITY, MEMOIZE_LENGTH, MEMOIZE_TTL } from '@server/initializers/constants'
|
||||||
import { TagModel } from '@server/models/video/tag'
|
import { TagModel } from '@server/models/video/tag'
|
||||||
import { VideoModel } from '@server/models/video/video'
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
import { VideoJobInfoModel } from '@server/models/video/video-job-info'
|
||||||
|
@ -10,6 +10,7 @@ import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingP
|
||||||
import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
|
import { CreateJobOptions, JobQueue } from './job-queue/job-queue'
|
||||||
import { updateVideoMiniatureFromExisting } from './thumbnail'
|
import { updateVideoMiniatureFromExisting } from './thumbnail'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
|
import memoizee from 'memoizee'
|
||||||
|
|
||||||
function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> {
|
function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes<VideoModel> {
|
||||||
return {
|
return {
|
||||||
|
@ -150,6 +151,24 @@ async function addMoveToObjectStorageJob (options: {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function getVideoDuration (videoId: number | string) {
|
||||||
|
const video = await VideoModel.load(videoId)
|
||||||
|
|
||||||
|
const duration = video.isLive
|
||||||
|
? undefined
|
||||||
|
: video.duration
|
||||||
|
|
||||||
|
return { duration, isLive: video.isLive }
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCachedVideoDuration = memoizee(getVideoDuration, {
|
||||||
|
promise: true,
|
||||||
|
max: MEMOIZE_LENGTH.VIDEO_DURATION,
|
||||||
|
maxAge: MEMOIZE_TTL.VIDEO_DURATION
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
buildLocalVideoFromReq,
|
buildLocalVideoFromReq,
|
||||||
buildVideoThumbnailsFromReq,
|
buildVideoThumbnailsFromReq,
|
||||||
|
@ -157,5 +176,6 @@ export {
|
||||||
addOptimizeOrMergeAudioJob,
|
addOptimizeOrMergeAudioJob,
|
||||||
addTranscodingJob,
|
addTranscodingJob,
|
||||||
addMoveToObjectStorageJob,
|
addMoveToObjectStorageJob,
|
||||||
getTranscodingJobPriority
|
getTranscodingJobPriority,
|
||||||
|
getCachedVideoDuration
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { sendView } from '@server/lib/activitypub/send/send-view'
|
||||||
import { PeerTubeSocket } from '@server/lib/peertube-socket'
|
import { PeerTubeSocket } from '@server/lib/peertube-socket'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { VideoModel } from '@server/models/video/video'
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { MVideo } from '@server/types/models'
|
import { MVideo, MVideoImmutable } from '@server/types/models'
|
||||||
import { buildUUID, sha256 } from '@shared/extra-utils'
|
import { buildUUID, sha256 } from '@shared/extra-utils'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('views')
|
const lTags = loggerTagsFactory('views')
|
||||||
|
@ -33,7 +33,7 @@ export class VideoViewerCounters {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async addLocalViewer (options: {
|
async addLocalViewer (options: {
|
||||||
video: MVideo
|
video: MVideoImmutable
|
||||||
ip: string
|
ip: string
|
||||||
}) {
|
}) {
|
||||||
const { video, ip } = options
|
const { video, ip } = options
|
||||||
|
@ -86,7 +86,7 @@ export class VideoViewerCounters {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private async addViewerToVideo (options: {
|
private async addViewerToVideo (options: {
|
||||||
video: MVideo
|
video: MVideoImmutable
|
||||||
viewerId: string
|
viewerId: string
|
||||||
viewerExpires?: Date
|
viewerExpires?: Date
|
||||||
}) {
|
}) {
|
||||||
|
@ -162,7 +162,7 @@ export class VideoViewerCounters {
|
||||||
return sha256(this.salt + '-' + ip + '-' + videoUUID)
|
return sha256(this.salt + '-' + ip + '-' + videoUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async federateViewerIfNeeded (video: MVideo, 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()
|
||||||
const federationLimit = now - (VIEW_LIFETIME.VIEWER_COUNTER / 2)
|
const federationLimit = now - (VIEW_LIFETIME.VIEWER_COUNTER / 2)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Redis } from '@server/lib/redis'
|
||||||
import { VideoModel } from '@server/models/video/video'
|
import { VideoModel } from '@server/models/video/video'
|
||||||
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
|
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
|
||||||
import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section'
|
import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section'
|
||||||
import { MVideo } from '@server/types/models'
|
import { MVideo, MVideoImmutable } from '@server/types/models'
|
||||||
import { VideoViewEvent } from '@shared/models'
|
import { VideoViewEvent } from '@shared/models'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('views')
|
const lTags = loggerTagsFactory('views')
|
||||||
|
@ -41,7 +41,7 @@ export class VideoViewerStats {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async addLocalViewer (options: {
|
async addLocalViewer (options: {
|
||||||
video: MVideo
|
video: MVideoImmutable
|
||||||
currentTime: number
|
currentTime: number
|
||||||
ip: string
|
ip: string
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
|
@ -64,7 +64,7 @@ export class VideoViewerStats {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private async updateLocalViewerStats (options: {
|
private async updateLocalViewerStats (options: {
|
||||||
video: MVideo
|
video: MVideoImmutable
|
||||||
ip: string
|
ip: string
|
||||||
currentTime: number
|
currentTime: number
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { sendView } from '@server/lib/activitypub/send/send-view'
|
import { sendView } from '@server/lib/activitypub/send/send-view'
|
||||||
|
import { getCachedVideoDuration } from '@server/lib/video'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { MVideo } from '@server/types/models'
|
import { MVideo, MVideoImmutable } from '@server/types/models'
|
||||||
import { buildUUID } from '@shared/extra-utils'
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
import { Redis } from '../../redis'
|
import { Redis } from '../../redis'
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ const lTags = loggerTagsFactory('views')
|
||||||
export class VideoViews {
|
export class VideoViews {
|
||||||
|
|
||||||
async addLocalView (options: {
|
async addLocalView (options: {
|
||||||
video: MVideo
|
video: MVideoImmutable
|
||||||
ip: string
|
ip: string
|
||||||
watchTime: number
|
watchTime: number
|
||||||
}) {
|
}) {
|
||||||
|
@ -18,7 +19,7 @@ export class VideoViews {
|
||||||
|
|
||||||
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 (!this.hasEnoughWatchTime(video, watchTime)) return false
|
if (!await this.hasEnoughWatchTime(video, watchTime)) return false
|
||||||
|
|
||||||
const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid)
|
const viewExists = await Redis.Instance.doesVideoIPViewExist(ip, video.uuid)
|
||||||
if (viewExists) return false
|
if (viewExists) return false
|
||||||
|
@ -46,7 +47,7 @@ export class VideoViews {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private async addView (video: MVideo) {
|
private async addView (video: MVideoImmutable) {
|
||||||
const promises: Promise<any>[] = []
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
if (video.isOwned()) {
|
if (video.isOwned()) {
|
||||||
|
@ -58,10 +59,12 @@ export class VideoViews {
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasEnoughWatchTime (video: MVideo, watchTime: number) {
|
private async hasEnoughWatchTime (video: MVideoImmutable, watchTime: number) {
|
||||||
if (video.isLive || video.duration >= 30) return watchTime >= 30
|
const { duration, isLive } = await getCachedVideoDuration(video.id)
|
||||||
|
|
||||||
|
if (isLive || duration >= 30) return watchTime >= 30
|
||||||
|
|
||||||
// Check more than 50% of the video is watched
|
// Check more than 50% of the video is watched
|
||||||
return video.duration / watchTime < 2
|
return duration / watchTime < 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger'
|
||||||
import { MVideo } from '@server/types/models'
|
import { MVideo, MVideoImmutable } from '@server/types/models'
|
||||||
import { VideoViewEvent } from '@shared/models'
|
import { VideoViewEvent } from '@shared/models'
|
||||||
import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared'
|
import { VideoViewerCounters, VideoViewerStats, VideoViews } from './shared'
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export class VideoViewsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async processLocalView (options: {
|
async processLocalView (options: {
|
||||||
video: MVideo
|
video: MVideoImmutable
|
||||||
currentTime: number
|
currentTime: number
|
||||||
ip: string | null
|
ip: string | null
|
||||||
viewEvent?: VideoViewEvent
|
viewEvent?: VideoViewEvent
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
|
||||||
import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
import { exists, isIdValid, isIntOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
|
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
|
||||||
|
import { getCachedVideoDuration } from '@server/lib/video'
|
||||||
|
|
||||||
const getVideoLocalViewerValidator = [
|
const getVideoLocalViewerValidator = [
|
||||||
param('localViewerId')
|
param('localViewerId')
|
||||||
|
@ -42,20 +43,18 @@ const videoViewValidator = [
|
||||||
logger.debug('Checking videoView parameters', { parameters: req.body })
|
logger.debug('Checking videoView parameters', { parameters: req.body })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
|
if (!await doesVideoExist(req.params.videoId, res, 'only-immutable-attributes')) return
|
||||||
|
|
||||||
const video = res.locals.onlyVideo
|
const video = res.locals.onlyImmutableVideo
|
||||||
const videoDuration = video.isLive
|
const { duration } = await getCachedVideoDuration(video.id)
|
||||||
? undefined
|
|
||||||
: video.duration
|
|
||||||
|
|
||||||
if (!exists(req.body.currentTime)) { // TODO: remove in a few versions, introduced in 4.2
|
if (!exists(req.body.currentTime)) { // TODO: remove in a few versions, introduced in 4.2
|
||||||
req.body.currentTime = Math.min(videoDuration ?? 0, 30)
|
req.body.currentTime = Math.min(duration ?? 0, 30)
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTime: number = req.body.currentTime
|
const currentTime: number = req.body.currentTime
|
||||||
|
|
||||||
if (!isVideoTimeValid(currentTime, videoDuration)) {
|
if (!isVideoTimeValid(currentTime, duration)) {
|
||||||
return res.fail({
|
return res.fail({
|
||||||
status: HttpStatusCode.BAD_REQUEST_400,
|
status: HttpStatusCode.BAD_REQUEST_400,
|
||||||
message: 'Current time is invalid'
|
message: 'Current time is invalid'
|
||||||
|
|
Loading…
Reference in a new issue