diff --git a/package.json b/package.json index f7a06cae8..8a4c90e80 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "scratch-render": "0.1.0-prerelease.20180427143155", "scratch-svg-renderer": "0.1.0-prerelease.20180423193917", "scratch-storage": "0.4.1", - "scratch-vm": "0.1.0-prerelease.1524765647", + "scratch-vm": "0.1.0-prerelease.1525095619", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.20.0", diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index f3444af1a..96fdf71a6 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -6,6 +6,7 @@ import VM from 'scratch-vm'; import {connect} from 'react-redux'; import {getEventXY} from '../lib/touch-utils'; +import VideoProvider from '../lib/video/video-provider'; import StageComponent from '../components/stage/stage.jsx'; @@ -57,6 +58,7 @@ class Stage extends React.Component { this.renderer = new Renderer(this.canvas); this.props.vm.attachRenderer(this.renderer); this.props.vm.runtime.addListener('QUESTION', this.questionListener); + this.props.vm.setVideoProvider(new VideoProvider()); } shouldComponentUpdate (nextProps, nextState) { return this.props.width !== nextProps.width || diff --git a/src/lib/video/camera.js b/src/lib/video/camera.js new file mode 100644 index 000000000..0dc2f14e9 --- /dev/null +++ b/src/lib/video/camera.js @@ -0,0 +1,35 @@ +import getUserMedia from 'get-user-media-promise'; + +// Single Setup For All Video Streams used by the GUI +// While VideoProvider uses a private _singleSetup +// property to ensure that each instance of a VideoProvider +// use the same setup, this ensures that all instances +// of VideoProviders use a single stream. This way, closing a camera modal +// does not affect the video on the stage, and a program running and disabling +// video on the stage will not affect the camera modal's video. +const requestStack = []; +const requestVideoStream = videoDesc => { + let streamPromise; + if (requestStack.length === 0) { + streamPromise = getUserMedia({ + audio: false, + video: videoDesc + }); + requestStack.push(streamPromise); + } else if (requestStack.length > 0) { + streamPromise = requestStack[0]; + requestStack.push(true); + } + return streamPromise; +}; + +const requestDisableVideo = () => { + requestStack.pop(); + if (requestStack.length > 0) return false; + return true; +}; + +export { + requestVideoStream, + requestDisableVideo +}; diff --git a/src/lib/video/modal-video-manager.js b/src/lib/video/modal-video-manager.js new file mode 100644 index 000000000..fee3d452d --- /dev/null +++ b/src/lib/video/modal-video-manager.js @@ -0,0 +1,62 @@ +import VideoProvider from './video-provider.js'; +/** + * Video Manager for Camera Modal + */ +class ModalVideoManager { + constructor (canvas) { + this._videoProvider = new VideoProvider(); + + this._frameTimeout = 16; + + this._canvas = canvas; + // These values are double the stage dimensions so that the resulting + // image does not have to get sized down to accomodate double resolution + this._canvasWidth = 960; // Double Stage Width + this._canvasHeight = 720; // Double Stage Height + + } + + enableVideo (onPermissionSuccess, onVideoLoaded) { + const thisContext = this; + this._videoProvider.enableVideo(onVideoLoaded).then(() => { + if (onPermissionSuccess) onPermissionSuccess(); + const ctx = thisContext._canvas.getContext('2d'); + ctx.scale(-1, 1); + ctx.translate(thisContext._canvasWidth * -1, 0); + + if (onVideoLoaded) { + thisContext._videoProvider.video.onloadeddata = () => { + onVideoLoaded(); + }; + } + + thisContext._drawFrames(); + }); + } + + _drawFrames () { + const video = this._videoProvider.video; + this._videoFeedInterval = setInterval(() => + this._canvas.getContext('2d').drawImage(video, + // source x, y, width, height + 0, 0, video.videoWidth, video.videoHeight, + // dest x, y, width, height + 0, 0, this._canvasWidth, this._canvasHeight + ), this._frameTimeout); + } + + takeSnapshot () { + clearInterval(this._videoFeedInterval); + return this._canvas.toDataURL('image/png'); + } + + clearSnapshot () { + this._drawFrames(); + } + + disableVideo () { + this._videoProvider.disableVideo(); + } +} + +export default ModalVideoManager; diff --git a/src/lib/video/video-provider.js b/src/lib/video/video-provider.js new file mode 100644 index 000000000..dffe4716f --- /dev/null +++ b/src/lib/video/video-provider.js @@ -0,0 +1,287 @@ +import {requestVideoStream, requestDisableVideo} from './camera.js'; +import log from '../log.js'; + +/** + * Video Manager for video extensions. + */ +class VideoProvider { + constructor () { + /** + * Default value for mirrored frames. + * @type boolean + */ + this.mirror = true; + + /** + * Cache frames for this many ms. + * @type number + */ + this._frameCacheTimeout = 16; + + /** + * DOM Video element + * @private + */ + this._video = null; + + /** + * Usermedia stream track + * @private + */ + this._track = null; + + /** + * Stores some canvas/frame data per resolution/mirror states + */ + this._workspace = []; + } + + 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.} + */ + static get DIMENSIONS () { + return [480, 360]; + } + + /** + * Order preview drawable is inserted at in the renderer. + * @type {number} + */ + static get ORDER () { + return 1; + } + + /** + * Get the HTML video element containing the stream + */ + get video () { + return this._video; + } + + /** + * Request video be enabled. Sets up video, creates video skin and enables preview. + * + * @return {Promise.