Skip to content

Commit

Permalink
Merge pull request #528 from brown-ccv/jsPsych-global
Browse files Browse the repository at this point in the history
jsPsych as global variable
  • Loading branch information
YUUU23 authored Aug 15, 2024
2 parents 1fb344c + 99556aa commit b969b3d
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 178 deletions.
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

0 comments on commit b969b3d

Please sign in to comment.