mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
add a basic working version of html-video-response plugin
This commit is contained in:
parent
29656816e8
commit
dff17df884
30
examples/jspsych-html-video-response.html
Normal file
30
examples/jspsych-html-video-response.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src="../packages/jspsych/dist/index.browser.js"></script>
|
||||
<script src="../packages/plugin-initialize-camera/dist/index.browser.js"></script>
|
||||
<script src="../packages/plugin-html-video-response/dist/index.browser.js"></script>
|
||||
<link rel="stylesheet" href="../packages/jspsych/css/jspsych.css">
|
||||
</head>
|
||||
<body></body>
|
||||
<script>
|
||||
|
||||
var jsPsych = initJsPsych();
|
||||
|
||||
const init_camera = {
|
||||
type: jsPsychInitializeCamera,
|
||||
}
|
||||
|
||||
const record = {
|
||||
type: jsPsychHtmlVideoResponse,
|
||||
stimulus: `<div style="width:100vw; height:100vh; position: relative;">
|
||||
<div style="width:20px; height:20px; border-radius: 20px; background-color:red; position: absolute; top:10%; left:10%; transform: translate(-50%, -50%);"></div>
|
||||
</div>`,
|
||||
show_done_button: false,
|
||||
recording_duration: 2000
|
||||
}
|
||||
|
||||
jsPsych.run([init_camera, record]);
|
||||
|
||||
</script>
|
||||
</html>
|
25
package-lock.json
generated
25
package-lock.json
generated
@ -2640,6 +2640,10 @@
|
||||
"resolved": "packages/plugin-html-slider-response",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@jspsych/plugin-html-video-response": {
|
||||
"resolved": "packages/plugin-html-video-response",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@jspsych/plugin-iat-html": {
|
||||
"resolved": "packages/plugin-iat-html",
|
||||
"link": true
|
||||
@ -16650,6 +16654,7 @@
|
||||
}
|
||||
},
|
||||
"packages/extension-record-video": {
|
||||
"name": "@jspsych/extension-record-video",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@ -16941,6 +16946,18 @@
|
||||
"jspsych": ">=7.1.0"
|
||||
}
|
||||
},
|
||||
"packages/plugin-html-video-response": {
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@jspsych/config": "^1.3.0",
|
||||
"@jspsych/test-utils": "^1.1.0",
|
||||
"@types/resize-observer-browser": "^0.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jspsych": ">=7.1.0"
|
||||
}
|
||||
},
|
||||
"packages/plugin-iat-html": {
|
||||
"name": "@jspsych/plugin-iat-html",
|
||||
"version": "1.1.1",
|
||||
@ -19360,6 +19377,14 @@
|
||||
"@jspsych/test-utils": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@jspsych/plugin-html-video-response": {
|
||||
"version": "file:packages/plugin-html-video-response",
|
||||
"requires": {
|
||||
"@jspsych/config": "^1.3.0",
|
||||
"@jspsych/test-utils": "^1.1.0",
|
||||
"@types/resize-observer-browser": "^0.1.6"
|
||||
}
|
||||
},
|
||||
"@jspsych/plugin-iat-html": {
|
||||
"version": "file:packages/plugin-iat-html",
|
||||
"requires": {
|
||||
|
1
packages/plugin-html-video-response/jest.config.cjs
Normal file
1
packages/plugin-html-video-response/jest.config.cjs
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require("@jspsych/config/jest").makePackageConfig(__dirname);
|
44
packages/plugin-html-video-response/package.json
Normal file
44
packages/plugin-html-video-response/package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@jspsych/plugin-html-video-response",
|
||||
"version": "0.0.1",
|
||||
"description": "jsPsych plugin for displaying a stimulus and recording a video response through a camera",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"exports": {
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"typings": "dist/index.d.ts",
|
||||
"unpkg": "dist/index.browser.min.js",
|
||||
"files": [
|
||||
"src",
|
||||
"dist"
|
||||
],
|
||||
"source": "src/index.ts",
|
||||
"scripts": {
|
||||
"test": "jest --passWithNoTests",
|
||||
"test:watch": "npm test -- --watch",
|
||||
"tsc": "tsc",
|
||||
"build": "rollup --config",
|
||||
"build:watch": "npm run build -- --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jspsych/jsPsych.git",
|
||||
"directory": "packages/plugin-html-video-response"
|
||||
},
|
||||
"author": "Josh de Leeuw",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jspsych/jsPsych/issues"
|
||||
},
|
||||
"homepage": "https://www.jspsych.org/latest/plugins/html-video-response",
|
||||
"peerDependencies": {
|
||||
"jspsych": ">=7.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jspsych/config": "^1.3.0",
|
||||
"@jspsych/test-utils": "^1.1.0",
|
||||
"@types/resize-observer-browser": "^0.1.6"
|
||||
}
|
||||
}
|
3
packages/plugin-html-video-response/rollup.config.mjs
Normal file
3
packages/plugin-html-video-response/rollup.config.mjs
Normal file
@ -0,0 +1,3 @@
|
||||
import { makeRollupConfig } from "@jspsych/config/rollup";
|
||||
|
||||
export default makeRollupConfig("jsPsychHtmlVideoResponse");
|
235
packages/plugin-html-video-response/src/index.ts
Normal file
235
packages/plugin-html-video-response/src/index.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
|
||||
|
||||
const info = <const>{
|
||||
name: "html-video-response",
|
||||
parameters: {
|
||||
/** The HTML string to be displayed */
|
||||
stimulus: {
|
||||
type: ParameterType.HTML_STRING,
|
||||
default: undefined,
|
||||
},
|
||||
/** How long to show the stimulus. */
|
||||
stimulus_duration: {
|
||||
type: ParameterType.INT,
|
||||
default: null,
|
||||
},
|
||||
/** How long to show the trial. */
|
||||
recording_duration: {
|
||||
type: ParameterType.INT,
|
||||
default: 2000,
|
||||
},
|
||||
show_done_button: {
|
||||
type: ParameterType.BOOL,
|
||||
default: true,
|
||||
},
|
||||
done_button_label: {
|
||||
type: ParameterType.STRING,
|
||||
default: "Continue",
|
||||
},
|
||||
record_again_button_label: {
|
||||
type: ParameterType.STRING,
|
||||
default: "Record again",
|
||||
},
|
||||
accept_button_label: {
|
||||
type: ParameterType.STRING,
|
||||
default: "Continue",
|
||||
},
|
||||
allow_playback: {
|
||||
type: ParameterType.BOOL,
|
||||
default: false,
|
||||
},
|
||||
save_video_url: {
|
||||
type: ParameterType.BOOL,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Info = typeof info;
|
||||
|
||||
/**
|
||||
* html-video-response
|
||||
* jsPsych plugin for displaying a stimulus and recording a video response through a camera
|
||||
* @author Josh de Leeuw
|
||||
* @see {@link https://www.jspsych.org/plugins/jspsych-html-video-response/ html-video-response plugin documentation on jspsych.org}
|
||||
*/
|
||||
class HtmlVideoResponsePlugin implements JsPsychPlugin<Info> {
|
||||
static info = info;
|
||||
private stimulus_start_time;
|
||||
private recorder_start_time;
|
||||
private recorder: MediaRecorder;
|
||||
private video_url;
|
||||
private response;
|
||||
private load_resolver;
|
||||
private rt: number = null;
|
||||
private start_event_handler;
|
||||
private stop_event_handler;
|
||||
private data_available_handler;
|
||||
private recorded_data_chunks = [];
|
||||
|
||||
constructor(private jsPsych: JsPsych) {}
|
||||
|
||||
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
||||
this.recorder = this.jsPsych.pluginAPI.getCameraRecorder();
|
||||
|
||||
this.setupRecordingEvents(display_element, trial);
|
||||
|
||||
this.startRecording();
|
||||
}
|
||||
|
||||
private showDisplay(display_element, trial) {
|
||||
let html = `<div id="jspsych-html-video-response-stimulus">${trial.stimulus}</div>`;
|
||||
|
||||
if (trial.show_done_button) {
|
||||
html += `<p><button class="jspsych-btn" id="finish-trial">${trial.done_button_label}</button></p>`;
|
||||
}
|
||||
|
||||
display_element.innerHTML = html;
|
||||
}
|
||||
|
||||
private hideStimulus(display_element: HTMLElement) {
|
||||
const el: HTMLElement = display_element.querySelector("#jspsych-html-video-response-stimulus");
|
||||
if (el) {
|
||||
el.style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
private addButtonEvent(display_element, trial) {
|
||||
const btn = display_element.querySelector("#finish-trial");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", () => {
|
||||
const end_time = performance.now();
|
||||
this.rt = Math.round(end_time - this.stimulus_start_time);
|
||||
this.stopRecording().then(() => {
|
||||
if (trial.allow_playback) {
|
||||
this.showPlaybackControls(display_element, trial);
|
||||
} else {
|
||||
this.endTrial(display_element, trial);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private setupRecordingEvents(display_element, trial) {
|
||||
this.data_available_handler = (e) => {
|
||||
if (e.data.size > 0) {
|
||||
this.recorded_data_chunks.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
this.stop_event_handler = () => {
|
||||
const data = new Blob(this.recorded_data_chunks, { type: 'video/webm;codecs="vp9"' });
|
||||
this.video_url = URL.createObjectURL(data);
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
const base64 = (reader.result as string).split(",")[1];
|
||||
this.response = base64;
|
||||
this.load_resolver();
|
||||
});
|
||||
reader.readAsDataURL(data);
|
||||
};
|
||||
|
||||
this.start_event_handler = (e) => {
|
||||
// resets the recorded data
|
||||
this.recorded_data_chunks.length = 0;
|
||||
|
||||
this.recorder_start_time = e.timeStamp;
|
||||
this.showDisplay(display_element, trial);
|
||||
this.addButtonEvent(display_element, trial);
|
||||
|
||||
// setup timer for hiding the stimulus
|
||||
if (trial.stimulus_duration !== null) {
|
||||
this.jsPsych.pluginAPI.setTimeout(() => {
|
||||
this.hideStimulus(display_element);
|
||||
}, trial.stimulus_duration);
|
||||
}
|
||||
|
||||
// setup timer for ending the trial
|
||||
if (trial.recording_duration !== null) {
|
||||
this.jsPsych.pluginAPI.setTimeout(() => {
|
||||
// this check is necessary for cases where the
|
||||
// done_button is clicked before the timer expires
|
||||
if (this.recorder.state !== "inactive") {
|
||||
this.stopRecording().then(() => {
|
||||
if (trial.allow_playback) {
|
||||
this.showPlaybackControls(display_element, trial);
|
||||
} else {
|
||||
this.endTrial(display_element, trial);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, trial.recording_duration);
|
||||
}
|
||||
};
|
||||
|
||||
this.recorder.addEventListener("dataavailable", this.data_available_handler);
|
||||
|
||||
this.recorder.addEventListener("stop", this.stop_event_handler);
|
||||
|
||||
this.recorder.addEventListener("start", this.start_event_handler);
|
||||
}
|
||||
|
||||
private startRecording() {
|
||||
this.recorder.start();
|
||||
}
|
||||
|
||||
private stopRecording() {
|
||||
this.recorder.stop();
|
||||
return new Promise((resolve) => {
|
||||
this.load_resolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private showPlaybackControls(display_element, trial) {
|
||||
display_element.innerHTML = `
|
||||
<p><video id="playback" src="${this.video_url}" controls></video></p>
|
||||
<button id="record-again" class="jspsych-btn">${trial.record_again_button_label}</button>
|
||||
<button id="continue" class="jspsych-btn">${trial.accept_button_label}</button>
|
||||
`;
|
||||
|
||||
display_element.querySelector("#record-again").addEventListener("click", () => {
|
||||
// release object url to save memory
|
||||
URL.revokeObjectURL(this.video_url);
|
||||
this.startRecording();
|
||||
});
|
||||
display_element.querySelector("#continue").addEventListener("click", () => {
|
||||
this.endTrial(display_element, trial);
|
||||
});
|
||||
|
||||
// const video = display_element.querySelector('#playback');
|
||||
// video.src =
|
||||
}
|
||||
|
||||
private endTrial(display_element, trial) {
|
||||
// clear recordering event handler
|
||||
|
||||
this.recorder.removeEventListener("dataavailable", this.data_available_handler);
|
||||
this.recorder.removeEventListener("start", this.start_event_handler);
|
||||
this.recorder.removeEventListener("stop", this.stop_event_handler);
|
||||
|
||||
// kill any remaining setTimeout handlers
|
||||
this.jsPsych.pluginAPI.clearAllTimeouts();
|
||||
|
||||
// gather the data to store for the trial
|
||||
var trial_data: any = {
|
||||
rt: this.rt,
|
||||
stimulus: trial.stimulus,
|
||||
response: this.response,
|
||||
};
|
||||
|
||||
if (trial.save_video_url) {
|
||||
trial_data.video_url = this.video_url;
|
||||
} else {
|
||||
URL.revokeObjectURL(this.video_url);
|
||||
}
|
||||
|
||||
// clear the display
|
||||
display_element.innerHTML = "";
|
||||
|
||||
// move on to the next trial
|
||||
this.jsPsych.finishTrial(trial_data);
|
||||
}
|
||||
}
|
||||
|
||||
export default HtmlVideoResponsePlugin;
|
7
packages/plugin-html-video-response/tsconfig.json
Normal file
7
packages/plugin-html-video-response/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "@jspsych/config/tsconfig.core.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user