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
195991eec3
@ -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);
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,14 @@ export class Shelf extends PsychObject
|
||||
|
||||
this._addAttribute('autoLog', autoLog);
|
||||
this._addAttribute('status', Shelf.Status.READY);
|
||||
|
||||
// minimum period of time, in ms, before two calls to Shelf methods, i.e. throttling:
|
||||
this._throttlingPeriod_ms = 5000.0;
|
||||
|
||||
// timestamp of the last actual call to a Shelf method:
|
||||
this._lastCallTimestamp = 0.0;
|
||||
// timestamp of the last scheduled call to a Shelf method:
|
||||
this._lastScheduledCallTimestamp = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,12 +68,9 @@ export class Shelf extends PsychObject
|
||||
context: `when getting the value associated with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("getValue");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("getValue");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -130,12 +135,9 @@ export class Shelf extends PsychObject
|
||||
context: `when setting the value associated with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("setValue");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("setValue");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -195,12 +197,9 @@ export class Shelf extends PsychObject
|
||||
context: `when getting the names of the fields in the dictionary record associated with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("getDictionaryFieldNames");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("getDictionaryFieldNames");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -260,12 +259,9 @@ export class Shelf extends PsychObject
|
||||
context: `when getting value of field: ${fieldName} in the dictionary record associated with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("getDictionaryValue");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("getDictionaryValue");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -330,12 +326,9 @@ export class Shelf extends PsychObject
|
||||
context: `when setting a field with name: ${fieldName} in the dictionary record associated with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("setDictionaryField");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("setDictionaryField");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -451,12 +444,9 @@ export class Shelf extends PsychObject
|
||||
context: `when incrementing the integer counter with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("increment");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("increment");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -518,12 +508,9 @@ export class Shelf extends PsychObject
|
||||
context: `when getting the name of a group, using a counterbalanced design, with key: ${JSON.stringify(key)}`
|
||||
};
|
||||
|
||||
// TODO what to do if the status of shelf is currently BUSY? Wait until it is READY again?
|
||||
|
||||
try
|
||||
{
|
||||
this._checkAvailability("counterBalanceSelect");
|
||||
this._status = Shelf.Status.BUSY;
|
||||
await this._checkAvailability("counterBalanceSelect");
|
||||
this._checkKey(key);
|
||||
|
||||
// prepare the request:
|
||||
@ -575,7 +562,7 @@ export class Shelf extends PsychObject
|
||||
* @function
|
||||
* @public
|
||||
* @param {string} [methodName=""] name of the method requiring a check
|
||||
* @throw {Object.<string, *>} exception when it is not possible to run shelf commands
|
||||
* @throw {Object.<string, *>} exception when it is not possible to run the given shelf command
|
||||
*/
|
||||
_checkAvailability(methodName = "")
|
||||
{
|
||||
@ -588,6 +575,37 @@ export class Shelf extends PsychObject
|
||||
error: 'the experiment has to be run on the server: shelf commands are not available locally'
|
||||
}
|
||||
}
|
||||
|
||||
// throttle calls to Shelf methods:
|
||||
const self = this;
|
||||
return new Promise((resolve, reject) =>
|
||||
{
|
||||
const now = performance.now();
|
||||
|
||||
// if the last scheduled call already occurred, schedule this one as soon as possible,
|
||||
// taking into account the throttling period:
|
||||
let timeoutDuration;
|
||||
if (now > self._lastScheduledCallTimestamp)
|
||||
{
|
||||
timeoutDuration = Math.max(0.0, self._throttlingPeriod_ms - (now - self._lastCallTimestamp));
|
||||
self._lastScheduledCallTimestamp = now + timeoutDuration;
|
||||
}
|
||||
// otherwise, schedule it after the next call:
|
||||
else
|
||||
{
|
||||
self._lastScheduledCallTimestamp += self._throttlingPeriod_ms;
|
||||
timeoutDuration = self._lastScheduledCallTimestamp;
|
||||
}
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
self._lastCallTimestamp = performance.now();
|
||||
self._status = Shelf.Status.BUSY;
|
||||
resolve();
|
||||
},
|
||||
timeoutDuration
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
@ -576,19 +623,11 @@ export class Camera extends PsychObject
|
||||
self._psychoJS.logger.debug("video recording stopped");
|
||||
self._status = PsychoJS.Status.NOT_STARTED;
|
||||
|
||||
// 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";
|
@ -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';
|
||||
|
@ -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";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -334,13 +334,13 @@ export class TextStim extends util.mix(VisualStim).with(ColorMixin)
|
||||
"pix",
|
||||
this._win,
|
||||
this._units,
|
||||
);
|
||||
); console.log(this._name, '>>', textSize);
|
||||
|
||||
// take the alignment into account:
|
||||
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