@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
package/src/errors.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/** Error thrown when recorder is null. */
|
|
2
|
+
export class RecorderInitializeError extends Error {
|
|
3
|
+
/**
|
|
4
|
+
* When there isn't a recorder, provide the user with an explanation of what
|
|
5
|
+
* they could do to resolve the issue.
|
|
6
|
+
*/
|
|
7
|
+
public constructor() {
|
|
8
|
+
const message = "Neither camera nor microphone has been initialized.";
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "RecorderInitializeError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Error thrown when stream is inactive and recorder is started. */
|
|
15
|
+
export class StreamInactiveInitializeError extends Error {
|
|
16
|
+
/**
|
|
17
|
+
* Error check on initialize. Attempting to validate recorder is ready to
|
|
18
|
+
* start recording.
|
|
19
|
+
*/
|
|
20
|
+
public constructor() {
|
|
21
|
+
super(
|
|
22
|
+
"Stream is inactive when attempting to start recording. Recorder reset might be needed.",
|
|
23
|
+
);
|
|
24
|
+
this.name = "StreamInactiveInitializeError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Error thrown when stream data is available and recorder is started. */
|
|
29
|
+
export class StreamDataInitializeError extends Error {
|
|
30
|
+
/**
|
|
31
|
+
* Error check on recorder initialize. Attempt to validate recorder data array
|
|
32
|
+
* is empty and ready to start recording.
|
|
33
|
+
*/
|
|
34
|
+
public constructor() {
|
|
35
|
+
super(
|
|
36
|
+
"Stream data from another recording still available when attempting to start recording. Recorder reset might be needed. ",
|
|
37
|
+
);
|
|
38
|
+
this.name = "StreamDataInitializeError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Error thrown when trying to stop an active session recording that cannot be
|
|
44
|
+
* found.
|
|
45
|
+
*/
|
|
46
|
+
export class NoSessionRecordingError extends Error {
|
|
47
|
+
/**
|
|
48
|
+
* When trying to stop a recording that isn't found, provide the user with an
|
|
49
|
+
* explanation of what they could do to resolve the issue.
|
|
50
|
+
*/
|
|
51
|
+
public constructor() {
|
|
52
|
+
const message =
|
|
53
|
+
"Cannot stop a session recording because no active session recording was found. Maybe it needs to be started, or there was a problem starting the recording.";
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "NoSessionRecordingError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Error thrown when trying to trying to start a recording while another is
|
|
61
|
+
* already active.
|
|
62
|
+
*/
|
|
63
|
+
export class ExistingRecordingError extends Error {
|
|
64
|
+
/**
|
|
65
|
+
* When trying to start a recording but there is already an active recording
|
|
66
|
+
* in progress, provide the user with an explanation of what they could do to
|
|
67
|
+
* resolve the issue.
|
|
68
|
+
*/
|
|
69
|
+
public constructor() {
|
|
70
|
+
const message =
|
|
71
|
+
"Cannot start a new recording because an active recording was found. Maybe a session recording needs to be stopped, trial recording is being used during session recording, or there was a problem stopping a prior recording.";
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = "ExistingRecordingError";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Error thrown when trying to to stop the recorder and the stop promise doesn't
|
|
79
|
+
* exist.
|
|
80
|
+
*/
|
|
81
|
+
export class NoStopPromiseError extends Error {
|
|
82
|
+
/**
|
|
83
|
+
* When attempting to stop a recording but there's no stop promise to ensure
|
|
84
|
+
* the stop has completed.
|
|
85
|
+
*/
|
|
86
|
+
public constructor() {
|
|
87
|
+
const message =
|
|
88
|
+
"There is no Stop Promise, which means the recorder wasn't started properly.";
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = "NoStopPromiseError";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Error thrown when attempting an action that relies on an input stream, such
|
|
96
|
+
* as the mic volume check, but no such stream is found.
|
|
97
|
+
*/
|
|
98
|
+
export class NoStreamError extends Error {
|
|
99
|
+
/**
|
|
100
|
+
* When attempting an action that requires an input stream, such as the mic
|
|
101
|
+
* check, but no stream is found.
|
|
102
|
+
*/
|
|
103
|
+
public constructor() {
|
|
104
|
+
const message =
|
|
105
|
+
"No input stream found. Maybe the recorder was not initialized with initializeRecorder.";
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "NoStreamError";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Error thrown if there's a problem setting up the microphone input level
|
|
113
|
+
* check.
|
|
114
|
+
*/
|
|
115
|
+
export class MicCheckError extends Error {
|
|
116
|
+
/**
|
|
117
|
+
* Occurs if there's a problem setting up the mic check, including setting up
|
|
118
|
+
* the audio context and stream source, loading the audio worklet processor
|
|
119
|
+
* script, setting up the port message event handler, and resolving the
|
|
120
|
+
* promise chain via message events passed to onMicActivityLevel.
|
|
121
|
+
*
|
|
122
|
+
* @param err - Error passed into this error that is thrown in the catch
|
|
123
|
+
* block, if any. Errors passed to catch blocks must have type unknown.
|
|
124
|
+
*/
|
|
125
|
+
public constructor(err: unknown) {
|
|
126
|
+
let message = `There was a problem setting up and running the microphone check.`;
|
|
127
|
+
if (
|
|
128
|
+
err instanceof Object &&
|
|
129
|
+
"message" in err &&
|
|
130
|
+
typeof err.message === "string"
|
|
131
|
+
) {
|
|
132
|
+
message += ` ${err.message}`;
|
|
133
|
+
}
|
|
134
|
+
if (typeof err === "string") {
|
|
135
|
+
message += ` ${err}`;
|
|
136
|
+
}
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "MicCheckError";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Error thrown when attempting to access S3 object and it's, unknowingly,
|
|
144
|
+
* undefined.
|
|
145
|
+
*/
|
|
146
|
+
export class S3UndefinedError extends Error {
|
|
147
|
+
/**
|
|
148
|
+
* Provide feed back when recorder attempts to use S3 object and it's
|
|
149
|
+
* undefined.
|
|
150
|
+
*/
|
|
151
|
+
public constructor() {
|
|
152
|
+
super("S3 object is undefined.");
|
|
153
|
+
this.name = "S3UndefinedError";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Error thrown when attempting to reset recorder, but its stream is still
|
|
159
|
+
* active.
|
|
160
|
+
*/
|
|
161
|
+
export class StreamActiveOnResetError extends Error {
|
|
162
|
+
/**
|
|
163
|
+
* This error will be thrown when developer attempts to reset recorder while
|
|
164
|
+
* active.
|
|
165
|
+
*/
|
|
166
|
+
public constructor() {
|
|
167
|
+
super("Won't reset recorder. Stream is still active.");
|
|
168
|
+
this.name = "StreamActiveOnResetError";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Error thrown when attempting to select webcam element and it's not found. */
|
|
173
|
+
export class NoWebCamElementError extends Error {
|
|
174
|
+
/**
|
|
175
|
+
* Error thrown when attempting to retrieve webcam element and it's not in the
|
|
176
|
+
* DOM.
|
|
177
|
+
*/
|
|
178
|
+
public constructor() {
|
|
179
|
+
super("No webcam element found.");
|
|
180
|
+
this.name = "NoWebCamElementError";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Error thrown when attempting to create playback/download url and data array
|
|
186
|
+
* is empty.
|
|
187
|
+
*/
|
|
188
|
+
export class CreateURLError extends Error {
|
|
189
|
+
/**
|
|
190
|
+
* Throw this error when data array is empty and url still needs to be
|
|
191
|
+
* created. Sometimes this means the "reset()" method was called too early.
|
|
192
|
+
*/
|
|
193
|
+
public constructor() {
|
|
194
|
+
super("Video/audio URL couldn't be created. No data available.");
|
|
195
|
+
this.name = "CreateURLError";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
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 {
|
|
210
|
+
/**
|
|
211
|
+
* Button couldn't be found by ID field.
|
|
212
|
+
*
|
|
213
|
+
* @param id - HTML ID parameter.
|
|
214
|
+
*/
|
|
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.`);
|
|
230
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.png";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { LookitWindow } from "@lookit/data/dist/types";
|
|
2
|
+
import { initJsPsych } from "jspsych";
|
|
3
|
+
import { ExistingRecordingError, NoSessionRecordingError } from "./errors";
|
|
4
|
+
import Rec from "./index";
|
|
5
|
+
import Recorder from "./recorder";
|
|
6
|
+
|
|
7
|
+
declare const window: LookitWindow;
|
|
8
|
+
|
|
9
|
+
jest.mock("./recorder");
|
|
10
|
+
jest.mock("@lookit/data");
|
|
11
|
+
jest.mock("jspsych", () => ({
|
|
12
|
+
...jest.requireActual("jspsych"),
|
|
13
|
+
initJsPsych: jest
|
|
14
|
+
.fn()
|
|
15
|
+
.mockReturnValue({ finishTrial: jest.fn().mockImplementation() }),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Manual mock to set up window.chs value for testing.
|
|
20
|
+
*
|
|
21
|
+
* @param chs - Contents of chs storage.
|
|
22
|
+
*/
|
|
23
|
+
const setCHSValue = (chs = {}) => {
|
|
24
|
+
Object.defineProperty(global, "window", {
|
|
25
|
+
value: {
|
|
26
|
+
chs,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
setCHSValue();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
jest.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("Trial recording", () => {
|
|
40
|
+
const mockRecStart = jest.spyOn(Recorder.prototype, "start");
|
|
41
|
+
const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
|
|
42
|
+
const jsPsych = initJsPsych();
|
|
43
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
44
|
+
|
|
45
|
+
trialRec.on_start();
|
|
46
|
+
trialRec.on_load();
|
|
47
|
+
trialRec.on_finish();
|
|
48
|
+
|
|
49
|
+
expect(Recorder).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(mockRecStart).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(mockRecStop).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("Trial recording's initialize does nothing", async () => {
|
|
55
|
+
const jsPsych = initJsPsych();
|
|
56
|
+
const trialRec = new Rec.TrialRecordExtension(jsPsych);
|
|
57
|
+
|
|
58
|
+
expect(await trialRec.initialize()).toBeUndefined();
|
|
59
|
+
expect(Recorder).toHaveBeenCalledTimes(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("Start Recording", async () => {
|
|
63
|
+
const mockRecStart = jest.spyOn(Recorder.prototype, "start");
|
|
64
|
+
const jsPsych = initJsPsych();
|
|
65
|
+
const startRec = new Rec.StartRecordPlugin(jsPsych);
|
|
66
|
+
|
|
67
|
+
// manual mock
|
|
68
|
+
mockRecStart.mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
|
|
69
|
+
|
|
70
|
+
await startRec.trial();
|
|
71
|
+
|
|
72
|
+
expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
|
|
73
|
+
expect(() => {
|
|
74
|
+
new Rec.StartRecordPlugin(jsPsych);
|
|
75
|
+
}).toThrow(ExistingRecordingError);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("Stop Recording", async () => {
|
|
79
|
+
const mockRecStop = jest.spyOn(Recorder.prototype, "stop");
|
|
80
|
+
const jsPsych = initJsPsych();
|
|
81
|
+
|
|
82
|
+
setCHSValue({
|
|
83
|
+
sessionRecorder: new Recorder(jsPsych),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const stopRec = new Rec.StopRecordPlugin(jsPsych);
|
|
87
|
+
const display_element = jest
|
|
88
|
+
.fn()
|
|
89
|
+
.mockImplementation() as unknown as HTMLElement;
|
|
90
|
+
|
|
91
|
+
mockRecStop.mockImplementation(jest.fn().mockReturnValue(Promise.resolve()));
|
|
92
|
+
|
|
93
|
+
await stopRec.trial(display_element);
|
|
94
|
+
|
|
95
|
+
expect(jsPsych.finishTrial).toHaveBeenCalledTimes(1);
|
|
96
|
+
expect(window.chs.sessionRecorder).toBeNull();
|
|
97
|
+
expect(display_element.innerHTML).toStrictEqual("");
|
|
98
|
+
|
|
99
|
+
setCHSValue();
|
|
100
|
+
|
|
101
|
+
expect(async () => await new Rec.StopRecordPlugin(jsPsych)).rejects.toThrow(
|
|
102
|
+
NoSessionRecordingError,
|
|
103
|
+
);
|
|
104
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { VideoConsentPlugin } from "./consentVideo";
|
|
2
|
+
import StartRecordPlugin from "./start";
|
|
3
|
+
import StopRecordPlugin from "./stop";
|
|
4
|
+
import TrialRecordExtension from "./trial";
|
|
5
|
+
import VideoConfigPlugin from "./video_config";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
TrialRecordExtension,
|
|
9
|
+
StartRecordPlugin,
|
|
10
|
+
StopRecordPlugin,
|
|
11
|
+
VideoConfigPlugin,
|
|
12
|
+
VideoConsentPlugin,
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*";
|
package/src/mic_check.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const SMOOTHING_FACTOR = 0.99;
|
|
2
|
+
const SCALING_FACTOR = 5;
|
|
3
|
+
/**
|
|
4
|
+
* Audio Worklet Processor class for processing audio input streams. This is
|
|
5
|
+
* used by the Recorder to run a volume check on the microphone input stream.
|
|
6
|
+
* Source:
|
|
7
|
+
* https://www.webrtc-developers.com/how-to-know-if-my-microphone-works/#detect-noise-or-silence
|
|
8
|
+
*/
|
|
9
|
+
export default class MicCheckProcessor extends AudioWorkletProcessor {
|
|
10
|
+
_volume;
|
|
11
|
+
_micChecked;
|
|
12
|
+
/** Constructor for the mic check processor. */
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this._volume = 0;
|
|
16
|
+
this._micChecked = false;
|
|
17
|
+
/**
|
|
18
|
+
* Callback to handle a message event on the processor's port. This
|
|
19
|
+
* determines how the processor responds when the recorder posts a message
|
|
20
|
+
* to the processor with e.g. this.processorNode.port.postMessage({
|
|
21
|
+
* micChecked: true }).
|
|
22
|
+
*
|
|
23
|
+
* @param event - Message event generated from the 'postMessage' call, which
|
|
24
|
+
* includes, among other things, the data property.
|
|
25
|
+
* @param event.data - Data sent by the message emitter.
|
|
26
|
+
*/
|
|
27
|
+
this.port.onmessage = (event) => {
|
|
28
|
+
if (
|
|
29
|
+
event.data &&
|
|
30
|
+
event.data.micChecked &&
|
|
31
|
+
event.data.micChecked == true
|
|
32
|
+
) {
|
|
33
|
+
this._micChecked = true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Process method that implements the audio processing algorithm for the Audio
|
|
39
|
+
* Processor Worklet. "Although the method is not a part of the
|
|
40
|
+
* AudioWorkletProcessor interface, any implementation of
|
|
41
|
+
* AudioWorkletProcessor must provide a process() method." Source:
|
|
42
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process
|
|
43
|
+
* The process method can take the following arguments: inputs, outputs,
|
|
44
|
+
* parameters. Here we are only using inputs.
|
|
45
|
+
*
|
|
46
|
+
* @param inputs - An array of inputs from the audio stream (microphone)
|
|
47
|
+
* connnected to the node. Each item in the inputs array is an array of
|
|
48
|
+
* channels. Each channel is a Float32Array containing 128 samples. For
|
|
49
|
+
* example, inputs[n][m][i] will access n-th input, m-th channel of that
|
|
50
|
+
* input, and i-th sample of that channel.
|
|
51
|
+
* @returns Boolean indicating whether or not the Audio Worklet Node should
|
|
52
|
+
* remain active, even if the User Agent thinks it is safe to shut down. In
|
|
53
|
+
* this case, when the recorder decides that the mic check criteria has been
|
|
54
|
+
* met, it will return false (processor should be shut down), otherwise it
|
|
55
|
+
* will return true (processor should remain active).
|
|
56
|
+
*/
|
|
57
|
+
process(inputs) {
|
|
58
|
+
if (this._micChecked) {
|
|
59
|
+
return false;
|
|
60
|
+
} else {
|
|
61
|
+
const input = inputs[0];
|
|
62
|
+
const samples = input[0];
|
|
63
|
+
if (samples) {
|
|
64
|
+
const sumSquare = samples.reduce((p, c) => p + c * c, 0);
|
|
65
|
+
const rms =
|
|
66
|
+
Math.sqrt(sumSquare / (samples.length || 1)) * SCALING_FACTOR;
|
|
67
|
+
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
|
|
68
|
+
this.port.postMessage({ volume: this._volume });
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
registerProcessor("mic-check-processor", MicCheckProcessor);
|
package/src/mic_check.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const SMOOTHING_FACTOR = 0.99;
|
|
2
|
+
const SCALING_FACTOR = 5;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Audio Worklet Processor class for processing audio input streams. This is
|
|
6
|
+
* used by the Recorder to run a volume check on the microphone input stream.
|
|
7
|
+
* Source:
|
|
8
|
+
* https://www.webrtc-developers.com/how-to-know-if-my-microphone-works/#detect-noise-or-silence
|
|
9
|
+
*/
|
|
10
|
+
export default class MicCheckProcessor extends AudioWorkletProcessor {
|
|
11
|
+
private _volume: number;
|
|
12
|
+
private _micChecked: boolean;
|
|
13
|
+
/** Constructor for the mic check processor. */
|
|
14
|
+
public constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this._volume = 0;
|
|
17
|
+
this._micChecked = false;
|
|
18
|
+
/**
|
|
19
|
+
* Callback to handle a message event on the processor's port. This
|
|
20
|
+
* determines how the processor responds when the recorder posts a message
|
|
21
|
+
* to the processor with e.g. this.processorNode.port.postMessage({
|
|
22
|
+
* micChecked: true }).
|
|
23
|
+
*
|
|
24
|
+
* @param event - Message event generated from the 'postMessage' call, which
|
|
25
|
+
* includes, among other things, the data property.
|
|
26
|
+
* @param event.data - Data sent by the message emitter.
|
|
27
|
+
*/
|
|
28
|
+
this.port.onmessage = (event: MessageEvent) => {
|
|
29
|
+
if (
|
|
30
|
+
event.data &&
|
|
31
|
+
event.data.micChecked &&
|
|
32
|
+
event.data.micChecked == true
|
|
33
|
+
) {
|
|
34
|
+
this._micChecked = true;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Process method that implements the audio processing algorithm for the Audio
|
|
41
|
+
* Processor Worklet. "Although the method is not a part of the
|
|
42
|
+
* AudioWorkletProcessor interface, any implementation of
|
|
43
|
+
* AudioWorkletProcessor must provide a process() method." Source:
|
|
44
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process
|
|
45
|
+
* The process method can take the following arguments: inputs, outputs,
|
|
46
|
+
* parameters. Here we are only using inputs.
|
|
47
|
+
*
|
|
48
|
+
* @param inputs - An array of inputs from the audio stream (microphone)
|
|
49
|
+
* connnected to the node. Each item in the inputs array is an array of
|
|
50
|
+
* channels. Each channel is a Float32Array containing 128 samples. For
|
|
51
|
+
* example, inputs[n][m][i] will access n-th input, m-th channel of that
|
|
52
|
+
* input, and i-th sample of that channel.
|
|
53
|
+
* @returns Boolean indicating whether or not the Audio Worklet Node should
|
|
54
|
+
* remain active, even if the User Agent thinks it is safe to shut down. In
|
|
55
|
+
* this case, when the recorder decides that the mic check criteria has been
|
|
56
|
+
* met, it will return false (processor should be shut down), otherwise it
|
|
57
|
+
* will return true (processor should remain active).
|
|
58
|
+
*/
|
|
59
|
+
public process(inputs: Float32Array[][]) {
|
|
60
|
+
if (this._micChecked) {
|
|
61
|
+
return false;
|
|
62
|
+
} else {
|
|
63
|
+
const input = inputs[0];
|
|
64
|
+
const samples = input[0];
|
|
65
|
+
if (samples) {
|
|
66
|
+
const sumSquare = samples.reduce((p, c) => p + c * c, 0);
|
|
67
|
+
const rms =
|
|
68
|
+
Math.sqrt(sumSquare / (samples.length || 1)) * SCALING_FACTOR;
|
|
69
|
+
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
|
|
70
|
+
this.port.postMessage({ volume: this._volume });
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
registerProcessor("mic-check-processor", MicCheckProcessor);
|