soiz1's picture
Upload 811 files
30c32c8 verified
raw
history blame
7.54 kB
const StageLayering = require('../engine/stage-layering');
class Video {
constructor (runtime) {
this.runtime = runtime;
/**
* @typedef VideoProvider
* @property {Function} enableVideo - Requests camera access from the user, and upon success,
* enables the video feed
* @property {Function} disableVideo - Turns off the video feed
* @property {Function} getFrame - Return frame data from the video feed in
* specified dimensions, format, and mirroring.
*/
this.provider = null;
/**
* Id representing a Scratch Renderer skin the video is rendered to for
* previewing.
* @type {number}
*/
this._skinId = -1;
/**
* Id for a drawable using the video's skin that will render as a video
* preview.
* @type {Drawable}
*/
this._drawable = -1;
/**
* Store the last state of the video transparency ghost effect
* @type {number}
*/
this._ghost = 0;
/**
* Store a flag that allows the preview to be forced transparent.
* @type {number}
*/
this._forceTransparentPreview = false;
}
static get FORMAT_IMAGE_DATA () {
return 'image-data';
}
static get FORMAT_CANVAS () {
return 'canvas';
}
/**
* Dimensions the video stream is analyzed at after its rendered to the
* sample canvas.
* @type {Array.<number>}
*/
static get DIMENSIONS () {
return [480, 360];
}
/**
* Order preview drawable is inserted at in the renderer.
* @type {number}
*/
static get ORDER () {
return 1;
}
/**
* Set a video provider for this device. A default implementation of
* a video provider can be found in scratch-gui/src/lib/video/video-provider
* @param {VideoProvider} provider - Video provider to use
*/
setProvider (provider) {
this.provider = provider;
}
/**
* Request video be enabled. Sets up video, creates video skin and enables preview.
*
* ioDevices.video.requestVideo()
*
* @return {Promise.<Video>} resolves a promise to this IO device when video is ready.
*/
enableVideo () {
if (!this.provider) return null;
return this.provider.enableVideo().then(() => this._setupPreview());
}
/**
* Disable video stream (turn video off)
* @return {void}
*/
disableVideo () {
this._disablePreview();
if (!this.provider) return null;
this.provider.disableVideo();
}
/**
* Return frame data from the video feed in a specified dimensions, format, and mirroring.
*
* @param {object} frameInfo A descriptor of the frame you would like to receive.
* @param {Array.<number>} frameInfo.dimensions [width, height] array of numbers. Defaults to [480,360]
* @param {boolean} frameInfo.mirror If you specificly want a mirror/non-mirror frame, defaults to the global
* mirror state (ioDevices.video.mirror)
* @param {string} frameInfo.format Requested video format, available formats are 'image-data' and 'canvas'.
* @param {number} frameInfo.cacheTimeout Will reuse previous image data if the time since capture is less than
* the cacheTimeout. Defaults to 16ms.
*
* @return {ArrayBuffer|Canvas|string|null} Frame data in requested format, null when errors.
*/
getFrame ({
dimensions = Video.DIMENSIONS,
mirror = this.mirror,
format = Video.FORMAT_IMAGE_DATA,
cacheTimeout = this._frameCacheTimeout
}) {
if (this.provider) return this.provider.getFrame({dimensions, mirror, format, cacheTimeout});
return null;
}
/**
* Set the preview ghost effect
* @param {number} ghost from 0 (visible) to 100 (invisible) - ghost effect
*/
setPreviewGhost (ghost) {
this._ghost = ghost;
// Confirm that the default value has been changed to a valid id for the drawable
if (this._drawable !== -1) {
this.runtime.renderer.updateDrawableEffect(
this._drawable,
'ghost',
this._forceTransparentPreview ? 100 : ghost
);
}
}
_disablePreview () {
if (this._skinId !== -1) {
this.runtime.renderer.updateBitmapSkin(this._skinId, new ImageData(...Video.DIMENSIONS), 1);
this.runtime.renderer.updateDrawableVisible(this._drawable, false);
}
this._renderPreviewFrame = null;
}
_setupPreview () {
const {renderer} = this.runtime;
if (!renderer) return;
if (this._skinId === -1 && this._drawable === -1) {
this._skinId = renderer.createBitmapSkin(new ImageData(...Video.DIMENSIONS), 1);
this._drawable = renderer.createDrawable(StageLayering.VIDEO_LAYER);
renderer.updateDrawableSkinId(this._drawable, this._skinId);
// TW: Video probably contains the user's face. This is private information.
// This API won't exist if we're using a vanilla scratch-render
if (renderer.markSkinAsPrivate) {
renderer.markSkinAsPrivate(this._skinId);
}
}
// if we haven't already created and started a preview frame render loop, do so
if (!this._renderPreviewFrame) {
renderer.updateDrawableEffect(this._drawable, 'ghost', this._forceTransparentPreview ? 100 : this._ghost);
renderer.updateDrawableVisible(this._drawable, true);
this._renderPreviewFrame = () => {
clearTimeout(this._renderPreviewTimeout);
if (!this._renderPreviewFrame) {
return;
}
this._renderPreviewTimeout = setTimeout(this._renderPreviewFrame, this.runtime.currentStepTime);
const imageData = this.getFrame({
format: Video.FORMAT_IMAGE_DATA,
cacheTimeout: this.runtime.currentStepTime
});
if (!imageData) {
renderer.updateBitmapSkin(this._skinId, new ImageData(...Video.DIMENSIONS), 1);
return;
}
renderer.updateBitmapSkin(this._skinId, imageData, 1);
this.runtime.requestRedraw();
};
this._renderPreviewFrame();
}
}
get videoReady () {
if (this.provider) return this.provider.videoReady;
return false;
}
/**
* Method implemented by all IO devices to allow external changes.
* The only change available externally is hiding the preview, used e.g. to
* prevent drawing the preview into project thumbnails.
* @param {object} - data passed to this IO device.
* @property {boolean} forceTransparentPreview - whether the preview should be forced transparent.
*/
postData ({forceTransparentPreview}) {
this._forceTransparentPreview = forceTransparentPreview;
// Setting the ghost to the current value will pick up the forceTransparentPreview
// flag and override the current ghost. The complexity is to prevent blocks
// from overriding forceTransparentPreview
this.setPreviewGhost(this._ghost);
}
}
module.exports = Video;