aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/job-queue/handlers/generate-storyboard.ts
diff options
context:
space:
mode:
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}