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:
parent
3b0308a77f
commit
173d08947d
31
package-lock.json
generated
31
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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).
|
||||
*
|
||||
|
1026
src/data/Shelf.js
1026
src/data/Shelf.js
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
@ -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
1
src/hardware/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./Camera.js";
|
138
src/index.css
138
src/index.css
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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],
|
||||
);
|
||||
|
@ -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],
|
||||
);
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user