@lookit/record 3.0.0 → 4.0.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
@@ -272,6 +272,11 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
272
272
  };
273
273
  private readonly recorder;
274
274
  private readonly video_container_id;
275
+ private readonly msg_container_id;
276
+ private uploadingMsg;
277
+ private startingMsg;
278
+ private recordingMsg;
279
+ private notRecordingMsg;
275
280
  /**
276
281
  * Instantiate video consent plugin.
277
282
  *
@@ -305,13 +310,30 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
305
310
  */
306
311
  private playbackFeed;
307
312
  /**
308
- * Put back the webcam feed once the video recording has ended. This is used
309
- * with the "ended" Event.
313
+ * Get message container that appears alongside the video element.
314
+ *
315
+ * @param display - HTML element for experiment.
316
+ * @returns Message container div element.
317
+ */
318
+ private getMessageContainer;
319
+ /**
320
+ * Add HTML-formatted message alongside the video feed, e.g. for waiting
321
+ * periods during webcam feed transitions (starting, stopping/uploading). This
322
+ * will also replace an existing message with the new one. To clear any
323
+ * existing messages, pass an empty string.
324
+ *
325
+ * @param display - HTML element for experiment.
326
+ * @param message - HTML content for message div.
327
+ */
328
+ private addMessage;
329
+ /**
330
+ * Put back the webcam feed once the video recording playback has ended. This
331
+ * is used with the "ended" Event.
310
332
  *
311
333
  * @param display - JsPsych display HTML element.
312
334
  * @returns Event function
313
335
  */
314
- private onEnded;
336
+ private onPlaybackEnded;
315
337
  /**
316
338
  * Retrieve button element from DOM.
317
339
  *
@@ -553,6 +575,7 @@ declare class VideoConfigPlugin implements JsPsychPlugin<Info> {
553
575
  private minVolume;
554
576
  private micChecked;
555
577
  private processorNode;
578
+ private mimeType;
556
579
  /**
557
580
  * Constructor for video config plugin.
558
581
  *
@@ -663,6 +686,8 @@ declare class VideoConfigPlugin implements JsPsychPlugin<Info> {
663
686
  * @param stream - Media stream returned from getUserMedia that should be used
664
687
  * to set up the jsPsych recorder.
665
688
  * @param opts - Media recorder options to use when setting up the recorder.
689
+ * This will include the mimeType property that is set via getMimeTypeCodec,
690
+ * as well as any other options that can passed via the calling context.
666
691
  */
667
692
  initializeAndCreateRecorder: (stream: MediaStream, opts?: MediaRecorderOptions) => void;
668
693
  /**
@@ -753,6 +778,20 @@ declare class VideoConfigPlugin implements JsPsychPlugin<Info> {
753
778
  * button.
754
779
  */
755
780
  private enableNext;
781
+ /**
782
+ * Check support for recording containers/codecs, in order of preference, and
783
+ * get the first supported type. The first supported type found in the
784
+ * mime_types array is returned and will be passed to the "mimeType" property
785
+ * in the recorder options object that is passed to the recorder
786
+ * initialization function (jsPsych.pluginAPI.initializeCameraRecorder). If
787
+ * none of these types is supported, the function returns null.
788
+ *
789
+ * Note: we will likely need to continuously update the mime_types list as new
790
+ * formats become supported, we support other browsers/versions, etc.
791
+ *
792
+ * @returns Mime type string, or null (if none from the array are supported).
793
+ */
794
+ private getCompatibleMimeType;
756
795
  }
757
796
 
758
797
  declare const _default: {
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.0",
9
+ version: "4.0.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
 
@@ -175,8 +164,10 @@ class Recorder {
175
164
  this.blobs = [];
176
165
  this.localDownload = "false"?.toLowerCase() === "true";
177
166
  this.webcam_element_id = "lookit-jspsych-webcam";
167
+ this.mimeType = "video/webm";
178
168
  this.streamClone = this.stream.clone();
179
169
  autoBind(this);
170
+ this.mimeType = this.recorder?.mimeType || this.mimeType;
180
171
  }
181
172
  get recorder() {
182
173
  return this.jsPsych.pluginAPI.getCameraRecorder() || this.jsPsych.pluginAPI.getMicrophoneRecorder();
@@ -194,7 +185,11 @@ class Recorder {
194
185
  this._s3 = value;
195
186
  }
196
187
  initializeRecorder(stream, opts) {
197
- this.jsPsych.pluginAPI.initializeCameraRecorder(stream, opts);
188
+ const recorder_options = {
189
+ ...opts,
190
+ mimeType: this.mimeType
191
+ };
192
+ this.jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
198
193
  }
199
194
  reset() {
200
195
  if (this.stream.active) {
@@ -262,9 +257,9 @@ class Recorder {
262
257
  this.recorder.stop();
263
258
  this.stream.getTracks().map((t) => t.stop());
264
259
  }
265
- stop() {
260
+ stop(maintain_container_size = false) {
261
+ this.clearWebcamFeed(maintain_container_size);
266
262
  this.stopTracks();
267
- this.clearWebcamFeed();
268
263
  if (!this.stopPromise) {
269
264
  throw new NoStopPromiseError();
270
265
  }
@@ -309,11 +304,20 @@ class Recorder {
309
304
  link.click();
310
305
  }
311
306
  }
312
- clearWebcamFeed() {
307
+ clearWebcamFeed(maintain_container_size) {
313
308
  const webcam_feed_element = document.querySelector(
314
309
  `#${this.webcam_element_id}`
315
310
  );
316
311
  if (webcam_feed_element) {
312
+ if (maintain_container_size) {
313
+ const parent_div = webcam_feed_element.parentElement;
314
+ if (parent_div) {
315
+ const width = webcam_feed_element.offsetWidth;
316
+ const height = webcam_feed_element.offsetHeight;
317
+ parent_div.style.height = `${height}px`;
318
+ parent_div.style.width = `${width}px`;
319
+ }
320
+ }
317
321
  webcam_feed_element.remove();
318
322
  }
319
323
  }
@@ -383,6 +387,11 @@ const _VideoConsentPlugin = class {
383
387
  constructor(jsPsych) {
384
388
  this.jsPsych = jsPsych;
385
389
  this.video_container_id = "lookit-jspsych-video-container";
390
+ this.msg_container_id = "lookit-jspsych-video-msg-container";
391
+ this.uploadingMsg = null;
392
+ this.startingMsg = null;
393
+ this.recordingMsg = null;
394
+ this.notRecordingMsg = null;
386
395
  this.jsPsych = jsPsych;
387
396
  this.recorder = new Recorder(this.jsPsych);
388
397
  }
@@ -394,13 +403,25 @@ const _VideoConsentPlugin = class {
394
403
  this.stopButton(display);
395
404
  this.playButton(display);
396
405
  this.nextButton(display);
406
+ this.uploadingMsg = chsTemplates.translateString(
407
+ "exp-lookit-video-consent.Stopping-and-uploading"
408
+ );
409
+ this.startingMsg = chsTemplates.translateString(
410
+ "exp-lookit-video-consent.Starting-recorder"
411
+ );
412
+ this.recordingMsg = chsTemplates.translateString(
413
+ "exp-lookit-video-consent.Recording"
414
+ );
415
+ this.notRecordingMsg = chsTemplates.translateString(
416
+ "exp-lookit-video-consent.Not-recording"
417
+ );
397
418
  }
398
419
  getVideoContainer(display) {
399
420
  const videoContainer = display.querySelector(
400
421
  `div#${this.video_container_id}`
401
422
  );
402
423
  if (!videoContainer) {
403
- throw new VideoContainerNotFoundError();
424
+ throw new ElementNotFoundError(this.video_container_id, "div");
404
425
  }
405
426
  return videoContainer;
406
427
  }
@@ -411,14 +432,31 @@ const _VideoConsentPlugin = class {
411
432
  }
412
433
  playbackFeed(display) {
413
434
  const videoContainer = this.getVideoContainer(display);
414
- this.recorder.insertPlaybackFeed(videoContainer, this.onEnded(display));
435
+ this.recorder.insertPlaybackFeed(
436
+ videoContainer,
437
+ this.onPlaybackEnded(display)
438
+ );
415
439
  }
416
- onEnded(display) {
440
+ getMessageContainer(display) {
441
+ const msgContainer = display.querySelector(
442
+ `div#${this.msg_container_id}`
443
+ );
444
+ if (!msgContainer) {
445
+ throw new ElementNotFoundError(this.msg_container_id, "div");
446
+ }
447
+ return msgContainer;
448
+ }
449
+ addMessage(display, message) {
450
+ const msgContainer = this.getMessageContainer(display);
451
+ msgContainer.innerHTML = message;
452
+ }
453
+ onPlaybackEnded(display) {
417
454
  return () => {
418
455
  const next = this.getButton(display, "next");
419
456
  const play = this.getButton(display, "play");
420
457
  const record = this.getButton(display, "record");
421
458
  this.recordFeed(display);
459
+ this.addMessage(display, this.notRecordingMsg);
422
460
  next.disabled = false;
423
461
  play.disabled = false;
424
462
  record.disabled = false;
@@ -427,14 +465,14 @@ const _VideoConsentPlugin = class {
427
465
  getButton(display, id) {
428
466
  const btn = display.querySelector(`button#${id}`);
429
467
  if (!btn) {
430
- throw new ButtonNotFoundError(id);
468
+ throw new ElementNotFoundError(id, "button");
431
469
  }
432
470
  return btn;
433
471
  }
434
472
  getImg(display, id) {
435
473
  const img = display.querySelector(`img#${id}`);
436
474
  if (!img) {
437
- throw new ImageNotFoundError(id);
475
+ throw new ElementNotFoundError(id, "img");
438
476
  }
439
477
  return img;
440
478
  }
@@ -444,12 +482,14 @@ const _VideoConsentPlugin = class {
444
482
  const play = this.getButton(display, "play");
445
483
  const next = this.getButton(display, "next");
446
484
  record.addEventListener("click", async () => {
485
+ this.addMessage(display, this.startingMsg);
447
486
  record.disabled = true;
448
- stop.disabled = false;
449
487
  play.disabled = true;
450
488
  next.disabled = true;
451
- this.getImg(display, "record-icon").style.visibility = "visible";
452
489
  await this.recorder.start(true, _VideoConsentPlugin.info.name);
490
+ this.getImg(display, "record-icon").style.visibility = "visible";
491
+ this.addMessage(display, this.recordingMsg);
492
+ stop.disabled = false;
453
493
  });
454
494
  }
455
495
  playButton(display) {
@@ -467,11 +507,14 @@ const _VideoConsentPlugin = class {
467
507
  const play = this.getButton(display, "play");
468
508
  stop.addEventListener("click", async () => {
469
509
  stop.disabled = true;
470
- record.disabled = false;
471
- play.disabled = false;
472
- await this.recorder.stop();
510
+ this.addMessage(display, this.uploadingMsg);
511
+ await this.recorder.stop(true);
473
512
  this.recorder.reset();
474
513
  this.recordFeed(display);
514
+ this.getImg(display, "record-icon").style.visibility = "hidden";
515
+ this.addMessage(display, this.notRecordingMsg);
516
+ play.disabled = false;
517
+ record.disabled = false;
475
518
  });
476
519
  }
477
520
  nextButton(display) {
@@ -638,6 +681,7 @@ class VideoConfigPlugin {
638
681
  this.minVolume = 0.1;
639
682
  this.micChecked = false;
640
683
  this.processorNode = null;
684
+ this.mimeType = "video/webm";
641
685
  this.addHtmlContent = (trial) => {
642
686
  this.display_el.innerHTML = chsTemplates.videoConfig(trial, html_params);
643
687
  };
@@ -780,7 +824,12 @@ class VideoConfigPlugin {
780
824
  return { cameras: unique_cameras, mics: unique_mics };
781
825
  };
782
826
  this.initializeAndCreateRecorder = (stream, opts) => {
783
- this.jsPsych.pluginAPI.initializeCameraRecorder(stream, opts);
827
+ this.mimeType = this.getCompatibleMimeType() || this.mimeType;
828
+ const recorder_options = {
829
+ ...opts,
830
+ mimeType: this.mimeType
831
+ };
832
+ this.jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
784
833
  this.recorder = new Recorder(this.jsPsych);
785
834
  };
786
835
  this.checkMic = async (minVol = this.minVolume) => {
@@ -933,6 +982,20 @@ class VideoConfigPlugin {
933
982
  };
934
983
  });
935
984
  }
985
+ getCompatibleMimeType() {
986
+ const mime_types = [
987
+ "video/webm;codecs=vp9,opus",
988
+ "video/webm;codecs=vp8,opus"
989
+ ];
990
+ let mime_type_index = 0;
991
+ while (mime_type_index < mime_types.length) {
992
+ if (MediaRecorder.isTypeSupported(mime_types[mime_type_index])) {
993
+ return mime_types[mime_type_index];
994
+ }
995
+ mime_type_index++;
996
+ }
997
+ return null;
998
+ }
936
999
  }
937
1000
  VideoConfigPlugin.info = info;
938
1001