- import Ember from 'ember';
- import { observer } from '@ember/object';
- import VideoRecorder from '../services/video-recorder';
- import { colorSpecToRgbaArray, isColor } from '../utils/is-color';
- import { expFormat } from '../helpers/exp-format';
-
- let {
- $
- } = Ember;
-
- /**
- * @module exp-player
- * @submodule mixins
- */
-
- /**
- *
- * Reference for DEVELOPERS of new frames only!
- *
- * A mixin that can be used to add basic support for video recording across frames
- *
- * By default, the recorder will be installed when this frame loads, but recording
- * will not start automatically. To override either of these settings, set
- * the properties `doUseCamera` and/or `startRecordingAutomatically` in the consuming
- * frame.
- *
- * You will also need to set `recorderElement` if the recorder is to be housed other than
- * in an element identified by the ID `recorder`.
- *
- * The properties `recorder`, `videoList`, `stoppedRecording`, `recorderReady`, and
- * `videoId` become available to the consuming frame. The recorder object has fields
- * that give information about its state: `hasWebCam`, 'hasCamAccess`, `recording`,
- * `connected`, and `micChecked` - for details, see services/video-recorder.js. These
- * can be accessed from the consuming frame as e.g. `this.get('recorder').get('hasWebCam')`.
- *
- * If starting recording automatically, the function `onRecordingStarted` will be called
- * once recording begins. If you want to do other things at this point, like proceeding
- * to a test trial, you can add this hook in your frame.
- *
- * See 'methods' for the functions you can use on a frame that extends VideoRecord.
- *
- * Events recorded in a frame that extends VideoRecord will automatically have additional
- * fields videoId (video filename), pipeId (temporary filename initially assigned by
- * the recording service),
- * and streamTime (when in the video they happened, in s).
- *
- * Setting up the camera is handled in didInsertElement, and making sure recording is
- * stopped is handled in willDestroyElement (Ember hooks that fire during the component
- * lifecycle). It is very important (in general, but especially when using this mixin)
- * that you call `this._super(...arguments);` in any functions where your frame overrides
- * hooks like this, so that the mixin's functions get called too!
- *
- *
- * @class Video-record
- */
-
- /**
- * When recorder detects a change in camera access
- *
- * @event hasCamAccess
- * @param {Boolean} hasCamAccess
- */
-
- /**
- * When recorder detects a change in video stream connection status
- *
- * @event videoStreamConnection
- * @param {String} status status of video stream connection, e.g.
- * 'NetConnection.Connect.Success' if successful
- */
-
- /**
- * When pausing study, immediately before request to pause webcam recording
- *
- * @event pauseVideo
- */
-
- /**
- * When unpausing study, immediately before request to resume webcam recording
- *
- * @event unpauseVideo
- */
-
- /**
- * Just before stopping webcam video capture
- *
- * @event stoppingCapture
- */
-
- export default Ember.Mixin.create({
-
- /**
- * The recorder object, accessible to the consuming frame. Includes properties
- * recorder.nWebcams, recorder.hasCamAccess, recorder.micChecked, recorder.connected.
- * @property {VideoRecorder} recorder
- * @private
- */
- recorder: null,
-
- /**
- * A list of all video IDs used in this mixin (a new one is created for each recording).
- * Accessible to consuming frame.
- * @property {List} videoList
- * @private
- */
- videoList: null,
-
- /**
- * Whether recording is stopped already, meaning it doesn't need to be re-stopped when
- * destroying frame. This should be set to true by the consuming frame when video is
- * stopped.
- * @property {Boolean} stoppedRecording
- * @private
- */
- stoppedRecording: false,
-
- /**
- * JQuery string to identify the recorder element.
- * @property {String} recorderElement
- * @default '#recorder'
- * @private
- */
- recorderElement: '#recorder',
-
- /**
- * Whether recorder has been set up yet. Automatically set when doing setup.
- * Accessible to consuming frame.
- * @property {Boolean} recorderReady
- * @private
- */
- recorderReady: false,
-
- /**
- * Maximum recording length in seconds. Can be overridden by consuming frame.
- * @property {Number} maxRecordingLength
- * @default 7200
- */
- maxRecordingLength: 7200,
-
- /**
- * Maximum time allowed for video upload before proceeding, in seconds.
- * Can be overridden by researcher, based on tradeoff between making families wait and
- * losing data.
- * @property {Number} maxUploadSeconds
- * @default 5
- */
- maxUploadSeconds: 5,
-
- /**
- * Whether to autosave recordings. Can be overridden by consuming frame.
- * TODO: eventually use this to set up non-recording option for previewing
- * @property {Number} autosave
- * @default 1
- * @private
- */
- autosave: 1,
-
- /**
- * Whether to do audio-only (vs also video) recording. Can be overridden by consuming frame.
- * @property {Number} audioOnly
- * @default 0
- */
- audioOnly: 0,
-
- /**
- * Whether to use the camera in this frame. Consuming frame should set this property
- * to override if needed.
- * @property {Boolean} doUseCamera
- * @default true
- */
- doUseCamera: true,
-
- /**
- * Whether to start recording ASAP (only applies if doUseCamera). Consuming frame
- * should set to override if needed.
- * @property {Boolean} startRecordingAutomatically
- * @default false
- */
- startRecordingAutomatically: false,
-
- /**
- * A video ID to use for the current recording. Format is
- * `videoStream_<experimentId>_<frameId>_<sessionId>_timestampMS_RRR`
- * where RRR are random numeric digits.
- *
- * @property {String} videoId
- * @private
- */
- videoId: '',
-
- /**
- * Whether to initially show a message saying to wait until recording starts, covering the entire frame.
- * This prevents participants from seeing any stimuli before recording begins. Only used if recording is being
- * started immediately.
- * @property {Boolean} showWaitForRecordingMessage
- * @default true
- */
- showWaitForRecordingMessage: true,
-
- /**
- * [Only used if showWaitForRecordingMessage is true] Text to display while waiting for recording to begin.
- * @property {Boolean} waitForRecordingMessage
- * @default 'Please wait... <br><br> starting webcam recording'
- */
- waitForRecordingMessage: 'Please wait... <br><br> starting webcam recording',
-
- /**
- * [Only used if showWaitForRecordingMessage is true] Background color of screen while waiting for recording to
- * begin. See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
- * for acceptable syntax: can use either color names ('blue', 'red', 'green', etc.), or
- * rgb hex values (e.g. '#800080' - include the '#'). The text on top of this will be either black or white
- * depending on which will have higher contrast.
- * @property {Boolean} waitForRecordingMessageColor
- * @default 'white'
- */
- waitForRecordingMessageColor: 'white',
-
- /**
- * Whether to stop media and hide stimuli with a message saying to wait for video upload when stopping recording.
- * Do NOT set this to true if end of recording does not correspond to end of the frame (e.g. during consent or
- * observation frames) since it will hide everything upon stopping the recording!
- * @property {Boolean} showWaitForUploadMessage
- * @default true
- */
- showWaitForUploadMessage: false,
-
- /**
- * [Only used if showWaitForUploadMessage is true] Text to display while waiting for recording to begin.
- * @property {Boolean} waitForUploadMessage
- * @default 'Please wait... <br><br> uploading video'
- */
- waitForUploadMessage: 'Please wait... <br><br> uploading video',
-
- /**
- * [Only used if showWaitForUploadMessage is true] Background color of screen while waiting for recording to
- * upload. See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
- * for acceptable syntax: can use either color names ('blue', 'red', 'green', etc.), or
- * rgb hex values (e.g. '#800080' - include the '#'). The text on top of this will be either black or white
- * depending on which will have higher contrast.
- * @property {String} waitForUploadMessageColor
- * @default 'white'
- */
- waitForUploadMessageColor: 'white',
-
- /**
- * [Only used if showWaitForUploadMessage and/or showWaitForRecordingMessage are true] Image to display along with
- * any wait-for-recording or wait-for-upload message. Either waitForWebcamImage or waitForWebcamVideo can be
- * specified. This can be either a full URL ('https://...') or just a filename, which will be assumed to be
- * inside ``baseDir/img/`` if this frame otherwise supports use of ``baseDir``.
- * @property {String} waitForWebcamImage
- * @default ''
- */
- waitForWebcamImage: '',
-
- /**
- * [Only used if showWaitForUploadMessage and/or showWaitForRecordingMessage are true] Video to display along with
- * any wait-for-recording or wait-for-upload message (looping). Either waitForWebcamImage or waitForWebcamVideo can be
- * specified. This can be either an array of ``{'src': 'https://...', 'type': '...'}`` objects (e.g. providing both
- * webm and mp4 versions at specified URLS) or a single string relative to ``baseDir/<EXT>/`` if this frame otherwise
- * supports use of ``baseDir``.
- * @property {String} waitForWebcamVideo
- * @default ''
- */
- waitForWebcamVideo: '',
-
-
- _generateVideoId() {
- return [
- 'videoStream',
- this.get('experiment.id'),
- this.get('id'), // parser enforces that id is composed of a-z, A-Z, -, ., [space]
- this.get('session.id'),
- +Date.now(), // Timestamp in ms
- Math.floor(Math.random() * 1000)
- ].join('_');
- },
-
- /**
- * Extend any base time event capture with information about the recorded video
- * @method makeTimeEvent
- * @param eventName
- * @param extra
- * @return {Object} Event data object
- */
- makeTimeEvent(eventName, extra) {
- // All frames using this mixin will add streamTime to every server event
- let base = this._super(eventName, extra);
- Ember.assign(base, {
- streamTime: this.get('recorder') ? this.get('recorder').getTime() : null
- });
- return base;
- },
-
- /**
- * Set up a video recorder instance
- * @method setupRecorder
- * @param {Node} element A DOM node representing where to mount the recorder
- * @return {Promise} A promise representing the result of installing the recorder
- */
- setupRecorder(element) {
- const videoId = this._generateVideoId();
- this.set('videoId', videoId);
- const recorder = new VideoRecorder({element: element});
- const pipeLoc = Ember.getOwner(this).resolveRegistration('config:environment').pipeLoc;
- const pipeEnv = Ember.getOwner(this).resolveRegistration('config:environment').pipeEnv;
- const installPromise = recorder.install(this.get('videoId'), pipeLoc, pipeEnv,
- this.get('maxRecordingLength'), this.get('autosave'), this.get('audioOnly'));
-
- // Track specific events for all frames that use VideoRecorder
- var _this = this;
- recorder.on('onCamAccess', (recId, hasAccess) => { // eslint-disable-line no-unused-vars
- if (!(_this.get('isDestroyed') || _this.get('isDestroying'))) {
- _this.send('setTimeEvent', 'recorder.hasCamAccess', {
- hasCamAccess: hasAccess
- });
- }
- });
- recorder.on('onConnectionStatus', (recId, status) => { // eslint-disable-line no-unused-vars
- if (!(_this.get('isDestroyed') || _this.get('isDestroying'))) {
- _this.send('setTimeEvent', 'videoStreamConnection', {
- status: status
- });
- }
- });
- this.set('recorder', recorder);
- this.send('setTimeEvent', 'setupVideoRecorder', {
- videoId: videoId
- });
- return installPromise;
- },
-
- /**
- * Start recording
- * @method startRecorder
- * @return Promise Resolves when recording has started
- */
- startRecorder() {
- const recorder = this.get('recorder');
- if (recorder) {
- return recorder.record().then(() => {
- this.send('setTimeEvent', 'startRecording', {
- pipeId: recorder.get('pipeVideoName')
- });
- if (this.get('videoList') == null) {
- this.set('videoList', [this.get('videoId')]);
- } else {
- this.set('videoList', this.get('videoList').concat([this.get('videoId')]));
- }
- });
- } else {
- return Ember.RSVP.resolve();
- }
- },
-
- /**
- * Stop the recording
- * @method stopRecorder
- * @return Promise A promise that resolves when upload is complete
- */
- stopRecorder() {
- const recorder = this.get('recorder');
- if (recorder && recorder.get('recording')) {
- this.send('setTimeEvent', 'stoppingCapture');
- if (this.get('showWaitForUploadMessage')) {
- // TODO: consider adding progress bar
- $( "video audio" ).each(function() {
- this.pause();
- });
-
- let colorSpec = this.get('waitForUploadMessageColor');
- if (!isColor(colorSpec)) {
- console.warn(`Invalid background color waitForRecordingUploadColor (${colorSpec}) provided; using default instead.`);
- colorSpec = 'white';
- }
- let colorSpecRGBA = colorSpecToRgbaArray(colorSpec);
- $('.video-record-mixin-wait-for-video').css('background-color', colorSpec);
- $('.video-record-mixin-wait-for-video-text').css('color', (colorSpecRGBA[0] + colorSpecRGBA[1] + colorSpecRGBA[2] > 128 * 3) ? 'black' : 'white');
- $('.video-record-mixin-wait-for-video-text').html(`${expFormat(this.get('waitForUploadMessage'))}`);
- $('.video-record-mixin-wait-for-video').show();
-
- }
- return recorder.stop(this.get('maxUploadSeconds') * 1000);
- } else {
- return Ember.RSVP.reject(1);
- }
- },
-
- /**
- * Destroy recorder and stop accessing webcam
- * @method destroyRecorder
- */
- destroyRecorder() {
- const recorder = this.get('recorder');
- if (recorder) {
- if (!(this.get('isDestroyed') || this.get('isDestroying'))) {
- this.send('setTimeEvent', 'destroyingRecorder');
- }
- recorder.destroy();
- }
- },
-
- willDestroyElement() {
- var _this = this;
- if (_this.get('recorder')) {
- window.clearTimeout(_this.get('recorder').get('uploadTimeout'));
- if (_this.get('stoppedRecording', true)) {
- _this.destroyRecorder();
- } else {
- _this.stopRecorder().then(() => {
- _this.set('stoppedRecording', true);
- _this.destroyRecorder();
- }, () => {
- _this.destroyRecorder();
- });
- }
- }
- _this._super(...arguments);
- },
-
- didReceiveAttrs() {
- let assets = this.get('assetsToExpand');
- if (assets) {
- if (assets.image) {
- assets.image.push('waitForUploadImage');
- } else {
- assets.image = ['waitForUploadImage'];
- }
- if (assets.video) {
- assets.video.push('waitForUploadVideo');
- } else {
- assets.video = ['waitForUploadVideo'];
- }
- } else {
- this.set('assetsToExpand', {'image': ['waitForUploadImage'], 'video': ['waitForUploadVideo']})
- }
- console.log(this.get('assetsToExpand'));
- this._super(...arguments);
- },
-
- didInsertElement() {
- // Give any active session recorder precedence over individual-frame recording
- if (this.get('sessionRecorder') && this.get('session').get('recordingInProgress')) {
- console.warn('Recording on this frame was specified, but session recording is already active. Not making frame recording.');
- this.set('doUseCamera', false);
- }
-
- if (this.get('doUseCamera')) {
-
- // If showing a wait-for-recording or wait-for-upload message, set it up now.
- if ((this.get('showWaitForRecordingMessage') && this.get('startRecordingAutomatically')) || this.get('showWaitForUploadMessage')) {
- let $waitForVideoCover = $('<div></div>');
- $waitForVideoCover.addClass('video-record-mixin-wait-for-video'); // for easily referencing later to show/hide
-
- // Set the background color of the cover
- let colorSpec = this.get('waitForRecordingMessageColor');
- if (!isColor(colorSpec)) {
- console.warn(`Invalid background color waitForRecordingMessageColor (${colorSpec}) provided; using default instead.`);
- colorSpec = 'white';
- }
- let colorSpecRGBA = colorSpecToRgbaArray(colorSpec);
- $waitForVideoCover.css('background-color', colorSpec);
-
- // Add the image, if any
- if (this.get('waitForUploadImage')) {
- let imageSource = this.get('waitForUploadImage_parsed') ? this.get('waitForUploadImage_parsed') : this.get('waitForUploadImage');
- $waitForVideoCover.append($(`<img src='${imageSource}' class='video-record-mixin-image'>`));
- }
-
- // Add the video, if any
- if (this.get('waitForUploadVideo')) {
- let $videoElement = $('<video loop autoplay="autoplay" class="video-record-mixin-image"></video>');
- let videoSources = this.get('waitForUploadVideo_parsed') ? this.get('waitForUploadVideo_parsed') : this.get('waitForUploadVideo');
- console.log(videoSources);
- console.log(this);
- $.each(videoSources, function (idx, source) {
- $videoElement.append(`<source src=${source.src} type=${source.type}>`);
- });
- $waitForVideoCover.append($videoElement);
- }
-
- // Add the text and set its color so it'll be visible against the background
- let $waitForVideoText = $(`<div>${expFormat(this.get('waitForRecordingMessage'))}</div>`);
- $waitForVideoText.addClass('video-record-mixin-wait-for-video-text');
- $waitForVideoText.css('color', (colorSpecRGBA[0] + colorSpecRGBA[1] + colorSpecRGBA[2] > 128 * 3) ? 'black' : 'white');
- $waitForVideoCover.append($waitForVideoText);
-
- $('div.lookit-frame').append($waitForVideoCover);
-
- if (this.get('showWaitForRecordingMessage') && this.get('startRecordingAutomatically')) {
- $waitForVideoCover.css('display', 'block');
- }
- }
-
- var _this = this;
- this.setupRecorder(this.$(this.get('recorderElement'))).then(() => {
- /**
- * When video recorder has been installed
- *
- * @event recorderReady
- */
- _this.send('setTimeEvent', 'recorderReady');
- _this.set('recorderReady', true);
- _this.whenPossibleToRecordObserver(); // make sure this fires
- });
- }
- this._super(...arguments);
- },
-
- /**
- * Function called when frame recording is started automatically. Override to do
- * frame-specific actions at this point (e.g., beginning a test trial).
- *
- * @method onRecordingStarted
- */
- onRecordingStarted() {
- },
-
- /**
- * Observer that starts recording once recorder is ready.
- * @method whenPossibleToRecordObserver
- */
- whenPossibleToRecordObserver: observer('recorder.hasCamAccess', 'recorderReady', function() {
- if (this.get('doUseCamera') && this.get('startRecordingAutomatically')) {
- var _this = this;
- if (this.get('recorder.hasCamAccess') && this.get('recorderReady')) {
- this.startRecorder().then(() => {
- _this.set('recorderReady', false);
- $('.video-record-mixin-wait-for-video').hide();
- _this.onRecordingStarted();
- });
- }
- }
- }),
-
- /**
- * Hide the recorder from display. Useful if you would like to keep recording without extra UI elements to
- * distract the user.
- * @method hideRecorder
- */
- hideRecorder() {
- $(this.get('recorderElement')).parent().addClass('video-recorder-hidden');
- },
-
- /**
- * Show the recorder to the user. Useful if you want to temporarily show a hidden recorder- eg to let the user fix
- * a problem with video capture settings
- * @method showRecorder
- */
- showRecorder() {
- $(this.get('recorderElement')).parent().removeClass('video-recorder-hidden');
- },
-
- init() {
- this._super(...arguments);
- this.set('videoList', []);
- }
-
- });
-
-