unoffical wafrn mirror
wafrn.net
atproto
social-network
activitypub
1import { logger } from './logger.js'
2
3import sharp from 'sharp'
4
5/* eslint-disable max-len */
6import fs from 'fs'
7import FfmpegCommand from 'fluent-ffmpeg'
8
9export default async function optimizeMedia(
10 inputPath: string,
11 options?: {
12 outPath?: string
13 maxSize?: number
14 keep?: boolean
15 forceImageExtension?: string
16 disableAnimation?: boolean
17 }
18): Promise<string> {
19 const fileAndExtension = options?.outPath ? [options.outPath, ''] : inputPath.split('.')
20 const originalExtension = fileAndExtension[1].toLowerCase()
21 fileAndExtension[1] = options?.forceImageExtension ? options.forceImageExtension : 'webp'
22 let outputPath = fileAndExtension.join('.')
23 const doNotDelete = options?.keep ? options.keep : false
24 switch (originalExtension) {
25 case 'pdf':
26 // TODO make a conversion for audio files
27 case 'ogg':
28 case 'aac':
29 case 'mp3':
30 case 'wav':
31 case 'oga':
32 case 'm4a':
33 break
34 case 'mp4':
35 fileAndExtension[0] = fileAndExtension[0] + '_processed'
36 case 'webm':
37 case 'mov':
38 case 'mkv':
39 case 'av1':
40 fileAndExtension[1] = 'mp4'
41 outputPath = fileAndExtension.join('.')
42 // eslint-disable-next-line no-unused-vars
43 const videoPromise = await new Promise((resolve: any, reject: any) => {
44 FfmpegCommand(inputPath).ffprobe(function (err: any, data: any) {
45 if (data) {
46 {
47 const stream = data.streams.find((stream: any) => stream.coded_height)
48 let horizontalResolution = stream ? stream.coded_width : 1280
49 let verticalResolution = stream ? stream.coded_height : 1280
50 horizontalResolution = Math.min(horizontalResolution, 1280)
51 verticalResolution = Math.min(verticalResolution, 1280)
52 const resolutionString =
53 horizontalResolution > verticalResolution ? `${horizontalResolution}x?` : `?x${verticalResolution}`
54 const videoCodec = stream.codec_name == 'h264' ? 'copy' : 'libx264'
55 const command = FfmpegCommand(inputPath)
56 if (videoCodec != 'copy') {
57 command.size(resolutionString)
58 command.videoBitrate('3500')
59 }
60 command
61 .audioCodec('aac')
62 .videoCodec(videoCodec)
63 .renice(20)
64 .save(outputPath)
65 .on('end', () => {
66 try {
67 fs.unlinkSync(inputPath)
68 logger.trace('media converted')
69 resolve()
70 } catch (exc) {
71 logger.warn(exc)
72 reject(exc)
73 }
74 })
75 }
76 } else {
77 logger.warn({
78 message: `Error on optimizemedia`,
79 error: err
80 })
81 }
82 })
83 })
84
85 break
86 default:
87 const metadata = await sharp(inputPath).metadata()
88 if (!options?.outPath) {
89 fileAndExtension[0] = fileAndExtension[0] + '_processed'
90 outputPath = fileAndExtension.join('.')
91 }
92 if (metadata.delay && !(options?.forceImageExtension && options.forceImageExtension == 'png')) {
93 fileAndExtension[1] = 'webp'
94 outputPath = fileAndExtension.join('.')
95 }
96
97 let conversion = sharp(inputPath, { animated: !options?.disableAnimation, failOnError: false }).rotate()
98 if (options?.maxSize) {
99 const imageMetadata = await conversion.metadata()
100 if (
101 imageMetadata.height &&
102 imageMetadata.width &&
103 (imageMetadata.height > options.maxSize || imageMetadata.width > options.maxSize)
104 ) {
105 let height = imageMetadata.delay ? imageMetadata.height / imageMetadata.delay.length : imageMetadata.height
106 let width = imageMetadata.width
107 const maxSize = options.maxSize
108 if (height > width) {
109 height = Math.round((maxSize * width) / height)
110 width = maxSize
111 } else {
112 width = Math.round((maxSize * height) / width)
113 height = maxSize
114 }
115
116 conversion.resize(height, width)
117 }
118 }
119 if (fileAndExtension[1] == 'webp') {
120 let stat = await fs.promises.stat(inputPath)
121 let lossless = false
122 // if the input is PNG we probably want the output to be lossless too
123 // also allow GIFs under 2MB to be kept as lossless
124 // (smaller GIFs are likely to be something like pixel art
125 // where we want to keep fine detail)
126 const lower = inputPath.toLowerCase()
127 if (lower.endsWith('png') || (lower.endsWith('gif') && stat.size <= 1024 ** 2 * 2)) {
128 lossless = true
129 }
130 conversion.webp({
131 lossless: lossless
132 })
133 }
134 await conversion.toFile(outputPath)
135 if (!doNotDelete) {
136 fs.unlinkSync(inputPath)
137 }
138 }
139 return outputPath
140}