import BaseTexture from './BaseTexture';
import { uid, BaseTextureCache } from '../utils';
import { shared } from '../ticker';
import { UPDATE_PRIORITY } from '../const';
import determineCrossOrigin from '../../../utils/determineCrossOrigin';
/**
* A texture of a [playing] Video.
*
* Video base textures mimic Tiny BaseTexture.from.... method in their creation process.
*
* This can be used in several ways, such as:
*
* ```js
* let texture = Tiny.VideoBaseTexture.fromUrl('http://mydomain.com/video.mp4');
*
* let texture = Tiny.VideoBaseTexture.fromUrl({ src: 'http://mydomain.com/video.mp4', mime: 'video/mp4' });
*
* let texture = Tiny.VideoBaseTexture.fromUrls(['/video.webm', '/video.mp4']);
*
* let texture = Tiny.VideoBaseTexture.fromUrls([
* { src: '/video.webm', mime: 'video/webm' },
* { src: '/video.mp4', mime: 'video/mp4' }
* ]);
* ```
*
* @class
* @extends Tiny.BaseTexture
* @memberof Tiny
*/
export default class VideoBaseTexture extends BaseTexture {
/**
* @param {HTMLVideoElement} source - Video source
* @param {number} [scaleMode=Tiny.settings.SCALE_MODE] - See {@link Tiny.SCALE_MODES} for possible values
* @param {boolean} [autoPlay=true] - Start playing video as soon as it is loaded
*/
constructor(source, scaleMode, autoPlay = true) {
if (!source) {
throw new Error('No video source element specified.');
}
// hook in here to check if video is already available.
// BaseTexture looks for a source.complete boolean, plus width & height.
if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) && source.width && source.height) {
source.complete = true;
}
super(source, scaleMode);
this.width = source.videoWidth;
this.height = source.videoHeight;
this._autoUpdate = true;
this._isAutoUpdating = false;
/**
* When set to true will automatically play videos used by this texture once they are loaded. If false, it will not modify the playing state.
*
* @member {boolean}
* @default true
*/
this.autoPlay = autoPlay;
this.update = this.update.bind(this);
this._onCanPlay = this._onCanPlay.bind(this);
source.addEventListener('play', this._onPlayStart.bind(this));
source.addEventListener('pause', this._onPlayStop.bind(this));
this.hasLoaded = false;
this.__loaded = false;
if (!this._isSourceReady()) {
source.addEventListener('canplay', this._onCanPlay);
source.addEventListener('canplaythrough', this._onCanPlay);
} else {
this._onCanPlay();
}
}
/**
* Returns true if the underlying source is playing.
*
* @private
* @return {boolean} True if playing.
*/
_isSourcePlaying() {
const source = this.source;
return (source.currentTime > 0 && source.paused === false && source.ended === false && source.readyState > 2);
}
/**
* Returns true if the underlying source is ready for playing.
*
* @private
* @return {boolean} True if ready.
*/
_isSourceReady() {
return this.source.readyState === 3 || this.source.readyState === 4;
}
/**
* Runs the update loop when the video is ready to play
*
* @private
*/
_onPlayStart() {
// Just in case the video has not received its can play even yet..
if (!this.hasLoaded) {
this._onCanPlay();
}
if (!this._isAutoUpdating && this.autoUpdate) {
shared.add(this.update, this, UPDATE_PRIORITY.HIGH);
this._isAutoUpdating = true;
}
}
/**
* Fired when a pause event is triggered, stops the update loop
*
* @private
*/
_onPlayStop() {
if (this._isAutoUpdating) {
shared.remove(this.update, this);
this._isAutoUpdating = false;
}
}
/**
* Fired when the video is loaded and ready to play
*
* @private
*/
_onCanPlay() {
this.hasLoaded = true;
if (this.source) {
this.source.removeEventListener('canplay', this._onCanPlay);
this.source.removeEventListener('canplaythrough', this._onCanPlay);
this.width = this.source.videoWidth;
this.height = this.source.videoHeight;
// prevent multiple loaded dispatches..
if (!this.__loaded) {
this.__loaded = true;
this.emit('loaded', this);
}
if (this._isSourcePlaying()) {
this._onPlayStart();
} else if (this.autoPlay) {
this.source.play();
}
}
}
/**
* Destroys this texture
*
*/
destroy() {
if (this._isAutoUpdating) {
shared.remove(this.update, this);
}
if (this.source && this.source._tinyId) {
BaseTexture.removeFromCache(this.source._tinyId);
delete this.source._tinyId;
this.source.pause();
this.source.src = '';
this.source.load();
}
super.destroy();
}
/**
* Mimic Tiny BaseTexture.from.... method.
*
* @static
* @param {HTMLVideoElement} video - Video to create texture from
* @param {number} [scaleMode=Tiny.settings.SCALE_MODE] - See {@link Tiny.SCALE_MODES} for possible values
* @param {boolean} [autoPlay=true] - Start playing video as soon as it is loaded
* @return {Tiny.VideoBaseTexture} Newly created VideoBaseTexture
*/
static fromVideo(video, scaleMode, autoPlay) {
if (!video._tinyId) {
video._tinyId = `video_${uid()}`;
}
let baseTexture = BaseTextureCache[video._tinyId];
if (!baseTexture) {
baseTexture = new VideoBaseTexture(video, scaleMode, autoPlay);
BaseTexture.addToCache(baseTexture, video._tinyId);
}
return baseTexture;
}
/**
* Helper function that creates a new BaseTexture based on the given video element.
* This BaseTexture can then be used to create a texture
*
* @static
* @param {string|object|string[]|object[]} videoSrc - The URL(s) for the video.
* @param {string} [videoSrc.src] - One of the source urls for the video
* @param {string} [videoSrc.mime] - The mimetype of the video (e.g. 'video/mp4'). If not specified
* the url's extension will be used as the second part of the mime type.
* @param {number} scaleMode - See {@link Tiny.SCALE_MODES} for possible values
* @param {boolean} [crossorigin=(auto)] - Should use anonymous CORS? Defaults to true if the URL is not a data-URI.
* @param {boolean} [autoPlay=true] - Start playing video as soon as it is loaded
* @return {Tiny.VideoBaseTexture} Newly created VideoBaseTexture
*/
static fromUrl(videoSrc, scaleMode, crossorigin, autoPlay) {
const video = document.createElement('video');
video.setAttribute('webkit-playsinline', '');
video.setAttribute('playsinline', '');
const url = Array.isArray(videoSrc) ? (videoSrc[0].src || videoSrc[0]) : (videoSrc.src || videoSrc);
if (crossorigin === undefined && url.indexOf('data:') !== 0) {
video.crossOrigin = determineCrossOrigin(url);
} else if (crossorigin) {
video.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous';
}
// array of objects or strings
if (Array.isArray(videoSrc)) {
for (let i = 0; i < videoSrc.length; ++i) {
video.appendChild(createSource(videoSrc[i].src || videoSrc[i], videoSrc[i].mime));
}
} else { // single object or string
video.appendChild(createSource(url, videoSrc.mime));
}
video.load();
return VideoBaseTexture.fromVideo(video, scaleMode, autoPlay);
}
/**
* Should the base texture automatically update itself, set to true by default
*
* @member {boolean}
*/
get autoUpdate() {
return this._autoUpdate;
}
set autoUpdate(value) {
if (value !== this._autoUpdate) {
this._autoUpdate = value;
if (!this._autoUpdate && this._isAutoUpdating) {
shared.remove(this.update, this);
this._isAutoUpdating = false;
} else if (this._autoUpdate && !this._isAutoUpdating) {
shared.add(this.update, this, UPDATE_PRIORITY.HIGH);
this._isAutoUpdating = true;
}
}
}
}
VideoBaseTexture.fromUrls = VideoBaseTexture.fromUrl;
function createSource(path, type) {
if (!type) {
const purePath = path.split('?').shift().toLowerCase();
type = `video/${purePath.substr(purePath.lastIndexOf('.') + 1)}`;
}
const source = document.createElement('source');
source.src = path;
source.type = type;
return source;
}