@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/README.md +158 -15
- package/dist/index.browser.js +212 -43
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +16 -16
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +211 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +113 -9
- package/dist/index.js +211 -42
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/consentVideo.spec.ts +9 -0
- package/src/consentVideo.ts +24 -2
- package/src/errors.ts +28 -0
- package/src/index.spec.ts +497 -54
- package/src/recorder.spec.ts +669 -109
- package/src/recorder.ts +184 -36
- package/src/start.ts +45 -5
- package/src/stop.ts +29 -12
- package/src/trial.ts +42 -16
- package/src/types.ts +21 -0
- package/src/utils.spec.ts +129 -0
- package/src/utils.ts +45 -0
package/dist/index.cjs
CHANGED
|
@@ -8,7 +8,7 @@ var Handlebars = require('handlebars');
|
|
|
8
8
|
|
|
9
9
|
var _package = {
|
|
10
10
|
name: "@lookit/record",
|
|
11
|
-
version: "
|
|
11
|
+
version: "6.0.0",
|
|
12
12
|
description: "Recording extensions and plugins for CHS studies.",
|
|
13
13
|
homepage: "https://github.com/lookit/lookit-jspsych#readme",
|
|
14
14
|
bugs: {
|
|
@@ -49,8 +49,8 @@ var _package = {
|
|
|
49
49
|
typescript: "^5.6.2"
|
|
50
50
|
},
|
|
51
51
|
peerDependencies: {
|
|
52
|
-
"@lookit/data": "^0.
|
|
53
|
-
"@lookit/templates": "^
|
|
52
|
+
"@lookit/data": "^0.3.0",
|
|
53
|
+
"@lookit/templates": "^3.1.0",
|
|
54
54
|
jspsych: "^8.0.3"
|
|
55
55
|
}
|
|
56
56
|
};
|
|
@@ -125,6 +125,12 @@ class S3UndefinedError extends Error {
|
|
|
125
125
|
this.name = "S3UndefinedError";
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
+
class NoFileNameError extends Error {
|
|
129
|
+
constructor() {
|
|
130
|
+
super("No filename found for recording.");
|
|
131
|
+
this.name = "NoFileNameError";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
128
134
|
class StreamActiveOnResetError extends Error {
|
|
129
135
|
constructor() {
|
|
130
136
|
super("Won't reset recorder. Stream is still active.");
|
|
@@ -149,6 +155,12 @@ class ElementNotFoundError extends Error {
|
|
|
149
155
|
this.name = "ElementNotFoundError";
|
|
150
156
|
}
|
|
151
157
|
}
|
|
158
|
+
class TimeoutError extends Error {
|
|
159
|
+
constructor(msg) {
|
|
160
|
+
super(`${msg}`);
|
|
161
|
+
this.name = "TimeoutError";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
152
164
|
|
|
153
165
|
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>";
|
|
154
166
|
|
|
@@ -160,6 +172,31 @@ var img$8 = "data:image/svg+xml,%3c%3fxml version='1.0' encoding='utf-8' %3f%3e%
|
|
|
160
172
|
|
|
161
173
|
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";
|
|
162
174
|
|
|
175
|
+
const promiseWithTimeout = (promise, promiseId, timeoutMs, onTimeoutCleanup) => {
|
|
176
|
+
let timeoutHandle;
|
|
177
|
+
const timeout = new Promise((resolve) => {
|
|
178
|
+
timeoutHandle = setTimeout(() => {
|
|
179
|
+
onTimeoutCleanup?.();
|
|
180
|
+
resolve("timeout");
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
});
|
|
183
|
+
return Promise.race([promise, timeout]).then(
|
|
184
|
+
(value) => {
|
|
185
|
+
if (value == "timeout") {
|
|
186
|
+
console.log(`Upload for ${promiseId} timed out.`);
|
|
187
|
+
} else {
|
|
188
|
+
console.log(`Upload for ${promiseId} completed.`);
|
|
189
|
+
clearTimeout(timeoutHandle);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
},
|
|
193
|
+
(err) => {
|
|
194
|
+
clearTimeout(timeoutHandle);
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
163
200
|
class Recorder {
|
|
164
201
|
constructor(jsPsych) {
|
|
165
202
|
this.jsPsych = jsPsych;
|
|
@@ -259,13 +296,73 @@ class Recorder {
|
|
|
259
296
|
this.recorder.stop();
|
|
260
297
|
this.stream.getTracks().map((t) => t.stop());
|
|
261
298
|
}
|
|
262
|
-
stop(
|
|
299
|
+
stop({
|
|
300
|
+
maintain_container_size = false,
|
|
301
|
+
stop_timeout_ms = null,
|
|
302
|
+
upload_timeout_ms = 1e4
|
|
303
|
+
} = {}) {
|
|
304
|
+
this.preStopCheck();
|
|
263
305
|
this.clearWebcamFeed(maintain_container_size);
|
|
264
306
|
this.stopTracks();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
307
|
+
const snapshot = {
|
|
308
|
+
s3: !this.localDownload ? this.s3 : null,
|
|
309
|
+
filename: this.filename,
|
|
310
|
+
localDownload: this.localDownload,
|
|
311
|
+
url: "null"
|
|
312
|
+
};
|
|
313
|
+
const stopped = stop_timeout_ms ? promiseWithTimeout(
|
|
314
|
+
this.stopPromise,
|
|
315
|
+
`${snapshot.filename}-stopped`,
|
|
316
|
+
stop_timeout_ms,
|
|
317
|
+
this.createTimeoutHandler("stop", snapshot.filename)
|
|
318
|
+
) : this.stopPromise;
|
|
319
|
+
stopped.finally(() => {
|
|
320
|
+
try {
|
|
321
|
+
this.reset();
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error("Error while resetting recorder after stop: ", err);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
const uploadPromise = (async () => {
|
|
327
|
+
let url;
|
|
328
|
+
try {
|
|
329
|
+
url = await stopped;
|
|
330
|
+
if (url == "timeout") {
|
|
331
|
+
throw new TimeoutError("Recorder stop timed out.");
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.warn("Upload failed because recorder stop timed out");
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
snapshot.url = url;
|
|
338
|
+
if (snapshot.localDownload) {
|
|
339
|
+
try {
|
|
340
|
+
this.download(snapshot.filename, snapshot.url);
|
|
341
|
+
await Promise.resolve();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.error("Local download failed: ", err);
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
try {
|
|
348
|
+
await snapshot.s3.completeUpload();
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error("Upload failed: ", err);
|
|
351
|
+
throw err;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
const uploaded = upload_timeout_ms ? promiseWithTimeout(
|
|
356
|
+
uploadPromise,
|
|
357
|
+
`${snapshot.filename}-uploaded`,
|
|
358
|
+
upload_timeout_ms,
|
|
359
|
+
this.createTimeoutHandler("upload", snapshot.filename)
|
|
360
|
+
) : uploadPromise;
|
|
361
|
+
window.chs.pendingUploads.push({
|
|
362
|
+
promise: uploadPromise,
|
|
363
|
+
file: snapshot.filename
|
|
364
|
+
});
|
|
365
|
+
return { stopped, uploaded };
|
|
269
366
|
}
|
|
270
367
|
initializeCheck() {
|
|
271
368
|
if (!this.recorder) {
|
|
@@ -278,19 +375,27 @@ class Recorder {
|
|
|
278
375
|
throw new StreamDataInitializeError();
|
|
279
376
|
}
|
|
280
377
|
}
|
|
378
|
+
preStopCheck() {
|
|
379
|
+
if (!this.recorder) {
|
|
380
|
+
throw new RecorderInitializeError();
|
|
381
|
+
}
|
|
382
|
+
if (!this.stream.active) {
|
|
383
|
+
throw new StreamInactiveInitializeError();
|
|
384
|
+
}
|
|
385
|
+
if (!this.stopPromise) {
|
|
386
|
+
throw new NoStopPromiseError();
|
|
387
|
+
}
|
|
388
|
+
if (!this.filename) {
|
|
389
|
+
throw new NoFileNameError();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
281
392
|
handleStop(resolve) {
|
|
282
|
-
return
|
|
393
|
+
return () => {
|
|
283
394
|
if (this.blobs.length === 0) {
|
|
284
395
|
throw new CreateURLError();
|
|
285
396
|
}
|
|
286
397
|
this.url = URL.createObjectURL(new Blob(this.blobs));
|
|
287
|
-
|
|
288
|
-
this.download();
|
|
289
|
-
} else {
|
|
290
|
-
await this.s3.completeUpload();
|
|
291
|
-
}
|
|
292
|
-
this.reset();
|
|
293
|
-
resolve();
|
|
398
|
+
resolve(this.url);
|
|
294
399
|
};
|
|
295
400
|
}
|
|
296
401
|
handleDataAvailable(event) {
|
|
@@ -299,11 +404,11 @@ class Recorder {
|
|
|
299
404
|
this.s3.onDataAvailable(event.data);
|
|
300
405
|
}
|
|
301
406
|
}
|
|
302
|
-
download() {
|
|
303
|
-
if (
|
|
407
|
+
download(filename, url) {
|
|
408
|
+
if (filename && url) {
|
|
304
409
|
const link = document.createElement("a");
|
|
305
|
-
link.href =
|
|
306
|
-
link.download =
|
|
410
|
+
link.href = url;
|
|
411
|
+
link.download = filename;
|
|
307
412
|
link.click();
|
|
308
413
|
}
|
|
309
414
|
}
|
|
@@ -332,13 +437,33 @@ class Recorder {
|
|
|
332
437
|
const rand_digits = Math.floor(Math.random() * 1e3);
|
|
333
438
|
return `${prefix}_${window.chs.study.id}_${trial_id}_${window.chs.response.id}_${new Date().getTime()}_${rand_digits}.webm`;
|
|
334
439
|
}
|
|
440
|
+
createTimeoutHandler(eventName, id) {
|
|
441
|
+
return () => {
|
|
442
|
+
console.warn(`Recorder ${eventName} timed out: ${id}`);
|
|
443
|
+
if (!this.stream.active) {
|
|
444
|
+
try {
|
|
445
|
+
this.reset();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
console.error("Error while resetting recorder after timeout: ", err);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
335
452
|
}
|
|
336
453
|
|
|
337
454
|
const info$3 = {
|
|
338
455
|
name: "consent-video",
|
|
339
456
|
version: _package.version,
|
|
340
457
|
parameters: {
|
|
341
|
-
template: {
|
|
458
|
+
template: {
|
|
459
|
+
type: jspsych.ParameterType.SELECT,
|
|
460
|
+
options: [
|
|
461
|
+
"consent-template-5",
|
|
462
|
+
"consent-garden",
|
|
463
|
+
"consent-recording-only"
|
|
464
|
+
],
|
|
465
|
+
default: "consent-template-5"
|
|
466
|
+
},
|
|
342
467
|
locale: { type: jspsych.ParameterType.STRING, default: "en-us" },
|
|
343
468
|
additional_video_privacy_statement: {
|
|
344
469
|
type: jspsych.ParameterType.STRING,
|
|
@@ -383,7 +508,8 @@ const info$3 = {
|
|
|
383
508
|
prompt_all_adults: { type: jspsych.ParameterType.BOOL, default: false },
|
|
384
509
|
prompt_only_adults: { type: jspsych.ParameterType.BOOL, default: false },
|
|
385
510
|
consent_statement_text: { type: jspsych.ParameterType.STRING, default: "" },
|
|
386
|
-
omit_injury_phrase: { type: jspsych.ParameterType.BOOL, default: false }
|
|
511
|
+
omit_injury_phrase: { type: jspsych.ParameterType.BOOL, default: false },
|
|
512
|
+
only_consent_on_chs: { type: jspsych.ParameterType.BOOL, default: false }
|
|
387
513
|
},
|
|
388
514
|
data: {
|
|
389
515
|
chs_type: {
|
|
@@ -516,7 +642,10 @@ const _VideoConsentPlugin = class {
|
|
|
516
642
|
stop.addEventListener("click", async () => {
|
|
517
643
|
stop.disabled = true;
|
|
518
644
|
this.addMessage(display, this.uploadingMsg);
|
|
519
|
-
|
|
645
|
+
const { stopped, uploaded } = this.recorder.stop({
|
|
646
|
+
maintain_container_size: true
|
|
647
|
+
});
|
|
648
|
+
await stopped;
|
|
520
649
|
this.recordFeed(display);
|
|
521
650
|
this.getImg(display, "record-icon").style.visibility = "hidden";
|
|
522
651
|
this.addMessage(display, this.notRecordingMsg);
|
|
@@ -544,7 +673,16 @@ VideoConsentPlugin.info = info$3;
|
|
|
544
673
|
const info$2 = {
|
|
545
674
|
name: "start-record-plugin",
|
|
546
675
|
version: _package.version,
|
|
547
|
-
parameters: {
|
|
676
|
+
parameters: {
|
|
677
|
+
wait_for_connection_message: {
|
|
678
|
+
type: jspsych.ParameterType.HTML_STRING,
|
|
679
|
+
default: null
|
|
680
|
+
},
|
|
681
|
+
locale: {
|
|
682
|
+
type: jspsych.ParameterType.STRING,
|
|
683
|
+
default: "en-us"
|
|
684
|
+
}
|
|
685
|
+
},
|
|
548
686
|
data: {}
|
|
549
687
|
};
|
|
550
688
|
const _StartRecordPlugin = class {
|
|
@@ -557,8 +695,14 @@ const _StartRecordPlugin = class {
|
|
|
557
695
|
throw new ExistingRecordingError();
|
|
558
696
|
}
|
|
559
697
|
}
|
|
560
|
-
trial() {
|
|
561
|
-
|
|
698
|
+
async trial(display_element, trial) {
|
|
699
|
+
if (trial.wait_for_connection_message == null) {
|
|
700
|
+
display_element.innerHTML = chsTemplates.establishingConnection(trial);
|
|
701
|
+
} else {
|
|
702
|
+
display_element.innerHTML = trial.wait_for_connection_message;
|
|
703
|
+
}
|
|
704
|
+
await this.recorder.start(false, `${_StartRecordPlugin.info.name}-multiframe`).then(() => {
|
|
705
|
+
display_element.innerHTML = "";
|
|
562
706
|
this.jsPsych.finishTrial();
|
|
563
707
|
});
|
|
564
708
|
}
|
|
@@ -577,6 +721,10 @@ const info$1 = {
|
|
|
577
721
|
locale: {
|
|
578
722
|
type: jspsych.ParameterType.STRING,
|
|
579
723
|
default: "en-us"
|
|
724
|
+
},
|
|
725
|
+
max_upload_seconds: {
|
|
726
|
+
type: jspsych.ParameterType.INT,
|
|
727
|
+
default: 10
|
|
580
728
|
}
|
|
581
729
|
},
|
|
582
730
|
data: {}
|
|
@@ -590,19 +738,25 @@ class StopRecordPlugin {
|
|
|
590
738
|
throw new NoSessionRecordingError();
|
|
591
739
|
}
|
|
592
740
|
}
|
|
593
|
-
trial(display_element, trial) {
|
|
741
|
+
async trial(display_element, trial) {
|
|
594
742
|
if (trial.wait_for_upload_message == null) {
|
|
595
743
|
display_element.innerHTML = chsTemplates.uploadingVideo(trial);
|
|
596
744
|
} else {
|
|
597
745
|
display_element.innerHTML = trial.wait_for_upload_message;
|
|
598
746
|
}
|
|
599
|
-
this.recorder.stop(
|
|
747
|
+
const { stopped, uploaded } = this.recorder.stop({
|
|
748
|
+
upload_timeout_ms: trial.max_upload_seconds !== null ? trial.max_upload_seconds * 1e3 : null
|
|
749
|
+
});
|
|
750
|
+
try {
|
|
751
|
+
await stopped;
|
|
752
|
+
await uploaded;
|
|
753
|
+
} catch (err) {
|
|
754
|
+
console.error("StopRecordPlugin: recorder stop/upload failed.", err);
|
|
755
|
+
} finally {
|
|
600
756
|
window.chs.sessionRecorder = null;
|
|
601
757
|
display_element.innerHTML = "";
|
|
602
758
|
this.jsPsych.finishTrial();
|
|
603
|
-
}
|
|
604
|
-
console.error("StopRecordPlugin: recorder stop/upload failed.", err);
|
|
605
|
-
});
|
|
759
|
+
}
|
|
606
760
|
}
|
|
607
761
|
}
|
|
608
762
|
StopRecordPlugin.info = info$1;
|
|
@@ -612,14 +766,14 @@ class TrialRecordExtension {
|
|
|
612
766
|
this.jsPsych = jsPsych;
|
|
613
767
|
this.uploadMsg = null;
|
|
614
768
|
this.locale = "en-us";
|
|
769
|
+
this.maxUploadSeconds = void 0;
|
|
615
770
|
autoBind(this);
|
|
616
771
|
}
|
|
617
772
|
async initialize(params) {
|
|
618
773
|
await new Promise((resolve) => {
|
|
619
774
|
this.uploadMsg = params?.wait_for_upload_message ? params.wait_for_upload_message : null;
|
|
620
775
|
this.locale = params?.locale ? params.locale : "en-us";
|
|
621
|
-
|
|
622
|
-
console.log(this.locale);
|
|
776
|
+
this.maxUploadSeconds = params?.max_upload_seconds === void 0 ? 10 : params.max_upload_seconds;
|
|
623
777
|
resolve();
|
|
624
778
|
});
|
|
625
779
|
}
|
|
@@ -630,13 +784,14 @@ class TrialRecordExtension {
|
|
|
630
784
|
if (startParams?.locale) {
|
|
631
785
|
this.locale = startParams.locale;
|
|
632
786
|
}
|
|
633
|
-
|
|
634
|
-
|
|
787
|
+
if (startParams?.max_upload_seconds !== void 0) {
|
|
788
|
+
this.maxUploadSeconds = startParams?.max_upload_seconds;
|
|
789
|
+
}
|
|
635
790
|
this.recorder = new Recorder(this.jsPsych);
|
|
791
|
+
this.pluginName = this.getCurrentPluginName();
|
|
792
|
+
this.recorder.start(false, `${this.pluginName}`);
|
|
636
793
|
}
|
|
637
794
|
on_load() {
|
|
638
|
-
this.pluginName = this.getCurrentPluginName();
|
|
639
|
-
this.recorder?.start(false, `${this.pluginName}`);
|
|
640
795
|
}
|
|
641
796
|
async on_finish() {
|
|
642
797
|
const displayEl = this.jsPsych.getDisplayElement();
|
|
@@ -648,13 +803,27 @@ class TrialRecordExtension {
|
|
|
648
803
|
} else {
|
|
649
804
|
displayEl.innerHTML = this.uploadMsg;
|
|
650
805
|
}
|
|
651
|
-
|
|
652
|
-
|
|
806
|
+
if (this.recorder) {
|
|
807
|
+
const { stopped, uploaded } = this.recorder.stop({
|
|
808
|
+
upload_timeout_ms: this.maxUploadSeconds !== null ? this.maxUploadSeconds * 1e3 : null
|
|
809
|
+
});
|
|
810
|
+
try {
|
|
811
|
+
await stopped;
|
|
812
|
+
await uploaded;
|
|
813
|
+
displayEl.innerHTML = "";
|
|
814
|
+
return {};
|
|
815
|
+
} catch (err) {
|
|
816
|
+
console.error(
|
|
817
|
+
"TrialRecordExtension: recorder stop/upload failed.",
|
|
818
|
+
err
|
|
819
|
+
);
|
|
820
|
+
displayEl.innerHTML = "";
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
653
824
|
displayEl.innerHTML = "";
|
|
654
|
-
|
|
655
|
-
console.error("TrialRecordExtension: recorder stop/upload failed.", err);
|
|
825
|
+
return {};
|
|
656
826
|
}
|
|
657
|
-
return {};
|
|
658
827
|
}
|
|
659
828
|
getCurrentPluginName() {
|
|
660
829
|
const current_plugin_class = this.jsPsych.getCurrentTrial().type;
|