From 7e33af85cb8938340712ab275cbfd315c6f134b3 Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Fri, 12 Jul 2019 11:38:21 -0400 Subject: [PATCH] add video-* plugins #636 --- examples/jspsych-video-button-response.html | 46 +++ examples/jspsych-video-keyboard-response.html | 43 +++ examples/jspsych-video-slider-response.html | 48 +++ plugins/jspsych-video-button-response.js | 279 +++++++++++++++++ plugins/jspsych-video-keyboard-response.js | 64 +++- plugins/jspsych-video-slider-response.js | 291 ++++++++++++++++++ plugins/jspsych-video.js | 164 ---------- 7 files changed, 762 insertions(+), 173 deletions(-) create mode 100644 examples/jspsych-video-button-response.html create mode 100644 examples/jspsych-video-keyboard-response.html create mode 100644 examples/jspsych-video-slider-response.html create mode 100644 plugins/jspsych-video-button-response.js create mode 100644 plugins/jspsych-video-slider-response.js delete mode 100644 plugins/jspsych-video.js diff --git a/examples/jspsych-video-button-response.html b/examples/jspsych-video-button-response.html new file mode 100644 index 00000000..093562bb --- /dev/null +++ b/examples/jspsych-video-button-response.html @@ -0,0 +1,46 @@ + + + + + + + + + + + diff --git a/examples/jspsych-video-keyboard-response.html b/examples/jspsych-video-keyboard-response.html new file mode 100644 index 00000000..cb2840b9 --- /dev/null +++ b/examples/jspsych-video-keyboard-response.html @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/examples/jspsych-video-slider-response.html b/examples/jspsych-video-slider-response.html new file mode 100644 index 00000000..ef0521cb --- /dev/null +++ b/examples/jspsych-video-slider-response.html @@ -0,0 +1,48 @@ + + + + + + + + + + + diff --git a/plugins/jspsych-video-button-response.js b/plugins/jspsych-video-button-response.js new file mode 100644 index 00000000..b34447cb --- /dev/null +++ b/plugins/jspsych-video-button-response.js @@ -0,0 +1,279 @@ +/** + * jspsych-video-button-response + * Josh de Leeuw + * + * plugin for playing a video file and getting a button response + * + * documentation: docs.jspsych.org + * + **/ + +jsPsych.plugins["video-button-response"] = (function() { + + var plugin = {}; + + jsPsych.pluginAPI.registerPreload('video-button-response', 'stimulus', 'video'); + + plugin.info = { + name: 'video-button-response', + description: '', + parameters: { + sources: { + type: jsPsych.plugins.parameterType.VIDEO, + pretty_name: 'Video', + default: undefined, + description: 'The video file to play.' + }, + choices: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Choices', + default: undefined, + array: true, + description: 'The labels for the buttons.' + }, + button_html: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button HTML', + default: '', + array: true, + description: 'The html of the button. Can create own style.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the buttons.' + }, + width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Width', + default: '', + description: 'The width of the video in pixels.' + }, + height: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Height', + default: '', + description: 'The height of the video display in pixels.' + }, + autoplay: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Autoplay', + default: true, + description: 'If true, the video will begin playing as soon as it has loaded.' + }, + controls: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Controls', + default: false, + description: 'If true, the subject will be able to pause the video or move the playback to any point in the video.' + }, + start: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'Start', + default: null, + description: 'Time to start the clip.' + }, + stop: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'Stop', + default: null, + description: 'Time to stop the clip.' + }, + rate: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'Rate', + default: 1, + description: 'The playback rate of the video. 1 is normal, <1 is slower, >1 is faster.' + }, + trial_ends_after_video: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'End trial after video finishes', + default: false, + description: 'If true, the trial will end immediately after the video finishes playing.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show trial before it ends.' + }, + margin_vertical: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin vertical', + default: '0px', + description: 'The vertical margin of the button.' + }, + margin_horizontal: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Margin horizontal', + default: '8px', + description: 'The horizontal margin of the button.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, the trial will end when subject makes a response.' + } + } + } + + plugin.trial = function(display_element, trial) { + + // setup stimulus + var video_html = '
' + video_html += '"; + video_html += "
"; + + //display buttons + var buttons = []; + if (Array.isArray(trial.button_html)) { + if (trial.button_html.length == trial.choices.length) { + buttons = trial.button_html; + } else { + console.error('Error in video-button-response plugin. The length of the button_html array does not equal the length of the choices array'); + } + } else { + for (var i = 0; i < trial.choices.length; i++) { + buttons.push(trial.button_html); + } + } + video_html += '
'; + for (var i = 0; i < trial.choices.length; i++) { + var str = buttons[i].replace(/%choice%/g, trial.choices[i]); + video_html += '
'+str+'
'; + } + video_html += '
'; + + // add prompt if there is one + if (trial.prompt !== null) { + video_html += trial.prompt; + } + + display_element.innerHTML = video_html; + + var start_time = performance.now(); + + if(video_preload_blob){ + display_element.querySelector('#jspsych-video-button-response-stimulus').src = video_preload_blob; + } + + display_element.querySelector('#jspsych-video-button-response-stimulus').onended = function(){ + if(trial.trial_ends_after_video){ + end_trial(); + } + } + + if(trial.start !== null){ + display_element.querySelector('#jspsych-video-button-response-stimulus').currentTime = trial.start; + } + + if(trial.stop !== null){ + display_element.querySelector('#jspsych-video-button-response-stimulus').addEventListener('timeupdate', function(e){ + var currenttime = display_element.querySelector('#jspsych-video-button-response-stimulus').currentTime; + if(currenttime >= trial.stop){ + display_element.querySelector('#jspsych-video-button-response-stimulus').pause(); + } + }) + } + + display_element.querySelector('#jspsych-video-button-response-stimulus').playbackRate = trial.rate; + + // add event listeners to buttons + for (var i = 0; i < trial.choices.length; i++) { + display_element.querySelector('#jspsych-video-button-response-button-' + i).addEventListener('click', function(e){ + var choice = e.currentTarget.getAttribute('data-choice'); // don't use dataset for jsdom compatibility + after_response(choice); + }); + } + + // store response + var response = { + rt: null, + button: null + }; + + // function to end trial when it is time + function end_trial() { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // gather the data to store for the trial + var trial_data = { + "rt": response.rt, + "stimulus": trial.stimulus, + "button_pressed": response.button + }; + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + }; + + // function to handle responses by the subject + function after_response(choice) { + + // measure rt + var end_time = performance.now(); + var rt = end_time - start_time; + response.button = choice; + response.rt = rt; + + // after a valid response, the stimulus will have the CSS class 'responded' + // which can be used to provide visual feedback that a response was recorded + display_element.querySelector('#jspsych-video-button-response-stimulus').className += ' responded'; + + // disable all the buttons after a response + var btns = document.querySelectorAll('.jspsych-video-button-response-button button'); + for(var i=0; i1 is faster.' + }, + trial_ends_after_video: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'End trial after video finishes', + default: false, + description: 'If true, the trial will end immediately after the video finishes playing.' + }, trial_duration: { type: jsPsych.plugins.parameterType.INT, pretty_name: 'Trial duration', @@ -79,7 +103,8 @@ jsPsych.plugins["video-keyboard-response"] = (function() { plugin.trial = function(display_element, trial) { // setup stimulus - var video_html = '"; + video_html += ""; // add prompt if there is one if (trial.prompt !== null) { @@ -118,6 +146,27 @@ jsPsych.plugins["video-keyboard-response"] = (function() { display_element.querySelector('#jspsych-video-keyboard-response-stimulus').src = video_preload_blob; } + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').onended = function(){ + if(trial.trial_ends_after_video){ + end_trial(); + } + } + + if(trial.start !== null){ + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').currentTime = trial.start; + } + + if(trial.stop !== null){ + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').addEventListener('timeupdate', function(e){ + var currenttime = display_element.querySelector('#jspsych-video-keyboard-response-stimulus').currentTime; + if(currenttime >= trial.stop){ + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').pause(); + } + }) + } + + display_element.querySelector('#jspsych-video-keyboard-response-stimulus').playbackRate = trial.rate; + // store response var response = { rt: null, @@ -134,9 +183,6 @@ jsPsych.plugins["video-keyboard-response"] = (function() { jsPsych.pluginAPI.cancelAllKeyboardResponses(); // gather the data to store for the trial - if(response.rt !== null){ - response.rt = Math.round(response.rt * 1000); - } var trial_data = { "rt": response.rt, "stimulus": trial.stimulus, diff --git a/plugins/jspsych-video-slider-response.js b/plugins/jspsych-video-slider-response.js new file mode 100644 index 00000000..067251d5 --- /dev/null +++ b/plugins/jspsych-video-slider-response.js @@ -0,0 +1,291 @@ +/** + * jspsych-video-slider-response + * Josh de Leeuw + * + * plugin for playing a video file and getting a slider response + * + * documentation: docs.jspsych.org + * + **/ + +jsPsych.plugins["video-slider-response"] = (function() { + + var plugin = {}; + + jsPsych.pluginAPI.registerPreload('video-slider-response', 'stimulus', 'video'); + + plugin.info = { + name: 'video-slider-response', + description: '', + parameters: { + sources: { + type: jsPsych.plugins.parameterType.VIDEO, + pretty_name: 'Video', + default: undefined, + description: 'The video file to play.' + }, + prompt: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Prompt', + default: null, + description: 'Any content here will be displayed below the stimulus.' + }, + width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Width', + default: '', + description: 'The width of the video in pixels.' + }, + height: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Height', + default: '', + description: 'The height of the video display in pixels.' + }, + autoplay: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Autoplay', + default: true, + description: 'If true, the video will begin playing as soon as it has loaded.' + }, + controls: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Controls', + default: false, + description: 'If true, the subject will be able to pause the video or move the playback to any point in the video.' + }, + start: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'Start', + default: null, + description: 'Time to start the clip.' + }, + stop: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'Stop', + default: null, + description: 'Time to stop the clip.' + }, + rate: { + type: jsPsych.plugins.parameterType.FLOAT, + pretty_name: 'Rate', + default: 1, + description: 'The playback rate of the video. 1 is normal, <1 is slower, >1 is faster.' + }, + min: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Min slider', + default: 0, + description: 'Sets the minimum value of the slider.' + }, + max: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Max slider', + default: 100, + description: 'Sets the maximum value of the slider', + }, + slider_start: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Slider starting value', + default: 50, + description: 'Sets the starting value of the slider', + }, + step: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Step', + default: 1, + description: 'Sets the step of the slider' + }, + labels: { + type: jsPsych.plugins.parameterType.HTML_STRING, + pretty_name:'Labels', + default: [], + array: true, + description: 'Labels of the slider.', + }, + slider_width: { + type: jsPsych.plugins.parameterType.INT, + pretty_name:'Slider width', + default: null, + description: 'Width of the slider in pixels.' + }, + button_label: { + type: jsPsych.plugins.parameterType.STRING, + pretty_name: 'Button label', + default: 'Continue', + array: false, + description: 'Label of the button to advance.' + }, + require_movement: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Require movement', + default: false, + description: 'If true, the participant will have to move the slider before continuing.' + }, + trial_ends_after_video: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'End trial after video finishes', + default: false, + description: 'If true, the trial will end immediately after the video finishes playing.' + }, + trial_duration: { + type: jsPsych.plugins.parameterType.INT, + pretty_name: 'Trial duration', + default: null, + description: 'How long to show trial before it ends.' + }, + response_ends_trial: { + type: jsPsych.plugins.parameterType.BOOL, + pretty_name: 'Response ends trial', + default: true, + description: 'If true, the trial will end when subject makes a response.' + } + } + } + + plugin.trial = function(display_element, trial) { + + // setup stimulus + var video_html = '"; + + var html = '
'; + html += '
' + video_html + '
'; + html += '
'; + html += ''; + html += '
' + for(var j=0; j < trial.labels.length; j++){ + var width = 100/(trial.labels.length-1); + var left_offset = (j * (100 /(trial.labels.length - 1))) - (width/2); + html += '
'; + html += ''+trial.labels[j]+''; + html += '
' + } + html += '
'; + html += '
'; + html += '
'; + + // add prompt if there is one + if (trial.prompt !== null) { + html += '
'+trial.prompt+'
'; + } + + // add submit button + html += ''; + + display_element.innerHTML = html; + + if(video_preload_blob){ + display_element.querySelector('#jspsych-video-slider-response-stimulus').src = video_preload_blob; + } + + display_element.querySelector('#jspsych-video-slider-response-stimulus').onended = function(){ + if(trial.trial_ends_after_video){ + end_trial(); + } + } + + if(trial.start !== null){ + display_element.querySelector('#jspsych-video-slider-response-stimulus').currentTime = trial.start; + } + + if(trial.stop !== null){ + display_element.querySelector('#jspsych-video-slider-response-stimulus').addEventListener('timeupdate', function(e){ + var currenttime = display_element.querySelector('#jspsych-video-slider-response-stimulus').currentTime; + if(currenttime >= trial.stop){ + display_element.querySelector('#jspsych-video-slider-response-stimulus').pause(); + } + }) + } + + display_element.querySelector('#jspsych-video-slider-response-stimulus').playbackRate = trial.rate; + + if(trial.require_movement){ + display_element.querySelector('#jspsych-video-slider-response-response').addEventListener('change', function(){ + display_element.querySelector('#jspsych-video-slider-response-next').disabled = false; + }) + } + + var startTime = performance.now(); + + // store response + var response = { + rt: null, + response: null + }; + + display_element.querySelector('#jspsych-video-slider-response-next').addEventListener('click', function() { + // measure response time + var endTime = performance.now(); + response.rt = endTime - startTime; + response.response = display_element.querySelector('#jspsych-video-slider-response-response').value; + + if(trial.response_ends_trial){ + end_trial(); + } else { + display_element.querySelector('#jspsych-video-slider-response-next').disabled = true; + } + + }); + + // function to end trial when it is time + function end_trial() { + + // kill any remaining setTimeout handlers + jsPsych.pluginAPI.clearAllTimeouts(); + + // gather the data to store for the trial + var trial_data = { + "rt": response.rt, + "stimulus": trial.stimulus, + "response": response.response + }; + + // clear the display + display_element.innerHTML = ''; + + // move on to the next trial + jsPsych.finishTrial(trial_data); + }; + + // end trial if time limit is set + if (trial.trial_duration !== null) { + jsPsych.pluginAPI.setTimeout(function() { + end_trial(); + }, trial.trial_duration); + } + }; + + return plugin; +})(); diff --git a/plugins/jspsych-video.js b/plugins/jspsych-video.js deleted file mode 100644 index 39ace459..00000000 --- a/plugins/jspsych-video.js +++ /dev/null @@ -1,164 +0,0 @@ -/* jspsych-video.js - * Josh de Leeuw - * - * This plugin displays a video. The trial ends when the video finishes. - * - * documentation: docs.jspsych.org - * - */ - -jsPsych.plugins.video = (function() { - - var plugin = {}; - - jsPsych.pluginAPI.registerPreload('video', 'sources', 'video'); - - plugin.info = { - name: 'video', - description: '', - parameters: { - sources: { - type: jsPsych.plugins.parameterType.VIDEO, - pretty_name: 'Sources', - array: true, - default: undefined, - description: 'The video file to play.' - }, - width: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Width', - default: '', - description: 'The width of the video in pixels.' - }, - height: { - type: jsPsych.plugins.parameterType.INT, - pretty_name: 'Height', - default: '', - description: 'The height of the video display in pixels.' - }, - autoplay: { - type: jsPsych.plugins.parameterType.BOOL, - pretty_name: 'Autoplay', - default: true, - description: 'If true, the video will begin playing as soon as it has loaded.' - }, - controls: { - type: jsPsych.plugins.parameterType.BOOL, - pretty_name: 'Controls', - default: false, - description: 'If true, the subject will be able to pause the video or move the playback to any point in the video.' - }, - prompt: { - type: jsPsych.plugins.parameterType.STRING, - pretty_name: 'Prompt', - default: null, - description: 'Any content here will be displayed below the video content.' - }, - start: { - type: jsPsych.plugins.parameterType.FLOAT, - pretty_name: 'Start', - default: null, - description: 'Time to start the clip.' - }, - stop: { - type: jsPsych.plugins.parameterType.FLOAT, - pretty_name: 'Stop', - default: null, - description: 'Time to stop the clip.' - }, - rate: { - type: jsPsych.plugins.parameterType.FLOAT, - pretty_name: 'Rate', - default: null, - description: 'The playback rate of the video. 1 is normal, <1 is slower, >1 is faster.' - } - } - } - - - plugin.trial = function(display_element, trial) { - - // display stimulus - - - var video_html = '"; - - //show prompt if there is one - if (trial.prompt !== null) { - video_html += trial.prompt; - } - - display_element.innerHTML = video_html; - if(video_preload_blob){ - display_element.querySelector('#jspsych-video-player').src = video_preload_blob; - } - - display_element.querySelector('#jspsych-video-player').onended = function(){ - end_trial(); - } - - // event handler to set timeout to end trial if video is stopped - display_element.querySelector('#jspsych-video-player').onplay = function(){ - if(trial.stop !== null){ - if(trial.start == null){ - trial.start = 0; - } - jsPsych.pluginAPI.setTimeout(end_trial, (trial.stop-trial.start)*1000); - } - } - - if(trial.start !== null){ - display_element.querySelector('#jspsych-video-player').currentTime = trial.start; - } - - if(trial.rate !== null){ - display_element.querySelector('#jspsych-video-player').playbackRate = trial.rate; - } - - // function to end trial when it is time - var end_trial = function() { - - // gather the data to store for the trial - var trial_data = { - stimulus: JSON.stringify(trial.sources) - }; - - // clear the display - display_element.innerHTML = ''; - - // move on to the next trial - jsPsych.finishTrial(trial_data); - }; - - }; - - return plugin; -})();