@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,268 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { initJsPsych, JsPsych } from "jspsych";
|
|
3
|
+
import {
|
|
4
|
+
audioContextMock,
|
|
5
|
+
AudioWorkletNodeMock,
|
|
6
|
+
} from "../fixtures/MockWebAudioAPI";
|
|
7
|
+
import { MicCheckError, NoStreamError } from "./errors";
|
|
8
|
+
import VideoConfigPlugin from "./video_config";
|
|
9
|
+
|
|
10
|
+
// The video config mic check relies on the WebAudio API, which is not available in Node/Jest/jsdom, so we'll mock it here.
|
|
11
|
+
// This is in a separate file to avoid polluting the other test environments with the WebAudio API mocks.
|
|
12
|
+
|
|
13
|
+
global.AudioContext = jest.fn(() => audioContextMock) as any;
|
|
14
|
+
global.AudioWorkletNode = AudioWorkletNodeMock as any;
|
|
15
|
+
|
|
16
|
+
/** Add mock registerProcessor to the global scope. */
|
|
17
|
+
global.registerProcessor = () => {};
|
|
18
|
+
|
|
19
|
+
jest.mock("jspsych", () => ({
|
|
20
|
+
...jest.requireActual("jspsych"),
|
|
21
|
+
initJsPsych: jest.fn().mockReturnValue({
|
|
22
|
+
pluginAPI: {
|
|
23
|
+
getCameraStream: jest.fn().mockReturnValue({
|
|
24
|
+
active: true,
|
|
25
|
+
clone: jest.fn(),
|
|
26
|
+
getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
|
|
27
|
+
}),
|
|
28
|
+
getCameraRecorder: jest.fn().mockReturnValue({
|
|
29
|
+
addEventListener: jest.fn(),
|
|
30
|
+
start: jest.fn(),
|
|
31
|
+
stop: jest.fn(),
|
|
32
|
+
stream: {
|
|
33
|
+
active: true,
|
|
34
|
+
clone: jest.fn(),
|
|
35
|
+
getTracks: jest.fn().mockReturnValue([{ stop: jest.fn() }]),
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
let display_el: HTMLBodyElement;
|
|
43
|
+
let jsPsych: JsPsych;
|
|
44
|
+
let video_config: VideoConfigPlugin;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jsPsych = initJsPsych();
|
|
48
|
+
display_el = document.getElementsByTagName("body")[0] as HTMLBodyElement;
|
|
49
|
+
video_config = new VideoConfigPlugin(jsPsych);
|
|
50
|
+
video_config["display_el"] = display_el;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
jest.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("Video config check mic", async () => {
|
|
58
|
+
video_config["setupPortOnMessage"] = jest
|
|
59
|
+
.fn()
|
|
60
|
+
.mockReturnValue(() => Promise.resolve());
|
|
61
|
+
|
|
62
|
+
const createMediaStreamSourceSpy = jest.spyOn(
|
|
63
|
+
audioContextMock,
|
|
64
|
+
"createMediaStreamSource",
|
|
65
|
+
);
|
|
66
|
+
const addModuleSpy = jest.spyOn(audioContextMock.audioWorklet, "addModule");
|
|
67
|
+
const createConnectProcessorSpy = jest.spyOn(
|
|
68
|
+
video_config,
|
|
69
|
+
"createConnectProcessor",
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const expectedAudioContext = new AudioContext();
|
|
73
|
+
const expectedMicrophone = expectedAudioContext.createMediaStreamSource(
|
|
74
|
+
jsPsych.pluginAPI.getCameraStream(),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
await video_config["checkMic"]();
|
|
78
|
+
|
|
79
|
+
expect(createMediaStreamSourceSpy).toHaveBeenCalledWith(
|
|
80
|
+
jsPsych.pluginAPI.getCameraStream(),
|
|
81
|
+
);
|
|
82
|
+
expect(addModuleSpy).toHaveBeenCalledWith("/static/js/mic_check.js");
|
|
83
|
+
expect(createConnectProcessorSpy).toHaveBeenCalledWith(
|
|
84
|
+
expectedAudioContext,
|
|
85
|
+
expectedMicrophone,
|
|
86
|
+
);
|
|
87
|
+
expect(video_config["setupPortOnMessage"]).toHaveBeenCalledWith(
|
|
88
|
+
video_config["minVolume"],
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("Throws MicCheckError with createConnectProcessor error", () => {
|
|
93
|
+
// Mock the resolution of the last promise in checkMic to make sure that the rejection/error occurs before this point.
|
|
94
|
+
video_config["setupPortOnMessage"] = jest
|
|
95
|
+
.fn()
|
|
96
|
+
.mockReturnValue(() => Promise.resolve());
|
|
97
|
+
expect(async () => await video_config["checkMic"]()).resolves;
|
|
98
|
+
|
|
99
|
+
// No error message
|
|
100
|
+
const mockError = jest.fn(() => {
|
|
101
|
+
const promise = new Promise<void>(() => {
|
|
102
|
+
throw "Error";
|
|
103
|
+
});
|
|
104
|
+
promise.catch(() => null); // Prevent an uncaught error here so that it propogates to the catch block.
|
|
105
|
+
return promise;
|
|
106
|
+
});
|
|
107
|
+
video_config["createConnectProcessor"] = mockError;
|
|
108
|
+
expect(async () => await video_config["checkMic"]()).rejects.toThrow(
|
|
109
|
+
MicCheckError,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const mockErrorMsg = jest.fn(() => {
|
|
113
|
+
const promise = new Promise<void>(() => {
|
|
114
|
+
throw new Error("This is an error message");
|
|
115
|
+
});
|
|
116
|
+
promise.catch(() => null); // Prevent an uncaught error here so that it propogates to the catch block.
|
|
117
|
+
return promise;
|
|
118
|
+
});
|
|
119
|
+
video_config["createConnectProcessor"] = mockErrorMsg;
|
|
120
|
+
expect(async () => await video_config["checkMic"]()).rejects.toThrow(
|
|
121
|
+
MicCheckError,
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("Throws MicCheckError with addModule error", () => {
|
|
126
|
+
// Mock the resolution of the last promise in checkMic to make sure that the rejection/error occurs before this point.
|
|
127
|
+
video_config["setupPortOnMessage"] = jest
|
|
128
|
+
.fn()
|
|
129
|
+
.mockReturnValue(() => Promise.resolve());
|
|
130
|
+
expect(async () => await video_config["checkMic"]()).resolves;
|
|
131
|
+
|
|
132
|
+
const mockError = jest.fn(() => {
|
|
133
|
+
const promise = new Promise<void>(() => {
|
|
134
|
+
throw "Error";
|
|
135
|
+
});
|
|
136
|
+
promise.catch(() => null); // Prevent an uncaught error here so that it propogates to the catch block.
|
|
137
|
+
return promise;
|
|
138
|
+
});
|
|
139
|
+
audioContextMock.audioWorklet.addModule = mockError;
|
|
140
|
+
expect(async () => await video_config["checkMic"]()).rejects.toThrow(
|
|
141
|
+
MicCheckError,
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("Throws MicCheckError with setupPortOnMessage error", () => {
|
|
146
|
+
// Mock the resolution of the last promise in checkMic to make sure that the rejection/error occurs before this point.
|
|
147
|
+
video_config["setupPortOnMessage"] = jest
|
|
148
|
+
.fn()
|
|
149
|
+
.mockReturnValue(() => Promise.resolve());
|
|
150
|
+
expect(async () => await video_config.checkMic()).resolves;
|
|
151
|
+
|
|
152
|
+
const mockError = jest.fn(() => {
|
|
153
|
+
const promise = new Promise<void>(() => {
|
|
154
|
+
throw "Error";
|
|
155
|
+
});
|
|
156
|
+
promise.catch(() => null); // Prevent an uncaught error here so that it propogates to the catch block.
|
|
157
|
+
return promise;
|
|
158
|
+
});
|
|
159
|
+
video_config["setupPortOnMessage"] = mockError;
|
|
160
|
+
expect(async () => await video_config["checkMic"]()).rejects.toThrow(
|
|
161
|
+
MicCheckError,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("checkMic should process microphone input and handle messages", () => {
|
|
166
|
+
const onMicActivityLevelSpy = jest.spyOn(
|
|
167
|
+
video_config,
|
|
168
|
+
"onMicActivityLevel" as never,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(video_config["processorNode"]).toBe(null);
|
|
172
|
+
|
|
173
|
+
// Setup the processor node.
|
|
174
|
+
const audioContext = new AudioContext();
|
|
175
|
+
video_config["processorNode"] = new AudioWorkletNode(
|
|
176
|
+
audioContext,
|
|
177
|
+
"mic-check-processor",
|
|
178
|
+
);
|
|
179
|
+
expect(video_config["processorNode"]).not.toBeNull();
|
|
180
|
+
video_config["setupPortOnMessage"](video_config["minVolume"]);
|
|
181
|
+
expect(video_config["processorNode"].port.onmessage).toBeTruthy();
|
|
182
|
+
|
|
183
|
+
expect(video_config["micChecked"]).toBe(false);
|
|
184
|
+
|
|
185
|
+
// Simulate a failing event
|
|
186
|
+
const failVol = 0.0001;
|
|
187
|
+
const mockEventFail = { data: { volume: failVol } } as MessageEvent;
|
|
188
|
+
if (
|
|
189
|
+
video_config["processorNode"] &&
|
|
190
|
+
video_config["processorNode"].port &&
|
|
191
|
+
video_config["processorNode"].port.onmessage
|
|
192
|
+
) {
|
|
193
|
+
video_config["processorNode"].port.onmessage(mockEventFail);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Verify onMicActivityLevel is called with params and micChecked is still false.
|
|
197
|
+
expect(onMicActivityLevelSpy).toHaveBeenCalledWith(
|
|
198
|
+
failVol,
|
|
199
|
+
video_config["minVolume"],
|
|
200
|
+
expect.any(Function),
|
|
201
|
+
);
|
|
202
|
+
expect(video_config["micChecked"]).toBe(false);
|
|
203
|
+
|
|
204
|
+
// Simulate a passing event
|
|
205
|
+
const passVol = 0.6;
|
|
206
|
+
const mockEventPass = { data: { volume: passVol } } as MessageEvent;
|
|
207
|
+
if (
|
|
208
|
+
video_config["processorNode"] &&
|
|
209
|
+
video_config["processorNode"].port &&
|
|
210
|
+
video_config["processorNode"].port.onmessage
|
|
211
|
+
) {
|
|
212
|
+
video_config["processorNode"].port.onmessage(mockEventPass);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Verify onMicActivityLevel is called with params and micChecked is set to true.
|
|
216
|
+
expect(onMicActivityLevelSpy).toHaveBeenCalledWith(
|
|
217
|
+
passVol,
|
|
218
|
+
video_config["minVolume"],
|
|
219
|
+
expect.any(Function),
|
|
220
|
+
);
|
|
221
|
+
expect(video_config["micChecked"]).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("Recorder setupPortOnMessage should setup port's on message callback", () => {
|
|
225
|
+
expect(video_config["processorNode"]).toBe(null);
|
|
226
|
+
|
|
227
|
+
// Setup the processor node.
|
|
228
|
+
const audioContext = new AudioContext();
|
|
229
|
+
video_config["processorNode"] = new AudioWorkletNode(
|
|
230
|
+
audioContext,
|
|
231
|
+
"mic-check-processor",
|
|
232
|
+
);
|
|
233
|
+
expect(video_config["processorNode"]).toBeTruthy();
|
|
234
|
+
|
|
235
|
+
video_config["onMicActivityLevel"] = jest.fn();
|
|
236
|
+
|
|
237
|
+
video_config["setupPortOnMessage"](video_config["minVolume"]);
|
|
238
|
+
expect(video_config["processorNode"].port.onmessage).toBeTruthy();
|
|
239
|
+
|
|
240
|
+
// Simulate a message event to test the message event callback.
|
|
241
|
+
const passVol = 0.6;
|
|
242
|
+
const mockEventPass = { data: { volume: passVol } } as MessageEvent;
|
|
243
|
+
if (
|
|
244
|
+
video_config["processorNode"] &&
|
|
245
|
+
video_config["processorNode"].port &&
|
|
246
|
+
video_config["processorNode"].port.onmessage
|
|
247
|
+
) {
|
|
248
|
+
video_config["processorNode"].port.onmessage(mockEventPass);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// The port message event should trigger onMicActivityLevel.
|
|
252
|
+
expect(video_config["onMicActivityLevel"]).toHaveBeenCalledWith(
|
|
253
|
+
passVol,
|
|
254
|
+
video_config["minVolume"],
|
|
255
|
+
expect.any(Function),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("Video config mic check throws error if no stream", () => {
|
|
260
|
+
const getCameraStreamSpy = jest
|
|
261
|
+
.spyOn(jsPsych.pluginAPI, "getCameraStream")
|
|
262
|
+
.mockImplementation(jest.fn().mockReturnValue(null));
|
|
263
|
+
|
|
264
|
+
expect(async () => {
|
|
265
|
+
await video_config["checkMic"]();
|
|
266
|
+
}).rejects.toThrow(NoStreamError);
|
|
267
|
+
expect(getCameraStreamSpy).toHaveBeenCalledTimes(1);
|
|
268
|
+
});
|