aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/job-queue/handlers/generate-storyboard.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/lib/job-queue/handlers/generate-storyboard.ts
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/lib/job-queue/handlers/generate-storyboard.ts')
-rw-r--r--server/lib/job-queue/handlers/generate-storyboard.ts163
1 files changed, 0 insertions, 163 deletions
diff --git a/server/lib/job-queue/handlers/generate-storyboard.ts b/server/lib/job-queue/handlers/generate-storyboard.ts
deleted file mode 100644
index eea20274a..000000000
--- a/server/lib/job-queue/handlers/generate-storyboard.ts
+++ /dev/null
@@ -1,163 +0,0 @@
1import { Job } from 'bullmq'
2import { join } from 'path'
3import { retryTransactionWrapper } from '@server/helpers/database-utils'
4import { getFFmpegCommandWrapperOptions } from '@server/helpers/ffmpeg'
5import { generateImageFilename, getImageSize } from '@server/helpers/image-utils'
6import { logger, loggerTagsFactory } from '@server/helpers/logger'
7import { deleteFileAndCatch } from '@server/helpers/utils'
8import { CONFIG } from '@server/initializers/config'
9import { STORYBOARD } from '@server/initializers/constants'
10import { sequelizeTypescript } from '@server/initializers/database'
11import { federateVideoIfNeeded } from '@server/lib/activitypub/videos'
12import { VideoPathManager } from '@server/lib/video-path-manager'
13import { StoryboardModel } from '@server/models/video/storyboard'
14import { VideoModel } from '@server/models/video/video'
15import { MVideo } from '@server/types/models'
16import { FFmpegImage, isAudioFile } from '@shared/ffmpeg'
17import { GenerateStoryboardPayload } from '@shared/models'
18
19const lTagsBase = loggerTagsFactory('storyboard')
20
21async function processGenerateStoryboard (job: Job): Promise<void> {
22 const payload = job.data as GenerateStoryboardPayload
23 const lTags = lTagsBase(payload.videoUUID)
24
25 logger.info('Processing generate storyboard of %s in job %s.', payload.videoUUID, job.id, lTags)
26
27 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(payload.videoUUID)
28
29 try {
30 const video = await VideoModel.loadFull(payload.videoUUID)
31 if (!video) {
32 logger.info('Video %s does not exist anymore, skipping storyboard generation.', payload.videoUUID, lTags)
33 return
34 }
35
36 const inputFile = video.getMaxQualityFile()
37
38 await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async videoPath => {
39 const isAudio = await isAudioFile(videoPath)
40
41 if (isAudio) {
42 logger.info('Do not generate a storyboard of %s since the video does not have a video stream', payload.videoUUID, lTags)
43 return
44 }
45
46 const ffmpeg = new FFmpegImage(getFFmpegCommandWrapperOptions('thumbnail'))
47
48 const filename = generateImageFilename()
49 const destination = join(CONFIG.STORAGE.STORYBOARDS_DIR, filename)
50
51 const totalSprites = buildTotalSprites(video)
52 if (totalSprites === 0) {
53 logger.info('Do not generate a storyboard of %s because the video is not long enough', payload.videoUUID, lTags)
54 return
55 }
56
57 const spriteDuration = Math.round(video.duration / totalSprites)
58
59 const spritesCount = findGridSize({
60 toFind: totalSprites,
61 maxEdgeCount: STORYBOARD.SPRITES_MAX_EDGE_COUNT
62 })
63
64 logger.debug(
65 'Generating storyboard from video of %s to %s', video.uuid, destination,
66 { ...lTags, spritesCount, spriteDuration, videoDuration: video.duration }
67 )
68
69 await ffmpeg.generateStoryboardFromVideo({
70 destination,
71 path: videoPath,
72 sprites: {
73 size: STORYBOARD.SPRITE_SIZE,
74 count: spritesCount,
75 duration: spriteDuration
76 }
77 })
78
79 const imageSize = await getImageSize(destination)
80
81 await retryTransactionWrapper(() => {
82 return sequelizeTypescript.transaction(async transaction => {
83 const videoStillExists = await VideoModel.load(video.id, transaction)
84 if (!videoStillExists) {
85 logger.info('Video %s does not exist anymore, skipping storyboard generation.', payload.videoUUID, lTags)
86 deleteFileAndCatch(destination)
87 return
88 }
89
90 const existing = await StoryboardModel.loadByVideo(video.id, transaction)
91 if (existing) await existing.destroy({ transaction })
92
93 await StoryboardModel.create({
94 filename,
95 totalHeight: imageSize.height,
96 totalWidth: imageSize.width,
97 spriteHeight: STORYBOARD.SPRITE_SIZE.height,
98 spriteWidth: STORYBOARD.SPRITE_SIZE.width,
99 spriteDuration,
100 videoId: video.id
101 }, { transaction })
102
103 logger.info('Storyboard generation %s ended for video %s.', destination, video.uuid, lTags)
104
105 if (payload.federate) {
106 await federateVideoIfNeeded(video, false, transaction)
107 }
108 })
109 })
110 })
111 } finally {
112 inputFileMutexReleaser()
113 }
114}
115
116// ---------------------------------------------------------------------------
117
118export {
119 processGenerateStoryboard
120}
121
122function buildTotalSprites (video: MVideo) {
123 const maxSprites = STORYBOARD.SPRITE_SIZE.height * STORYBOARD.SPRITE_SIZE.width
124 const totalSprites = Math.min(Math.ceil(video.duration), maxSprites)
125
126 // We can generate a single line
127 if (totalSprites <= STORYBOARD.SPRITES_MAX_EDGE_COUNT) return totalSprites
128
129 return findGridFit(totalSprites, STORYBOARD.SPRITES_MAX_EDGE_COUNT)
130}
131
132function findGridSize (options: {
133 toFind: number
134 maxEdgeCount: number
135}) {
136 const { toFind, maxEdgeCount } = options
137
138 for (let i = 1; i <= maxEdgeCount; i++) {
139 for (let j = i; j <= maxEdgeCount; j++) {
140 if (toFind === i * j) return { width: j, height: i }
141 }
142 }
143
144 throw new Error(`Could not find grid size (to find: ${toFind}, max edge count: ${maxEdgeCount}`)
145}
146
147function findGridFit (value: number, maxMultiplier: number) {
148 for (let i = value; i--; i > 0) {
149 if (!isPrimeWithin(i, maxMultiplier)) return i
150 }
151
152 throw new Error('Could not find prime number below ' + value)
153}
154
155function isPrimeWithin (value: number, maxMultiplier: number) {
156 if (value < 2) return false
157
158 for (let i = 2, end = Math.min(Math.sqrt(value), maxMultiplier); i <= end; i++) {
159 if (value % i === 0 && value / i <= maxMultiplier) return false
160 }
161
162 return true
163}