@jspsych/plugin-video-keyboard-response 2.1.1 → 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.1";
4
+ var version = "2.2.0";
5
5
 
6
6
  const info = {
7
7
  name: "video-keyboard-response",
@@ -26,7 +26,7 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
26
26
  pretty_name: "Prompt",
27
27
  default: null
28
28
  },
29
- /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
29
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
30
30
  * or properly scaled with the aspect ratio if the height is also specified.
31
31
  */
32
32
  width: {
@@ -95,6 +95,17 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
95
95
  type: jspsych.ParameterType.BOOL,
96
96
  pretty_name: "Response allowed while playing",
97
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
98
109
  }
99
110
  },
100
111
  data: {
@@ -112,6 +123,10 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
112
123
  stimulus: {
113
124
  type: jspsych.ParameterType.STRING,
114
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
115
130
  }
116
131
  },
117
132
  // prettier-ignore
@@ -185,7 +200,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
185
200
  valid_responses: trial.choices,
186
201
  rt_method: "performance",
187
202
  persist: false,
188
- allow_held_key: false
203
+ allow_held_key: false,
204
+ wait_for_key_release: trial.wait_for_key_release
189
205
  });
190
206
  }
191
207
  };
@@ -222,7 +238,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
222
238
  valid_responses: trial.choices,
223
239
  rt_method: "performance",
224
240
  persist: false,
225
- allow_held_key: false
241
+ allow_held_key: false,
242
+ wait_for_key_release: trial.wait_for_key_release
226
243
  });
227
244
  }
228
245
  video_element.pause();
@@ -235,7 +252,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
235
252
  }
236
253
  var response = {
237
254
  rt: null,
238
- key: null
255
+ key: null,
256
+ rt_key_duration: null
239
257
  };
240
258
  const end_trial = () => {
241
259
  this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -247,14 +265,19 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
247
265
  var trial_data = {
248
266
  rt: response.rt,
249
267
  stimulus: trial.stimulus,
250
- response: response.key
268
+ response: response.key,
269
+ rt_key_duration: response.rt_key_duration
251
270
  };
252
271
  this.jsPsych.finishTrial(trial_data);
253
272
  };
254
273
  var after_response = (info2) => {
255
274
  display_element.querySelector("#jspsych-video-keyboard-response-stimulus").className += " responded";
256
275
  if (response.key == null) {
257
- response = info2;
276
+ response = {
277
+ rt: info2.rt,
278
+ key: info2.key,
279
+ rt_key_duration: info2.rt_key_duration ?? null
280
+ };
258
281
  }
259
282
  if (trial.response_ends_trial) {
260
283
  end_trial();
@@ -266,7 +289,8 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
266
289
  valid_responses: trial.choices,
267
290
  rt_method: "performance",
268
291
  persist: false,
269
- allow_held_key: false
292
+ allow_held_key: false,
293
+ wait_for_key_release: trial.wait_for_key_release
270
294
  });
271
295
  }
272
296
  if (trial.trial_duration !== null) {
@@ -309,8 +333,10 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
309
333
  const default_data = {
310
334
  stimulus: trial.stimulus,
311
335
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
312
- response: this.jsPsych.pluginAPI.getValidKey(trial.choices)
336
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
337
+ rt_key_duration: null
313
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);
314
340
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
315
341
  this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
316
342
  return data;
@@ -320,4 +346,4 @@ var jsPsychVideoKeyboardResponse = (function (jspsych) {
320
346
  return VideoKeyboardResponsePlugin;
321
347
 
322
348
  })(jsPsychModule);
323
- //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-video-keyboard-response@2.1.1/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.1\",\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.1\",\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 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 },\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 // 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICkHA,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,2 +1,2 @@
1
- var jsPsychVideoKeyboardResponse=function(r){"use strict";var c="2.1.1";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(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 i=this.jsPsych.pluginAPI.getVideoBuffer(e.stimulus[0]);if(!i)for(var n=0;n<e.stimulus.length;n++){var l=e.stimulus[n];l.indexOf("?")>-1&&(l=l.substring(0,l.indexOf("?")));var o=l.substr(l.lastIndexOf(".")+1);o=o.toLowerCase(),o=="mov"&&console.warn("Warning: video-keyboard-response plugin does not reliably support .mov files."),t+='<source src="'+l+'" 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");i&&(s.src=i),s.onended=()=>{if(e.trial_ends_after_video&&d(),e.response_allowed_while_playing==!1&&!e.trial_ends_after_video)var u=this.jsPsych.pluginAPI.getKeyboardResponse({callback_function:y,valid_responses:e.choices,rt_method:"performance",persist:!1,allow_held_key:!1})},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",u=>{var v=s.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});s.pause(),e.trial_ends_after_video&&!h&&(h=!0,d())}});var p={rt:null,key:null};const d=()=>{this.jsPsych.pluginAPI.cancelAllKeyboardResponses(),a.querySelector("#jspsych-video-keyboard-response-stimulus").pause(),a.querySelector("#jspsych-video-keyboard-response-stimulus").onended=()=>{};var u={rt:p.rt,stimulus:e.stimulus,response:p.key};this.jsPsych.finishTrial(u)};var y=u=>{a.querySelector("#jspsych-video-keyboard-response-stimulus").className+=" responded",p.key==null&&(p=u),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(a,e,t,i){e=="data-only"&&(i(),this.simulate_data_only(a,t)),e=="visual"&&this.simulate_visual(a,t,i)}simulate_data_only(a,e){const t=this.create_simulation_data(a,e);this.jsPsych.finishTrial(t)}simulate_visual(a,e,t){const i=this.create_simulation_data(a,e),n=this.jsPsych.getDisplayElement();this.trial(n,a),t();const l=n.querySelector("#jspsych-video-button-response-stimulus"),o=()=>{i.rt!==null&&this.jsPsych.pluginAPI.pressKey(i.response,i.rt)};a.response_allowed_while_playing?o():l.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)},i=this.jsPsych.pluginAPI.mergeSimulationData(t,e);return this.jsPsych.pluginAPI.ensureSimulationDataConsistency(a,i),i}}return m.info=_,m}(jsPsychModule);
2
- //# sourceMappingURL=https://unpkg.com/@jspsych/plugin-video-keyboard-response@2.1.1/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.1\",\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.1\",\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 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 },\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 // 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,kxCCkHA,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.1";
5
+ var version = "2.2.0";
6
6
 
7
7
  const info = {
8
8
  name: "video-keyboard-response",
@@ -27,7 +27,7 @@ const info = {
27
27
  pretty_name: "Prompt",
28
28
  default: null
29
29
  },
30
- /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
30
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
31
31
  * or properly scaled with the aspect ratio if the height is also specified.
32
32
  */
33
33
  width: {
@@ -96,6 +96,17 @@ const info = {
96
96
  type: jspsych.ParameterType.BOOL,
97
97
  pretty_name: "Response allowed while playing",
98
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
99
110
  }
100
111
  },
101
112
  data: {
@@ -113,6 +124,10 @@ const info = {
113
124
  stimulus: {
114
125
  type: jspsych.ParameterType.STRING,
115
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
116
131
  }
117
132
  },
118
133
  // prettier-ignore
@@ -186,7 +201,8 @@ class VideoKeyboardResponsePlugin {
186
201
  valid_responses: trial.choices,
187
202
  rt_method: "performance",
188
203
  persist: false,
189
- allow_held_key: false
204
+ allow_held_key: false,
205
+ wait_for_key_release: trial.wait_for_key_release
190
206
  });
191
207
  }
192
208
  };
@@ -223,7 +239,8 @@ class VideoKeyboardResponsePlugin {
223
239
  valid_responses: trial.choices,
224
240
  rt_method: "performance",
225
241
  persist: false,
226
- allow_held_key: false
242
+ allow_held_key: false,
243
+ wait_for_key_release: trial.wait_for_key_release
227
244
  });
228
245
  }
229
246
  video_element.pause();
@@ -236,7 +253,8 @@ class VideoKeyboardResponsePlugin {
236
253
  }
237
254
  var response = {
238
255
  rt: null,
239
- key: null
256
+ key: null,
257
+ rt_key_duration: null
240
258
  };
241
259
  const end_trial = () => {
242
260
  this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -248,14 +266,19 @@ class VideoKeyboardResponsePlugin {
248
266
  var trial_data = {
249
267
  rt: response.rt,
250
268
  stimulus: trial.stimulus,
251
- response: response.key
269
+ response: response.key,
270
+ rt_key_duration: response.rt_key_duration
252
271
  };
253
272
  this.jsPsych.finishTrial(trial_data);
254
273
  };
255
274
  var after_response = (info2) => {
256
275
  display_element.querySelector("#jspsych-video-keyboard-response-stimulus").className += " responded";
257
276
  if (response.key == null) {
258
- response = info2;
277
+ response = {
278
+ rt: info2.rt,
279
+ key: info2.key,
280
+ rt_key_duration: info2.rt_key_duration ?? null
281
+ };
259
282
  }
260
283
  if (trial.response_ends_trial) {
261
284
  end_trial();
@@ -267,7 +290,8 @@ class VideoKeyboardResponsePlugin {
267
290
  valid_responses: trial.choices,
268
291
  rt_method: "performance",
269
292
  persist: false,
270
- allow_held_key: false
293
+ allow_held_key: false,
294
+ wait_for_key_release: trial.wait_for_key_release
271
295
  });
272
296
  }
273
297
  if (trial.trial_duration !== null) {
@@ -310,8 +334,10 @@ class VideoKeyboardResponsePlugin {
310
334
  const default_data = {
311
335
  stimulus: trial.stimulus,
312
336
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
313
- response: this.jsPsych.pluginAPI.getValidKey(trial.choices)
337
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
338
+ rt_key_duration: null
314
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);
315
341
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
316
342
  this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
317
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.1\",\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.1\",\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 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 },\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 // 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECkHA,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
@@ -93,6 +93,17 @@ declare const info: {
93
93
  readonly pretty_name: "Response allowed while playing";
94
94
  readonly default: true;
95
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
+ };
96
107
  };
97
108
  readonly data: {
98
109
  /** Indicates which key the participant pressed. */
@@ -110,6 +121,10 @@ declare const info: {
110
121
  readonly type: ParameterType.STRING;
111
122
  readonly array: true;
112
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
+ };
113
128
  };
114
129
  readonly citations: "__CITATIONS__";
115
130
  };
@@ -225,6 +240,17 @@ declare class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
225
240
  readonly pretty_name: "Response allowed while playing";
226
241
  readonly default: true;
227
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
+ };
228
254
  };
229
255
  readonly data: {
230
256
  /** Indicates which key the participant pressed. */
@@ -242,6 +268,10 @@ declare class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
242
268
  readonly type: ParameterType.STRING;
243
269
  readonly array: true;
244
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
+ };
245
275
  };
246
276
  readonly citations: "__CITATIONS__";
247
277
  };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ParameterType } from 'jspsych';
2
2
 
3
- var version = "2.1.1";
3
+ var version = "2.2.0";
4
4
 
5
5
  const info = {
6
6
  name: "video-keyboard-response",
@@ -25,7 +25,7 @@ const info = {
25
25
  pretty_name: "Prompt",
26
26
  default: null
27
27
  },
28
- /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
28
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
29
29
  * or properly scaled with the aspect ratio if the height is also specified.
30
30
  */
31
31
  width: {
@@ -94,6 +94,17 @@ const info = {
94
94
  type: ParameterType.BOOL,
95
95
  pretty_name: "Response allowed while playing",
96
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
97
108
  }
98
109
  },
99
110
  data: {
@@ -111,6 +122,10 @@ const info = {
111
122
  stimulus: {
112
123
  type: ParameterType.STRING,
113
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
114
129
  }
115
130
  },
116
131
  // prettier-ignore
@@ -184,7 +199,8 @@ class VideoKeyboardResponsePlugin {
184
199
  valid_responses: trial.choices,
185
200
  rt_method: "performance",
186
201
  persist: false,
187
- allow_held_key: false
202
+ allow_held_key: false,
203
+ wait_for_key_release: trial.wait_for_key_release
188
204
  });
189
205
  }
190
206
  };
@@ -221,7 +237,8 @@ class VideoKeyboardResponsePlugin {
221
237
  valid_responses: trial.choices,
222
238
  rt_method: "performance",
223
239
  persist: false,
224
- allow_held_key: false
240
+ allow_held_key: false,
241
+ wait_for_key_release: trial.wait_for_key_release
225
242
  });
226
243
  }
227
244
  video_element.pause();
@@ -234,7 +251,8 @@ class VideoKeyboardResponsePlugin {
234
251
  }
235
252
  var response = {
236
253
  rt: null,
237
- key: null
254
+ key: null,
255
+ rt_key_duration: null
238
256
  };
239
257
  const end_trial = () => {
240
258
  this.jsPsych.pluginAPI.cancelAllKeyboardResponses();
@@ -246,14 +264,19 @@ class VideoKeyboardResponsePlugin {
246
264
  var trial_data = {
247
265
  rt: response.rt,
248
266
  stimulus: trial.stimulus,
249
- response: response.key
267
+ response: response.key,
268
+ rt_key_duration: response.rt_key_duration
250
269
  };
251
270
  this.jsPsych.finishTrial(trial_data);
252
271
  };
253
272
  var after_response = (info2) => {
254
273
  display_element.querySelector("#jspsych-video-keyboard-response-stimulus").className += " responded";
255
274
  if (response.key == null) {
256
- response = info2;
275
+ response = {
276
+ rt: info2.rt,
277
+ key: info2.key,
278
+ rt_key_duration: info2.rt_key_duration ?? null
279
+ };
257
280
  }
258
281
  if (trial.response_ends_trial) {
259
282
  end_trial();
@@ -265,7 +288,8 @@ class VideoKeyboardResponsePlugin {
265
288
  valid_responses: trial.choices,
266
289
  rt_method: "performance",
267
290
  persist: false,
268
- allow_held_key: false
291
+ allow_held_key: false,
292
+ wait_for_key_release: trial.wait_for_key_release
269
293
  });
270
294
  }
271
295
  if (trial.trial_duration !== null) {
@@ -308,8 +332,10 @@ class VideoKeyboardResponsePlugin {
308
332
  const default_data = {
309
333
  stimulus: trial.stimulus,
310
334
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
311
- response: this.jsPsych.pluginAPI.getValidKey(trial.choices)
335
+ response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
336
+ rt_key_duration: null
312
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);
313
339
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
314
340
  this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
315
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.1\",\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.1\",\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 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 },\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 // 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECkHA,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.1",
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.3.1",
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
@@ -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,7 +25,7 @@ const info = <const>{
25
25
  pretty_name: "Prompt",
26
26
  default: null,
27
27
  },
28
- /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
28
+ /** The width of the video display in pixels. If `null`, the video will take the original video's dimensions,
29
29
  * or properly scaled with the aspect ratio if the height is also specified.
30
30
  */
31
31
  width: {
@@ -95,6 +95,17 @@ const info = <const>{
95
95
  pretty_name: "Response allowed while playing",
96
96
  default: true,
97
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
+ },
98
109
  },
99
110
  data: {
100
111
  /** Indicates which key the participant pressed. */
@@ -112,6 +123,10 @@ const info = <const>{
112
123
  type: ParameterType.STRING,
113
124
  array: true,
114
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
+ },
115
130
  },
116
131
  // prettier-ignore
117
132
  citations: '__CITATIONS__',
@@ -213,6 +228,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
213
228
  rt_method: "performance",
214
229
  persist: false,
215
230
  allow_held_key: false,
231
+ wait_for_key_release: trial.wait_for_key_release,
216
232
  });
217
233
  }
218
234
  };
@@ -255,6 +271,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
255
271
  rt_method: "performance",
256
272
  persist: false,
257
273
  allow_held_key: false,
274
+ wait_for_key_release: trial.wait_for_key_release,
258
275
  });
259
276
  }
260
277
  video_element.pause();
@@ -272,6 +289,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
272
289
  var response = {
273
290
  rt: null,
274
291
  key: null,
292
+ rt_key_duration: null,
275
293
  };
276
294
 
277
295
  // function to end trial when it is time
@@ -293,6 +311,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
293
311
  rt: response.rt,
294
312
  stimulus: trial.stimulus,
295
313
  response: response.key,
314
+ rt_key_duration: response.rt_key_duration,
296
315
  };
297
316
 
298
317
  // move on to the next trial
@@ -308,7 +327,11 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
308
327
 
309
328
  // only record the first response
310
329
  if (response.key == null) {
311
- response = info;
330
+ response = {
331
+ rt: info.rt,
332
+ key: info.key,
333
+ rt_key_duration: info.rt_key_duration ?? null,
334
+ };
312
335
  }
313
336
 
314
337
  if (trial.response_ends_trial) {
@@ -324,6 +347,7 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
324
347
  rt_method: "performance",
325
348
  persist: false,
326
349
  allow_held_key: false,
350
+ wait_for_key_release: trial.wait_for_key_release,
327
351
  });
328
352
  }
329
353
 
@@ -384,7 +408,12 @@ class VideoKeyboardResponsePlugin implements JsPsychPlugin<Info> {
384
408
  stimulus: trial.stimulus,
385
409
  rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
386
410
  response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
411
+ rt_key_duration: null,
387
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);
388
417
 
389
418
  const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
390
419