Show:

File: app/components/exp-lookit-calibration/component.js

import Ember from 'ember';
import layout from './template';
import ExpFrameBaseComponent from '../exp-frame-base/component';
import FullScreen from '../../mixins/full-screen';
import MediaReload from '../../mixins/media-reload';
import VideoRecord from '../../mixins/video-record';
import ExpandAssets from '../../mixins/expand-assets';
import isColor from '../../utils/is-color';
import { audioAssetOptions, imageAssetOptions, videoAssetOptions } from '../../mixins/expand-assets';

let {
    $
} = Ember;

/**
 * @module exp-player
 * @submodule frames
 */

/**
*
* Frame to do calibration for looking direction. Shows a small video/image in a sequence
* of locations so you'll have video of the child looking to those locations at known times.
*
* The attention-grabber can be either a small video or an image (see examples below of each).
* Images can be animated (spinning or bouncing).
*
* Generally you will want to have video of this frame. You can set doRecording to true to
* make a video clip just for this frame, or you can use session-level recording (set
* startSessionRecording to true on this or a previous frame). If either type of recording
* is starting on this frame, it waits until recording starts to display the first calibration
* segment.
*
* It can be displayed at center, left, or right of the screen. You can specify the sequence
* of positions or use the default ['center', 'left', 'right', 'center']. Each time it moves,
* the video (if any) and audio restart, and an event is recorded with the location and time (and time
* relative to any video recording) of the segment start.
*
* For details about specifying media locations, please see
* https://lookit.readthedocs.io/en/develop/researchers-prep-stimuli.html?highlight=basedir#directory-structure
*
* This frame is displayed fullscreen, to match the frames you will likely want to compare
* looking behavior on. If the participant leaves fullscreen, that will be
* recorded as an event, and a large "return to fullscreen" button will be displayed. Don't
* use video coding from any intervals where the participant isn't in fullscreen mode - the
* position of the attention-grabbers won't be as expected.
*
* If the frame before it is not fullscreen, that frame
* needs to include a manual "next" button so that there's a user interaction
* event to trigger fullscreen mode. (Browsers don't allow us to switch to FS
* without a user event.)
*
* Example usage:

```json

    "calibration-with-image": {
        "kind": "exp-lookit-calibration",
        "baseDir": "https://www.mit.edu/~kimscott/placeholderstimuli/",
        "audioTypes": [
            "ogg",
            "mp3"
        ],
        "videoTypes": [
            "webm",
            "mp4"
        ],
        "calibrationImage": "peekaboo_remy.jpg",
        "calibrationLength": 3000,
        "calibrationPositions": [
            "center",
            "left",
            "right"
        ],
        "calibrationAudio": "chimes",
        "calibrationImageAnimation": "spin"
    },

    "calibration-with-video": {
        "kind": "exp-lookit-calibration",
        "baseDir": "https://www.mit.edu/~kimscott/placeholderstimuli/",
        "audioTypes": [
            "ogg",
            "mp3"
        ],
        "videoTypes": [
            "webm",
            "mp4"
        ],
        "calibrationLength": 3000,
        "calibrationPositions": [
            "center",
            "left",
            "right"
        ],
        "calibrationAudio": "chimes",
        "calibrationVideo": "attentiongrabber"
    },

* ```
* @class Exp-lookit-calibration
* @extends Exp-frame-base
* @uses Full-screen
* @uses Media-reload
* @uses Video-record
* @uses Expand-assets
*/

export default ExpFrameBaseComponent.extend(FullScreen, MediaReload, VideoRecord, ExpandAssets, {
    layout: layout,
    type: 'exp-lookit-calibration',

    displayFullscreen: true, // force fullscreen for all uses of this component
    fullScreenElementId: 'experiment-player',
    fsButtonID: 'fsButton',

    assetsToExpand: {
        'audio': [
            'calibrationAudio',
        ],
        'video': [
            'calibrationVideo'
        ],
        'image': [
            'calibrationImage'
        ]
    },

    // Override setting in VideoRecord mixin - only use camera if doing recording
    doUseCamera: Ember.computed.alias('doRecording'),
    startRecordingAutomatically: Ember.computed.alias('doRecording'),

    calTimer: null, // reference to timer counting how long calibration segment has played
    retryCalibrationAudio: false, // see exp-lookit-video regarding this workaround

    frameSchemaProperties: {

        /**
        Whether to do any video recording during this frame. Default true. Set to false for e.g. last frame where just doing an announcement.
        @property {Boolean} doRecording
        @default true
        */
        doRecording: {
            type: 'boolean',
            description: 'Whether to do video recording',
            default: true
        },
        /**
         * Color of background. See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
         * for acceptable syntax: can use color names ('blue', 'red', 'green', etc.), or
         * rgb hex values (e.g. '#800080' - include the '#')
         *
         * @property {String} backgroundColor
         * @default 'black'
         */
        backgroundColor: {
            type: 'string',
            description: 'Color of background',
            default: 'white'
        },
        /**
         * Length of single calibration segment in ms.
         *
         * @property {Number} calibrationLength
         * @default 3000
         */
        calibrationLength: {
            type: 'number',
            description: 'length of single calibration segment in ms',
            default: 3000
        },
        /**
         * Ordered list of positions to show calibration segment in. Options are
         * "center", "left", "right". Ignored if calibrationLength is 0.
         *
         * @property {Array} calibrationPositions
         * @default ["center", "left", "right", "center"]
         */
        calibrationPositions: {
            type: 'array',
            description: 'Ordered list of positions to show calibration',
            items: {
                type: 'string',
                enum: ['center', 'left', 'right']
            },
            default: ['center', 'left', 'right', 'center']
        },
        /**
         * Audio to play when the attention-grabber is placed at each location (will be
         * played once from the start, but cut off if it's longer than calibrationLength).
         *
         * This can either be an array of `{src: 'url', type: 'MIMEtype'}` objects for
         * calibration audio, or just a string to use the full URLs based on `baseDir`.
         *
         * @property {Object[]} calibrationAudio
         * @default []
         */
        calibrationAudio: {
            anyOf: audioAssetOptions,
            description: 'list of objects specifying audio src and type for calibration audio',
            default: []
        },
        /**
         * Calibration video (played from start at each calibration position). Supply
         * either a calibration video or calibration image, not both.
         *
         * This can be either an array of {src: 'url', type: 'MIMEtype'} objects or
         * just a string like `attentiongrabber` to rely on the `baseDir` and `videoTypes`
         * to generate full paths.
         *
         * @property {Object[]} calibrationVideo
         * @default []
         */
        calibrationVideo: {
            anyOf: videoAssetOptions,
            description: 'list of objects specifying video src and type for calibration audio',
            default: []
        },
        /**
         * Image to use for calibration - will be placed at each location. Supply
         * either a calibration video or calibration image, not both.
         *
         * This can be either a full URL or just the filename (e.g. "star.png") to
         * use the full path based on `baseDir` (e.g. `baseDir/img/star.png`).
         *
         * @property {String} calibrationImage
         * @default []
         */
        calibrationImage: {
            anyOf: imageAssetOptions,
            description: 'Image to use for calibration',
            default: ''
        },

        /**
         * Which animation to use for the calibration image. Options are 'bounce', 'spin',
         * or '' (empty to not animate).
         *
         * @property {String} calibrationImageAnimation
         * @default []
         */
        calibrationImageAnimation: {
            type: 'string',
            enum: ['bounce', 'spin', ''],
            description: 'Which animation to use for the calibration image',
            default: 'spin'
        }
    },

    meta: {
        data: {
            type: 'object',
            properties: {
                videoId: {
                    type: 'string'
                }
            }
        }
    },

    finish() { // Move to next frame altogether
        // Call this something separate from next because stopRecorder promise needs
        // to call next AFTER recording is stopped and we don't want this to have
        // already been destroyed at that point.
        window.clearInterval(this.get('calTimer'));
        var _this = this;
        if (this.get('doRecording')) {
            this.stopRecorder().then(() => {
                _this.set('stoppedRecording', true);
                _this.send('next');
                return;
            }, () => {
                _this.send('next');
                return;
            });
        } else {
            _this.send('next');
        }
    },

    startCalibration() {
        var _this = this;

        // First check whether any calibration video provided. If not, skip.
        if (!this.get('calibrationLength')) {
            this.finish();
        }

        var calAudio = $('#player-calibration-audio')[0];
        $('.calibration-stimulus').show();

        // Show the calibration segment at center, left, right, center, each
        // time recording an event and playing the calibration audio.
        var doCalibrationSegments = function(calList, lastLoc) {
            if (calList.length === 0) {
                _this.finish();
            } else {
                var thisLoc = calList.shift();
                /**
                 * Start of EACH calibration segment
                 *
                 * @event startCalibration
                 * @param {String} location location of calibration ball, relative to child: 'left', 'right', or 'center'
                 */
                _this.send('setTimeEvent', 'startCalibration',
                    {location: thisLoc});


                _this.set('retryCalibrationAudio', true);
                calAudio.pause();
                calAudio.currentTime = 0;
                calAudio.play().then(() => {}, () => {
                    calAudio.play();
                });

                $('.calibration-stimulus').removeClass(lastLoc);
                $('.calibration-stimulus').addClass(thisLoc);
                if (_this.get('hasVideo')) {
                    var calVideo = $('#player-calibration-video')[0];
                    calVideo.pause();
                    calVideo.currentTime = 0;
                    calVideo.play();
                }


                _this.set('calTimer', window.setTimeout(function() {
                    _this.set('retryCalibrationAudio', false);
                    doCalibrationSegments(calList, thisLoc);
                }, _this.get('calibrationLength')));
            }
        };

        doCalibrationSegments(this.get('calibrationPositions').slice(), '');

    },

    reloadObserver: Ember.observer('reloadingMedia', function() {
        console.log('reloadObserver');
        if (!this.get('reloadingMedia')) {  // done with most recent reload
            if (this.get('retryCalibrationAudio')) {
                $('#player-calibration-audio')[0].play();
            }
        }
    }),

    onRecordingStarted() {
        this.startCalibration();
    },

    onSessionRecordingStarted() {
        $('#waitForVideo').hide();
        this.startCalibration();
    },

    didInsertElement() {
        this._super(...arguments);
        this.set('hasVideo', this.get('calibrationVideo').length > 0);
        $('.calibration-stimulus').hide();

        // Apply background colors
        if (isColor(this.get('backgroundColor'))) {
            $('div.exp-lookit-calibration').css('background-color', this.get('backgroundColor'));
        } else {
            console.warn('Invalid background color provided; not applying.');
        }

        // Apply calibration animation class
        if (this.get('calibrationImage')) {
            $('#calibration-image').addClass(this.get('calibrationImageAnimation'));
        }
        if (!(this.get('doRecording') || this.get('startSessionRecording'))) {
            this.startCalibration();
        }
    },

    willDestroyElement() {
        window.clearInterval(this.get('calTimer'));
        this._super(...arguments);
    }
});