@lookit/record 0.0.3
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 +376 -0
- package/dist/consentVideo.d.ts +353 -0
- package/dist/errors.d.ts +158 -0
- package/dist/index.browser.js +26321 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.min.js +4 -0
- package/dist/index.browser.min.js.map +1 -0
- package/dist/index.cjs +26321 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +26319 -0
- package/dist/index.js.map +1 -0
- package/dist/mic_check.d.ts +33 -0
- package/dist/recorder.d.ts +146 -0
- package/dist/start.d.ts +24 -0
- package/dist/stop.d.ts +30 -0
- package/dist/trial.d.ts +28 -0
- package/dist/types.d.ts +5 -0
- package/dist/utils.d.ts +21 -0
- package/dist/video_config.d.ts +296 -0
- package/package.json +52 -0
- package/src/consentVideo.spec.ts +274 -0
- package/src/consentVideo.ts +284 -0
- package/src/environment.d.ts +10 -0
- package/src/errors.ts +243 -0
- package/src/img-import.d.ts +1 -0
- package/src/index.spec.ts +104 -0
- package/src/index.ts +13 -0
- package/src/mic_check.d.ts +1 -0
- package/src/mic_check.js +74 -0
- package/src/mic_check.ts +77 -0
- package/src/recorder.spec.ts +446 -0
- package/src/recorder.ts +353 -0
- package/src/start.ts +36 -0
- package/src/stop.ts +46 -0
- package/src/string-import.d.ts +14 -0
- package/src/trial.ts +47 -0
- package/src/types.ts +8 -0
- package/src/utils.spec.ts +55 -0
- package/src/utils.ts +119 -0
- package/src/video_config.spec.ts +1113 -0
- package/src/video_config.ts +665 -0
- package/src/video_config_mic_check.spec.ts +268 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
|
+
import Handlebars from "handlebars";
|
|
3
|
+
import { initJsPsych, PluginInfo, TrialType } from "jspsych";
|
|
4
|
+
import consentVideoTrial from "../templates/consent-video-trial.hbs";
|
|
5
|
+
import recordFeed from "../templates/record-feed.hbs";
|
|
6
|
+
import { VideoConsentPlugin } from "./consentVideo";
|
|
7
|
+
import {
|
|
8
|
+
ButtonNotFoundError,
|
|
9
|
+
ImageNotFoundError,
|
|
10
|
+
VideoContainerNotFoundError,
|
|
11
|
+
} from "./errors";
|
|
12
|
+
import Recorder from "./recorder";
|
|
13
|
+
|
|
14
|
+
declare const window: LookitWindow;
|
|
15
|
+
|
|
16
|
+
jest.mock("./recorder");
|
|
17
|
+
|
|
18
|
+
test("Instantiate recorder", () => {
|
|
19
|
+
const jsPsych = initJsPsych();
|
|
20
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
21
|
+
expect(plugin["recorder"]).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Trial", () => {
|
|
25
|
+
const jsPsych = initJsPsych();
|
|
26
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
27
|
+
const display = document.createElement("div");
|
|
28
|
+
const trial = { locale: "en-us" } as unknown as TrialType<PluginInfo>;
|
|
29
|
+
|
|
30
|
+
window.chs = {
|
|
31
|
+
study: {
|
|
32
|
+
attributes: {
|
|
33
|
+
name: "name",
|
|
34
|
+
duration: "duration",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} as typeof window.chs;
|
|
38
|
+
|
|
39
|
+
plugin["recordFeed"] = jest.fn();
|
|
40
|
+
plugin["recordButton"] = jest.fn();
|
|
41
|
+
plugin["stopButton"] = jest.fn();
|
|
42
|
+
plugin["playButton"] = jest.fn();
|
|
43
|
+
plugin["nextButton"] = jest.fn();
|
|
44
|
+
|
|
45
|
+
plugin.trial(display, trial);
|
|
46
|
+
|
|
47
|
+
expect(plugin["recordFeed"]).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(plugin["recordButton"]).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(plugin["stopButton"]).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(plugin["playButton"]).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(plugin["nextButton"]).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("GetVideoContainer error when no container", () => {
|
|
55
|
+
const jsPsych = initJsPsych();
|
|
56
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
57
|
+
expect(() =>
|
|
58
|
+
plugin["getVideoContainer"](document.createElement("div")),
|
|
59
|
+
).toThrow(VideoContainerNotFoundError);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("GetVideoContainer", () => {
|
|
63
|
+
const jsPsych = initJsPsych();
|
|
64
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
65
|
+
const display = document.createElement("div");
|
|
66
|
+
|
|
67
|
+
display.innerHTML = `<div id="${plugin["video_container_id"]}"></div>`;
|
|
68
|
+
|
|
69
|
+
const html = plugin["getVideoContainer"](display).outerHTML;
|
|
70
|
+
expect(html).toBe(`<div id="lookit-jspsych-video-container"></div>`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("recordFeed", () => {
|
|
74
|
+
const jsPsych = initJsPsych();
|
|
75
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
76
|
+
const display = document.createElement("div");
|
|
77
|
+
|
|
78
|
+
display.innerHTML = `<div id="${plugin["video_container_id"]}"><img id="record-icon"></div>`;
|
|
79
|
+
plugin["getVideoContainer"] = jest.fn();
|
|
80
|
+
plugin["recordFeed"](display);
|
|
81
|
+
|
|
82
|
+
expect(display.innerHTML).toContain(
|
|
83
|
+
`<img id="record-icon" style="visibility: hidden;">`,
|
|
84
|
+
);
|
|
85
|
+
expect(Recorder.prototype.insertRecordFeed).toHaveBeenCalledTimes(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("playbackFeed", () => {
|
|
89
|
+
const jsPsych = initJsPsych();
|
|
90
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
91
|
+
const display = document.createElement("div");
|
|
92
|
+
const vidContainer = "some video container";
|
|
93
|
+
|
|
94
|
+
plugin["getVideoContainer"] = jest.fn().mockReturnValue(vidContainer);
|
|
95
|
+
plugin["onEnded"] = jest.fn().mockReturnValue("some func");
|
|
96
|
+
plugin["playbackFeed"](display);
|
|
97
|
+
|
|
98
|
+
expect(Recorder.prototype.insertPlaybackFeed).toHaveBeenCalledWith(
|
|
99
|
+
vidContainer,
|
|
100
|
+
"some func",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("onEnded", () => {
|
|
105
|
+
const jsPsych = initJsPsych();
|
|
106
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
107
|
+
const display = document.createElement("div");
|
|
108
|
+
const play = document.createElement("button");
|
|
109
|
+
const next = document.createElement("button");
|
|
110
|
+
const record = document.createElement("button");
|
|
111
|
+
|
|
112
|
+
display.innerHTML = Handlebars.compile(consentVideoTrial)({});
|
|
113
|
+
plugin["recordFeed"] = jest.fn();
|
|
114
|
+
plugin["getButton"] = jest.fn().mockImplementation((_display, id) => {
|
|
115
|
+
switch (id) {
|
|
116
|
+
case "play":
|
|
117
|
+
return play;
|
|
118
|
+
case "next":
|
|
119
|
+
return next;
|
|
120
|
+
case "record":
|
|
121
|
+
return record;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (id === "play") {
|
|
125
|
+
return play;
|
|
126
|
+
} else if (id === "next") {
|
|
127
|
+
return next;
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
plugin["onEnded"](display)();
|
|
133
|
+
|
|
134
|
+
expect(plugin["recordFeed"]).toHaveBeenCalledWith(display);
|
|
135
|
+
expect(plugin["recordFeed"]).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(plugin["getButton"]).toHaveBeenCalledTimes(3);
|
|
137
|
+
expect(play.disabled).toBeFalsy();
|
|
138
|
+
expect(next.disabled).toBeFalsy();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("getButton", () => {
|
|
142
|
+
const jsPsych = initJsPsych();
|
|
143
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
144
|
+
const display = document.createElement("div");
|
|
145
|
+
|
|
146
|
+
display.innerHTML = Handlebars.compile(consentVideoTrial)({});
|
|
147
|
+
|
|
148
|
+
expect(plugin["getButton"](display, "next").id).toStrictEqual("next");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("getButton error when button not found", () => {
|
|
152
|
+
const jsPsych = initJsPsych();
|
|
153
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
154
|
+
const display = document.createElement("div");
|
|
155
|
+
|
|
156
|
+
expect(() => plugin["getButton"](display, "next")).toThrow(
|
|
157
|
+
ButtonNotFoundError,
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("getImg error when image not found", () => {
|
|
162
|
+
const jsPsych = initJsPsych();
|
|
163
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
164
|
+
const display = document.createElement("div");
|
|
165
|
+
expect(() => plugin["getImg"](display, "record-icon")).toThrow(
|
|
166
|
+
ImageNotFoundError,
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("recordButton", async () => {
|
|
171
|
+
const jsPsych = initJsPsych();
|
|
172
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
173
|
+
const display = document.createElement("div");
|
|
174
|
+
|
|
175
|
+
display.innerHTML =
|
|
176
|
+
Handlebars.compile(consentVideoTrial)({}) +
|
|
177
|
+
Handlebars.compile(recordFeed)({});
|
|
178
|
+
|
|
179
|
+
plugin["recordButton"](display);
|
|
180
|
+
|
|
181
|
+
// Trigger event
|
|
182
|
+
const click = new Event("click");
|
|
183
|
+
await display
|
|
184
|
+
.querySelector<HTMLButtonElement>("button#record")!
|
|
185
|
+
.dispatchEvent(click);
|
|
186
|
+
|
|
187
|
+
// Check for query length
|
|
188
|
+
const disabledButtons = display.querySelectorAll<HTMLButtonElement>(
|
|
189
|
+
"button#record, button#play, button#next",
|
|
190
|
+
);
|
|
191
|
+
expect(disabledButtons.length).toStrictEqual(3);
|
|
192
|
+
|
|
193
|
+
// Check for buttons to be disabled.
|
|
194
|
+
disabledButtons.forEach((button) => {
|
|
195
|
+
expect(button.disabled).toBeTruthy();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Stop button should not be disabled.
|
|
199
|
+
expect(
|
|
200
|
+
display.querySelector<HTMLButtonElement>("button#stop")!.disabled,
|
|
201
|
+
).toBeFalsy();
|
|
202
|
+
|
|
203
|
+
// Show record record icon
|
|
204
|
+
expect(
|
|
205
|
+
display.querySelector<HTMLImageElement>("img#record-icon")!.style
|
|
206
|
+
.visibility,
|
|
207
|
+
).toStrictEqual("visible");
|
|
208
|
+
|
|
209
|
+
// Start recorder
|
|
210
|
+
expect(Recorder.prototype.start).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(Recorder.prototype.start).toHaveBeenCalledWith("consent");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("playButton", () => {
|
|
215
|
+
const jsPsych = initJsPsych();
|
|
216
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
217
|
+
const display = document.createElement("div");
|
|
218
|
+
|
|
219
|
+
plugin["playbackFeed"] = jest.fn();
|
|
220
|
+
|
|
221
|
+
display.innerHTML = Handlebars.compile(consentVideoTrial)({});
|
|
222
|
+
|
|
223
|
+
plugin["playButton"](display);
|
|
224
|
+
|
|
225
|
+
const playButton = display.querySelector<HTMLButtonElement>("button#play");
|
|
226
|
+
|
|
227
|
+
playButton!.dispatchEvent(new Event("click"));
|
|
228
|
+
|
|
229
|
+
expect(playButton!.disabled).toBeTruthy();
|
|
230
|
+
expect(plugin["playbackFeed"]).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(plugin["playbackFeed"]).toHaveBeenCalledWith(display);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("stopButton", async () => {
|
|
235
|
+
const jsPsych = initJsPsych();
|
|
236
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
237
|
+
const display = document.createElement("div");
|
|
238
|
+
|
|
239
|
+
display.innerHTML =
|
|
240
|
+
Handlebars.compile(consentVideoTrial)({
|
|
241
|
+
video_container_id: plugin["video_container_id"],
|
|
242
|
+
}) + Handlebars.compile(recordFeed)({});
|
|
243
|
+
|
|
244
|
+
plugin["recordFeed"] = jest.fn();
|
|
245
|
+
plugin["stopButton"](display);
|
|
246
|
+
|
|
247
|
+
const stopButton = display.querySelector<HTMLButtonElement>("button#stop");
|
|
248
|
+
await stopButton!.dispatchEvent(new Event("click"));
|
|
249
|
+
|
|
250
|
+
display
|
|
251
|
+
.querySelectorAll<HTMLButtonElement>("button#record, button#play")
|
|
252
|
+
.forEach((button) => expect(button.disabled).toBeFalsy());
|
|
253
|
+
expect(stopButton!.disabled).toBeTruthy();
|
|
254
|
+
expect(Recorder.prototype.stop).toHaveBeenCalledTimes(1);
|
|
255
|
+
expect(Recorder.prototype.reset).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(plugin["recordFeed"]).toHaveBeenCalledTimes(1);
|
|
257
|
+
expect(plugin["recordFeed"]).toHaveBeenCalledWith(display);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("nextButton", () => {
|
|
261
|
+
const jsPsych = initJsPsych();
|
|
262
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
263
|
+
const display = document.createElement("div");
|
|
264
|
+
|
|
265
|
+
display.innerHTML = Handlebars.compile(consentVideoTrial)({});
|
|
266
|
+
jsPsych.finishTrial = jest.fn();
|
|
267
|
+
|
|
268
|
+
plugin["nextButton"](display);
|
|
269
|
+
display
|
|
270
|
+
.querySelector<HTMLButtonElement>("button#next")!
|
|
271
|
+
.dispatchEvent(new Event("click"));
|
|
272
|
+
|
|
273
|
+
expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
|
|
274
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
|
+
import Handlebars from "handlebars";
|
|
3
|
+
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
|
4
|
+
import { version } from "../package.json";
|
|
5
|
+
import consentDocTemplate from "../templates/consent-document.hbs";
|
|
6
|
+
import consentVideoTrialTemplate from "../templates/consent-video-trial.hbs";
|
|
7
|
+
import {
|
|
8
|
+
ButtonNotFoundError,
|
|
9
|
+
ImageNotFoundError,
|
|
10
|
+
VideoContainerNotFoundError,
|
|
11
|
+
} from "./errors";
|
|
12
|
+
import Recorder from "./recorder";
|
|
13
|
+
import { initI18nAndTemplates } from "./utils";
|
|
14
|
+
|
|
15
|
+
declare const window: LookitWindow;
|
|
16
|
+
|
|
17
|
+
const info = <const>{
|
|
18
|
+
name: "consent-video",
|
|
19
|
+
version,
|
|
20
|
+
parameters: {
|
|
21
|
+
template: { type: ParameterType.STRING, default: "consent_005" },
|
|
22
|
+
locale: { type: ParameterType.STRING, default: "en-us" },
|
|
23
|
+
additional_video_privacy_statement: {
|
|
24
|
+
type: ParameterType.STRING,
|
|
25
|
+
default: "",
|
|
26
|
+
},
|
|
27
|
+
datause: { type: ParameterType.STRING, default: "" },
|
|
28
|
+
gdpr: { type: ParameterType.BOOL, default: false },
|
|
29
|
+
gdpr_personal_data: { type: ParameterType.STRING, default: "" },
|
|
30
|
+
gdpr_sensitive_data: { type: ParameterType.STRING, default: "" },
|
|
31
|
+
PIName: { type: ParameterType.STRING, default: "" },
|
|
32
|
+
include_databrary: { type: ParameterType.BOOL, default: false },
|
|
33
|
+
institution: { type: ParameterType.STRING, default: "" },
|
|
34
|
+
PIContact: { type: ParameterType.STRING, default: "" },
|
|
35
|
+
payment: { type: ParameterType.STRING, default: "" },
|
|
36
|
+
private_level_only: { type: ParameterType.BOOL, default: false },
|
|
37
|
+
procedures: { type: ParameterType.STRING, default: "" },
|
|
38
|
+
purpose: { type: ParameterType.STRING, default: "" },
|
|
39
|
+
research_rights_statement: { type: ParameterType.STRING, default: "" },
|
|
40
|
+
risk_statement: { type: ParameterType.STRING, default: "" },
|
|
41
|
+
voluntary_participation: { type: ParameterType.STRING, default: "" },
|
|
42
|
+
purpose_header: { type: ParameterType.STRING, default: "" },
|
|
43
|
+
procedures_header: { type: ParameterType.STRING, default: "" },
|
|
44
|
+
participation_header: { type: ParameterType.STRING, default: "" },
|
|
45
|
+
benefits_header: { type: ParameterType.STRING, default: "" },
|
|
46
|
+
risk_header: { type: ParameterType.STRING, default: "" },
|
|
47
|
+
summary_statement: { type: ParameterType.STRING, default: "" },
|
|
48
|
+
additional_segments: {
|
|
49
|
+
type: ParameterType.COMPLEX,
|
|
50
|
+
array: true,
|
|
51
|
+
nested: {
|
|
52
|
+
title: {
|
|
53
|
+
type: ParameterType.STRING,
|
|
54
|
+
default: "",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
text: {
|
|
58
|
+
type: ParameterType.STRING,
|
|
59
|
+
default: "",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
prompt_all_adults: { type: ParameterType.BOOL, default: false },
|
|
63
|
+
prompt_only_adults: { type: ParameterType.BOOL, default: false },
|
|
64
|
+
consent_statement_text: { type: ParameterType.STRING, default: "" },
|
|
65
|
+
omit_injury_phrase: { type: ParameterType.BOOL, default: false },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
type Info = typeof info;
|
|
69
|
+
|
|
70
|
+
/** The video consent plugin. */
|
|
71
|
+
export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
72
|
+
public static readonly info = info;
|
|
73
|
+
private readonly recorder: Recorder;
|
|
74
|
+
private readonly video_container_id = "lookit-jspsych-video-container";
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Instantiate video consent plugin.
|
|
78
|
+
*
|
|
79
|
+
* @param jsPsych - JsPsych object
|
|
80
|
+
*/
|
|
81
|
+
public constructor(private readonly jsPsych: JsPsych) {
|
|
82
|
+
this.jsPsych = jsPsych;
|
|
83
|
+
this.recorder = new Recorder(this.jsPsych);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create/Show trial view.
|
|
88
|
+
*
|
|
89
|
+
* @param display - HTML element for experiment.
|
|
90
|
+
* @param trial - Trial data including user supplied parameters.
|
|
91
|
+
*/
|
|
92
|
+
public trial(display: HTMLElement, trial: TrialType<Info>) {
|
|
93
|
+
const { video_container_id } = this;
|
|
94
|
+
const experiment = window.chs.study.attributes;
|
|
95
|
+
const { PIName, PIContact } = trial;
|
|
96
|
+
|
|
97
|
+
// Initialize both i18next and Handlebars
|
|
98
|
+
initI18nAndTemplates(trial);
|
|
99
|
+
|
|
100
|
+
// Render left side (consent text)
|
|
101
|
+
const consent = Handlebars.compile(consentDocTemplate)({
|
|
102
|
+
...trial,
|
|
103
|
+
name: PIName,
|
|
104
|
+
contact: PIContact,
|
|
105
|
+
experiment,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Render whole document with above consent text
|
|
109
|
+
const consentVideoTrial = Handlebars.compile(consentVideoTrialTemplate)({
|
|
110
|
+
...trial,
|
|
111
|
+
consent,
|
|
112
|
+
video_container_id,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Add rendered document to display HTML
|
|
116
|
+
display.insertAdjacentHTML("afterbegin", consentVideoTrial);
|
|
117
|
+
|
|
118
|
+
// Video recording HTML
|
|
119
|
+
this.recordFeed(display);
|
|
120
|
+
this.recordButton(display);
|
|
121
|
+
this.stopButton(display);
|
|
122
|
+
this.playButton(display);
|
|
123
|
+
this.nextButton(display);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Retrieve video container element.
|
|
127
|
+
*
|
|
128
|
+
* @param display - HTML element for experiment.
|
|
129
|
+
* @returns Video container
|
|
130
|
+
*/
|
|
131
|
+
private getVideoContainer(display: HTMLElement) {
|
|
132
|
+
const videoContainer = display.querySelector<HTMLDivElement>(
|
|
133
|
+
`div#${this.video_container_id}`,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!videoContainer) {
|
|
137
|
+
throw new VideoContainerNotFoundError();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return videoContainer;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Add webcam feed to HTML.
|
|
145
|
+
*
|
|
146
|
+
* @param display - HTML element for experiment.
|
|
147
|
+
*/
|
|
148
|
+
private recordFeed(display: HTMLElement) {
|
|
149
|
+
const videoContainer = this.getVideoContainer(display);
|
|
150
|
+
this.recorder.insertRecordFeed(videoContainer);
|
|
151
|
+
this.getImg(display, "record-icon").style.visibility = "hidden";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Playback Feed
|
|
156
|
+
*
|
|
157
|
+
* @param display - JsPsych display HTML element.
|
|
158
|
+
*/
|
|
159
|
+
private playbackFeed(display: HTMLElement) {
|
|
160
|
+
const videoContainer = this.getVideoContainer(display);
|
|
161
|
+
this.recorder.insertPlaybackFeed(videoContainer, this.onEnded(display));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Put back the webcam feed once the video recording has ended. This is used
|
|
166
|
+
* with the "ended" Event.
|
|
167
|
+
*
|
|
168
|
+
* @param display - JsPsych display HTML element.
|
|
169
|
+
* @returns Event function
|
|
170
|
+
*/
|
|
171
|
+
private onEnded(display: HTMLElement) {
|
|
172
|
+
return () => {
|
|
173
|
+
const next = this.getButton(display, "next");
|
|
174
|
+
const play = this.getButton(display, "play");
|
|
175
|
+
const record = this.getButton(display, "record");
|
|
176
|
+
|
|
177
|
+
this.recordFeed(display);
|
|
178
|
+
next.disabled = false;
|
|
179
|
+
play.disabled = false;
|
|
180
|
+
record.disabled = false;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Retrieve button element from DOM.
|
|
186
|
+
*
|
|
187
|
+
* @param display - HTML element for experiment.
|
|
188
|
+
* @param id - Element id
|
|
189
|
+
* @returns Button element
|
|
190
|
+
*/
|
|
191
|
+
private getButton(
|
|
192
|
+
display: HTMLElement,
|
|
193
|
+
id: "play" | "next" | "stop" | "record",
|
|
194
|
+
) {
|
|
195
|
+
const btn = display.querySelector<HTMLButtonElement>(`button#${id}`);
|
|
196
|
+
if (!btn) {
|
|
197
|
+
throw new ButtonNotFoundError(id);
|
|
198
|
+
}
|
|
199
|
+
return btn;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Select and return the image element.
|
|
204
|
+
*
|
|
205
|
+
* @param display - HTML element for experiment.
|
|
206
|
+
* @param id - ID string of Image element
|
|
207
|
+
* @returns Image Element
|
|
208
|
+
*/
|
|
209
|
+
private getImg(display: HTMLElement, id: "record-icon") {
|
|
210
|
+
const img = display.querySelector<HTMLImageElement>(`img#${id}`);
|
|
211
|
+
|
|
212
|
+
if (!img) {
|
|
213
|
+
throw new ImageNotFoundError(id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return img;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Add record button to HTML.
|
|
221
|
+
*
|
|
222
|
+
* @param display - HTML element for experiment.
|
|
223
|
+
*/
|
|
224
|
+
private recordButton(display: HTMLElement) {
|
|
225
|
+
const record = this.getButton(display, "record");
|
|
226
|
+
const stop = this.getButton(display, "stop");
|
|
227
|
+
const play = this.getButton(display, "play");
|
|
228
|
+
const next = this.getButton(display, "next");
|
|
229
|
+
|
|
230
|
+
record.addEventListener("click", async () => {
|
|
231
|
+
record.disabled = true;
|
|
232
|
+
stop.disabled = false;
|
|
233
|
+
play.disabled = true;
|
|
234
|
+
next.disabled = true;
|
|
235
|
+
this.getImg(display, "record-icon").style.visibility = "visible";
|
|
236
|
+
await this.recorder.start("consent");
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Set up play button to playback last recorded video.
|
|
242
|
+
*
|
|
243
|
+
* @param display - HTML element for experiment.
|
|
244
|
+
*/
|
|
245
|
+
private playButton(display: HTMLElement) {
|
|
246
|
+
const play = this.getButton(display, "play");
|
|
247
|
+
const record = this.getButton(display, "record");
|
|
248
|
+
|
|
249
|
+
play.addEventListener("click", () => {
|
|
250
|
+
play.disabled = true;
|
|
251
|
+
record.disabled = true;
|
|
252
|
+
this.playbackFeed(display);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Add stop button to HTML.
|
|
258
|
+
*
|
|
259
|
+
* @param display - HTML element for experiment.
|
|
260
|
+
*/
|
|
261
|
+
private stopButton(display: HTMLElement) {
|
|
262
|
+
const stop = this.getButton(display, "stop");
|
|
263
|
+
const record = this.getButton(display, "record");
|
|
264
|
+
const play = this.getButton(display, "play");
|
|
265
|
+
|
|
266
|
+
stop.addEventListener("click", async () => {
|
|
267
|
+
stop.disabled = true;
|
|
268
|
+
record.disabled = false;
|
|
269
|
+
play.disabled = false;
|
|
270
|
+
await this.recorder.stop();
|
|
271
|
+
this.recorder.reset();
|
|
272
|
+
this.recordFeed(display);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Add next button to HTML.
|
|
277
|
+
*
|
|
278
|
+
* @param display - HTML element for experiment.
|
|
279
|
+
*/
|
|
280
|
+
private nextButton(display: HTMLElement) {
|
|
281
|
+
const next = this.getButton(display, "next");
|
|
282
|
+
next.addEventListener("click", () => this.jsPsych.finishTrial());
|
|
283
|
+
}
|
|
284
|
+
}
|