Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jsPsych as global variable #528

Merged
merged 13 commits into from
Aug 15, 2024
7 changes: 5 additions & 2 deletions src/App/components/JsPsychExperiment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from "react";
import { ENV } from "../../config/";
import { buildTimeline, jsPsychOptions } from "../../experiment";
import { initParticipant } from "../deployments/firebase";
import { getJsPsych } from "../../lib/utils";

// ID used to identify the DOM element that holds the experiment.
const EXPERIMENT_ID = "experiment-window";
Expand Down Expand Up @@ -67,8 +68,10 @@ export default function JsPsychExperiment({
*/
React.useEffect(() => {
if (jsPsych) {
const timeline = buildTimeline(jsPsych, studyID, participantID);
jsPsych.run(timeline);
// set up jsPsych object as global variable
window.jsPsych = jsPsych;
const timeline = buildTimeline(studyID, participantID);
getJsPsych().run(timeline);
}
}, [jsPsych]);

Expand Down
13 changes: 6 additions & 7 deletions src/experiment/honeycomb.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,20 @@ export const honeycombOptions = {
* Take a look at how the code here compares to the jsPsych documentation!
* See the jsPsych documentation for more: https://www.jspsych.org/7.3/tutorials/rt-task/
*
* @param {Object} jsPsych The jsPsych instance being used to run the task
* @returns {Object} A jsPsych timeline object
*/
export function buildHoneycombTimeline(jsPsych) {
export const buildHoneycombTimeline = () => {
// Build the trials that make up the start procedure
const startProcedure = buildStartProcedure(jsPsych);
const startProcedure = buildStartProcedure();

// Build the trials that make up the task procedure
const honeycombProcedure = buildHoneycombProcedure(jsPsych);
const honeycombProcedure = buildHoneycombProcedure();

// Builds the trial needed to debrief the participant on their performance
const debriefTrial = buildDebriefTrial(jsPsych);
const debriefTrial = buildDebriefTrial;

// Builds the trials that make up the end procedure
const endProcedure = buildEndProcedure(jsPsych);
const endProcedure = buildEndProcedure();

const timeline = [
startProcedure,
Expand All @@ -56,4 +55,4 @@ export function buildHoneycombTimeline(jsPsych) {
endProcedure,
];
return timeline;
}
};
4 changes: 2 additions & 2 deletions src/experiment/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export const jsPsychOptions = honeycombOptions;
* @param {string} participantID The ID of the participant that was just logged in
* @returns The timeline for JsPsych to run
*/
export function buildTimeline(jsPsych, studyID, participantID) {
export function buildTimeline(studyID, participantID) {
console.log(`Building timeline for participant ${participantID} on study ${studyID}`);

/**
* ! Your timeline should be built in a newly created function, not this one
* https://brown-ccv.github.io/honeycomb-docs/docs/quick_start#2-add-a-file-for-the-task
*/
const timeline = buildHoneycombTimeline(jsPsych);
const timeline = buildHoneycombTimeline();
return timeline;
}
7 changes: 3 additions & 4 deletions src/experiment/procedures/endProcedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@ import { exitFullscreenTrial } from "../trials/fullscreen";
* 1) Trial used to complete the user's camera recording is displayed
* 2) The experiment exits fullscreen
*
* @param {Object} jsPsych The jsPsych instance being used to run the task
* @returns {Object} A jsPsych (nested) timeline object
*/
export function buildEndProcedure(jsPsych) {
export const buildEndProcedure = () => {
const procedure = [];

// Conditionally add the camera breakdown trials
if (ENV.USE_CAMERA) {
procedure.push(buildCameraEndTrial(jsPsych));
procedure.push(buildCameraEndTrial);
}

// Add the other trials needed to end the experiment
procedure.push(exitFullscreenTrial, conclusionTrial);

// Return the block as a nested timeline
return { timeline: procedure };
}
};
16 changes: 7 additions & 9 deletions src/experiment/procedures/honeycombProcedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ENV, SETTINGS } from "../../config/";
import { eventCodes } from "../../config/trigger";
import { pdSpotEncode, photodiodeGhostBox } from "../../lib/markup/photodiode";
import { buildFixationTrial } from "../trials/fixation";
import { getJsPsych } from "../../lib/utils";

/**
* Builds the block of trials that form the core of the Honeycomb experiment
Expand All @@ -12,14 +13,11 @@ import { buildFixationTrial } from "../trials/fixation";
*
* Note that the block is conditionally rendered and repeated based on the task settings
*
* @param {Object} jsPsych The jsPsych instance being used to run the task
* @returns {Object} A jsPsych (nested) timeline object
*/
export function buildHoneycombProcedure(jsPsych) {
export const buildHoneycombProcedure = () => {
const honeycombSettings = SETTINGS.honeycomb;

const fixationTrial = buildFixationTrial(jsPsych);

const fixationTrial = buildFixationTrial;
/**
* Displays a colored circle and waits for participant to response with a keyboard press
*
Expand All @@ -31,7 +29,7 @@ export function buildHoneycombProcedure(jsPsych) {
const taskTrial = {
type: imageKeyboardResponse,
// Display the image passed as a timeline variable
stimulus: jsPsych.timelineVariable("stimulus"),
stimulus: getJsPsych().timelineVariable("stimulus"),
prompt: function () {
// Conditionally displays the photodiodeGhostBox
if (ENV.USE_PHOTODIODE) return photodiodeGhostBox;
Expand All @@ -42,15 +40,15 @@ export function buildHoneycombProcedure(jsPsych) {
data: {
// Record the correct_response passed as a timeline variable
code: eventCodes.honeycomb,
correct_response: jsPsych.timelineVariable("correct_response"),
correct_response: getJsPsych().timelineVariable("correct_response"),
},
on_load: function () {
// Conditionally flashes the photodiode when the trial first loads
if (ENV.USE_PHOTODIODE) pdSpotEncode(eventCodes.honeycomb);
},
// Add a boolean value ("correct") to the data - if the user responded with the correct key or not
on_finish: function (data) {
data.correct = jsPsych.pluginAPI.compareKeys(data.response, data.correct_response);
data.correct = getJsPsych().pluginAPI.compareKeys(data.response, data.correct_response);
},
};

Expand All @@ -70,4 +68,4 @@ export function buildHoneycombProcedure(jsPsych) {
timeline: [fixationTrial, taskTrial],
};
return honeycombBlock;
}
};
7 changes: 3 additions & 4 deletions src/experiment/procedures/startProcedure.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import { introductionTrial } from "../trials/introduction";
* 4) Trials used to set up a photodiode and trigger box are displayed (if applicable)
* 5) Trials used to set up the user's camera are displayed (if applicable)
*
* @param {Object} jsPsych The jsPsych instance being used to run the task
* @returns {Object} A jsPsych (nested) timeline object
*/
export function buildStartProcedure(jsPsych) {
export const buildStartProcedure = () => {
const procedure = [nameTrial, enterFullscreenTrial, introductionTrial];

// Conditionally add the photodiode setup trials
Expand All @@ -29,9 +28,9 @@ export function buildStartProcedure(jsPsych) {

// Conditionally add the camera setup trials
if (ENV.USE_CAMERA) {
procedure.push(buildCameraStartTrial(jsPsych));
procedure.push(buildCameraStartTrial);
}

// Return the block as a nested timeline
return { timeline: procedure };
}
};
176 changes: 86 additions & 90 deletions src/experiment/trials/camera.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,116 +4,112 @@ import initializeCamera from "@jspsych/plugin-initialize-camera";

import { LANGUAGE, ENV } from "../../config/";
import { div, h1, p, tag } from "../../lib/markup/tags";
import { getJsPsych } from "../../lib/utils";

const WEBCAM_ID = "webcam";

/**
* A trial that begins recording the participant using their computer's default camera
* @param {Object} jsPsych The jsPsych instance being used to run the task
* @returns {Object} A jsPsych trial object
*
* @type {Object} A jsPsych trial object
*/
// TODO @brown-ccv #301: Use jsPsych extension, deprecate this function
// TODO @brown-ccv #301: Use jsPsych extension, deprecate this variable
// TODO @brown-ccv #343: We should be able to make this work on both electron and browser?
// TODO @brown-ccv #301: Rolling save to the deployment (webm is a subset of mkv)
export function buildCameraStartTrial(jsPsych) {
return {
timeline: [
{
// Prompts user permission for camera device
type: initializeCamera,
include_audio: true,
mime_type: "video/webm",
export const buildCameraStartTrial = {
timeline: [
{
// Prompts user permission for camera device
type: initializeCamera,
include_audio: true,
mime_type: "video/webm",
},
{
// Helps participant center themselves inside the camera
type: htmlButtonResponse,
stimulus: function () {
const videoMarkup = tag("video", "", {
id: WEBCAM_ID,
width: 640,
height: 480,
autoplay: true,
});
const cameraStartMarkup = p(LANGUAGE.trials.camera.start);
const trialMarkup = div(cameraStartMarkup + videoMarkup, {
class: "align-items-center-col",
});
return div(trialMarkup);
},
{
// Helps participant center themselves inside the camera
type: htmlButtonResponse,
stimulus: function () {
const videoMarkup = tag("video", "", {
id: WEBCAM_ID,
width: 640,
height: 480,
autoplay: true,
});
const cameraStartMarkup = p(LANGUAGE.trials.camera.start);
const trialMarkup = div(cameraStartMarkup + videoMarkup, {
class: "align-items-center-col",
});
return div(trialMarkup);
},
choices: [LANGUAGE.prompts.continue.button],
response_ends_trial: true,
on_start: function () {
// Initialize and store the camera feed
if (!ENV.USE_ELECTRON) {
throw new Error("video recording is only available when running inside Electron");
}
choices: [LANGUAGE.prompts.continue.button],
response_ends_trial: true,
on_start: function () {
// Initialize and store the camera feed
if (!ENV.USE_ELECTRON) {
throw new Error("video recording is only available when running inside Electron");
}

const cameraRecorder = jsPsych.pluginAPI.getCameraRecorder();
if (!cameraRecorder) {
console.error("Camera is not initialized, no data will be recorded.");
return;
}
const cameraChunks = [];
const cameraRecorder = getJsPsych().pluginAPI.getCameraRecorder();
if (!cameraRecorder) {
console.error("Camera is not initialized, no data will be recorded.");
return;
}
const cameraChunks = [];

// Push data whenever available
cameraRecorder.addEventListener("dataavailable", (event) => {
if (event.data.size > 0) cameraChunks.push(event.data);
});
// Push data whenever available
cameraRecorder.addEventListener("dataavailable", (event) => {
if (event.data.size > 0) cameraChunks.push(event.data);
});

// Saves the raw data feed from the participants camera (executed on cameraRecorder.stop()).
cameraRecorder.addEventListener("stop", () => {
const blob = new Blob(cameraChunks, { type: cameraRecorder.mimeType });
// Saves the raw data feed from the participants camera (executed on cameraRecorder.stop()).
cameraRecorder.addEventListener("stop", () => {
const blob = new Blob(cameraChunks, { type: cameraRecorder.mimeType });

// Pass video data to Electron as a base64 encoded string
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
window.electronAPI.saveVideo(reader.result);
};
});
},
on_load: function () {
// Assign camera feed to the <video> element
const camera = document.getElementById(WEBCAM_ID);
// Pass video data to Electron as a base64 encoded string
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
window.electronAPI.saveVideo(reader.result);
};
});
},
on_load: function () {
// Assign camera feed to the <video> element
const camera = document.getElementById(WEBCAM_ID);

camera.srcObject = jsPsych.pluginAPI.getCameraRecorder().stream;
},
on_finish: function () {
// Begin video recording
jsPsych.pluginAPI.getCameraRecorder().start();
},
camera.srcObject = getJsPsych().pluginAPI.getCameraRecorder().stream;
},
on_finish: function () {
// Begin video recording
getJsPsych().pluginAPI.getCameraRecorder().start();
},
],
};
}
},
],
};

const recordingEndMarkup = h1(LANGUAGE.trials.camera.end);

/**
* A trial that finishes recording the participant using their computer's default camera
*
* @param {Number} duration How long to show the trial for
* @returns {Object} A jsPsych trial object
* @type {Object} A jsPsych trial object
*/
export function buildCameraEndTrial(jsPsych) {
const recordingEndMarkup = h1(LANGUAGE.trials.camera.end);
export const buildCameraEndTrial = {
type: htmlKeyboardResponse,
stimulus: div(recordingEndMarkup),
trial_duration: 5000,
on_start: function () {
// Complete the camera recording

return {
type: htmlKeyboardResponse,
stimulus: div(recordingEndMarkup),
trial_duration: 5000,
on_start: function () {
// Complete the camera recording
if (!ENV.USE_ELECTRON) {
throw new Error("video recording is only available when running inside Electron");
}

if (!ENV.USE_ELECTRON) {
throw new Error("video recording is only available when running inside Electron");
}
const cameraRecorder = getJsPsych().pluginAPI.getCameraRecorder();
if (!cameraRecorder) {
console.error("Camera is not initialized, no data will be recorded.");
return;
}

const cameraRecorder = jsPsych.pluginAPI.getCameraRecorder();
if (!cameraRecorder) {
console.error("Camera is not initialized, no data will be recorded.");
return;
}

cameraRecorder.stop();
},
};
}
cameraRecorder.stop();
},
};
Loading
Loading