@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.
@@ -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 prefix - Prefix for the video recording file name (string). This is
104
- * the string that comes before "_<TIMESTAMP>.webm".
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(prefix: "consent" | "session_video" | "trial_video"): Promise<void>;
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",
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.3",
49
+ "@lookit/data": "^0.0.4",
50
+ "@lookit/templates": "^0.0.1",
50
51
  "jspsych": "^8.0.2"
51
52
  }
52
53
  }
@@ -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 consentVideoTrial from "../templates/consent-video-trial.hbs";
5
- import recordFeed from "../templates/record-feed.hbs";
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 = { 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;
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 = Handlebars.compile(consentVideoTrial)({});
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
- return;
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 = Handlebars.compile(consentVideoTrial)({});
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(consentVideoTrial)({}) +
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 = Handlebars.compile(consentVideoTrial)({});
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(consentVideoTrial)({
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 = Handlebars.compile(consentVideoTrial)({});
266
- jsPsych.finishTrial = jest.fn();
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(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
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
  });
@@ -1,16 +1,14 @@
1
+ import Data from "@lookit/data";
1
2
  import { LookitWindow } from "@lookit/data/dist/types";
2
- import Handlebars from "handlebars";
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: "consent_005" },
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
- 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
- });
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", consentVideoTrial);
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("consent");
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.jsPsych.finishTrial());
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
- .mockReturnValue({ finishTrial: jest.fn().mockImplementation() }),
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 () => {
@@ -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 prefix - Prefix for the video recording file name (string). This is
223
- * the string that comes before "_<TIMESTAMP>.webm".
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(prefix: "consent" | "session_video" | "trial_video") {
230
+ public async start(consent: boolean, trial_type: string) {
226
231
  this.initializeCheck();
227
232
 
228
- // Set filename
229
- this.filename = `${prefix}_${new Date().getTime()}.webm`;
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.start("session_video").then(() => {
33
- this.jsPsych.finishTrial();
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 "../templates/uploading-video.hbs";
4
+ import uploadingVideo from "../hbs/uploading-video.hbs";
5
5
  import { NoSessionRecordingError } from "./errors";
6
6
  import Recorder from "./recorder";
7
7