@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/README.md +125 -39
- package/dist/index.browser.js +96 -33
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +13 -13
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +95 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +42 -3
- package/dist/index.js +95 -32
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/consentVideo.spec.ts +38 -11
- package/src/consentVideo.ts +75 -18
- package/src/errors.ts +7 -27
- package/src/recorder.spec.ts +208 -1
- package/src/recorder.ts +36 -5
- package/src/videoConfig.spec.ts +115 -0
- package/src/videoConfig.ts +38 -1
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
|
-
*
|
|
309
|
-
*
|
|
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
|
|
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: "
|
|
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.
|
|
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
|
|
145
|
-
constructor() {
|
|
146
|
-
super("
|
|
147
|
-
this.name = "
|
|
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
|
-
|
|
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
|
|
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(
|
|
435
|
+
this.recorder.insertPlaybackFeed(
|
|
436
|
+
videoContainer,
|
|
437
|
+
this.onPlaybackEnded(display)
|
|
438
|
+
);
|
|
415
439
|
}
|
|
416
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
471
|
-
|
|
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.
|
|
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
|
|