diff --git a/README.md b/README.md index 6a2666d..dbc39c4 100644 --- a/README.md +++ b/README.md @@ -119,8 +119,15 @@ Params: , keep_aspect_ratio : true // Mantain the original aspect ratio , padding_color : 'black' // Padding color , file_name : null // File name + , qscale : null // Quality scale of the video frame. } ``` + > new parameter added for quality of the frames: __qscale__ + + > qscale can be set between 1-31 *(lower is better)* + + > qscale : 1 //better quality framing. + * __callback__: *(optional)* If specified at the end of the process will be returned list of paths of frames created: > function (error, files) diff --git a/lib/video.js b/lib/video.js index 903efdb..2b1dc32 100644 --- a/lib/video.js +++ b/lib/video.js @@ -7,25 +7,25 @@ var errors = require('./errors') , utils = require('./utils'); module.exports = function (filePath, settings, infoConfiguration, infoFile) { - + // Public info about file and ffmpeg configuration this.file_path = filePath; this.info_configuration = infoConfiguration; this.metadata = infoFile; - + // Commands for building the ffmpeg string conversion var commands = new Array() , inputs = new Array() , filtersComlpex = new Array() , output = null; - + // List of options generated from setting functions var options = new Object(); - + /*****************************************/ /* FUNCTION FOR FILL THE COMMANDS OBJECT */ /*****************************************/ - + /** * Add a command to be bundled into the ffmpeg command call */ @@ -37,35 +37,35 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Add the argument to new command if (argument != undefined) commands.push(argument); - } else + } else throw errors.renderError('command_already_exists', command); } - + /** * Add an input stream */ this.addInput = function (argument) { inputs.push(argument); } - + /** * Add a filter complex */ this.addFilterComplex = function (argument) { filtersComlpex.push(argument); } - + /** * Set the output path */ var setOutput = function (path) { output = path; } - + /*********************/ /* SETTING FUNCTIONS */ /*********************/ - + /** * Disables audio encoding */ @@ -87,7 +87,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.video.disabled = true; return this; } - + /** * Sets the new video format */ @@ -99,10 +99,10 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Set the new option options.video.format = format; return this; - } else + } else throw errors.renderError('format_not_supported', format); } - + /** * Sets the new audio codec */ @@ -114,10 +114,10 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Set the new option options.video.codec = codec; return this; - } else + } else throw errors.renderError('codec_not_supported', codec); } - + /** * Sets the video bitrate */ @@ -128,7 +128,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.video.bitrate = bitrate; return this; } - + /** * Sets the framerate of the video */ @@ -137,42 +137,42 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.video = new Object(); // Set the new option options.video.framerate = framerate; - return this; + return this; } - + /** * Sets the start time */ this.setVideoStartTime = function (time) { if (options.video == undefined) options.video = new Object(); - + // Check if time is a string that contain: hours, minutes and seconds if (isNaN(time) && /([0-9]+):([0-9]{2}):([0-9]{2})/.exec(time)) { - time = utils.durationToSeconds(time); + time = utils.durationToSeconds(time); } else if (!isNaN(time) && parseInt(time) == time) { - time = parseInt(time, 10); + time = parseInt(time, 10); } else { - time = 0; + time = 0; } // Set the new option options.video.startTime = time; return this; } - + /** * Sets the duration */ this.setVideoDuration = function (duration) { if (options.video == undefined) options.video = new Object(); - + // Check if duration is a string that contain: hours, minutes and seconds if (isNaN(duration) && /([0-9]+):([0-9]{2}):([0-9]{2})/.exec(duration)) { duration = utils.durationToSeconds(duration); } else if (!isNaN(duration) && parseInt(duration) == duration) { - duration = parseInt(duration, 10); + duration = parseInt(duration, 10); } else { duration = 0; } @@ -181,7 +181,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.video.duration = duration; return this; } - + /** * Sets the new aspetc ratio */ @@ -196,14 +196,14 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { aspect = this.metadata.video.aspect.value; } } - + if (options.video == undefined) options.video = new Object(); // Set the new option options.video.aspect = aspect; return this; } - + /** * Set the size of the video */ @@ -217,7 +217,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.video.paddingColor = paddingColor; return this; } - + /** * Sets the new audio codec */ @@ -227,16 +227,16 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Check if codec is equal 'MP3' and check if the version of ffmpeg support the libmp3lame function if (codec == 'mp3' && this.info_configuration.modules.indexOf('libmp3lame') != -1) codec = 'libmp3lame'; - + if (options.audio == undefined) options.audio = new Object(); // Set the new option options.audio.codec = codec; return this; - } else + } else throw errors.renderError('codec_not_supported', codec); } - + /** * Sets the audio sample frequency for audio outputs */ @@ -247,7 +247,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.audio.frequency = frequency; return this; } - + /** * Sets the number of audio channels */ @@ -258,11 +258,11 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.audio = new Object(); // Set the new option options.audio.channel = channel; - return this; - } else + return this; + } else throw errors.renderError('audio_channel_is_invalid', channel); } - + /** * Sets the audio bitrate */ @@ -273,7 +273,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.audio.bitrate = bitrate; return this; } - + /** * Sets the audio quality */ @@ -284,7 +284,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { options.audio.quality = quality; return this; } - + /** * Sets the watermark */ @@ -297,21 +297,21 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { , margin_east : null // Margin east , margin_west : null // Margin west }; - + // Check if watermark exists if (!fs.existsSync(watermarkPath)) throw errors.renderError('invalid_watermark', watermarkPath); - + // Check if the settings are specified if (settings != null) utils.mergeObject(baseSettings, settings); - + // Check if position is valid if (baseSettings.position == null || utils.in_array(baseSettings.position, ['NE','NC','NW','SE','SC','SW','C','CE','CW']) === false) throw errors.renderError('invalid_watermark_position', baseSettings.position); - + // Check if margins are valid - + if (baseSettings.margin_nord == null || isNaN(baseSettings.margin_nord)) baseSettings.margin_nord = 0; if (baseSettings.margin_sud == null || isNaN(baseSettings.margin_sud)) @@ -320,22 +320,22 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { baseSettings.margin_east = 0; if (baseSettings.margin_west == null || isNaN(baseSettings.margin_west)) baseSettings.margin_west = 0; - + var overlay = ''; - + var getSing = function (val, inverse) { return (val > 0 ? (inverse ? '-' : '+') : (inverse ? '+' : '-')).toString() + Math.abs(val).toString(); } - + var getHorizontalMargins = function (east, west) { return getSing(east, false).toString() + getSing(west, true).toString(); } - + var getVerticalMargins = function (nord, sud) { return getSing(nord, false).toString() + getSing(sud, true).toString(); } - - // Calculate formula + + // Calculate formula switch (baseSettings.position) { case 'NE': overlay = '0' + getHorizontalMargins(baseSettings.margin_east, baseSettings.margin_west) + ':0' + getVerticalMargins(baseSettings.margin_nord, baseSettings.margin_sud); @@ -365,7 +365,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { overlay = 'main_w-overlay_w' + getHorizontalMargins(baseSettings.margin_east, baseSettings.margin_west) + ':main_h/2-overlay_h/2' + getVerticalMargins(baseSettings.margin_nord, baseSettings.margin_sud); break; } - + // Check if the call comes from internal function if (arguments[2] == undefined || arguments[2] == null) { if (options.video == undefined) @@ -378,7 +378,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { this.addFilterComplex('overlay=' + overlay); } } - + /** * Save all set commands */ @@ -387,7 +387,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { if (options.hasOwnProperty('video')) { // Check if video is disabled if (options.video.hasOwnProperty('disabled')) { - this.addCommand('-vn'); + this.addCommand('-vn'); } else { // Check all video property if (options.video.hasOwnProperty('format')) @@ -402,21 +402,21 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { this.addCommand('-ss', parseInt(options.video.startTime, 10)); if (options.video.hasOwnProperty('duration')) this.addCommand('-t', parseInt(options.video.duration, 10)); - + if (options.video.hasOwnProperty('watermark')) { this.addInput(options.video.watermark.path); this.addFilterComplex('overlay=' + options.video.watermark.overlay); } - + // Check if the video should be scaled if (options.video.hasOwnProperty('size')) { var newDimension = _calculateNewDimension.call(this); - + if (newDimension.aspect != null) { this.addFilterComplex('scale=iw*sar:ih, pad=max(iw\\,ih*(' + newDimension.aspect.x + '/' + newDimension.aspect.y + ')):ow/(' + newDimension.aspect.x + '/' + newDimension.aspect.y + '):(ow-iw)/2:(oh-ih)/2' + (options.video.paddingColor != null ? ':' + options.video.paddingColor : '')); this.addCommand('-aspect', newDimension.aspect.string); } - + this.addCommand('-s', newDimension.width + 'x' + newDimension.height); } } @@ -425,7 +425,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { if (options.hasOwnProperty('audio')) { // Check if audio is disabled if (options.audio.hasOwnProperty('disabled')) { - this.addCommand('-an'); + this.addCommand('-an'); } else { // Check all audio property if (options.audio.hasOwnProperty('codec')) @@ -440,16 +440,16 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { this.addCommand('-ab', parseInt(options.audio.bitrate, 10) + 'k'); } } - + setOutput(destionationFileName); - + return execCommand.call(this, callback); } - + /*********************/ /* INTERNAL FUNCTION */ /*********************/ - + /** * Reset the list of commands */ @@ -469,7 +469,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { var keepPixelAspectRatio = typeof options.video.keepPixelAspectRatio != 'boolean' ? false : options.video.keepPixelAspectRatio; // Check if keepAspectRatio is undefined var keepAspectRatio = typeof options.video.keepAspectRatio != 'boolean' ? false : options.video.keepAspectRatio; - + // Resolution to be taken as a reference var referrerResolution = this.metadata.video.resolution; // Check if is need keep pixel aspect ratio @@ -477,11 +477,11 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Check if exists resolution for pixel aspect ratio if (utils.isEmptyObj(this.metadata.video.resolutionSquare)) throw errors.renderError('resolution_square_not_defined'); - + // Apply the resolutionSquare referrerResolution = this.metadata.video.resolutionSquare; } - + // Final data var width = null , height = null @@ -492,10 +492,10 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { , fixedHeight = /\?x([0-9]+)/.exec(options.video.size) , percentage = /([0-9]{1,2})%/.exec(options.video.size) , classicSize = /([0-9]+)x([0-9]+)/.exec(options.video.size); - + if (fixedWidth) { // Set the width dimension - width = parseInt(fixedWidth[1], 10); + width = parseInt(fixedWidth[1], 10); // Check if the video has the aspect ratio setted if (!utils.isEmptyObj(this.metadata.video.aspect)) { height = Math.round((width / this.metadata.video.aspect.x) * this.metadata.video.aspect.y); @@ -505,14 +505,14 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { } } else if (fixedHeight) { // Set the width dimension - height = parseInt(fixedHeight[1], 10); + height = parseInt(fixedHeight[1], 10); // Check if the video has the aspect ratio setted if (!utils.isEmptyObj(this.metadata.video.aspect)) { width = Math.round((height / this.metadata.video.aspect.y) * this.metadata.video.aspect.x); } else { // Calculte the new width width = Math.round(referrerResolution.w / (referrerResolution.h / parseInt(fixedHeight[1], 10))); - } + } } else if (percentage) { // Calculte the ratio from percentage var ratio = parseInt(percentage[1], 10) / 100; @@ -522,42 +522,37 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { } else if (classicSize) { width = parseInt(classicSize[1], 10); height = parseInt(classicSize[2], 10); - } else + } else throw errors.renderError('size_format', options.video.size); - + // If the width or height are not multiples of 2 will be decremented by one unit if (width % 2 != 0) width -= 1; if (height % 2 != 0) height -= 1; - + if (keepAspectRatio) { // Calculate the new aspect ratio var gcdValue = utils.gcd(width, height); - + aspect = new Object(); aspect.x = width / gcdValue; aspect.y = height / gcdValue; aspect.string = aspect.x + ':' + aspect.y; } - + return { width : width, height : height, aspect : aspect }; } - + /** * Executing the commands list */ var execCommand = function (callback, folder) { - var i; // Checking if folder is defined var onlyDestinationFile = folder != undefined ? false : true; // Building the value for return value. Check if the callback is not a function. In this case will created a new instance of the deferred class var deferred = typeof callback != 'function' ? when.defer() : { promise : null }; - // Deal with input paths that have spaces in them, by quoting them - for (i=0; i 0 ? ['-filter_complex "'].concat(filtersComlpex.join(', ')).join('') + '"' : []) .concat([output]); @@ -599,49 +594,49 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Return a possible promise instance return deferred.promise; } - + /*******************/ /* PRESET FUNCTION */ /*******************/ - + /** * Extracting sound from a video, and save it as Mp3 */ this.fnExtractSoundToMP3 = function (destionationFileName, callback) { // Check if file already exists. In this case will remove it - if (fs.existsSync(destionationFileName)) + if (fs.existsSync(destionationFileName)) fs.unlinkSync(destionationFileName); // Building the final path var destinationDirName = path.dirname(destionationFileName) , destinationFileNameWE = path.basename(destionationFileName, path.extname(destionationFileName)) + '.mp3' , finalPath = path.join(destinationDirName, destinationFileNameWE); - + resetCommands(this); - + // Adding commands to the list this.addCommand('-vn'); this.addCommand('-ar', 44100); this.addCommand('-ac', 2); this.addCommand('-ab', 192); this.addCommand('-f', 'mp3'); - + // Add destination file path to the command list setOutput(finalPath); - + // Executing the commands list return execCommand.call(this, callback); } - + /** * Extract frame from video file */ this.fnExtractFrameToJPG = function (/* destinationFolder, settings, callback */) { - + var destinationFolder = null , newSettings = null , callback = null; - + var settings = { start_time : null // Start time to recording , duration_time : null // Duration of recording @@ -655,8 +650,9 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { , keep_aspect_ratio : true // Mantain the original aspect ratio , padding_color : 'black' // Padding color , file_name : null // File name + , qscale : null // Quality scale of the video frame. }; - + // Scan all arguments for (var i in arguments) { // Check the type of the argument @@ -672,7 +668,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { break; } } - + // Check if the settings are specified if (newSettings !== null) utils.mergeObject(settings, newSettings); @@ -700,6 +696,10 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { // Check if the value of the framerate is number type if (settings.frame_rate != null && isNaN(settings.frame_rate)) settings.frame_rate = null; + + // Check if the value of the qscale is number type + if (settings.qscale != null && isNaN(settings.qscale)) + settings.qscale = null; // If the size is not settings then the size of the screenshots is equal to video size if (settings.size == null) @@ -728,15 +728,15 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { settings.every_n_percentage = null; every_n_check++; } - + if (every_n_check >= 2) { if (callback) { callback(errors.renderError('extract_frame_invalid_everyN_options')); } else { throw errors.renderError('extract_frame_invalid_everyN_options'); } - } - + } + // If filename is null then his value is equal to original filename if (settings.file_name == null) { settings.file_name = path.basename(this.file_path, path.extname(this.file_path)); @@ -768,19 +768,21 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { } // At the filename will added the number of the frame settings.file_name = path.basename(settings.file_name, path.extname(settings.file_name)) + '_%d.jpg'; - + // Create the directory to save the extracted frames - utils.mkdir(destinationFolder, 0x0777); - + utils.mkdir(destinationFolder, 0777); + resetCommands(this); - + // Adding commands to the list - if (settings.start_time) - this.addCommand('-ss', settings.start_time); + if (settings.startTime) + this.addCommand('-ss', settings.startTime); if (settings.duration_time) this.addCommand('-t', settings.duration_time); if (settings.frame_rate) this.addCommand('-r', settings.frame_rate); + if (settings.qscale) + this.addCommand('-qscale:v', settings.qscale); // Setting the size and padding settings this.setVideoSize(settings.size, settings.keep_pixel_aspect_ratio, settings.keep_aspect_ratio, settings.padding_color); @@ -797,7 +799,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { if (settings.number) this.addCommand('-vframes', settings.number); if (settings.every_n_frames) { - this.addCommand('-vsync', 0); + this.addCommand('-vsync', 0); this.addFilterComplex('select=not(mod(n\\,' + settings.every_n_frames + '))'); } if (settings.every_n_seconds) { @@ -808,7 +810,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { this.addCommand('-vsync', 0); this.addFilterComplex('select=not(mod(t\\,' + parseInt((this.metadata.duration.seconds / 100) * settings.every_n_percentage) + '))'); } - + // Add destination file path to the command list setOutput([destinationFolder,settings.file_name].join('/')); @@ -824,7 +826,7 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { var newFilepath = null , newSettings = null , callback = null; - + // Scan all arguments for (var i = 1; i < arguments.length; i++) { // Check the type of the argument @@ -840,32 +842,29 @@ module.exports = function (filePath, settings, infoConfiguration, infoFile) { break; } } - + resetCommands(this); // Call the function to add the watermark options this.setWatermark(watermarkPath, newSettings, true); - + if (newFilepath == null) - newFilepath = path.dirname(this.file_path) + '/' + - path.basename(this.file_path, path.extname(this.file_path)) + '_watermark_' + - path.basename(watermarkPath, path.extname(watermarkPath)) + + newFilepath = path.dirname(this.file_path) + '/' + + path.basename(this.file_path, path.extname(this.file_path)) + '_watermark_' + + path.basename(watermarkPath, path.extname(watermarkPath)) + path.extname(this.file_path); - + // Add destination file path to the command list setOutput(newFilepath); - commands.push('-strict'); - commands.push('-2') - // Executing the commands list return execCommand.call(this, callback); } - + /** * Constructor */ var __constructor = function (self) { resetCommands(self); }(this); -} +} \ No newline at end of file