@lookit/record 3.0.0 → 3.0.1

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.0",
3
+ "version": "3.0.1",
4
4
  "description": "Recording extensions and plugins for CHS studies.",
5
5
  "homepage": "https://github.com/lookit/lookit-jspsych#readme",
6
6
  "bugs": {
@@ -40,6 +40,7 @@ jest.mock("jspsych", () => ({
40
40
  pluginAPI: {
41
41
  getCameraRecorder: jest.fn().mockReturnValue({
42
42
  addEventListener: jest.fn(),
43
+ mimeType: "video/webm",
43
44
  start: jest.fn(),
44
45
  stop: jest.fn(),
45
46
  stream: {
@@ -356,7 +357,7 @@ test("Recorder reset", () => {
356
357
  expect(jsPsych.pluginAPI.initializeCameraRecorder).toHaveBeenCalledTimes(1);
357
358
  expect(jsPsych.pluginAPI.initializeCameraRecorder).toHaveBeenCalledWith(
358
359
  streamClone,
359
- undefined,
360
+ { mimeType: "video/webm" },
360
361
  );
361
362
  expect(rec["blobs"]).toStrictEqual([]);
362
363
 
@@ -506,3 +507,89 @@ test("Recorder createFileName constructs video file names correctly", () => {
506
507
  // Restore Math.random
507
508
  jest.spyOn(global.Math, "random").mockRestore();
508
509
  });
510
+
511
+ test("Initializing a new recorder gets the mime type from the initialization", () => {
512
+ const jsPsych = initJsPsych();
513
+ const originalInitializeCameraRecorder =
514
+ jsPsych.pluginAPI.initializeCameraRecorder;
515
+
516
+ const stream = {
517
+ active: true,
518
+ clone: jest.fn(),
519
+ getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
520
+ } as unknown as MediaStream;
521
+
522
+ jsPsych.pluginAPI.initializeCameraRecorder = jest
523
+ .fn()
524
+ .mockImplementation(
525
+ (stream: MediaStream, recorder_options: MediaRecorderOptions) => {
526
+ return {
527
+ addEventListener: jest.fn(),
528
+ mimeType: recorder_options.mimeType,
529
+ start: jest.fn(),
530
+ stop: jest.fn(),
531
+ stream: stream,
532
+ };
533
+ },
534
+ );
535
+
536
+ jsPsych.pluginAPI.getCameraRecorder = jest.fn().mockImplementation(() => {
537
+ return jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
538
+ });
539
+
540
+ // Initialize with vp9
541
+ let recorder_options: MediaRecorderOptions = {
542
+ mimeType: "video/webm;codecs=vp9,opus",
543
+ };
544
+ jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
545
+ const rec_1 = new Recorder(jsPsych);
546
+ // Called twice per construction - once for stream clone and once for mime type
547
+ expect(jsPsych.pluginAPI.getCameraRecorder).toHaveBeenCalledTimes(2);
548
+ expect(rec_1["mimeType"]).toBe("video/webm;codecs=vp9,opus");
549
+
550
+ // Initialize with vp8
551
+ recorder_options = {
552
+ mimeType: "video/webm;codecs=vp8,opus",
553
+ };
554
+ jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
555
+ const rec_2 = new Recorder(jsPsych);
556
+ expect(jsPsych.pluginAPI.getCameraRecorder).toHaveBeenCalledTimes(4);
557
+ expect(rec_2["mimeType"]).toBe("video/webm;codecs=vp8,opus");
558
+
559
+ jsPsych.pluginAPI.initializeCameraRecorder = originalInitializeCameraRecorder;
560
+ });
561
+
562
+ test("New recorder uses a default mime type if none is set already", () => {
563
+ const jsPsych = initJsPsych();
564
+ const originalInitializeCameraRecorder =
565
+ jsPsych.pluginAPI.initializeCameraRecorder;
566
+
567
+ const stream = {
568
+ active: true,
569
+ clone: jest.fn(),
570
+ getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
571
+ } as unknown as MediaStream;
572
+
573
+ jsPsych.pluginAPI.initializeCameraRecorder = jest
574
+ .fn()
575
+ .mockImplementation((stream: MediaStream) => {
576
+ return {
577
+ addEventListener: jest.fn(),
578
+ start: jest.fn(),
579
+ stop: jest.fn(),
580
+ stream: stream,
581
+ };
582
+ });
583
+
584
+ jsPsych.pluginAPI.getCameraRecorder = jest.fn().mockImplementation(() => {
585
+ return jsPsych.pluginAPI.initializeCameraRecorder(stream);
586
+ });
587
+
588
+ jsPsych.pluginAPI.initializeCameraRecorder(stream);
589
+ const rec = new Recorder(jsPsych);
590
+ // Called twice per construction - once for stream clone and once for mime type
591
+ expect(jsPsych.pluginAPI.getCameraRecorder).toHaveBeenCalledTimes(2);
592
+ expect(rec["mimeType"]).toBe("video/webm");
593
+
594
+ jsPsych.pluginAPI.initializeCameraRecorder = originalInitializeCameraRecorder;
595
+ });
package/src/recorder.ts CHANGED
@@ -34,6 +34,7 @@ export default class Recorder {
34
34
  private filename?: string;
35
35
  private stopPromise?: Promise<void>;
36
36
  private webcam_element_id = "lookit-jspsych-webcam";
37
+ private mimeType = "video/webm";
37
38
 
38
39
  private streamClone: MediaStream;
39
40
 
@@ -45,6 +46,8 @@ export default class Recorder {
45
46
  public constructor(private jsPsych: JsPsych) {
46
47
  this.streamClone = this.stream.clone();
47
48
  autoBind(this);
49
+ // Use the class instance's mimeType default as a fallback if we can't get the mime type from the initialized jsPsych recorder.
50
+ this.mimeType = this.recorder?.mimeType || this.mimeType;
48
51
  }
49
52
 
50
53
  /**
@@ -99,7 +102,11 @@ export default class Recorder {
99
102
  * @param opts - Media recorder options to use when setting up the recorder.
100
103
  */
101
104
  public initializeRecorder(stream: MediaStream, opts?: MediaRecorderOptions) {
102
- this.jsPsych.pluginAPI.initializeCameraRecorder(stream, opts);
105
+ const recorder_options: MediaRecorderOptions = {
106
+ ...opts,
107
+ mimeType: this.mimeType,
108
+ };
109
+ this.jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
103
110
  }
104
111
 
105
112
  /** Reset the recorder to be used again. */
@@ -132,6 +132,30 @@ beforeEach(() => {
132
132
  cameras: [devicesObj.cam1, devicesObj.cam2],
133
133
  mics: [devicesObj.mic1, devicesObj.mic2],
134
134
  };
135
+
136
+ // Global MediaRecorder.isTypeSupported
137
+ Object.defineProperty(global, "MediaRecorder", {
138
+ writable: true,
139
+ value: jest.fn().mockImplementation(() => ({
140
+ start: jest.fn(),
141
+ ondataavailable: jest.fn(),
142
+ onerror: jest.fn(),
143
+ state: "",
144
+ stop: jest.fn(),
145
+ pause: jest.fn(),
146
+ resume: jest.fn(),
147
+ })),
148
+ });
149
+
150
+ Object.defineProperty(MediaRecorder, "isTypeSupported", {
151
+ writable: true,
152
+ /**
153
+ * Placeholder for value
154
+ *
155
+ * @returns True
156
+ */
157
+ value: () => true,
158
+ });
135
159
  });
136
160
 
137
161
  afterEach(() => {
@@ -1096,3 +1120,94 @@ test("Video config onMicActivityLevel", () => {
1096
1120
  expect(video_config["micChecked"]).toBe(true);
1097
1121
  expect(event_pass.resolve).toHaveBeenCalled();
1098
1122
  });
1123
+
1124
+ test("Video config initializeAndCreateRecorder uses supported mime type", () => {
1125
+ const getCompatibleMimeTypeSpy = jest.spyOn(
1126
+ video_config,
1127
+ "getCompatibleMimeType",
1128
+ );
1129
+
1130
+ // getCameraRecorder is just a convenient way of grabbing the mock stream.
1131
+ video_config["initializeAndCreateRecorder"](
1132
+ jsPsych.pluginAPI.getCameraRecorder().stream,
1133
+ );
1134
+
1135
+ expect(getCompatibleMimeTypeSpy).toHaveBeenCalled();
1136
+ // isTypeSupported is already mocked to return true, so this should use the first mime type value in the list
1137
+ expect(jsPsych.pluginAPI.initializeCameraRecorder).toHaveBeenCalledWith(
1138
+ jsPsych.pluginAPI.getCameraRecorder().stream,
1139
+ { mimeType: "video/webm;codecs=vp9,opus" },
1140
+ );
1141
+ });
1142
+
1143
+ test("Video config initializeAndCreateRecorder uses default if no mime types are supported", () => {
1144
+ const getCompatibleMimeTypeSpy = jest.spyOn(
1145
+ video_config,
1146
+ "getCompatibleMimeType",
1147
+ );
1148
+
1149
+ // Override the isTypeSupported mock that is set in beforeEach
1150
+ // No type is supported
1151
+ jest.spyOn(MediaRecorder, "isTypeSupported").mockImplementation(() => {
1152
+ return false;
1153
+ });
1154
+ expect(MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus")).toBe(
1155
+ false,
1156
+ );
1157
+
1158
+ // getCameraRecorder is just a convenient way of grabbing the mock stream.
1159
+ video_config["initializeAndCreateRecorder"](
1160
+ jsPsych.pluginAPI.getCameraRecorder().stream,
1161
+ );
1162
+
1163
+ expect(getCompatibleMimeTypeSpy).toHaveBeenCalled();
1164
+ // If there are no compatible mime types, then it should use the default "video/webm" for initialization.
1165
+ expect(jsPsych.pluginAPI.initializeCameraRecorder).toHaveBeenCalledWith(
1166
+ jsPsych.pluginAPI.getCameraRecorder().stream,
1167
+ { mimeType: "video/webm" },
1168
+ );
1169
+ });
1170
+
1171
+ test("Video config getCompatibleMimeType gets correct mime type or null", () => {
1172
+ // Note - don't use 'mockImplementationOnce' for the isTypeSupported mock because isTypeSupported can be called multiple times by getCompatibleMimeType.
1173
+
1174
+ // Override the isTypeSupported mock that is set in beforeEach
1175
+ // 1. only supports vp9,opus
1176
+ const isTypeSupportedSpy = jest
1177
+ .spyOn(MediaRecorder, "isTypeSupported")
1178
+ .mockImplementation((type) => {
1179
+ if (type == "video/webm;codecs=vp9,opus") {
1180
+ return true;
1181
+ } else {
1182
+ return false;
1183
+ }
1184
+ });
1185
+ const mime_type_1 = video_config["getCompatibleMimeType"]();
1186
+ expect(mime_type_1).toBe("video/webm;codecs=vp9,opus");
1187
+
1188
+ // 2. only supports vp8,opus
1189
+ isTypeSupportedSpy.mockImplementation((type) => {
1190
+ if (type == "video/webm;codecs=vp8,opus") {
1191
+ return true;
1192
+ } else {
1193
+ return false;
1194
+ }
1195
+ });
1196
+ const mime_type_2 = video_config["getCompatibleMimeType"]();
1197
+ expect(mime_type_2).toBe("video/webm;codecs=vp8,opus");
1198
+
1199
+ // 3. supports vp9,opus and vp8,opus, should use the former
1200
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1201
+ isTypeSupportedSpy.mockImplementation((type) => {
1202
+ return true;
1203
+ });
1204
+ const mime_type_3 = video_config["getCompatibleMimeType"]();
1205
+ expect(mime_type_3).toBe("video/webm;codecs=vp9,opus");
1206
+
1207
+ // 4. none supported, should return null
1208
+ isTypeSupportedSpy.mockImplementation(() => {
1209
+ return false;
1210
+ });
1211
+ const mime_type_4 = video_config["getCompatibleMimeType"]();
1212
+ expect(mime_type_4).toBeNull();
1213
+ });
@@ -99,6 +99,7 @@ export default class VideoConfigPlugin implements JsPsychPlugin<Info> {
99
99
  private minVolume: number = 0.1;
100
100
  private micChecked: boolean = false;
101
101
  private processorNode: AudioWorkletNode | null = null;
102
+ private mimeType = "video/webm";
102
103
 
103
104
  /**
104
105
  * Constructor for video config plugin.
@@ -404,12 +405,20 @@ export default class VideoConfigPlugin implements JsPsychPlugin<Info> {
404
405
  * @param stream - Media stream returned from getUserMedia that should be used
405
406
  * to set up the jsPsych recorder.
406
407
  * @param opts - Media recorder options to use when setting up the recorder.
408
+ * This will include the mimeType property that is set via getMimeTypeCodec,
409
+ * as well as any other options that can passed via the calling context.
407
410
  */
408
411
  public initializeAndCreateRecorder = (
409
412
  stream: MediaStream,
410
413
  opts?: MediaRecorderOptions,
411
414
  ) => {
412
- this.jsPsych.pluginAPI.initializeCameraRecorder(stream, opts);
415
+ // If no mime types from the list are supported (getCompatibleMimeType returns null) then use the default.
416
+ this.mimeType = this.getCompatibleMimeType() || this.mimeType;
417
+ const recorder_options: MediaRecorderOptions = {
418
+ ...opts,
419
+ mimeType: this.mimeType,
420
+ };
421
+ this.jsPsych.pluginAPI.initializeCameraRecorder(stream, recorder_options);
413
422
  this.recorder = new Recorder(this.jsPsych);
414
423
  };
415
424
 
@@ -660,4 +669,32 @@ export default class VideoConfigPlugin implements JsPsychPlugin<Info> {
660
669
  next_button_el.classList.remove(`${html_params.step_complete_class}`);
661
670
  }
662
671
  };
672
+
673
+ /**
674
+ * Check support for recording containers/codecs, in order of preference, and
675
+ * get the first supported type. The first supported type found in the
676
+ * mime_types array is returned and will be passed to the "mimeType" property
677
+ * in the recorder options object that is passed to the recorder
678
+ * initialization function (jsPsych.pluginAPI.initializeCameraRecorder). If
679
+ * none of these types is supported, the function returns null.
680
+ *
681
+ * Note: we will likely need to continuously update the mime_types list as new
682
+ * formats become supported, we support other browsers/versions, etc.
683
+ *
684
+ * @returns Mime type string, or null (if none from the array are supported).
685
+ */
686
+ private getCompatibleMimeType() {
687
+ const mime_types = [
688
+ "video/webm;codecs=vp9,opus",
689
+ "video/webm;codecs=vp8,opus",
690
+ ];
691
+ let mime_type_index = 0;
692
+ while (mime_type_index < mime_types.length) {
693
+ if (MediaRecorder.isTypeSupported(mime_types[mime_type_index])) {
694
+ return mime_types[mime_type_index];
695
+ }
696
+ mime_type_index++;
697
+ }
698
+ return null;
699
+ }
663
700
  }