@jspsych/plugin-video-keyboard-response 2.1.0 → 2.2.0

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.
@@ -1,7 +1,7 @@
1
1
  var jsPsychVideoKeyboardResponse = (function (jspsych) {
2
2
  'use strict';
3
3
 
4
- var version = "2.1.0";
4
+ var version = "2.2.0";
5
5
 
6
6
  const info = {
7
7
  name: "video-keyboard-response",
@@ -26,13 +26,17 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
26
26
  pretty_name: "Prompt",
27
27
  default: null
28
28
  },
29
- /** The width of the video in pixels. */
29
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
30
+ * or properly scaled with the aspect ratio if the height is also specified.
31
+ */
30
32
  width: {
31
33
  type: jspsych.ParameterType.INT,
32
34
  pretty_name: "Width",
33
35
  default: ""
34
36
  },
35
- /** The height of the video display in pixels. */
37
+ /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,
38
+ * or properly scaled with the aspect ratio if the width is also specified.
39
+ */
36
40
  height: {
37
41
  type: jspsych.ParameterType.INT,
38
42
  pretty_name: "Height",
@@ -91,6 +95,17 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
91
95
  type: jspsych.ParameterType.BOOL,
92
96
  pretty_name: "Response allowed while playing",
93
97
  default: true
98
+ },
99
+ /** If true, the response is not registered until the participant releases the key. The response
100
+ * time (`rt`) still reflects when the key was pressed, and the additional data field
101
+ * `rt_key_duration` records how long the key was held down. Note that when this is true, the
102
+ * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at
103
+ * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the
104
+ * key is still held, no response is recorded for the trial.
105
+ */
106
+ wait_for_key_release: {
107
+ type: jspsych.ParameterType.BOOL,
108
+ default: false
94
109
  }
95
110
  },
96
111
  data: {
@@ -100,7 +115,7 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
100
115
  },
101
116
  /** The response time in milliseconds for the participant to make a response. The time is measured from when the
102
117
  * stimulus first appears on the screen until the participant's response.
103
- * */
118
+ */
104
119
  rt: {
105
120
  type: jspsych.ParameterType.INT
106
121
  },
@@ -108,6 +123,10 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
108
123
  stimulus: {
109
124
  type: jspsych.ParameterType.STRING,
110
125
  array: true
126
+ },
127
+ /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */
128
+ rt_key_duration: {
129
+ type: jspsych.ParameterType.INT
111
130
  }
112
131
  },
113
132
  // prettier-ignore
@@ -124,12 +143,6 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
124
143
  this.info = info;
125
144
  }
126
145
  trial(display_element, trial) {
127
- if (!Array.isArray(trial.stimulus)) {
128
- throw new Error(`
129
- The stimulus property for the video-keyboard-response plugin must be an array
130
- of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters
131
- `);
132
- }
133
146
  var video_html = "<div>";
134
147
  video_html += '<video id="jspsych-video-keyboard-response-stimulus"';
135
148
  if (trial.width) {
@@ -187,7 +200,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
187
200
  valid_responses: trial.choices,
188
201
  rt_method: "performance",
189
202
  persist: false,
190
- allow_held_key: false
203
+ allow_held_key: false,
204
+ wait_for_key_release: trial.wait_for_key_release
191
205
  });
192
206
  }
193
207
  };
@@ -224,7 +238,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
224
238
  valid_responses: trial.choices,
225
239
  rt_method: "performance",
226
240
  persist: false,
227
- allow_held_key: false
241
+ allow_held_key: false,
242
+ wait_for_key_release: trial.wait_for_key_release
228
243
  });
229
244
  }
230
245
  video_element.pause();
@@ -237,7 +252,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
237
252
  }
238
253
  var response = {
239
254
  rt: null,
240
- key: null
255
+ key: null,
256
+ rt_key_duration: null
241
257
  };
242
258
  const end_trial = () => {
243
259
  this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -249,14 +265,19 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
249
265
  var trial_data = {
250
266
  rt: response.rt,
251
267
  stimulus: trial.stimulus,
252
- response: response.key
268
+ response: response.key,
269
+ rt_key_duration: response.rt_key_duration
253
270
  };
254
271
  this.jsPsych.finishTrial(trial_data);
255
272
  };
256
273
  var after_response = (info2) => {
257
274
  display_element.querySelector("#jspsych-video-keyboard-response-stimulus").className += " responded";
258
275
  if (response.key == null) {
259
- response = info2;
276
+ response = {
277
+ rt: info2.rt,
278
+ key: info2.key,
279
+ rt_key_duration: info2.rt_key_duration ?? null
280
+ };
260
281
  }
261
282
  if (trial.response_ends_trial) {
262
283
  end_trial();
@@ -268,7 +289,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
268
289
  valid_responses: trial.choices,
269
290
  rt_method: "performance",
270
291
  persist: false,
271
- allow_held_key: false
292
+ allow_held_key: false,
293
+ wait_for_key_release: trial.wait_for_key_release
272
294
  });
273
295
  }
274
296
  if (trial.trial_duration !== null) {
@@ -311,8 +333,10 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
311
333
  const default_data = {
312
334
  stimulus: trial.stimulus,
313
335
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
314
- response: this.jsPsych.pluginAPI.getValidKey(trial.choices)
336
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
337
+ rt_key_duration: null
315
338
  };
339
+ default_data.rt_key_duration = default_data.rt === null || !trial.wait_for_key_release ? null : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);
316
340
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
317
341
  this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
318
342
  return data;
@@ -322,4 +346,4 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
322
346
  return VideoKeyboardResponsePlugin;
323
347
 
324
348
  })(jsPsychModule);
325
- //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-video-keyboard-response@2.1.0/dist/index.browser.js.map
349
+ //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-video-keyboard-response@2.2.0/dist/index.browser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.1.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video in pixels. */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n * */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // catch mistake where stimuli are not an array\n if (!Array.isArray(trial.stimulus)) {\n throw new Error(`\n The stimulus property for the video-keyboard-response plugin must be an array\n of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters\n `);\n }\n\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = info;\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":[],"mappings":";;;EAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IC8GA,SAAA,EAAA;EAAA;;KAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.browser.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.2.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.3.4\",\n \"@jspsych/test-utils\": \"^1.3.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the height is also specified.\n */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the width is also specified.\n */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n /** If true, the response is not registered until the participant releases the key. The response\n * time (`rt`) still reflects when the key was pressed, and the additional data field\n * `rt_key_duration` records how long the key was held down. Note that when this is true, the\n * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at\n * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the\n * key is still held, no response is recorded for the trial.\n */\n wait_for_key_release: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */\n rt_key_duration: {\n type: ParameterType.INT,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n rt_key_duration: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n rt_key_duration: response.rt_key_duration,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = {\n rt: info.rt,\n key: info.key,\n rt_key_duration: info.rt_key_duration ?? null,\n };\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n rt_key_duration: null,\n };\n default_data.rt_key_duration =\n default_data.rt === null || !trial.wait_for_key_release\n ? null\n : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":[],"mappings":";;;EAEE,IAAA,OAAA,GAAW,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICiIA,SAAA,EAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,5 +1,2 @@
1
- var jsPsychVideoKeyboardResponse=function(r){"use strict";var c="2.1.0";const _={name:"video-keyboard-response",version:c,parameters:{stimulus:{type:r.ParameterType.VIDEO,pretty_name:"Video",default:void 0,array:!0},choices:{type:r.ParameterType.KEYS,pretty_name:"Choices",default:"ALL_KEYS"},prompt:{type:r.ParameterType.HTML_STRING,pretty_name:"Prompt",default:null},width:{type:r.ParameterType.INT,pretty_name:"Width",default:""},height:{type:r.ParameterType.INT,pretty_name:"Height",default:""},autoplay:{type:r.ParameterType.BOOL,pretty_name:"Autoplay",default:!0},controls:{type:r.ParameterType.BOOL,pretty_name:"Controls",default:!1},start:{type:r.ParameterType.FLOAT,pretty_name:"Start",default:null},stop:{type:r.ParameterType.FLOAT,pretty_name:"Stop",default:null},rate:{type:r.ParameterType.FLOAT,pretty_name:"Rate",default:1},trial_ends_after_video:{type:r.ParameterType.BOOL,pretty_name:"End trial after video finishes",default:!1},trial_duration:{type:r.ParameterType.INT,pretty_name:"Trial duration",default:null},response_ends_trial:{type:r.ParameterType.BOOL,pretty_name:"Response ends trial",default:!0},response_allowed_while_playing:{type:r.ParameterType.BOOL,pretty_name:"Response allowed while playing",default:!0}},data:{response:{type:r.ParameterType.STRING},rt:{type:r.ParameterType.INT},stimulus:{type:r.ParameterType.STRING,array:!0}},citations:{apa:"de Leeuw, J. R., Gilbert, R. A., & Luchterhandt, B. (2023). jsPsych: Enabling an Open-Source Collaborative Ecosystem of Behavioral Experiments. Journal of Open Source Software, 8(85), 5351. https://doi.org/10.21105/joss.05351 ",bibtex:'@article{Leeuw2023jsPsych, author = {de Leeuw, Joshua R. and Gilbert, Rebecca A. and Luchterhandt, Bj{\\" o}rn}, journal = {Journal of Open Source Software}, doi = {10.21105/joss.05351}, issn = {2475-9066}, number = {85}, year = {2023}, month = {may 11}, pages = {5351}, publisher = {Open Journals}, title = {jsPsych: Enabling an {Open}-{Source} {Collaborative} {Ecosystem} of {Behavioral} {Experiments}}, url = {https://joss.theoj.org/papers/10.21105/joss.05351}, volume = {8}, } '}};class m{constructor(s){this.jsPsych=s}trial(s,e){if(!Array.isArray(e.stimulus))throw new Error(`
2
- The stimulus property for the video-keyboard-response plugin must be an array
3
- of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters
4
- `);var t="<div>";t+='<video id="jspsych-video-keyboard-response-stimulus"',e.width&&(t+=' width="'+e.width+'"'),e.height&&(t+=' height="'+e.height+'"'),e.autoplay&&e.start==null&&(t+=" autoplay "),e.controls&&(t+=" controls "),e.start!==null&&(t+=' style="visibility: hidden;"'),t+=">";var i=this.jsPsych.pluginAPI.getVideoBuffer(e.stimulus[0]);if(!i)for(var n=0;n<e.stimulus.length;n++){var o=e.stimulus[n];o.indexOf("?")>-1&&(o=o.substring(0,o.indexOf("?")));var l=o.substr(o.lastIndexOf(".")+1);l=l.toLowerCase(),l=="mov"&&console.warn("Warning: video-keyboard-response plugin does not reliably support .mov files."),t+='<source src="'+o+'" type="video/'+l+'">'}t+="</video>",t+="</div>",e.prompt!==null&&(t+=e.prompt),s.innerHTML=t;var a=s.querySelector("#jspsych-video-keyboard-response-stimulus");i&&(a.src=i),a.onended=()=>{if(e.trial_ends_after_video&&d(),e.response_allowed_while_playing==!1&&!e.trial_ends_after_video)var p=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:y,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1})},a.playbackRate=e.rate,e.start!==null&&(a.pause(),a.onseeked=()=>{a.style.visibility="visible",a.muted=!1,e.autoplay?a.play():a.pause(),a.onseeked=()=>{}},a.onplaying=()=>{a.currentTime=e.start,a.onplaying=()=>{}},a.muted=!0,a.play());let h=!1;e.stop!==null&&a.addEventListener("timeupdate",p=>{var v=a.currentTime;if(v>=e.stop){if(!e.response_allowed_while_playing)var g=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:y,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1});a.pause(),e.trial_ends_after_video&&!h&&(h=!0,d())}});var u={rt:null,key:null};const d=()=>{this.jsPsych.pluginAPI.cancelAllKeyboardResponses(),s.querySelector("#jspsych-video-keyboard-response-stimulus").pause(),s.querySelector("#jspsych-video-keyboard-response-stimulus").onended=()=>{};var p={rt:u.rt,stimulus:e.stimulus,response:u.key};this.jsPsych.finishTrial(p)};var y=p=>{s.querySelector("#jspsych-video-keyboard-response-stimulus").className+=" responded",u.key==null&&(u=p),e.response_ends_trial&&d()};if(e.choices!="NO_KEYS"&&e.response_allowed_while_playing)var P=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:y,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1});e.trial_duration!==null&&this.jsPsych.pluginAPI.setTimeout(d,e.trial_duration)}simulate(s,e,t,i){e=="data-only"&&(i(),this.simulate_data_only(s,t)),e=="visual"&&this.simulate_visual(s,t,i)}simulate_data_only(s,e){const t=this.create_simulation_data(s,e);this.jsPsych.finishTrial(t)}simulate_visual(s,e,t){const i=this.create_simulation_data(s,e),n=this.jsPsych.getDisplayElement();this.trial(n,s),t();const o=n.querySelector("#jspsych-video-button-response-stimulus"),l=()=>{i.rt!==null&&this.jsPsych.pluginAPI.pressKey(i.response,i.rt)};s.response_allowed_while_playing?l():o.addEventListener("ended",l)}create_simulation_data(s,e){const t={stimulus:s.stimulus,rt:this.jsPsych.randomization.sampleExGaussian(500,50,.006666666666666667,!0),response:this.jsPsych.pluginAPI.getValidKey(s.choices)},i=this.jsPsych.pluginAPI.mergeSimulationData(t,e);return this.jsPsych.pluginAPI.ensureSimulationDataConsistency(s,i),i}}return m.info=_,m}(jsPsychModule);
5
- //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-video-keyboard-response@2.1.0/dist/index.browser.min.js.map
1
+ var jsPsychVideoKeyboardResponse=(function(r){"use strict";var c="2.2.0";const f={name:"video-keyboard-response",version:c,parameters:{stimulus:{type:r.ParameterType.VIDEO,pretty_name:"Video",default:void 0,array:!0},choices:{type:r.ParameterType.KEYS,pretty_name:"Choices",default:"ALL_KEYS"},prompt:{type:r.ParameterType.HTML_STRING,pretty_name:"Prompt",default:null},width:{type:r.ParameterType.INT,pretty_name:"Width",default:""},height:{type:r.ParameterType.INT,pretty_name:"Height",default:""},autoplay:{type:r.ParameterType.BOOL,pretty_name:"Autoplay",default:!0},controls:{type:r.ParameterType.BOOL,pretty_name:"Controls",default:!1},start:{type:r.ParameterType.FLOAT,pretty_name:"Start",default:null},stop:{type:r.ParameterType.FLOAT,pretty_name:"Stop",default:null},rate:{type:r.ParameterType.FLOAT,pretty_name:"Rate",default:1},trial_ends_after_video:{type:r.ParameterType.BOOL,pretty_name:"End trial after video finishes",default:!1},trial_duration:{type:r.ParameterType.INT,pretty_name:"Trial duration",default:null},response_ends_trial:{type:r.ParameterType.BOOL,pretty_name:"Response ends trial",default:!0},response_allowed_while_playing:{type:r.ParameterType.BOOL,pretty_name:"Response allowed while playing",default:!0},wait_for_key_release:{type:r.ParameterType.BOOL,default:!1}},data:{response:{type:r.ParameterType.STRING},rt:{type:r.ParameterType.INT},stimulus:{type:r.ParameterType.STRING,array:!0},rt_key_duration:{type:r.ParameterType.INT}},citations:{apa:"de Leeuw, J. R., Gilbert, R. A., & Luchterhandt, B. (2023). jsPsych: Enabling an Open-Source Collaborative Ecosystem of Behavioral Experiments. Journal of Open Source Software, 8(85), 5351. https://doi.org/10.21105/joss.05351 ",bibtex:'@article{Leeuw2023jsPsych, author = {de Leeuw, Joshua R. and Gilbert, Rebecca A. and Luchterhandt, Bj{\\" o}rn}, journal = {Journal of Open Source Software}, doi = {10.21105/joss.05351}, issn = {2475-9066}, number = {85}, year = {2023}, month = {may 11}, pages = {5351}, publisher = {Open Journals}, title = {jsPsych: Enabling an {Open}-{Source} {Collaborative} {Ecosystem} of {Behavioral} {Experiments}}, url = {https://joss.theoj.org/papers/10.21105/joss.05351}, volume = {8}, } '}};class m{constructor(a){this.jsPsych=a}trial(a,e){var t="<div>";t+='<video id="jspsych-video-keyboard-response-stimulus"',e.width&&(t+=' width="'+e.width+'"'),e.height&&(t+=' height="'+e.height+'"'),e.autoplay&&e.start==null&&(t+=" autoplay "),e.controls&&(t+=" controls "),e.start!==null&&(t+=' style="visibility: hidden;"'),t+=">";var l=this.jsPsych.pluginAPI.getVideoBuffer(e.stimulus[0]);if(!l)for(var u=0;u<e.stimulus.length;u++){var i=e.stimulus[u];i.indexOf("?")>-1&&(i=i.substring(0,i.indexOf("?")));var o=i.substr(i.lastIndexOf(".")+1);o=o.toLowerCase(),o=="mov"&&console.warn("Warning: video-keyboard-response plugin does not reliably support .mov files."),t+='<source src="'+i+'" type="video/'+o+'">'}t+="</video>",t+="</div>",e.prompt!==null&&(t+=e.prompt),a.innerHTML=t;var s=a.querySelector("#jspsych-video-keyboard-response-stimulus");l&&(s.src=l),s.onended=()=>{if(e.trial_ends_after_video&&y(),e.response_allowed_while_playing==!1&&!e.trial_ends_after_video)var n=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:_,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1,wait_for_key_release:e.wait_for_key_release})},s.playbackRate=e.rate,e.start!==null&&(s.pause(),s.onseeked=()=>{s.style.visibility="visible",s.muted=!1,e.autoplay?s.play():s.pause(),s.onseeked=()=>{}},s.onplaying=()=>{s.currentTime=e.start,s.onplaying=()=>{}},s.muted=!0,s.play());let h=!1;e.stop!==null&&s.addEventListener("timeupdate",n=>{var d=s.currentTime;if(d>=e.stop){if(!e.response_allowed_while_playing)var g=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:_,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1,wait_for_key_release:e.wait_for_key_release});s.pause(),e.trial_ends_after_video&&!h&&(h=!0,y())}});var p={rt:null,key:null,rt_key_duration:null};const y=()=>{this.jsPsych.pluginAPI.cancelAllKeyboardResponses(),a.querySelector("#jspsych-video-keyboard-response-stimulus").pause(),a.querySelector("#jspsych-video-keyboard-response-stimulus").onended=()=>{};var n={rt:p.rt,stimulus:e.stimulus,response:p.key,rt_key_duration:p.rt_key_duration};this.jsPsych.finishTrial(n)};var _=n=>{var d;a.querySelector("#jspsych-video-keyboard-response-stimulus").className+=" responded",p.key==null&&(p={rt:n.rt,key:n.key,rt_key_duration:(d=n.rt_key_duration)!=null?d:null}),e.response_ends_trial&&y()};if(e.choices!="NO_KEYS"&&e.response_allowed_while_playing)var P=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:_,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1,wait_for_key_release:e.wait_for_key_release});e.trial_duration!==null&&this.jsPsych.pluginAPI.setTimeout(y,e.trial_duration)}simulate(a,e,t,l){e=="data-only"&&(l(),this.simulate_data_only(a,t)),e=="visual"&&this.simulate_visual(a,t,l)}simulate_data_only(a,e){const t=this.create_simulation_data(a,e);this.jsPsych.finishTrial(t)}simulate_visual(a,e,t){const l=this.create_simulation_data(a,e),u=this.jsPsych.getDisplayElement();this.trial(u,a),t();const i=u.querySelector("#jspsych-video-button-response-stimulus"),o=()=>{l.rt!==null&&this.jsPsych.pluginAPI.pressKey(l.response,l.rt)};a.response_allowed_while_playing?o():i.addEventListener("ended",o)}create_simulation_data(a,e){const t={stimulus:a.stimulus,rt:this.jsPsych.randomization.sampleExGaussian(500,50,.006666666666666667,!0),response:this.jsPsych.pluginAPI.getValidKey(a.choices),rt_key_duration:null};t.rt_key_duration=t.rt===null||!a.wait_for_key_release?null:this.jsPsych.randomization.sampleExGaussian(150,30,.01,!0);const l=this.jsPsych.pluginAPI.mergeSimulationData(t,e);return this.jsPsych.pluginAPI.ensureSimulationDataConsistency(a,l),l}}return m.info=f,m})(jsPsychModule);
2
+ //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-video-keyboard-response@2.2.0/dist/index.browser.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.min.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.1.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video in pixels. */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n * */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // catch mistake where stimuli are not an array\n if (!Array.isArray(trial.stimulus)) {\n throw new Error(`\n The stimulus property for the video-keyboard-response plugin must be an array\n of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters\n `);\n }\n\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = info;\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":["version"],"mappings":"0DAEEA,IAAAA,EAAW,kxCC8GA,UAAA,iuBAAe;;;"}
1
+ {"version":3,"file":"index.browser.min.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.2.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.3.4\",\n \"@jspsych/test-utils\": \"^1.3.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the height is also specified.\n */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the width is also specified.\n */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n /** If true, the response is not registered until the participant releases the key. The response\n * time (`rt`) still reflects when the key was pressed, and the additional data field\n * `rt_key_duration` records how long the key was held down. Note that when this is true, the\n * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at\n * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the\n * key is still held, no response is recorded for the trial.\n */\n wait_for_key_release: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */\n rt_key_duration: {\n type: ParameterType.INT,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n rt_key_duration: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n rt_key_duration: response.rt_key_duration,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = {\n rt: info.rt,\n key: info.key,\n rt_key_duration: info.rt_key_duration ?? null,\n };\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n rt_key_duration: null,\n };\n default_data.rt_key_duration =\n default_data.rt === null || !trial.wait_for_key_release\n ? null\n : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":["version","_a"],"mappings":"2DACE,IACAA,EAAW,y3CCiIA,UAAA,iuBAAe,unEAnI5B,IAAAC"}
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  var jspsych = require('jspsych');
4
4
 
5
- var version = "2.1.0";
5
+ var version = "2.2.0";
6
6
 
7
7
  const info = {
8
8
  name: "video-keyboard-response",
@@ -27,13 +27,17 @@ const info = {
27
27
  pretty_name: "Prompt",
28
28
  default: null
29
29
  },
30
- /** The width of the video in pixels. */
30
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
31
+ * or properly scaled with the aspect ratio if the height is also specified.
32
+ */
31
33
  width: {
32
34
  type: jspsych.ParameterType.INT,
33
35
  pretty_name: "Width",
34
36
  default: ""
35
37
  },
36
- /** The height of the video display in pixels. */
38
+ /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,
39
+ * or properly scaled with the aspect ratio if the width is also specified.
40
+ */
37
41
  height: {
38
42
  type: jspsych.ParameterType.INT,
39
43
  pretty_name: "Height",
@@ -92,6 +96,17 @@ const info = {
92
96
  type: jspsych.ParameterType.BOOL,
93
97
  pretty_name: "Response allowed while playing",
94
98
  default: true
99
+ },
100
+ /** If true, the response is not registered until the participant releases the key. The response
101
+ * time (`rt`) still reflects when the key was pressed, and the additional data field
102
+ * `rt_key_duration` records how long the key was held down. Note that when this is true, the
103
+ * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at
104
+ * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the
105
+ * key is still held, no response is recorded for the trial.
106
+ */
107
+ wait_for_key_release: {
108
+ type: jspsych.ParameterType.BOOL,
109
+ default: false
95
110
  }
96
111
  },
97
112
  data: {
@@ -101,7 +116,7 @@ const info = {
101
116
  },
102
117
  /** The response time in milliseconds for the participant to make a response. The time is measured from when the
103
118
  * stimulus first appears on the screen until the participant's response.
104
- * */
119
+ */
105
120
  rt: {
106
121
  type: jspsych.ParameterType.INT
107
122
  },
@@ -109,6 +124,10 @@ const info = {
109
124
  stimulus: {
110
125
  type: jspsych.ParameterType.STRING,
111
126
  array: true
127
+ },
128
+ /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */
129
+ rt_key_duration: {
130
+ type: jspsych.ParameterType.INT
112
131
  }
113
132
  },
114
133
  // prettier-ignore
@@ -125,12 +144,6 @@ class VideoKeyboardResponsePlugin {
125
144
  this.info = info;
126
145
  }
127
146
  trial(display_element, trial) {
128
- if (!Array.isArray(trial.stimulus)) {
129
- throw new Error(`
130
- The stimulus property for the video-keyboard-response plugin must be an array
131
- of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters
132
- `);
133
- }
134
147
  var video_html = "<div>";
135
148
  video_html += '<video id="jspsych-video-keyboard-response-stimulus"';
136
149
  if (trial.width) {
@@ -188,7 +201,8 @@ class VideoKeyboardResponsePlugin {
188
201
  valid_responses: trial.choices,
189
202
  rt_method: "performance",
190
203
  persist: false,
191
- allow_held_key: false
204
+ allow_held_key: false,
205
+ wait_for_key_release: trial.wait_for_key_release
192
206
  });
193
207
  }
194
208
  };
@@ -225,7 +239,8 @@ class VideoKeyboardResponsePlugin {
225
239
  valid_responses: trial.choices,
226
240
  rt_method: "performance",
227
241
  persist: false,
228
- allow_held_key: false
242
+ allow_held_key: false,
243
+ wait_for_key_release: trial.wait_for_key_release
229
244
  });
230
245
  }
231
246
  video_element.pause();
@@ -238,7 +253,8 @@ class VideoKeyboardResponsePlugin {
238
253
  }
239
254
  var response = {
240
255
  rt: null,
241
- key: null
256
+ key: null,
257
+ rt_key_duration: null
242
258
  };
243
259
  const end_trial = () => {
244
260
  this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -250,14 +266,19 @@ class VideoKeyboardResponsePlugin {
250
266
  var trial_data = {
251
267
  rt: response.rt,
252
268
  stimulus: trial.stimulus,
253
- response: response.key
269
+ response: response.key,
270
+ rt_key_duration: response.rt_key_duration
254
271
  };
255
272
  this.jsPsych.finishTrial(trial_data);
256
273
  };
257
274
  var after_response = (info2) => {
258
275
  display_element.querySelector("#jspsych-video-keyboard-response-stimulus").className += " responded";
259
276
  if (response.key == null) {
260
- response = info2;
277
+ response = {
278
+ rt: info2.rt,
279
+ key: info2.key,
280
+ rt_key_duration: info2.rt_key_duration ?? null
281
+ };
261
282
  }
262
283
  if (trial.response_ends_trial) {
263
284
  end_trial();
@@ -269,7 +290,8 @@ class VideoKeyboardResponsePlugin {
269
290
  valid_responses: trial.choices,
270
291
  rt_method: "performance",
271
292
  persist: false,
272
- allow_held_key: false
293
+ allow_held_key: false,
294
+ wait_for_key_release: trial.wait_for_key_release
273
295
  });
274
296
  }
275
297
  if (trial.trial_duration !== null) {
@@ -312,8 +334,10 @@ class VideoKeyboardResponsePlugin {
312
334
  const default_data = {
313
335
  stimulus: trial.stimulus,
314
336
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
315
- response: this.jsPsych.pluginAPI.getValidKey(trial.choices)
337
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
338
+ rt_key_duration: null
316
339
  };
340
+ default_data.rt_key_duration = default_data.rt === null || !trial.wait_for_key_release ? null : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);
317
341
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
318
342
  this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
319
343
  return data;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.1.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video in pixels. */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n * */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // catch mistake where stimuli are not an array\n if (!Array.isArray(trial.stimulus)) {\n throw new Error(`\n The stimulus property for the video-keyboard-response plugin must be an array\n of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters\n `);\n }\n\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = info;\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC8GA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.cjs","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.2.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.3.4\",\n \"@jspsych/test-utils\": \"^1.3.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the height is also specified.\n */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the width is also specified.\n */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n /** If true, the response is not registered until the participant releases the key. The response\n * time (`rt`) still reflects when the key was pressed, and the additional data field\n * `rt_key_duration` records how long the key was held down. Note that when this is true, the\n * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at\n * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the\n * key is still held, no response is recorded for the trial.\n */\n wait_for_key_release: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */\n rt_key_duration: {\n type: ParameterType.INT,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n rt_key_duration: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n rt_key_duration: response.rt_key_duration,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = {\n rt: info.rt,\n key: info.key,\n rt_key_duration: info.rt_key_duration ?? null,\n };\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n rt_key_duration: null,\n };\n default_data.rt_key_duration =\n default_data.rt === null || !trial.wait_for_key_release\n ? null\n : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":[],"mappings":";;;;AAEE,IAAA,OAAA,GAAW,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECiIA,SAAA,EAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.d.ts CHANGED
@@ -23,13 +23,17 @@ declare const info: {
23
23
  readonly pretty_name: "Prompt";
24
24
  readonly default: any;
25
25
  };
26
- /** The width of the video in pixels. */
26
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
27
+ * or properly scaled with the aspect ratio if the height is also specified.
28
+ */
27
29
  readonly width: {
28
30
  readonly type: ParameterType.INT;
29
31
  readonly pretty_name: "Width";
30
32
  readonly default: "";
31
33
  };
32
- /** The height of the video display in pixels. */
34
+ /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,
35
+ * or properly scaled with the aspect ratio if the width is also specified.
36
+ */
33
37
  readonly height: {
34
38
  readonly type: ParameterType.INT;
35
39
  readonly pretty_name: "Height";
@@ -89,6 +93,17 @@ declare const info: {
89
93
  readonly pretty_name: "Response allowed while playing";
90
94
  readonly default: true;
91
95
  };
96
+ /** If true, the response is not registered until the participant releases the key. The response
97
+ * time (`rt`) still reflects when the key was pressed, and the additional data field
98
+ * `rt_key_duration` records how long the key was held down. Note that when this is true, the
99
+ * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at
100
+ * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the
101
+ * key is still held, no response is recorded for the trial.
102
+ */
103
+ readonly wait_for_key_release: {
104
+ readonly type: ParameterType.BOOL;
105
+ readonly default: false;
106
+ };
92
107
  };
93
108
  readonly data: {
94
109
  /** Indicates which key the participant pressed. */
@@ -97,7 +112,7 @@ declare const info: {
97
112
  };
98
113
  /** The response time in milliseconds for the participant to make a response. The time is measured from when the
99
114
  * stimulus first appears on the screen until the participant's response.
100
- * */
115
+ */
101
116
  readonly rt: {
102
117
  readonly type: ParameterType.INT;
103
118
  };
@@ -106,6 +121,10 @@ declare const info: {
106
121
  readonly type: ParameterType.STRING;
107
122
  readonly array: true;
108
123
  };
124
+ /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */
125
+ readonly rt_key_duration: {
126
+ readonly type: ParameterType.INT;
127
+ };
109
128
  };
110
129
  readonly citations: "__CITATIONS__";
111
130
  };
@@ -151,13 +170,17 @@ declare class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
151
170
  readonly pretty_name: "Prompt";
152
171
  readonly default: any;
153
172
  };
154
- /** The width of the video in pixels. */
173
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
174
+ * or properly scaled with the aspect ratio if the height is also specified.
175
+ */
155
176
  readonly width: {
156
177
  readonly type: ParameterType.INT;
157
178
  readonly pretty_name: "Width";
158
179
  readonly default: "";
159
180
  };
160
- /** The height of the video display in pixels. */
181
+ /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,
182
+ * or properly scaled with the aspect ratio if the width is also specified.
183
+ */
161
184
  readonly height: {
162
185
  readonly type: ParameterType.INT;
163
186
  readonly pretty_name: "Height";
@@ -217,6 +240,17 @@ declare class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
217
240
  readonly pretty_name: "Response allowed while playing";
218
241
  readonly default: true;
219
242
  };
243
+ /** If true, the response is not registered until the participant releases the key. The response
244
+ * time (`rt`) still reflects when the key was pressed, and the additional data field
245
+ * `rt_key_duration` records how long the key was held down. Note that when this is true, the
246
+ * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at
247
+ * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the
248
+ * key is still held, no response is recorded for the trial.
249
+ */
250
+ readonly wait_for_key_release: {
251
+ readonly type: ParameterType.BOOL;
252
+ readonly default: false;
253
+ };
220
254
  };
221
255
  readonly data: {
222
256
  /** Indicates which key the participant pressed. */
@@ -225,7 +259,7 @@ declare class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
225
259
  };
226
260
  /** The response time in milliseconds for the participant to make a response. The time is measured from when the
227
261
  * stimulus first appears on the screen until the participant's response.
228
- * */
262
+ */
229
263
  readonly rt: {
230
264
  readonly type: ParameterType.INT;
231
265
  };
@@ -234,6 +268,10 @@ declare class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
234
268
  readonly type: ParameterType.STRING;
235
269
  readonly array: true;
236
270
  };
271
+ /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */
272
+ readonly rt_key_duration: {
273
+ readonly type: ParameterType.INT;
274
+ };
237
275
  };
238
276
  readonly citations: "__CITATIONS__";
239
277
  };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ParameterType } from 'jspsych';
2
2
 
3
- var version = "2.1.0";
3
+ var version = "2.2.0";
4
4
 
5
5
  const info = {
6
6
  name: "video-keyboard-response",
@@ -25,13 +25,17 @@ const info = {
25
25
  pretty_name: "Prompt",
26
26
  default: null
27
27
  },
28
- /** The width of the video in pixels. */
28
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
29
+ * or properly scaled with the aspect ratio if the height is also specified.
30
+ */
29
31
  width: {
30
32
  type: ParameterType.INT,
31
33
  pretty_name: "Width",
32
34
  default: ""
33
35
  },
34
- /** The height of the video display in pixels. */
36
+ /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,
37
+ * or properly scaled with the aspect ratio if the width is also specified.
38
+ */
35
39
  height: {
36
40
  type: ParameterType.INT,
37
41
  pretty_name: "Height",
@@ -90,6 +94,17 @@ const info = {
90
94
  type: ParameterType.BOOL,
91
95
  pretty_name: "Response allowed while playing",
92
96
  default: true
97
+ },
98
+ /** If true, the response is not registered until the participant releases the key. The response
99
+ * time (`rt`) still reflects when the key was pressed, and the additional data field
100
+ * `rt_key_duration` records how long the key was held down. Note that when this is true, the
101
+ * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at
102
+ * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the
103
+ * key is still held, no response is recorded for the trial.
104
+ */
105
+ wait_for_key_release: {
106
+ type: ParameterType.BOOL,
107
+ default: false
93
108
  }
94
109
  },
95
110
  data: {
@@ -99,7 +114,7 @@ const info = {
99
114
  },
100
115
  /** The response time in milliseconds for the participant to make a response. The time is measured from when the
101
116
  * stimulus first appears on the screen until the participant's response.
102
- * */
117
+ */
103
118
  rt: {
104
119
  type: ParameterType.INT
105
120
  },
@@ -107,6 +122,10 @@ const info = {
107
122
  stimulus: {
108
123
  type: ParameterType.STRING,
109
124
  array: true
125
+ },
126
+ /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */
127
+ rt_key_duration: {
128
+ type: ParameterType.INT
110
129
  }
111
130
  },
112
131
  // prettier-ignore
@@ -123,12 +142,6 @@ class VideoKeyboardResponsePlugin {
123
142
  this.info = info;
124
143
  }
125
144
  trial(display_element, trial) {
126
- if (!Array.isArray(trial.stimulus)) {
127
- throw new Error(`
128
- The stimulus property for the video-keyboard-response plugin must be an array
129
- of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters
130
- `);
131
- }
132
145
  var video_html = "<div>";
133
146
  video_html += '<video id="jspsych-video-keyboard-response-stimulus"';
134
147
  if (trial.width) {
@@ -186,7 +199,8 @@ class VideoKeyboardResponsePlugin {
186
199
  valid_responses: trial.choices,
187
200
  rt_method: "performance",
188
201
  persist: false,
189
- allow_held_key: false
202
+ allow_held_key: false,
203
+ wait_for_key_release: trial.wait_for_key_release
190
204
  });
191
205
  }
192
206
  };
@@ -223,7 +237,8 @@ class VideoKeyboardResponsePlugin {
223
237
  valid_responses: trial.choices,
224
238
  rt_method: "performance",
225
239
  persist: false,
226
- allow_held_key: false
240
+ allow_held_key: false,
241
+ wait_for_key_release: trial.wait_for_key_release
227
242
  });
228
243
  }
229
244
  video_element.pause();
@@ -236,7 +251,8 @@ class VideoKeyboardResponsePlugin {
236
251
  }
237
252
  var response = {
238
253
  rt: null,
239
- key: null
254
+ key: null,
255
+ rt_key_duration: null
240
256
  };
241
257
  const end_trial = () => {
242
258
  this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -248,14 +264,19 @@ class VideoKeyboardResponsePlugin {
248
264
  var trial_data = {
249
265
  rt: response.rt,
250
266
  stimulus: trial.stimulus,
251
- response: response.key
267
+ response: response.key,
268
+ rt_key_duration: response.rt_key_duration
252
269
  };
253
270
  this.jsPsych.finishTrial(trial_data);
254
271
  };
255
272
  var after_response = (info2) => {
256
273
  display_element.querySelector("#jspsych-video-keyboard-response-stimulus").className += " responded";
257
274
  if (response.key == null) {
258
- response = info2;
275
+ response = {
276
+ rt: info2.rt,
277
+ key: info2.key,
278
+ rt_key_duration: info2.rt_key_duration ?? null
279
+ };
259
280
  }
260
281
  if (trial.response_ends_trial) {
261
282
  end_trial();
@@ -267,7 +288,8 @@ class VideoKeyboardResponsePlugin {
267
288
  valid_responses: trial.choices,
268
289
  rt_method: "performance",
269
290
  persist: false,
270
- allow_held_key: false
291
+ allow_held_key: false,
292
+ wait_for_key_release: trial.wait_for_key_release
271
293
  });
272
294
  }
273
295
  if (trial.trial_duration !== null) {
@@ -310,8 +332,10 @@ class VideoKeyboardResponsePlugin {
310
332
  const default_data = {
311
333
  stimulus: trial.stimulus,
312
334
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
313
- response: this.jsPsych.pluginAPI.getValidKey(trial.choices)
335
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
336
+ rt_key_duration: null
314
337
  };
338
+ default_data.rt_key_duration = default_data.rt === null || !trial.wait_for_key_release ? null : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);
315
339
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
316
340
  this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
317
341
  return data;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.1.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.2.0\",\n \"@jspsych/test-utils\": \"^1.2.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video in pixels. */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n * */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // catch mistake where stimuli are not an array\n if (!Array.isArray(trial.stimulus)) {\n throw new Error(`\n The stimulus property for the video-keyboard-response plugin must be an array\n of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters\n `);\n }\n\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = info;\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n };\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":[],"mappings":";;AAEE,IAAW,OAAA,GAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC8GA,SAAA,EAAA;AAAA;;GAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":["../package.json","../src/index.ts"],"sourcesContent":["{\n \"name\": \"@jspsych/plugin-video-keyboard-response\",\n \"version\": \"2.2.0\",\n \"description\": \"jsPsych plugin for playing a video file and getting a keyboard response\",\n \"type\": \"module\",\n \"main\": \"dist/index.cjs\",\n \"exports\": {\n \"import\": \"./dist/index.js\",\n \"require\": \"./dist/index.cjs\"\n },\n \"typings\": \"dist/index.d.ts\",\n \"unpkg\": \"dist/index.browser.min.js\",\n \"files\": [\n \"src\",\n \"dist\"\n ],\n \"source\": \"src/index.ts\",\n \"scripts\": {\n \"test\": \"jest --passWithNoTests\",\n \"test:watch\": \"npm test -- --watch\",\n \"tsc\": \"tsc\",\n \"build\": \"rollup --config\",\n \"build:watch\": \"npm run build -- --watch\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/jspsych/jsPsych.git\",\n \"directory\": \"packages/plugin-video-keyboard-response\"\n },\n \"author\": \"Josh de Leeuw\",\n \"license\": \"MIT\",\n \"bugs\": {\n \"url\": \"https://github.com/jspsych/jsPsych/issues\"\n },\n \"homepage\": \"https://www.jspsych.org/latest/plugins/video-keyboard-response\",\n \"peerDependencies\": {\n \"jspsych\": \">=7.1.0\"\n },\n \"devDependencies\": {\n \"@jspsych/config\": \"^3.3.4\",\n \"@jspsych/test-utils\": \"^1.3.0\"\n }\n}\n","import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from \"jspsych\";\n\nimport { version } from \"../package.json\";\n\nconst info = <const>{\n name: \"video-keyboard-response\",\n version: version,\n parameters: {\n /** Array of the video file(s) to play. Video can be provided in multiple file formats for better cross-browser support. */\n stimulus: {\n type: ParameterType.VIDEO,\n pretty_name: \"Video\",\n default: undefined,\n array: true,\n },\n /** Array containing the key(s) the subject is allowed to press to respond to the stimulus. */\n choices: {\n type: ParameterType.KEYS,\n pretty_name: \"Choices\",\n default: \"ALL_KEYS\",\n },\n /** Any content here will be displayed below the stimulus. */\n prompt: {\n type: ParameterType.HTML_STRING,\n pretty_name: \"Prompt\",\n default: null,\n },\n /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the height is also specified.\n */\n width: {\n type: ParameterType.INT,\n pretty_name: \"Width\",\n default: \"\",\n },\n /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,\n * or properly scaled with the aspect ratio if the width is also specified.\n */\n height: {\n type: ParameterType.INT,\n pretty_name: \"Height\",\n default: \"\",\n },\n /** If true, the video will begin playing as soon as it has loaded. */\n autoplay: {\n type: ParameterType.BOOL,\n pretty_name: \"Autoplay\",\n default: true,\n },\n /** If true, the subject will be able to pause the video or move the playback to any point in the video. */\n controls: {\n type: ParameterType.BOOL,\n pretty_name: \"Controls\",\n default: false,\n },\n /** Time to start the clip. If null (default), video will start at the beginning of the file. */\n start: {\n type: ParameterType.FLOAT,\n pretty_name: \"Start\",\n default: null,\n },\n /** Time to stop the clip. If null (default), video will stop at the end of the file. */\n stop: {\n type: ParameterType.FLOAT,\n pretty_name: \"Stop\",\n default: null,\n },\n /** The playback rate of the video. 1 is normal, <1 is slower, >1 is faster. */\n rate: {\n type: ParameterType.FLOAT,\n pretty_name: \"Rate\",\n default: 1,\n },\n /** If true, the trial will end immediately after the video finishes playing. */\n trial_ends_after_video: {\n type: ParameterType.BOOL,\n pretty_name: \"End trial after video finishes\",\n default: false,\n },\n /** How long to show trial before it ends. */\n trial_duration: {\n type: ParameterType.INT,\n pretty_name: \"Trial duration\",\n default: null,\n },\n /** If true, the trial will end when subject makes a response. */\n response_ends_trial: {\n type: ParameterType.BOOL,\n pretty_name: \"Response ends trial\",\n default: true,\n },\n /** If true, then responses are allowed while the video is playing. If false, then the video must finish playing before a response is accepted. */\n response_allowed_while_playing: {\n type: ParameterType.BOOL,\n pretty_name: \"Response allowed while playing\",\n default: true,\n },\n /** If true, the response is not registered until the participant releases the key. The response\n * time (`rt`) still reflects when the key was pressed, and the additional data field\n * `rt_key_duration` records how long the key was held down. Note that when this is true, the\n * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at\n * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the\n * key is still held, no response is recorded for the trial.\n */\n wait_for_key_release: {\n type: ParameterType.BOOL,\n default: false,\n },\n },\n data: {\n /** Indicates which key the participant pressed. */\n response: {\n type: ParameterType.STRING,\n },\n /** The response time in milliseconds for the participant to make a response. The time is measured from when the\n * stimulus first appears on the screen until the participant's response.\n */\n rt: {\n type: ParameterType.INT,\n },\n /** The `stimulus` array. This will be encoded as a JSON string when data is saved using the `.json()` or `.csv()` functions. */\n stimulus: {\n type: ParameterType.STRING,\n array: true,\n },\n /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */\n rt_key_duration: {\n type: ParameterType.INT,\n },\n },\n // prettier-ignore\n citations: '__CITATIONS__',\n};\n\ntype Info = typeof info;\n\n/**\n * This plugin plays a video file and records a keyboard response. The stimulus can be displayed until a response is\n * given, or for a pre-determined amount of time. The trial can be ended automatically when the participant responds,\n * when the video file has finished playing, or if the participant has failed to respond within a fixed length of time.\n * You can also prevent a keyboard response from being recorded before the video has finished playing.\n *\n * Video files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are\n * using timeline variables or another dynamic method to specify the video stimulus, you will need to\n * [manually preload](../overview/media-preloading.md#manual-preloading) the videos. Also note that video preloading\n * is disabled when the experiment is running as a file (i.e. opened directly in the browser, rather than through a\n * server), in order to prevent CORS errors - see the section on [Running Experiments](../overview/running-experiments.md)\n * for more information.\n *\n * @author Josh de Leeuw\n * @see {@link https://www.jspsych.org/latest/plugins/video-keyboard-response/ video-keyboard-response plugin documentation on jspsych.org}\n */\nclass VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {\n static info = info;\n\n constructor(private jsPsych: JsPsych) {}\n\n trial(display_element: HTMLElement, trial: TrialType<Info>) {\n // setup stimulus\n var video_html = \"<div>\";\n video_html += '<video id=\"jspsych-video-keyboard-response-stimulus\"';\n\n if (trial.width) {\n video_html += ' width=\"' + trial.width + '\"';\n }\n if (trial.height) {\n video_html += ' height=\"' + trial.height + '\"';\n }\n if (trial.autoplay && trial.start == null) {\n // if autoplay is true and the start time is specified, then the video will start automatically\n // via the play() method, rather than the autoplay attribute, to prevent showing the first frame\n video_html += \" autoplay \";\n }\n if (trial.controls) {\n video_html += \" controls \";\n }\n if (trial.start !== null) {\n // hide video element when page loads if the start time is specified,\n // to prevent the video element from showing the first frame\n video_html += ' style=\"visibility: hidden;\"';\n }\n video_html += \">\";\n\n var video_preload_blob = this.jsPsych.pluginAPI.getVideoBuffer(trial.stimulus[0]);\n if (!video_preload_blob) {\n for (var i = 0; i < trial.stimulus.length; i++) {\n var file_name = trial.stimulus[i];\n if (file_name.indexOf(\"?\") > -1) {\n file_name = file_name.substring(0, file_name.indexOf(\"?\"));\n }\n var type = file_name.substr(file_name.lastIndexOf(\".\") + 1);\n type = type.toLowerCase();\n if (type == \"mov\") {\n console.warn(\n \"Warning: video-keyboard-response plugin does not reliably support .mov files.\"\n );\n }\n video_html += '<source src=\"' + file_name + '\" type=\"video/' + type + '\">';\n }\n }\n video_html += \"</video>\";\n video_html += \"</div>\";\n\n // add prompt if there is one\n if (trial.prompt !== null) {\n video_html += trial.prompt;\n }\n\n display_element.innerHTML = video_html;\n\n var video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n );\n\n if (video_preload_blob) {\n video_element.src = video_preload_blob;\n }\n\n video_element.onended = () => {\n if (trial.trial_ends_after_video) {\n end_trial();\n }\n if (trial.response_allowed_while_playing == false && !trial.trial_ends_after_video) {\n // start keyboard listener\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n };\n\n video_element.playbackRate = trial.rate;\n\n // if video start time is specified, hide the video and set the starting time\n // before showing and playing, so that the video doesn't automatically show the first frame\n if (trial.start !== null) {\n video_element.pause();\n video_element.onseeked = () => {\n video_element.style.visibility = \"visible\";\n video_element.muted = false;\n if (trial.autoplay) {\n video_element.play();\n } else {\n video_element.pause();\n }\n video_element.onseeked = () => {};\n };\n video_element.onplaying = () => {\n video_element.currentTime = trial.start;\n video_element.onplaying = () => {};\n };\n // fix for iOS/MacOS browsers: videos aren't seekable until they start playing, so need to hide/mute, play,\n // change current time, then show/unmute\n video_element.muted = true;\n video_element.play();\n }\n\n let stopped = false;\n if (trial.stop !== null) {\n video_element.addEventListener(\"timeupdate\", (e) => {\n var currenttime = video_element.currentTime;\n if (currenttime >= trial.stop) {\n if (!trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n video_element.pause();\n if (trial.trial_ends_after_video && !stopped) {\n // this is to prevent end_trial from being called twice, because the timeupdate event\n // can fire in quick succession\n stopped = true;\n end_trial();\n }\n }\n });\n }\n\n // store response\n var response = {\n rt: null,\n key: null,\n rt_key_duration: null,\n };\n\n // function to end trial when it is time\n const end_trial = () => {\n // kill keyboard listeners\n this.jsPsych.pluginAPI.cancelAllKeyboardResponses();\n\n // stop the video file if it is playing\n // remove end event listeners if they exist\n display_element\n .querySelector<HTMLVideoElement>(\"#jspsych-video-keyboard-response-stimulus\")\n .pause();\n display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-keyboard-response-stimulus\"\n ).onended = () => {};\n\n // gather the data to store for the trial\n var trial_data = {\n rt: response.rt,\n stimulus: trial.stimulus,\n response: response.key,\n rt_key_duration: response.rt_key_duration,\n };\n\n // move on to the next trial\n this.jsPsych.finishTrial(trial_data);\n };\n\n // function to handle responses by the subject\n var after_response = (info) => {\n // after a valid response, the stimulus will have the CSS class 'responded'\n // which can be used to provide visual feedback that a response was recorded\n display_element.querySelector(\"#jspsych-video-keyboard-response-stimulus\").className +=\n \" responded\";\n\n // only record the first response\n if (response.key == null) {\n response = {\n rt: info.rt,\n key: info.key,\n rt_key_duration: info.rt_key_duration ?? null,\n };\n }\n\n if (trial.response_ends_trial) {\n end_trial();\n }\n };\n\n // start the response listener\n if (trial.choices != \"NO_KEYS\" && trial.response_allowed_while_playing) {\n var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({\n callback_function: after_response,\n valid_responses: trial.choices,\n rt_method: \"performance\",\n persist: false,\n allow_held_key: false,\n wait_for_key_release: trial.wait_for_key_release,\n });\n }\n\n // end trial if time limit is set\n if (trial.trial_duration !== null) {\n this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);\n }\n }\n\n simulate(\n trial: TrialType<Info>,\n simulation_mode,\n simulation_options: any,\n load_callback: () => void\n ) {\n if (simulation_mode == \"data-only\") {\n load_callback();\n this.simulate_data_only(trial, simulation_options);\n }\n if (simulation_mode == \"visual\") {\n this.simulate_visual(trial, simulation_options, load_callback);\n }\n }\n\n private simulate_data_only(trial: TrialType<Info>, simulation_options) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n this.jsPsych.finishTrial(data);\n }\n\n private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {\n const data = this.create_simulation_data(trial, simulation_options);\n\n const display_element = this.jsPsych.getDisplayElement();\n\n this.trial(display_element, trial);\n load_callback();\n\n const video_element = display_element.querySelector<HTMLVideoElement>(\n \"#jspsych-video-button-response-stimulus\"\n );\n\n const respond = () => {\n if (data.rt !== null) {\n this.jsPsych.pluginAPI.pressKey(data.response, data.rt);\n }\n };\n\n if (!trial.response_allowed_while_playing) {\n video_element.addEventListener(\"ended\", respond);\n } else {\n respond();\n }\n }\n\n private create_simulation_data(trial: TrialType<Info>, simulation_options) {\n const default_data = {\n stimulus: trial.stimulus,\n rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),\n response: this.jsPsych.pluginAPI.getValidKey(trial.choices),\n rt_key_duration: null,\n };\n default_data.rt_key_duration =\n default_data.rt === null || !trial.wait_for_key_release\n ? null\n : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);\n\n const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);\n\n this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);\n\n return data;\n }\n}\n\nexport default VideoKeyboardResponsePlugin;\n"],"names":[],"mappings":";;AAEE,IAAA,OAAA,GAAW,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECiIA,SAAA,EAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspsych/plugin-video-keyboard-response",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "jsPsych plugin for playing a video file and getting a keyboard response",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -37,7 +37,7 @@
37
37
  "jspsych": ">=7.1.0"
38
38
  },
39
39
  "devDependencies": {
40
- "@jspsych/config": "^3.2.0",
41
- "@jspsych/test-utils": "^1.2.0"
40
+ "@jspsych/config": "^3.3.4",
41
+ "@jspsych/test-utils": "^1.3.0"
42
42
  }
43
43
  }
package/src/index.spec.ts CHANGED
@@ -23,7 +23,7 @@ describe("video-keyboard-response", () => {
23
23
 
24
24
  await expect(async () => {
25
25
  await jsPsych.run(timeline);
26
- }).rejects.toThrowError();
26
+ }).rejects.toThrow();
27
27
  });
28
28
  });
29
29
 
@@ -42,6 +42,24 @@ describe("video-keyboard-response simulation", () => {
42
42
 
43
43
  expect(getData().values()[0].rt).toBeGreaterThan(0);
44
44
  expect(typeof getData().values()[0].response).toBe("string");
45
+ expect(getData().values()[0].rt_key_duration).toBe(null);
46
+ });
47
+
48
+ test("data mode records rt_key_duration when wait_for_key_release is true", async () => {
49
+ const timeline = [
50
+ {
51
+ type: videoKeyboardResponse,
52
+ stimulus: ["foo.mp4"],
53
+ wait_for_key_release: true,
54
+ },
55
+ ];
56
+
57
+ const { expectFinished, getData } = await simulateTimeline(timeline);
58
+
59
+ await expectFinished();
60
+
61
+ expect(getData().values()[0].rt).toBeGreaterThan(0);
62
+ expect(typeof getData().values()[0].rt_key_duration).toBe("number");
45
63
  });
46
64
 
47
65
  // can't run this until we mock video elements.
package/src/index.ts CHANGED
@@ -25,13 +25,17 @@ const info = <const>{
25
25
  pretty_name: "Prompt",
26
26
  default: null,
27
27
  },
28
- /** The width of the video in pixels. */
28
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
29
+ * or properly scaled with the aspect ratio if the height is also specified.
30
+ */
29
31
  width: {
30
32
  type: ParameterType.INT,
31
33
  pretty_name: "Width",
32
34
  default: "",
33
35
  },
34
- /** The height of the video display in pixels. */
36
+ /** The height of the video display in pixels. If `null`, the video will take the original video's dimensions,
37
+ * or properly scaled with the aspect ratio if the width is also specified.
38
+ */
35
39
  height: {
36
40
  type: ParameterType.INT,
37
41
  pretty_name: "Height",
@@ -91,6 +95,17 @@ const info = <const>{
91
95
  pretty_name: "Response allowed while playing",
92
96
  default: true,
93
97
  },
98
+ /** If true, the response is not registered until the participant releases the key. The response
99
+ * time (`rt`) still reflects when the key was pressed, and the additional data field
100
+ * `rt_key_duration` records how long the key was held down. Note that when this is true, the
101
+ * trial cannot end until the key is released: with `response_ends_trial: true` the trial ends at
102
+ * the key release, and if the trial ends for another reason (e.g., `trial_duration`) while the
103
+ * key is still held, no response is recorded for the trial.
104
+ */
105
+ wait_for_key_release: {
106
+ type: ParameterType.BOOL,
107
+ default: false,
108
+ },
94
109
  },
95
110
  data: {
96
111
  /** Indicates which key the participant pressed. */
@@ -99,7 +114,7 @@ const info = <const>{
99
114
  },
100
115
  /** The response time in milliseconds for the participant to make a response. The time is measured from when the
101
116
  * stimulus first appears on the screen until the participant's response.
102
- * */
117
+ */
103
118
  rt: {
104
119
  type: ParameterType.INT,
105
120
  },
@@ -108,6 +123,10 @@ const info = <const>{
108
123
  type: ParameterType.STRING,
109
124
  array: true,
110
125
  },
126
+ /** The duration in milliseconds that the response key was held down, measured from key press to key release. Only recorded when `wait_for_key_release` is true; null otherwise or when no response was made. */
127
+ rt_key_duration: {
128
+ type: ParameterType.INT,
129
+ },
111
130
  },
112
131
  // prettier-ignore
113
132
  citations: '__CITATIONS__',
@@ -137,14 +156,6 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
137
156
  constructor(private jsPsych: JsPsych) {}
138
157
 
139
158
  trial(display_element: HTMLElement, trial: TrialType<Info>) {
140
- // catch mistake where stimuli are not an array
141
- if (!Array.isArray(trial.stimulus)) {
142
- throw new Error(`
143
- The stimulus property for the video-keyboard-response plugin must be an array
144
- of files. See https://www.jspsych.org/latest/plugins/video-keyboard-response/#parameters
145
- `);
146
- }
147
-
148
159
  // setup stimulus
149
160
  var video_html = "<div>";
150
161
  video_html += '<video id="jspsych-video-keyboard-response-stimulus"';
@@ -217,6 +228,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
217
228
  rt_method: "performance",
218
229
  persist: false,
219
230
  allow_held_key: false,
231
+ wait_for_key_release: trial.wait_for_key_release,
220
232
  });
221
233
  }
222
234
  };
@@ -259,6 +271,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
259
271
  rt_method: "performance",
260
272
  persist: false,
261
273
  allow_held_key: false,
274
+ wait_for_key_release: trial.wait_for_key_release,
262
275
  });
263
276
  }
264
277
  video_element.pause();
@@ -276,6 +289,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
276
289
  var response = {
277
290
  rt: null,
278
291
  key: null,
292
+ rt_key_duration: null,
279
293
  };
280
294
 
281
295
  // function to end trial when it is time
@@ -297,6 +311,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
297
311
  rt: response.rt,
298
312
  stimulus: trial.stimulus,
299
313
  response: response.key,
314
+ rt_key_duration: response.rt_key_duration,
300
315
  };
301
316
 
302
317
  // move on to the next trial
@@ -312,7 +327,11 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
312
327
 
313
328
  // only record the first response
314
329
  if (response.key == null) {
315
- response = info;
330
+ response = {
331
+ rt: info.rt,
332
+ key: info.key,
333
+ rt_key_duration: info.rt_key_duration ?? null,
334
+ };
316
335
  }
317
336
 
318
337
  if (trial.response_ends_trial) {
@@ -328,6 +347,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
328
347
  rt_method: "performance",
329
348
  persist: false,
330
349
  allow_held_key: false,
350
+ wait_for_key_release: trial.wait_for_key_release,
331
351
  });
332
352
  }
333
353
 
@@ -388,7 +408,12 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
388
408
  stimulus: trial.stimulus,
389
409
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
390
410
  response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
411
+ rt_key_duration: null,
391
412
  };
413
+ default_data.rt_key_duration =
414
+ default_data.rt === null || !trial.wait_for_key_release
415
+ ? null
416
+ : this.jsPsych.randomization.sampleExGaussian(150, 30, 1 / 100, true);
392
417
 
393
418
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
394
419