Skip to content

Commit

Permalink
video-config plugin (#45)
Browse files Browse the repository at this point in the history
* add video-config plugin to record package
* add video-config tests
* move setup-related methods from recorder.ts to video-config
* put mic check in typescript file and add tsc build step to generate js file
* add mocks for WebAudioAPI and test coverage for recorder checkMic
* add video-config CSS to record package index.scss
* add parameter for custom intro text in troubleshooting section
* add troubleshooting screenshots with rollup plugin-image and a filemock for test
* update Record docs with video-config parameters and example
---------
Co-authored-by: CJ Green <[email protected]>
  • Loading branch information
becky-gilbert authored Oct 3, 2024
1 parent b729922 commit 7f07fff
Show file tree
Hide file tree
Showing 27 changed files with 2,427 additions and 3,691 deletions.
3,230 changes: 11 additions & 3,219 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion packages/record/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,30 @@ This package contains the plugins and extensions to record audio and/or video of

## Video Configuration

To record any video during an experiment, including a consent video, you will have to add a video configuration trial that allows the user to give permissions and select the correct camera and microphone. This trial also does some basic checks on the webcam and mic inputs, so that the participant can fix common problems before the experiment starts.
To record _any_ video during an experiment, including a consent video, you must add a video configuration trial. This trial allows the user to give permissions and select the correct camera and microphone. This trial also does some basic checks on the webcam and mic inputs, so that the participant can fix common problems before the experiment starts.

Create a video configuration trial and put it in your experiment timeline prior to any other trials that use the participant's webcam/microphone. The trial type is `chsRecord.VideoConfigPlugin`.

```javascript
const videoConfig = { type: chsRecord.VideoConfigPlugin };
```

### Parameters

| Parameter | Type | Default Value | Description |
| --------------------- | ----------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| troubleshooting_intro | HTML string | "" | Optional text to add at the start of the "Setup tips and troubleshooting" section. This string allows HTML formatting (e.g. "<strong></strong>" for bold, "<em></em>" for italics). |

### Examples

```javascript
const videoConfig = {
type: chsRecord.VideoConfigPlugin,
troubleshooting_intro:
"If you're having any trouble getting your webcam set up, please feel free to call the XYZ lab at (123) 456-7890 and we'd be glad to help you out!",
};
```

## Trial Recording

To record a single trial, you will have to first load the extension in `initJsPsych`.
Expand Down
1 change: 1 addition & 0 deletions packages/record/fixtures/fileMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
Binary file added packages/record/img/chrome_initialprompt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/record/img/chrome_step1_alwaysallow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/record/img/chrome_step1_permissions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/record/img/firefox_initialprompt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/record/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ module.exports = {
...config.testEnvironmentOptions,
url: "https://localhost:8000/exp/studies/j/1647e101-282a-4fde-a32b-4f493d14f57e/8a2b2f04-63eb-485a-8e55-7b9362368f19/",
},
moduleNameMapper: {
"\\.(png)$": "../fixtures/fileMock.ts",
},
};
1 change: 1 addition & 0 deletions packages/record/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"devDependencies": {
"@jspsych/config": "^2.0.0",
"@jspsych/test-utils": "^1.2.0",
"@rollup/plugin-image": "^3.0.3",
"@types/audioworklet": "^0.0.60",
"@types/mustache": "^4.2.5",
Expand Down
1 change: 0 additions & 1 deletion packages/record/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default makeRollupConfig("chsRecord").map((config) => {
importAsString({
include: ["**/*.mustache"],
}),

image(),
],
};
Expand Down
182 changes: 182 additions & 0 deletions packages/record/scss/index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,181 @@
div#lookit-jspsych-video-config {
margin-top: 5%;
margin-bottom: 10%;

button.lookit-jspsych-btn {
font-size: 18px;
}

/* Webcam/instructions row container */
#lookit-jspsych-video-config-row-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
}

/* Title container */
#lookit-jspsych-video-config-title-row {
text-align: left;
margin-bottom: 10px;
margin-left: 10%;
}

/* Webcam column */
#lookit-jspsych-video-config-webcam-column {
flex-basis: 40%;
margin: 5px;
min-width: 300px;
margin-bottom: 1.6em;
}

/* Webcam container */
#lookit-jspsych-webcam-container {
width: 100%;
margin-bottom: 5px;
min-width: 300px;
min-height: 225px;
}

/* Container for elements displayed below the webcam feed (reload and device selection) */
#lookit-jspsych-webcam-buttons {
display: flex;
flex-direction: row;
}

/* Webcam reload button container */
#lookit-jspsych-webcam-reload-container {
flex: none;
align-content: center;
}

/* Webcam/mic device selection container */
#lookit-jspsych-device-selection-container {
flex: auto;
}

/* Device selection elements */
.lookit-jspsych-device-selection {
font-size: 14px;
font-family: "Open Sans", "Arial", sans-serif;
padding: 4px;
}

#lookit-jspsych-video-config-instructions-column {
flex-basis: 40%;
margin: 5px;
text-align: left;
}

/* Error/info message div */
#lookit-jspsych-video-config-errors {
min-height: 3em;
margin-top: -3em;
text-align: center;
font-weight: bold;
color: red;
}

/* Instructions */
#lookit-jspsych-video-config-instructions {
margin-top: 0;
}

/* Next button container */
#lookit-jspsych-next-container {
padding: 10px 0px;
}

/* Instruction step complete class */
.lookit-jspsych-step-complete {
background-color: lightgreen;
font-weight: bold;
}

/** Help text, such as that about browser compatibility */
.lookit-jspsych-help-text {
font-size: 0.8em;
line-height: 1.5em;
text-align: left;
margin-top: 10px;
}

#lookit-jspsych-config-troubleshooting-column-container {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
align-items: flex-start;
justify-content: center;
margin-left: auto;
margin-right: auto;
margin-bottom: 10%;
max-width: 90%;
}

div#lookit-jspsych-accordion-header-container {
text-align: center;
width: 100%;
}

div#lookit-jspsych-accordion-container {
text-align: left;
}

p#lookit-jspsych-troubleshooting-intro {
text-align: left;
padding: 0 18px;
}

/* Accordion section titles (buttons) for opening/closing the accordion panel content. */
button.lookit-jspsych-accordion {
background-color: #eee;
color: #444;
cursor: pointer;
padding: 18px;
width: 100%;
text-align: left;
border: none;
outline: none;
transition: 0.4s;
float: left;
font-size: 1.2em;
font-weight: bold;
}

/* Background color for the accordion section (button) when "active" (selected) and on hover */
.active,
button.lookit-jspsych-accordion:hover {
background-color: #ccc;
}

/* Content within each accordion button. */
div.lookit-jspsych-accordion-panel {
padding: 0 18px;
background-color: white;
display: none;
overflow: hidden;
}

button.lookit-jspsych-accordion:after {
content: "\02795"; /* Unicode character for "plus" sign (+) */
font-size: 13px;
color: #777;
float: right;
margin-left: 5px;
}

button.lookit-jspsych-accordion.active:after {
content: "\2796"; /* Unicode character for "minus" sign (-) */
}

.lookit-jspsych-screenshot {
display: block;
margin: 15px auto;
max-width: 30%;
}
}

img#record-icon,
img#play-icon {
top: 7px;
Expand All @@ -6,6 +184,10 @@ img#play-icon {
position: absolute;
}

img#record-icon {
visibility: "hidden";
}

div#lookit-jspsych-video-container {
position: relative;
width: min-content;
Expand Down
18 changes: 14 additions & 4 deletions packages/record/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class NoStreamError extends Error {
*/
public constructor() {
const message =
"No input stream found. Maybe the recorder was not initialized with intializeRecorder.";
"No input stream found. Maybe the recorder was not initialized with initializeRecorder.";
super(message);
this.name = "NoStreamError";
}
Expand All @@ -120,10 +120,20 @@ export class MicCheckError extends Error {
* promise chain via message events passed to onMicActivityLevel.
*
* @param err - Error passed into this error that is thrown in the catch
* block, if any.
* block, if any. Errors passed to catch blocks must have type unknown.
*/
public constructor(err: Error) {
const message = `There was a problem setting up and running the microphone check. ${err.message}`;
public constructor(err: unknown) {
let message = `There was a problem setting up and running the microphone check.`;
if (
err instanceof Object &&
"message" in err &&
typeof err.message === "string"
) {
message += ` ${err.message}`;
}
if (typeof err === "string") {
message += ` ${err}`;
}
super(message);
this.name = "MicCheckError";
}
Expand Down
1 change: 1 addition & 0 deletions packages/record/src/img-import.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.png";
3 changes: 2 additions & 1 deletion packages/record/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare const window: LookitWindow;
jest.mock("./recorder");
jest.mock("@lookit/data");
jest.mock("jspsych", () => ({
...jest.requireActual("jspsych"),
initJsPsych: jest
.fn()
.mockReturnValue({ finishTrial: jest.fn().mockImplementation() }),
Expand Down Expand Up @@ -79,7 +80,7 @@ test("Stop Recording", async () => {
const jsPsych = initJsPsych();

setCHSValue({
sessionRecorder: new Recorder(jsPsych, "prefix"),
sessionRecorder: new Recorder(jsPsych),
});

const stopRec = new Rec.StopRecordPlugin(jsPsych);
Expand Down
2 changes: 2 additions & 0 deletions packages/record/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { VideoConsentPlugin } from "./consentVideo";
import StartRecordPlugin from "./start";
import StopRecordPlugin from "./stop";
import TrialRecordExtension from "./trial";
import VideoConfigPlugin from "./video_config";

export default {
TrialRecordExtension,
StartRecordPlugin,
StopRecordPlugin,
VideoConfigPlugin,
VideoConsentPlugin,
};
11 changes: 7 additions & 4 deletions packages/record/src/mic_check.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@ export default class MicCheckProcessor extends AudioWorkletProcessor {
} else {
const input = inputs[0];
const samples = input[0];
const sumSquare = samples.reduce((p, c) => p + c * c, 0);
const rms = Math.sqrt(sumSquare / (samples.length || 1)) * SCALING_FACTOR;
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
this.port.postMessage({ volume: this._volume });
if (samples) {
const sumSquare = samples.reduce((p, c) => p + c * c, 0);
const rms =
Math.sqrt(sumSquare / (samples.length || 1)) * SCALING_FACTOR;
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
this.port.postMessage({ volume: this._volume });
}
return true;
}
}
Expand Down
11 changes: 7 additions & 4 deletions packages/record/src/mic_check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ export default class MicCheckProcessor extends AudioWorkletProcessor {
} else {
const input = inputs[0];
const samples = input[0];
const sumSquare = samples.reduce((p, c) => p + c * c, 0);
const rms = Math.sqrt(sumSquare / (samples.length || 1)) * SCALING_FACTOR;
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
this.port.postMessage({ volume: this._volume });
if (samples) {
const sumSquare = samples.reduce((p, c) => p + c * c, 0);
const rms =
Math.sqrt(sumSquare / (samples.length || 1)) * SCALING_FACTOR;
this._volume = Math.max(rms, this._volume * SMOOTHING_FACTOR);
this.port.postMessage({ volume: this._volume });
}
return true;
}
}
Expand Down
Loading

0 comments on commit 7f07fff

Please sign in to comment.