CamiloMM/avisynth

View on GitHub
code/script.js

Summary

Maintainability
C
7 hrs
Test Coverage
var crypto        = require('crypto');
var loader        = require('./loader');
var autoload      = require('./autoload');
var pluginSystem  = require('./plugins');
var system        = require('./system');
var utils         = require('./utils');
var AvisynthError = require('./errors').AvisynthError;

// Avisynth script constructor.
// Note that I don't like the overhead of having getters and setters,
// so the coding style here is "KISS". Defensive programming is for afflicted people.
function Script(code) {
    // Raw copy of the code.
    this.rawCode = code || '';

    // These references work like in the loader. See loader.js.
    // This is not intended for direct insertion, using the functions below is safer.
    this.references = {};

    // Similar to avisynth.load, but local to one script.
    this.load = function(file, ignoreErrors) {
        loader.load(file, ignoreErrors, this.references);
    };

    // Similar to avisynth.autoload, but local to one script.
    this.autoload = function(dir) {
        autoload(dir, this.load.bind(this));
    };

    // Get a list of references that apply to this script (global + local).
    this.allReferences = function() {
        var refs = {};
        for (var i in loader.references) refs[i] = loader.references[i];
        for (var i in   this.references) refs[i] =   this.references[i];
        return refs;
    };

    // Adds one or more lines of code.
    this.code = function(code) {
        if (this.rawCode && this.rawCode[this.rawCode.length - 1] !== '\n') {
            this.rawCode += '\n';
        }
        this.rawCode += code + '\n';
    };

    // Gets the full code, including loading of scripts/plugins and all generated code.
    this.fullCode = function() {
        var fullCode = '', refs = this.allReferences();

        for (var ref in refs) {
            if (refs[ref] === 'script') fullCode += 'Import("' + ref + '")\n';
            if (refs[ref] === 'plugin') fullCode += 'LoadPlugin("' + ref + '")\n';
        }

        if (this.rawCode.trim()) fullCode += this.rawCode + '\n';
        return fullCode;
    };

    // Gets the MD5 (in hex) of this script's contents.
    this.md5 = function() {
        var code = this.fullCode();
        var hash = crypto.createHash('md5');
        return hash.update(code).digest('hex');
    };

    // Gets a path to a generated file containing the contents of this script.
    // The path will be located in a temporary directory, and identified by
    // the script fullCode's MD5 hash (all generated scripts will be in the same folder).
    // Note that by the nature of this hash ID mechanism, once you get a path, the
    // file at that path is guaranteed to never change (even if you edit the instance).
    this.getPath = function() {
        var md5 = this.md5();
        var sub = 'scripts/' + md5 + '.avs';
        var path = system.temp(sub);
        if (path) return path;
        var path = system.tempWrite(sub, this.fullCode());
        return path;
    };

    // Renders a frame of the script to a path.
    // The callback, if given, will be executed once the frame is rendered,
    // with an error parameter (null unless an error is encountered).
    this.renderFrame = function(time, path, callback) {
        // Time is optional.
        if (!utils.isNumeric(time)) {
            callback = path;
            path = time;
            time = 0;
        }

        // Path must be absolute.
        path = require('path').resolve(path);

        // This is the command that will be ran:
        var args = ['-hide_banner', '-y', '-loglevel', 'error', '-ss',
                    time, '-i', this.getPath(), '-frames:v', 1, path];

        system.spawn(system.ffmpeg, args, 'scripts', callback);
    };

    // "Just runs" the script; this is useful if you want the script to
    // do something but don't care about its output (e.g., writing files).
    this.run = function(callback) {
        var path = this.getPath();
        var args = ['-hide_banner', '-loglevel', 'error', '-i', path, '-f', 'null', '-'];
        system.spawn(system.ffmpeg, args, 'scripts', callback);
    };

    // Lints the script. The callback will be called with an "error" argument
    // where error is an AvisynthError, if any.
    this.lint = function(callback) {
        Script.lint(this.getPath(), 'scripts', callback);
    };

    // Returns raw info on the running script, in machine-readable form.
    // The callback is called with (error, info).
    this.info = function(callback) {
        Script.info(this.getPath(), 'scripts', callback);
    };
}

// Static method for the lint instance method, used by the cli script too.
// The callback is called with an error argument, if any.
Script.lint = function(scriptPath, cwd, callback) {
    system.spawn(system.avslint, [scriptPath], cwd, callback);
};

// Static method for the info instance method, used by the cli script too.
// The callback is called with (error, info).
Script.info = function(scriptPath, cwd, callback) {
    var arg = ['-m', scriptPath];
    system.spawn(system.avsinfo, arg, cwd, true, function(code, stdout, stderr) {
        if (code && code !== 2) {
            // This probably means status code 5, which happens on crashes,
            // or a status code I'm not aware of. Either way, it's unexpected.
            // Note that it happens when code does not produce output.
            var message = 'Unexpected condition (' + code + ').\n'
                        + 'This may be caused by a blank script.';
            return callback(new AvisynthError(message));
        }

        // This means no output could be analyzed.
        // Unfortunately, audio-only clips will not be analyzed by avsinfo.
        if (!stdout) {
            // Note that "no info" and "everything is wrong" return the same
            // exit code (rather unhelpfully). So anything that does not look
            // just right will be thrown as an error for safety. This is detected
            // from the stderr looking like "<inputfile> has no video: C:\foo.avs".
            if (stderr.substr(0, 19) !== '<inputfile> has no ') {
                // Btw, yes, this is damn ugly, I'm aware.
                // Still not as ugly as getting anywhere near VC++ to fix avsinfo.
                var message = stderr.replace(/\r?\n$/, '');
                return callback(new AvisynthError(message));
            } else {
                return callback(undefined, null);
            }
        }

        // If it does not contain audio, the last 7 properties will be undefined.
        var properties = [
            'width',         // Video width in pixels.
            'height',        // Video height in pixels.
            'ratio',         // Aspect ratio, such as '16:9'.
            'fps',           // Frames per second as a number, like 29.97.
            'fpsFraction',   // Frames per second as a fraction, like '30000/1001'.
            'videoTime',     // Video length in seconds, like 123.456.
            'frameCount',    // Frame count.
            'colorspace',    // Colorspace, such as 'RGB' or 'YV12'.
            'bitsPerPixel',  // Number of bits per pixel.
            'interlaceType', // Can be 'field-based' or 'frame-based'.
            'fieldOrder',    // 'TFF' (Top Field First) or 'BFF' (Bottom Field First).
            'channels',      // Number of channels.                   May be undefined.
            'bitsPerSample', // Number of bits per audio sample.      May be undefined.
            'sampleType',    // Sample type can be 'int' or 'float'.  May be undefined.
            'audioTime',     // Audio length in seconds.              May be undefined.
            'samplingRate',  // Sampling rate in Hertz (e.g., 44100). May be undefined.
            'sampleCount',   // Number of samples (time * rate).      May be undefined.
            'blockSize',     // Block size of audio samples in Bytes. May be undefined.
        ];

        // Each of the properties above should correspond to a line in the stdout.
        var values = stdout.split(/\r?\n/); // Windows works in CRLF, remember.
        var info = utils.zipObject(properties, values);

        // Cast some properties to numbers.
        var numberProps = ['width', 'height', 'fps', 'videoTime', 'frameCount',
                           'bitsPerPixel', 'interlaceType', 'fieldOrder',
                           'channels', 'bitsPerSample', 'sampleType', 'audioTime',
                           'samplingRate', 'sampleCount', 'blockSize'];

        numberProps.forEach(function(prop) {
            if (info[prop] !== '' && info[prop] !== undefined) {
                info[prop] *= 1;
            }
        });

        // Convert some bools to more identifiable strings.
        var enums = {
            interlaceType: ['frame-based', 'field-based'],
            fieldOrder:    ['BFF'        , 'TFF'        ],
            sampleType:    ['float'      , 'int'        ],
        };

        // If you're confused: info[prop] will either be 0, 1 or undefined.
        for (var prop in enums) info[prop] = enums[prop][info[prop]];

        callback(undefined, info); // No error, and there's your info.
    });
};

// Constructor that's ok with forgetting "new".
Script.wrappedConstructor = function(code) { return new Script(code); };

// Static API.
var props = ['lint', 'info'];
props.forEach(function(prop) {
    Script.wrappedConstructor[prop] = Script[prop];
});

// Add the plugins via prototype inheritance.
Script.prototype = pluginSystem.pluginPrototype;

module.exports = Script;