Source: tiny/core/textures/VideoBaseTexture.js

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;
}
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:26 GMT+0800 (CST)