Video Consent Recording

Hello! I want to have participants record their video consent, as I am collecting adolescent data and want to verify that the adolescent is in fact an adolescent. However, the videos do not seem to be recording. When I download the data from Pavlovia, the download folder is empty. I can see the camera is on, but I am not sure why the videos are not being saved to Pavlovia. The template code is below! I’d also ultimately like to redirect participants to a Qualtrics survey. Thanks!

/***************

  • Webcam Test *
    ***************/

import { core, data, sound, util, visual, hardware } from ‘./lib/psychojs-2022.2.5.js’;
const { PsychoJS } = core;
const { TrialHandler, MultiStairHandler } = data;
const { Scheduler } = util;
//some handy aliases as in the psychopy scripts;
const { abs, sin, cos, PI: pi, sqrt } = Math;
const { round } = util;

// store info about the experiment session:
let expName = ‘webcam’; // from the Builder filename that created this script
let expInfo = {
‘participant’: ${util.pad(Number.parseFloat(util.randint(0, 999999)).toFixed(0), 6)},
‘session’: ‘001’,
};

// Start code blocks for ‘Before Experiment’
// init psychoJS:
const psychoJS = new PsychoJS({
debug: true
});

// open window:
psychoJS.openWindow({
fullscr: true,
color: new util.Color([0,0,0]),
units: ‘height’,
waitBlanking: true
});
// schedule the experiment:
psychoJS.schedule(psychoJS.gui.DlgFromDict({
dictionary: expInfo,
title: expName
}));

const flowScheduler = new Scheduler(psychoJS);
const dialogCancelScheduler = new Scheduler(psychoJS);
psychoJS.scheduleCondition(function() { return (psychoJS.gui.dialogComponent.button === ‘OK’); }, flowScheduler, dialogCancelScheduler);

// flowScheduler gets run if the participants presses OK
flowScheduler.add(updateInfo); // add timeStamp
flowScheduler.add(experimentInit);
flowScheduler.add(instructionRoutineBegin());
flowScheduler.add(instructionRoutineEachFrame());
flowScheduler.add(instructionRoutineEnd());
flowScheduler.add(trialRoutineBegin());
flowScheduler.add(trialRoutineEachFrame());
flowScheduler.add(trialRoutineEnd());
flowScheduler.add(thnksRoutineBegin());
flowScheduler.add(thnksRoutineEachFrame());
flowScheduler.add(thnksRoutineEnd());
flowScheduler.add(quitPsychoJS, ‘’, true);

// quit if user presses Cancel in dialog box:
dialogCancelScheduler.add(quitPsychoJS, ‘’, false);

psychoJS.start({
expName: expName,
expInfo: expInfo,
resources: [
]
});

psychoJS.experimentLogger.setLevel(core.Logger.ServerLevel.EXP);

var currentLoop;
var frameDur;
async function updateInfo() {
currentLoop = psychoJS.experiment; // right now there are no loops
expInfo[‘date’] = util.MonotonicClock.getDateStr(); // add a simple timestamp
expInfo[‘expName’] = expName;
expInfo[‘psychopyVersion’] = ‘2022.2.5’;
expInfo[‘OS’] = window.navigator.platform;

// store frame rate of monitor if we can measure it successfully
expInfo[‘frameRate’] = psychoJS.window.getActualFrameRate();
if (typeof expInfo[‘frameRate’] !== ‘undefined’)
frameDur = 1.0 / Math.round(expInfo[‘frameRate’]);
else
frameDur = 1.0 / 60.0; // couldn’t get a reliable measure so guess

// add info from the URL:
util.addInfoFromUrl(expInfo);

psychoJS.experiment.dataFileName = ((“.” + “/”) + data/${expInfo["participant"]}_${expName}_${expInfo["date"]});

return Scheduler.Event.NEXT;
}

var instructionClock;
var text;
var key_resp;
var trialClock;
var cam;
var text_2;
var key_resp_2;
var thnksClock;
var text_3;
var globalClock;
var routineTimer;
async function experimentInit() {
// Initialize components for Routine “instruction”
instructionClock = new util.Clock();
text = new visual.TextStim({
win: psychoJS.window,
name: ‘text’,
text: ‘We would like you to record your video consent/assent.\n\n’ +
'If you would like to record your video consent/assent, ’ +
‘please allow camera access when prompted!\n\n’ +
‘To continue, press the space bar.’,
font: ‘Arial’,
units: undefined,
pos: [0, 0], height: 0.06, wrapWidth: 1.4, ori: 0.0,
languageStyle: ‘LTR’,
color: new util.Color(‘black’), opacity: 1.0,
depth: 0.0
});

key_resp = new core.Keyboard({psychoJS: psychoJS, clock: new util.Clock(), waitForStart: true});

// Initialize components for Routine “trial”
trialClock = new util.Clock();
cam = new hardware.Camera({
name:‘cam’,
win: psychoJS.window,});
// Get permission from participant to access their camera
await cam.authorize()
// Switch on cam
await cam.open()

text_2 = new visual.TextStim({
win: psychoJS.window,
name: ‘text_2’,
text: ‘Parents/caregivers, please say: “I consent to my child participating.” \n\n’ +
‘Adolescents, please say: “I agree to participate.”\n\n’ +
‘Press the space bar when you are done.’,
font: ‘Arial’,
units: undefined,
pos: [0, 0], height: 0.06, wrapWidth:1.4, undefined, ori: 0.0,
languageStyle: ‘LTR’,
color: new util.Color(‘black’), opacity: 1,
depth: -1.0
});

key_resp_2 = new core.Keyboard({psychoJS: psychoJS, clock: new util.Clock(), waitForStart: true});

// Initialize components for Routine “thnks”
thnksClock = new util.Clock();
text_3 = new visual.TextStim({
win: psychoJS.window,
name: ‘text_3’,
text: ‘Thank you!’,
font: ‘Arial’,
units: undefined,
pos: [0, 0], height: 0.06, wrapWidth:1.4, undefined, ori: 0.0,
languageStyle: ‘LTR’,
color: new util.Color(‘black’), opacity: 1,
depth: 0.0
});

// Create some handy timers
globalClock = new util.Clock(); // to track the time since experiment started
routineTimer = new util.CountdownTimer(); // to track time remaining of each (non-slip) routine

return Scheduler.Event.NEXT;
}

var t;
var frameN;
var continueRoutine;
var _key_resp_allKeys;
var instructionComponents;
function instructionRoutineBegin(snapshot) {
return async function () {
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date

//--- Prepare to start Routine 'instruction' ---
t = 0;
instructionClock.reset(); // clock
frameN = -1;
continueRoutine = true; // until we're told otherwise
// update component parameters for each repeat
key_resp.keys = undefined;
key_resp.rt = undefined;
_key_resp_allKeys = [];
// keep track of which components have finished
instructionComponents = [];
instructionComponents.push(text);
instructionComponents.push(key_resp);

for (const thisComponent of instructionComponents)
  if ('status' in thisComponent)
    thisComponent.status = PsychoJS.Status.NOT_STARTED;
return Scheduler.Event.NEXT;

}
}

function instructionRoutineEachFrame() {
return async function () {
//— Loop for each frame of Routine ‘instruction’ —
// get current time
t = instructionClock.getTime();
frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
// update/draw components on each frame

// *text* updates
if (t >= 0.0 && text.status === PsychoJS.Status.NOT_STARTED) {
  // keep track of start time/frame for later
  text.tStart = t;  // (not accounting for frame time here)
  text.frameNStart = frameN;  // exact frame index
  
  text.setAutoDraw(true);
}


// *key_resp* updates
if (t >= 0.0 && key_resp.status === PsychoJS.Status.NOT_STARTED) {
  // keep track of start time/frame for later
  key_resp.tStart = t;  // (not accounting for frame time here)
  key_resp.frameNStart = frameN;  // exact frame index
  
  // keyboard checking is just starting
  psychoJS.window.callOnFlip(function() { key_resp.clock.reset(); });  // t=0 on next screen flip
  psychoJS.window.callOnFlip(function() { key_resp.start(); }); // start on screen flip
  psychoJS.window.callOnFlip(function() { key_resp.clearEvents(); });
}

if (key_resp.status === PsychoJS.Status.STARTED) {
  let theseKeys = key_resp.getKeys({keyList: ['space'], waitRelease: false});
  _key_resp_allKeys = _key_resp_allKeys.concat(theseKeys);
  if (_key_resp_allKeys.length > 0) {
    key_resp.keys = _key_resp_allKeys[_key_resp_allKeys.length - 1].name;  // just the last key pressed
    key_resp.rt = _key_resp_allKeys[_key_resp_allKeys.length - 1].rt;
    // a response ends the routine
    continueRoutine = false;
  }
}

// check for quit (typically the Esc key)
if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
  return quitPsychoJS('The [Escape] key was pressed. Goodbye!', false);
}

// check if the Routine should terminate
if (!continueRoutine) {  // a component has requested a forced-end of Routine
  return Scheduler.Event.NEXT;
}

continueRoutine = false;  // reverts to True if at least one component still running
for (const thisComponent of instructionComponents)
  if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
    continueRoutine = true;
    break;
  }

// refresh the screen if continuing
if (continueRoutine) {
  return Scheduler.Event.FLIP_REPEAT;
} else {
  return Scheduler.Event.NEXT;
}

};
}

function instructionRoutineEnd(snapshot) {
return async function () {
//— Ending Routine ‘instruction’ —
for (const thisComponent of instructionComponents) {
if (typeof thisComponent.setAutoDraw === ‘function’) {
thisComponent.setAutoDraw(false);
}
}
// update the trial handler
if (currentLoop instanceof MultiStairHandler) {
currentLoop.addResponse(key_resp.corr, level);
}
psychoJS.experiment.addData(‘key_resp.keys’, key_resp.keys);
if (typeof key_resp.keys !== ‘undefined’) { // we had a response
psychoJS.experiment.addData(‘key_resp.rt’, key_resp.rt);
routineTimer.reset();
}

key_resp.stop();
// the Routine "instruction" was not non-slip safe, so reset the non-slip timer
routineTimer.reset();

// Routines running outside a loop should always advance the datafile row
if (currentLoop === psychoJS.experiment) {
  psychoJS.experiment.nextEntry(snapshot);
}
return Scheduler.Event.NEXT;

}
}

var _key_resp_2_allKeys;
var trialComponents;
function trialRoutineBegin(snapshot) {
return async function () {
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date

//--- Prepare to start Routine 'trial' ---
t = 0;
trialClock.reset(); // clock
frameN = -1;
continueRoutine = true; // until we're told otherwise
// update component parameters for each repeat
key_resp_2.keys = undefined;
key_resp_2.rt = undefined;
_key_resp_2_allKeys = [];
// keep track of which components have finished
trialComponents = [];
trialComponents.push(cam);
trialComponents.push(text_2);
trialComponents.push(key_resp_2);

for (const thisComponent of trialComponents)
  if ('status' in thisComponent)
    thisComponent.status = PsychoJS.Status.NOT_STARTED;
return Scheduler.Event.NEXT;

}
}

var frameRemains;
function trialRoutineEachFrame() {
return async function () {
//— Loop for each frame of Routine ‘trial’ —
// get current time
t = trialClock.getTime();
frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
// update/draw components on each frame
if (t >= 0 && cam.status === PsychoJS.Status.NOT_STARTED) {
// keep track of start time/frame for later
cam.tStart = t; // (not accounting for frame time here)
cam.frameNStart = frameN; // exact frame index

  await cam.record()
};
frameRemains = 0 +  - psychoJS.window.monitorFramePeriod * 0.75;  // most of one frame period left
if (cam.status === PsychoJS.Status.STARTED && t >= frameRemains) {
  await cam.stop()
};

// *text_2* updates
if (t >= 0.0 && text_2.status === PsychoJS.Status.NOT_STARTED) {
  // keep track of start time/frame for later
  text_2.tStart = t;  // (not accounting for frame time here)
  text_2.frameNStart = frameN;  // exact frame index
  
  text_2.setAutoDraw(true);
}


// *key_resp_2* updates
if (t >= 0.0 && key_resp_2.status === PsychoJS.Status.NOT_STARTED) {
  // keep track of start time/frame for later
  key_resp_2.tStart = t;  // (not accounting for frame time here)
  key_resp_2.frameNStart = frameN;  // exact frame index
  
  // keyboard checking is just starting
  psychoJS.window.callOnFlip(function() { key_resp_2.clock.reset(); });  // t=0 on next screen flip
  psychoJS.window.callOnFlip(function() { key_resp_2.start(); }); // start on screen flip
  psychoJS.window.callOnFlip(function() { key_resp_2.clearEvents(); });
}

if (key_resp_2.status === PsychoJS.Status.STARTED) {
  let theseKeys = key_resp_2.getKeys({keyList: ['space'], waitRelease: false});
  _key_resp_2_allKeys = _key_resp_2_allKeys.concat(theseKeys);
  if (_key_resp_2_allKeys.length > 0) {
    key_resp_2.keys = _key_resp_2_allKeys[_key_resp_2_allKeys.length - 1].name;  // just the last key pressed
    key_resp_2.rt = _key_resp_2_allKeys[_key_resp_2_allKeys.length - 1].rt;
    // a response ends the routine
    continueRoutine = false;
  }
}

// check for quit (typically the Esc key)
if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
  return quitPsychoJS('The [Escape] key was pressed. Goodbye!', false);
}

// check if the Routine should terminate
if (!continueRoutine) {  // a component has requested a forced-end of Routine
  return Scheduler.Event.NEXT;
}

continueRoutine = false;  // reverts to True if at least one component still running
for (const thisComponent of trialComponents)
  if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
    continueRoutine = true;
    break;
  }

// refresh the screen if continuing
if (continueRoutine) {
  return Scheduler.Event.FLIP_REPEAT;
} else {
  return Scheduler.Event.NEXT;
}

};
}

function trialRoutineEnd(snapshot) {
return async function () {
//— Ending Routine ‘trial’ —
for (const thisComponent of trialComponents) {
if (typeof thisComponent.setAutoDraw === ‘function’) {
thisComponent.setAutoDraw(false);
}
}
// Ensure that cam is stopped
if (cam.status === PsychoJS.Status.STARTED) {
await cam.stop()
}
// Save cam recording
let camFilename = recording_cam_${util.MonotonicClock.getDateStr()};
await cam.save({
tag: camFilename,
waitForCompletion: true,
showDialog: true,
dialogMsg: “Please wait a few moments while the video is uploading to the server…”
});
psychoJS.experiment.addData(‘cam.clip’, camFilename);
// update the trial handler
if (currentLoop instanceof MultiStairHandler) {
currentLoop.addResponse(key_resp_2.corr, level);
}
psychoJS.experiment.addData(‘key_resp_2.keys’, key_resp_2.keys);
if (typeof key_resp_2.keys !== ‘undefined’) { // we had a response
psychoJS.experiment.addData(‘key_resp_2.rt’, key_resp_2.rt);
routineTimer.reset();
}

key_resp_2.stop();
// the Routine "trial" was not non-slip safe, so reset the non-slip timer
routineTimer.reset();

// Routines running outside a loop should always advance the datafile row
if (currentLoop === psychoJS.experiment) {
  psychoJS.experiment.nextEntry(snapshot);
}
return Scheduler.Event.NEXT;

}
}

var thnksComponents;
function thnksRoutineBegin(snapshot) {
return async function () {
TrialHandler.fromSnapshot(snapshot); // ensure that .thisN vals are up to date

//--- Prepare to start Routine 'thnks' ---
t = 0;
thnksClock.reset(); // clock
frameN = -1;
continueRoutine = true; // until we're told otherwise
routineTimer.add(3.000000);
// update component parameters for each repeat
// keep track of which components have finished
thnksComponents = [];
thnksComponents.push(text_3);

for (const thisComponent of thnksComponents)
  if ('status' in thisComponent)
    thisComponent.status = PsychoJS.Status.NOT_STARTED;
return Scheduler.Event.NEXT;

}
}

function thnksRoutineEachFrame() {
return async function () {
//— Loop for each frame of Routine ‘thnks’ —
// get current time
t = thnksClock.getTime();
frameN = frameN + 1;// number of completed frames (so 0 is the first frame)
// update/draw components on each frame

// *text_3* updates
if (t >= 0.0 && text_3.status === PsychoJS.Status.NOT_STARTED) {
  // keep track of start time/frame for later
  text_3.tStart = t;  // (not accounting for frame time here)
  text_3.frameNStart = frameN;  // exact frame index
  
  text_3.setAutoDraw(true);
}

frameRemains = 0.0 + 1.0 - psychoJS.window.monitorFramePeriod * 0.75;  // most of one frame period left
if (text_3.status === PsychoJS.Status.STARTED && t >= frameRemains) {
  text_3.setAutoDraw(false);
}
// check for quit (typically the Esc key)
if (psychoJS.experiment.experimentEnded || psychoJS.eventManager.getKeys({keyList:['escape']}).length > 0) {
  return quitPsychoJS('The [Escape] key was pressed. Goodbye!', false);
}

// check if the Routine should terminate
if (!continueRoutine) {  // a component has requested a forced-end of Routine
  return Scheduler.Event.NEXT;
}

continueRoutine = false;  // reverts to True if at least one component still running
for (const thisComponent of thnksComponents)
  if ('status' in thisComponent && thisComponent.status !== PsychoJS.Status.FINISHED) {
    continueRoutine = true;
    break;
  }

// refresh the screen if continuing
if (continueRoutine && routineTimer.getTime() > 0) {
  return Scheduler.Event.FLIP_REPEAT;
} else {
  return Scheduler.Event.NEXT;
}

};
}

function thnksRoutineEnd(snapshot) {
return async function () {
//— Ending Routine ‘thnks’ —
for (const thisComponent of thnksComponents) {
if (typeof thisComponent.setAutoDraw === ‘function’) {
thisComponent.setAutoDraw(false);
}
}
// Routines running outside a loop should always advance the datafile row
if (currentLoop === psychoJS.experiment) {
psychoJS.experiment.nextEntry(snapshot);
}
return Scheduler.Event.NEXT;
}
}

function importConditions(currentLoop) {
return async function () {
psychoJS.importAttributes(currentLoop.getCurrentTrial());
return Scheduler.Event.NEXT;
};
}

async function quitPsychoJS(message, isCompleted) {
// Check for and save orphaned data
if (psychoJS.experiment.isEntryEmpty()) {
psychoJS.experiment.nextEntry();
}
psychoJS.window.close();
psychoJS.quit({message: message, isCompleted: isCompleted});

return Scheduler.Event.QUIT;
}

Hi Shauna,
It looks like you’re using custom PsychoJS code rather than a PsychoPy Builder experiment—is that correct? We strongly recommend using Builder whenever possible, as it simplifies the process and ensures better compatibility. If you did use Builder, it is always useful to share your .psyexp file on your post and also say what version of psychopy you were using.

To set up webcam recording, you just need a Builder-based experiment with a webcam component (like the one I’ve attached).
To download the webcam recordings:

  1. Go to your Pavlovia Dashboard
  2. Select the relevant experiment
  3. Click Download Data
  4. The media files should be included in .webm format

Let me know if you run into any issues!

webcam_example.psyexp (19.0 KB)