mirror of https://github.com/Chocobozzz/PeerTube
Process images in a dedicated worker
parent
88edc66eda
commit
3a54605d4e
|
@ -110,7 +110,7 @@ async function generateSmallerAvatar (actor: MActorDefault) {
|
||||||
const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
|
const source = join(CONFIG.STORAGE.ACTOR_IMAGES, sourceFilename)
|
||||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName)
|
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, newImageName)
|
||||||
|
|
||||||
await processImage(source, destination, imageSize, true)
|
await processImage({ path: source, destination, newSize: imageSize, keepOriginal: true })
|
||||||
|
|
||||||
const actorImageInfo = {
|
const actorImageInfo = {
|
||||||
name: newImageName,
|
name: newImageName,
|
||||||
|
|
|
@ -52,7 +52,7 @@ async function processVideo (id: number) {
|
||||||
thumbnail.height = size.height
|
thumbnail.height = size.height
|
||||||
|
|
||||||
const thumbnailPath = thumbnail.getPath()
|
const thumbnailPath = thumbnail.getPath()
|
||||||
await processImage(previewPath, thumbnailPath, size, true)
|
await processImage({ path: previewPath, destination: thumbnailPath, newSize: size, keepOriginal: true })
|
||||||
|
|
||||||
// Save new attributes
|
// Save new attributes
|
||||||
await thumbnail.save()
|
await thumbnail.save()
|
||||||
|
|
|
@ -12,12 +12,14 @@ function generateImageFilename (extension = '.jpg') {
|
||||||
return buildUUID() + extension
|
return buildUUID() + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processImage (
|
async function processImage (options: {
|
||||||
path: string,
|
path: string
|
||||||
destination: string,
|
destination: string
|
||||||
newSize: { width: number, height: number },
|
newSize: { width: number, height: number }
|
||||||
keepOriginal = false
|
keepOriginal?: boolean // default false
|
||||||
) {
|
}) {
|
||||||
|
const { path, destination, newSize, keepOriginal = false } = options
|
||||||
|
|
||||||
const extension = getLowercaseExtension(path)
|
const extension = getLowercaseExtension(path)
|
||||||
|
|
||||||
if (path === destination) {
|
if (path === destination) {
|
||||||
|
@ -36,7 +38,14 @@ async function processImage (
|
||||||
if (keepOriginal !== true) await remove(path)
|
if (keepOriginal !== true) await remove(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) {
|
async function generateImageFromVideoFile (options: {
|
||||||
|
fromPath: string
|
||||||
|
folder: string
|
||||||
|
imageName: string
|
||||||
|
size: { width: number, height: number }
|
||||||
|
}) {
|
||||||
|
const { fromPath, folder, imageName, size } = options
|
||||||
|
|
||||||
const pendingImageName = 'pending-' + imageName
|
const pendingImageName = 'pending-' + imageName
|
||||||
const pendingImagePath = join(folder, pendingImageName)
|
const pendingImagePath = join(folder, pendingImageName)
|
||||||
|
|
||||||
|
@ -44,7 +53,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
|
||||||
await generateThumbnailFromVideo(fromPath, folder, imageName)
|
await generateThumbnailFromVideo(fromPath, folder, imageName)
|
||||||
|
|
||||||
const destination = join(folder, imageName)
|
const destination = join(folder, imageName)
|
||||||
await processImage(pendingImagePath, destination, size)
|
await processImage({ path: pendingImagePath, destination, newSize: size })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() })
|
logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() })
|
||||||
|
|
||||||
|
|
|
@ -748,6 +748,10 @@ const WORKER_THREADS = {
|
||||||
DOWNLOAD_IMAGE: {
|
DOWNLOAD_IMAGE: {
|
||||||
CONCURRENCY: 3,
|
CONCURRENCY: 3,
|
||||||
MAX_THREADS: 1
|
MAX_THREADS: 1
|
||||||
|
},
|
||||||
|
PROCESS_IMAGE: {
|
||||||
|
CONCURRENCY: 1,
|
||||||
|
MAX_THREADS: 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,13 @@ import { getLowercaseExtension } from '@shared/core-utils'
|
||||||
import { buildUUID } from '@shared/extra-utils'
|
import { buildUUID } from '@shared/extra-utils'
|
||||||
import { ActivityPubActorType, ActorImageType } from '@shared/models'
|
import { ActivityPubActorType, ActorImageType } from '@shared/models'
|
||||||
import { retryTransactionWrapper } from '../helpers/database-utils'
|
import { retryTransactionWrapper } from '../helpers/database-utils'
|
||||||
import { processImage } from '../helpers/image-utils'
|
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants'
|
import { ACTOR_IMAGES_SIZE, LRU_CACHE, WEBSERVER } from '../initializers/constants'
|
||||||
import { sequelizeTypescript } from '../initializers/database'
|
import { sequelizeTypescript } from '../initializers/database'
|
||||||
import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
|
import { MAccountDefault, MActor, MChannelDefault } from '../types/models'
|
||||||
import { deleteActorImages, updateActorImages } from './activitypub/actors'
|
import { deleteActorImages, updateActorImages } from './activitypub/actors'
|
||||||
import { sendUpdateActor } from './activitypub/send'
|
import { sendUpdateActor } from './activitypub/send'
|
||||||
import { downloadImageFromWorker } from './worker/parent-process'
|
import { downloadImageFromWorker, processImageFromWorker } from './worker/parent-process'
|
||||||
|
|
||||||
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
|
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
|
||||||
return new ActorModel({
|
return new ActorModel({
|
||||||
|
@ -42,7 +41,7 @@ async function updateLocalActorImageFiles (
|
||||||
|
|
||||||
const imageName = buildUUID() + extension
|
const imageName = buildUUID() + extension
|
||||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
|
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
|
||||||
await processImage(imagePhysicalFile.path, destination, imageSize, true)
|
await processImageFromWorker({ path: imagePhysicalFile.path, destination, newSize: imageSize, keepOriginal: true })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageName,
|
imageName,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { ThumbnailType } from '@shared/models'
|
import { ThumbnailType } from '@shared/models'
|
||||||
import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils'
|
import { generateImageFilename, generateImageFromVideoFile } from '../helpers/image-utils'
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
|
import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
|
||||||
import { ThumbnailModel } from '../models/video/thumbnail'
|
import { ThumbnailModel } from '../models/video/thumbnail'
|
||||||
|
@ -9,6 +9,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
|
||||||
import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
|
import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
|
||||||
import { downloadImageFromWorker } from './local-actor'
|
import { downloadImageFromWorker } from './local-actor'
|
||||||
import { VideoPathManager } from './video-path-manager'
|
import { VideoPathManager } from './video-path-manager'
|
||||||
|
import { processImageFromWorker } from './worker/parent-process'
|
||||||
|
|
||||||
type ImageSize = { height?: number, width?: number }
|
type ImageSize = { height?: number, width?: number }
|
||||||
|
|
||||||
|
@ -23,7 +24,10 @@ function updatePlaylistMiniatureFromExisting (options: {
|
||||||
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
|
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
|
||||||
const type = ThumbnailType.MINIATURE
|
const type = ThumbnailType.MINIATURE
|
||||||
|
|
||||||
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
|
const thumbnailCreator = () => {
|
||||||
|
return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
|
||||||
|
}
|
||||||
|
|
||||||
return updateThumbnailFromFunction({
|
return updateThumbnailFromFunction({
|
||||||
thumbnailCreator,
|
thumbnailCreator,
|
||||||
filename,
|
filename,
|
||||||
|
@ -99,7 +103,10 @@ function updateVideoMiniatureFromExisting (options: {
|
||||||
const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
|
const { inputPath, video, type, automaticallyGenerated, size, keepOriginal = false } = options
|
||||||
|
|
||||||
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
|
||||||
const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
|
|
||||||
|
const thumbnailCreator = () => {
|
||||||
|
return processImageFromWorker({ path: inputPath, destination: outputPath, newSize: { width, height }, keepOriginal })
|
||||||
|
}
|
||||||
|
|
||||||
return updateThumbnailFromFunction({
|
return updateThumbnailFromFunction({
|
||||||
thumbnailCreator,
|
thumbnailCreator,
|
||||||
|
@ -123,8 +130,18 @@ function generateVideoMiniature (options: {
|
||||||
const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
|
const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
|
||||||
|
|
||||||
const thumbnailCreator = videoFile.isAudio()
|
const thumbnailCreator = videoFile.isAudio()
|
||||||
? () => processImage(ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND, outputPath, { width, height }, true)
|
? () => processImageFromWorker({
|
||||||
: () => generateImageFromVideoFile(input, basePath, filename, { height, width })
|
path: ASSETS_PATH.DEFAULT_AUDIO_BACKGROUND,
|
||||||
|
destination: outputPath,
|
||||||
|
newSize: { width, height },
|
||||||
|
keepOriginal: true
|
||||||
|
})
|
||||||
|
: () => generateImageFromVideoFile({
|
||||||
|
fromPath: input,
|
||||||
|
folder: basePath,
|
||||||
|
imageName: filename,
|
||||||
|
size: { height, width }
|
||||||
|
})
|
||||||
|
|
||||||
return updateThumbnailFromFunction({
|
return updateThumbnailFromFunction({
|
||||||
thumbnailCreator,
|
thumbnailCreator,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { join } from 'path'
|
||||||
import Piscina from 'piscina'
|
import Piscina from 'piscina'
|
||||||
import { WORKER_THREADS } from '@server/initializers/constants'
|
import { WORKER_THREADS } from '@server/initializers/constants'
|
||||||
import { downloadImage } from './workers/image-downloader'
|
import { downloadImage } from './workers/image-downloader'
|
||||||
|
import { processImage } from '@server/helpers/image-utils'
|
||||||
|
|
||||||
const downloadImagerWorker = new Piscina({
|
const downloadImagerWorker = new Piscina({
|
||||||
filename: join(__dirname, 'workers', 'image-downloader.js'),
|
filename: join(__dirname, 'workers', 'image-downloader.js'),
|
||||||
|
@ -13,6 +14,19 @@ function downloadImageFromWorker (options: Parameters<typeof downloadImage>[0]):
|
||||||
return downloadImagerWorker.run(options)
|
return downloadImagerWorker.run(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
// ---------------------------------------------------------------------------
|
||||||
downloadImageFromWorker
|
|
||||||
|
const processImageWorker = new Piscina({
|
||||||
|
filename: join(__dirname, 'workers', 'image-processor.js'),
|
||||||
|
concurrentTasksPerWorker: WORKER_THREADS.DOWNLOAD_IMAGE.CONCURRENCY,
|
||||||
|
maxThreads: WORKER_THREADS.DOWNLOAD_IMAGE.MAX_THREADS
|
||||||
|
})
|
||||||
|
|
||||||
|
function processImageFromWorker (options: Parameters<typeof processImage>[0]): Promise<ReturnType<typeof processImage>> {
|
||||||
|
return processImageWorker.run(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
downloadImageFromWorker,
|
||||||
|
processImageFromWorker
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ async function downloadImage (options: {
|
||||||
const destPath = join(destDir, destName)
|
const destPath = join(destDir, destName)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await processImage(tmpPath, destPath, size)
|
await processImage({ path: tmpPath, destination: destPath, newSize: size })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await remove(tmpPath)
|
await remove(tmpPath)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { processImage } from '@server/helpers/image-utils'
|
||||||
|
|
||||||
|
module.exports = processImage
|
||||||
|
|
||||||
|
export {
|
||||||
|
processImage
|
||||||
|
}
|
|
@ -37,28 +37,28 @@ describe('Image helpers', function () {
|
||||||
|
|
||||||
it('Should skip processing if the source image is okay', async function () {
|
it('Should skip processing if the source image is okay', async function () {
|
||||||
const input = buildAbsoluteFixturePath('thumbnail.jpg')
|
const input = buildAbsoluteFixturePath('thumbnail.jpg')
|
||||||
await processImage(input, imageDestJPG, thumbnailSize, true)
|
await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
|
||||||
|
|
||||||
await checkBuffers(input, imageDestJPG, true)
|
await checkBuffers(input, imageDestJPG, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not skip processing if the source image does not have the appropriate extension', async function () {
|
it('Should not skip processing if the source image does not have the appropriate extension', async function () {
|
||||||
const input = buildAbsoluteFixturePath('thumbnail.png')
|
const input = buildAbsoluteFixturePath('thumbnail.png')
|
||||||
await processImage(input, imageDestJPG, thumbnailSize, true)
|
await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
|
||||||
|
|
||||||
await checkBuffers(input, imageDestJPG, false)
|
await checkBuffers(input, imageDestJPG, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not skip processing if the source image does not have the appropriate size', async function () {
|
it('Should not skip processing if the source image does not have the appropriate size', async function () {
|
||||||
const input = buildAbsoluteFixturePath('preview.jpg')
|
const input = buildAbsoluteFixturePath('preview.jpg')
|
||||||
await processImage(input, imageDestJPG, thumbnailSize, true)
|
await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
|
||||||
|
|
||||||
await checkBuffers(input, imageDestJPG, false)
|
await checkBuffers(input, imageDestJPG, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not skip processing if the source image does not have the appropriate size', async function () {
|
it('Should not skip processing if the source image does not have the appropriate size', async function () {
|
||||||
const input = buildAbsoluteFixturePath('thumbnail-big.jpg')
|
const input = buildAbsoluteFixturePath('thumbnail-big.jpg')
|
||||||
await processImage(input, imageDestJPG, thumbnailSize, true)
|
await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
|
||||||
|
|
||||||
await checkBuffers(input, imageDestJPG, false)
|
await checkBuffers(input, imageDestJPG, false)
|
||||||
})
|
})
|
||||||
|
@ -67,7 +67,7 @@ describe('Image helpers', function () {
|
||||||
const input = buildAbsoluteFixturePath('exif.jpg')
|
const input = buildAbsoluteFixturePath('exif.jpg')
|
||||||
expect(await hasTitleExif(input)).to.be.true
|
expect(await hasTitleExif(input)).to.be.true
|
||||||
|
|
||||||
await processImage(input, imageDestJPG, { width: 100, height: 100 }, true)
|
await processImage({ path: input, destination: imageDestJPG, newSize: { width: 100, height: 100 }, keepOriginal: true })
|
||||||
await checkBuffers(input, imageDestJPG, false)
|
await checkBuffers(input, imageDestJPG, false)
|
||||||
|
|
||||||
expect(await hasTitleExif(imageDestJPG)).to.be.false
|
expect(await hasTitleExif(imageDestJPG)).to.be.false
|
||||||
|
@ -77,7 +77,7 @@ describe('Image helpers', function () {
|
||||||
const input = buildAbsoluteFixturePath('exif.jpg')
|
const input = buildAbsoluteFixturePath('exif.jpg')
|
||||||
expect(await hasTitleExif(input)).to.be.true
|
expect(await hasTitleExif(input)).to.be.true
|
||||||
|
|
||||||
await processImage(input, imageDestJPG, thumbnailSize, true)
|
await processImage({ path: input, destination: imageDestJPG, newSize: thumbnailSize, keepOriginal: true })
|
||||||
await checkBuffers(input, imageDestJPG, false)
|
await checkBuffers(input, imageDestJPG, false)
|
||||||
|
|
||||||
expect(await hasTitleExif(imageDestJPG)).to.be.false
|
expect(await hasTitleExif(imageDestJPG)).to.be.false
|
||||||
|
@ -87,7 +87,7 @@ describe('Image helpers', function () {
|
||||||
const input = buildAbsoluteFixturePath('exif.png')
|
const input = buildAbsoluteFixturePath('exif.png')
|
||||||
expect(await hasTitleExif(input)).to.be.true
|
expect(await hasTitleExif(input)).to.be.true
|
||||||
|
|
||||||
await processImage(input, imageDestPNG, thumbnailSize, true)
|
await processImage({ path: input, destination: imageDestPNG, newSize: thumbnailSize, keepOriginal: true })
|
||||||
expect(await hasTitleExif(imageDestPNG)).to.be.false
|
expect(await hasTitleExif(imageDestPNG)).to.be.false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue