1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 10:40:54 +00:00

Camera: unified API with PsychoPy, Shelf: throttling

This commit is contained in:
Alain Pitiot 2022-05-27 13:04:48 +02:00
parent 3b0308a77f
commit 173d08947d
19 changed files with 1407 additions and 761 deletions

31
package-lock.json generated
View File

@ -1,15 +1,16 @@
{
"name": "psychojs",
"version": "2022.1.1",
"version": "2022.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "psychojs",
"version": "2022.1.1",
"version": "2022.2.0",
"license": "MIT",
"dependencies": {
"@pixi/filter-adjustment": "^4.1.3",
"a11y-dialog": "^7.5.0",
"esbuild-plugin-glsl": "^1.0.5",
"howler": "^2.2.1",
"log4javascript": "github:Ritzlgrmft/log4javascript",
@ -642,6 +643,14 @@
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz",
"integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ=="
},
"node_modules/a11y-dialog": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz",
"integrity": "sha512-UF7cy4lfZQtvjRV5N4xdWFba+Pb1qW6FPp0p58dLjMTJ4PwIGGekTbmqUt3etBBRo9HbTqhlNsXQhzIuXeJpng==",
"dependencies": {
"focusable-selectors": "^0.3.1"
}
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -1466,6 +1475,11 @@
"integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==",
"dev": true
},
"node_modules/focusable-selectors": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/focusable-selectors/-/focusable-selectors-0.3.1.tgz",
"integrity": "sha512-5JLtr0e1YJIfmnVlpLiG+av07dd0Xkf/KfswsXcei5KmLfdwOysTQsjF058ynXniujb1fvev7nql1x+CkC5ikw=="
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
@ -3011,6 +3025,14 @@
"resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.1.tgz",
"integrity": "sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ=="
},
"a11y-dialog": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.0.tgz",
"integrity": "sha512-UF7cy4lfZQtvjRV5N4xdWFba+Pb1qW6FPp0p58dLjMTJ4PwIGGekTbmqUt3etBBRo9HbTqhlNsXQhzIuXeJpng==",
"requires": {
"focusable-selectors": "^0.3.1"
}
},
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@ -3618,6 +3640,11 @@
"integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==",
"dev": true
},
"focusable-selectors": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/focusable-selectors/-/focusable-selectors-0.3.1.tgz",
"integrity": "sha512-5JLtr0e1YJIfmnVlpLiG+av07dd0Xkf/KfswsXcei5KmLfdwOysTQsjF058ynXniujb1fvev7nql1x+CkC5ikw=="
},
"frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",

View File

@ -28,6 +28,7 @@
},
"dependencies": {
"@pixi/filter-adjustment": "^4.1.3",
"a11y-dialog": "^7.5.0",
"esbuild-plugin-glsl": "^1.0.5",
"howler": "^2.2.1",
"log4javascript": "github:Ritzlgrmft/log4javascript",

View File

@ -179,7 +179,8 @@ export class PsychoJS
this.logger.info("[PsychoJS] @version 2022.2.0");
// hide the initialisation message:
jQuery("#root").addClass("is-ready");
const root = document.getElementById("root");
root.classList.add("is-ready");
}
/**
@ -444,10 +445,9 @@ export class PsychoJS
}
/**
* Make the attributes of the given object those of PsychoJS and those of
* the top level variable (e.g. window) as well.
* Make the attributes of the given object those of window, such that they become global.
*
* @param {Object.<string, *>} obj the object whose attributes we will mirror
* @param {Object.<string, *>} obj the object whose attributes are to become global
* @public
*/
importAttributes(obj)
@ -461,7 +461,6 @@ export class PsychoJS
for (const attribute in obj)
{
// this[attribute] = obj[attribute];
window[attribute] = obj[attribute];
}
}
@ -678,8 +677,21 @@ export class PsychoJS
this._IP = {};
try
{
const geoResponse = await jQuery.get("http://www.geoplugin.net/json.gp");
const geoData = JSON.parse(geoResponse);
const url = "http://www.geoplugin.net/json.gp";
const response = await fetch(url, {
method: "GET",
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
redirect: "follow",
referrerPolicy: "no-referrer"
});
if (response.status !== 200)
{
throw `unable to obtain the IP of the participant: ${response.statusText}`;
}
const geoData = await response.json();
this._IP = {
IP: geoData.geoplugin_request,
country: geoData.geoplugin_countryName,

View File

@ -84,23 +84,38 @@ export class ServerManager extends PsychObject
this._psychoJS.logger.debug("reading the configuration file: " + configURL);
const self = this;
return new Promise((resolve, reject) =>
return new Promise(async (resolve, reject) =>
{
jQuery.get(configURL, "json")
.done((config, textStatus) =>
{
// resolve({ ...response, config });
resolve(Object.assign(response, { config }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
self.setStatus(ServerManager.Status.ERROR);
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error("error:", errorMsg);
reject(Object.assign(response, { error: errorMsg }));
try
{
const getResponse = await fetch(configURL, {
method: "GET",
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
redirect: "follow",
referrerPolicy: "no-referrer"
});
if (getResponse.status === 404)
{
throw "the configuration file could not be found";
}
else if (getResponse.status !== 200)
{
throw `unable to read the configuration file: status= ${getResponse.status}`;
}
// the configuration file should be valid json:
const config = await getResponse.json();
resolve(Object.assign(response, { config }));
}
catch (error)
{
self.setStatus(ServerManager.Status.ERROR);
console.error("error:", error);
reject(Object.assign(response, { error }));
}
});
}
@ -125,74 +140,76 @@ export class ServerManager extends PsychObject
origin: "ServerManager.openSession",
context: "when opening a session for experiment: " + this._psychoJS.config.experiment.fullpath,
};
this._psychoJS.logger.debug("opening a session for experiment: " + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
// prepare POST query:
// prepare a POST query:
let data = {};
if (this._psychoJS._serverMsg.has("__pilotToken"))
{
data.pilotToken = this._psychoJS._serverMsg.get("__pilotToken");
}
// query pavlovia server:
// query the server:
const self = this;
return new Promise((resolve, reject) =>
return new Promise(async (resolve, reject) =>
{
const url = this._psychoJS.config.pavlovia.URL
+ "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ "/sessions";
jQuery.post(url, data, null, "json")
.done((data, textStatus) =>
try
{
const postResponse = await this._queryServerAPI(
"POST",
`experiments/${this._psychoJS.config.gitlab.projectId}/sessions`,
data
);
const openSessionResponse = await postResponse.json();
if (postResponse.status !== 200)
{
if (!("token" in data))
{
self.setStatus(ServerManager.Status.ERROR);
reject(Object.assign(response, { error: "unexpected answer from server: no token" }));
// reject({...response, error: 'unexpected answer from server: no token'});
}
if (!("experiment" in data))
{
self.setStatus(ServerManager.Status.ERROR);
// reject({...response, error: 'unexpected answer from server: no experiment'});
reject(Object.assign(response, { error: "unexpected answer from server: no experiment" }));
}
self._psychoJS.config.session = {
token: data.token,
status: "OPEN",
};
self._psychoJS.config.experiment.status = data.experiment.status2;
self._psychoJS.config.experiment.saveFormat = Symbol.for(data.experiment.saveFormat);
self._psychoJS.config.experiment.saveIncompleteResults = data.experiment.saveIncompleteResults;
self._psychoJS.config.experiment.license = data.experiment.license;
self._psychoJS.config.experiment.runMode = data.experiment.runMode;
// secret keys for various services, e.g. Google Speech API
if ("keys" in data.experiment)
{
self._psychoJS.config.experiment.keys = data.experiment.keys;
}
else
{
self._psychoJS.config.experiment.keys = [];
}
self.setStatus(ServerManager.Status.READY);
// resolve({ ...response, token: data.token, status: data.status });
resolve(Object.assign(response, { token: data.token, status: data.status }));
})
.fail((jqXHR, textStatus, errorThrown) =>
throw ('error' in openSessionResponse) ? openSessionResponse.error : openSessionResponse;
}
if (!("token" in openSessionResponse))
{
self.setStatus(ServerManager.Status.ERROR);
throw "unexpected answer from the server: no token";
}
if (!("experiment" in openSessionResponse))
{
self.setStatus(ServerManager.Status.ERROR);
throw "unexpected answer from server: no experiment";
}
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error("error:", errorMsg);
self._psychoJS.config.session = {
token: openSessionResponse.token,
status: "OPEN",
};
const experiment = openSessionResponse.experiment;
self._psychoJS.config.experiment.status = experiment.status2;
self._psychoJS.config.experiment.saveFormat = Symbol.for(experiment.saveFormat);
self._psychoJS.config.experiment.saveIncompleteResults = experiment.saveIncompleteResults;
self._psychoJS.config.experiment.license = experiment.license;
self._psychoJS.config.experiment.runMode = experiment.runMode;
reject(Object.assign(response, { error: errorMsg }));
});
// secret keys for various services, e.g. Google Speech API
if ("keys" in experiment)
{
self._psychoJS.config.experiment.keys = experiment.keys;
}
else
{
self._psychoJS.config.experiment.keys = [];
}
self.setStatus(ServerManager.Status.READY);
resolve({...response, token: openSessionResponse.token, status: openSessionResponse.status });
}
catch (error)
{
console.error(error);
self.setStatus(ServerManager.Status.ERROR);
reject({...response, error});
}
});
}
@ -218,68 +235,53 @@ export class ServerManager extends PsychObject
origin: "ServerManager.closeSession",
context: "when closing the session for experiment: " + this._psychoJS.config.experiment.fullpath,
};
this._psychoJS.logger.debug("closing the session for experiment: " + this._psychoJS.config.experiment.name);
this.setStatus(ServerManager.Status.BUSY);
// prepare DELETE query:
const url = this._psychoJS.config.pavlovia.URL
+ "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ "/sessions/" + this._psychoJS.config.session.token;
// synchronous query the pavlovia server:
// synchronously query the pavlovia server:
if (sync)
{
/* This is now deprecated in most browsers.
const request = new XMLHttpRequest();
request.open("DELETE", url, false);
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
request.send(JSON.stringify(data));
*/
/* This does not work in Chrome because of a CORS bug
await fetch(url, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
body: JSON.stringify(data),
// keepalive makes it possible for the request to outlive the page (e.g. when the participant closes the tab)
keepalive: true
});
*/
const url = this._psychoJS.config.pavlovia.URL
+ "/api/v2/experiments/" + this._psychoJS.config.gitlab.projectId
+ "/sessions/" + this._psychoJS.config.session.token + "/delete";
const formData = new FormData();
formData.append("isCompleted", isCompleted);
navigator.sendBeacon(url + "/delete", formData);
navigator.sendBeacon(url, formData);
this._psychoJS.config.session.status = "CLOSED";
}
// asynchronously query the pavlovia server:
else
{
const self = this;
return new Promise((resolve, reject) =>
return new Promise(async (resolve, reject) =>
{
jQuery.ajax({
url,
type: "delete",
data: { isCompleted },
dataType: "json",
})
.done((data, textStatus) =>
try
{
const deleteResponse = await this._queryServerAPI(
"DELETE",
`experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}`,
{ isCompleted }
);
const closeSessionResponse = await deleteResponse.json();
if (deleteResponse.status !== 200)
{
self.setStatus(ServerManager.Status.READY);
self._psychoJS.config.session.status = "CLOSED";
throw ('error' in closeSessionResponse) ? closeSessionResponse.error : closeSessionResponse;
}
// resolve({ ...response, data });
resolve(Object.assign(response, { data }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
self.setStatus(ServerManager.Status.ERROR);
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error("error:", errorMsg);
reject(Object.assign(response, { error: errorMsg }));
});
self.setStatus(ServerManager.Status.READY);
self._psychoJS.config.session.status = "CLOSED";
resolve({ ...response, ...closeSessionResponse });
}
catch (error)
{
console.error(error);
self.setStatus(ServerManager.Status.ERROR);
reject({...response, error});
}
});
}
}
@ -729,49 +731,51 @@ export class ServerManager extends PsychObject
origin: "ServerManager.uploadData",
context: "when uploading participant's results for experiment: " + this._psychoJS.config.experiment.fullpath,
};
this._psychoJS.logger.debug("uploading data for experiment: " + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
const url = this._psychoJS.config.pavlovia.URL
+ "/api/v2/experiments/" + encodeURIComponent(this._psychoJS.config.experiment.fullpath)
+ "/sessions/" + this._psychoJS.config.session.token
+ "/results";
const path = `experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}/results`;
// synchronous query the pavlovia server:
// synchronously query the pavlovia server:
if (sync)
{
const formData = new FormData();
formData.append("key", key);
formData.append("value", value);
navigator.sendBeacon(url, formData);
navigator.sendBeacon(`${this._psychoJS.config.pavlovia.URL}/api/v2/${path}`, formData);
}
// asynchronously query the pavlovia server:
else
{
const self = this;
return new Promise((resolve, reject) =>
return new Promise(async (resolve, reject) =>
{
const data = {
key,
value,
};
try
{
const postResponse = await this._queryServerAPI(
"POST",
`experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${this._psychoJS.config.session.token}/results`,
{ key, value },
"FORM"
);
jQuery.post(url, data, null, "json")
.done((serverData, textStatus) =>
const uploadDataResponse = await postResponse.json();
if (postResponse.status !== 200)
{
self.setStatus(ServerManager.Status.READY);
resolve(Object.assign(response, { serverData }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
self.setStatus(ServerManager.Status.ERROR);
throw ('error' in uploadDataResponse) ? uploadDataResponse.error : uploadDataResponse;
}
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error("error:", errorMsg);
reject(Object.assign(response, { error: errorMsg }));
});
self.setStatus(ServerManager.Status.READY);
resolve({ ...response, ...uploadDataResponse });
}
catch (error)
{
console.error(error);
self.setStatus(ServerManager.Status.ERROR);
reject({...response, error});
}
});
}
}
@ -792,46 +796,50 @@ export class ServerManager extends PsychObject
origin: "ServerManager.uploadLog",
context: "when uploading participant's log for experiment: " + this._psychoJS.config.experiment.fullpath,
};
this._psychoJS.logger.debug("uploading server log for experiment: " + this._psychoJS.config.experiment.fullpath);
this.setStatus(ServerManager.Status.BUSY);
// prepare the POST query:
// prepare a POST query:
const info = this.psychoJS.experiment.extraInfo;
const participant = ((typeof info.participant === "string" && info.participant.length > 0) ? info.participant : "PARTICIPANT");
const experimentName = (typeof info.expName !== "undefined") ? info.expName : this.psychoJS.config.experiment.name;
const datetime = ((typeof info.date !== "undefined") ? info.date : MonotonicClock.getDateStr());
const filename = participant + "_" + experimentName + "_" + datetime + ".log";
const filenameWithoutPath = this.psychoJS.experiment.dataFileName.split(/[\\/]/).pop();
const filename = `${filenameWithoutPath}.log`;
const data = {
filename,
logs,
compressed,
compressed
};
// query the pavlovia server:
const self = this;
return new Promise((resolve, reject) =>
return new Promise(async (resolve, reject) =>
{
const url = self._psychoJS.config.pavlovia.URL
+ "/api/v2/experiments/" + encodeURIComponent(self._psychoJS.config.experiment.fullpath)
+ "/sessions/" + self._psychoJS.config.session.token
+ "/logs";
try
{
const postResponse = await this._queryServerAPI(
"POST",
`experiments/${this._psychoJS.config.gitlab.projectId}/sessions/${self._psychoJS.config.session.token}/logs`,
data,
"FORM"
);
jQuery.post(url, data, null, "json")
.done((serverData, textStatus) =>
const uploadLogsResponse = await postResponse.json();
if (postResponse.status !== 200)
{
self.setStatus(ServerManager.Status.READY);
resolve(Object.assign(response, { serverData }));
})
.fail((jqXHR, textStatus, errorThrown) =>
{
self.setStatus(ServerManager.Status.ERROR);
throw ('error' in uploadLogsResponse) ? uploadLogsResponse.error : uploadLogsResponse;
}
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error("error:", errorMsg);
self.setStatus(ServerManager.Status.READY);
resolve({...response, ...uploadLogsResponse });
}
catch (error)
{
console.error(error);
self.setStatus(ServerManager.Status.ERROR);
reject({...response, error});
}
reject(Object.assign(response, { error: errorMsg }));
});
});
}
@ -983,59 +991,49 @@ export class ServerManager extends PsychObject
origin: "ServerManager._listResourcesSession",
context: "when listing the resources for experiment: " + this._psychoJS.config.experiment.fullpath,
};
this._psychoJS.logger.debug(
"listing the resources for experiment: "
+ this._psychoJS.config.experiment.fullpath,
);
this._psychoJS.logger.debug(`listing the resources for experiment: ${this._psychoJS.config.experiment.fullpath}`);
this.setStatus(ServerManager.Status.BUSY);
// prepare GET data:
// prepare a GET query:
const data = {
"token": this._psychoJS.config.session.token,
};
// query pavlovia server:
// query the server:
const self = this;
return new Promise((resolve, reject) =>
return new Promise(async (resolve, reject) =>
{
const url = this._psychoJS.config.pavlovia.URL
+ "/api/v2/experiments/" + encodeURIComponent(this._psychoJS.config.experiment.fullpath)
+ "/resources";
try
{
const getResponse = await this._queryServerAPI(
"GET",
`experiments/${this._psychoJS.config.gitlab.projectId}/resources`,
data
);
jQuery.get(url, data, null, "json")
.done((data, textStatus) =>
{
if (!("resources" in data))
{
self.setStatus(ServerManager.Status.ERROR);
// reject({ ...response, error: 'unexpected answer from server: no resources' });
reject(Object.assign(response, { error: "unexpected answer from server: no resources" }));
}
if (!("resourceDirectory" in data))
{
self.setStatus(ServerManager.Status.ERROR);
// reject({ ...response, error: 'unexpected answer from server: no resourceDirectory' });
reject(Object.assign(response, { error: "unexpected answer from server: no resourceDirectory" }));
}
const getResourcesResponse = await getResponse.json();
self.setStatus(ServerManager.Status.READY);
// resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
resolve(Object.assign(response, {
resources: data.resources,
resourceDirectory: data.resourceDirectory,
}));
})
.fail((jqXHR, textStatus, errorThrown) =>
if (!("resources" in getResourcesResponse))
{
self.setStatus(ServerManager.Status.ERROR);
throw "unexpected answer from server: no resources";
}
if (!("resourceDirectory" in getResourcesResponse))
{
self.setStatus(ServerManager.Status.ERROR);
throw "unexpected answer from server: no resourceDirectory";
}
const errorMsg = util.getRequestError(jqXHR, textStatus, errorThrown);
console.error("error:", errorMsg);
reject(Object.assign(response, { error: errorMsg }));
});
self.setStatus(ServerManager.Status.READY);
resolve({ ...response, resources: data.resources, resourceDirectory: data.resourceDirectory });
}
catch (error)
{
console.error(error);
self.setStatus(ServerManager.Status.ERROR);
reject({...response, error});
}
});
}
@ -1335,6 +1333,79 @@ export class ServerManager extends PsychObject
});
}
/**
* Query the pavlovia server API.
*
* @name module:core.ServerManager#_queryServerAPI
* @function
* @protected
* @param method the HTTP method, i.e. GET, PUT, POST, or DELETE
* @param path the resource path, without the server address
* @param data the data to be sent
* @param {string} [contentType="JSON"] the content type, either JSON or FORM
*/
_queryServerAPI(method, path, data, contentType = "JSON")
{
const fullPath = `${this._psychoJS.config.pavlovia.URL}/api/v2/${path}`;
if (method === "PUT" || method === "POST" || method === "DELETE")
{
if (contentType === "JSON")
{
return fetch(fullPath, {
method,
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
redirect: 'follow',
referrerPolicy: 'no-referrer',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
}
else
{
const formData = new FormData();
for (const attribute in data)
{
formData.append(attribute, data[attribute]);
}
return fetch(fullPath, {
method,
mode: 'cors',
cache: 'no-cache',
credentials: 'same-origin',
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: formData
});
}
}
if (method === "GET")
{
let url = new URL(fullPath);
url.search = new URLSearchParams(data).toString();
return fetch(url, {
method: "GET",
mode: "cors",
cache: "no-cache",
credentials: "same-origin",
redirect: "follow",
referrerPolicy: "no-referrer"
});
}
throw {
origin: "ServerManager._queryServer",
context: "when querying the server",
error: "the method should be GET, PUT, POST, or DELETE"
};
}
}

View File

@ -312,7 +312,8 @@ export class Window extends PsychObject
* @function
* @public
*/
addPixiObject (pixiObject) {
addPixiObject(pixiObject)
{
this._stimsContainer.addChild(pixiObject);
}
@ -323,7 +324,8 @@ export class Window extends PsychObject
* @function
* @public
*/
removePixiObject (pixiObject) {
removePixiObject(pixiObject)
{
this._stimsContainer.removeChild(pixiObject);
}

View File

@ -88,6 +88,43 @@ export class MultiStairHandler extends TrialHandler
this._nextTrial();
}
/**
* Get the current staircase.
*
* @name module:data.MultiStairHandler#currentStaircase
* @function
* @public
* @returns {module.data.TrialHandler} the current staircase, or undefined if the trial has ended
*/
get currentStaircase()
{
return this._currentStaircase;
}
/**
* Get the current intensity.
*
* @name module:data.MultiStairHandler#intensity
* @function
* @public
* @returns {number} the intensity of the current staircase, or undefined if the trial has ended
*/
get intensity()
{
if (this._currentStaircase instanceof QuestHandler)
{
return this._currentStaircase.getQuestValue();
}
// TODO similar for simple staircase:
// if (this._currentStaircase instanceof StaircaseHandler)
// {
// return this._currentStaircase.getStairValue();
// }
return undefined;
}
/**
* Add a response to the current staircase.
*
@ -344,10 +381,10 @@ export class MultiStairHandler extends TrialHandler
if (typeof this._snapshots[t] !== "undefined")
{
let fieldName = this._name + "." + this._varName;
let fieldName = /*this._name + "." +*/ this._varName;
this._snapshots[t][fieldName] = value;
this._snapshots[t].trialAttributes.push(fieldName);
fieldName = this._name + ".intensity";
fieldName = /*this._name + ".*/ "intensity";
this._snapshots[t][fieldName] = value;
this._snapshots[t].trialAttributes.push(fieldName);
@ -356,13 +393,13 @@ export class MultiStairHandler extends TrialHandler
// "name" becomes "label" again:
if (attribute === 'name')
{
fieldName = this._name + ".label";
fieldName = /*this._name + ".*/ "label";
this._snapshots[t][fieldName] = this._currentStaircase["_name"];
this._snapshots[t].trialAttributes.push(fieldName);
}
else if (attribute !== 'trialList' && attribute !== 'extraInfo')
{
fieldName = this._name+"."+attribute;
fieldName = /*this._name+"."+*/ attribute;
this._snapshots[t][fieldName] = this._currentStaircase["_" + attribute];
this._snapshots[t].trialAttributes.push(fieldName);
}

View File

@ -248,6 +248,21 @@ export class QuestHandler extends TrialHandler
return this._questValue;
}
/**
* Get the current value of the variable / contrast / threshold.
*
* <p>This is the getter associated to getQuestValue.</p>
*
* @name module:data.MultiStairHandler#intensity
* @function
* @public
* @returns {number} the intensity of the current staircase, or undefined if the trial has ended
*/
get intensity()
{
return this.getQuestValue();
}
/**
* Get an estimate of the 5%-95% confidence interval (CI).
*

File diff suppressed because it is too large Load Diff

View File

@ -294,11 +294,12 @@ export class TrialHandler extends PsychObject
}
/**
* Set the internal state of this trial handler from the given snapshot.
* Set the internal state of the snapshot's trial handler from the snapshot.
*
* @public
* @static
* @param {Snapshot} snapshot - the snapshot from which to update the current internal state.
* @param {Snapshot} snapshot - the snapshot from which to update the current internal state of the
* snapshot's trial handler
*/
static fromSnapshot(snapshot)
{
@ -317,7 +318,6 @@ export class TrialHandler extends PsychObject
snapshot.handler.thisIndex = snapshot.thisIndex;
snapshot.handler.ran = snapshot.ran;
snapshot.handler._finished = snapshot._finished;
snapshot.handler.thisTrial = snapshot.handler.getCurrentTrial();
// add the snapshot's trial attributes to a global variable, whose name is derived from

View File

@ -2,7 +2,7 @@
* Manager handling the recording of video signal.
*
* @author Alain Pitiot
* @version 2021.2.0
* @version 2022.2.0
* @copyright (c) 2021 Open Science Tools Ltd. (https://opensciencetools.org)
* @license Distributed under the terms of the MIT License
*/
@ -18,15 +18,11 @@ import {ExperimentHandler} from "../data/ExperimentHandler.js";
/**
* <p>This manager handles the recording of video signal.</p>
*
* @name module:visual.Camera
* @name module:hardware.Camera
* @class
* @param {Object} options
* @param {module:core.Window} options.win - the associated Window
* @param {string} [options.format='video/webm;codecs=vp9'] the video format
* @param {boolean} [options.showDialog=false] - whether or not to open a dialog box to inform the
* participant to wait for the camera to be initialised
* @param {string} [options.dialogMsg="Please wait a few moments while the camera initialises"] -
* default message informing the participant to wait for the camera to initialise
* @param {Clock} [options.clock= undefined] - an optional clock
* @param {boolean} [options.autoLog= false] - whether or not to log
*
@ -34,7 +30,7 @@ import {ExperimentHandler} from "../data/ExperimentHandler.js";
*/
export class Camera extends PsychObject
{
constructor({win, name, format, showDialog, dialogMsg = "Please wait a few moments while the camera initialises", clock, autoLog} = {})
constructor({win, name, format, clock, autoLog} = {})
{
super(win._psychoJS);
@ -45,23 +41,8 @@ export class Camera extends PsychObject
this._addAttribute("autoLog", autoLog, false);
this._addAttribute("status", PsychoJS.Status.NOT_STARTED);
// open pop-up dialog:
if (showDialog)
{
this.psychoJS.gui.dialog({
warning: dialogMsg,
showOK: false,
});
}
// prepare the recording:
this._prepareRecording().then( () =>
{
if (showDialog)
{
this.psychoJS.gui.closeDialog();
}
})
this._stream = null;
this._recorder = null;
if (this._autoLog)
{
@ -70,14 +51,69 @@ export class Camera extends PsychObject
}
/**
* Query whether or not the camera is ready to record.
* Prompt the user for permission to use the camera on their device.
*
* @name module:visual.Camera#isReady
* @name module:hardware.Camera#authorize
* @function
* @public
* @param {boolean} [showDialog=false] - whether to open a dialog box to inform the
* participant to wait for the camera to be initialised
* @param {string} [dialogMsg] - the dialog message
* @returns {boolean} whether or not the camera is ready to record
*/
isReady()
async authorize(showDialog = false, dialogMsg = undefined)
{
const response = {
origin: "Camera.authorize",
context: "when authorizing access to the device's camera"
};
// open pop-up dialog, if required:
if (showDialog)
{
dialogMsg ??= "Please wait a few moments while the camera initialises. You may need to grant permission to your browser to use the camera.";
this.psychoJS.gui.dialog({
warning: dialogMsg,
showOK: false,
});
}
try
{
// prompt for permission and get a MediaStream:
// TODO use size constraints [https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia]
this._stream = await navigator.mediaDevices.getUserMedia({
video: true
});
}
catch (error)
{
// close the dialog, if need be:
if (showDialog)
{
this.psychoJS.gui.closeDialog();
}
this._status = PsychoJS.Status.ERROR;
throw {...response, error};
}
// close the dialog, if need be:
if (showDialog)
{
this.psychoJS.gui.closeDialog();
}
}
/**
* Query whether the camera is ready to record.
*
* @name module:hardware.Camera#isReady
* @function
* @public
* @returns {boolean} true if the camera is ready to record, false otherwise
*/
get isReady()
{
return (this._recorder !== null);
}
@ -85,7 +121,7 @@ export class Camera extends PsychObject
/**
* Get the underlying video stream.
*
* @name module:visual.Camera#getStream
* @name module:hardware.Camera#getStream
* @function
* @public
* @returns {MediaStream} the video stream
@ -98,7 +134,7 @@ export class Camera extends PsychObject
/**
* Get a video element pointing to the Camera stream.
*
* @name module:visual.Camera#getVideo
* @name module:hardware.Camera#getVideo
* @function
* @public
* @returns {HTMLVideoElement} a video element
@ -130,14 +166,36 @@ export class Camera extends PsychObject
}
/**
* Submit a request to start the recording.
* Open the video stream.
*
* @name module:visual.Camera#start
* @name module:hardware.Camera#open
* @function
* @public
* @return {Promise} promise fulfilled when the recording actually started
*/
start()
open()
{
if (this._stream === null)
{
throw {
origin: "Camera.open",
context: "when opening the camera's video stream",
error: "access to the camera has not been authorized, or no camera could be found"
};
}
// prepare the recording:
this._prepareRecording();
}
/**
* Submit a request to start the recording.
*
* @name module:hardware.Camera#record
* @function
* @public
* @return {Promise} promise fulfilled when the recording actually starts
*/
record()
{
// if the camera is currently paused, a call to start resumes it
// with a new recording:
@ -146,7 +204,6 @@ export class Camera extends PsychObject
return this.resume({clear: true});
}
if (this._status !== PsychoJS.Status.STARTED)
{
this._psychoJS.logger.debug("request to start video recording");
@ -175,7 +232,7 @@ export class Camera extends PsychObject
this._status = PsychoJS.Status.ERROR;
throw {
origin: "Camera.start",
origin: "Camera.record",
context: "when starting the video recording for camera: " + this._name,
error
};
@ -188,16 +245,14 @@ export class Camera extends PsychObject
/**
* Submit a request to stop the recording.
*
* @name module:visual.Camera#stop
* @name module:hardware.Camera#stop
* @function
* @public
* @param {Object} options
* @param {string} [options.filename] the name of the file to which the video recording
* will be saved
* @return {Promise} promise fulfilled when the recording actually stopped, and the recorded
* data was made available
*/
stop({filename} = {})
stop()
{
if (this._status === PsychoJS.Status.STARTED || this._status === PsychoJS.Status.PAUSED)
{
@ -209,12 +264,7 @@ export class Camera extends PsychObject
video.pause();
}
this._stopOptions = {
filename
};
// note: calling the stop method of the MediaRecorder will first raise
// a dataavailable event, and then a stop event
// note: calling the MediaRecorder.stop will first raise a dataavailable event, and then a stop event
// ref: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/stop
this._recorder.stop();
@ -232,7 +282,7 @@ export class Camera extends PsychObject
/**
* Submit a request to pause the recording.
*
* @name module:visual.Camera#pause
* @name module:hardware.Camera#pause
* @function
* @public
* @return {Promise} promise fulfilled when the recording actually paused
@ -281,7 +331,7 @@ export class Camera extends PsychObject
*
* <p>resume has no effect if the recording was not previously paused.</p>
*
* @name module:visual.Camera#resume
* @name module:hardware.Camera#resume
* @function
* @param {Object} options
* @param {boolean} [options.clear= false] whether or not to empty the video buffer before
@ -336,7 +386,7 @@ export class Camera extends PsychObject
/**
* Submit a request to flush the recording.
*
* @name module:visual.Camera#flush
* @name module:hardware.Camera#flush
* @function
* @public
* @return {Promise} promise fulfilled when the data has actually been made available
@ -362,74 +412,10 @@ export class Camera extends PsychObject
}
}
/**
* Offer the audio recording to the participant as a video file to download.
*
* @name module:visual.Camera#download
* @function
* @public
* @param {string} filename - the filename of the video file
*/
download(filename = "video.webm")
{
const videoBlob = new Blob(this._videoBuffer);
const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(videoBlob);
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
}
/**
* Upload the video recording to the pavlovia server.
*
* @name module:visual.Camera#upload
* @function
* @public
* @param @param {Object} options
* @param {string} options.tag an optional tag for the video file
* @param {boolean} [options.waitForCompletion= false] whether or not to wait for completion
* before returning
* @param {boolean} [options.showDialog=false] - whether or not to open a dialog box to inform the participant to wait for the data to be uploaded to the server
* @param {string} [options.dialogMsg=""] - default message informing the participant to wait for the data to be uploaded to the server
*/
async upload({tag, waitForCompletion = false, showDialog = false, dialogMsg = ""} = {})
{
// default tag: the name of this Camera object
if (typeof tag === "undefined")
{
tag = this._name;
}
// add a format-dependent video extension to the tag:
tag += util.extensionFromMimeType(this._format);
// if the video recording cannot be uploaded, e.g. the experiment is running locally, or
// if it is piloting mode, then we offer the video recording as a file for download:
if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
this._psychoJS.config.experiment.status !== "RUNNING" ||
this._psychoJS._serverMsg.has("__pilotToken"))
{
return this.download(tag);
}
// upload the blob:
const videoBlob = new Blob(this._videoBuffer);
return this._psychoJS.serverManager.uploadAudioVideo({
mediaBlob: videoBlob,
tag,
waitForCompletion,
showDialog,
dialogMsg});
}
/**
* Get the current video recording as a VideoClip in the given format.
*
* @name module:visual.Camera#getRecording
* @name module:hardware.Camera#getRecording
* @function
* @public
* @param {string} tag an optional tag for the video clip
@ -446,12 +432,82 @@ export class Camera extends PsychObject
// TODO
}
/**
* Upload the video recording to the pavlovia server.
*
* @name module:hardware.Camera#_upload
* @function
* @protected
* @param {string} tag an optional tag for the video file
* @param {boolean} [waitForCompletion= false] whether to wait for completion
* before returning
* @param {boolean} [showDialog=false] - whether to open a dialog box to inform the participant to wait for the data to be uploaded to the server
* @param {string} [dialogMsg=""] - default message informing the participant to wait for the data to be uploaded to the server
*/
save({tag, waitForCompletion = false, showDialog = false, dialogMsg = ""} = {})
{
this._psychoJS.logger.info("[PsychoJS] Save video recording.");
// default tag: the name of this Camera object
if (typeof tag === "undefined")
{
tag = this._name;
}
// add a format-dependent video extension to the tag:
tag += util.extensionFromMimeType(this._format);
// if the video recording cannot be uploaded, e.g. the experiment is running locally, or
// if it is piloting mode, then we offer the video recording as a file for download:
if (this._psychoJS.getEnvironment() !== ExperimentHandler.Environment.SERVER ||
this._psychoJS.config.experiment.status !== "RUNNING" ||
this._psychoJS._serverMsg.has("__pilotToken"))
{
const videoBlob = new Blob(this._videoBuffer);
const anchor = document.createElement("a");
anchor.href = window.URL.createObjectURL(videoBlob);
anchor.download = tag;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
return;
}
// upload the blob:
const videoBlob = new Blob(this._videoBuffer);
return this._psychoJS.serverManager.uploadAudioVideo({
mediaBlob: videoBlob,
tag,
waitForCompletion,
showDialog,
dialogMsg});
}
/**
* Close the camera stream.
*
* @name module:hardware.Camera#close
* @function
* @public
* @returns {Promise<void>} promise fulfilled when the stream has stopped and is now closed
*/
async close()
{
await this.stop();
this._videos = [];
this._stream = null;
this._recorder = null;
}
/**
* Callback for changes to the recording settings.
*
* <p>Changes to the settings require the recording to stop and be re-started.</p>
*
* @name module:visual.Camera#_onChange
* @name module:hardware.Camera#_onChange
* @function
* @protected
*/
@ -470,28 +526,21 @@ export class Camera extends PsychObject
/**
* Prepare the recording.
*
* @name module:visual.Camera#_prepareRecording
* @name module:hardware.Camera#_prepareRecording
* @function
* @protected
*/
async _prepareRecording()
_prepareRecording()
{
// empty the video buffer:
this._videoBuffer = [];
this._recorder = null;
this._videos = [];
// create a new stream with ideal dimensions:
// TODO use size constraints
this._stream = await navigator.mediaDevices.getUserMedia({
video: true
});
// check the actual width and height:
this._streamSettings = this._stream.getVideoTracks()[0].getSettings();
this._psychoJS.logger.debug(`camera stream settings: ${JSON.stringify(this._streamSettings)}`);
// check that the specified format is supported, use default if it is not:
let options;
if (typeof this._format === "string" && MediaRecorder.isTypeSupported(this._format))
@ -503,11 +552,9 @@ export class Camera extends PsychObject
this._psychoJS.logger.warn(`The specified video format, ${this._format}, is not supported by this browser, using the default format instead`);
}
// create a video recorder:
this._recorder = new MediaRecorder(this._stream, options);
// setup the callbacks:
const self = this;
@ -521,7 +568,7 @@ export class Camera extends PsychObject
self._status = PsychoJS.Status.STARTED;
self._psychoJS.logger.debug("video recording started");
// resolve the Microphone.start promise:
// resolve the Camera.start promise:
if (self._startCallback)
{
self._startCallback(self._psychoJS.monotonicClock.getTime());
@ -534,7 +581,7 @@ export class Camera extends PsychObject
self._status = PsychoJS.Status.PAUSED;
self._psychoJS.logger.debug("video recording paused");
// resolve the Microphone.pause promise:
// resolve the Camera.pause promise:
if (self._pauseCallback)
{
self._pauseCallback(self._psychoJS.monotonicClock.getTime());
@ -547,7 +594,7 @@ export class Camera extends PsychObject
self._status = PsychoJS.Status.STARTED;
self._psychoJS.logger.debug("video recording resumed");
// resolve the Microphone.resume promise:
// resolve the Camera.resume promise:
if (self._resumeCallback)
{
self._resumeCallback(self._psychoJS.monotonicClock.getTime());
@ -574,21 +621,13 @@ export class Camera extends PsychObject
this._recorder.onstop = () =>
{
self._psychoJS.logger.debug("video recording stopped");
self._status = PsychoJS.Status.NOT_STARTED;
self._status = PsychoJS.Status.STOPPED;
// resolve the Microphone.stop promise:
// resolve the Camera.stop promise:
if (self._stopCallback)
{
self._stopCallback(self._psychoJS.monotonicClock.getTime());
}
// treat stop options if there are any:
// download to a file, immediately offered to the participant:
if (typeof self._stopOptions.filename === "string")
{
self.download(self._stopOptions.filename);
}
};
// called upon recording errors:
@ -598,7 +637,6 @@ export class Camera extends PsychObject
self._psychoJS.logger.error("video recording error: " + JSON.stringify(event));
self._status = PsychoJS.Status.ERROR;
};
}
}

1
src/hardware/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./Camera.js";

View File

@ -89,17 +89,139 @@ a:hover {
/* Initialisation message (which will disappear behind the canvas) */
#root::after {
content: "initialising the experiment...";
left: 50%;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Initialisation message for IE11 */
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
#root::after {
color: #a05000;
content: "initialising the experiment... | Internet Explorer / Edge [beta]";
font-weight: bold;
}
.dialog-container,
.dialog-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.dialog-container {
z-index: 2;
display: flex;
}
.dialog-container[aria-hidden='true'] {
display: none;
}
.dialog-overlay {
background-color: rgba(0, 0, 0, 0.2);
}
.dialog-content {
margin: auto;
z-index: 2;
position: relative;
width: 500px;
max-width: 88vw;
padding: 0.5em;
border-radius: 2px;
font-family: 'Open Sans', sans-serif;
color: #333333;
background-color: #EEEEEE;
border-color: #CCCCCC;
box-shadow: 1px 1px 3px #555555;
}
.dialog-title {
padding: 0.5em;
margin-bottom: 1em;
background-color: #009900;
border-radius: 2px;
}
.dialog-title p {
margin: 0;
padding: 0;
font-weight: bold;
}
.dialog-close {
position: absolute;
top: 0.7em;
right: 0.7em;
border: 0;
padding: 0;
border-radius: 2px;
width: 1.1em;
height: 1.1em;
color: #333333;
background-color: #FFFFFF;
font-weight: bold;
font-size: 1.25em;
text-align: center;
cursor: pointer;
transition: 0.15s;
}
.progress-msg {
box-sizing: border-box;
padding: 0.5em 0;
}
.progress-container {
padding: 0.2em;
border: 1px solid #555555;
border-radius: 2px;
background-color: #FFFFFF;
}
.progress-bar {
width: 0;
height: 1.5em;
background-color: #CCCCCC;
}
.dialog-button {
padding: 0.5em 1em 0.5em 1em;
margin: 0.5em 0.5em 0.5em 0;
border: 1px solid #555555;
border-radius: 2px;
font-size: 0.9em;
color: #000000;
background-color: #FFFFFF;
cursor: pointer;
}
.dialog-button:hover {
background-color: #EEEEEE;
}
.dialog-button:active {
background-color: #CCCCCC;
}
.dialog-button:focus {
border: 1px solid #000000;
}
.disabled {
border: 1px solid #AAAAAA;
color: #AAAAAA;
background-color: #EEEEEE;
cursor: not-allowed;
pointer-events: none;
}

View File

@ -3,3 +3,4 @@ export * as core from './core/index.js';
export * as data from './data/index.js';
export * as visual from './visual/index.js';
export * as sound from './sound/index.js';
export * as hardware from './hardware/index.js';

View File

@ -1352,6 +1352,27 @@ export function count(input, value)
}
}
/**
* Pad the given floating-point number with however many 0 needed at the start such that
* the padded integer part of the number is of the given width.
*
* @param n - the input floating-point number
* @param width - the desired width
* @returns {string} - the padded number, whose integer part has the given width
*/
export function pad(n, width = 2)
{
const integerPart = Number.parseInt(n);
let decimalPart = (n+'').match(/\.[0-9]*/);
if (!decimalPart)
{
decimalPart = '';
}
return (integerPart+'').padStart(width,'0') + decimalPart;
}
/**
* Get the index in the input array of the first element that matches the given value.
*
@ -1433,7 +1454,7 @@ export function extensionFromMimeType(mimeType)
return ".webm";
}
return '.dat';
return ".dat";
}
/**
@ -1490,3 +1511,4 @@ export async function getDownloadSpeed(psychoJS, nbDownloads = 1)
download.src = `${imageUrl}?salt=${tic}`;
});
}

View File

@ -11,7 +11,7 @@ import {PsychoJS} from "../core/PsychoJS.js";
import * as util from "../util/Util.js";
import { to_pixiPoint } from "../util/Pixi.js";
import {Color} from "../util/Color.js";
import {Camera} from "./Camera.js";
import {Camera} from "../hardware/Camera.js";
import {VisualStim} from "./VisualStim.js";
import * as PIXI from "pixi.js-legacy";

View File

@ -13,6 +13,7 @@ import { ColorMixin } from "../util/ColorMixin.js";
import { to_pixiPoint } from "../util/Pixi.js";
import * as util from "../util/Util.js";
import { VisualStim } from "./VisualStim.js";
import {Camera} from "../hardware";
/**
* Image Stimulus.
@ -133,13 +134,27 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
image = this.psychoJS.serverManager.getResource(image);
}
// image should now be an actual HTMLImageElement: we raise an error if it is not
if (!(image instanceof HTMLImageElement))
if (image instanceof Camera)
{
throw "the argument: " + image.toString() + ' is not an image" }';
const video = image.getVideo();
// TODO remove previous one if there is one
// document.body.appendChild(video);
image = video;
}
this.psychoJS.logger.debug("set the image of ImageStim: " + this._name + " as: src= " + image.src + ", size= " + image.width + "x" + image.height);
// image should now be either an HTMLImageElement or an HTMLVideoElement:
if (image instanceof HTMLImageElement)
{
this.psychoJS.logger.debug("set the image of ImageStim: " + this._name + " as: src= " + image.src + ", size= " + image.width + "x" + image.height);
}
else if (image instanceof HTMLVideoElement)
{
this.psychoJS.logger.debug(`set the image of ImageStim: ${this._name} as: src= ${image.src}, size= ${image.videoWidth}x${image.videoHeight}, duration= ${image.duration}s`);
}
else
{
throw "the argument: " + image.toString() + ' is neither an image nor a video" }';
}
}
const existingImage = this.getImage();
@ -263,9 +278,18 @@ export class ImageStim extends util.mix(VisualStim).with(ColorMixin)
return;
}
const baseTexture = new PIXI.BaseTexture(this._image);
// deal with both static images and videos:
if (this._image instanceof HTMLImageElement)
{
this._texture = PIXI.Texture.from(this._image);
// const baseTexture = new PIXI.BaseTexture(this._image);
// this._texture = new PIXI.Texture(baseTexture);
}
else if (this._image instanceof HTMLVideoElement)
{
this._texture = PIXI.Texture.from(this._image, { resourceOptions: { autoPlay: true } });
}
this._texture = new PIXI.Texture(baseTexture);
this._pixi = PIXI.Sprite.from(this._texture);
// add a mask if need be:

View File

@ -14,7 +14,7 @@ import { ColorMixin } from "../util/ColorMixin.js";
import { to_pixiPoint } from "../util/Pixi.js";
import * as util from "../util/Util.js";
import { VisualStim } from "./VisualStim.js";
import {Camera} from "./Camera.js";
import {Camera} from "../hardware/Camera.js";
/**
@ -145,14 +145,14 @@ export class MovieStim extends VisualStim
{
const response = {
origin: "MovieStim.setMovie",
context: "when setting the movie of MovieStim: " + this._name,
context: `when setting the movie of MovieStim: ${this._name}`,
};
try
{
// movie is undefined: that's fine but we raise a warning in case this is
// a symptom of an actual problem
if (typeof movie === 'undefined')
if (typeof movie === "undefined")
{
this.psychoJS.logger.warn(
`setting the movie of MovieStim: ${this._name} with argument: undefined.`);
@ -170,16 +170,22 @@ export class MovieStim extends VisualStim
// if movie is an instance of camera, get a video element from it:
else if (movie instanceof Camera)
{
// old behaviour: feeding a Camera to MovieStim plays the live stream:
const video = movie.getVideo();
// TODO remove previous one if there is one
// document.body.appendChild(video);
movie = video;
/*
// new behaviour: feeding a Camera to MovieStim replays the video previously recorded by the Camera:
const video = movie.getRecording();
movie = video;
*/
}
// check that movie is now an HTMLVideoElement
if (!(movie instanceof HTMLVideoElement))
{
throw movie.toString() + " is not a video";
throw `${movie.toString()} is not a video`;
}
this.psychoJS.logger.debug(`set the movie of MovieStim: ${this._name} as: src= ${movie.src}, size= ${movie.videoWidth}x${movie.videoHeight}, duration= ${movie.duration}s`);
@ -320,8 +326,8 @@ export class MovieStim extends VisualStim
if (typeof size !== "undefined")
{
this._boundingBox = new PIXI.Rectangle(
this._pos[0] - size[0] / 2,
this._pos[1] - size[1] / 2,
this._pos[0] - (size[0] / 2),
this._pos[1] - (size[1] / 2),
size[0],
size[1],
);

View File

@ -340,7 +340,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
const anchor = this._getAnchor();
this._boundingBox = new PIXI.Rectangle(
this._pos[0] - anchor[0] * textSize[0],
this._pos[1] - textSize[1] - anchor[1] * textSize[1],
this._pos[1] - textSize[1] + anchor[1] * textSize[1],
textSize[0],
textSize[1],
);
@ -445,7 +445,7 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
// refine the estimate of the bounding box:
this._boundingBox = new PIXI.Rectangle(
this._pos[0] - anchor[0] * this._size[0],
this._pos[1] - this._size[1] - anchor[1] * this._size[1],
this._pos[1] - this._size[1] + anchor[1] * this._size[1],
this._size[0],
this._size[1],
);

View File

@ -11,5 +11,4 @@ export * from "./TextBox.js";
export * from "./TextInput.js";
export * from "./TextStim.js";
export * from "./VisualStim.js";
export * from "./Camera.js";
export * from "./FaceDetector.js";