@lookit/record 4.1.0 → 6.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
@@ -5,7 +5,8 @@ declare const info$3: {
5
5
  readonly version: string;
6
6
  readonly parameters: {
7
7
  readonly template: {
8
- readonly type: ParameterType.STRING;
8
+ readonly type: ParameterType.SELECT;
9
+ readonly options: readonly ["consent-template-5", "consent-garden", "consent-recording-only"];
9
10
  readonly default: "consent-template-5";
10
11
  };
11
12
  readonly locale: {
@@ -131,6 +132,19 @@ declare const info$3: {
131
132
  readonly type: ParameterType.BOOL;
132
133
  readonly default: false;
133
134
  };
135
+ /**
136
+ * This parameter is only relevant for the consent-recording-only template.
137
+ * If a different template is used, this parameter will be ignored. Whether
138
+ * or not the consent trial is the only data being collected on CHS (i.e.
139
+ * the study redirects to an external URL immediately after the consent
140
+ * trial). If false (the default), the consent template contains information
141
+ * about how CHS handles data/responses. If true, any statements about
142
+ * session data/responses are omitted.
143
+ */
144
+ readonly only_consent_on_chs: {
145
+ readonly type: ParameterType.BOOL;
146
+ readonly default: false;
147
+ };
134
148
  };
135
149
  readonly data: {
136
150
  readonly chs_type: {
@@ -147,7 +161,8 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
147
161
  readonly version: string;
148
162
  readonly parameters: {
149
163
  readonly template: {
150
- readonly type: ParameterType.STRING;
164
+ readonly type: ParameterType.SELECT;
165
+ readonly options: readonly ["consent-template-5", "consent-garden", "consent-recording-only"];
151
166
  readonly default: "consent-template-5";
152
167
  };
153
168
  readonly locale: {
@@ -273,6 +288,19 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
273
288
  readonly type: ParameterType.BOOL;
274
289
  readonly default: false;
275
290
  };
291
+ /**
292
+ * This parameter is only relevant for the consent-recording-only template.
293
+ * If a different template is used, this parameter will be ignored. Whether
294
+ * or not the consent trial is the only data being collected on CHS (i.e.
295
+ * the study redirects to an external URL immediately after the consent
296
+ * trial). If false (the default), the consent template contains information
297
+ * about how CHS handles data/responses. If true, any statements about
298
+ * session data/responses are omitted.
299
+ */
300
+ readonly only_consent_on_chs: {
301
+ readonly type: ParameterType.BOOL;
302
+ readonly default: false;
303
+ };
276
304
  };
277
305
  readonly data: {
278
306
  readonly chs_type: {
@@ -404,7 +432,30 @@ declare class VideoConsentPlugin implements JsPsychPlugin<Info$3> {
404
432
  declare const info$2: {
405
433
  readonly name: "start-record-plugin";
406
434
  readonly version: string;
407
- readonly parameters: {};
435
+ readonly parameters: {
436
+ /**
437
+ * This string can contain HTML markup. Any content provided will be
438
+ * displayed while the video recording connection is established. If null
439
+ * (the default), then the default 'establishing video connection, please
440
+ * wait' (or appropriate translation based on 'locale') will be displayed.
441
+ * Use a blank string for no message/content.
442
+ */
443
+ readonly wait_for_connection_message: {
444
+ readonly type: ParameterType.HTML_STRING;
445
+ readonly default: string | null;
446
+ };
447
+ /**
448
+ * Locale code used for translating the default 'establishing video
449
+ * connection, please wait' message. This code must be present in the
450
+ * translation files. If the code is not found then English will be used. If
451
+ * the 'wait_for_connection_message' parameter is specified then this value
452
+ * is ignored.
453
+ */
454
+ readonly locale: {
455
+ readonly type: ParameterType.STRING;
456
+ readonly default: "en-us";
457
+ };
458
+ };
408
459
  readonly data: {};
409
460
  };
410
461
  type Info$2 = typeof info$2;
@@ -414,7 +465,30 @@ declare class StartRecordPlugin implements JsPsychPlugin<Info$2> {
414
465
  static readonly info: {
415
466
  readonly name: "start-record-plugin";
416
467
  readonly version: string;
417
- readonly parameters: {};
468
+ readonly parameters: {
469
+ /**
470
+ * This string can contain HTML markup. Any content provided will be
471
+ * displayed while the video recording connection is established. If null
472
+ * (the default), then the default 'establishing video connection, please
473
+ * wait' (or appropriate translation based on 'locale') will be displayed.
474
+ * Use a blank string for no message/content.
475
+ */
476
+ readonly wait_for_connection_message: {
477
+ readonly type: ParameterType.HTML_STRING;
478
+ readonly default: string | null;
479
+ };
480
+ /**
481
+ * Locale code used for translating the default 'establishing video
482
+ * connection, please wait' message. This code must be present in the
483
+ * translation files. If the code is not found then English will be used. If
484
+ * the 'wait_for_connection_message' parameter is specified then this value
485
+ * is ignored.
486
+ */
487
+ readonly locale: {
488
+ readonly type: ParameterType.STRING;
489
+ readonly default: "en-us";
490
+ };
491
+ };
418
492
  readonly data: {};
419
493
  };
420
494
  private recorder;
@@ -424,8 +498,15 @@ declare class StartRecordPlugin implements JsPsychPlugin<Info$2> {
424
498
  * @param jsPsych - Object provided by jsPsych.
425
499
  */
426
500
  constructor(jsPsych: JsPsych);
427
- /** Trial function called by jsPsych. */
428
- trial(): void;
501
+ /**
502
+ * Trial function called by jsPsych.
503
+ *
504
+ * @param display_element - DOM element where jsPsych content is being
505
+ * rendered (set in initJsPsych and automatically made available to a
506
+ * plugin's trial method via jsPsych core).
507
+ * @param trial - Trial object with parameters/values.
508
+ */
509
+ trial(display_element: HTMLElement, trial: TrialType<Info$2>): Promise<void>;
429
510
  }
430
511
 
431
512
  declare const info$1: {
@@ -454,6 +535,14 @@ declare const info$1: {
454
535
  readonly type: ParameterType.STRING;
455
536
  readonly default: "en-us";
456
537
  };
538
+ /**
539
+ * Maximum duration (in seconds) to wait for the session recording to finish
540
+ * uploading before continuing with the experiment.
541
+ */
542
+ readonly max_upload_seconds: {
543
+ readonly type: ParameterType.INT;
544
+ readonly default: 10;
545
+ };
457
546
  };
458
547
  readonly data: {};
459
548
  };
@@ -487,6 +576,14 @@ declare class StopRecordPlugin implements JsPsychPlugin<Info$1> {
487
576
  readonly type: ParameterType.STRING;
488
577
  readonly default: "en-us";
489
578
  };
579
+ /**
580
+ * Maximum duration (in seconds) to wait for the session recording to finish
581
+ * uploading before continuing with the experiment.
582
+ */
583
+ readonly max_upload_seconds: {
584
+ readonly type: ParameterType.INT;
585
+ readonly default: 10;
586
+ };
490
587
  };
491
588
  readonly data: {};
492
589
  };
@@ -505,7 +602,7 @@ declare class StopRecordPlugin implements JsPsychPlugin<Info$1> {
505
602
  * plugin's trial method via jsPsych core).
506
603
  * @param trial - Trial object with parameters/values.
507
604
  */
508
- trial(display_element: HTMLElement, trial: TrialType<Info$1>): void;
605
+ trial(display_element: HTMLElement, trial: TrialType<Info$1>): Promise<void>;
509
606
  }
510
607
 
511
608
  interface Parameters {
@@ -526,12 +623,18 @@ interface Parameters {
526
623
  * Locale code used for translating the default 'uploading video, please
527
624
  * wait...' message. This code must be present in the translation files. If
528
625
  * 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.
626
+ * 'wait_for_upload_message' parameter is specified then this value is
627
+ * ignored.
531
628
  *
532
629
  * @default "en-us"
533
630
  */
534
631
  locale?: string;
632
+ /**
633
+ * Maximum duration (in seconds) to wait for the trial recording to finish
634
+ * uploading before continuing with the experiment. Default is 10 seconds (set
635
+ * during initialize).
636
+ */
637
+ max_upload_seconds?: null | number;
535
638
  }
536
639
  /** This extension allows researchers to record webcam audio/video during trials. */
537
640
  declare class TrialRecordExtension implements JsPsychExtension {
@@ -541,6 +644,7 @@ declare class TrialRecordExtension implements JsPsychExtension {
541
644
  private pluginName;
542
645
  private uploadMsg;
543
646
  private locale;
647
+ private maxUploadSeconds;
544
648
  /**
545
649
  * Video recording extension.
546
650
  *
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: "4.1.0",
9
+ version: "6.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: {
@@ -47,8 +47,8 @@ var _package = {
47
47
  typescript: "^5.6.2"
48
48
  },
49
49
  peerDependencies: {
50
- "@lookit/data": "^0.2.0",
51
- "@lookit/templates": "^2.1.0",
50
+ "@lookit/data": "^0.3.0",
51
+ "@lookit/templates": "^3.1.0",
52
52
  jspsych: "^8.0.3"
53
53
  }
54
54
  };
@@ -123,6 +123,12 @@ class S3UndefinedError extends Error {
123
123
  this.name = "S3UndefinedError";
124
124
  }
125
125
  }
126
+ class NoFileNameError extends Error {
127
+ constructor() {
128
+ super("No filename found for recording.");
129
+ this.name = "NoFileNameError";
130
+ }
131
+ }
126
132
  class StreamActiveOnResetError extends Error {
127
133
  constructor() {
128
134
  super("Won't reset recorder. Stream is still active.");
@@ -147,6 +153,12 @@ class ElementNotFoundError extends Error {
147
153
  this.name = "ElementNotFoundError";
148
154
  }
149
155
  }
156
+ class TimeoutError extends Error {
157
+ constructor(msg) {
158
+ super(`${msg}`);
159
+ this.name = "TimeoutError";
160
+ }
161
+ }
150
162
 
151
163
  var playbackFeed = "<video\n autoplay\n playsinline\n src=\"{{{src}}}\"\n class=\"webcam-feed\"\n id=\"{{webcam_element_id}}\"\n width=\"{{width}}\"\n height=\"{{height}}\"\n controls\n></video>";
152
164
 
@@ -158,6 +170,31 @@ var img$8 = "data:image/svg+xml,%3c%3fxml version='1.0' encoding='utf-8' %3f%3e%
158
170
 
159
171
  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";
160
172
 
173
+ const promiseWithTimeout = (promise, promiseId, timeoutMs, onTimeoutCleanup) => {
174
+ let timeoutHandle;
175
+ const timeout = new Promise((resolve) => {
176
+ timeoutHandle = setTimeout(() => {
177
+ onTimeoutCleanup?.();
178
+ resolve("timeout");
179
+ }, timeoutMs);
180
+ });
181
+ return Promise.race([promise, timeout]).then(
182
+ (value) => {
183
+ if (value == "timeout") {
184
+ console.log(`Upload for ${promiseId} timed out.`);
185
+ } else {
186
+ console.log(`Upload for ${promiseId} completed.`);
187
+ clearTimeout(timeoutHandle);
188
+ }
189
+ return value;
190
+ },
191
+ (err) => {
192
+ clearTimeout(timeoutHandle);
193
+ throw err;
194
+ }
195
+ );
196
+ };
197
+
161
198
  class Recorder {
162
199
  constructor(jsPsych) {
163
200
  this.jsPsych = jsPsych;
@@ -257,13 +294,73 @@ class Recorder {
257
294
  this.recorder.stop();
258
295
  this.stream.getTracks().map((t) => t.stop());
259
296
  }
260
- stop(maintain_container_size = false) {
297
+ stop({
298
+ maintain_container_size = false,
299
+ stop_timeout_ms = null,
300
+ upload_timeout_ms = 1e4
301
+ } = {}) {
302
+ this.preStopCheck();
261
303
  this.clearWebcamFeed(maintain_container_size);
262
304
  this.stopTracks();
263
- if (!this.stopPromise) {
264
- throw new NoStopPromiseError();
265
- }
266
- return this.stopPromise;
305
+ const snapshot = {
306
+ s3: !this.localDownload ? this.s3 : null,
307
+ filename: this.filename,
308
+ localDownload: this.localDownload,
309
+ url: "null"
310
+ };
311
+ const stopped = stop_timeout_ms ? promiseWithTimeout(
312
+ this.stopPromise,
313
+ `${snapshot.filename}-stopped`,
314
+ stop_timeout_ms,
315
+ this.createTimeoutHandler("stop", snapshot.filename)
316
+ ) : this.stopPromise;
317
+ stopped.finally(() => {
318
+ try {
319
+ this.reset();
320
+ } catch (err) {
321
+ console.error("Error while resetting recorder after stop: ", err);
322
+ }
323
+ });
324
+ const uploadPromise = (async () => {
325
+ let url;
326
+ try {
327
+ url = await stopped;
328
+ if (url == "timeout") {
329
+ throw new TimeoutError("Recorder stop timed out.");
330
+ }
331
+ } catch (err) {
332
+ console.warn("Upload failed because recorder stop timed out");
333
+ throw err;
334
+ }
335
+ snapshot.url = url;
336
+ if (snapshot.localDownload) {
337
+ try {
338
+ this.download(snapshot.filename, snapshot.url);
339
+ await Promise.resolve();
340
+ } catch (err) {
341
+ console.error("Local download failed: ", err);
342
+ throw err;
343
+ }
344
+ } else {
345
+ try {
346
+ await snapshot.s3.completeUpload();
347
+ } catch (err) {
348
+ console.error("Upload failed: ", err);
349
+ throw err;
350
+ }
351
+ }
352
+ })();
353
+ const uploaded = upload_timeout_ms ? promiseWithTimeout(
354
+ uploadPromise,
355
+ `${snapshot.filename}-uploaded`,
356
+ upload_timeout_ms,
357
+ this.createTimeoutHandler("upload", snapshot.filename)
358
+ ) : uploadPromise;
359
+ window.chs.pendingUploads.push({
360
+ promise: uploadPromise,
361
+ file: snapshot.filename
362
+ });
363
+ return { stopped, uploaded };
267
364
  }
268
365
  initializeCheck() {
269
366
  if (!this.recorder) {
@@ -276,19 +373,27 @@ class Recorder {
276
373
  throw new StreamDataInitializeError();
277
374
  }
278
375
  }
376
+ preStopCheck() {
377
+ if (!this.recorder) {
378
+ throw new RecorderInitializeError();
379
+ }
380
+ if (!this.stream.active) {
381
+ throw new StreamInactiveInitializeError();
382
+ }
383
+ if (!this.stopPromise) {
384
+ throw new NoStopPromiseError();
385
+ }
386
+ if (!this.filename) {
387
+ throw new NoFileNameError();
388
+ }
389
+ }
279
390
  handleStop(resolve) {
280
- return async () => {
391
+ return () => {
281
392
  if (this.blobs.length === 0) {
282
393
  throw new CreateURLError();
283
394
  }
284
395
  this.url = URL.createObjectURL(new Blob(this.blobs));
285
- if (this.localDownload) {
286
- this.download();
287
- } else {
288
- await this.s3.completeUpload();
289
- }
290
- this.reset();
291
- resolve();
396
+ resolve(this.url);
292
397
  };
293
398
  }
294
399
  handleDataAvailable(event) {
@@ -297,11 +402,11 @@ class Recorder {
297
402
  this.s3.onDataAvailable(event.data);
298
403
  }
299
404
  }
300
- download() {
301
- if (this.filename && this.url) {
405
+ download(filename, url) {
406
+ if (filename && url) {
302
407
  const link = document.createElement("a");
303
- link.href = this.url;
304
- link.download = this.filename;
408
+ link.href = url;
409
+ link.download = filename;
305
410
  link.click();
306
411
  }
307
412
  }
@@ -330,13 +435,33 @@ class Recorder {
330
435
  const rand_digits = Math.floor(Math.random() * 1e3);
331
436
  return `${prefix}_${window.chs.study.id}_${trial_id}_${window.chs.response.id}_${new Date().getTime()}_${rand_digits}.webm`;
332
437
  }
438
+ createTimeoutHandler(eventName, id) {
439
+ return () => {
440
+ console.warn(`Recorder ${eventName} timed out: ${id}`);
441
+ if (!this.stream.active) {
442
+ try {
443
+ this.reset();
444
+ } catch (err) {
445
+ console.error("Error while resetting recorder after timeout: ", err);
446
+ }
447
+ }
448
+ };
449
+ }
333
450
  }
334
451
 
335
452
  const info$3 = {
336
453
  name: "consent-video",
337
454
  version: _package.version,
338
455
  parameters: {
339
- template: { type: ParameterType.STRING, default: "consent-template-5" },
456
+ template: {
457
+ type: ParameterType.SELECT,
458
+ options: [
459
+ "consent-template-5",
460
+ "consent-garden",
461
+ "consent-recording-only"
462
+ ],
463
+ default: "consent-template-5"
464
+ },
340
465
  locale: { type: ParameterType.STRING, default: "en-us" },
341
466
  additional_video_privacy_statement: {
342
467
  type: ParameterType.STRING,
@@ -381,7 +506,8 @@ const info$3 = {
381
506
  prompt_all_adults: { type: ParameterType.BOOL, default: false },
382
507
  prompt_only_adults: { type: ParameterType.BOOL, default: false },
383
508
  consent_statement_text: { type: ParameterType.STRING, default: "" },
384
- omit_injury_phrase: { type: ParameterType.BOOL, default: false }
509
+ omit_injury_phrase: { type: ParameterType.BOOL, default: false },
510
+ only_consent_on_chs: { type: ParameterType.BOOL, default: false }
385
511
  },
386
512
  data: {
387
513
  chs_type: {
@@ -514,7 +640,10 @@ const _VideoConsentPlugin = class {
514
640
  stop.addEventListener("click", async () => {
515
641
  stop.disabled = true;
516
642
  this.addMessage(display, this.uploadingMsg);
517
- await this.recorder.stop(true);
643
+ const { stopped, uploaded } = this.recorder.stop({
644
+ maintain_container_size: true
645
+ });
646
+ await stopped;
518
647
  this.recordFeed(display);
519
648
  this.getImg(display, "record-icon").style.visibility = "hidden";
520
649
  this.addMessage(display, this.notRecordingMsg);
@@ -542,7 +671,16 @@ VideoConsentPlugin.info = info$3;
542
671
  const info$2 = {
543
672
  name: "start-record-plugin",
544
673
  version: _package.version,
545
- parameters: {},
674
+ parameters: {
675
+ wait_for_connection_message: {
676
+ type: ParameterType.HTML_STRING,
677
+ default: null
678
+ },
679
+ locale: {
680
+ type: ParameterType.STRING,
681
+ default: "en-us"
682
+ }
683
+ },
546
684
  data: {}
547
685
  };
548
686
  const _StartRecordPlugin = class {
@@ -555,8 +693,14 @@ const _StartRecordPlugin = class {
555
693
  throw new ExistingRecordingError();
556
694
  }
557
695
  }
558
- trial() {
559
- this.recorder.start(false, `${_StartRecordPlugin.info.name}-multiframe`).then(() => {
696
+ async trial(display_element, trial) {
697
+ if (trial.wait_for_connection_message == null) {
698
+ display_element.innerHTML = chsTemplates.establishingConnection(trial);
699
+ } else {
700
+ display_element.innerHTML = trial.wait_for_connection_message;
701
+ }
702
+ await this.recorder.start(false, `${_StartRecordPlugin.info.name}-multiframe`).then(() => {
703
+ display_element.innerHTML = "";
560
704
  this.jsPsych.finishTrial();
561
705
  });
562
706
  }
@@ -575,6 +719,10 @@ const info$1 = {
575
719
  locale: {
576
720
  type: ParameterType.STRING,
577
721
  default: "en-us"
722
+ },
723
+ max_upload_seconds: {
724
+ type: ParameterType.INT,
725
+ default: 10
578
726
  }
579
727
  },
580
728
  data: {}
@@ -588,19 +736,25 @@ class StopRecordPlugin {
588
736
  throw new NoSessionRecordingError();
589
737
  }
590
738
  }
591
- trial(display_element, trial) {
739
+ async trial(display_element, trial) {
592
740
  if (trial.wait_for_upload_message == null) {
593
741
  display_element.innerHTML = chsTemplates.uploadingVideo(trial);
594
742
  } else {
595
743
  display_element.innerHTML = trial.wait_for_upload_message;
596
744
  }
597
- this.recorder.stop().then(() => {
745
+ const { stopped, uploaded } = this.recorder.stop({
746
+ upload_timeout_ms: trial.max_upload_seconds !== null ? trial.max_upload_seconds * 1e3 : null
747
+ });
748
+ try {
749
+ await stopped;
750
+ await uploaded;
751
+ } catch (err) {
752
+ console.error("StopRecordPlugin: recorder stop/upload failed.", err);
753
+ } finally {
598
754
  window.chs.sessionRecorder = null;
599
755
  display_element.innerHTML = "";
600
756
  this.jsPsych.finishTrial();
601
- }).catch((err) => {
602
- console.error("StopRecordPlugin: recorder stop/upload failed.", err);
603
- });
757
+ }
604
758
  }
605
759
  }
606
760
  StopRecordPlugin.info = info$1;
@@ -610,14 +764,14 @@ class TrialRecordExtension {
610
764
  this.jsPsych = jsPsych;
611
765
  this.uploadMsg = null;
612
766
  this.locale = "en-us";
767
+ this.maxUploadSeconds = void 0;
613
768
  autoBind(this);
614
769
  }
615
770
  async initialize(params) {
616
771
  await new Promise((resolve) => {
617
772
  this.uploadMsg = params?.wait_for_upload_message ? params.wait_for_upload_message : null;
618
773
  this.locale = params?.locale ? params.locale : "en-us";
619
- console.log(this.uploadMsg);
620
- console.log(this.locale);
774
+ this.maxUploadSeconds = params?.max_upload_seconds === void 0 ? 10 : params.max_upload_seconds;
621
775
  resolve();
622
776
  });
623
777
  }
@@ -628,13 +782,14 @@ class TrialRecordExtension {
628
782
  if (startParams?.locale) {
629
783
  this.locale = startParams.locale;
630
784
  }
631
- console.log(this.uploadMsg);
632
- console.log(this.locale);
785
+ if (startParams?.max_upload_seconds !== void 0) {
786
+ this.maxUploadSeconds = startParams?.max_upload_seconds;
787
+ }
633
788
  this.recorder = new Recorder(this.jsPsych);
789
+ this.pluginName = this.getCurrentPluginName();
790
+ this.recorder.start(false, `${this.pluginName}`);
634
791
  }
635
792
  on_load() {
636
- this.pluginName = this.getCurrentPluginName();
637
- this.recorder?.start(false, `${this.pluginName}`);
638
793
  }
639
794
  async on_finish() {
640
795
  const displayEl = this.jsPsych.getDisplayElement();
@@ -646,13 +801,27 @@ class TrialRecordExtension {
646
801
  } else {
647
802
  displayEl.innerHTML = this.uploadMsg;
648
803
  }
649
- try {
650
- await this.recorder?.stop();
804
+ if (this.recorder) {
805
+ const { stopped, uploaded } = this.recorder.stop({
806
+ upload_timeout_ms: this.maxUploadSeconds !== null ? this.maxUploadSeconds * 1e3 : null
807
+ });
808
+ try {
809
+ await stopped;
810
+ await uploaded;
811
+ displayEl.innerHTML = "";
812
+ return {};
813
+ } catch (err) {
814
+ console.error(
815
+ "TrialRecordExtension: recorder stop/upload failed.",
816
+ err
817
+ );
818
+ displayEl.innerHTML = "";
819
+ return {};
820
+ }
821
+ } else {
651
822
  displayEl.innerHTML = "";
652
- } catch (err) {
653
- console.error("TrialRecordExtension: recorder stop/upload failed.", err);
823
+ return {};
654
824
  }
655
- return {};
656
825
  }
657
826
  getCurrentPluginName() {
658
827
  const current_plugin_class = this.jsPsych.getCurrentTrial().type;