Spaces:
Runtime error
Runtime error
| 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.<number>} | |
| */ | |
| 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.<Video>} resolves a promise to this video provider when video is ready. | |
| */ | |
| enableVideo () { | |
| this.enabled = true; | |
| return this._setupVideo(); | |
| } | |
| /** | |
| * Disable video stream (turn video off) | |
| */ | |
| disableVideo () { | |
| this.enabled = false; | |
| // If we have begun a setup process, call _teardown after it completes | |
| if (this._singleSetup) { | |
| this._singleSetup | |
| .then(this._teardown.bind(this)) | |
| .catch(err => this.onError(err)); | |
| } | |
| } | |
| /** | |
| * async part of disableVideo | |
| * @private | |
| */ | |
| _teardown () { | |
| // we might be asked to re-enable before _teardown is called, just ignore it. | |
| if (this.enabled === false) { | |
| const disableTrack = requestDisableVideo(); | |
| this._singleSetup = null; | |
| // by clearing refs to video and track, we should lose our hold over the camera | |
| this._video = null; | |
| if (this._track && disableTrack) { | |
| this._track.stop(); | |
| } | |
| this._track = null; | |
| } | |
| } | |
| /** | |
| * 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 true | |
| * @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 = VideoProvider.DIMENSIONS, | |
| mirror = this.mirror, | |
| format = VideoProvider.FORMAT_IMAGE_DATA, | |
| cacheTimeout = this._frameCacheTimeout | |
| }) { | |
| if (!this.videoReady) { | |
| return null; | |
| } | |
| const [width, height] = dimensions; | |
| const workspace = this._getWorkspace({dimensions, mirror: Boolean(mirror)}); | |
| const {videoWidth, videoHeight} = this._video; | |
| const {canvas, context, lastUpdate, cacheData} = workspace; | |
| const now = Date.now(); | |
| // if the canvas hasn't been updated... | |
| if (lastUpdate + cacheTimeout < now) { | |
| if (mirror) { | |
| context.scale(-1, 1); | |
| context.translate(width * -1, 0); | |
| } | |
| context.drawImage(this._video, | |
| // source x, y, width, height | |
| 0, 0, videoWidth, videoHeight, | |
| // dest x, y, width, height | |
| 0, 0, width, height | |
| ); | |
| // context.resetTransform() doesn't work on Edge but the following should | |
| context.setTransform(1, 0, 0, 1, 0, 0); | |
| workspace.lastUpdate = now; | |
| } | |
| // each data type has it's own data cache, but the canvas is the same | |
| if (!cacheData[format]) { | |
| cacheData[format] = {lastUpdate: 0}; | |
| } | |
| const formatCache = cacheData[format]; | |
| if (formatCache.lastUpdate + cacheTimeout < now) { | |
| if (format === VideoProvider.FORMAT_IMAGE_DATA) { | |
| formatCache.lastData = context.getImageData(0, 0, width, height); | |
| } else if (format === VideoProvider.FORMAT_CANVAS) { | |
| // this will never change | |
| formatCache.lastUpdate = Infinity; | |
| formatCache.lastData = canvas; | |
| } else { | |
| log.error(`video io error - unimplemented format ${format}`); | |
| // cache the null result forever, don't log about it again.. | |
| formatCache.lastUpdate = Infinity; | |
| formatCache.lastData = null; | |
| } | |
| // rather than set to now, this data is as stale as it's canvas is | |
| formatCache.lastUpdate = Math.max(workspace.lastUpdate, formatCache.lastUpdate); | |
| } | |
| return formatCache.lastData; | |
| } | |
| /** | |
| * Method called when an error happens. Default implementation is just to log error. | |
| * | |
| * @abstract | |
| * @param {Error} error An error object from getUserMedia or other source of error. | |
| */ | |
| onError (error) { | |
| log.error('Unhandled video io device error', error); | |
| } | |
| /** | |
| * Create a video stream. | |
| * @private | |
| * @return {Promise} When video has been received, rejected if video is not received | |
| */ | |
| _setupVideo () { | |
| // We cache the result of this setup so that we can only ever have a single | |
| // video/getUserMedia request happen at a time. | |
| if (this._singleSetup) { | |
| return this._singleSetup; | |
| } | |
| this._singleSetup = requestVideoStream({ | |
| width: {min: 480, ideal: 640}, | |
| height: {min: 360, ideal: 480} | |
| }) | |
| .then(stream => { | |
| this._video = document.createElement('video'); | |
| // Use the new srcObject API, falling back to createObjectURL | |
| try { | |
| this._video.srcObject = stream; | |
| } catch (error) { | |
| this._video.src = window.URL.createObjectURL(stream); | |
| } | |
| // Hint to the stream that it should load. A standard way to do this | |
| // is add the video tag to the DOM. Since this extension wants to | |
| // hide the video tag and instead render a sample of the stream into | |
| // the webgl rendered Scratch canvas, another hint like this one is | |
| // needed. | |
| this._video.play(); // Needed for Safari/Firefox, Chrome auto-plays. | |
| this._track = stream.getTracks()[0]; | |
| return this; | |
| }) | |
| .catch(error => { | |
| this._singleSetup = null; | |
| this.onError(error); | |
| }); | |
| return this._singleSetup; | |
| } | |
| get videoReady () { | |
| if (!this.enabled) { | |
| return false; | |
| } | |
| if (!this._video) { | |
| return false; | |
| } | |
| if (!this._track) { | |
| return false; | |
| } | |
| const {videoWidth, videoHeight} = this._video; | |
| if (typeof videoWidth !== 'number' || typeof videoHeight !== 'number') { | |
| return false; | |
| } | |
| if (videoWidth === 0 || videoHeight === 0) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| /** | |
| * get an internal workspace for canvas/context/caches | |
| * this uses some document stuff to create a canvas and what not, probably needs abstraction | |
| * into the renderer layer? | |
| * @private | |
| * @return {object} A workspace for canvas/data storage. Internal format not documented intentionally | |
| */ | |
| _getWorkspace ({dimensions, mirror}) { | |
| let workspace = this._workspace.find(space => ( | |
| space.dimensions.join('-') === dimensions.join('-') && | |
| space.mirror === mirror | |
| )); | |
| if (!workspace) { | |
| workspace = { | |
| dimensions, | |
| mirror, | |
| canvas: document.createElement('canvas'), | |
| lastUpdate: 0, | |
| cacheData: {} | |
| }; | |
| workspace.canvas.width = dimensions[0]; | |
| workspace.canvas.height = dimensions[1]; | |
| workspace.context = workspace.canvas.getContext('2d'); | |
| this._workspace.push(workspace); | |
| } | |
| return workspace; | |
| } | |
| } | |
| export default VideoProvider; | |