@lookit/record 3.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -132,6 +132,11 @@ declare const info$3: {
132
132
  readonly default: false;
133
133
  };
134
134
  };
135
+ readonly data: {
136
+ readonly chs_type: {
137
+ readonly type: ParameterType.STRING;
138
+ };
139
+ };
135
140
  };
136
141
  type Info$3 = typeof info$3;
137
142
  /** The video consent plugin. */
@@ -269,9 +274,19 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
269
274
  readonly default: false;
270
275
  };
271
276
  };
277
+ readonly data: {
278
+ readonly chs_type: {
279
+ readonly type: ParameterType.STRING;
280
+ };
281
+ };
272
282
  };
273
283
  private readonly recorder;
274
284
  private readonly video_container_id;
285
+ private readonly msg_container_id;
286
+ private uploadingMsg;
287
+ private startingMsg;
288
+ private recordingMsg;
289
+ private notRecordingMsg;
275
290
  /**
276
291
  * Instantiate video consent plugin.
277
292
  *
@@ -305,13 +320,30 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
305
320
  */
306
321
  private playbackFeed;
307
322
  /**
308
- * Put back the webcam feed once the video recording has ended. This is used
309
- * with the "ended" Event.
323
+ * Get message container that appears alongside the video element.
324
+ *
325
+ * @param display - HTML element for experiment.
326
+ * @returns Message container div element.
327
+ */
328
+ private getMessageContainer;
329
+ /**
330
+ * Add HTML-formatted message alongside the video feed, e.g. for waiting
331
+ * periods during webcam feed transitions (starting, stopping/uploading). This
332
+ * will also replace an existing message with the new one. To clear any
333
+ * existing messages, pass an empty string.
334
+ *
335
+ * @param display - HTML element for experiment.
336
+ * @param message - HTML content for message div.
337
+ */
338
+ private addMessage;
339
+ /**
340
+ * Put back the webcam feed once the video recording playback has ended. This
341
+ * is used with the "ended" Event.
310
342
  *
311
343
  * @param display - JsPsych display HTML element.
312
344
  * @returns Event function
313
345
  */
314
- private onEnded;
346
+ private onPlaybackEnded;
315
347
  /**
316
348
  * Retrieve button element from DOM.
317
349
  *
@@ -371,7 +403,9 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
371
403
 
372
404
  declare const info$2: {
373
405
  readonly name: "start-record-plugin";
406
+ readonly version: string;
374
407
  readonly parameters: {};
408
+ readonly data: {};
375
409
  };
376
410
  type Info$2 = typeof info$2;
377
411
  /** Start recording. Used by researchers who want to record across trials. */
@@ -379,7 +413,9 @@ declare class StartRecordPlugin implements JsPsychPlugin<Info$2> {
379
413
  private jsPsych;
380
414
  static readonly info: {
381
415
  readonly name: "start-record-plugin";
416
+ readonly version: string;
382
417
  readonly parameters: {};
418
+ readonly data: {};
383
419
  };
384
420
  private recorder;
385
421
  /**
@@ -394,12 +430,32 @@ declare class StartRecordPlugin implements JsPsychPlugin<Info$2> {
394
430
 
395
431
  declare const info$1: {
396
432
  readonly name: "stop-record-plugin";
433
+ readonly version: string;
397
434
  readonly parameters: {
435
+ /**
436
+ * This string can contain HTML markup. Any content provided will be
437
+ * displayed while the recording is uploading. If null (the default), then
438
+ * the default 'uploading video, please wait' (or appropriate translation
439
+ * based on 'locale') will be displayed. Use a blank string for no
440
+ * message/content.
441
+ */
442
+ readonly wait_for_upload_message: {
443
+ readonly type: ParameterType.HTML_STRING;
444
+ readonly default: string | null;
445
+ };
446
+ /**
447
+ * Locale code used for translating the default 'uploading video, please
448
+ * wait' message. This code must be present in the translation files. If the
449
+ * code is not found then English will be used. If the
450
+ * 'wait_for_upload_message' parameter is specified then this value is
451
+ * ignored.
452
+ */
398
453
  readonly locale: {
399
454
  readonly type: ParameterType.STRING;
400
455
  readonly default: "en-us";
401
456
  };
402
457
  };
458
+ readonly data: {};
403
459
  };
404
460
  type Info$1 = typeof info$1;
405
461
  /** Stop recording. Used by researchers who want to record across trials. */
@@ -407,12 +463,32 @@ declare class StopRecordPlugin implements JsPsychPlugin<Info$1> {
407
463
  private jsPsych;
408
464
  static readonly info: {
409
465
  readonly name: "stop-record-plugin";
466
+ readonly version: string;
410
467
  readonly parameters: {
468
+ /**
469
+ * This string can contain HTML markup. Any content provided will be
470
+ * displayed while the recording is uploading. If null (the default), then
471
+ * the default 'uploading video, please wait' (or appropriate translation
472
+ * based on 'locale') will be displayed. Use a blank string for no
473
+ * message/content.
474
+ */
475
+ readonly wait_for_upload_message: {
476
+ readonly type: ParameterType.HTML_STRING;
477
+ readonly default: string | null;
478
+ };
479
+ /**
480
+ * Locale code used for translating the default 'uploading video, please
481
+ * wait' message. This code must be present in the translation files. If the
482
+ * code is not found then English will be used. If the
483
+ * 'wait_for_upload_message' parameter is specified then this value is
484
+ * ignored.
485
+ */
411
486
  readonly locale: {
412
487
  readonly type: ParameterType.STRING;
413
488
  readonly default: "en-us";
414
489
  };
415
490
  };
491
+ readonly data: {};
416
492
  };
417
493
  private recorder;
418
494
  /**
@@ -432,12 +508,39 @@ declare class StopRecordPlugin implements JsPsychPlugin<Info$1> {
432
508
  trial(display_element: HTMLElement, trial: TrialType<Info$1>): void;
433
509
  }
434
510
 
435
- /** This extension will allow reasearchers to record trials. */
511
+ interface Parameters {
512
+ /**
513
+ * Content that should be displayed while the recording is uploading. If null
514
+ * (the default), then the default 'uploading video, please wait...' (or
515
+ * appropriate translation based on 'locale') will be displayed. Use a blank
516
+ * string for no message/content. Otherwise this parameter can be set to a
517
+ * custom string and can contain HTML markup. If you want to embed
518
+ * images/video/audio in this HTML string, be sure to preload the media files
519
+ * with the `preload` plugin and manual preloading. Use a blank string (`""`)
520
+ * for no message/content.
521
+ *
522
+ * @default null
523
+ */
524
+ wait_for_upload_message?: null | string;
525
+ /**
526
+ * Locale code used for translating the default 'uploading video, please
527
+ * wait...' message. This code must be present in the translation files. If
528
+ * the code is not found then English will be used. If the
529
+ * 'wait_for_upload_message' parameter is specified then this value
530
+ * isignored.
531
+ *
532
+ * @default "en-us"
533
+ */
534
+ locale?: string;
535
+ }
536
+ /** This extension allows researchers to record webcam audio/video during trials. */
436
537
  declare class TrialRecordExtension implements JsPsychExtension {
437
538
  private jsPsych;
438
539
  static readonly info: JsPsychExtensionInfo;
439
540
  private recorder?;
440
541
  private pluginName;
542
+ private uploadMsg;
543
+ private locale;
441
544
  /**
442
545
  * Video recording extension.
443
546
  *
@@ -445,20 +548,34 @@ declare class TrialRecordExtension implements JsPsychExtension {
445
548
  */
446
549
  constructor(jsPsych: JsPsych);
447
550
  /**
448
- * Ran on the initialize step for extensions, called when an instance of
551
+ * Runs on the initialize step for extensions, called when an instance of
449
552
  * jsPsych is first initialized through initJsPsych().
553
+ *
554
+ * @param params - Parameters object
555
+ * @param params.wait_for_upload_message - Message to display while waiting
556
+ * for upload. String or null (default)
557
+ * @param params.locale - Message to display while waiting for upload. String
558
+ * or null (default).
559
+ */
560
+ initialize(params?: Parameters): Promise<void>;
561
+ /**
562
+ * Runs at the start of a trial.
563
+ *
564
+ * @param startParams - Parameters object
565
+ * @param startParams.wait_for_upload_message - Message to display while
566
+ * waiting for upload. String or null (default). If given, this will
567
+ * overwrite the value used during initialization.
450
568
  */
451
- initialize(): Promise<void>;
452
- /** Ran at the start of a trial. */
453
- on_start(): void;
454
- /** Ran when the trial has loaded. */
569
+ on_start(startParams?: Parameters): void;
570
+ /** Runs when the trial has loaded. */
455
571
  on_load(): void;
456
572
  /**
457
- * Ran when trial has finished.
573
+ * Runs when trial has finished.
458
574
  *
459
- * @returns Trial data.
575
+ * @returns Any data from the trial extension that should be added to the rest
576
+ * of the trial data.
460
577
  */
461
- on_finish(): {};
578
+ on_finish(): Promise<{}>;
462
579
  /**
463
580
  * Gets the plugin name for the trial that is being extended. This is same as
464
581
  * the "trial_type" value that is stored in the data for this trial.
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import Handlebars from 'handlebars';
6
6
 
7
7
  var _package = {
8
8
  name: "@lookit/record",
9
- version: "3.0.1",
9
+ version: "4.1.0",
10
10
  description: "Recording extensions and plugins for CHS studies.",
11
11
  homepage: "https://github.com/lookit/lookit-jspsych#readme",
12
12
  bugs: {
@@ -48,7 +48,7 @@ var _package = {
48
48
  },
49
49
  peerDependencies: {
50
50
  "@lookit/data": "^0.2.0",
51
- "@lookit/templates": "^2.0.0",
51
+ "@lookit/templates": "^2.1.0",
52
52
  jspsych: "^8.0.3"
53
53
  }
54
54
  };
@@ -141,21 +141,10 @@ class CreateURLError extends Error {
141
141
  this.name = "CreateURLError";
142
142
  }
143
143
  }
144
- class VideoContainerNotFoundError extends Error {
145
- constructor() {
146
- super("Video Container could not be found.");
147
- this.name = "VideoContainerError";
148
- }
149
- }
150
- class ButtonNotFoundError extends Error {
151
- constructor(id) {
152
- super(`"${id}" button not found.`);
153
- this.name = "ButtonNotFoundError";
154
- }
155
- }
156
- class ImageNotFoundError extends Error {
157
- constructor(id) {
158
- super(`"${id}" image not found.`);
144
+ class ElementNotFoundError extends Error {
145
+ constructor(id, tag) {
146
+ super(`"${id}" ${tag} not found.`);
147
+ this.name = "ElementNotFoundError";
159
148
  }
160
149
  }
161
150
 
@@ -268,9 +257,9 @@ class Recorder {
268
257
  this.recorder.stop();
269
258
  this.stream.getTracks().map((t) => t.stop());
270
259
  }
271
- stop() {
260
+ stop(maintain_container_size = false) {
261
+ this.clearWebcamFeed(maintain_container_size);
272
262
  this.stopTracks();
273
- this.clearWebcamFeed();
274
263
  if (!this.stopPromise) {
275
264
  throw new NoStopPromiseError();
276
265
  }
@@ -298,6 +287,7 @@ class Recorder {
298
287
  } else {
299
288
  await this.s3.completeUpload();
300
289
  }
290
+ this.reset();
301
291
  resolve();
302
292
  };
303
293
  }
@@ -315,11 +305,20 @@ class Recorder {
315
305
  link.click();
316
306
  }
317
307
  }
318
- clearWebcamFeed() {
308
+ clearWebcamFeed(maintain_container_size) {
319
309
  const webcam_feed_element = document.querySelector(
320
310
  `#${this.webcam_element_id}`
321
311
  );
322
312
  if (webcam_feed_element) {
313
+ if (maintain_container_size) {
314
+ const parent_div = webcam_feed_element.parentElement;
315
+ if (parent_div) {
316
+ const width = webcam_feed_element.offsetWidth;
317
+ const height = webcam_feed_element.offsetHeight;
318
+ parent_div.style.height = `${height}px`;
319
+ parent_div.style.width = `${width}px`;
320
+ }
321
+ }
323
322
  webcam_feed_element.remove();
324
323
  }
325
324
  }
@@ -383,12 +382,22 @@ const info$3 = {
383
382
  prompt_only_adults: { type: ParameterType.BOOL, default: false },
384
383
  consent_statement_text: { type: ParameterType.STRING, default: "" },
385
384
  omit_injury_phrase: { type: ParameterType.BOOL, default: false }
385
+ },
386
+ data: {
387
+ chs_type: {
388
+ type: ParameterType.STRING
389
+ }
386
390
  }
387
391
  };
388
392
  const _VideoConsentPlugin = class {
389
393
  constructor(jsPsych) {
390
394
  this.jsPsych = jsPsych;
391
395
  this.video_container_id = "lookit-jspsych-video-container";
396
+ this.msg_container_id = "lookit-jspsych-video-msg-container";
397
+ this.uploadingMsg = null;
398
+ this.startingMsg = null;
399
+ this.recordingMsg = null;
400
+ this.notRecordingMsg = null;
392
401
  this.jsPsych = jsPsych;
393
402
  this.recorder = new Recorder(this.jsPsych);
394
403
  }
@@ -400,13 +409,25 @@ const _VideoConsentPlugin = class {
400
409
  this.stopButton(display);
401
410
  this.playButton(display);
402
411
  this.nextButton(display);
412
+ this.uploadingMsg = chsTemplates.translateString(
413
+ "exp-lookit-video-consent.Stopping-and-uploading"
414
+ );
415
+ this.startingMsg = chsTemplates.translateString(
416
+ "exp-lookit-video-consent.Starting-recorder"
417
+ );
418
+ this.recordingMsg = chsTemplates.translateString(
419
+ "exp-lookit-video-consent.Recording"
420
+ );
421
+ this.notRecordingMsg = chsTemplates.translateString(
422
+ "exp-lookit-video-consent.Not-recording"
423
+ );
403
424
  }
404
425
  getVideoContainer(display) {
405
426
  const videoContainer = display.querySelector(
406
427
  `div#${this.video_container_id}`
407
428
  );
408
429
  if (!videoContainer) {
409
- throw new VideoContainerNotFoundError();
430
+ throw new ElementNotFoundError(this.video_container_id, "div");
410
431
  }
411
432
  return videoContainer;
412
433
  }
@@ -417,14 +438,31 @@ const _VideoConsentPlugin = class {
417
438
  }
418
439
  playbackFeed(display) {
419
440
  const videoContainer = this.getVideoContainer(display);
420
- this.recorder.insertPlaybackFeed(videoContainer, this.onEnded(display));
441
+ this.recorder.insertPlaybackFeed(
442
+ videoContainer,
443
+ this.onPlaybackEnded(display)
444
+ );
445
+ }
446
+ getMessageContainer(display) {
447
+ const msgContainer = display.querySelector(
448
+ `div#${this.msg_container_id}`
449
+ );
450
+ if (!msgContainer) {
451
+ throw new ElementNotFoundError(this.msg_container_id, "div");
452
+ }
453
+ return msgContainer;
454
+ }
455
+ addMessage(display, message) {
456
+ const msgContainer = this.getMessageContainer(display);
457
+ msgContainer.innerHTML = message;
421
458
  }
422
- onEnded(display) {
459
+ onPlaybackEnded(display) {
423
460
  return () => {
424
461
  const next = this.getButton(display, "next");
425
462
  const play = this.getButton(display, "play");
426
463
  const record = this.getButton(display, "record");
427
464
  this.recordFeed(display);
465
+ this.addMessage(display, this.notRecordingMsg);
428
466
  next.disabled = false;
429
467
  play.disabled = false;
430
468
  record.disabled = false;
@@ -433,14 +471,14 @@ const _VideoConsentPlugin = class {
433
471
  getButton(display, id) {
434
472
  const btn = display.querySelector(`button#${id}`);
435
473
  if (!btn) {
436
- throw new ButtonNotFoundError(id);
474
+ throw new ElementNotFoundError(id, "button");
437
475
  }
438
476
  return btn;
439
477
  }
440
478
  getImg(display, id) {
441
479
  const img = display.querySelector(`img#${id}`);
442
480
  if (!img) {
443
- throw new ImageNotFoundError(id);
481
+ throw new ElementNotFoundError(id, "img");
444
482
  }
445
483
  return img;
446
484
  }
@@ -450,12 +488,14 @@ const _VideoConsentPlugin = class {
450
488
  const play = this.getButton(display, "play");
451
489
  const next = this.getButton(display, "next");
452
490
  record.addEventListener("click", async () => {
491
+ this.addMessage(display, this.startingMsg);
453
492
  record.disabled = true;
454
- stop.disabled = false;
455
493
  play.disabled = true;
456
494
  next.disabled = true;
457
- this.getImg(display, "record-icon").style.visibility = "visible";
458
495
  await this.recorder.start(true, _VideoConsentPlugin.info.name);
496
+ this.getImg(display, "record-icon").style.visibility = "visible";
497
+ this.addMessage(display, this.recordingMsg);
498
+ stop.disabled = false;
459
499
  });
460
500
  }
461
501
  playButton(display) {
@@ -473,11 +513,13 @@ const _VideoConsentPlugin = class {
473
513
  const play = this.getButton(display, "play");
474
514
  stop.addEventListener("click", async () => {
475
515
  stop.disabled = true;
476
- record.disabled = false;
477
- play.disabled = false;
478
- await this.recorder.stop();
479
- this.recorder.reset();
516
+ this.addMessage(display, this.uploadingMsg);
517
+ await this.recorder.stop(true);
480
518
  this.recordFeed(display);
519
+ this.getImg(display, "record-icon").style.visibility = "hidden";
520
+ this.addMessage(display, this.notRecordingMsg);
521
+ play.disabled = false;
522
+ record.disabled = false;
481
523
  });
482
524
  }
483
525
  nextButton(display) {
@@ -497,7 +539,12 @@ const _VideoConsentPlugin = class {
497
539
  let VideoConsentPlugin = _VideoConsentPlugin;
498
540
  VideoConsentPlugin.info = info$3;
499
541
 
500
- const info$2 = { name: "start-record-plugin", parameters: {} };
542
+ const info$2 = {
543
+ name: "start-record-plugin",
544
+ version: _package.version,
545
+ parameters: {},
546
+ data: {}
547
+ };
501
548
  const _StartRecordPlugin = class {
502
549
  constructor(jsPsych) {
503
550
  this.jsPsych = jsPsych;
@@ -519,9 +566,18 @@ StartRecordPlugin.info = info$2;
519
566
 
520
567
  const info$1 = {
521
568
  name: "stop-record-plugin",
569
+ version: _package.version,
522
570
  parameters: {
523
- locale: { type: ParameterType.STRING, default: "en-us" }
524
- }
571
+ wait_for_upload_message: {
572
+ type: ParameterType.HTML_STRING,
573
+ default: null
574
+ },
575
+ locale: {
576
+ type: ParameterType.STRING,
577
+ default: "en-us"
578
+ }
579
+ },
580
+ data: {}
525
581
  };
526
582
  class StopRecordPlugin {
527
583
  constructor(jsPsych) {
@@ -533,11 +589,17 @@ class StopRecordPlugin {
533
589
  }
534
590
  }
535
591
  trial(display_element, trial) {
536
- display_element.innerHTML = chsTemplates.uploadingVideo(trial);
592
+ if (trial.wait_for_upload_message == null) {
593
+ display_element.innerHTML = chsTemplates.uploadingVideo(trial);
594
+ } else {
595
+ display_element.innerHTML = trial.wait_for_upload_message;
596
+ }
537
597
  this.recorder.stop().then(() => {
538
598
  window.chs.sessionRecorder = null;
539
599
  display_element.innerHTML = "";
540
600
  this.jsPsych.finishTrial();
601
+ }).catch((err) => {
602
+ console.error("StopRecordPlugin: recorder stop/upload failed.", err);
541
603
  });
542
604
  }
543
605
  }
@@ -546,19 +608,50 @@ StopRecordPlugin.info = info$1;
546
608
  class TrialRecordExtension {
547
609
  constructor(jsPsych) {
548
610
  this.jsPsych = jsPsych;
611
+ this.uploadMsg = null;
612
+ this.locale = "en-us";
549
613
  autoBind(this);
550
614
  }
551
- async initialize() {
615
+ async initialize(params) {
616
+ await new Promise((resolve) => {
617
+ this.uploadMsg = params?.wait_for_upload_message ? params.wait_for_upload_message : null;
618
+ this.locale = params?.locale ? params.locale : "en-us";
619
+ console.log(this.uploadMsg);
620
+ console.log(this.locale);
621
+ resolve();
622
+ });
552
623
  }
553
- on_start() {
624
+ on_start(startParams) {
625
+ if (startParams?.wait_for_upload_message) {
626
+ this.uploadMsg = startParams.wait_for_upload_message;
627
+ }
628
+ if (startParams?.locale) {
629
+ this.locale = startParams.locale;
630
+ }
631
+ console.log(this.uploadMsg);
632
+ console.log(this.locale);
554
633
  this.recorder = new Recorder(this.jsPsych);
555
634
  }
556
635
  on_load() {
557
636
  this.pluginName = this.getCurrentPluginName();
558
637
  this.recorder?.start(false, `${this.pluginName}`);
559
638
  }
560
- on_finish() {
561
- this.recorder?.stop();
639
+ async on_finish() {
640
+ const displayEl = this.jsPsych.getDisplayElement();
641
+ if (this.uploadMsg == null) {
642
+ displayEl.innerHTML = chsTemplates.uploadingVideo({
643
+ type: this.jsPsych.getCurrentTrial().type,
644
+ locale: this.locale
645
+ });
646
+ } else {
647
+ displayEl.innerHTML = this.uploadMsg;
648
+ }
649
+ try {
650
+ await this.recorder?.stop();
651
+ displayEl.innerHTML = "";
652
+ } catch (err) {
653
+ console.error("TrialRecordExtension: recorder stop/upload failed.", err);
654
+ }
562
655
  return {};
563
656
  }
564
657
  getCurrentPluginName() {
@@ -567,7 +660,9 @@ class TrialRecordExtension {
567
660
  }
568
661
  }
569
662
  TrialRecordExtension.info = {
570
- name: "chs-trial-record-extension"
663
+ name: "chs-trial-record-extension",
664
+ version: _package.version,
665
+ data: {}
571
666
  };
572
667
 
573
668
  var img$6 = "data:image/png;base64,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";