unoffical wafrn mirror wafrn.net
atproto social-network activitypub
at angular21 140 lines 5.0 kB view raw
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}