@lookit/record 3.0.1 → 4.1.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 -6
- package/dist/index.browser.js +136 -41
- 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 +135 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +129 -12
- package/dist/index.js +135 -40
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/consentVideo.spec.ts +38 -12
- package/src/consentVideo.ts +80 -19
- package/src/errors.ts +7 -27
- package/src/index.spec.ts +387 -12
- package/src/recorder.spec.ts +170 -6
- package/src/recorder.ts +37 -5
- package/src/start.ts +7 -1
- package/src/stop.ts +41 -7
- package/src/trial.ts +97 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lookit/record",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Recording extensions and plugins for CHS studies.",
|
|
5
5
|
"homepage": "https://github.com/lookit/lookit-jspsych#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@lookit/data": "^0.2.0",
|
|
45
|
-
"@lookit/templates": "^2.
|
|
45
|
+
"@lookit/templates": "^2.1.0",
|
|
46
46
|
"jspsych": "^8.0.3"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/consentVideo.spec.ts
CHANGED
|
@@ -6,11 +6,7 @@ import { initJsPsych, PluginInfo, TrialType } from "jspsych";
|
|
|
6
6
|
import playbackFeed from "../hbs/playback-feed.hbs";
|
|
7
7
|
import recordFeed from "../hbs/record-feed.hbs";
|
|
8
8
|
import { VideoConsentPlugin } from "./consentVideo";
|
|
9
|
-
import {
|
|
10
|
-
ButtonNotFoundError,
|
|
11
|
-
ImageNotFoundError,
|
|
12
|
-
VideoContainerNotFoundError,
|
|
13
|
-
} from "./errors";
|
|
9
|
+
import { ElementNotFoundError } from "./errors";
|
|
14
10
|
import Recorder from "./recorder";
|
|
15
11
|
|
|
16
12
|
declare const window: LookitWindow;
|
|
@@ -67,7 +63,21 @@ test("GetVideoContainer error when no container", () => {
|
|
|
67
63
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
68
64
|
expect(() =>
|
|
69
65
|
plugin["getVideoContainer"](document.createElement("div")),
|
|
70
|
-
).toThrow(
|
|
66
|
+
).toThrow(ElementNotFoundError);
|
|
67
|
+
expect(() =>
|
|
68
|
+
plugin["getVideoContainer"](document.createElement("div")),
|
|
69
|
+
).toThrow(`"${plugin["video_container_id"]}" div not found.`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("getMessageContainer error when no container", () => {
|
|
73
|
+
const jsPsych = initJsPsych();
|
|
74
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
75
|
+
expect(() =>
|
|
76
|
+
plugin["getMessageContainer"](document.createElement("div")),
|
|
77
|
+
).toThrow(ElementNotFoundError);
|
|
78
|
+
expect(() =>
|
|
79
|
+
plugin["getMessageContainer"](document.createElement("div")),
|
|
80
|
+
).toThrow(`"${plugin["msg_container_id"]}" div not found.`);
|
|
71
81
|
});
|
|
72
82
|
|
|
73
83
|
test("GetVideoContainer", () => {
|
|
@@ -81,6 +91,17 @@ test("GetVideoContainer", () => {
|
|
|
81
91
|
expect(html).toBe(`<div id="lookit-jspsych-video-container"></div>`);
|
|
82
92
|
});
|
|
83
93
|
|
|
94
|
+
test("getMessageContainer", () => {
|
|
95
|
+
const jsPsych = initJsPsych();
|
|
96
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
97
|
+
const display = document.createElement("div");
|
|
98
|
+
|
|
99
|
+
display.innerHTML = `<div id="${plugin["msg_container_id"]}"></div>`;
|
|
100
|
+
|
|
101
|
+
const html = plugin["getMessageContainer"](display).outerHTML;
|
|
102
|
+
expect(html).toBe(`<div id="lookit-jspsych-video-msg-container"></div>`);
|
|
103
|
+
});
|
|
104
|
+
|
|
84
105
|
test("recordFeed", () => {
|
|
85
106
|
const jsPsych = initJsPsych();
|
|
86
107
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
@@ -103,7 +124,7 @@ test("playbackFeed", () => {
|
|
|
103
124
|
const vidContainer = "some video container";
|
|
104
125
|
|
|
105
126
|
plugin["getVideoContainer"] = jest.fn().mockReturnValue(vidContainer);
|
|
106
|
-
plugin["
|
|
127
|
+
plugin["onPlaybackEnded"] = jest.fn().mockReturnValue("some func");
|
|
107
128
|
plugin["playbackFeed"](display);
|
|
108
129
|
|
|
109
130
|
expect(Recorder.prototype.insertPlaybackFeed).toHaveBeenCalledWith(
|
|
@@ -112,7 +133,7 @@ test("playbackFeed", () => {
|
|
|
112
133
|
);
|
|
113
134
|
});
|
|
114
135
|
|
|
115
|
-
test("
|
|
136
|
+
test("onPlaybackEnded", () => {
|
|
116
137
|
const jsPsych = initJsPsych();
|
|
117
138
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
118
139
|
const display = document.createElement("div");
|
|
@@ -145,7 +166,7 @@ test("onEnded", () => {
|
|
|
145
166
|
return "";
|
|
146
167
|
});
|
|
147
168
|
|
|
148
|
-
plugin["
|
|
169
|
+
plugin["onPlaybackEnded"](display)();
|
|
149
170
|
|
|
150
171
|
expect(plugin["recordFeed"]).toHaveBeenCalledWith(display);
|
|
151
172
|
expect(plugin["recordFeed"]).toHaveBeenCalledTimes(1);
|
|
@@ -174,7 +195,10 @@ test("getButton error when button not found", () => {
|
|
|
174
195
|
const display = document.createElement("div");
|
|
175
196
|
|
|
176
197
|
expect(() => plugin["getButton"](display, "next")).toThrow(
|
|
177
|
-
|
|
198
|
+
ElementNotFoundError,
|
|
199
|
+
);
|
|
200
|
+
expect(() => plugin["getButton"](display, "next")).toThrow(
|
|
201
|
+
`"next" button not found.`,
|
|
178
202
|
);
|
|
179
203
|
});
|
|
180
204
|
|
|
@@ -183,7 +207,10 @@ test("getImg error when image not found", () => {
|
|
|
183
207
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
184
208
|
const display = document.createElement("div");
|
|
185
209
|
expect(() => plugin["getImg"](display, "record-icon")).toThrow(
|
|
186
|
-
|
|
210
|
+
ElementNotFoundError,
|
|
211
|
+
);
|
|
212
|
+
expect(() => plugin["getImg"](display, "record-icon")).toThrow(
|
|
213
|
+
`"record-icon" img not found.`,
|
|
187
214
|
);
|
|
188
215
|
});
|
|
189
216
|
|
|
@@ -283,7 +310,6 @@ test("stopButton", async () => {
|
|
|
283
310
|
.forEach((button) => expect(button.disabled).toBeFalsy());
|
|
284
311
|
expect(stopButton!.disabled).toBeTruthy();
|
|
285
312
|
expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
286
|
-
expect(Recorder.prototype.reset).toHaveBeenCalledTimes(1);
|
|
287
313
|
expect(plugin["recordFeed"]).toHaveBeenCalledTimes(1);
|
|
288
314
|
expect(plugin["recordFeed"]).toHaveBeenCalledWith(display);
|
|
289
315
|
});
|
package/src/consentVideo.ts
CHANGED
|
@@ -3,11 +3,7 @@ import { LookitWindow } from "@lookit/data/dist/types";
|
|
|
3
3
|
import chsTemplates from "@lookit/templates";
|
|
4
4
|
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
|
5
5
|
import { version } from "../package.json";
|
|
6
|
-
import {
|
|
7
|
-
ButtonNotFoundError,
|
|
8
|
-
ImageNotFoundError,
|
|
9
|
-
VideoContainerNotFoundError,
|
|
10
|
-
} from "./errors";
|
|
6
|
+
import { ElementNotFoundError } from "./errors";
|
|
11
7
|
import Recorder from "./recorder";
|
|
12
8
|
|
|
13
9
|
declare const window: LookitWindow;
|
|
@@ -63,6 +59,11 @@ const info = <const>{
|
|
|
63
59
|
consent_statement_text: { type: ParameterType.STRING, default: "" },
|
|
64
60
|
omit_injury_phrase: { type: ParameterType.BOOL, default: false },
|
|
65
61
|
},
|
|
62
|
+
data: {
|
|
63
|
+
chs_type: {
|
|
64
|
+
type: ParameterType.STRING,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
66
67
|
};
|
|
67
68
|
type Info = typeof info;
|
|
68
69
|
|
|
@@ -71,6 +72,11 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
71
72
|
public static readonly info = info;
|
|
72
73
|
private readonly recorder: Recorder;
|
|
73
74
|
private readonly video_container_id = "lookit-jspsych-video-container";
|
|
75
|
+
private readonly msg_container_id = "lookit-jspsych-video-msg-container";
|
|
76
|
+
private uploadingMsg: string | null = null;
|
|
77
|
+
private startingMsg: string | null = null;
|
|
78
|
+
private recordingMsg: string | null = null;
|
|
79
|
+
private notRecordingMsg: string | null = null;
|
|
74
80
|
|
|
75
81
|
/**
|
|
76
82
|
* Instantiate video consent plugin.
|
|
@@ -89,7 +95,7 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
89
95
|
* @param trial - Trial data including user supplied parameters.
|
|
90
96
|
*/
|
|
91
97
|
public trial(display: HTMLElement, trial: TrialType<Info>) {
|
|
92
|
-
// Get trial HTML string
|
|
98
|
+
// Get trial HTML string from templates package. This will also set the i18n locale.
|
|
93
99
|
const consentVideo = chsTemplates.consentVideo(trial);
|
|
94
100
|
|
|
95
101
|
// Add rendered document to display HTML
|
|
@@ -97,11 +103,26 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
97
103
|
|
|
98
104
|
// Video recording HTML
|
|
99
105
|
this.recordFeed(display);
|
|
106
|
+
// Set event listeners for buttons.
|
|
100
107
|
this.recordButton(display);
|
|
101
108
|
this.stopButton(display);
|
|
102
109
|
this.playButton(display);
|
|
103
110
|
this.nextButton(display);
|
|
111
|
+
// Translate and store any messages that may need to be shown. Locale has already been set via the chsTemplates consentVideo method.
|
|
112
|
+
this.uploadingMsg = chsTemplates.translateString(
|
|
113
|
+
"exp-lookit-video-consent.Stopping-and-uploading",
|
|
114
|
+
);
|
|
115
|
+
this.startingMsg = chsTemplates.translateString(
|
|
116
|
+
"exp-lookit-video-consent.Starting-recorder",
|
|
117
|
+
);
|
|
118
|
+
this.recordingMsg = chsTemplates.translateString(
|
|
119
|
+
"exp-lookit-video-consent.Recording",
|
|
120
|
+
);
|
|
121
|
+
this.notRecordingMsg = chsTemplates.translateString(
|
|
122
|
+
"exp-lookit-video-consent.Not-recording",
|
|
123
|
+
);
|
|
104
124
|
}
|
|
125
|
+
|
|
105
126
|
/**
|
|
106
127
|
* Retrieve video container element.
|
|
107
128
|
*
|
|
@@ -114,7 +135,7 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
114
135
|
);
|
|
115
136
|
|
|
116
137
|
if (!videoContainer) {
|
|
117
|
-
throw new
|
|
138
|
+
throw new ElementNotFoundError(this.video_container_id, "div");
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
return videoContainer;
|
|
@@ -138,23 +159,59 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
138
159
|
*/
|
|
139
160
|
private playbackFeed(display: HTMLElement) {
|
|
140
161
|
const videoContainer = this.getVideoContainer(display);
|
|
141
|
-
this.recorder.insertPlaybackFeed(
|
|
162
|
+
this.recorder.insertPlaybackFeed(
|
|
163
|
+
videoContainer,
|
|
164
|
+
this.onPlaybackEnded(display),
|
|
165
|
+
);
|
|
142
166
|
}
|
|
143
167
|
|
|
144
168
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
169
|
+
* Get message container that appears alongside the video element.
|
|
170
|
+
*
|
|
171
|
+
* @param display - HTML element for experiment.
|
|
172
|
+
* @returns Message container div element.
|
|
173
|
+
*/
|
|
174
|
+
private getMessageContainer(display: HTMLElement) {
|
|
175
|
+
const msgContainer = display.querySelector<HTMLDivElement>(
|
|
176
|
+
`div#${this.msg_container_id}`,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (!msgContainer) {
|
|
180
|
+
throw new ElementNotFoundError(this.msg_container_id, "div");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return msgContainer;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Add HTML-formatted message alongside the video feed, e.g. for waiting
|
|
188
|
+
* periods during webcam feed transitions (starting, stopping/uploading). This
|
|
189
|
+
* will also replace an existing message with the new one. To clear any
|
|
190
|
+
* existing messages, pass an empty string.
|
|
191
|
+
*
|
|
192
|
+
* @param display - HTML element for experiment.
|
|
193
|
+
* @param message - HTML content for message div.
|
|
194
|
+
*/
|
|
195
|
+
private addMessage(display: HTMLElement, message: string) {
|
|
196
|
+
const msgContainer = this.getMessageContainer(display);
|
|
197
|
+
msgContainer.innerHTML = message;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Put back the webcam feed once the video recording playback has ended. This
|
|
202
|
+
* is used with the "ended" Event.
|
|
147
203
|
*
|
|
148
204
|
* @param display - JsPsych display HTML element.
|
|
149
205
|
* @returns Event function
|
|
150
206
|
*/
|
|
151
|
-
private
|
|
207
|
+
private onPlaybackEnded(display: HTMLElement) {
|
|
152
208
|
return () => {
|
|
153
209
|
const next = this.getButton(display, "next");
|
|
154
210
|
const play = this.getButton(display, "play");
|
|
155
211
|
const record = this.getButton(display, "record");
|
|
156
212
|
|
|
157
213
|
this.recordFeed(display);
|
|
214
|
+
this.addMessage(display, this.notRecordingMsg!);
|
|
158
215
|
next.disabled = false;
|
|
159
216
|
play.disabled = false;
|
|
160
217
|
record.disabled = false;
|
|
@@ -174,7 +231,7 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
174
231
|
) {
|
|
175
232
|
const btn = display.querySelector<HTMLButtonElement>(`button#${id}`);
|
|
176
233
|
if (!btn) {
|
|
177
|
-
throw new
|
|
234
|
+
throw new ElementNotFoundError(id, "button");
|
|
178
235
|
}
|
|
179
236
|
return btn;
|
|
180
237
|
}
|
|
@@ -190,7 +247,7 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
190
247
|
const img = display.querySelector<HTMLImageElement>(`img#${id}`);
|
|
191
248
|
|
|
192
249
|
if (!img) {
|
|
193
|
-
throw new
|
|
250
|
+
throw new ElementNotFoundError(id, "img");
|
|
194
251
|
}
|
|
195
252
|
|
|
196
253
|
return img;
|
|
@@ -208,12 +265,14 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
208
265
|
const next = this.getButton(display, "next");
|
|
209
266
|
|
|
210
267
|
record.addEventListener("click", async () => {
|
|
268
|
+
this.addMessage(display, this.startingMsg!);
|
|
211
269
|
record.disabled = true;
|
|
212
|
-
stop.disabled = false;
|
|
213
270
|
play.disabled = true;
|
|
214
271
|
next.disabled = true;
|
|
215
|
-
this.getImg(display, "record-icon").style.visibility = "visible";
|
|
216
272
|
await this.recorder.start(true, VideoConsentPlugin.info.name);
|
|
273
|
+
this.getImg(display, "record-icon").style.visibility = "visible";
|
|
274
|
+
this.addMessage(display, this.recordingMsg!);
|
|
275
|
+
stop.disabled = false;
|
|
217
276
|
});
|
|
218
277
|
}
|
|
219
278
|
|
|
@@ -245,11 +304,13 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
245
304
|
|
|
246
305
|
stop.addEventListener("click", async () => {
|
|
247
306
|
stop.disabled = true;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await this.recorder.stop();
|
|
251
|
-
this.recorder.reset();
|
|
307
|
+
this.addMessage(display, this.uploadingMsg!);
|
|
308
|
+
await this.recorder.stop(true);
|
|
252
309
|
this.recordFeed(display);
|
|
310
|
+
this.getImg(display, "record-icon").style.visibility = "hidden";
|
|
311
|
+
this.addMessage(display, this.notRecordingMsg!);
|
|
312
|
+
play.disabled = false;
|
|
313
|
+
record.disabled = false;
|
|
253
314
|
});
|
|
254
315
|
}
|
|
255
316
|
/**
|
package/src/errors.ts
CHANGED
|
@@ -196,36 +196,16 @@ export class CreateURLError extends Error {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
/** Error thrown when
|
|
200
|
-
export class
|
|
201
|
-
/** No video container found. */
|
|
202
|
-
public constructor() {
|
|
203
|
-
super("Video Container could not be found.");
|
|
204
|
-
this.name = "VideoContainerError";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Error thrown when button not found. */
|
|
209
|
-
export class ButtonNotFoundError extends Error {
|
|
199
|
+
/** Error thrown when an HTML element is not found. */
|
|
200
|
+
export class ElementNotFoundError extends Error {
|
|
210
201
|
/**
|
|
211
|
-
*
|
|
202
|
+
* Element couldn't be found by ID field.
|
|
212
203
|
*
|
|
213
204
|
* @param id - HTML ID parameter.
|
|
205
|
+
* @param tag - HTML tag of the element.
|
|
214
206
|
*/
|
|
215
|
-
public constructor(id: string) {
|
|
216
|
-
super(`"${id}"
|
|
217
|
-
this.name = "
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Throw Error when image couldn't be found. */
|
|
222
|
-
export class ImageNotFoundError extends Error {
|
|
223
|
-
/**
|
|
224
|
-
* Error when image couldn't be found by ID field.
|
|
225
|
-
*
|
|
226
|
-
* @param id - HTML ID parameter
|
|
227
|
-
*/
|
|
228
|
-
public constructor(id: string) {
|
|
229
|
-
super(`"${id}" image not found.`);
|
|
207
|
+
public constructor(id: string, tag: string) {
|
|
208
|
+
super(`"${id}" ${tag} not found.`);
|
|
209
|
+
this.name = "ElementNotFoundError";
|
|
230
210
|
}
|
|
231
211
|
}
|