phaser
Version:
A fast, free and fun HTML5 Game Framework for Desktop and Mobile web browsers from the team at Phaser Studio Inc.
1,452 lines (1,277 loc) • 76.8 kB
JavaScript
/**
* @author Richard Davey <rich@phaser.io>
* @copyright 2013-2025 Phaser Studio Inc.
* @license {@link https://opensource.org/licenses/MIT|MIT License}
*/
var Clamp = require('../../math/Clamp');
var Class = require('../../utils/Class');
var Components = require('../components');
var Events = require('../events');
var GameEvents = require('../../core/events/');
var GameObject = require('../GameObject');
var MATH_CONST = require('../../math/const');
var SoundEvents = require('../../sound/events/');
var UUID = require('../../utils/string/UUID');
var VideoRender = require('./VideoRender');
/**
* @classdesc
* A Video Game Object.
*
* This Game Object is capable of handling playback of a video file, video stream or media stream.
*
* You can optionally 'preload' the video into the Phaser Video Cache:
*
* ```javascript
* preload () {
* this.load.video('ripley', 'assets/aliens.mp4');
* }
*
* create () {
* this.add.video(400, 300, 'ripley');
* }
* ```
*
* You don't have to 'preload' the video. You can also play it directly from a URL:
*
* ```javascript
* create () {
* this.add.video(400, 300).loadURL('assets/aliens.mp4');
* }
* ```
*
* To all intents and purposes, a video is a standard Game Object, just like a Sprite. And as such, you can do
* all the usual things to it, such as scaling, rotating, cropping, tinting, making interactive, giving a
* physics body, etc.
*
* Transparent videos are also possible via the WebM file format. Providing the video file has was encoded with
* an alpha channel, and providing the browser supports WebM playback (not all of them do), then it will render
* in-game with full transparency.
*
* Playback is handled entirely via the Request Video Frame API, which is supported by most modern browsers.
* A polyfill is provided for older browsers.
*
* ### Autoplaying Videos
*
* Videos can only autoplay if the browser has been unlocked with an interaction, or satisfies the MEI settings.
* The policies that control autoplaying are vast and vary between browser. You can, and should, read more about
* it here: https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
*
* If your video doesn't contain any audio, then set the `noAudio` parameter to `true` when the video is _loaded_,
* and it will often allow the video to play immediately:
*
* ```javascript
* preload () {
* this.load.video('pixar', 'nemo.mp4', true);
* }
* ```
*
* The 3rd parameter in the load call tells Phaser that the video doesn't contain any audio tracks. Video without
* audio can autoplay without requiring a user interaction. Video with audio cannot do this unless it satisfies
* the browsers MEI settings. See the MDN Autoplay Guide for further details.
*
* Or:
*
* ```javascript
* create () {
* this.add.video(400, 300).loadURL('assets/aliens.mp4', true);
* }
* ```
*
* You can set the `noAudio` parameter to `true` even if the video does contain audio. It will still allow the video
* to play immediately, but the audio will not start.
*
* Note that due to a bug in IE11 you cannot play a video texture to a Sprite in WebGL. For IE11 force Canvas mode.
*
* More details about video playback and the supported media formats can be found on MDN:
*
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement
* https://developer.mozilla.org/en-US/docs/Web/Media/Formats
*
* @class Video
* @extends Phaser.GameObjects.GameObject
* @memberof Phaser.GameObjects
* @constructor
* @since 3.20.0
*
* @extends Phaser.GameObjects.Components.Alpha
* @extends Phaser.GameObjects.Components.BlendMode
* @extends Phaser.GameObjects.Components.ComputedSize
* @extends Phaser.GameObjects.Components.Depth
* @extends Phaser.GameObjects.Components.Flip
* @extends Phaser.GameObjects.Components.GetBounds
* @extends Phaser.GameObjects.Components.Mask
* @extends Phaser.GameObjects.Components.Origin
* @extends Phaser.GameObjects.Components.Pipeline
* @extends Phaser.GameObjects.Components.PostPipeline
* @extends Phaser.GameObjects.Components.ScrollFactor
* @extends Phaser.GameObjects.Components.TextureCrop
* @extends Phaser.GameObjects.Components.Tint
* @extends Phaser.GameObjects.Components.Transform
* @extends Phaser.GameObjects.Components.Visible
*
* @param {Phaser.Scene} scene - The Scene to which this Game Object belongs. A Game Object can only belong to one Scene at a time.
* @param {number} x - The horizontal position of this Game Object in the world.
* @param {number} y - The vertical position of this Game Object in the world.
* @param {string} [key] - Optional key of the Video this Game Object will play, as stored in the Video Cache.
*/
var Video = new Class({
Extends: GameObject,
Mixins: [
Components.Alpha,
Components.BlendMode,
Components.ComputedSize,
Components.Depth,
Components.Flip,
Components.GetBounds,
Components.Mask,
Components.Origin,
Components.Pipeline,
Components.PostPipeline,
Components.ScrollFactor,
Components.TextureCrop,
Components.Tint,
Components.Transform,
Components.Visible,
VideoRender
],
initialize:
function Video (scene, x, y, key)
{
GameObject.call(this, scene, 'Video');
/**
* A reference to the HTML Video Element this Video Game Object is playing.
*
* Will be `undefined` until a video is loaded for playback.
*
* @name Phaser.GameObjects.Video#video
* @type {?HTMLVideoElement}
* @since 3.20.0
*/
this.video;
/**
* The Phaser Texture this Game Object is using to render the video to.
*
* Will be `undefined` until a video is loaded for playback.
*
* @name Phaser.GameObjects.Video#videoTexture
* @type {?Phaser.Textures.Texture}
* @since 3.20.0
*/
this.videoTexture;
/**
* A reference to the TextureSource backing the `videoTexture` Texture object.
*
* Will be `undefined` until a video is loaded for playback.
*
* @name Phaser.GameObjects.Video#videoTextureSource
* @type {?Phaser.Textures.TextureSource}
* @since 3.20.0
*/
this.videoTextureSource;
/**
* A Phaser `CanvasTexture` instance that holds the most recent snapshot taken from the video.
*
* This will only be set if the `snapshot` or `snapshotArea` methods have been called.
*
* Until those methods are called, this property will be `undefined`.
*
* @name Phaser.GameObjects.Video#snapshotTexture
* @type {?Phaser.Textures.CanvasTexture}
* @since 3.20.0
*/
this.snapshotTexture;
/**
* If you have saved this video to a texture via the `saveTexture` method, this controls if the video
* is rendered with `flipY` in WebGL or not. You often need to set this if you wish to use the video texture
* as the input source for a shader. If you find your video is appearing upside down within a shader or
* custom pipeline, flip this property.
*
* @name Phaser.GameObjects.Video#flipY
* @type {boolean}
* @since 3.20.0
*/
this.flipY = false;
/**
* The key used by the texture as stored in the Texture Manager.
*
* @name Phaser.GameObjects.Video#_key
* @type {string}
* @private
* @since 3.20.0
*/
this._key = UUID();
/**
* An internal flag holding the current state of the video lock, should document interaction be required
* before playback can begin.
*
* @name Phaser.GameObjects.Video#touchLocked
* @type {boolean}
* @readonly
* @since 3.20.0
*/
this.touchLocked = false;
/**
* Should the video auto play when document interaction is required and happens?
*
* @name Phaser.GameObjects.Video#playWhenUnlocked
* @type {boolean}
* @since 3.20.0
*/
this.playWhenUnlocked = false;
/**
* Has the video created its texture and populated it with the first frame of video?
*
* @name Phaser.GameObjects.Video#frameReady
* @type {boolean}
* @since 3.60.0
*/
this.frameReady = false;
/**
* This read-only property returns `true` if the video is currently stalled, i.e. it has stopped
* playing due to a lack of data, or too much data, but hasn't yet reached the end of the video.
*
* This is set if the Video DOM element emits any of the following events:
*
* `stalled`
* `suspend`
* `waiting`
*
* And is cleared if the Video DOM element emits the `playing` event, or handles
* a requestVideoFrame call.
*
* Listen for the Phaser Event `VIDEO_STALLED` to be notified and inspect the event
* to see which DOM event caused it.
*
* Note that being stalled isn't always a negative thing. A video can be stalled if it
* has downloaded enough data in to its buffer to not need to download any more until
* the current batch of frames have rendered.
*
* @name Phaser.GameObjects.Video#isStalled
* @type {boolean}
* @readonly
* @since 3.60.0
*/
this.isStalled = false;
/**
* Records the number of times the video has failed to play,
* typically because the user hasn't interacted with the page yet.
*
* @name Phaser.GameObjects.Video#failedPlayAttempts
* @type {number}
* @since 3.60.0
*/
this.failedPlayAttempts = 0;
/**
* If the browser supports the Request Video Frame API then this
* property will hold the metadata that is returned from
* the callback each time it is invoked.
*
* See https://wicg.github.io/video-rvfc/#video-frame-metadata-callback
* for a complete list of all properties that will be in this object.
* Likely of most interest is the `mediaTime` property:
*
* The media presentation timestamp (PTS) in seconds of the frame presented
* (e.g. its timestamp on the video.currentTime timeline). MAY have a zero
* value for live-streams or WebRTC applications.
*
* If the browser doesn't support the API then this property will be undefined.
*
* @name Phaser.GameObjects.Video#metadata
* @type {VideoFrameCallbackMetadata}
* @since 3.60.0
*/
this.metadata;
/**
* The current retry elapsed time.
*
* @name Phaser.GameObjects.Video#retry
* @type {number}
* @since 3.20.0
*/
this.retry = 0;
/**
* If a video fails to play due to a lack of user interaction, this is the
* amount of time, in ms, that the video will wait before trying again to
* play. The default is 500ms.
*
* @name Phaser.GameObjects.Video#retryInterval
* @type {number}
* @since 3.20.0
*/
this.retryInterval = 500;
/**
* The video was muted due to a system event, such as the game losing focus.
*
* @name Phaser.GameObjects.Video#_systemMuted
* @type {boolean}
* @private
* @since 3.20.0
*/
this._systemMuted = false;
/**
* The video was muted due to game code, not a system event.
*
* @name Phaser.GameObjects.Video#_codeMuted
* @type {boolean}
* @private
* @since 3.20.0
*/
this._codeMuted = false;
/**
* The video was paused due to a system event, such as the game losing focus.
*
* @name Phaser.GameObjects.Video#_systemPaused
* @type {boolean}
* @private
* @since 3.20.0
*/
this._systemPaused = false;
/**
* The video was paused due to game code, not a system event.
*
* @name Phaser.GameObjects.Video#_codePaused
* @type {boolean}
* @private
* @since 3.20.0
*/
this._codePaused = false;
/**
* The locally bound event callback handlers.
*
* @name Phaser.GameObjects.Video#_callbacks
* @type {any}
* @private
* @since 3.20.0
*/
this._callbacks = {
ended: this.completeHandler.bind(this),
legacy: this.legacyPlayHandler.bind(this),
playing: this.playingHandler.bind(this),
seeked: this.seekedHandler.bind(this),
seeking: this.seekingHandler.bind(this),
stalled: this.stalledHandler.bind(this),
suspend: this.stalledHandler.bind(this),
waiting: this.stalledHandler.bind(this)
};
/**
* The locally bound callback handler specifically for load and load error events.
*
* @name Phaser.GameObjects.Video#_loadCallbackHandler
* @type {function}
* @private
* @since 3.60.0
*/
this._loadCallbackHandler = this.loadErrorHandler.bind(this);
/**
* The locally bound callback handler specifically for the loadedmetadata event.
*
* @name Phaser.GameObjects.Video#_metadataCallbackHandler
* @type {function}
* @private
* @since 3.80.0
*/
this._metadataCallbackHandler = this.metadataHandler.bind(this);
/**
* The internal crop data object, as used by `setCrop` and passed to the `Frame.setCropUVs` method.
*
* @name Phaser.GameObjects.Video#_crop
* @type {object}
* @private
* @since 3.20.0
*/
this._crop = this.resetCropObject();
/**
* An object containing in and out markers for sequence playback.
*
* @name Phaser.GameObjects.Video#markers
* @type {any}
* @since 3.20.0
*/
this.markers = {};
/**
* The in marker.
*
* @name Phaser.GameObjects.Video#_markerIn
* @type {number}
* @private
* @since 3.20.0
*/
this._markerIn = 0;
/**
* The out marker.
*
* @name Phaser.GameObjects.Video#_markerOut
* @type {number}
* @private
* @since 3.20.0
*/
this._markerOut = 0;
/**
* Are we playing a marked segment of the video?
*
* @name Phaser.GameObjects.Video#_playingMarker
* @type {boolean}
* @private
* @since 3.60.0
*/
this._playingMarker = false;
/**
* The previous frames mediaTime.
*
* @name Phaser.GameObjects.Video#_lastUpdate
* @type {number}
* @private
* @since 3.60.0
*/
this._lastUpdate = 0;
/**
* The key of the current video as stored in the Video cache.
*
* If the video did not come from the cache this will be an empty string.
*
* @name Phaser.GameObjects.Video#cacheKey
* @type {string}
* @readonly
* @since 3.60.0
*/
this.cacheKey = '';
/**
* Is the video currently seeking?
*
* This is set to `true` when the `seeking` event is fired,
* and set to `false` when the `seeked` event is fired.
*
* @name Phaser.GameObjects.Video#isSeeking
* @type {boolean}
* @readonly
* @since 3.60.0
*/
this.isSeeking = false;
/**
* Has Video.play been called? This is reset if a new Video is loaded.
*
* @name Phaser.GameObjects.Video#_playCalled
* @type {boolean}
* @private
* @since 3.60.0
*/
this._playCalled = false;
/**
* Has Video.getFirstFrame been called? This is reset if a new Video is loaded or played.
*
* @name Phaser.GameObjects.Video#_getFrame
* @type {boolean}
* @private
* @since 3.85.0
*/
this._getFrame = false;
/**
* The Callback ID returned by Request Video Frame.
*
* @name Phaser.GameObjects.Video#_rfvCallbackId
* @type {number}
* @private
* @since 3.60.0
*/
this._rfvCallbackId = 0;
var game = scene.sys.game;
/**
* A reference to Device.Video.
*
* @name Phaser.GameObjects.Video#_device
* @type {string[]}
* @private
* @since 3.60.0
*/
this._device = game.device.video;
this.setPosition(x, y);
this.setSize(256, 256);
this.initPipeline();
this.initPostPipeline(true);
game.events.on(GameEvents.PAUSE, this.globalPause, this);
game.events.on(GameEvents.RESUME, this.globalResume, this);
var sound = scene.sys.sound;
if (sound)
{
sound.on(SoundEvents.GLOBAL_MUTE, this.globalMute, this);
}
if (key)
{
this.load(key);
}
},
// Overrides Game Object method
addedToScene: function ()
{
this.scene.sys.updateList.add(this);
},
// Overrides Game Object method
removedFromScene: function ()
{
this.scene.sys.updateList.remove(this);
},
/**
* Loads a Video from the Video Cache, ready for playback with the `Video.play` method.
*
* If a video is already playing, this method allows you to change the source of the current video element.
* It works by first stopping the current video and then starts playback of the new source through the existing video element.
*
* The reason you may wish to do this is because videos that require interaction to unlock, remain in an unlocked
* state, even if you change the source of the video. By changing the source to a new video you avoid having to
* go through the unlock process again.
*
* @method Phaser.GameObjects.Video#load
* @since 3.60.0
*
* @param {string} key - The key of the Video this Game Object will play, as stored in the Video Cache.
*
* @return {this} This Video Game Object for method chaining.
*/
load: function (key)
{
var video = this.scene.sys.cache.video.get(key);
if (video)
{
this.cacheKey = key;
this.loadHandler(video.url, video.noAudio, video.crossOrigin);
}
else
{
console.warn('No video in cache for key: ' + key);
}
return this;
},
/**
* This method allows you to change the source of the current video element. It works by first stopping the
* current video, if playing. Then deleting the video texture, if one has been created. Finally, it makes a
* new video texture and starts playback of the new source through the existing video element.
*
* The reason you may wish to do this is because videos that require interaction to unlock, remain in an unlocked
* state, even if you change the source of the video. By changing the source to a new video you avoid having to
* go through the unlock process again.
*
* @method Phaser.GameObjects.Video#changeSource
* @since 3.20.0
*
* @param {string} key - The key of the Video this Game Object will swap to playing, as stored in the Video Cache.
* @param {boolean} [autoplay=true] - Should the video start playing immediately, once the swap is complete?
* @param {boolean} [loop=false] - Should the video loop automatically when it reaches the end? Please note that not all browsers support _seamless_ video looping for all encoding formats.
* @param {number} [markerIn] - Optional in marker time, in seconds, for playback of a sequence of the video.
* @param {number} [markerOut] - Optional out marker time, in seconds, for playback of a sequence of the video.
*
* @return {this} This Video Game Object for method chaining.
*/
changeSource: function (key, autoplay, loop, markerIn, markerOut)
{
if (autoplay === undefined) { autoplay = true; }
if (loop === undefined) { loop = false; }
if (this.cacheKey !== key)
{
this.load(key);
if (autoplay)
{
this.play(loop, markerIn, markerOut);
}
}
},
/**
* Returns the key of the currently played video, as stored in the Video Cache.
*
* If the video did not come from the cache this will return an empty string.
*
* @method Phaser.GameObjects.Video#getVideoKey
* @since 3.20.0
*
* @return {string} The key of the video being played from the Video Cache, if any.
*/
getVideoKey: function ()
{
return this.cacheKey;
},
/**
* Loads a Video from the given URL, ready for playback with the `Video.play` method.
*
* If a video is already playing, this method allows you to change the source of the current video element.
* It works by first stopping the current video and then starts playback of the new source through the existing video element.
*
* The reason you may wish to do this is because videos that require interaction to unlock, remain in an unlocked
* state, even if you change the source of the video. By changing the source to a new video you avoid having to
* go through the unlock process again.
*
* @method Phaser.GameObjects.Video#loadURL
* @since 3.60.0
*
* @param {(string|string[]|Phaser.Types.Loader.FileTypes.VideoFileURLConfig|Phaser.Types.Loader.FileTypes.VideoFileURLConfig[])} [urls] - The absolute or relative URL to load the video files from.
* @param {boolean} [noAudio=false] - Does the video have an audio track? If not you can enable auto-playing on it.
* @param {string} [crossOrigin] - The value to use for the `crossOrigin` property in the video load request. Either undefined, `anonymous` or `use-credentials`. If no value is given, `crossorigin` will not be set in the request.
*
* @return {this} This Video Game Object for method chaining.
*/
loadURL: function (urls, noAudio, crossOrigin)
{
if (noAudio === undefined) { noAudio = false; }
var urlConfig = this._device.getVideoURL(urls);
if (!urlConfig)
{
console.warn('No supported video format found for ' + urls);
}
else
{
this.cacheKey = '';
this.loadHandler(urlConfig.url, noAudio, crossOrigin);
}
return this;
},
/**
* Loads a Video from the given MediaStream object, ready for playback with the `Video.play` method.
*
* @method Phaser.GameObjects.Video#loadMediaStream
* @since 3.50.0
*
* @param {MediaStream} stream - The MediaStream object.
* @param {boolean} [noAudio=false] - Does the video have an audio track? If not you can enable auto-playing on it.
* @param {string} [crossOrigin] - The value to use for the `crossOrigin` property in the video load request. Either undefined, `anonymous` or `use-credentials`. If no value is given, `crossorigin` will not be set in the request.
*
* @return {this} This Video Game Object for method chaining.
*/
loadMediaStream: function (stream, noAudio, crossOrigin)
{
return this.loadHandler(null, noAudio, crossOrigin, stream);
},
/**
* Internal method that loads a Video from the given URL, ready for playback with the
* `Video.play` method.
*
* Normally you don't call this method directly, but instead use the `Video.loadURL` method,
* or the `Video.load` method if you have preloaded the video.
*
* Calling this method will skip checking if the browser supports the given format in
* the URL, where-as the other two methods enforce these checks.
*
* @method Phaser.GameObjects.Video#loadHandler
* @since 3.60.0
*
* @param {string} [url] - The absolute or relative URL to load the video file from. Set to `null` if passing in a MediaStream object.
* @param {boolean} [noAudio] - Does the video have an audio track? If not you can enable auto-playing on it.
* @param {string} [crossOrigin] - The value to use for the `crossOrigin` property in the video load request. Either undefined, `anonymous` or `use-credentials`. If no value is given, `crossorigin` will not be set in the request.
* @param {string} [stream] - A MediaStream object if this is playing a stream instead of a file.
*
* @return {this} This Video Game Object for method chaining.
*/
loadHandler: function (url, noAudio, crossOrigin, stream)
{
if (!noAudio) { noAudio = false; }
var video = this.video;
if (video)
{
// Re-use the existing video element
this.removeLoadEventHandlers();
this.stop();
}
else
{
video = document.createElement('video');
video.controls = false;
video.setAttribute('playsinline', 'playsinline');
video.setAttribute('preload', 'auto');
video.setAttribute('disablePictureInPicture', 'true');
}
if (noAudio)
{
video.muted = true;
video.defaultMuted = true;
video.setAttribute('autoplay', 'autoplay');
}
else
{
video.muted = false;
video.defaultMuted = false;
video.removeAttribute('autoplay');
}
if (!crossOrigin)
{
video.removeAttribute('crossorigin');
}
else
{
video.setAttribute('crossorigin', crossOrigin);
}
if (stream)
{
if ('srcObject' in video)
{
try
{
video.srcObject = stream;
}
catch (err)
{
if (err.name !== 'TypeError')
{
throw err;
}
video.src = URL.createObjectURL(stream);
}
}
else
{
video.src = URL.createObjectURL(stream);
}
}
else
{
video.src = url;
}
this.retry = 0;
this.video = video;
this._playCalled = false;
video.load();
this.addLoadEventHandlers();
var texture = this.scene.sys.textures.get(this._key);
this.setTexture(texture);
return this;
},
/**
* This method handles the Request Video Frame callback.
*
* It is called by the browser when a new video frame is ready to be displayed.
*
* It's also responsible for the creation of the video texture, if it doesn't
* already exist. If it does, it updates the texture as required.
*
* For more details about the Request Video Frame callback, see:
* https://web.dev/requestvideoframecallback-rvfc
*
* @method Phaser.GameObjects.Video#requestVideoFrame
* @fires Phaser.GameObjects.Events#VIDEO_CREATED
* @fires Phaser.GameObjects.Events#VIDEO_LOOP
* @fires Phaser.GameObjects.Events#VIDEO_COMPLETE
* @fires Phaser.GameObjects.Events#VIDEO_PLAY
* @fires Phaser.GameObjects.Events#VIDEO_TEXTURE
* @since 3.60.0
*
* @param {DOMHighResTimeStamp} now - The current time in milliseconds.
* @param {VideoFrameCallbackMetadata} metadata - Useful metadata about the video frame that was most recently presented for composition. See https://wicg.github.io/video-rvfc/#video-frame-metadata-callback
*/
requestVideoFrame: function (now, metadata)
{
var video = this.video;
if (!video)
{
return;
}
var width = metadata.width;
var height = metadata.height;
var texture = this.videoTexture;
var textureSource = this.videoTextureSource;
var newVideo = (!texture || textureSource.source !== video);
if (newVideo)
{
// First frame of a new video
this._codePaused = video.paused;
this._codeMuted = video.muted;
if (!texture)
{
texture = this.scene.sys.textures.create(this._key, video, width, height);
texture.add('__BASE', 0, 0, 0, width, height);
this.setTexture(texture);
this.videoTexture = texture;
this.videoTextureSource = texture.source[0];
this.videoTextureSource.setFlipY(this.flipY);
this.emit(Events.VIDEO_TEXTURE, this, texture);
}
else
{
// Re-use the existing texture
textureSource.source = video;
textureSource.width = width;
textureSource.height = height;
// Resize base frame
texture.get().setSize(width, height);
}
this.setSizeToFrame();
this.updateDisplayOrigin();
}
else
{
textureSource.update();
}
this.isStalled = false;
this.metadata = metadata;
var currentTime = metadata.mediaTime;
if (newVideo)
{
this._lastUpdate = currentTime;
this.emit(Events.VIDEO_CREATED, this, width, height);
if (!this.frameReady)
{
this.frameReady = true;
this.emit(Events.VIDEO_PLAY, this);
}
}
if (this._playingMarker)
{
if (currentTime >= this._markerOut)
{
if (video.loop)
{
video.currentTime = this._markerIn;
this.emit(Events.VIDEO_LOOP, this);
}
else
{
this.stop(false);
this.emit(Events.VIDEO_COMPLETE, this);
}
}
}
else if (currentTime < this._lastUpdate)
{
this.emit(Events.VIDEO_LOOP, this);
}
this._lastUpdate = currentTime;
if (this._getFrame)
{
this.removeEventHandlers();
video.pause();
this._getFrame = false;
}
else
{
this._rfvCallbackId = this.video.requestVideoFrameCallback(this.requestVideoFrame.bind(this));
}
},
/**
* Starts this video playing.
*
* If the video is already playing, or has been queued to play with `changeSource` then this method just returns.
*
* Videos can only autoplay if the browser has been unlocked. This happens if you have interacted with the browser, i.e.
* by clicking on it or pressing a key, or due to server settings. The policies that control autoplaying are vast and
* vary between browser. You can read more here: https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
*
* If your video doesn't contain any audio, then set the `noAudio` parameter to `true` when the video is loaded,
* and it will often allow the video to play immediately:
*
* ```javascript
* preload () {
* this.load.video('pixar', 'nemo.mp4', true);
* }
* ```
*
* The 3rd parameter in the load call tells Phaser that the video doesn't contain any audio tracks. Video without
* audio can autoplay without requiring a user interaction. Video with audio cannot do this unless it satisfies
* the browsers MEI settings. See the MDN Autoplay Guide for details.
*
* If you need audio in your videos, then you'll have to consider the fact that the video cannot start playing until the
* user has interacted with the browser, into your game flow.
*
* @method Phaser.GameObjects.Video#play
* @since 3.20.0
*
* @param {boolean} [loop=false] - Should the video loop automatically when it reaches the end? Please note that not all browsers support _seamless_ video looping for all encoding formats.
* @param {number} [markerIn] - Optional in marker time, in seconds, for playback of a sequence of the video.
* @param {number} [markerOut] - Optional out marker time, in seconds, for playback of a sequence of the video.
*
* @return {this} This Video Game Object for method chaining.
*/
play: function (loop, markerIn, markerOut)
{
if (markerIn === undefined) { markerIn = -1; }
if (markerOut === undefined) { markerOut = MATH_CONST.MAX_SAFE_INTEGER; }
var video = this.video;
if (!video || this.isPlaying())
{
if (!video)
{
console.warn('Video not loaded');
}
return this;
}
// We can reset these each time play is called, even if the video hasn't started yet
if (loop === undefined) { loop = video.loop; }
video.loop = loop;
this._markerIn = markerIn;
this._markerOut = markerOut;
this._playingMarker = (markerIn > -1 && markerOut > markerIn && markerOut < MATH_CONST.MAX_SAFE_INTEGER);
// But we go no further if play has already been called
if (!this._playCalled)
{
this._getFrame = false;
this._rfvCallbackId = video.requestVideoFrameCallback(this.requestVideoFrame.bind(this));
this._playCalled = true;
this.createPlayPromise();
}
return this;
},
/**
* Attempts to get the first frame of the video by running the `requestVideoFrame` callback once,
* then stopping. This is useful if you need to grab the first frame of the video to display behind
* a 'play' button, without actually calling the 'play' method.
*
* If the video is already playing, or has been queued to play with `changeSource` then this method just returns.
*
* @method Phaser.GameObjects.Video#getFirstFrame
* @since 3.85.0
*
* @return {this} This Video Game Object for method chaining.
*/
getFirstFrame: function ()
{
var video = this.video;
if (!video || this.isPlaying())
{
if (!video)
{
console.warn('Video not loaded');
}
return this;
}
if (!this._playCalled)
{
this._getFrame = true;
this._rfvCallbackId = video.requestVideoFrameCallback(this.requestVideoFrame.bind(this));
this.createPlayPromise();
}
return this;
},
/**
* Adds the loading specific event handlers to the video element.
*
* @method Phaser.GameObjects.Video#addLoadEventHandlers
* @since 3.60.0
*/
addLoadEventHandlers: function ()
{
var video = this.video;
if (video)
{
video.addEventListener('error', this._loadCallbackHandler);
video.addEventListener('abort', this._loadCallbackHandler);
video.addEventListener('loadedmetadata', this._metadataCallbackHandler);
}
},
/**
* Removes the loading specific event handlers from the video element.
*
* @method Phaser.GameObjects.Video#removeLoadEventHandlers
* @since 3.60.0
*/
removeLoadEventHandlers: function ()
{
var video = this.video;
if (video)
{
video.removeEventListener('error', this._loadCallbackHandler);
video.removeEventListener('abort', this._loadCallbackHandler);
}
},
/**
* Adds the playback specific event handlers to the video element.
*
* @method Phaser.GameObjects.Video#addEventHandlers
* @since 3.60.0
*/
addEventHandlers: function ()
{
var video = this.video;
// Set these _after_ calling `video.play` or they don't fire
// (really useful, thanks browsers!)
if (video)
{
var callbacks = this._callbacks;
for (var callback in callbacks)
{
video.addEventListener(callback, callbacks[callback]);
}
}
},
/**
* Removes the playback specific event handlers from the video element.
*
* @method Phaser.GameObjects.Video#removeEventHandlers
* @since 3.60.0
*/
removeEventHandlers: function ()
{
var video = this.video;
if (video)
{
var callbacks = this._callbacks;
for (var callback in callbacks)
{
video.removeEventListener(callback, callbacks[callback]);
}
}
},
/**
* Creates the video.play promise and adds the success and error handlers to it.
*
* Not all browsers support the video.play promise, so this method will fall back to
* the old-school way of handling the video.play call.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play#browser_compatibility for details.
*
* @method Phaser.GameObjects.Video#createPlayPromise
* @since 3.60.0
*
* @param {boolean} [catchError=true] - Should the error be caught and the video marked as failed to play?
*/
createPlayPromise: function (catchError)
{
if (catchError === undefined) { catchError = true; }
var video = this.video;
var playPromise = video.play();
if (playPromise !== undefined)
{
var success = this.playSuccess.bind(this);
var error = this.playError.bind(this);
if (!catchError)
{
var _this = this;
error = function ()
{
_this.failedPlayAttempts++;
};
}
playPromise.then(success).catch(error);
}
else
{
// Old-school fallback here for pre-2019 browsers
video.addEventListener('playing', this._callbacks.legacy);
if (!catchError)
{
this.failedPlayAttempts++;
}
}
},
/**
* Adds a sequence marker to this video.
*
* Markers allow you to split a video up into sequences, delineated by a start and end time, given in seconds.
*
* You can then play back specific markers via the `playMarker` method.
*
* Note that marker timing is _not_ frame-perfect. You should construct your videos in such a way that you allow for
* plenty of extra padding before and after each sequence to allow for discrepancies in browser seek and currentTime accuracy.
*
* See https://github.com/w3c/media-and-entertainment/issues/4 for more details about this issue.
*
* @method Phaser.GameObjects.Video#addMarker
* @since 3.20.0
*
* @param {string} key - A unique name to give this marker.
* @param {number} markerIn - The time, in seconds, representing the start of this marker.
* @param {number} markerOut - The time, in seconds, representing the end of this marker.
*
* @return {this} This Video Game Object for method chaining.
*/
addMarker: function (key, markerIn, markerOut)
{
if (!isNaN(markerIn) && markerIn >= 0 && !isNaN(markerOut) && markerOut > markerIn)
{
this.markers[key] = [ markerIn, markerOut ];
}
return this;
},
/**
* Plays a pre-defined sequence in this video.
*
* Markers allow you to split a video up into sequences, delineated by a start and end time, given in seconds and
* specified via the `addMarker` method.
*
* Note that marker timing is _not_ frame-perfect. You should construct your videos in such a way that you allow for
* plenty of extra padding before and after each sequence to allow for discrepancies in browser seek and currentTime accuracy.
*
* See https://github.com/w3c/media-and-entertainment/issues/4 for more details about this issue.
*
* @method Phaser.GameObjects.Video#playMarker
* @since 3.20.0
*
* @param {string} key - The name of the marker sequence to play.
* @param {boolean} [loop=false] - Should the video loop automatically when it reaches the end? Please note that not all browsers support _seamless_ video looping for all encoding formats.
*
* @return {this} This Video Game Object for method chaining.
*/
playMarker: function (key, loop)
{
var marker = this.markers[key];
if (marker)
{
this.play(loop, marker[0], marker[1]);
}
return this;
},
/**
* Removes a previously set marker from this video.
*
* If the marker is currently playing it will _not_ stop playback.
*
* @method Phaser.GameObjects.Video#removeMarker
* @since 3.20.0
*
* @param {string} key - The name of the marker to remove.
*
* @return {this} This Video Game Object for method chaining.
*/
removeMarker: function (key)
{
delete this.markers[key];
return this;
},
/**
* Takes a snapshot of the current frame of the video and renders it to a CanvasTexture object,
* which is then returned. You can optionally resize the grab by passing a width and height.
*
* This method returns a reference to the `Video.snapshotTexture` object. Calling this method
* multiple times will overwrite the previous snapshot with the most recent one.
*
* @method Phaser.GameObjects.Video#snapshot
* @since 3.20.0
*
* @param {number} [width] - The width of the resulting CanvasTexture.
* @param {number} [height] - The height of the resulting CanvasTexture.
*
* @return {Phaser.Textures.CanvasTexture}
*/
snapshot: function (width, height)
{
if (width === undefined) { width = this.width; }
if (height === undefined) { height = this.height; }
return this.snapshotArea(0, 0, this.width, this.height, width, height);
},
/**
* Takes a snapshot of the specified area of the current frame of the video and renders it to a CanvasTexture object,
* which is then returned. You can optionally resize the grab by passing a different `destWidth` and `destHeight`.
*
* This method returns a reference to the `Video.snapshotTexture` object. Calling this method
* multiple times will overwrite the previous snapshot with the most recent one.
*
* @method Phaser.GameObjects.Video#snapshotArea
* @since 3.20.0
*
* @param {number} [x=0] - The horizontal location of the top-left of the area to grab from.
* @param {number} [y=0] - The vertical location of the top-left of the area to grab from.
* @param {number} [srcWidth] - The width of area to grab from the video. If not given it will grab the full video dimensions.
* @param {number} [srcHeight] - The height of area to grab from the video. If not given it will grab the full video dimensions.
* @param {number} [destWidth] - The destination width of the grab, allowing you to resize it.
* @param {number} [destHeight] - The destination height of the grab, allowing you to resize it.
*
* @return {Phaser.Textures.CanvasTexture}
*/
snapshotArea: function (x, y, srcWidth, srcHeight, destWidth, destHeight)
{
if (x === undefined) { x = 0; }
if (y === undefined) { y = 0; }
if (srcWidth === undefined) { srcWidth = this.width; }
if (srcHeight === undefined) { srcHeight = this.height; }
if (destWidth === undefined) { destWidth = srcWidth; }
if (destHeight === undefined) { destHeight = srcHeight; }
var video = this.video;
var snap = this.snapshotTexture;
if (!snap)
{
snap = this.scene.sys.textures.createCanvas(UUID(), destWidth, destHeight);
this.snapshotTexture = snap;
if (video)
{
snap.context.drawImage(video, x, y, srcWidth, srcHeight, 0, 0, destWidth, destHeight);
}
}
else
{
snap.setSize(destWidth, destHeight);
if (video)
{
snap.context.drawImage(video, x, y, srcWidth, srcHeight, 0, 0, destWidth, destHeight);
}
}
return snap.update();
},
/**
* Stores a copy of this Videos `snapshotTexture` in the Texture Manager using the given key.
*
* This texture is created when the `snapshot` or `snapshotArea` methods are called.
*
* After doing this, any texture based Game Object, such as a Sprite, can use the contents of the
* snapshot by using the texture key:
*
* ```javascript
* var vid = this.add.video(0, 0, 'intro');
*
* vid.snapshot();
*
* vid.saveSnapshotTexture('doodle');
*
* this.add.image(400, 300, 'doodle');
* ```
*
* Updating the contents of the `snapshotTexture`, for example by calling `snapshot` again,
* will automatically update _any_ Game Object that is using it as a texture.
* Calling `saveSnapshotTexture` again will not save another copy of the same texture,
* it will just rename the existing one.
*
* By default it will create a single base texture. You can add frames to the texture
* by using the `Texture.add` method. After doing this, you can then allow Game Objects
* to use a specific frame.
*
* @method Phaser.GameObjects.Video#saveSnapshotTexture
* @since 3.20.0
*
* @param {string} key - The unique key to store the texture as within the global Texture Manager.
*
* @return {Phaser.Textures.CanvasTexture} The Texture that was saved.
*/
saveSnapshotTexture: function (key)
{
if (this.snapshotTexture)
{
this.scene.sys.textures.renameTexture(this.snapshotTexture.key, key);
}
else
{
this.snapshotTexture = this.scene.sys.textures.createCanvas(key, this.width, this.height);
}
return this.snapshotTexture;
},
/**
* This internal method is called automatically if the playback Promise resolves successfully.
*
* @method Phaser.GameObjects.Video#playSuccess
* @fires Phaser.GameObjects.Events#VIDEO_UNLOCKED
* @since 3.60.0
*/
playSuccess: function ()
{
if (!this._playCalled)
{
// The stop method has been called but the Promise has resolved
// after this, so we need to just abort.
return;
}
this.addEventHandlers();
this._codePaused = false;
if (this.touchLocked)
{
this.touchLocked = false;
this.emit(Events.VIDEO_UNLOCKED, this);
}
var sound = this.scene.sys.sound;
if (sound && sound.mute)
{
// Mute will be set based on the global mute state of the Sound Manager (if there is one)
this.setMute(true);
}
if (this._markerIn > -1)
{
this.video.currentTime = this._markerIn;
}
},
/**
* This internal method is called automatically if the playback Promise fails to resolve.
*
* @method Phaser.GameObjects.Video#playError
* @fires Phaser.GameObjects.Events#VIDEO_ERROR
* @fires Phaser.GameObjects.Events#VIDEO_UNSUPPORTED
* @fires Phas