/** * @author Richard Davey * @copyright 2015 Photon Storm Ltd. * @license {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License} */ /** * A Video object that takes a previously loaded Video from the Phaser Cache and handles playback of it. * * Alternatively it takes a getUserMedia feed from an active webcam and streams the contents of that to * the Video instead. * * This can be applied to a Sprite as a texture. If multiple Sprites share the same Video texture and playback * changes (i.e. you pause the video, or seek to a new time) then this change will be seen across all Sprites simultaneously. * * If you need each Sprite to be able to play a video fully independently then you will need one Video object per Sprite. * Please understand the obvious performance implications of doing this, and the memory required to hold videos in RAM. * * On some mobile browsers such as iOS Safari, you cannot play a video until the user has explicitly touched the screen. * This works in the same way as audio unlocking. Phaser will handle the touch unlocking for you, however unlike with audio * it's worth noting that every single Video needs to be touch unlocked, not just the first one. You can use the `changeSource` * method to try and work around this limitation, but see the method help for details. * * Small screen devices, especially iPod and iPhone will launch the video in its own native video player, * outside of the Safari browser. There is no way to avoid this, it's a device imposed limitation. * * @class Phaser.Video * @constructor * @param {Phaser.Game} game - A reference to the currently running game. * @param {string|null} key - The key of the video file in the Phaser.Cache that this Video object should use. If null a `getUserMedia` video stream will be established instead. * @param {boolean} [captureAudio=false] - If the key is null this controls if audio should be captured along with video in the video stream. * @param {integer} [width] - If the key is null this width is used to create the video stream. If not provided the video width will be set to the width of the webcam input source. * @param {integer} [height] - If the key is null this height is used to create the video stream. If not provided the video height will be set to the height of the webcam input source. */ Phaser.Video = function (game, key, captureAudio, width, height) { if (typeof key === 'undefined') { key = null; } if (typeof captureAudio === 'undefined') { captureAudio = false; } /** * @property {Phaser.Game} game - A reference to the currently running game. */ this.game = game; /** * @property {string} key - The key of the Video in the Cache, if stored there. */ this.key = key; /** * @property {number} width - The width of the video in pixels. */ this.width = (width) ? width : 0; /** * @property {number} height - The height of the video in pixels. */ this.height = (height) ? height : 0; /** * @property {HTMLVideoElement} video - The HTML Video Element that is added to the document. */ this.video = null; if (key === null) { this.createVideoStream(captureAudio, width, height); } else { var _video = this.game.cache.getVideo(key); if (_video.isBlob) { this.createVideoFromBlob(_video.data); } else { this.video = _video.data; } this.width = this.video.videoWidth; this.height = this.video.videoHeight; } /** * @property {PIXI.BaseTexture} baseTexture - The PIXI.BaseTexture. * @default */ this.baseTexture = new PIXI.BaseTexture(this.video); this.baseTexture.forceLoaded(this.width, this.height); /** * @property {PIXI.Texture} texture - The PIXI.Texture. * @default */ this.texture = new PIXI.Texture(this.baseTexture); /** * @property {Phaser.Frame} textureFrame - The Frame this video uses for rendering. * @default */ this.textureFrame = new Phaser.Frame(0, 0, 0, this.width, this.height, 'video'); this.texture.setFrame(this.textureFrame); /** * @property {number} type - The const type of this object. * @default */ this.type = Phaser.VIDEO; /** * @property {boolean} disableTextureUpload - If true this video will never send its image data to the GPU when its dirty flag is true. This only applies in WebGL. */ this.disableTextureUpload = false; /** * @property {Phaser.Signal} onPlay - This signal is dispatched when the Video starts to play. It sends 3 parameters: a reference to the Video object, if the video is set to loop or not and the playback rate. */ this.onPlay = new Phaser.Signal(); /** * @property {Phaser.Signal} onChangeSource - This signal is dispatched if the Video source is changed. It sends 3 parameters: a reference to the Video object and the new width and height of the new video source. */ this.onChangeSource = new Phaser.Signal(); /** * @property {Phaser.Signal} onComplete - This signal is dispatched when the Video completes playback, i.e. enters an 'ended' state. Videos set to loop will never dispatch this signal. */ this.onComplete = new Phaser.Signal(); /** * @property {Phaser.Signal} onAccess - This signal is dispatched if the user allows access to their webcam. */ this.onAccess = new Phaser.Signal(); /** * @property {Phaser.Signal} onError - This signal is dispatched if an error occurs either getting permission to use the webcam (for a Video Stream) or when trying to play back a video file. */ this.onError = new Phaser.Signal(); /** * @property {boolean} touchLocked - true if this video is currently locked awaiting a touch event. This happens on some mobile devices, such as iOS. * @default */ this.touchLocked = false; /** * @property {MediaStream} videoStream - The Video Stream data. Only set if this Video is streaming from the webcam via `createVideoStream`. */ this.videoStream = null; /** * A snapshot grabbed from the video. This is initially black. Populate it by calling Video.grab(). * When called the BitmapData is updated with a grab taken from the current video playing or active video stream. * If Phaser has been compiled without BitmapData support this property will always be `null`. * * @property {Phaser.BitmapData} snapshot * @readOnly */ this.snapshot = null; if (Phaser.BitmapData) { this.snapshot = new Phaser.BitmapData(this.game, '', this.width, this.height); } /** * @property {boolean} _codeMuted - Internal mute tracking var. * @private * @default */ this._codeMuted = false; /** * @property {boolean} _muted - Internal mute tracking var. * @private * @default */ this._muted = false; /** * @property {boolean} _codePaused - Internal paused tracking var. * @private * @default */ this._codePaused = false; /** * @property {boolean} _paused - Internal paused tracking var. * @private * @default */ this._paused = false; /** * @property {boolean} _pending - Internal var tracking play pending. * @private * @default */ this._pending = false; /** * @property {boolean} _autoplay - Internal var tracking autoplay when changing source. * @private * @default */ this._autoplay = false; if (!this.game.device.cocoonJS && this.game.device.iOS || (window['PhaserGlobal'] && window['PhaserGlobal'].fakeiOSTouchLock)) { this.setTouchLock(); } else { if (_video) { _video.locked = false; } } }; Phaser.Video.prototype = { /** * Instead of playing a video file this method allows you to stream video data from an attached webcam. * * As soon as this method is called the user will be prompted by their browser to "Allow" access to the webcam. * If they allow it the webcam feed is directed to this Video. Call `Video.play` to start the stream. * * If they block the webcam the onError signal will be dispatched containing the NavigatorUserMediaError event. * * You can optionally set a width and height for the stream. If set the input will be cropped to these dimensions. * If not given then as soon as the stream has enough data the video dimensions will be changed to match the webcam device. * You can listen for this with the onChangeSource signal. * * @method Phaser.Video#createVideoStream * @param {boolean} [captureAudio=false] - Controls if audio should be captured along with video in the video stream. * @param {integer} [width] - The width is used to create the video stream. If not provided the video width will be set to the width of the webcam input source. * @param {integer} [height] - The height is used to create the video stream. If not provided the video height will be set to the height of the webcam input source. * @return {Phaser.Video} This Video object for method chaining. */ createVideoStream: function (captureAudio, width, height) { if (typeof captureAudio === 'undefined') { captureAudio = false; } if (typeof width === 'undefined') { width = null; } if (typeof height === 'undefined') { height = null; } if (!this.game.device.getUserMedia) { return false; } this.video = document.createElement("video"); this.video.controls = false; this.video.autoplay = false; if (width !== null) { this.video.width = width; } if (height !== null) { this.video.height = height; } if (!navigator['getUserMedia']) { navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; } var _this = this; var _streamStart = function(stream) { _this.videoStream = stream; _this.video.src = window.URL.createObjectURL(stream); _this.video.addEventListener('loadeddata', function (event) { _this.updateTexture(event, width, height); }, true); _this.onAccess.dispatch(_this); }; var _streamError = function(e) { _this.onError.dispatch(_this, e); }; navigator.getUserMedia({ audio: captureAudio, video: true }, _streamStart, _streamError); return this; }, /** * Creates a new Video element from the given Blob. The Blob must contain the video data in the correct encoded format. * This method is typically called by the Phaser.Loader and Phaser.Cache for you, but is exposed publicly for convenience. * * @method Phaser.Video#createVideoFromBlob * @param {Blob} blob - The Blob containing the video data: `Blob([new Uint8Array(data)])` * @return {Phaser.Video} This Video object for method chaining. */ createVideoFromBlob: function (blob) { this.video = document.createElement("video"); this.video.controls = false; this.video.autoplay = false; this.video.src = window.URL.createObjectURL(blob); this.updateTexture(); return this; }, /** * Called automatically if the video source changes and updates the internal texture dimensions. * Then dispatches the onChangeSource signal. * * @method Phaser.Video#updateTexture * @param {object} [event] - The event which triggered the texture update. * @param {integer} [width] - The new width of the video. If undefined `video.videoWidth` is used. * @param {integer} [height] - The new height of the video. If undefined `video.videoHeight` is used. */ updateTexture: function (event, width, height) { var change = false; if (typeof width === 'undefined' || width === null) { width = this.video.videoWidth; change = true; } if (typeof height === 'undefined' || height === null) { height = this.video.videoHeight; } // if (change) // { // console.log('updateTexture resizing to webcam', width, height); // } // else // { // console.log('updateTexture resizing to fixed dimensions', width, height); // } this.width = width; this.height = height; this.baseTexture.forceLoaded(width, height); this.texture.frame.resize(width, height); this.texture.width = width; this.texture.height = height; if (this.snapshot) { this.snapshot.resize(width, height); } if (change) { this.video.removeEventListener('canplaythrough', this.updateTexture.bind(this)); this.onChangeSource.dispatch(this, width, height); if (this._autoplay) { this.video.play(); this.onPlay.dispatch(this, this.loop, this.playbackRate); } } }, /** * Called when the video completes playback (reaches and ended state). * Dispatches the Video.onComplete signal. * * @method Phaser.Video#complete */ complete: function () { this.onComplete.dispatch(this); }, /** * Starts this video playing if it's not already doing so. * * @method Phaser.Video#play * @param {boolean} [loop=false] - Should the video loop automatically when it reaches the end? Please note that at present some browsers (i.e. Chrome) do not support *seamless* video looping. * @param {number} [playbackRate=1] - The playback rate of the video. 1 is normal speed, 2 is x2 speed, and so on. You cannot set a negative playback rate. * @return {Phaser.Video} This Video object for method chaining. */ play: function (loop, playbackRate) { if (typeof loop === 'undefined') { loop = false; } if (typeof playbackRate === 'undefined') { playbackRate = 1; } if (this.game.sound.onMute) { this.game.sound.onMute.add(this.setMute, this); this.game.sound.onUnMute.add(this.unsetMute, this); if (this.game.sound.mute) { this.setMute(); } } this.game.onPause.add(this.setPause, this); this.game.onResume.add(this.setResume, this); this.video.addEventListener('ended', this.complete.bind(this), true); if (loop) { this.video.loop = 'loop'; } else { this.video.loop = ''; } this.video.playbackRate = playbackRate; if (this.touchLocked) { this._pending = true; } else { this._pending = false; this.video.play(); this.onPlay.dispatch(this, loop, playbackRate); } return this; }, /** * Stops the video playing. * * This removes all locally set signals. * * If you only wish to pause playback of the video, to resume at a later time, use `Video.paused = true` instead. * If the video hasn't finished downloading calling `Video.stop` will not abort the download. To do that you need to * call `Video.destroy` instead. * * If you are using a video stream from a webcam then calling Stop will disconnect the MediaStream session and disable the webcam. * * @method Phaser.Video#stop * @return {Phaser.Video} This Video object for method chaining. */ stop: function () { if (this.game.sound.onMute) { this.game.sound.onMute.remove(this.setMute, this); this.game.sound.onUnMute.remove(this.unsetMute, this); } this.game.onPause.remove(this.setPause, this); this.game.onResume.remove(this.setResume, this); // Stream or file? if (this.videoStream) { if (this.video.mozSrcObject) { this.video.mozSrcObject.stop(); this.video.src = null; } else { this.video.src = ""; this.videoStream.stop(); } this.videoStream = null; } else { this.video.removeEventListener('ended', this.complete.bind(this)); if (this.touchLocked) { this._pending = false; } else { this.video.pause(); } } return this; }, /** * Updates the given Display Objects so they use this Video as their texture. * This will replace any texture they will currently have set. * * @method Phaser.Video#add * @param {Phaser.Sprite|Phaser.Sprite[]|Phaser.Image|Phaser.Image[]} object - Either a single Sprite/Image or an Array of Sprites/Images. * @return {Phaser.Video} This Video object for method chaining. */ add: function (object) { if (Array.isArray(object)) { for (var i = 0; i < object.length; i++) { if (object[i]['loadTexture']) { object[i].loadTexture(this); } } } else { object.loadTexture(this); } return this; }, /** * Creates a new Phaser.Image object, assigns this Video to be its texture, adds it to the world then returns it. * * @method Phaser.Video#addToWorld * @param {number} [x=0] - The x coordinate to place the Image at. * @param {number} [y=0] - The y coordinate to place the Image at. * @param {number} [anchorX=0] - Set the x anchor point of the Image. A value between 0 and 1, where 0 is the top-left and 1 is bottom-right. * @param {number} [anchorY=0] - Set the y anchor point of the Image. A value between 0 and 1, where 0 is the top-left and 1 is bottom-right. * @param {number} [scaleX=1] - The horizontal scale factor of the Image. A value of 1 means no scaling. 2 would be twice the size, and so on. * @param {number} [scaleY=1] - The vertical scale factor of the Image. A value of 1 means no scaling. 2 would be twice the size, and so on. * @return {Phaser.Image} The newly added Image object. */ addToWorld: function (x, y, anchorX, anchorY, scaleX, scaleY) { scaleX = scaleX || 1; scaleY = scaleY || 1; var image = this.game.add.image(x, y, this); image.anchor.set(anchorX, anchorY); image.scale.set(scaleX, scaleY); return image; }, /** * If the game is running in WebGL this will push the texture up to the GPU if it's dirty. * This is called automatically if the Video is being used by a Sprite, otherwise you need to remember to call it in your render function. * If you wish to suppress this functionality set Video.disableTextureUpload to `true`. * * @method Phaser.Video#render */ render: function () { if (!this.disableTextureUpload && this.playing) { this.baseTexture.dirty(); } }, /** * Internal handler called automatically by the Video.mute setter. * * @method Phaser.Video#setMute * @private */ setMute: function () { if (this._muted) { return; } this._muted = true; this.video.muted = true; }, /** * Internal handler called automatically by the Video.mute setter. * * @method Phaser.Video#unsetMute * @private */ unsetMute: function () { if (!this._muted || this._codeMuted) { return; } this._muted = false; this.video.muted = false; }, /** * Internal handler called automatically by the Video.paused setter. * * @method Phaser.Video#setPause * @private */ setPause: function () { if (this._paused || this.touchLocked) { return; } this._paused = true; this.video.pause(); }, /** * Internal handler called automatically by the Video.paused setter. * * @method Phaser.Video#setResume * @private */ setResume: function () { if (!this._paused || this._codePaused || this.touchLocked) { return; } this._paused = false; if (!this.video.ended) { this.video.play(); } }, /** * On some mobile browsers you cannot play a video until the user has explicitly touched the video to allow it. * Phaser handles this via the `setTouchLock` method. However if you have 3 different videos, maybe an "Intro", "Start" and "Game Over" * split into three different Video objects, then you will need the user to touch-unlock every single one of them. * * You can avoid this by using just one Video object and simply changing the video source. Once a Video element is unlocked it remains * unlocked, even if the source changes. So you can use this to your benefit to avoid forcing the user to 'touch' the video yet again. * * As you'd expect there are limitations. So far we've found that the videos need to be in the same encoding format and bitrate. * This method will automatically handle a change in video dimensions, but if you try swapping to a different bitrate we've found it * cannot render the new video on iOS (desktop browsers cope better). * * When the video source is changed the video file is requested over the network. Listen for the `onChangeSource` signal to know * when the new video has downloaded enough content to be able to be played. Previous settings such as the volume and loop state * are adopted automatically by the new video. * * @method Phaser.Video#changeSource * @param {string} src - The new URL to change the video.src to. * @param {boolean} [autoplay=true] - Should the video play automatically after the source has been updated? * @return {Phaser.Video} This Video object for method chaining. */ changeSource: function (src, autoplay) { if (typeof autoplay === 'undefined') { autoplay = true; } this.video.pause(); this.video.addEventListener('canplaythrough', this.updateTexture.bind(this), true); this.video.src = src; this.video.load(); this._autoplay = autoplay; if (!autoplay) { this.paused = true; } return this; }, /** * Sets the Input Manager touch callback to be Video.unlock. * Required for iOS video unlocking. Mostly just used internally. * * @method Phaser.Video#setTouchLock */ setTouchLock: function () { this.game.input.touch.addTouchLockCallback(this.unlock, this); this.touchLocked = true; }, /** * Enables the video on mobile devices, usually after the first touch. * If the SoundManager hasn't been unlocked then this will automatically unlock that as well. * Only one video can be pending unlock at any one time. * * @method Phaser.Video#unlock */ unlock: function () { this.touchLocked = false; this.video.play(); this.onPlay.dispatch(this, this.loop, this.playbackRate); var _video = this.game.cache.getVideo(this.key); if (!_video.isBlob) { _video.locked = false; } return true; }, /** * Grabs the current frame from the Video or Video Stream and renders it to the Video.snapshot BitmapData. * * You can optionally set if the BitmapData should be cleared or not, the alpha and the blend mode of the draw. * * If you need more advanced control over the grabbing them call `Video.snapshot.copy` directly with the same parameters as BitmapData.copy. * * @method Phaser.Video#grab * @param {boolean} [clear=false] - Should the BitmapData be cleared before the Video is grabbed? Unless you are using alpha or a blend mode you can usually leave this set to false. * @param {number} [alpha=1] - The alpha that will be set on the video before drawing. A value between 0 (fully transparent) and 1, opaque. * @param {string} [blendMode=null] - The composite blend mode that will be used when drawing. The default is no blend mode at all. This is a Canvas globalCompositeOperation value such as 'lighter' or 'xor'. * @return {Phaser.BitmapData} A reference to the Video.snapshot BitmapData object for further method chaining. */ grab: function (clear, alpha, blendMode) { if (typeof clear === 'undefined') { clear = false; } if (typeof alpha === 'undefined') { alpha = 1; } if (typeof blendMode === 'undefined') { blendMode = null; } if (this.snapshot === null) { console.warn('Video.grab cannot run because Phaser.BitmapData is unavailable'); return; } if (clear) { this.snapshot.cls(); } this.snapshot.copy(this.video, 0, 0, this.width, this.height, 0, 0, this.width, this.height, 0, 0, 0, 1, 1, alpha, blendMode); return this.snapshot; }, /** * Destroys the Video object. This calls Video.stop(), then sets the Video src to a blank string and nulls the reference. * If any Sprites are using this Video as their texture it is up to you to manage those. * * @method Phaser.Video#destroy */ destroy: function () { this.stop(); this.video.src = ''; this.video = null; if (this.touchLocked) { this.game.input.touch.removeTouchLockCallback(this.unlock, this); } } }; /** * @memberof Phaser.Video * @property {number} currentTime - The current time of the video in seconds. If set the video will attempt to seek to that point in time. */ Object.defineProperty(Phaser.Video.prototype, "currentTime", { get: function () { return this.video.currentTime; }, set: function (value) { this.video.currentTime = value; } }); /** * @memberof Phaser.Video * @property {number} duration - The duration of the video in seconds. * @readOnly */ Object.defineProperty(Phaser.Video.prototype, "duration", { get: function () { return this.video.duration; } }); /** * @memberof Phaser.Video * @property {number} progress - The progress of this video. This is a value between 0 and 1, where 0 is the start and 1 is the end of the video. * @readOnly */ Object.defineProperty(Phaser.Video.prototype, "progress", { get: function () { return (this.video.currentTime / this.video.duration); } }); /** * @name Phaser.Video#mute * @property {boolean} mute - Gets or sets the muted state of the Video. */ Object.defineProperty(Phaser.Video.prototype, "mute", { get: function () { return this._muted; }, set: function (value) { value = value || null; if (value) { if (this._muted) { return; } this._codeMuted = true; this.setMute(); } else { if (!this._muted) { return; } this._codeMuted = false; this.unsetMute(); } } }); /** * Gets or sets the paused state of the Video. * If the video is still touch locked (such as on iOS devices) this call has no effect. * * @name Phaser.Video#paused * @property {boolean} paused */ Object.defineProperty(Phaser.Video.prototype, "paused", { get: function () { return this._paused; }, set: function (value) { value = value || null; if (this.touchLocked) { return; } if (value) { if (this._paused) { return; } this._codePaused = true; this.setPause(); } else { if (!this._paused) { return; } this._codePaused = false; this.setResume(); } } }); /** * @name Phaser.Video#volume * @property {number} volume - Gets or sets the volume of the Video, a value between 0 and 1. The value given is clamped to the range 0 to 1. */ Object.defineProperty(Phaser.Video.prototype, "volume", { get: function () { return this.video.volume; }, set: function (value) { if (value < 0) { value = 0; } else if (value > 1) { value = 1; } this.video.volume = value; } }); /** * @name Phaser.Video#playbackRate * @property {number} playbackRate - Gets or sets the playback rate of the Video. This is the speed at which the video is playing. */ Object.defineProperty(Phaser.Video.prototype, "playbackRate", { get: function () { return this.video.playbackRate; }, set: function (value) { this.video.playbackRate = value; } }); /** * Gets or sets if the Video is set to loop. * Please note that at present some browsers (i.e. Chrome) do not support *seamless* video looping. * * @name Phaser.Video#loop * @property {number} loop */ Object.defineProperty(Phaser.Video.prototype, "loop", { get: function () { return this.video.loop; }, set: function (value) { if (value) { this.video.loop = 'loop'; } else { this.video.loop = ''; } } }); /** * @name Phaser.Video#playing * @property {boolean} playing - True if the video is currently playing (and not paused or ended), otherwise false. * @readOnly */ Object.defineProperty(Phaser.Video.prototype, "playing", { get: function () { return !(this.video.paused && this.video.ended); } }); Phaser.Video.prototype.constructor = Phaser.Video;