image-video-optimizer 2.0.3 ā 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +5 -5
- package/package.json +11 -18
- package/src/fileSearcher.js +72 -24
- package/src/imageProcessor.js +4 -5
- package/src/index.js +18 -19
- package/src/videoProcessor.js +110 -117
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const optimizer = require('../src/index');
|
|
7
6
|
|
|
@@ -13,14 +12,15 @@ program
|
|
|
13
12
|
.action(async (targetDir) => {
|
|
14
13
|
try {
|
|
15
14
|
const resolvedPath = path.resolve(targetDir);
|
|
16
|
-
console.log(
|
|
17
|
-
console.log(
|
|
15
|
+
console.log('\nš Image Video Optimizer\n');
|
|
16
|
+
console.log('Target directory: ' + resolvedPath);
|
|
18
17
|
|
|
19
18
|
await optimizer.optimize(resolvedPath);
|
|
20
19
|
|
|
21
|
-
console.log(
|
|
20
|
+
console.log('\nā
Optimization complete!\n');
|
|
22
21
|
} catch (error) {
|
|
23
|
-
console.error(
|
|
22
|
+
console.error('\nā Error:\n');
|
|
23
|
+
console.error(error.message);
|
|
24
24
|
process.exit(1);
|
|
25
25
|
}
|
|
26
26
|
});
|
package/package.json
CHANGED
|
@@ -1,43 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-video-optimizer",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "CLI tool to optimize and compress images and videos with configurable settings",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"image-video-optimizer": "./bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"start": "node bin/cli.js",
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"image",
|
|
15
15
|
"video",
|
|
16
16
|
"optimizer",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"batch",
|
|
17
|
+
"compress",
|
|
18
|
+
"ffmpeg",
|
|
20
19
|
"cli"
|
|
21
20
|
],
|
|
22
21
|
"author": "",
|
|
23
22
|
"license": "MIT",
|
|
24
23
|
"dependencies": {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"chalk": "^5.3.0",
|
|
29
|
-
"ora": "^8.0.1"
|
|
30
|
-
},
|
|
31
|
-
"devDependencies": {
|
|
32
|
-
"ffmpeg-static": "^5.2.0",
|
|
33
|
-
"ffprobe-static": "^3.1.0"
|
|
24
|
+
"commander": "^11.0.0",
|
|
25
|
+
"sharp": "^0.32.0",
|
|
26
|
+
"fluent-ffmpeg": "^2.1.2"
|
|
34
27
|
},
|
|
28
|
+
"devDependencies": {},
|
|
35
29
|
"engines": {
|
|
36
30
|
"node": ">=14.0.0"
|
|
37
31
|
},
|
|
38
|
-
"preferGlobal": true,
|
|
39
32
|
"repository": {
|
|
40
33
|
"type": "git",
|
|
41
|
-
"url": "https://
|
|
34
|
+
"url": "https://git.siliconpin.com/kar/image-video-optimezer-npm-cli"
|
|
42
35
|
}
|
|
43
36
|
}
|
package/src/fileSearcher.js
CHANGED
|
@@ -1,51 +1,99 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
|
-
const SUPPORTED_IMAGES = ['
|
|
5
|
-
const SUPPORTED_VIDEOS = ['
|
|
4
|
+
const SUPPORTED_IMAGES = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'];
|
|
5
|
+
const SUPPORTED_VIDEOS = ['avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'm4v', 'mp4'];
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Recursively search for media files in a directory
|
|
9
|
-
* @param {string} dirPath -
|
|
10
|
-
* @
|
|
9
|
+
* @param {string} dirPath - Starting directory path
|
|
10
|
+
* @param {Object} options - Search options
|
|
11
|
+
* @returns {Promise<Object>} Object with images and videos arrays
|
|
11
12
|
*/
|
|
12
|
-
async function searchMediaFiles(dirPath) {
|
|
13
|
+
async function searchMediaFiles(dirPath, options = {}) {
|
|
13
14
|
const images = [];
|
|
14
15
|
const videos = [];
|
|
16
|
+
const verbose = options.verbose || false;
|
|
15
17
|
|
|
16
18
|
async function walk(currentPath) {
|
|
17
19
|
try {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
for (const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
// Recursively walk subdirectories
|
|
28
|
+
await walk(fullPath);
|
|
29
|
+
} else if (entry.isFile()) {
|
|
30
|
+
const ext = path.extname(entry.name).toLowerCase().slice(1);
|
|
31
|
+
|
|
32
|
+
if (SUPPORTED_IMAGES.includes(ext)) {
|
|
33
|
+
images.push(fullPath);
|
|
34
|
+
if (verbose) {
|
|
35
|
+
console.log(` Found image: ${fullPath}`);
|
|
36
|
+
}
|
|
37
|
+
} else if (SUPPORTED_VIDEOS.includes(ext)) {
|
|
38
|
+
videos.push(fullPath);
|
|
39
|
+
if (verbose) {
|
|
40
|
+
console.log(` Found video: ${fullPath}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (verbose) {
|
|
46
|
+
console.warn(` Warning: Could not process ${fullPath}: ${error.message}`);
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
49
|
}
|
|
37
50
|
} catch (error) {
|
|
38
|
-
|
|
51
|
+
if (verbose) {
|
|
52
|
+
console.warn(` Warning: Could not read directory ${currentPath}: ${error.message}`);
|
|
53
|
+
}
|
|
39
54
|
}
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
await walk(dirPath);
|
|
43
58
|
|
|
44
|
-
return {
|
|
59
|
+
return {
|
|
60
|
+
images,
|
|
61
|
+
videos,
|
|
62
|
+
total: images.length + videos.length
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get file size in bytes
|
|
68
|
+
* @param {string} filePath - Path to file
|
|
69
|
+
* @returns {Promise<number>} File size in bytes
|
|
70
|
+
*/
|
|
71
|
+
async function getFileSize(filePath) {
|
|
72
|
+
try {
|
|
73
|
+
const stats = await fs.stat(filePath);
|
|
74
|
+
return stats.size;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw new Error(`Could not get file size for ${filePath}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get human-readable file size
|
|
82
|
+
* @param {number} bytes - Size in bytes
|
|
83
|
+
* @returns {string} Human-readable size
|
|
84
|
+
*/
|
|
85
|
+
function formatFileSize(bytes) {
|
|
86
|
+
if (bytes === 0) return '0 Bytes';
|
|
87
|
+
const k = 1024;
|
|
88
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
89
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
90
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
45
91
|
}
|
|
46
92
|
|
|
47
93
|
module.exports = {
|
|
48
94
|
searchMediaFiles,
|
|
95
|
+
getFileSize,
|
|
96
|
+
formatFileSize,
|
|
49
97
|
SUPPORTED_IMAGES,
|
|
50
98
|
SUPPORTED_VIDEOS
|
|
51
99
|
};
|
package/src/imageProcessor.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const sharp = require('sharp');
|
|
2
1
|
const fs = require('fs');
|
|
2
|
+
const sharp = require('sharp');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Process a single image file
|
|
@@ -78,7 +77,7 @@ async function processImage(imagePath, config) {
|
|
|
78
77
|
success: true,
|
|
79
78
|
originalSize,
|
|
80
79
|
optimizedSize,
|
|
81
|
-
message:
|
|
80
|
+
message: `ā ${action} ${sizeInfo}`,
|
|
82
81
|
filePath: finalPath
|
|
83
82
|
};
|
|
84
83
|
} else {
|
|
@@ -89,7 +88,7 @@ async function processImage(imagePath, config) {
|
|
|
89
88
|
success: false,
|
|
90
89
|
originalSize,
|
|
91
90
|
optimizedSize,
|
|
92
|
-
message:
|
|
91
|
+
message: `ā ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
|
|
93
92
|
filePath: imagePath
|
|
94
93
|
};
|
|
95
94
|
}
|
|
@@ -98,7 +97,7 @@ async function processImage(imagePath, config) {
|
|
|
98
97
|
success: false,
|
|
99
98
|
originalSize: 0,
|
|
100
99
|
optimizedSize: 0,
|
|
101
|
-
message:
|
|
100
|
+
message: `ā ${path.basename(imagePath)} - Error: ${error.message}`,
|
|
102
101
|
error: true
|
|
103
102
|
};
|
|
104
103
|
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const chalk = require('chalk');
|
|
4
3
|
const fileSearcher = require('./fileSearcher');
|
|
5
4
|
const imageProcessor = require('./imageProcessor');
|
|
6
5
|
const videoProcessor = require('./videoProcessor');
|
|
@@ -49,7 +48,7 @@ function parseConfig(configPath) {
|
|
|
49
48
|
|
|
50
49
|
return config;
|
|
51
50
|
} catch (error) {
|
|
52
|
-
console.warn(
|
|
51
|
+
console.warn(`Warning: Could not parse config file: ${error.message}`);
|
|
53
52
|
return DEFAULT_CONFIG;
|
|
54
53
|
}
|
|
55
54
|
}
|
|
@@ -73,9 +72,9 @@ video_encode=${DEFAULT_CONFIG.video_encode} # Video encoding format
|
|
|
73
72
|
|
|
74
73
|
try {
|
|
75
74
|
fs.writeFileSync(configPath, configContent);
|
|
76
|
-
console.log(
|
|
75
|
+
console.log(`š Created default config: ${CONFIG_FILENAME}`);
|
|
77
76
|
} catch (error) {
|
|
78
|
-
console.warn(
|
|
77
|
+
console.warn(`Warning: Could not create config file: ${error.message}`);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
}
|
|
@@ -97,20 +96,20 @@ async function optimize(targetDir) {
|
|
|
97
96
|
const configPath = path.join(targetDir, CONFIG_FILENAME);
|
|
98
97
|
const config = parseConfig(configPath);
|
|
99
98
|
|
|
100
|
-
console.log(
|
|
101
|
-
console.log(
|
|
102
|
-
console.log(
|
|
103
|
-
console.log(
|
|
104
|
-
console.log(
|
|
99
|
+
console.log('Configuration:');
|
|
100
|
+
console.log(` Image Max Width: ${config.img_max_width}px`);
|
|
101
|
+
console.log(` Image Format: ${config.img_format}`);
|
|
102
|
+
console.log(` Video Max Width: ${config.video_max_width}px`);
|
|
103
|
+
console.log(` Video Encode: ${config.video_encode}\n`);
|
|
105
104
|
|
|
106
105
|
// Search for media files
|
|
107
|
-
console.log(
|
|
106
|
+
console.log('š Searching for media files...');
|
|
108
107
|
const { images, videos } = await fileSearcher.searchMediaFiles(targetDir);
|
|
109
108
|
|
|
110
|
-
console.log(
|
|
109
|
+
console.log(`Found ${images.length} image(s) and ${videos.length} video(s)\n`);
|
|
111
110
|
|
|
112
111
|
if (images.length === 0 && videos.length === 0) {
|
|
113
|
-
console.log(
|
|
112
|
+
console.log('No media files found to optimize.');
|
|
114
113
|
return;
|
|
115
114
|
}
|
|
116
115
|
|
|
@@ -120,7 +119,7 @@ async function optimize(targetDir) {
|
|
|
120
119
|
|
|
121
120
|
// Process images
|
|
122
121
|
if (images.length > 0) {
|
|
123
|
-
console.log(
|
|
122
|
+
console.log('š· Processing images...');
|
|
124
123
|
for (const imagePath of images) {
|
|
125
124
|
const result = await imageProcessor.processImage(imagePath, config);
|
|
126
125
|
console.log(result.message);
|
|
@@ -137,7 +136,7 @@ async function optimize(targetDir) {
|
|
|
137
136
|
|
|
138
137
|
// Process videos
|
|
139
138
|
if (videos.length > 0) {
|
|
140
|
-
console.log(
|
|
139
|
+
console.log('š¬ Processing videos...');
|
|
141
140
|
for (const videoPath of videos) {
|
|
142
141
|
const result = await videoProcessor.processVideo(videoPath, config);
|
|
143
142
|
console.log(result.message);
|
|
@@ -153,16 +152,16 @@ async function optimize(targetDir) {
|
|
|
153
152
|
}
|
|
154
153
|
|
|
155
154
|
// Print summary
|
|
156
|
-
console.log(
|
|
157
|
-
console.log(
|
|
155
|
+
console.log('š Summary:');
|
|
156
|
+
console.log(` Files optimized: ${successCount}/${images.length + videos.length}`);
|
|
158
157
|
|
|
159
158
|
if (totalOriginalSize > 0) {
|
|
160
159
|
const savedSize = totalOriginalSize - totalOptimizedSize;
|
|
161
160
|
const savedPercent = ((savedSize / totalOriginalSize) * 100).toFixed(1);
|
|
162
161
|
|
|
163
|
-
console.log(
|
|
164
|
-
console.log(
|
|
165
|
-
console.log(
|
|
162
|
+
console.log(` Original size: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`);
|
|
163
|
+
console.log(` Optimized size: ${(totalOptimizedSize / 1024 / 1024).toFixed(2)}MB`);
|
|
164
|
+
console.log(` Space saved: ${(savedSize / 1024 / 1024).toFixed(2)}MB (${savedPercent}%)`);
|
|
166
165
|
}
|
|
167
166
|
}
|
|
168
167
|
|
package/src/videoProcessor.js
CHANGED
|
@@ -1,141 +1,134 @@
|
|
|
1
1
|
const ffmpeg = require('fluent-ffmpeg');
|
|
2
|
-
const fs = require('fs');
|
|
3
2
|
const path = require('path');
|
|
4
|
-
const
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
|
|
5
|
+
// Set ffmpeg and ffprobe paths if using static binaries
|
|
6
|
+
try {
|
|
7
|
+
const ffmpegStatic = require('ffmpeg-static');
|
|
8
|
+
const ffprobeStatic = require('ffprobe-static');
|
|
9
|
+
ffmpeg.setFfmpegPath(ffmpegStatic);
|
|
10
|
+
ffmpeg.setFfprobePath(ffprobeStatic.path);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
// Fallback to system ffmpeg/ffprobe if static binaries not available
|
|
13
|
+
console.warn('Note: ffmpeg-static not found, using system ffmpeg/ffprobe');
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @param {string} videoPath - Path to
|
|
9
|
-
* @
|
|
10
|
-
* @returns {Promise<{success: boolean, originalSize: number, optimizedSize: number, message: string}>}
|
|
17
|
+
* Get video metadata (width, height, duration, etc.)
|
|
18
|
+
* @param {string} videoPath - Path to video file
|
|
19
|
+
* @returns {Promise<Object>} Video metadata
|
|
11
20
|
*/
|
|
12
|
-
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
function getVideoMetadata(videoPath) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
ffmpeg.ffprobe(videoPath, (err, metadata) => {
|
|
24
|
+
if (err) {
|
|
25
|
+
reject(new Error(`Could not read video metadata: ${err.message}`));
|
|
26
|
+
} else {
|
|
27
|
+
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
|
|
28
|
+
if (videoStream) {
|
|
29
|
+
resolve({
|
|
30
|
+
width: videoStream.width,
|
|
31
|
+
height: videoStream.height,
|
|
32
|
+
duration: metadata.format.duration,
|
|
33
|
+
codec: videoStream.codec_name,
|
|
34
|
+
bitrate: metadata.format.bit_rate
|
|
23
35
|
});
|
|
36
|
+
} else {
|
|
37
|
+
reject(new Error('No video stream found in file'));
|
|
24
38
|
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Process (resize and encode) a video
|
|
46
|
+
* @param {string} inputPath - Path to input video
|
|
47
|
+
* @param {string} outputPath - Path to output video
|
|
48
|
+
* @param {Object} config - Configuration object
|
|
49
|
+
* @returns {Promise<Object>} Processing result
|
|
50
|
+
*/
|
|
51
|
+
function processVideo(inputPath, outputPath, config) {
|
|
52
|
+
return new Promise(async (resolve) => {
|
|
53
|
+
try {
|
|
54
|
+
const metadata = await getVideoMetadata(inputPath);
|
|
55
|
+
const currentWidth = metadata.width;
|
|
56
|
+
const currentHeight = metadata.height;
|
|
57
|
+
const maxWidth = parseInt(config.video_max_width) || 720;
|
|
58
|
+
const videoEncode = config.video_encode || 'h264';
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
let command = ffmpeg(inputPath)
|
|
61
|
+
.videoCodec(videoEncode)
|
|
62
|
+
.audioCodec('aac')
|
|
63
|
+
.audioChannels(2)
|
|
64
|
+
.audioFrequency(48000)
|
|
65
|
+
.format('mp4');
|
|
32
66
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
'h265': 'libx265',
|
|
38
|
-
'vp8': 'libvpx',
|
|
39
|
-
'vp9': 'libvpx-vp9'
|
|
40
|
-
};
|
|
67
|
+
// Only resize if video is wider than max_width
|
|
68
|
+
if (currentWidth > maxWidth) {
|
|
69
|
+
const scaleFactor = maxWidth / currentWidth;
|
|
70
|
+
const newHeight = Math.round(currentHeight * scaleFactor);
|
|
41
71
|
|
|
42
|
-
|
|
72
|
+
// Ensure dimensions are even (required for most codecs)
|
|
73
|
+
const finalHeight = newHeight % 2 === 0 ? newHeight : newHeight - 1;
|
|
74
|
+
const finalWidth = maxWidth % 2 === 0 ? maxWidth : maxWidth - 1;
|
|
43
75
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.size(`${config.video_max_width}x?`)
|
|
47
|
-
.outputOptions([
|
|
48
|
-
'-preset fast',
|
|
49
|
-
'-crf 28',
|
|
50
|
-
'-c:a aac' // Use AAC for audio (widely compatible)
|
|
51
|
-
])
|
|
52
|
-
.output(tmpPath);
|
|
53
|
-
|
|
54
|
-
command
|
|
55
|
-
.on('end', async () => {
|
|
56
|
-
try {
|
|
57
|
-
const tmpStat = await new Promise((res, rej) => {
|
|
58
|
-
fs.stat(tmpPath, (err, stat) => {
|
|
59
|
-
if (err) rej(err);
|
|
60
|
-
else res(stat);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
76
|
+
command = command.size(`${finalWidth}x${finalHeight}`);
|
|
77
|
+
}
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
// Set bitrate based on resolution to maintain quality while reducing size
|
|
80
|
+
const bitrate = currentWidth > maxWidth ? '2000k' : '3000k';
|
|
81
|
+
command = command.videoBitrate(bitrate);
|
|
65
82
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
command
|
|
84
|
+
.on('start', (cmdline) => {
|
|
85
|
+
// Processing started
|
|
86
|
+
})
|
|
87
|
+
.on('end', () => {
|
|
88
|
+
resolve({
|
|
89
|
+
success: true,
|
|
90
|
+
inputPath,
|
|
91
|
+
outputPath,
|
|
92
|
+
originalWidth: currentWidth,
|
|
93
|
+
originalHeight: currentHeight,
|
|
94
|
+
resized: currentWidth > maxWidth,
|
|
95
|
+
duration: metadata.duration
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
.on('error', (err) => {
|
|
99
|
+
resolve({
|
|
100
|
+
success: false,
|
|
101
|
+
inputPath,
|
|
102
|
+
error: err.message
|
|
103
|
+
});
|
|
104
|
+
})
|
|
105
|
+
.output(outputPath)
|
|
106
|
+
.run();
|
|
81
107
|
|
|
82
|
-
resolve({
|
|
83
|
-
success: false,
|
|
84
|
-
originalSize,
|
|
85
|
-
optimizedSize,
|
|
86
|
-
message: `${chalk.yellow('ā')} ${path.basename(videoPath)} - Already optimized (${(originalSize / 1024 / 1024).toFixed(1)}MB)`,
|
|
87
|
-
filePath: videoPath
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
} catch (error) {
|
|
91
|
-
resolve({
|
|
92
|
-
success: false,
|
|
93
|
-
originalSize,
|
|
94
|
-
optimizedSize: 0,
|
|
95
|
-
message: `${chalk.red('ā')} ${path.basename(videoPath)} - Post-processing error: ${error.message}`,
|
|
96
|
-
error: true
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
.on('error', (err) => {
|
|
101
|
-
// Clean up temp file on error
|
|
102
|
-
fs.unlink(tmpPath, () => {
|
|
103
|
-
let errorMsg = err.message;
|
|
104
|
-
|
|
105
|
-
// Provide helpful suggestions for common errors
|
|
106
|
-
if (err.message.includes('Unknown encoder') || err.message.includes('Encoder')) {
|
|
107
|
-
errorMsg = `Video codec error. Check available codecs with: ffmpeg -encoders | grep lib`;
|
|
108
|
-
} else if (err.message.includes('is not available')) {
|
|
109
|
-
errorMsg = `Codec not available. Verify FFmpeg installation: ffmpeg -version`;
|
|
110
|
-
} else if (err.message.includes('ENOENT') || err.message.includes('ffmpeg')) {
|
|
111
|
-
errorMsg = `FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt-get install ffmpeg (Linux)`;
|
|
112
|
-
} else if (err.message.includes('Conversion failed') || err.message.includes('exited with code')) {
|
|
113
|
-
errorMsg = `Video encoding failed. Try a different codec in config: video_encode=h265 or video_encode=vp8`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
resolve({
|
|
117
|
-
success: false,
|
|
118
|
-
originalSize,
|
|
119
|
-
optimizedSize: 0,
|
|
120
|
-
message: `${chalk.red('ā')} ${path.basename(videoPath)} - ${errorMsg}`,
|
|
121
|
-
error: true
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
})
|
|
125
|
-
.run();
|
|
126
|
-
});
|
|
127
108
|
} catch (error) {
|
|
128
109
|
resolve({
|
|
129
110
|
success: false,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
message: `${chalk.red('ā')} ${path.basename(videoPath)} - Error: ${error.message}`,
|
|
133
|
-
error: true
|
|
111
|
+
inputPath,
|
|
112
|
+
error: error.message
|
|
134
113
|
});
|
|
135
114
|
}
|
|
136
115
|
});
|
|
137
116
|
}
|
|
138
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Check if ffmpeg is available on the system
|
|
120
|
+
* @returns {Promise<boolean>} True if ffmpeg is available
|
|
121
|
+
*/
|
|
122
|
+
function isFfmpegAvailable() {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
ffmpeg.getAvailableFormats((err) => {
|
|
125
|
+
resolve(!err);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
139
130
|
module.exports = {
|
|
140
|
-
processVideo
|
|
131
|
+
processVideo,
|
|
132
|
+
getVideoMetadata,
|
|
133
|
+
isFfmpegAvailable
|
|
141
134
|
};
|