@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lookit/record",
3
- "version": "3.0.1",
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.0.0",
45
+ "@lookit/templates": "^2.1.0",
46
46
  "jspsych": "^8.0.3"
47
47
  }
48
48
  }
@@ -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(VideoContainerNotFoundError);
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["onEnded"] = jest.fn().mockReturnValue("some func");
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("onEnded", () => {
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["onEnded"](display)();
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
- ButtonNotFoundError,
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
- ImageNotFoundError,
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
  });
@@ -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 VideoContainerNotFoundError();
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(videoContainer, this.onEnded(display));
162
+ this.recorder.insertPlaybackFeed(
163
+ videoContainer,
164
+ this.onPlaybackEnded(display),
165
+ );
142
166
  }
143
167
 
144
168
  /**
145
- * Put back the webcam feed once the video recording has ended. This is used
146
- * with the "ended" Event.
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 onEnded(display: HTMLElement) {
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 ButtonNotFoundError(id);
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 ImageNotFoundError(id);
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
- record.disabled = false;
249
- play.disabled = false;
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 video container couldn't be found. */
200
- export class VideoContainerNotFoundError extends Error {
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
- * Button couldn't be found by ID field.
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}" button not found.`);
217
- this.name = "ButtonNotFoundError";
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
  }