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 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(chalk.blue.bold('\nšŸš€ Image Video Optimizer\n'));
17
- console.log(chalk.gray(`Target directory: ${resolvedPath}`));
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(chalk.green.bold('\nāœ… Optimization complete!\n'));
20
+ console.log('\nāœ… Optimization complete!\n');
22
21
  } catch (error) {
23
- console.error(chalk.red.bold('\nāŒ Error:\n'), chalk.red(error.message));
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": "2.0.3",
4
- "description": "A CLI tool to optimize images and videos by resizing and converting them according to configuration",
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
- "test": "echo \"Error: no test specified\" && exit 1",
11
- "start": "node bin/cli.js"
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
- "resize",
18
- "convert",
19
- "batch",
17
+ "compress",
18
+ "ffmpeg",
20
19
  "cli"
21
20
  ],
22
21
  "author": "",
23
22
  "license": "MIT",
24
23
  "dependencies": {
25
- "sharp": "^0.33.0",
26
- "fluent-ffmpeg": "^2.1.3",
27
- "commander": "^11.1.0",
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://github.com/yourusername/image-video-optimizer.git"
34
+ "url": "https://git.siliconpin.com/kar/image-video-optimezer-npm-cli"
42
35
  }
43
36
  }
@@ -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 = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'];
5
- const SUPPORTED_VIDEOS = ['.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.m4v', '.mp4'];
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 - Directory path to search
10
- * @returns {Promise<{images: Array, videos: Array}>} Found media files
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 files = await fs.promises.readdir(currentPath);
19
-
20
- for (const file of files) {
21
- const filePath = path.join(currentPath, file);
22
- const stat = await fs.promises.stat(filePath);
23
-
24
- if (stat.isDirectory()) {
25
- // Recursively search subdirectories
26
- await walk(filePath);
27
- } else if (stat.isFile()) {
28
- const ext = path.extname(file).toLowerCase();
29
-
30
- if (SUPPORTED_IMAGES.includes(ext)) {
31
- images.push(filePath);
32
- } else if (SUPPORTED_VIDEOS.includes(ext)) {
33
- videos.push(filePath);
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
- console.warn(`Warning: Could not read directory ${currentPath}: ${error.message}`);
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 { images, videos };
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
  };
@@ -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: `${chalk.green('āœ“')} ${action} ${sizeInfo}`,
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: `${chalk.yellow('ā—‹')} ${path.basename(imagePath)} - Already optimized (${(originalSize / 1024).toFixed(1)}KB)`,
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: `${chalk.red('āœ—')} ${path.basename(imagePath)} - Error: ${error.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(chalk.yellow(`Warning: Could not parse config file: ${error.message}`));
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(chalk.cyan(`šŸ“ Created default config: ${CONFIG_FILENAME}`));
75
+ console.log(`šŸ“ Created default config: ${CONFIG_FILENAME}`);
77
76
  } catch (error) {
78
- console.warn(chalk.yellow(`Warning: Could not create config file: ${error.message}`));
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(chalk.gray('Configuration:'));
101
- console.log(chalk.gray(` Image Max Width: ${config.img_max_width}px`));
102
- console.log(chalk.gray(` Image Format: ${config.img_format}`));
103
- console.log(chalk.gray(` Video Max Width: ${config.video_max_width}px`));
104
- console.log(chalk.gray(` Video Encode: ${config.video_encode}\n`));
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(chalk.blue('šŸ” Searching for media files...'));
106
+ console.log('šŸ” Searching for media files...');
108
107
  const { images, videos } = await fileSearcher.searchMediaFiles(targetDir);
109
108
 
110
- console.log(chalk.gray(`Found ${images.length} image(s) and ${videos.length} video(s)\n`));
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(chalk.yellow('No media files found to optimize.'));
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(chalk.blue('šŸ“· Processing images...'));
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(chalk.blue('šŸŽ¬ Processing videos...'));
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(chalk.blue('šŸ“Š Summary:'));
157
- console.log(chalk.gray(` Files optimized: ${successCount}/${images.length + videos.length}`));
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(chalk.gray(` Original size: ${(totalOriginalSize / 1024 / 1024).toFixed(2)}MB`));
164
- console.log(chalk.gray(` Optimized size: ${(totalOptimizedSize / 1024 / 1024).toFixed(2)}MB`));
165
- console.log(chalk.green(` Space saved: ${(savedSize / 1024 / 1024).toFixed(2)}MB (${savedPercent}%)`));
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
 
@@ -1,141 +1,134 @@
1
1
  const ffmpeg = require('fluent-ffmpeg');
2
- const fs = require('fs');
3
2
  const path = require('path');
4
- const chalk = require('chalk');
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
- * Process a single video file
8
- * @param {string} videoPath - Path to the video
9
- * @param {object} config - Configuration object
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
- async function processVideo(videoPath, config) {
13
- return new Promise((resolve) => {
14
- try {
15
- fs.stat(videoPath, async (err, stat) => {
16
- if (err) {
17
- return resolve({
18
- success: false,
19
- originalSize: 0,
20
- optimizedSize: 0,
21
- message: `${chalk.red('āœ—')} ${path.basename(videoPath)} - Cannot access file`,
22
- error: true
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
- const originalSize = stat.size;
27
- const tmpPath = videoPath + '.tmp.mp4';
28
- const ext = path.extname(videoPath).toLowerCase();
29
- const directory = path.dirname(videoPath);
30
- const filename = path.basename(videoPath, ext);
31
- const finalPath = path.join(directory, filename + '.mp4');
60
+ let command = ffmpeg(inputPath)
61
+ .videoCodec(videoEncode)
62
+ .audioCodec('aac')
63
+ .audioChannels(2)
64
+ .audioFrequency(48000)
65
+ .format('mp4');
32
66
 
33
- // Create ffmpeg command
34
- // Map codec names to ffmpeg codec names
35
- const codecMap = {
36
- 'h264': 'libx264',
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
- const outputCodec = codecMap[config.video_encode.toLowerCase()] || config.video_encode || 'libx264';
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
- const command = ffmpeg(videoPath)
45
- .videoCodec(outputCodec)
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
- const optimizedSize = tmpStat.size;
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
- if (optimizedSize < originalSize) {
67
- // Remove original and rename temp to final
68
- await fs.promises.unlink(videoPath);
69
- await fs.promises.rename(tmpPath, finalPath);
70
-
71
- resolve({
72
- success: true,
73
- originalSize,
74
- optimizedSize,
75
- message: `${chalk.green('āœ“')} ${path.basename(videoPath)} → ${path.basename(finalPath)} (${(originalSize / 1024 / 1024).toFixed(1)}MB → ${(optimizedSize / 1024 / 1024).toFixed(1)}MB)`,
76
- filePath: finalPath
77
- });
78
- } else {
79
- // Remove temp file if not smaller
80
- await fs.promises.unlink(tmpPath);
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
- originalSize: 0,
131
- optimizedSize: 0,
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
  };