@lookit/record 0.0.3 → 0.0.4
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 +92 -78
- package/dist/consentVideo.d.ts +29 -14
- package/dist/errors.d.ts +0 -9
- package/dist/index.browser.js +2201 -18422
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +2 -4
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +2199 -18419
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2199 -18419
- package/dist/index.js.map +1 -1
- package/dist/recorder.d.ts +15 -3
- package/dist/trial.d.ts +8 -0
- package/dist/types.d.ts +5 -0
- package/package.json +3 -2
- package/src/consentVideo.spec.ts +79 -25
- package/src/consentVideo.ts +36 -35
- package/src/errors.ts +0 -12
- package/src/index.spec.ts +9 -3
- package/src/recorder.spec.ts +67 -5
- package/src/recorder.ts +36 -8
- package/src/start.ts +5 -3
- package/src/stop.ts +1 -1
- package/src/trial.ts +15 -1
- package/src/types.ts +8 -0
- package/src/video_config.spec.ts +1 -1
- package/src/video_config.ts +1 -1
- package/dist/utils.d.ts +0 -21
- package/src/utils.spec.ts +0 -55
- package/src/utils.ts +0 -119
package/dist/recorder.d.ts
CHANGED
|
@@ -100,10 +100,12 @@ export default class Recorder {
|
|
|
100
100
|
* Start recording. Also, adds event listeners for handling data and checks
|
|
101
101
|
* for recorder initialization.
|
|
102
102
|
*
|
|
103
|
-
* @param
|
|
104
|
-
*
|
|
103
|
+
* @param consent - Boolean indicating whether or not the recording is consent
|
|
104
|
+
* footage.
|
|
105
|
+
* @param trial_type - Trial type, as saved in the jsPsych data. This comes
|
|
106
|
+
* from the plugin info "name" value (not the class name).
|
|
105
107
|
*/
|
|
106
|
-
start(
|
|
108
|
+
start(consent: boolean, trial_type: string): Promise<void>;
|
|
107
109
|
/**
|
|
108
110
|
* Stop all streams/tracks. This stops any in-progress recordings and releases
|
|
109
111
|
* the media devices. This is can be called when recording is not in progress,
|
|
@@ -143,4 +145,14 @@ export default class Recorder {
|
|
|
143
145
|
private download;
|
|
144
146
|
/** Private helper to clear the webcam feed, if there is one. */
|
|
145
147
|
private clearWebcamFeed;
|
|
148
|
+
/**
|
|
149
|
+
* Creates a valid video file name based on parameters
|
|
150
|
+
*
|
|
151
|
+
* @param consent - Boolean indicating whether or not the recording is consent
|
|
152
|
+
* footage.
|
|
153
|
+
* @param trial_type - Trial type, as saved in the jsPsych data. This comes
|
|
154
|
+
* from the plugin info "name" value (not the class name).
|
|
155
|
+
* @returns File name string with .webm extension.
|
|
156
|
+
*/
|
|
157
|
+
private createFileName;
|
|
146
158
|
}
|
package/dist/trial.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export default class TrialRecordExtension implements JsPsychExtension {
|
|
|
4
4
|
private jsPsych;
|
|
5
5
|
static readonly info: JsPsychExtensionInfo;
|
|
6
6
|
private recorder?;
|
|
7
|
+
private pluginName;
|
|
7
8
|
/**
|
|
8
9
|
* Video recording extension.
|
|
9
10
|
*
|
|
@@ -25,4 +26,11 @@ export default class TrialRecordExtension implements JsPsychExtension {
|
|
|
25
26
|
* @returns Trial data.
|
|
26
27
|
*/
|
|
27
28
|
on_finish(): {};
|
|
29
|
+
/**
|
|
30
|
+
* Gets the plugin name for the trial that is being extended. This is same as
|
|
31
|
+
* the "trial_type" value that is stored in the data for this trial.
|
|
32
|
+
*
|
|
33
|
+
* @returns Plugin name string from the plugin class's info.
|
|
34
|
+
*/
|
|
35
|
+
private getCurrentPluginName;
|
|
28
36
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { JsPsychPlugin, PluginInfo } from "jspsych";
|
|
2
|
+
import { Class } from "type-fest";
|
|
3
|
+
export interface jsPsychPluginWithInfo extends Class<JsPsychPlugin<PluginInfo>> {
|
|
4
|
+
info: PluginInfo;
|
|
5
|
+
}
|
|
1
6
|
/**
|
|
2
7
|
* A valid CSS height/width value, which can be a number, a string containing a
|
|
3
8
|
* number with units, or 'auto'.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lookit/record",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Recording extensions and plugins for CHS studies.",
|
|
5
5
|
"homepage": "https://github.com/lookit/lookit-jspsych#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"typescript": "^5.6.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@lookit/data": "^0.0.
|
|
49
|
+
"@lookit/data": "^0.0.4",
|
|
50
|
+
"@lookit/templates": "^0.0.1",
|
|
50
51
|
"jspsych": "^8.0.2"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/consentVideo.spec.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import Data from "@lookit/data";
|
|
1
2
|
import { LookitWindow } from "@lookit/data/dist/types";
|
|
3
|
+
import chsTemplates from "@lookit/templates";
|
|
2
4
|
import Handlebars from "handlebars";
|
|
3
5
|
import { initJsPsych, PluginInfo, TrialType } from "jspsych";
|
|
4
|
-
import
|
|
5
|
-
import recordFeed from "../
|
|
6
|
+
import playbackFeed from "../hbs/playback-feed.hbs";
|
|
7
|
+
import recordFeed from "../hbs/record-feed.hbs";
|
|
6
8
|
import { VideoConsentPlugin } from "./consentVideo";
|
|
7
9
|
import {
|
|
8
10
|
ButtonNotFoundError,
|
|
@@ -13,7 +15,22 @@ import Recorder from "./recorder";
|
|
|
13
15
|
|
|
14
16
|
declare const window: LookitWindow;
|
|
15
17
|
|
|
18
|
+
window.chs = {
|
|
19
|
+
study: {
|
|
20
|
+
attributes: {
|
|
21
|
+
name: "name",
|
|
22
|
+
duration: "duration",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
response: {
|
|
26
|
+
id: "some id",
|
|
27
|
+
},
|
|
28
|
+
} as typeof window.chs;
|
|
29
|
+
|
|
16
30
|
jest.mock("./recorder");
|
|
31
|
+
jest.mock("@lookit/data", () => ({
|
|
32
|
+
updateResponse: jest.fn().mockReturnValue("Response"),
|
|
33
|
+
}));
|
|
17
34
|
|
|
18
35
|
test("Instantiate recorder", () => {
|
|
19
36
|
const jsPsych = initJsPsych();
|
|
@@ -25,16 +42,10 @@ test("Trial", () => {
|
|
|
25
42
|
const jsPsych = initJsPsych();
|
|
26
43
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
27
44
|
const display = document.createElement("div");
|
|
28
|
-
const trial = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
attributes: {
|
|
33
|
-
name: "name",
|
|
34
|
-
duration: "duration",
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
} as typeof window.chs;
|
|
45
|
+
const trial = {
|
|
46
|
+
locale: "en-us",
|
|
47
|
+
template: "consent-template-5",
|
|
48
|
+
} as unknown as TrialType<PluginInfo>;
|
|
38
49
|
|
|
39
50
|
plugin["recordFeed"] = jest.fn();
|
|
40
51
|
plugin["recordButton"] = jest.fn();
|
|
@@ -108,8 +119,12 @@ test("onEnded", () => {
|
|
|
108
119
|
const play = document.createElement("button");
|
|
109
120
|
const next = document.createElement("button");
|
|
110
121
|
const record = document.createElement("button");
|
|
122
|
+
const trial = {
|
|
123
|
+
locale: "en-us",
|
|
124
|
+
template: "consent-template-5",
|
|
125
|
+
} as unknown as TrialType<PluginInfo>;
|
|
111
126
|
|
|
112
|
-
display.innerHTML =
|
|
127
|
+
display.innerHTML = chsTemplates.consentVideo(trial);
|
|
113
128
|
plugin["recordFeed"] = jest.fn();
|
|
114
129
|
plugin["getButton"] = jest.fn().mockImplementation((_display, id) => {
|
|
115
130
|
switch (id) {
|
|
@@ -126,7 +141,8 @@ test("onEnded", () => {
|
|
|
126
141
|
} else if (id === "next") {
|
|
127
142
|
return next;
|
|
128
143
|
}
|
|
129
|
-
|
|
144
|
+
|
|
145
|
+
return "";
|
|
130
146
|
});
|
|
131
147
|
|
|
132
148
|
plugin["onEnded"](display)();
|
|
@@ -142,8 +158,12 @@ test("getButton", () => {
|
|
|
142
158
|
const jsPsych = initJsPsych();
|
|
143
159
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
144
160
|
const display = document.createElement("div");
|
|
161
|
+
const trial = {
|
|
162
|
+
locale: "en-us",
|
|
163
|
+
template: "consent-template-5",
|
|
164
|
+
} as unknown as TrialType<PluginInfo>;
|
|
145
165
|
|
|
146
|
-
display.innerHTML =
|
|
166
|
+
display.innerHTML = chsTemplates.consentVideo(trial);
|
|
147
167
|
|
|
148
168
|
expect(plugin["getButton"](display, "next").id).toStrictEqual("next");
|
|
149
169
|
});
|
|
@@ -171,10 +191,15 @@ test("recordButton", async () => {
|
|
|
171
191
|
const jsPsych = initJsPsych();
|
|
172
192
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
173
193
|
const display = document.createElement("div");
|
|
194
|
+
const trial = {
|
|
195
|
+
locale: "en-us",
|
|
196
|
+
template: "consent-template-5",
|
|
197
|
+
} as unknown as TrialType<PluginInfo>;
|
|
198
|
+
|
|
199
|
+
display.innerHTML = chsTemplates.consentVideo(trial);
|
|
174
200
|
|
|
175
201
|
display.innerHTML =
|
|
176
|
-
Handlebars.compile(
|
|
177
|
-
Handlebars.compile(recordFeed)({});
|
|
202
|
+
chsTemplates.consentVideo(trial) + Handlebars.compile(recordFeed)({});
|
|
178
203
|
|
|
179
204
|
plugin["recordButton"](display);
|
|
180
205
|
|
|
@@ -208,17 +233,21 @@ test("recordButton", async () => {
|
|
|
208
233
|
|
|
209
234
|
// Start recorder
|
|
210
235
|
expect(Recorder.prototype.start).toHaveBeenCalledTimes(1);
|
|
211
|
-
expect(Recorder.prototype.start).toHaveBeenCalledWith("consent");
|
|
236
|
+
expect(Recorder.prototype.start).toHaveBeenCalledWith(true, "consent-video");
|
|
212
237
|
});
|
|
213
238
|
|
|
214
239
|
test("playButton", () => {
|
|
215
240
|
const jsPsych = initJsPsych();
|
|
216
241
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
217
242
|
const display = document.createElement("div");
|
|
243
|
+
const trial = {
|
|
244
|
+
locale: "en-us",
|
|
245
|
+
template: "consent-template-5",
|
|
246
|
+
} as unknown as TrialType<PluginInfo>;
|
|
218
247
|
|
|
219
248
|
plugin["playbackFeed"] = jest.fn();
|
|
220
249
|
|
|
221
|
-
display.innerHTML =
|
|
250
|
+
display.innerHTML = chsTemplates.consentVideo(trial);
|
|
222
251
|
|
|
223
252
|
plugin["playButton"](display);
|
|
224
253
|
|
|
@@ -235,11 +264,13 @@ test("stopButton", async () => {
|
|
|
235
264
|
const jsPsych = initJsPsych();
|
|
236
265
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
237
266
|
const display = document.createElement("div");
|
|
267
|
+
const trial = {
|
|
268
|
+
locale: "en-us",
|
|
269
|
+
template: "consent-template-5",
|
|
270
|
+
} as unknown as TrialType<PluginInfo>;
|
|
238
271
|
|
|
239
272
|
display.innerHTML =
|
|
240
|
-
Handlebars.compile(
|
|
241
|
-
video_container_id: plugin["video_container_id"],
|
|
242
|
-
}) + Handlebars.compile(recordFeed)({});
|
|
273
|
+
chsTemplates.consentVideo(trial) + Handlebars.compile(recordFeed)({});
|
|
243
274
|
|
|
244
275
|
plugin["recordFeed"] = jest.fn();
|
|
245
276
|
plugin["stopButton"](display);
|
|
@@ -261,14 +292,37 @@ test("nextButton", () => {
|
|
|
261
292
|
const jsPsych = initJsPsych();
|
|
262
293
|
const plugin = new VideoConsentPlugin(jsPsych);
|
|
263
294
|
const display = document.createElement("div");
|
|
295
|
+
const trial = {
|
|
296
|
+
locale: "en-us",
|
|
297
|
+
template: "consent-template-5",
|
|
298
|
+
} as unknown as TrialType<PluginInfo>;
|
|
264
299
|
|
|
265
|
-
display.innerHTML =
|
|
266
|
-
|
|
300
|
+
display.innerHTML = chsTemplates.consentVideo(trial);
|
|
301
|
+
plugin["endTrial"] = jest.fn();
|
|
267
302
|
|
|
268
303
|
plugin["nextButton"](display);
|
|
269
304
|
display
|
|
270
305
|
.querySelector<HTMLButtonElement>("button#next")!
|
|
271
306
|
.dispatchEvent(new Event("click"));
|
|
272
307
|
|
|
273
|
-
expect(
|
|
308
|
+
expect(plugin["endTrial"]).toHaveBeenCalledTimes(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("endTrial", () => {
|
|
312
|
+
const jsPsych = initJsPsych();
|
|
313
|
+
const plugin = new VideoConsentPlugin(jsPsych);
|
|
314
|
+
|
|
315
|
+
plugin["endTrial"]();
|
|
316
|
+
|
|
317
|
+
expect(Data.updateResponse).toHaveBeenCalledWith(window.chs.response.id, {
|
|
318
|
+
completed_consent_frame: true,
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("Does video consent plugin return chsData correctly?", () => {
|
|
323
|
+
expect(VideoConsentPlugin.chsData()).toMatchObject({ chs_type: "consent" });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("Video playback should not be muted", () => {
|
|
327
|
+
expect(playbackFeed).not.toContain("muted");
|
|
274
328
|
});
|
package/src/consentVideo.ts
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
+
import Data from "@lookit/data";
|
|
1
2
|
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
|
-
import
|
|
3
|
+
import chsTemplates from "@lookit/templates";
|
|
3
4
|
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
|
4
5
|
import { version } from "../package.json";
|
|
5
|
-
import consentDocTemplate from "../templates/consent-document.hbs";
|
|
6
|
-
import consentVideoTrialTemplate from "../templates/consent-video-trial.hbs";
|
|
7
6
|
import {
|
|
8
7
|
ButtonNotFoundError,
|
|
9
8
|
ImageNotFoundError,
|
|
10
9
|
VideoContainerNotFoundError,
|
|
11
10
|
} from "./errors";
|
|
12
11
|
import Recorder from "./recorder";
|
|
13
|
-
import { initI18nAndTemplates } from "./utils";
|
|
14
12
|
|
|
15
13
|
declare const window: LookitWindow;
|
|
16
14
|
|
|
@@ -18,7 +16,7 @@ const info = <const>{
|
|
|
18
16
|
name: "consent-video",
|
|
19
17
|
version,
|
|
20
18
|
parameters: {
|
|
21
|
-
template: { type: ParameterType.STRING, default: "
|
|
19
|
+
template: { type: ParameterType.STRING, default: "consent-template-5" },
|
|
22
20
|
locale: { type: ParameterType.STRING, default: "en-us" },
|
|
23
21
|
additional_video_privacy_statement: {
|
|
24
22
|
type: ParameterType.STRING,
|
|
@@ -28,14 +26,14 @@ const info = <const>{
|
|
|
28
26
|
gdpr: { type: ParameterType.BOOL, default: false },
|
|
29
27
|
gdpr_personal_data: { type: ParameterType.STRING, default: "" },
|
|
30
28
|
gdpr_sensitive_data: { type: ParameterType.STRING, default: "" },
|
|
31
|
-
PIName: { type: ParameterType.STRING, default:
|
|
29
|
+
PIName: { type: ParameterType.STRING, default: undefined },
|
|
32
30
|
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:
|
|
31
|
+
institution: { type: ParameterType.STRING, default: undefined },
|
|
32
|
+
PIContact: { type: ParameterType.STRING, default: undefined },
|
|
33
|
+
payment: { type: ParameterType.STRING, default: undefined },
|
|
36
34
|
private_level_only: { type: ParameterType.BOOL, default: false },
|
|
37
|
-
procedures: { type: ParameterType.STRING, default:
|
|
38
|
-
purpose: { type: ParameterType.STRING, default:
|
|
35
|
+
procedures: { type: ParameterType.STRING, default: undefined },
|
|
36
|
+
purpose: { type: ParameterType.STRING, default: undefined },
|
|
39
37
|
research_rights_statement: { type: ParameterType.STRING, default: "" },
|
|
40
38
|
risk_statement: { type: ParameterType.STRING, default: "" },
|
|
41
39
|
voluntary_participation: { type: ParameterType.STRING, default: "" },
|
|
@@ -90,30 +88,11 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
90
88
|
* @param trial - Trial data including user supplied parameters.
|
|
91
89
|
*/
|
|
92
90
|
public trial(display: HTMLElement, trial: TrialType<Info>) {
|
|
93
|
-
|
|
94
|
-
const
|
|
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
|
-
});
|
|
91
|
+
// Get trial HTML string
|
|
92
|
+
const consentVideo = chsTemplates.consentVideo(trial);
|
|
114
93
|
|
|
115
94
|
// Add rendered document to display HTML
|
|
116
|
-
display.insertAdjacentHTML("afterbegin",
|
|
95
|
+
display.insertAdjacentHTML("afterbegin", consentVideo);
|
|
117
96
|
|
|
118
97
|
// Video recording HTML
|
|
119
98
|
this.recordFeed(display);
|
|
@@ -233,7 +212,7 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
233
212
|
play.disabled = true;
|
|
234
213
|
next.disabled = true;
|
|
235
214
|
this.getImg(display, "record-icon").style.visibility = "visible";
|
|
236
|
-
await this.recorder.start(
|
|
215
|
+
await this.recorder.start(true, VideoConsentPlugin.info.name);
|
|
237
216
|
});
|
|
238
217
|
}
|
|
239
218
|
|
|
@@ -279,6 +258,28 @@ export class VideoConsentPlugin implements JsPsychPlugin<Info> {
|
|
|
279
258
|
*/
|
|
280
259
|
private nextButton(display: HTMLElement) {
|
|
281
260
|
const next = this.getButton(display, "next");
|
|
282
|
-
next.addEventListener("click", () => this.
|
|
261
|
+
next.addEventListener("click", () => this.endTrial());
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Mark the response in the lookit-api database as having completed the
|
|
266
|
+
* consent frame, then finish the trial.
|
|
267
|
+
*/
|
|
268
|
+
private async endTrial() {
|
|
269
|
+
await Data.updateResponse(window.chs.response.id, {
|
|
270
|
+
completed_consent_frame: true,
|
|
271
|
+
});
|
|
272
|
+
this.jsPsych.finishTrial();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Add CHS type to experiment data. This will enable Lookit API to run the
|
|
277
|
+
* "consent" Frame Action Dispatcher method after the experiment has
|
|
278
|
+
* completed.
|
|
279
|
+
*
|
|
280
|
+
* @returns Object containing CHS type.
|
|
281
|
+
*/
|
|
282
|
+
public static chsData() {
|
|
283
|
+
return { chs_type: "consent" };
|
|
283
284
|
}
|
|
284
285
|
}
|
package/src/errors.ts
CHANGED
|
@@ -229,15 +229,3 @@ export class ImageNotFoundError extends Error {
|
|
|
229
229
|
super(`"${id}" image not found.`);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
|
|
233
|
-
/** Error throw what specified language isn't found */
|
|
234
|
-
export class TranslationNotFoundError extends Error {
|
|
235
|
-
/**
|
|
236
|
-
* This will be thrown when attempting to init i18n
|
|
237
|
-
*
|
|
238
|
-
* @param baseName - Language a2code with region
|
|
239
|
-
*/
|
|
240
|
-
public constructor(baseName: string) {
|
|
241
|
-
super(`"${baseName}" translation not found.`);
|
|
242
|
-
}
|
|
243
|
-
}
|
package/src/index.spec.ts
CHANGED
|
@@ -10,9 +10,12 @@ jest.mock("./recorder");
|
|
|
10
10
|
jest.mock("@lookit/data");
|
|
11
11
|
jest.mock("jspsych", () => ({
|
|
12
12
|
...jest.requireActual("jspsych"),
|
|
13
|
-
initJsPsych: jest
|
|
14
|
-
.fn()
|
|
15
|
-
|
|
13
|
+
initJsPsych: jest.fn().mockReturnValue({
|
|
14
|
+
finishTrial: jest.fn().mockImplementation(),
|
|
15
|
+
getCurrentTrial: jest
|
|
16
|
+
.fn()
|
|
17
|
+
.mockReturnValue({ type: { info: { name: "test-type" } } }),
|
|
18
|
+
}),
|
|
16
19
|
}));
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -41,6 +44,7 @@ test("Trial recording", () => {
|
|
|
41
44
|
const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
|
|
42
45
|
const jsPsych = initJsPsych();
|
|
43
46
|
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
47
|
+
const getCurrentPluginNameSpy = jest.spyOn(trialRec, "getCurrentPluginName");
|
|
44
48
|
|
|
45
49
|
trialRec.on_start();
|
|
46
50
|
trialRec.on_load();
|
|
@@ -48,7 +52,9 @@ test("Trial recording", () => {
|
|
|
48
52
|
|
|
49
53
|
expect(Recorder).toHaveBeenCalledTimes(1);
|
|
50
54
|
expect(mockRecStart).toHaveBeenCalledTimes(1);
|
|
55
|
+
expect(mockRecStart).toHaveBeenCalledWith(false, "test-type");
|
|
51
56
|
expect(mockRecStop).toHaveBeenCalledTimes(1);
|
|
57
|
+
expect(getCurrentPluginNameSpy).toHaveBeenCalledTimes(1);
|
|
52
58
|
});
|
|
53
59
|
|
|
54
60
|
test("Trial recording's initialize does nothing", async () => {
|
package/src/recorder.spec.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import Data from "@lookit/data";
|
|
2
|
+
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
3
|
import Handlebars from "handlebars";
|
|
3
4
|
import { initJsPsych } from "jspsych";
|
|
5
|
+
import playbackFeed from "../hbs/playback-feed.hbs";
|
|
6
|
+
import recordFeed from "../hbs/record-feed.hbs";
|
|
7
|
+
import webcamFeed from "../hbs/webcam-feed.hbs";
|
|
4
8
|
import play_icon from "../img/play-icon.svg";
|
|
5
9
|
import record_icon from "../img/record-icon.svg";
|
|
6
|
-
import playbackFeed from "../templates/playback-feed.hbs";
|
|
7
|
-
import recordFeed from "../templates/record-feed.hbs";
|
|
8
|
-
import webcamFeed from "../templates/webcam-feed.hbs";
|
|
9
10
|
import {
|
|
10
11
|
CreateURLError,
|
|
11
12
|
NoStopPromiseError,
|
|
@@ -19,6 +20,19 @@ import {
|
|
|
19
20
|
import Recorder from "./recorder";
|
|
20
21
|
import { CSSWidthHeight } from "./types";
|
|
21
22
|
|
|
23
|
+
declare const window: LookitWindow;
|
|
24
|
+
|
|
25
|
+
window.chs = {
|
|
26
|
+
study: {
|
|
27
|
+
id: "123",
|
|
28
|
+
},
|
|
29
|
+
response: {
|
|
30
|
+
id: "456",
|
|
31
|
+
},
|
|
32
|
+
} as typeof window.chs;
|
|
33
|
+
|
|
34
|
+
let originalDate: DateConstructor;
|
|
35
|
+
|
|
22
36
|
jest.mock("@lookit/data");
|
|
23
37
|
jest.mock("jspsych", () => ({
|
|
24
38
|
...jest.requireActual("jspsych"),
|
|
@@ -35,6 +49,13 @@ jest.mock("jspsych", () => ({
|
|
|
35
49
|
},
|
|
36
50
|
}),
|
|
37
51
|
},
|
|
52
|
+
data: {
|
|
53
|
+
getLastTrialData: jest.fn().mockReturnValue({
|
|
54
|
+
values: jest
|
|
55
|
+
.fn()
|
|
56
|
+
.mockReturnValue([{ trial_type: "test-type", trial_index: 0 }]),
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
38
59
|
}),
|
|
39
60
|
}));
|
|
40
61
|
|
|
@@ -68,7 +89,7 @@ test("Recorder start", async () => {
|
|
|
68
89
|
const jsPsych = initJsPsych();
|
|
69
90
|
const rec = new Recorder(jsPsych);
|
|
70
91
|
const media = jsPsych.pluginAPI.getCameraRecorder();
|
|
71
|
-
await rec.start("consent");
|
|
92
|
+
await rec.start(true, "video-consent");
|
|
72
93
|
|
|
73
94
|
expect(media.addEventListener).toHaveBeenCalledTimes(2);
|
|
74
95
|
expect(media.start).toHaveBeenCalledTimes(1);
|
|
@@ -112,7 +133,7 @@ test("Recorder initialize error", () => {
|
|
|
112
133
|
.fn()
|
|
113
134
|
.mockReturnValue(undefined);
|
|
114
135
|
|
|
115
|
-
expect(async () => await rec.start("consent")).rejects.toThrow(
|
|
136
|
+
expect(async () => await rec.start(true, "video-consent")).rejects.toThrow(
|
|
116
137
|
RecorderInitializeError,
|
|
117
138
|
);
|
|
118
139
|
|
|
@@ -444,3 +465,44 @@ test("Recorder insert record Feed with height/width", () => {
|
|
|
444
465
|
Handlebars.compile(recordFeed)(view),
|
|
445
466
|
);
|
|
446
467
|
});
|
|
468
|
+
|
|
469
|
+
test("Recorder createFileName constructs video file names correctly", () => {
|
|
470
|
+
const jsPsych = initJsPsych();
|
|
471
|
+
const rec = new Recorder(jsPsych);
|
|
472
|
+
|
|
473
|
+
// Mock Date().getTime() timestamp
|
|
474
|
+
originalDate = Date;
|
|
475
|
+
const mockTimestamp = 1634774400000;
|
|
476
|
+
jest.spyOn(global, "Date").mockImplementation(() => {
|
|
477
|
+
return new originalDate(mockTimestamp);
|
|
478
|
+
});
|
|
479
|
+
// Mock random 3-digit number
|
|
480
|
+
jest.spyOn(global.Math, "random").mockReturnValue(0.123456789);
|
|
481
|
+
const rand_digits = Math.floor(Math.random() * 1000);
|
|
482
|
+
|
|
483
|
+
const index = jsPsych.data.getLastTrialData().values()[0].trial_index + 1;
|
|
484
|
+
const trial_type = "test-type";
|
|
485
|
+
|
|
486
|
+
// Consent prefix is "consent-videoStream"
|
|
487
|
+
expect(rec["createFileName"](true, trial_type)).toBe(
|
|
488
|
+
`consent-videoStream_${window.chs.study.id}_${index.toString()}-${trial_type}_${window.chs.response.id}_${mockTimestamp.toString()}_${rand_digits.toString()}.webm`,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
// Non-consent prefix is "videoStream"
|
|
492
|
+
expect(rec["createFileName"](false, trial_type)).toBe(
|
|
493
|
+
`videoStream_${window.chs.study.id}_${index.toString()}-${trial_type}_${window.chs.response.id}_${mockTimestamp.toString()}_${rand_digits.toString()}.webm`,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
// Trial index is 0 if there's no value for 'last trial index' (jsPsych data is empty)
|
|
497
|
+
jsPsych.data.getLastTrialData = jest.fn().mockReturnValueOnce({
|
|
498
|
+
values: jest.fn().mockReturnValue([]),
|
|
499
|
+
});
|
|
500
|
+
expect(rec["createFileName"](false, trial_type)).toBe(
|
|
501
|
+
`videoStream_${window.chs.study.id}_${0}-${trial_type}_${window.chs.response.id}_${mockTimestamp.toString()}_${rand_digits.toString()}.webm`,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// Restore the original Date constructor
|
|
505
|
+
global.Date = originalDate;
|
|
506
|
+
// Restore Math.random
|
|
507
|
+
jest.spyOn(global.Math, "random").mockRestore();
|
|
508
|
+
});
|
package/src/recorder.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import Data from "@lookit/data";
|
|
2
2
|
import LookitS3 from "@lookit/data/dist/lookitS3";
|
|
3
|
+
import { LookitWindow } from "@lookit/data/dist/types";
|
|
3
4
|
import autoBind from "auto-bind";
|
|
4
5
|
import Handlebars from "handlebars";
|
|
5
6
|
import { JsPsych } from "jspsych";
|
|
7
|
+
import playbackFeed from "../hbs/playback-feed.hbs";
|
|
8
|
+
import recordFeed from "../hbs/record-feed.hbs";
|
|
9
|
+
import webcamFeed from "../hbs/webcam-feed.hbs";
|
|
6
10
|
import play_icon from "../img/play-icon.svg";
|
|
7
11
|
import record_icon from "../img/record-icon.svg";
|
|
8
|
-
import playbackFeed from "../templates/playback-feed.hbs";
|
|
9
|
-
import recordFeed from "../templates/record-feed.hbs";
|
|
10
|
-
import webcamFeed from "../templates/webcam-feed.hbs";
|
|
11
12
|
import {
|
|
12
13
|
CreateURLError,
|
|
13
14
|
NoStopPromiseError,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
} from "./errors";
|
|
21
22
|
import { CSSWidthHeight } from "./types";
|
|
22
23
|
|
|
24
|
+
declare const window: LookitWindow;
|
|
25
|
+
|
|
23
26
|
/** Recorder handles the state of recording and data storage. */
|
|
24
27
|
export default class Recorder {
|
|
25
28
|
private url?: string;
|
|
@@ -219,14 +222,16 @@ export default class Recorder {
|
|
|
219
222
|
* Start recording. Also, adds event listeners for handling data and checks
|
|
220
223
|
* for recorder initialization.
|
|
221
224
|
*
|
|
222
|
-
* @param
|
|
223
|
-
*
|
|
225
|
+
* @param consent - Boolean indicating whether or not the recording is consent
|
|
226
|
+
* footage.
|
|
227
|
+
* @param trial_type - Trial type, as saved in the jsPsych data. This comes
|
|
228
|
+
* from the plugin info "name" value (not the class name).
|
|
224
229
|
*/
|
|
225
|
-
public async start(
|
|
230
|
+
public async start(consent: boolean, trial_type: string) {
|
|
226
231
|
this.initializeCheck();
|
|
227
232
|
|
|
228
|
-
// Set filename
|
|
229
|
-
this.filename =
|
|
233
|
+
// Set video filename
|
|
234
|
+
this.filename = this.createFileName(consent, trial_type);
|
|
230
235
|
|
|
231
236
|
// Instantiate s3 object
|
|
232
237
|
if (!this.localDownload) {
|
|
@@ -350,4 +355,27 @@ export default class Recorder {
|
|
|
350
355
|
webcam_feed_element.remove();
|
|
351
356
|
}
|
|
352
357
|
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Creates a valid video file name based on parameters
|
|
361
|
+
*
|
|
362
|
+
* @param consent - Boolean indicating whether or not the recording is consent
|
|
363
|
+
* footage.
|
|
364
|
+
* @param trial_type - Trial type, as saved in the jsPsych data. This comes
|
|
365
|
+
* from the plugin info "name" value (not the class name).
|
|
366
|
+
* @returns File name string with .webm extension.
|
|
367
|
+
*/
|
|
368
|
+
private createFileName(consent: boolean, trial_type: string) {
|
|
369
|
+
// File name formats:
|
|
370
|
+
// consent: consent-videoStream_{study}_{frame_id}_{response}_{timestamp}_{random_digits}.webm
|
|
371
|
+
// non-consent: videoStream_{study}_{frame_id}_{response}_{timestamp}_{random_digits}.webm
|
|
372
|
+
const prefix = consent ? "consent-videoStream" : "videoStream";
|
|
373
|
+
const last_data = this.jsPsych.data.getLastTrialData().values();
|
|
374
|
+
const curr_trial_index = (
|
|
375
|
+
last_data.length > 0 ? last_data[last_data.length - 1].trial_index + 1 : 0
|
|
376
|
+
).toString();
|
|
377
|
+
const trial_id = `${curr_trial_index}-${trial_type}`;
|
|
378
|
+
const rand_digits = Math.floor(Math.random() * 1000);
|
|
379
|
+
return `${prefix}_${window.chs.study.id}_${trial_id}_${window.chs.response.id}_${new Date().getTime()}_${rand_digits}.webm`;
|
|
380
|
+
}
|
|
353
381
|
}
|
package/src/start.ts
CHANGED
|
@@ -29,8 +29,10 @@ export default class StartRecordPlugin implements JsPsychPlugin<Info> {
|
|
|
29
29
|
|
|
30
30
|
/** Trial function called by jsPsych. */
|
|
31
31
|
public trial() {
|
|
32
|
-
this.recorder
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
this.recorder
|
|
33
|
+
.start(false, `${StartRecordPlugin.info.name}-multiframe`)
|
|
34
|
+
.then(() => {
|
|
35
|
+
this.jsPsych.finishTrial();
|
|
36
|
+
});
|
|
35
37
|
}
|
|
36
38
|
}
|
package/src/stop.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
2
|
import Handlebars from "handlebars";
|
|
3
3
|
import { JsPsych, JsPsychPlugin } from "jspsych";
|
|
4
|
-
import uploadingVideo from "../
|
|
4
|
+
import uploadingVideo from "../hbs/uploading-video.hbs";
|
|
5
5
|
import { NoSessionRecordingError } from "./errors";
|
|
6
6
|
import Recorder from "./recorder";
|
|
7
7
|
|