Show:

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

  1. import Ember from 'ember';
  2. import layout from './template';
  3. import ExpFrameBaseComponent from '../exp-frame-base/component';
  4. import FullScreen from '../../mixins/full-screen';
  5. import VideoRecord from '../../mixins/video-record';
  6. import ExpandAssets from '../../mixins/expand-assets';
  7. import isColor, {colorSpecToRgbaArray} from '../../utils/is-color';
  8. import { audioAssetOptions, videoAssetOptions } from '../../mixins/expand-assets';
  9.  
  10. let {
  11. $
  12. } = Ember;
  13.  
  14. /**
  15. * @module exp-player
  16. * @submodule frames
  17. */
  18.  
  19. /**
  20. * Video display frame. This may be used for displaying videos to older children or parents, as well as for
  21. * typical looking measures trials or as brief filler in between test trials.
  22. *
  23. * (Note: this frame replaced the previous exp-lookit-video frame, which is now called
  24. * {{#crossLink "Exp-lookit-composite-video-trial"}}{{/crossLink}}.)
  25. *
  26. * This is very customizable: you can...
  27. * - position the video wherever you want on the screen, including specifying that it should fill the screen (while maintaining aspect ratio)
  28. * - choose the background color
  29. * - optionally specify audio that should play along with the video
  30. * - have the frame proceed automatically (`autoProceed`), or enable a Next button when the user can move on
  31. * - allow parents to press a key to pause the video (and then either restart when they un-pause, or move on to the next frame)
  32. *
  33. * Video (and audio if provided) start as soon as any recording begins, or right away if there is no recording starting.
  34. *
  35. * If the user pauses using the `pauseKey`, or if the user leaves fullscreen mode, the study will be paused.
  36. * While paused, the video/audio are stopped and not displayed, and instead a looping `pauseVideo` and text are displayed.
  37. *
  38. * There are several ways you can specify how long the trial should last. The frame will continue until
  39. * ALL of the following are true:
  40. * - the video has been played all the way through `requireVideoCount` times
  41. * - the audio has been played all the way through `requireAudioCount` times
  42. * - `requiredDuration` seconds have elapsed since beginning the video
  43. *
  44. * You do not need to use all of these - for instance, to play the video one time and then proceed, set
  45. * `requireVideoCount` to 1 and the others to 0. You can also specify whether the audio and video should loop (beyond
  46. * any replaying required to reach the required counts).
  47. *
  48. * This frame is displayed fullscreen; if the frame before it is not, that frame
  49. * needs to include a manual "next" button so that there's a user interaction
  50. * event to trigger fullscreen mode. (Browsers don't allow us to switch to FS
  51. * without a user event.)
  52. *
  53. * Example usage: (Note - this is a bit of an odd example with both audio ('peekaboo') and audio embedded in the video.
  54. * In general you would probably only want one or the other!)
  55.  
  56. ```json
  57. "play-video-twice": {
  58. "kind": "exp-lookit-video",
  59. "audio": {
  60. "loop": false,
  61. "source": "peekaboo"
  62. },
  63. "video": {
  64. "top": 10,
  65. "left": 25,
  66. "loop": true,
  67. "width": 50,
  68. "source": "cropped_apple"
  69. },
  70. "backgroundColor": "white",
  71. "autoProceed": true,
  72. "parentTextBlock": {
  73. "text": "If your child needs a break, just press X to pause!"
  74. },
  75. "requiredDuration": 0,
  76. "requireAudioCount": 0,
  77. "requireVideoCount": 2,
  78. "restartAfterPause": true,
  79. "pauseKey": "x",
  80. "pauseKeyDescription": "X",
  81. "pauseAudio": "pause",
  82. "pauseVideo": "attentiongrabber",
  83. "pauseText": "(You'll have a moment to turn around again.)",
  84. "unpauseAudio": "return_after_pause",
  85. "doRecording": true,
  86. "baseDir": "https://www.mit.edu/~kimscott/placeholderstimuli/",
  87. "audioTypes": [
  88. "ogg",
  89. "mp3"
  90. ],
  91. "videoTypes": [
  92. "webm",
  93. "mp4"
  94. ]
  95. },
  96.  
  97. * ```
  98. * @class Exp-lookit-video
  99. * @extends Exp-frame-base
  100. * @uses Full-screen
  101. * @uses Video-record
  102. * @uses Expand-assets
  103. */
  104.  
  105. export default ExpFrameBaseComponent.extend(FullScreen, VideoRecord, ExpandAssets, {
  106. layout: layout,
  107. type: 'exp-lookit-video',
  108.  
  109. displayFullscreen: true, // force fullscreen for all uses of this component
  110. fsButtonID: 'fsButton',
  111.  
  112. assetsToExpand: {
  113. 'audio': [
  114. 'audio/source',
  115. 'pauseAudio',
  116. 'unpauseAudio'
  117. ],
  118. 'video': [
  119. 'pauseVideo',
  120. 'video/source'
  121. ],
  122. 'image': [
  123. ]
  124. },
  125.  
  126. // Override setting in VideoRecord mixin - only use camera if doing recording
  127. doUseCamera: Ember.computed.alias('doRecording'),
  128. startRecordingAutomatically: Ember.computed.alias('doRecording'),
  129.  
  130. testTimer: null, // reference to timer counting how long video has been playing, if time-based limit
  131.  
  132. testVideoTimesPlayed: 0, // how many times the test video has been played (including current)
  133. testAudioTimesPlayed: 0, // how many times the test audio has been played (including current)
  134. satisfiedDuration: false, // whether we have completed the requiredDuration
  135.  
  136. skip: false,
  137. hasBeenPaused: false,
  138. isPaused: false,
  139. hasParentText: true,
  140. unpausing: false,
  141.  
  142. maximizeVideoArea: false,
  143. _finishing: false,
  144.  
  145. frameSchemaProperties: {
  146. /**
  147. * Object describing the video to show.
  148. *
  149. * @property {Object} video
  150. * @param {String} source The location of the main video to play. This can be either
  151. * an array of {'src': 'https://...', 'type': '...'} objects (e.g. providing both
  152. * webm and mp4 versions at specified URLS) or a single string relative to baseDir/<EXT>/.
  153. * @param {Number} left left margin, as percentage of screen width. If not provided,
  154. * the image is centered horizontally.
  155. * @param {Number} width image width, as percentage of screen width. Note:
  156. * in general only provide one of width and height; the other will be adjusted to
  157. * preserve the video aspect ratio.
  158. * @param {Number} top top margin, as percentage of video area height (i.e. whole screen,
  159. * unless parent text or next button are shown). If not provided,
  160. * the image is centered vertically.
  161. * @param {Number} height image height, as percentage of video area height. Note:
  162. * in general only provide one of width and height; the other will be adjusted to
  163. * preserve the video aspect ratio.
  164. * @param {String} position use 'fill' to fill the screen as much as possible while
  165. * preserving aspect ratio. This overrides left/width/top/height values if given.
  166. * @param {Boolean} loop whether the video should loop, even after any requireTestVideoCount
  167. * is satisfied
  168. */
  169. video: {
  170. type: 'object',
  171. properties: {
  172. 'source': {
  173. anyOf: videoAssetOptions
  174. },
  175. 'left': {
  176. type: 'number'
  177. },
  178. 'width': {
  179. type: 'number'
  180. },
  181. 'top': {
  182. type: 'number'
  183. },
  184. 'height': {
  185. type: 'number'
  186. },
  187. 'position': {
  188. type: 'string',
  189. enum: ['fill', '']
  190. },
  191. 'loop': {
  192. type: 'boolean'
  193. }
  194. }
  195. },
  196. /**
  197. * Object describing the audio file to play along with video (optional)
  198. *
  199. * @property {Object} audio
  200. * @default {'source': '', loop: false}
  201. * @param {String} source Location of the audio file to play.
  202. * This can either be an array of {src: 'url', type: 'MIMEtype'} objects, e.g.
  203. * listing equivalent .mp3 and .ogg files, or can be a single string `filename`
  204. * which will be expanded based on `baseDir` and `audioTypes` values (see `audioTypes`).
  205. * @param {Boolean} loop whether the audio should loop, even after any requireTestAudioCount
  206. * is satisfied
  207. */
  208. audio: {
  209. type: 'object',
  210. description: 'Audio to play along with video',
  211. properties: {
  212. 'source': {
  213. anyOf: audioAssetOptions
  214. },
  215. 'loop': {
  216. type: 'boolean'
  217. }
  218. },
  219. default: {}
  220. },
  221.  
  222. /**
  223. * Whether to proceed automatically when video is complete / requiredDuration is
  224. * achieved, vs. enabling a next button at that point.
  225. * If true, the frame auto-advances after ALL of the following happen
  226. * (a) the requiredDuration (if any) is achieved, counting from the video starting
  227. * (b) the video is played requireVideoCount times
  228. * (c) the audio is played requireAudioCount times
  229. * If false: a next button is displayed. It becomes possible to press 'next'
  230. * only once the conditions above are met.
  231. *
  232. * @property {Boolean} autoProceed
  233. * @default true
  234. */
  235. autoProceed: {
  236. type: 'boolean',
  237. description: 'Whether to proceed automatically after audio (and hide replay/next buttons)',
  238. default: true
  239. },
  240.  
  241. /**
  242. * Color of background. See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
  243. * for acceptable syntax: can use color names ('blue', 'red', 'green', etc.), or
  244. * rgb hex values (e.g. '#800080' - include the '#'). We recommend a light background if you need to
  245. * see children's eyes.
  246. *
  247. * @property {String} backgroundColor
  248. * @default 'white'
  249. */
  250. backgroundColor: {
  251. type: 'string',
  252. description: 'Color of background',
  253. default: 'white'
  254. },
  255.  
  256. /**
  257. Video to show (looping) when trial is paused. As with the main video, this can either be an array of
  258. {'src': 'https://...', 'type': '...'} objects (e.g. providing both webm and mp4 versions at specified URLS)
  259. or a single string relative to baseDir/<EXT>/.
  260. @property {Array} pauseVideo
  261. @param {String} src
  262. @param {String} type
  263. @default []
  264. */
  265. pauseVideo: {
  266. anyOf: videoAssetOptions,
  267. description: 'List of objects specifying video to show while trial is paused, each specifying src and type',
  268. default: []
  269. },
  270.  
  271. /**
  272. Key to pause the trial. Use an empty string, '', to not allow pausing using the keyboard. You can look up the names of keys at
  273. https://keycode.info. Default is the space bar (' ').
  274. @property {string} pauseKey
  275. @default ' '
  276. */
  277. pauseKey: {
  278. type: 'string',
  279. description: 'Key that will pause study (use \'\' to not allow pausing during video)',
  280. default: ' '
  281. },
  282.  
  283. /**
  284. Parent-facing description of the key to pause the study. This is just used to display text
  285. "Press {pauseKeyDescription} to resume" when the study is paused.
  286. @property {string} pauseKeyDescription
  287. @default 'space'
  288. */
  289. pauseKeyDescription: {
  290. type: 'string',
  291. description: 'Parent-facing description of the key to pause the study',
  292. default: 'space'
  293. },
  294.  
  295. /**
  296. Whether to restart this frame upon unpausing, vs moving on to the next frame
  297. @property {Array} restartAfterPause
  298. @default true
  299. */
  300. restartAfterPause: {
  301. type: 'boolean',
  302. description: 'Whether to restart this frame upon unpausing, vs moving on to the next frame',
  303. default: true
  304. },
  305.  
  306. /**
  307. Duration to require before proceeding, if any. Set if you want a time-based limit. E.g., setting requiredDuration to 20 means that the first 20 seconds of the video will be played, with shorter videos looping until they get to 20s. Leave out or set to 0 to play the video through to the end a set number of times instead.
  308. @property {Number} requiredDuration
  309. @default 0
  310. */
  311. requiredDuration: {
  312. type: 'number',
  313. description: 'Minimum trial duration to require (from start of video), in seconds',
  314. default: 0,
  315. minimum: 0
  316. },
  317.  
  318. /**
  319. Number of times to play test video before moving on.
  320. @property {Number} requireVideoCount
  321. @default 1
  322. */
  323. requireVideoCount: {
  324. type: 'number',
  325. description: 'Number of times to play test video',
  326. default: 1
  327. },
  328.  
  329. /**
  330. Number of times to play test audio before moving on.
  331. @property {Number} requireAudioCount
  332. @default 1
  333. */
  334. requireAudioCount: {
  335. type: 'number',
  336. description: 'Number of times to play test audio',
  337. default: 0
  338. },
  339.  
  340. /**
  341. Whether to do any video recording during this frame. Default true. Set to false for e.g. last frame where just doing an announcement.
  342. @property {Boolean} doRecording
  343. @default true
  344. */
  345. doRecording: {
  346. type: 'boolean',
  347. description: 'Whether to do video recording',
  348. default: true
  349. },
  350. /**
  351. * Sources Array of {src: 'url', type: 'MIMEtype'} objects for
  352. * audio played upon pausing study
  353. *
  354. * @property {Object[]} pauseAudio
  355. * @default []
  356. */
  357. pauseAudio: {
  358. anyOf: audioAssetOptions,
  359. description: 'List of objects specifying audio src and type for audio played when pausing study',
  360. default: []
  361. },
  362. /**
  363. * Sources Array of {src: 'url', type: 'MIMEtype'} objects for
  364. * audio played upon unpausing study
  365. *
  366. * @property {Object[]} unpauseAudio
  367. * @default []
  368. */
  369. unpauseAudio: {
  370. anyOf: audioAssetOptions,
  371. description: 'List of objects specifying audio src and type for audio played when unpausing study',
  372. default: []
  373. },
  374. /**
  375. * Text to show under "Study paused / Press space to resume" when study is paused.
  376. * Default: (You'll have a moment to turn around again.)
  377. *
  378. * @property {String} pauseText
  379. * @default []
  380. */
  381. pauseText: {
  382. type: 'string',
  383. description: 'Text to show under Study paused when study is paused.',
  384. default: "(You'll have a moment to turn around again.)"
  385. },
  386. /**
  387. * Text block to display to parent. (Each field is optional)
  388. *
  389. * @property {Object} parentTextBlock
  390. * @param {String} title title to display
  391. * @param {String} text paragraph of text
  392. * @param {Object} css object specifying any css properties
  393. * to apply to this section, and their values - e.g.
  394. * {'color': 'gray', 'font-size': 'large'}
  395. */
  396. parentTextBlock: {
  397. type: 'object',
  398. properties: {
  399. title: {
  400. type: 'string'
  401. },
  402. text: {
  403. type: 'string'
  404. },
  405. css: {
  406. type: 'object',
  407. default: {}
  408. }
  409. },
  410. default: {}
  411. }
  412. },
  413.  
  414. meta: {
  415. data: {
  416. type: 'object',
  417. properties: {
  418. /**
  419. * Source of video shown during this trial. Just stores first URL if multiple formats are offered.
  420. * @attribute videoShown
  421. * @type string
  422. */
  423. videoShown: {
  424. type: 'string',
  425. default: ''
  426. },
  427. /**
  428. * Source of audio played during this trial. Just stores first URL if multiple formats are offered.
  429. * @attribute audioPlayed
  430. * @type string
  431. */
  432. audioPlayed: {
  433. type: 'string',
  434. default: ''
  435. },
  436. videoId: {
  437. type: 'string'
  438. },
  439. /**
  440. * Whether the video was paused at any point during the trial
  441. * @attribute hasBeenPaused
  442. * @type boolean
  443. */
  444. hasBeenPaused: {
  445. type: 'boolean'
  446. }
  447. }
  448. }
  449. },
  450.  
  451. onFullscreen() {
  452. if (this.get('isDestroyed')) {
  453. return;
  454. }
  455. this._super(...arguments);
  456. if (!this.checkFullscreen()) {
  457. if (!this.get('isPaused')) {
  458. this.togglePauseStudy(true);
  459. }
  460. }
  461. },
  462.  
  463. actions: {
  464.  
  465. videoStarted() {
  466. /**
  467. * When video begins playing (recorded each time video starts if played through more than once)
  468. *
  469. * @event videoStarted
  470. */
  471. if (this.get('isDestroying') || this.get('isDestroyed')) {
  472. return;
  473. }
  474.  
  475. this.send('setTimeEvent', 'videoStarted');
  476. if (this.get('testVideoTimesPlayed') === 0) {
  477. window.clearInterval(this.get('testTimer'));
  478. if (this.get('requiredDuration')) {
  479. this.set('testTimer', window.setTimeout(() => {
  480. this.set('satisfiedDuration', true);
  481. if (this.isReadyToFinish()) {
  482. this.readyToFinish();
  483. }
  484. }, this.get('requiredDuration') * 1000));
  485. } else {
  486. this.set('satisfiedDuration', true);
  487. if (this.isReadyToFinish()) {
  488. this.readyToFinish();
  489. }
  490. }
  491. if ($('#player-audio').length) {
  492. $('#player-audio')[0].play();
  493. }
  494. }
  495. },
  496.  
  497. videoStopped() {
  498. if (this.get('isDestroying') || this.get('isDestroyed')) {
  499. return;
  500. }
  501. /**
  502. * When video completes playback (recorded each time if played more than once)
  503. *
  504. * @event videoStopped
  505. */
  506. this.send('setTimeEvent', 'videoStopped');
  507. this.set('testVideoTimesPlayed', this.get('testVideoTimesPlayed') + 1);
  508. if (this.isReadyToFinish()) {
  509. this.readyToFinish();
  510. }
  511. // Restart the video if it's supposed to loop OR if it's supposed to play another time
  512. if ($('#player-video').length && (this.get('video.loop') || (this.get('testVideoTimesPlayed') < this.get('requireVideoCount')))) {
  513. $('#player-video')[0].currentTime = 0;
  514. $('#player-video')[0].play();
  515. }
  516. },
  517.  
  518. audioStarted() {
  519. if (this.get('isDestroying') || this.get('isDestroyed')) {
  520. return;
  521. }
  522. /**
  523. * When audio begins playing (recorded each time video starts if played through more than once)
  524. *
  525. * @event audioStarted
  526. */
  527. this.send('setTimeEvent', 'audioStarted');
  528. },
  529.  
  530. audioStopped() {
  531. if (this.get('isDestroying') || this.get('isDestroyed')) {
  532. return;
  533. }
  534. /**
  535. * When audio completes playback (recorded each time if played more than once)
  536. *
  537. * @event audioStopped
  538. */
  539. this.send('setTimeEvent', 'audioStopped');
  540. this.set('testAudioTimesPlayed', this.get('testAudioTimesPlayed') + 1);
  541. if (this.isReadyToFinish()) { // in case this was the last criterion for being done
  542. this.readyToFinish();
  543. }
  544. // Restart the video if it's supposed to loop OR if it's supposed to play another time
  545. if ($('#player-audio').length && (this.get('audio.loop') || (this.get('testAudioTimesPlayed') < this.get('requireAudioCount')))) {
  546. $('#player-audio')[0].currentTime = 0;
  547. $('#player-audio')[0].play();
  548. }
  549. },
  550.  
  551. finish() {
  552. // Call this something separate from next because stopRecorder promise needs
  553. // to call next AFTER recording is stopped and we don't want this to have
  554. // already been destroyed at that point.
  555.  
  556. // Pause audio/video so we don't trigger started/stopped handlers while destroying
  557. $('audio, video').each(function() {
  558. this.pause();
  559. });
  560. /**
  561. * When trial is complete and begins cleanup (may then wait for video upload)
  562. *
  563. * @event trialCompleted
  564. */
  565. this.send('setTimeEvent', 'trialCompleted');
  566. window.clearInterval(this.get('testTimer'));
  567. this.set('testVideoTimesPlayed', 0);
  568. this.set('testAudioTimesPlayed', 0);
  569. this.set('satisfiedDuration', false);
  570. var _this = this;
  571. if (!this.get('_finishing')) {
  572. this.set('_finishing', true);
  573. if (this.get('doRecording')) {
  574. this.set('doingTest', false);
  575. this.stopRecorder().then(() => {
  576. _this.set('stoppedRecording', true);
  577. _this.send('next');
  578. }, () => {
  579. _this.send('next');
  580. });
  581. } else {
  582. _this.send('next');
  583. }
  584. }
  585. },
  586.  
  587. unpauseStudy() {
  588. this.set('unpausing', false);
  589. /**
  590. * When trial is unpaused (actually proceeding to beginning or next frame)
  591. *
  592. * @event unpauseTrial
  593. */
  594. this.send('setTimeEvent', 'unpauseTrial');
  595. if (this.get('restartAfterPause')) {
  596. this.isReadyToFinish(); // enable Next button if appropriate
  597. this.startVideo();
  598. } else {
  599. this.send('finish');
  600. }
  601. }
  602. },
  603.  
  604. isReadyToFinish() {
  605. let ready = (this.get('testVideoTimesPlayed') >= this.get('requireVideoCount')) &&
  606. (this.get('testAudioTimesPlayed') >= this.get('requireAudioCount')) &&
  607. (this.get('satisfiedDuration'));
  608. $('#nextbutton').prop('disabled', !ready);
  609. return ready;
  610. },
  611.  
  612. readyToFinish() {
  613. if (this.get('autoProceed')) {
  614. this.send('finish');
  615. } else {
  616. /**
  617. * When all requirements for this frame are completed and next button is enabled (only recorded if
  618. * autoProceed is false)
  619. *
  620. * @event nextButtonEnabled
  621. */
  622. this.send('setTimeEvent', 'nextButtonEnabled');
  623. $('#nextbutton').prop('disabled', false);
  624. }
  625. },
  626.  
  627. startVideo() {
  628. // Set doingTest to true, which displays test video in template; once that actually starts
  629. // it will trigger the videoStarted action
  630. this.set('doingTest', true);
  631. },
  632.  
  633. togglePauseStudy(pause) { // only called in FS mode
  634. try {
  635. this.set('hasBeenPaused', true);
  636. } catch (_) {
  637. return;
  638. }
  639. var wasPaused = this.get('isPaused');
  640.  
  641. // Currently paused: restart
  642. if (!pause && wasPaused) {
  643. this.set('unpausing', true);
  644. this.set('isPaused', false);
  645. // Start the unpausing audio.
  646. if (this.get('unpauseAudio_parsed', []).length) {
  647. $('#unpause-audio')[0].currentTime = 0;
  648. $('#unpause-audio')[0].play().catch(() => {
  649. this.send('unpauseStudy');
  650. });
  651. } else {
  652. this.send('unpauseStudy');
  653. }
  654.  
  655. } else if (pause || !wasPaused) { // Not currently paused: pause
  656. window.clearInterval(this.get('testTimer'));
  657.  
  658. if ($('#unpause-audio').length) {
  659. $('#unpause-audio')[0].pause();
  660. }
  661. this.set('testVideoTimesPlayed', 0);
  662. this.set('testAudioTimesPlayed', 0);
  663. this.set('satisfiedDuration', false);
  664. $('#nextbutton').prop('disabled', true); // disable Next while paused
  665. /**
  666. * When trial is paused
  667. *
  668. * @event pauseTrial
  669. */
  670. this.send('setTimeEvent', 'pauseTrial');
  671. this.set('doingTest', false);
  672. this.set('isPaused', true);
  673. if ($('#pause-audio').length && $('#pause-audio')[0].paused) {
  674. $('#pause-audio')[0].currentTime = 0;
  675. $('#pause-audio')[0].play();
  676. }
  677. }
  678. },
  679.  
  680. didInsertElement() {
  681. this._super(...arguments);
  682.  
  683.  
  684. $(document).on('keyup.pauser', (e) => {
  685. if (this.checkFullscreen()) {
  686. if (this.get('pauseKey') && e.key === this.get('pauseKey')) {
  687. this.togglePauseStudy();
  688. } else if (!this.get('pauseKey') && e.key === ' ' && this.get('isPaused')) {
  689. this.togglePauseStudy();
  690. }
  691. }
  692. });
  693. $('#nextbutton').prop('disabled', true);
  694.  
  695. // Store which video actually gets played for convenience when analyzing data
  696. let video = this.get('video_parsed', {});
  697. if (video.source && video.source.length) {
  698. this.set('videoShown', video.source[0].src);
  699. } else {
  700. this.set('videoShown', '');
  701. }
  702.  
  703. // Store which audio actually gets played for convenience when analyzing data
  704. let audio = this.get('audio_parsed', {});
  705. if (audio.source && audio.source.length) {
  706. this.set('audioPlayed', audio.source[0].src);
  707. } else {
  708. this.set('audioPlayed', '');
  709. }
  710.  
  711. // Apply user-provided CSS to parent text block
  712. let hasParentText = Object.keys(this.get('parentTextBlock')).length;
  713. this.set('hasParentText', hasParentText);
  714. if (hasParentText) {
  715. var parentTextBlock = this.get('parentTextBlock') || {};
  716. var css = parentTextBlock.css || {};
  717. $('#parenttext').css(css);
  718. }
  719. this.set('maximizeVideoArea', this.get('autoProceed') && !hasParentText);
  720.  
  721. // Apply user-provided CSS to video
  722. if (!video.position) {
  723. $('#test-video').css({
  724. 'left': `${video.left}%`,
  725. 'width': `${video.width}%`,
  726. 'top': `${video.top}%`,
  727. 'height': `${video.height}%`
  728. });
  729. }
  730.  
  731. // Apply background color
  732. let colorSpec = this.get('backgroundColor');
  733. if (isColor(colorSpec)) {
  734. $('div.story-image-container, div#image-area, div.exp-lookit-video').css('background-color', this.get('backgroundColor'));
  735. // Set text color so it'll be visible (black or white depending on how dark background is). Use style
  736. // so this applies whenever pause text actually appears.
  737. let colorSpecRGBA = colorSpecToRgbaArray(colorSpec);
  738. let textColor = (colorSpecRGBA[0] + colorSpecRGBA[1] + colorSpecRGBA[2] > 128 * 3) ? 'black' : 'white';
  739. $(`<style>div.exp-lookit-video p#waitForVideo, div.exp-lookit-video p.pause-instructions { color: ${textColor}; }</style>`).appendTo('div.exp-lookit-video');
  740. } else {
  741. console.warn(`Invalid backgroundColor (${colorSpec}) provided; using default instead.`);
  742. }
  743.  
  744. if (!this.get('doRecording') && !this.get('startSessionRecording') && !this.get('isPaused')) {
  745. this.startVideo();
  746. }
  747. },
  748.  
  749. willDestroyElement() { // remove event handler
  750. $(document).off('keyup.pauser');
  751. window.clearInterval(this.get('testTimer'));
  752. this._super(...arguments);
  753. },
  754.  
  755. // Hook for starting recording
  756. onRecordingStarted() {
  757. if (!this.get('isPaused')) {
  758. this.startVideo();
  759. }
  760. },
  761.  
  762. // Hook for starting session recorder
  763. onSessionRecordingStarted() {
  764. if (!this.get('isPaused')) {
  765. this.startVideo();
  766. }
  767. $('#waitForVideo').hide();
  768. }
  769. });
  770.