diff --git a/README.md b/README.md index c8f7127..93c7e53 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ plotframes =========== A Node.js CLI frame plotter inspired on [FFmpeg plotframes](https://github.com/FFmpeg/FFmpeg/blob/master/tools/plotframes) but 350x faster! -![plot](http://i.imgur.com/i0iIg8D.png "plot") +![Frame Based](http://i.imgur.com/M4gT0eX.png "Frame Based") +![Time Based](http://i.imgur.com/J3l0Y7h.png "Time Based") Requirements: @@ -25,18 +26,20 @@ options: file passed to the ffprobe command. If not specified it is the first argument passed to the script. - -s v, --stream=v Specify stream. The value must be a string + -s 0, --stream=0 Specify stream. The value must be a string containing a stream specifier. Default value - is "v". + is "0". -o FILE.png, --output=FILE.png Set the name of the output used by gnuplot. If not specified no output is created. Must be used in conjunction with the terminal option. -t png, --terminal=png Set the name of the terminal used by - gnuplot. By default it is "x11". Must be + gnuplot. By default it is "windows". Must be used in conjunction with the output option. Check the gnuplot manual for the valid values. + -f, --frames Create a plot based on frame number instead + of frame time. ``` >**WARNING:** This is **NOT** a node.js module for inclusion in other Node.js scripts, it is just a CLI for use in the terminal/console, maybe in the future I'll see to integrate it somehow. diff --git a/cli.js b/cli.js index a96a1c2..b6309e3 100755 --- a/cli.js +++ b/cli.js @@ -1,5 +1,6 @@ #!/usr/bin/env node var child = require('child_process'), + path = require('path'), dashdash = require('dashdash'), temp = require('temp'), split = require("split"), @@ -21,9 +22,9 @@ var options = [ }, { names: ['stream', 's'], type: 'string', - help: 'Specify stream. The value must be a string containing a stream specifier. Default value is "v".', - helpArg: 'v', - default: 'v' + help: 'Specify stream. The value must be a string containing a stream specifier. Default value is "0".', + helpArg: '0', + default: '0' }, { names: ['output', 'o'], type: 'string', @@ -35,6 +36,10 @@ var options = [ help: 'Set the name of the terminal used by gnuplot. By default it is "'+defterminal+'". Must be used in conjunction with the output option. Check the gnuplot manual for the valid values.', helpArg: 'png', default: defterminal + }, { + names: ['frames', 'f'], + type: 'bool', + help: 'Create a plot based on frame number instead of frame time.' } ]; @@ -66,7 +71,7 @@ if (!opts.input) { } } -// Automatically track and cleanup files at exit +// Cleanup stream files on exit temp.track(); var clnl = false; @@ -79,13 +84,14 @@ function cutelog(str, nl){ } } - +// Pad number function pad(num, size) { var str = num + ""; while (str.length < size) str = "0" + str; return str; } +// Seconds to time format function toHHMMSS(n) { var sep = ':', n = parseFloat(n), @@ -97,28 +103,67 @@ function toHHMMSS(n) { return pad(hh,2)+sep+pad(mm,2)+sep+pad(ss,2)+'.'+pad(sss,3); } -// Get file duration -function getDuration(input, cb){ - var r_duration = /Duration: ((\d{2}):(\d{2}):(\d{2}).(\d{2})), /; - var r; +// Get average in array +function getAvg(arr) { + return arr.reduce(function (p, c) {return p + c;}) / arr.length; +} + +// Bits into human readable units +function bandWidth(bits) { + bits = bits* 1000; + var unit = 1000; + if (bits < unit) return (bits % 1 === 0 ? bits : bits.toFixed(2)) + "B"; + var exp = parseInt(Math.log(bits) / Math.log(unit)); + var pre = "KMGTPE"[exp-1] + 'bps'; + var n = bits / Math.pow(unit, exp); + return (n % 1 === 0 ? n : n.toFixed(2))+pre; +} + + +// Get file Details +function getDetails(input, cb){ + + var rd, + rf, + frame_rate, + duration, + seconds, + r_frame_rate = /avg_frame_rate\=(\d+)\/(\d+)/, + r_duration = /Duration: ((\d{2}):(\d{2}):(\d{2}).(\d{2})), /; + var cli = child.spawn( 'ffprobe', [ + '-show_entries', + 'stream', + '-select_streams', + opts.stream, input ],[] ); - cli.stderr.on('data', function (data) { - if(r = r_duration.exec(data)){ - cb({ - duration: r[1], - seconds: ((((r[2]*60)+r[3])*60)+parseInt(r[4]))+parseFloat(r[5]/100) - }); + cli.stdout.pipe(split()).on('data', function (data) { + if(rf = r_frame_rate.exec(data)){ + frame_rate = (rf[1]/rf[2]) || 1; + } + }); + + cli.stderr.pipe(split()).on('data', function (data) { + if(rd = r_duration.exec(data)){ + duration = rd[1]; + seconds = ((((rd[2]*60)+rd[3])*60)+parseInt(rd[4]))+parseFloat(rd[5]/100); } }); cli.on('close', function (code) { if (code !== 0) { - cutelog('Error trying to get the file duration.',false); + cutelog('Error trying to get the file details.',false); + } + if(frame_rate && duration){ + cb({ + frame_rate: frame_rate, + duration: duration, + seconds: seconds + }); } }); @@ -127,18 +172,24 @@ function getDuration(input, cb){ }); cli.on('error', function() { - console.log('Error running FFprobe, check if it is installed correctly and if it is included in the system environment path.'); + cutelog('Error running FFprobe, check if it is installed correctly and if it is included in the system environment path.',false); process.exit(1); }); } // Get frame bitrate -function getBitrate(input, time, cb){ +function getBitrate(input, details, cb){ var frame_count = 0, kbps_count = 0, peak = 0, min = 0, start= true, + frame_bitrate, + frame_zbits, + frame_time, + frame_type, + time_sec, + times = [], streams = [], r, r_frame = /(?:media_type\=(\w+)\r?\n)(?:stream_index\=(\w+)\r?\n)(?:pkt_pts_time\=(\d*.?\d*)\r?\n)(?:pkt_size\=(\d+)\r?\n)(?:pict_type\=(\w+))?/; @@ -154,23 +205,39 @@ function getBitrate(input, time, cb){ ); cli.stdout.pipe(split(/\[\/FRAME\]\r?\n/)).on('data', function (data){ + if(r = r_frame.exec(data)){ - r[4] = (r[4]*8)/1000; - + + // Cleaning the data + frame_zbits = (r[4]*8)/1000; + frame_time = parseFloat(r[3]); + frame_type = r[5]?r[5]:'A'; + + // Create stream if not exists + if(!streams[frame_type]){streams[frame_type] = temp.createWriteStream();} + + // Counters frame_count++; - kbps_count += r[4]; + kbps_count += frame_zbits; - if(start){min = r[4]; start=null;} - peak = peak < r[4] ? r[4] : peak; - min = min > r[4] ? r[4] : min; - - r[5] = r[5]?r[5]:'A'; - if(!streams[r[5]]){streams[r[5]] = temp.createWriteStream();} - - r[3] = parseFloat(r[3]); + frame_bitrate = frame_zbits * details.frame_rate; - streams[r[5]].write(frame_count+' '+r[4]+'\n'); - cutelog('Analyzing '+toHHMMSS(r[3])+' / '+time.duration+' '+((r[3]/time.seconds)*100).toFixed(2)+'%',true); + if(opts.frames){ + if(start){min = frame_bitrate; start=null;} + peak = peak < frame_bitrate ? frame_bitrate : peak; + min = min > frame_bitrate ? frame_bitrate : min; + streams[frame_type].write(frame_count+' '+frame_bitrate+'\n'); + }else{ + time_sec = parseInt(frame_time); + if(times[time_sec]){ + times[time_sec]+=frame_zbits; + }else{ + times[time_sec] = frame_zbits; + } + streams[frame_type].write(frame_time+' '+frame_bitrate+'\n'); + } + + cutelog('Analyzing '+toHHMMSS(frame_time)+' / '+details.duration+' '+((frame_time/details.seconds)*100).toFixed(2)+'%',true); } }); @@ -178,14 +245,28 @@ function getBitrate(input, time, cb){ if (code !== 0) { cutelog('Error trying to get the file bitrate.',false); } - cutelog('Analysis complete.',false); - cb({ - streams: streams, - avg: kbps_count/frame_count, - peak: peak, - min: min - }); + cutelog('Analysis complete.',false); + + if(opts.frames){ + cb({ + streams: streams, + avg: kbps_count/details.seconds, + peak: peak, + min: min, + frames: frame_count, + seconds: details.seconds + }); + }else{ + cb({ + streams: streams, + avg: getAvg(times), + peak: Math.max.apply(Math, times), + min: Math.min.apply(Math, times), + frames: frame_count, + seconds: details.seconds + }); + } }); process.on('exit', function() { @@ -193,7 +274,7 @@ function getBitrate(input, time, cb){ }); cli.on('error', function() { - console.log('Error running FFprobe, check if it is installed correctly and if it is included in the system environment path.'); + cutelog('Error running FFprobe, check if it is installed correctly and if it is included in the system environment path.',false); process.exit(1); }); } @@ -209,7 +290,14 @@ function createPlot(data, cb){ }; var sep=''; - var scr='set title "Frames Sizes in Kbits"\nset xlabel "Average: '+parseInt(data.avg)+', Peak: '+parseInt(data.peak)+', Min: '+parseInt(data.min)+'."\nset ylabel "Kbits"\nset grid\nset terminal "'+opts.terminal+'"\n'; + var scr='set title "Frames Bitrates for \\"'+path.basename(opts.input)+':'+opts.stream+'\\" "\n'; + if(opts.frames){ + scr+='set xlabel "Avg Bitrate: '+bandWidth(data.avg)+'. '+data.frames+' Frames; Peak: '+bandWidth(data.peak)+' Min: '+bandWidth(data.min)+'."\n'; + }else{ + scr+='set xlabel " Avg Bitrate: '+bandWidth(data.avg)+'. '+parseInt(data.seconds)+' Seconds; Max: '+bandWidth(data.peak)+' Min: '+bandWidth(data.min)+'."\n'; + } + scr+='set ylabel "Frames Kbps"\nset grid\nset terminal "'+opts.terminal+'"\n'; + if(opts.output){ scr += 'set output "'+opts.output+'"\n'; } @@ -227,7 +315,6 @@ function createPlot(data, cb){ gnuplot.write(scr); gnuplot.end(); - // Run gnuplot var cli = child.spawn( 'gnuplot', [ '--persist', @@ -252,16 +339,15 @@ function createPlot(data, cb){ }); cli.on('error', function() { - console.log('Error running gnuplot, check if it is installed correctly and if it is included in the system environment path.'); + cutelog('Error running gnuplot, check if it is installed correctly and if it is included in the system environment path.',false); process.exit(1); }); } // Run -getDuration(opts.input, function(time){ - getBitrate(opts.input, time, function(data){ +getDetails(opts.input, function(details){ + getBitrate(opts.input, details, function(data){ createPlot(data, function(){ }); }); }); - diff --git a/package.json b/package.json index 0c73037..d8a637d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotframes", - "version": "1.0.4", + "version": "1.5.0", "description": "A Node.js frame plotter inspired by FFmpeg plotframes", "main": "cli.js", "preferGlobal": true, @@ -31,5 +31,5 @@ "split": "1.0.x", "temp": "0.8.x" }, - "readme": "plotframes\n===========\nA Node.js CLI frame plotter inspired on [FFmpeg plotframes](https://github.com/FFmpeg/FFmpeg/blob/master/tools/plotframes) but 350x faster!\n\n![plot](http://i.imgur.com/i0iIg8D.png \"plot\")\n\n\nRequirements:\n* [gnuplot](http://www.gnuplot.info/) \n* [FFmpeg >= 1.2 with the ffprobe command](https://www.ffmpeg.org/)\n\n\nInstallation and running\n```\nnpm install plotframes -g\nplotframes input.mkv\n```\n\nUsage\n```\nUsage: plotframes [OPTIONS]\noptions:\n -h, --help Print this help and exit.\n -i FILE, --input=FILE Specify multimedia file to read. This is the\n file passed to the ffprobe command. If not\n specified it is the first argument passed to\n the script.\n -s v, --stream=v Specify stream. The value must be a string\n containing a stream specifier. Default value\n is \"v\".\n -o FILE.png, --output=FILE.png Set the name of the output used by gnuplot.\n If not specified no output is created. Must\n be used in conjunction with the terminal\n option.\n -t png, --terminal=png Set the name of the terminal used by\n gnuplot. By default it is \"x11\". Must be\n used in conjunction with the output option.\n Check the gnuplot manual for the valid\n values.\n```\n\n>**WARNING:** This is **NOT** a node.js module for inclusion in other Node.js scripts, it is just a CLI for use in the terminal/console, maybe in the future I'll see to integrate it somehow.\n\n### Installing dependencies\n\n#### Windows\n1. [Download](https://nodejs.org) and install Node.js \n2. [Download](http://ffmpeg.zeranoe.com/builds/) a FFmpeg build, uncompress it into a directory that is included in the system `%path%`.\n3. [Download](http://sourceforge.net/projects/gnuplot/) and install `gnpuplot`: \n There are [other downloads](http://sourceforge.net/projects/gnuplot/files/gnuplot/) in case you want to download a different version. \n4. Open your `Command Promt` and run `npm install plotframes`.\n\n\n#### OS X\n* Install Xcode from the App Store\n* Install [Homebrew](http://brew.sh)\n\nThen, using Hombrew intall FFmpeg, XQuartz (needed to render to x11) and gnuplot.\n```\nbrew install ffmpeg\nbrew install Caskroom/cask/xquartz\nbrew install gnuplot --with-x11\n```\n\n#### Ubuntu\n\nInstall Node.js\n```\ncurl -sL https://deb.nodesource.com/setup | sudo bash -\nsudo apt-get install nodejs\n```\n\nInstall FFmpeg\n```\nsudo ppa-purge ppa:mc3man/trusty-media\nsudo add-apt-repository ppa:mc3man/trusty-media\nsudo apt-get update\nsudo apt-get install ffmpeg\n```\n\nInstall gnuplot\n```\nsudo apt-get install gnuplot-x11\n```\n\nOr do all at once\n```\ncurl -sL https://deb.nodesource.com/setup | sudo bash -\nsudo ppa-purge ppa:mc3man/trusty-media\nsudo add-apt-repository ppa:mc3man/trusty-media\nsudo apt-get update\nsudo apt-get install ffmpeg gnuplot-x11 nodejs\n```\n\nAfter that you can install `plotframes`, if it gives you a permision error, run `sudo` command before:\n```\nsudo npm install plotframes\n```\n\n\n## License\n\n(The MIT License)\n\nCopyright (c) by Rodrigo Polo http://RodrigoPolo.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE." + "readme": "plotframes\n===========\nA Node.js CLI frame plotter inspired on [FFmpeg plotframes](https://github.com/FFmpeg/FFmpeg/blob/master/tools/plotframes) but 350x faster!\n\n![Frame Based](http://i.imgur.com/M4gT0eX.png \"Frame Based\")\n![Time Based](http://i.imgur.com/J3l0Y7h.png \"Time Based\")\n\n\nRequirements:\n* [gnuplot](http://www.gnuplot.info/) \n* [FFmpeg >= 1.2 with the ffprobe command](https://www.ffmpeg.org/)\n\n\nInstallation and running\n```\nnpm install plotframes -g\nplotframes input.mkv\n```\n\nUsage\n```\nUsage: plotframes [OPTIONS]\noptions:\n -h, --help Print this help and exit.\n -i FILE, --input=FILE Specify multimedia file to read. This is the\n file passed to the ffprobe command. If not\n specified it is the first argument passed to\n the script.\n -s 0, --stream=0 Specify stream. The value must be a string\n containing a stream specifier. Default value\n is \"0\".\n -o FILE.png, --output=FILE.png Set the name of the output used by gnuplot.\n If not specified no output is created. Must\n be used in conjunction with the terminal\n option.\n -t png, --terminal=png Set the name of the terminal used by\n gnuplot. By default it is \"windows\". Must be\n used in conjunction with the output option.\n Check the gnuplot manual for the valid\n values.\n -f, --frames Create a plot based on frame number instead\n of frame time.\n```\n\n>**WARNING:** This is **NOT** a node.js module for inclusion in other Node.js scripts, it is just a CLI for use in the terminal/console, maybe in the future I'll see to integrate it somehow.\n\n### Installing dependencies\n\n#### Windows\n1. [Download](https://nodejs.org) and install Node.js \n2. [Download](http://ffmpeg.zeranoe.com/builds/) a FFmpeg build, uncompress it into a directory that is included in the system `%path%`.\n3. [Download](http://sourceforge.net/projects/gnuplot/) and install `gnpuplot`: \n There are [other downloads](http://sourceforge.net/projects/gnuplot/files/gnuplot/) in case you want to download a different version. \n4. Open your `Command Promt` and run `npm install plotframes`.\n\n\n#### OS X\n* Install Xcode from the App Store\n* Install [Homebrew](http://brew.sh)\n\nThen, using Hombrew intall FFmpeg, XQuartz (needed to render to x11) and gnuplot.\n```\nbrew install ffmpeg\nbrew install Caskroom/cask/xquartz\nbrew install gnuplot --with-x11\n```\n\n#### Ubuntu\n\nInstall Node.js\n```\ncurl -sL https://deb.nodesource.com/setup | sudo bash -\nsudo apt-get install nodejs\n```\n\nInstall FFmpeg\n```\nsudo ppa-purge ppa:mc3man/trusty-media\nsudo add-apt-repository ppa:mc3man/trusty-media\nsudo apt-get update\nsudo apt-get install ffmpeg\n```\n\nInstall gnuplot\n```\nsudo apt-get install gnuplot-x11\n```\n\nOr do all at once\n```\ncurl -sL https://deb.nodesource.com/setup | sudo bash -\nsudo ppa-purge ppa:mc3man/trusty-media\nsudo add-apt-repository ppa:mc3man/trusty-media\nsudo apt-get update\nsudo apt-get install ffmpeg gnuplot-x11 nodejs\n```\n\nAfter that you can install `plotframes`, if it gives you a permision error, run `sudo` command before:\n```\nsudo npm install plotframes\n```\n\n\n## License\n\n(The MIT License)\n\nCopyright (c) by Rodrigo Polo http://RodrigoPolo.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE." }