1
0
mirror of https://github.com/danog/plotframes.git synced 2024-11-30 04:19:07 +01:00
plotframes/main.js

495 lines
11 KiB
JavaScript
Raw Normal View History

2015-10-17 17:34:41 +02:00
/*! plotframes | (c) 2015 RodrigoPolo.com | MIT License | https://github.com/rodrigopolo/plotframes/blob/master/LICENSE */
var
fs = require('fs'),
stream = require('stream'),
child = require('child_process'),
extend = require('extend'),
path = require('path'),
split = require("split"),
isWin = /^win/.test(process.platform),
defterminal = isWin ? 'windows' : 'x11';
// Average array prototype
function arrAvg(arr){
return arr.reduce(function (p, c) {return p + c;}) / arr.length;
}
// Max array prototype
function arrMax(arr){
return Math.max.apply(Math, arr);
}
// Min array prototype
function arrMin(arr){
return Math.min.apply(Math, arr);
}
// 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
rf,
r_frame_rate = /avg_frame_rate\=(\d+)\/(\d+)/,
frame_rate,
rd,
r_duration = /Duration: ((\d{2}):(\d{2}):(\d{2}).(\d{2})), /,
duration,
seconds;
// Run ffprobe
var cli = child.spawn(
'ffprobe', [
'-show_entries',
'stream',
input
],[]
);
// Get frame rate from stdout
cli.stdout.pipe(split()).on('data', function (data) {
if(rf = r_frame_rate.exec(data)){
if(!frame_rate){
frame_rate = (rf[1]/rf[2]) || 1;
}
}
});
// Get duration from sterr
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);
}
});
// After running ffprobe return the results
cli.on('close', function (code) {
if (code !== 0) {
fs.exists(input, function(exists) {
if(exists){
cb('Error trying to get the file details.', null);
}else{
cb('Error trying to get the file details, input file doesn\'t exists.', null);
}
});
}
cb(null, {
frame_rate: frame_rate,
duration: duration,
seconds: seconds
});
});
// If node script exits, kills the child
process.on('exit', function() {
cli.kill();
});
// On error running ffprobe
cli.on('error', function() {
cb('Error running FFprobe, check if it is installed correctly and if it is included in the system environment path.', null);
});
}
2015-10-17 23:38:41 +02:00
2015-10-17 17:34:41 +02:00
// Get frame bitrate
function getBitrate(input, details, progress, cb){
var
2015-10-17 23:38:41 +02:00
// for regex
2015-10-17 17:34:41 +02:00
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+))?/,
r,
2015-10-17 23:38:41 +02:00
// For frame arrays
2015-10-17 17:34:41 +02:00
frame_count = 0,
2015-10-17 23:38:41 +02:00
frame_stream,
2015-10-17 17:34:41 +02:00
frame_type,
2015-10-17 23:38:41 +02:00
frame_bitrate,
2015-10-17 17:34:41 +02:00
frame_size,
frame_time,
2015-10-17 23:38:41 +02:00
// Frame Arrays
frames = {
count: [],
stream: [],
type: [],
bitrate: [],
size: [],
time: []
},
// For loop
frame_size;
2015-10-17 17:34:41 +02:00
// Run ffprobe
var cli = child.spawn(
'ffprobe', [
'-show_entries',
'frame=stream_index,media_type,pict_type,pkt_size,pkt_pts_time',
input
],[]
);
// Get frame data
cli.stdout.pipe(split(/\[\/FRAME\]\r?\n/)).on('data', function (data){
if(r = r_frame.exec(data)){
frame_count++;
2015-10-17 23:38:41 +02:00
frame_stream = parseInt(r[2]);
2015-10-17 17:34:41 +02:00
frame_type = r[5]?r[5]:'A';
frame_size = ((r[4]*8)/1000);
frame_bitrate = frame_size * details.frame_rate;
frame_time = parseFloat(r[3]);
2015-10-17 23:38:41 +02:00
frames.count.push(frame_count);
frames.stream.push(frame_stream);
frames.type.push(frame_type);
frames.bitrate.push(frame_bitrate);
frames.size.push(frame_size);
frames.time.push(frame_time);
2015-10-17 17:34:41 +02:00
// Show progress
progress({
time: frame_time,
duration: details.duration,
length: details.seconds
});
}
});
// After running ffprobe return the results
cli.on('close', function (code) {
if (code !== 0) {
cb('Error trying to get the file bitrate.', null);
}
2015-10-17 23:38:41 +02:00
cb(null, frames);
2015-10-17 17:34:41 +02:00
});
// If node script exits, kills the child
process.on('exit', function() {
cli.kill();
});
// On error running ffprobe
cli.on('error', function() {
cb('Error running FFprobe, check if it is installed correctly and if it is included in the system environment path.', null);
});
}
// Create an object containing all the frame data
function getFrames(input, progress, cb){
progress = progress || function(d){}
getDetails(input, function(err, details){
if(err){
cb(err, null);
}else{
getBitrate(input, details, progress, function(err, data){
if(err){
cb(err, null);
}else{
cb(null, data);
}
});
}
});
}
// Generate gnuplot script
2015-10-17 23:38:41 +02:00
function genScript(input, frames, ops, cb){
// Functional
var all_streams = (ops.stream=='all');
var is_frames = ops.frames;
var selected_stream = parseInt(ops.stream);
2015-10-17 17:34:41 +02:00
var graphs = [];
2015-10-17 23:38:41 +02:00
var frame_types = [
2015-10-17 17:34:41 +02:00
'I',
'P',
'B',
2015-10-17 23:38:41 +02:00
'A',
2015-10-17 17:34:41 +02:00
];
var label_sep = '\\n';
2015-10-17 23:38:41 +02:00
// for loops
var frame_sec;
// for storing
var bitrate = [];
var selected_stream_bitrate = [];
var selected_stream_bitrate_pos = [];
var selected_bitrate;
var gs = '';
var bitrate_max;
var bitrate_min;
var bitrate_avg;
var selected_frame_count = 0;
var selected_frame_types={};
var frame_start = frames.count[0];
var frame_end = frames.count[frames.count.length-1];
var time_start = frames.time[0];
var time_end = frames.time[frames.time.length-1];
// Create bitrate by sec for selected streams, count frames and save frame types
for (var i = 0; i < frames.size.length; i++) {
frame_sec = parseInt(frames.time[i]);
if(all_streams){
selected_frame_types[frames.type[i]] = []; // TODO: add stream number
selected_stream_bitrate.push(frames.size[i]);
selected_stream_bitrate_pos.push(frames.count[i]);
selected_frame_count++;
if(bitrate[frame_sec]){
bitrate[frame_sec] += frames.size[i];
}else{
bitrate[frame_sec] = frames.size[i];
}
}else{
if(frames.stream[i] == selected_stream){
selected_frame_types[frames.type[i]] = []; // TODO: add stream number
selected_stream_bitrate.push(frames.size[i]);
selected_stream_bitrate_pos.push(frames.count[i]);
selected_frame_count++;
if(bitrate[frame_sec]){
bitrate[frame_sec] += frames.size[i];
}else{
bitrate[frame_sec] = frames.size[i];
}
}
}
};
selected_bitrate = (is_frames)?frames.bitrate:bitrate;
if(bitrate.length == 0){
cb('There is no data in the selected stream.', null);
2015-10-17 17:34:41 +02:00
}
// Set font size
2015-10-17 23:38:41 +02:00
gs+='set key font ",10"\n';
2015-10-17 17:34:41 +02:00
// Graph range
2015-10-17 23:38:41 +02:00
if(is_frames){
gs+='set xrange ['+frame_start+':'+frame_end+']\n';
2015-10-17 17:34:41 +02:00
}else{
2015-10-17 23:38:41 +02:00
gs+='set xrange ['+time_start+':'+time_end+']\n';
2015-10-17 17:34:41 +02:00
}
// Title
gs += 'set title "Frames Bitrates for \\"'+path.basename(input);
2015-10-17 23:38:41 +02:00
if(all_streams){
2015-10-17 17:34:41 +02:00
gs += '\\""\n';
}else{
gs += ':'+ops.stream+'\\""\n';
}
2015-10-17 23:38:41 +02:00
// Measurement
bitrate_max = arrMax(selected_bitrate);
bitrate_min = arrMin(selected_bitrate);
bitrate_avg = arrAvg(selected_bitrate);
2015-10-17 17:34:41 +02:00
// Info label
gs+='set label "';
2015-10-17 23:38:41 +02:00
if(is_frames){
gs+='Frames: '+selected_frame_count+' of '+frames.count[frames.count.length-1]+label_sep;
2015-10-17 17:34:41 +02:00
}else{
2015-10-17 23:38:41 +02:00
gs+='Seconds: '+frames.time[frames.time.length-1]+label_sep;
2015-10-17 17:34:41 +02:00
}
gs+=
2015-10-17 23:38:41 +02:00
'Max: '+bandWidth(bitrate_max)+label_sep
+'Min: '+bandWidth(bitrate_min)+label_sep
+'Avg: '+bandWidth(bitrate_avg);
2015-10-17 17:34:41 +02:00
gs+='" left at graph 0.005, graph .970 font ",10"\n';
// X Label
2015-10-17 23:38:41 +02:00
gs+='set xlabel "'+((is_frames)?'Frames':'Seconds')+'" font ",10"\n';
2015-10-17 17:34:41 +02:00
// Y Label
gs+='set ylabel "Kbps" font ",10"\n';
// Grid
gs += (ops.grid)?'set grid\n':'';
// Terminal
gs+= 'set terminal '+ops.terminal+'\n';
// Output
gs += (ops.output)?'set output "'+ops.output+'"\n':'';
// Plotting
gs += 'plot \\\n';
2015-10-17 23:38:41 +02:00
// Loop trough selected stream frames
for (var i = 0; i < frame_types.length; i++) {
if(selected_frame_types[frame_types[i]]){
graphs.push('"-" title "'+frame_types[i]+'" with '+ops.styles[frame_types[i]]+' linecolor rgb "'+ops.colors[frame_types[i]]+'"');
}
};
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
if(!is_frames){
// Average bitrate
graphs.push('"-" title "Average" with lines lc rgb "'+ops.colors.average+'" lt 1 lw .5');
// Bitrate
graphs.push('"-" smooth bezier title "Bitrate" with lines lc rgb "'+ops.colors.bitrate+'" lt 1 lw 1.5');
}
2015-10-17 17:34:41 +02:00
// Add the graphs defs
gs += graphs.join(', \\\n')+' \n';
2015-10-17 23:38:41 +02:00
// Add frames for selected streams
for (var i = 0; i < frames.bitrate.length; i++) {
if(selected_frame_types[frames.type[i]]){
if(is_frames){
selected_frame_types[frames.type[i]].push(frames.count[i]+' '+frames.bitrate[i]);
}else{
selected_frame_types[frames.type[i]].push(frames.time[i]+' '+frames.bitrate[i]);
}
2015-10-17 17:34:41 +02:00
}
2015-10-17 23:38:41 +02:00
};
// Loop trough selected stream frames
for (var i = 0; i < frame_types.length; i++) {
if(selected_frame_types[frame_types[i]]){
gs += selected_frame_types[frame_types[i]].join('\n')+'\ne\n';
2015-10-17 17:34:41 +02:00
}
2015-10-17 23:38:41 +02:00
};
// Add average bitrate line
if(!is_frames){
gs += time_start+' '+bitrate_avg+'\n';
gs += time_end+' '+bitrate_avg+'\ne\n';
2015-10-17 17:34:41 +02:00
}
2015-10-17 23:38:41 +02:00
// Add bitrate line
if(!is_frames){
for (var i = 0; i < bitrate.length; i++) {
gs += i+' '+bitrate[i]+'\n';
2015-10-17 17:34:41 +02:00
};
2015-10-17 23:38:41 +02:00
gs += 'e';
2015-10-17 17:34:41 +02:00
}
2015-10-17 23:38:41 +02:00
cb(null, gs);
2015-10-17 17:34:41 +02:00
}
// Parse the options, gen the script and call gnuplot via stdin
function plotScript(input, cb, op){
var options = {
stream: 'all',
terminal: defterminal,
output: false,
frames: false,
colors:{
I: '#ff0000',
P: '#00ff00',
B: '#0000ff',
A: '#d200ff',
average: '#000000',
bitrate: '#ff9016',
},
styles:{
I: 'impulses',
P: 'impulses',
B: 'impulses',
A: 'impulses',
},
grid: true,
as_string: false,
stdout: null,
stderr: null,
progress: function(d){}
}
extend(options, op);
// To prevent almost imposible Command Injection
if(options.terminal != 'windows' && options.terminal != 'x11'){
options.terminal = defterminal;
}
2015-10-17 17:34:41 +02:00
var script_str='';
getFrames(input, options.progress, function(err, data){
if(err){
cb(err, null);
}else{
// gnuplot script
2015-10-17 23:38:41 +02:00
genScript(input, data, options,function(err, gs){
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
if(err){
cb(err, null);
}else{
if(options.as_string){
cb(null, gs);
2015-10-17 17:34:41 +02:00
}else{
2015-10-17 23:38:41 +02:00
// Run gnuplot
var cli = child.spawn(
'gnuplot', [
'-p'
], {stdin: 'pipe'}
);
// Pipe streams
if(options.stdout){
cli.stdout.pipe(options.stdout);
}
if(options.stderr){
cli.stderr.pipe(options.stderr);
}
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
// Check if there is an error code, if not, run the callback with true message
cli.on('close', function (code) {
if (code !== 0) {
cb('Error trying to run gnuplot.', null);
}else{
cb(null, true);
}
});
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
// If node script exits, kills the child
process.on('exit', function() {
cli.kill();
});
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
// On error running gnuplot
cli.on('error', function() {
cb('Error running gnuplot, check if it is installed correctly and if it is included in the system environment path.', null);
});
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
cli.stdin.setEncoding('utf-8');
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
var script = new stream.Readable();
script._read = function noop() {};
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
script.pipe(cli.stdin);
2015-10-17 17:34:41 +02:00
2015-10-17 23:38:41 +02:00
script.push(gs);
script.push(null);
}
}
});
2015-10-17 17:34:41 +02:00
}
}, options);
}
module.exports = {
getFrames: getFrames,
plotScript: plotScript
};