Source: org/terraswarm/accessor/accessors/web/cameras/Camera.js

// Copyright (c) 2015-2017 The Regents of the University of California.
// All rights reserved.
//
// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.
//
// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.
//

/** An accessor for a camera on the local host. This can be a built-in camera or
 *  a USB-connected camera. This accessor has two modes of operation, "triggered"
 *  and "open loop." In triggered mode, it captures an image whenever a trigger
 *  input is received and produces that image on its output. In open-loop mode,
 *  it captures every image produced by the camera, at the speed of the camera,
 *  and produces on the output a stream of such images. It limits the number of
 *  outputs to maxFrameRate images per second, even if the camera produces more
 *  images than that. You can use the maxFrameRate parameter to avoid overwhelming
 *  your application.
 *
 *  @accessor cameras/Camera
 *  @author Edward A. Lee (eal@eecs.berkeley.edu)
 *  @input trigger A trigger input for triggered mode.
 *   The value is ignored and can be anything.
 *  @output {Object} image A stream of captured images.
 *  @parameter {boolean} triggered If true, use triggered mode.
 *   Otherwise, use open-loop mode. This is a boolean that defaults to false.
 *  @parameter {number} maxFrameRate If not triggered, this limits the output
 *   to the specified number of frames per second. This is a number that defaults to 25.
 *  @parameter {string} camera The name of the camera to use.
 *   A list of available cameras is presented as options.
 *   This is a string that defaults to "default camera",
 *   which uses the system default, if there is one.
 *  @parameter {{width: number, height: number}} viewSize The view size
 *   to use for capture, in pixels. A list of available view sizes for
 *   the selected camara is presented as options. This is a JSON specification
 *   with a "width" and "height" field, as in for example {"width":640, "height":480}.
 *  @version $$Id$$
 */

// Stop extra messages from jslint and jshint.  Note that there should be no
// space between the / and the * and global. See https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*global addInputHandler, error, exports, getParameter, input, output, parameter, removeInputHandler, require, send */
/*jshint globalstrict: true*/
"use strict";

var cameras = require("@accessors-modules/cameras");
var camera;
var handle = null;

/** Create the inputs, outputs, and parameters, and update the parameters for the selected camera. */
exports.setup = function () {
    this.input('trigger');
    this.output('image');
    this.parameter('triggered', {
        'type': 'boolean',
        'value': false
    });
    this.parameter('maxFrameRate', {
        'type': 'number',
        'value': 25
    });
    // NOTE: The following assumes that setup() is reinvoked whenever a parameter
    // value changes, since the camera will change and so will the available options.
    this.parameter('camera', {
        'type': 'string',
        'value': 'default camera'
    });
    this.parameter('viewSize', {
        'type': 'JSON'
    });
    // This is in a try-catch so that this accessor can be instantiated even if the
    // host does not provide a cameras module.
    try {
        this.parameter('camera', {
            'options': cameras.cameras()
        });
        camera = new cameras.Camera(this.getParameter('camera'));
        this.parameter('viewSize', {
            'value': camera.getViewSize(),
            'options': camera.viewSizes()
        });
    } catch (err) {
        error(err);
    }
};

/** Set the view size of the camera, open it, and depending on the triggered mode, either
 *  set up an input handler for the trigger input or set up a handler for the 'image'
 *  event notification from the camera.
 */
exports.initialize = function () {
    camera.setViewSize(this.getParameter('viewSize'));
    camera.open();
    var self = this;
    if (this.getParameter('triggered')) {
        // Request a snapshot.  Note the video stream might not be playing.
        // An event will be generated when a snapshot is available.
        camera.on('snapshot', function (image) {
            self.send('image', image);
        });

        handle = this.addInputHandler('trigger', function () {
            camera.snapshot();
        });
    } else {
        var maxFrameRate = self.getParameter('maxFrameRate');
        var frameInterval = 1000.0/maxFrameRate; // In ms.
        var lastFrameTime = 0;
        camera.on('image', function (image) {
            var currentTime = Date.now();
            if (currentTime - lastFrameTime >= frameInterval) {
                self.send('image', image);
                lastFrameTime = currentTime;
            }
        });
    }
};

/** Remove handlers and close the camera. */
exports.wrapup = function () {
    camera.removeAllListeners('image');
    if (handle !== null) {
        this.removeInputHandler(handle);
    }
    camera.close();
};