@lookit/record 4.1.0 → 5.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/README.md CHANGED
@@ -368,6 +368,13 @@ string, be sure to preload the media files with the `preload` plugin and
368
368
  Use a blank string (`""`) for no message/content. If a value is provided then
369
369
  the `locale` parameter will be ignored.
370
370
 
371
+ **`max_upload_seconds` [ Integer | 10 ]**
372
+
373
+ Maximum duration (in seconds) to wait for the trial recording to finish
374
+ uploading before continuing with the experiment. If the maximum upload duration
375
+ is reached and the upload has not finished, then the experiment will move on and
376
+ the trial recording upload will continue in the background.
377
+
371
378
  ### Examples
372
379
 
373
380
  **Basic usage**
@@ -452,6 +459,27 @@ const trialRec = {
452
459
  };
453
460
  ```
454
461
 
462
+ By default, the trial recording extension will wait up to 10 seconds for the
463
+ recording to finish uploading before moving on to the next trial. If the upload
464
+ does not finish in that time, it will continue to upload in the background. You
465
+ can adjust this duration with the `max_upload_seconds` parameter. You may want
466
+ to increase or decrease this duration depending on the duration of your recorded
467
+ trials, or you expect that some participants may have slow/unreliable internet
468
+ connections. This example decreases the maximum upload duration from 10 to 5
469
+ seconds:
470
+
471
+ ```javascript
472
+ const trialRec = {
473
+ // ... Other trial parameters ...
474
+ extensions: [
475
+ {
476
+ type: chsRecord.TrialRecordExtension,
477
+ params: { max_upload_seconds: 5 },
478
+ },
479
+ ],
480
+ };
481
+ ```
482
+
455
483
  ## Session Recording
456
484
 
457
485
  You might prefer to record across multiple trials in a study session. This can
@@ -480,8 +508,18 @@ const startRec = { type: chsRecord.StartRecordPlugin };
480
508
 
481
509
  #### Parameters
482
510
 
483
- This plugin does not accept any parameters, other those available in all
484
- plugins.
511
+ **`wait_for_connection_message` [`null` or HTML string | `null` ]**
512
+
513
+ This parameter determines what content should be displayed while the session
514
+ recording is initializing. If `null` (the default), then the message
515
+ 'establishing video connection, please wait' (or appropriate translation based
516
+ on `locale`) will be displayed. Otherwise this parameter can be set to a custom
517
+ string and can contain HTML markup. If you want to embed images/video/audio in
518
+ this HTML string, be sure to preload the media files with the `preload` plugin
519
+ and
520
+ [manual preloading](https://www.jspsych.org/latest/overview/media-preloading/#manual-preloading).
521
+ Use a blank string (`""`) for no message/content. If a value is provided then
522
+ the `locale` parameter will be ignored.
485
523
 
486
524
  ### Stop Recording Plugin
487
525
 
@@ -519,6 +557,13 @@ string, be sure to preload the media files with the `preload` plugin and
519
557
  Use a blank string (`""`) for no message/content. If a value is provided then
520
558
  the `locale` parameter will be ignored.
521
559
 
560
+ **`max_upload_seconds` [ Integer | 10 ]**
561
+
562
+ Maximum duration (in seconds) to wait for the session recording to finish
563
+ uploading before continuing with the experiment. If the maximum upload duration
564
+ is reached and the upload has not finished, then the experiment will move on and
565
+ the session recording upload will continue in the background.
566
+
522
567
  ### Examples
523
568
 
524
569
  **Basic usage**
@@ -570,28 +615,55 @@ jsPsych.run([
570
615
 
571
616
  **Setting parameters**
572
617
 
573
- By default, the stop session recording plugin will display "uploading video,
574
- please wait..." while the session recording is uploading. You can set the
575
- `locale` parameter value to translate this message to another language. For
576
- example, the trial below will display the Brazilian Portuguese translation of
577
- this message. If the locale string does not match any of the translation codes
578
- that we support, then the message will be displayed in English.
618
+ By default, the start session recording plugin will display "establishing video
619
+ connection, please wait" while establishing the connection to our video storage,
620
+ and the stop session recording plugin will display "uploading video, please
621
+ wait..." while the session recording is uploading. You can set the `locale`
622
+ parameter value to translate these messages to another language. For example,
623
+ the trial below will display the Brazilian Portuguese translation of these
624
+ messages. If the locale string does not match any of the translation codes that
625
+ we support, then the message will be displayed in English.
579
626
 
580
627
  ```javascript
628
+ const startpRec = {
629
+ type: type: chsRecord.StartRecordPlugin,
630
+ locale: "pt-br",
631
+ };
581
632
  const stopRec = {
582
633
  type: chsRecord.StopRecordPlugin,
583
634
  locale: "pt-br",
584
635
  };
585
636
  ```
586
637
 
587
- You can also set custom content to be displayed while the session recording file
588
- is uploading. The value must be a string. It can include HTML-formatted content,
638
+ You can also set custom content to be displayed at the start of the session
639
+ recording, while it is initializing, and/or at the end, when the file is
640
+ uploading. The value must be a string. It can include HTML-formatted content,
589
641
  which means that you can embed audio, video, images etc. (be sure to preload any
590
642
  media files!).
591
643
 
592
644
  ```javascript
645
+ const startpRec = {
646
+ type: type: chsRecord.StartRecordPlugin,
647
+ wait_for_connection_message: "<p style='color:green;>Please wait...</p>"
648
+ };
593
649
  const stopRec = {
594
650
  type: chsRecord.StopRecordPlugin,
595
651
  wait_for_upload_message: "<p style='color:red;'>Hang on a sec!</p>",
596
652
  };
597
653
  ```
654
+
655
+ By default, the stop session recording plugin will wait up to 10 seconds for the
656
+ session recording to finish uploading before moving on with the experiment. If
657
+ the upload does not finish in that time, it will continue to upload in the
658
+ background. You can adjust this duration with the `max_upload_seconds`
659
+ parameter. You may want to increase this duration if, for example, your
660
+ experiment creates a very long session recording, or you expect that some
661
+ participants may have slow/unreliable internet connections. This example
662
+ increases the maximum upload duration from 10 to 20 seconds:
663
+
664
+ ```javascript
665
+ const stopRec = {
666
+ type: chsRecord.StopRecordPlugin,
667
+ max_upload_seconds: 20,
668
+ };
669
+ ```
@@ -32,7 +32,7 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
32
32
 
33
33
  var _package = {
34
34
  name: "@lookit/record",
35
- version: "4.1.0",
35
+ version: "5.0.0",
36
36
  description: "Recording extensions and plugins for CHS studies.",
37
37
  homepage: "https://github.com/lookit/lookit-jspsych#readme",
38
38
  bugs: {
@@ -73,8 +73,8 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
73
73
  typescript: "^5.6.2"
74
74
  },
75
75
  peerDependencies: {
76
- "@lookit/data": "^0.2.0",
77
- "@lookit/templates": "^2.1.0",
76
+ "@lookit/data": "^0.3.0",
77
+ "@lookit/templates": "^3.0.0",
78
78
  jspsych: "^8.0.3"
79
79
  }
80
80
  };
@@ -149,6 +149,12 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
149
149
  this.name = "S3UndefinedError";
150
150
  }
151
151
  }
152
+ class NoFileNameError extends Error {
153
+ constructor() {
154
+ super("No filename found for recording.");
155
+ this.name = "NoFileNameError";
156
+ }
157
+ }
152
158
  class StreamActiveOnResetError extends Error {
153
159
  constructor() {
154
160
  super("Won't reset recorder. Stream is still active.");
@@ -173,6 +179,12 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
173
179
  this.name = "ElementNotFoundError";
174
180
  }
175
181
  }
182
+ class TimeoutError extends Error {
183
+ constructor(msg) {
184
+ super(`${msg}`);
185
+ this.name = "TimeoutError";
186
+ }
187
+ }
176
188
 
177
189
  // Gets all non-builtin properties up the prototype chain.
178
190
  const getAllProperties = object => {
@@ -8465,6 +8477,31 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8465
8477
 
8466
8478
  var img$7 = "data:image/svg+xml,%3c%3fxml version='1.0' encoding='utf-8' %3f%3e%3csvg viewBox='-1 -1 18 18' xmlns='http://www.w3.org/2000/svg'%3e %3ccircle fill='red' stroke='black' stroke-width='0.5' cx='8' cy='8' r='8'%3e%3c/circle%3e%3c/svg%3e";
8467
8479
 
8480
+ const promiseWithTimeout = (promise, promiseId, timeoutMs, onTimeoutCleanup) => {
8481
+ let timeoutHandle;
8482
+ const timeout = new Promise((resolve) => {
8483
+ timeoutHandle = setTimeout(() => {
8484
+ onTimeoutCleanup?.();
8485
+ resolve("timeout");
8486
+ }, timeoutMs);
8487
+ });
8488
+ return Promise.race([promise, timeout]).then(
8489
+ (value) => {
8490
+ if (value == "timeout") {
8491
+ console.log(`Upload for ${promiseId} timed out.`);
8492
+ } else {
8493
+ console.log(`Upload for ${promiseId} completed.`);
8494
+ clearTimeout(timeoutHandle);
8495
+ }
8496
+ return value;
8497
+ },
8498
+ (err) => {
8499
+ clearTimeout(timeoutHandle);
8500
+ throw err;
8501
+ }
8502
+ );
8503
+ };
8504
+
8468
8505
  class Recorder {
8469
8506
  constructor(jsPsych) {
8470
8507
  this.jsPsych = jsPsych;
@@ -8569,13 +8606,73 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8569
8606
  this.recorder.stop();
8570
8607
  this.stream.getTracks().map((t) => t.stop());
8571
8608
  }
8572
- stop(maintain_container_size = false) {
8609
+ stop({
8610
+ maintain_container_size = false,
8611
+ stop_timeout_ms = null,
8612
+ upload_timeout_ms = 1e4
8613
+ } = {}) {
8614
+ this.preStopCheck();
8573
8615
  this.clearWebcamFeed(maintain_container_size);
8574
8616
  this.stopTracks();
8575
- if (!this.stopPromise) {
8576
- throw new NoStopPromiseError();
8577
- }
8578
- return this.stopPromise;
8617
+ const snapshot = {
8618
+ s3: !this.localDownload ? this.s3 : null,
8619
+ filename: this.filename,
8620
+ localDownload: this.localDownload,
8621
+ url: "null"
8622
+ };
8623
+ const stopped = stop_timeout_ms ? promiseWithTimeout(
8624
+ this.stopPromise,
8625
+ `${snapshot.filename}-stopped`,
8626
+ stop_timeout_ms,
8627
+ this.createTimeoutHandler("stop", snapshot.filename)
8628
+ ) : this.stopPromise;
8629
+ stopped.finally(() => {
8630
+ try {
8631
+ this.reset();
8632
+ } catch (err) {
8633
+ console.error("Error while resetting recorder after stop: ", err);
8634
+ }
8635
+ });
8636
+ const uploadPromise = (async () => {
8637
+ let url;
8638
+ try {
8639
+ url = await stopped;
8640
+ if (url == "timeout") {
8641
+ throw new TimeoutError("Recorder stop timed out.");
8642
+ }
8643
+ } catch (err) {
8644
+ console.warn("Upload failed because recorder stop timed out");
8645
+ throw err;
8646
+ }
8647
+ snapshot.url = url;
8648
+ if (snapshot.localDownload) {
8649
+ try {
8650
+ this.download(snapshot.filename, snapshot.url);
8651
+ await Promise.resolve();
8652
+ } catch (err) {
8653
+ console.error("Local download failed: ", err);
8654
+ throw err;
8655
+ }
8656
+ } else {
8657
+ try {
8658
+ await snapshot.s3.completeUpload();
8659
+ } catch (err) {
8660
+ console.error("Upload failed: ", err);
8661
+ throw err;
8662
+ }
8663
+ }
8664
+ })();
8665
+ const uploaded = upload_timeout_ms ? promiseWithTimeout(
8666
+ uploadPromise,
8667
+ `${snapshot.filename}-uploaded`,
8668
+ upload_timeout_ms,
8669
+ this.createTimeoutHandler("upload", snapshot.filename)
8670
+ ) : uploadPromise;
8671
+ window.chs.pendingUploads.push({
8672
+ promise: uploadPromise,
8673
+ file: snapshot.filename
8674
+ });
8675
+ return { stopped, uploaded };
8579
8676
  }
8580
8677
  initializeCheck() {
8581
8678
  if (!this.recorder) {
@@ -8588,19 +8685,27 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8588
8685
  throw new StreamDataInitializeError();
8589
8686
  }
8590
8687
  }
8688
+ preStopCheck() {
8689
+ if (!this.recorder) {
8690
+ throw new RecorderInitializeError();
8691
+ }
8692
+ if (!this.stream.active) {
8693
+ throw new StreamInactiveInitializeError();
8694
+ }
8695
+ if (!this.stopPromise) {
8696
+ throw new NoStopPromiseError();
8697
+ }
8698
+ if (!this.filename) {
8699
+ throw new NoFileNameError();
8700
+ }
8701
+ }
8591
8702
  handleStop(resolve) {
8592
- return async () => {
8703
+ return () => {
8593
8704
  if (this.blobs.length === 0) {
8594
8705
  throw new CreateURLError();
8595
8706
  }
8596
8707
  this.url = URL.createObjectURL(new Blob(this.blobs));
8597
- if (this.localDownload) {
8598
- this.download();
8599
- } else {
8600
- await this.s3.completeUpload();
8601
- }
8602
- this.reset();
8603
- resolve();
8708
+ resolve(this.url);
8604
8709
  };
8605
8710
  }
8606
8711
  handleDataAvailable(event) {
@@ -8609,11 +8714,11 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8609
8714
  this.s3.onDataAvailable(event.data);
8610
8715
  }
8611
8716
  }
8612
- download() {
8613
- if (this.filename && this.url) {
8717
+ download(filename, url) {
8718
+ if (filename && url) {
8614
8719
  const link = document.createElement("a");
8615
- link.href = this.url;
8616
- link.download = this.filename;
8720
+ link.href = url;
8721
+ link.download = filename;
8617
8722
  link.click();
8618
8723
  }
8619
8724
  }
@@ -8642,6 +8747,18 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8642
8747
  const rand_digits = Math.floor(Math.random() * 1e3);
8643
8748
  return `${prefix}_${window.chs.study.id}_${trial_id}_${window.chs.response.id}_${new Date().getTime()}_${rand_digits}.webm`;
8644
8749
  }
8750
+ createTimeoutHandler(eventName, id) {
8751
+ return () => {
8752
+ console.warn(`Recorder ${eventName} timed out: ${id}`);
8753
+ if (!this.stream.active) {
8754
+ try {
8755
+ this.reset();
8756
+ } catch (err) {
8757
+ console.error("Error while resetting recorder after timeout: ", err);
8758
+ }
8759
+ }
8760
+ };
8761
+ }
8645
8762
  }
8646
8763
 
8647
8764
  const info$3 = {
@@ -8828,7 +8945,10 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8828
8945
  stop.addEventListener("click", async () => {
8829
8946
  stop.disabled = true;
8830
8947
  this.addMessage(display, this.uploadingMsg);
8831
- await this.recorder.stop(true);
8948
+ const { stopped, uploaded } = this.recorder.stop({
8949
+ maintain_container_size: true
8950
+ });
8951
+ await stopped;
8832
8952
  this.recordFeed(display);
8833
8953
  this.getImg(display, "record-icon").style.visibility = "hidden";
8834
8954
  this.addMessage(display, this.notRecordingMsg);
@@ -8854,7 +8974,16 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8854
8974
  const info$2 = {
8855
8975
  name: "start-record-plugin",
8856
8976
  version: _package.version,
8857
- parameters: {},
8977
+ parameters: {
8978
+ wait_for_connection_message: {
8979
+ type: jspsych.ParameterType.HTML_STRING,
8980
+ default: null
8981
+ },
8982
+ locale: {
8983
+ type: jspsych.ParameterType.STRING,
8984
+ default: "en-us"
8985
+ }
8986
+ },
8858
8987
  data: {}
8859
8988
  };
8860
8989
  class StartRecordPlugin {
@@ -8869,8 +8998,14 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8869
8998
  }
8870
8999
  static info = info$2;
8871
9000
  recorder;
8872
- trial() {
8873
- this.recorder.start(false, `${StartRecordPlugin.info.name}-multiframe`).then(() => {
9001
+ async trial(display_element, trial) {
9002
+ if (trial.wait_for_connection_message == null) {
9003
+ display_element.innerHTML = chsTemplates.establishingConnection(trial);
9004
+ } else {
9005
+ display_element.innerHTML = trial.wait_for_connection_message;
9006
+ }
9007
+ await this.recorder.start(false, `${StartRecordPlugin.info.name}-multiframe`).then(() => {
9008
+ display_element.innerHTML = "";
8874
9009
  this.jsPsych.finishTrial();
8875
9010
  });
8876
9011
  }
@@ -8887,6 +9022,10 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8887
9022
  locale: {
8888
9023
  type: jspsych.ParameterType.STRING,
8889
9024
  default: "en-us"
9025
+ },
9026
+ max_upload_seconds: {
9027
+ type: jspsych.ParameterType.INT,
9028
+ default: 10
8890
9029
  }
8891
9030
  },
8892
9031
  data: {}
@@ -8902,19 +9041,25 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8902
9041
  }
8903
9042
  static info = info$1;
8904
9043
  recorder;
8905
- trial(display_element, trial) {
9044
+ async trial(display_element, trial) {
8906
9045
  if (trial.wait_for_upload_message == null) {
8907
9046
  display_element.innerHTML = chsTemplates.uploadingVideo(trial);
8908
9047
  } else {
8909
9048
  display_element.innerHTML = trial.wait_for_upload_message;
8910
9049
  }
8911
- this.recorder.stop().then(() => {
9050
+ const { stopped, uploaded } = this.recorder.stop({
9051
+ upload_timeout_ms: trial.max_upload_seconds !== null ? trial.max_upload_seconds * 1e3 : null
9052
+ });
9053
+ try {
9054
+ await stopped;
9055
+ await uploaded;
9056
+ } catch (err) {
9057
+ console.error("StopRecordPlugin: recorder stop/upload failed.", err);
9058
+ } finally {
8912
9059
  window.chs.sessionRecorder = null;
8913
9060
  display_element.innerHTML = "";
8914
9061
  this.jsPsych.finishTrial();
8915
- }).catch((err) => {
8916
- console.error("StopRecordPlugin: recorder stop/upload failed.", err);
8917
- });
9062
+ }
8918
9063
  }
8919
9064
  }
8920
9065
 
@@ -8932,12 +9077,12 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8932
9077
  pluginName;
8933
9078
  uploadMsg = null;
8934
9079
  locale = "en-us";
9080
+ maxUploadSeconds = void 0;
8935
9081
  async initialize(params) {
8936
9082
  await new Promise((resolve) => {
8937
9083
  this.uploadMsg = params?.wait_for_upload_message ? params.wait_for_upload_message : null;
8938
9084
  this.locale = params?.locale ? params.locale : "en-us";
8939
- console.log(this.uploadMsg);
8940
- console.log(this.locale);
9085
+ this.maxUploadSeconds = params?.max_upload_seconds === void 0 ? 10 : params.max_upload_seconds;
8941
9086
  resolve();
8942
9087
  });
8943
9088
  }
@@ -8948,13 +9093,14 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8948
9093
  if (startParams?.locale) {
8949
9094
  this.locale = startParams.locale;
8950
9095
  }
8951
- console.log(this.uploadMsg);
8952
- console.log(this.locale);
9096
+ if (startParams?.max_upload_seconds !== void 0) {
9097
+ this.maxUploadSeconds = startParams?.max_upload_seconds;
9098
+ }
8953
9099
  this.recorder = new Recorder(this.jsPsych);
9100
+ this.pluginName = this.getCurrentPluginName();
9101
+ this.recorder.start(false, `${this.pluginName}`);
8954
9102
  }
8955
9103
  on_load() {
8956
- this.pluginName = this.getCurrentPluginName();
8957
- this.recorder?.start(false, `${this.pluginName}`);
8958
9104
  }
8959
9105
  async on_finish() {
8960
9106
  const displayEl = this.jsPsych.getDisplayElement();
@@ -8966,13 +9112,27 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
8966
9112
  } else {
8967
9113
  displayEl.innerHTML = this.uploadMsg;
8968
9114
  }
8969
- try {
8970
- await this.recorder?.stop();
9115
+ if (this.recorder) {
9116
+ const { stopped, uploaded } = this.recorder.stop({
9117
+ upload_timeout_ms: this.maxUploadSeconds !== null ? this.maxUploadSeconds * 1e3 : null
9118
+ });
9119
+ try {
9120
+ await stopped;
9121
+ await uploaded;
9122
+ displayEl.innerHTML = "";
9123
+ return {};
9124
+ } catch (err) {
9125
+ console.error(
9126
+ "TrialRecordExtension: recorder stop/upload failed.",
9127
+ err
9128
+ );
9129
+ displayEl.innerHTML = "";
9130
+ return {};
9131
+ }
9132
+ } else {
8971
9133
  displayEl.innerHTML = "";
8972
- } catch (err) {
8973
- console.error("TrialRecordExtension: recorder stop/upload failed.", err);
9134
+ return {};
8974
9135
  }
8975
- return {};
8976
9136
  }
8977
9137
  getCurrentPluginName() {
8978
9138
  const current_plugin_class = this.jsPsych.getCurrentTrial().type;
@@ -9383,4 +9543,4 @@ var chsRecord = (function (Data, chsTemplates, jspsych) {
9383
9543
  return index;
9384
9544
 
9385
9545
  })(chsData, chsTemplates, jsPsychModule);
9386
- //# sourceMappingURL=https://unpkg.com/@lookit/record@4.1.0/dist/index.browser.js.map
9546
+ //# sourceMappingURL=https://unpkg.com/@lookit/record@5.0.0/dist/index.browser.js.map