pixi.js
Version:
<p align="center"> <a href="https://pixijs.com" target="_blank" rel="noopener noreferrer"> <img height="150" src="https://files.pixijs.download/branding/pixijs-logo-transparent-dark.svg?v=1" alt="PixiJS logo"> </a> </p> <br/> <p align="center">
328 lines (324 loc) • 11.2 kB
JavaScript
'use strict';
var Extensions = require('../../../../../extensions/Extensions.js');
var Ticker = require('../../../../../ticker/Ticker.js');
var detectVideoAlphaMode = require('../../../../../utils/browser/detectVideoAlphaMode.js');
var TextureSource = require('./TextureSource.js');
"use strict";
const _VideoSource = class _VideoSource extends TextureSource.TextureSource {
constructor(options) {
super(options);
// Public
/** Whether or not the video is ready to play. */
this.isReady = false;
/** The upload method for this texture. */
this.uploadMethodId = "video";
options = {
..._VideoSource.defaultOptions,
...options
};
this._autoUpdate = true;
this._isConnectedToTicker = false;
this._updateFPS = options.updateFPS || 0;
this._msToNextUpdate = 0;
this.autoPlay = options.autoPlay !== false;
this.alphaMode = options.alphaMode ?? "premultiply-alpha-on-upload";
this._videoFrameRequestCallback = this._videoFrameRequestCallback.bind(this);
this._videoFrameRequestCallbackHandle = null;
this._load = null;
this._resolve = null;
this._reject = null;
this._onCanPlay = this._onCanPlay.bind(this);
this._onCanPlayThrough = this._onCanPlayThrough.bind(this);
this._onError = this._onError.bind(this);
this._onPlayStart = this._onPlayStart.bind(this);
this._onPlayStop = this._onPlayStop.bind(this);
this._onSeeked = this._onSeeked.bind(this);
if (options.autoLoad !== false) {
void this.load();
}
}
/** Update the video frame if the source is not destroyed and meets certain conditions. */
updateFrame() {
if (this.destroyed) {
return;
}
if (this._updateFPS) {
const elapsedMS = Ticker.Ticker.shared.elapsedMS * this.resource.playbackRate;
this._msToNextUpdate = Math.floor(this._msToNextUpdate - elapsedMS);
}
if (!this._updateFPS || this._msToNextUpdate <= 0) {
this._msToNextUpdate = this._updateFPS ? Math.floor(1e3 / this._updateFPS) : 0;
}
if (this.isValid) {
this.update();
}
}
/** Callback to update the video frame and potentially request the next frame update. */
_videoFrameRequestCallback() {
this.updateFrame();
if (this.destroyed) {
this._videoFrameRequestCallbackHandle = null;
} else {
this._videoFrameRequestCallbackHandle = this.resource.requestVideoFrameCallback(
this._videoFrameRequestCallback
);
}
}
/**
* Checks if the resource has valid dimensions.
* @returns {boolean} True if width and height are set, otherwise false.
*/
get isValid() {
return !!this.resource.videoWidth && !!this.resource.videoHeight;
}
/**
* Start preloading the video resource.
* @returns {Promise<this>} Handle the validate event
*/
async load() {
if (this._load) {
return this._load;
}
const source = this.resource;
const options = this.options;
if ((source.readyState === source.HAVE_ENOUGH_DATA || source.readyState === source.HAVE_FUTURE_DATA) && source.width && source.height) {
source.complete = true;
}
source.addEventListener("play", this._onPlayStart);
source.addEventListener("pause", this._onPlayStop);
source.addEventListener("seeked", this._onSeeked);
if (!this._isSourceReady()) {
if (!options.preload) {
source.addEventListener("canplay", this._onCanPlay);
}
source.addEventListener("canplaythrough", this._onCanPlayThrough);
source.addEventListener("error", this._onError, true);
} else {
this._mediaReady();
}
this.alphaMode = await detectVideoAlphaMode.detectVideoAlphaMode();
this._load = new Promise((resolve, reject) => {
if (this.isValid) {
resolve(this);
} else {
this._resolve = resolve;
this._reject = reject;
if (options.preloadTimeoutMs !== void 0) {
this._preloadTimeout = setTimeout(() => {
this._onError(new ErrorEvent(`Preload exceeded timeout of ${options.preloadTimeoutMs}ms`));
});
}
source.load();
}
});
return this._load;
}
/**
* Handle video error events.
* @param event - The error event
*/
_onError(event) {
this.resource.removeEventListener("error", this._onError, true);
this.emit("error", event);
if (this._reject) {
this._reject(event);
this._reject = null;
this._resolve = null;
}
}
/**
* Checks if the underlying source is playing.
* @returns True if playing.
*/
_isSourcePlaying() {
const source = this.resource;
return !source.paused && !source.ended;
}
/**
* Checks if the underlying source is ready for playing.
* @returns True if ready.
*/
_isSourceReady() {
const source = this.resource;
return source.readyState > 2;
}
/** Runs the update loop when the video is ready to play. */
_onPlayStart() {
if (!this.isValid) {
this._mediaReady();
}
this._configureAutoUpdate();
}
/** Stops the update loop when a pause event is triggered. */
_onPlayStop() {
this._configureAutoUpdate();
}
/** Handles behavior when the video completes seeking to the current playback position. */
_onSeeked() {
if (this._autoUpdate && !this._isSourcePlaying()) {
this._msToNextUpdate = 0;
this.updateFrame();
this._msToNextUpdate = 0;
}
}
_onCanPlay() {
const source = this.resource;
source.removeEventListener("canplay", this._onCanPlay);
this._mediaReady();
}
_onCanPlayThrough() {
const source = this.resource;
source.removeEventListener("canplaythrough", this._onCanPlay);
if (this._preloadTimeout) {
clearTimeout(this._preloadTimeout);
this._preloadTimeout = void 0;
}
this._mediaReady();
}
/** Fired when the video is loaded and ready to play. */
_mediaReady() {
const source = this.resource;
if (this.isValid) {
this.isReady = true;
this.resize(source.videoWidth, source.videoHeight);
}
this._msToNextUpdate = 0;
this.updateFrame();
this._msToNextUpdate = 0;
if (this._resolve) {
this._resolve(this);
this._resolve = null;
this._reject = null;
}
if (this._isSourcePlaying()) {
this._onPlayStart();
} else if (this.autoPlay) {
void this.resource.play();
}
}
/** Cleans up resources and event listeners associated with this texture. */
destroy() {
this._configureAutoUpdate();
const source = this.resource;
if (source) {
source.removeEventListener("play", this._onPlayStart);
source.removeEventListener("pause", this._onPlayStop);
source.removeEventListener("seeked", this._onSeeked);
source.removeEventListener("canplay", this._onCanPlay);
source.removeEventListener("canplaythrough", this._onCanPlayThrough);
source.removeEventListener("error", this._onError, true);
source.pause();
source.src = "";
source.load();
}
super.destroy();
}
/** Should the base texture automatically update itself, set to true by default. */
get autoUpdate() {
return this._autoUpdate;
}
set autoUpdate(value) {
if (value !== this._autoUpdate) {
this._autoUpdate = value;
this._configureAutoUpdate();
}
}
/**
* How many times a second to update the texture from the video.
* Leave at 0 to update at every render.
* A lower fps can help performance, as updating the texture at 60fps on a 30ps video may not be efficient.
*/
get updateFPS() {
return this._updateFPS;
}
set updateFPS(value) {
if (value !== this._updateFPS) {
this._updateFPS = value;
this._configureAutoUpdate();
}
}
/**
* Configures the updating mechanism based on the current state and settings.
*
* This method decides between using the browser's native video frame callback or a custom ticker
* for updating the video frame. It ensures optimal performance and responsiveness
* based on the video's state, playback status, and the desired frames-per-second setting.
*
* - If `_autoUpdate` is enabled and the video source is playing:
* - It will prefer the native video frame callback if available and no specific FPS is set.
* - Otherwise, it will use a custom ticker for manual updates.
* - If `_autoUpdate` is disabled or the video isn't playing, any active update mechanisms are halted.
*/
_configureAutoUpdate() {
if (this._autoUpdate && this._isSourcePlaying()) {
if (!this._updateFPS && this.resource.requestVideoFrameCallback) {
if (this._isConnectedToTicker) {
Ticker.Ticker.shared.remove(this.updateFrame, this);
this._isConnectedToTicker = false;
this._msToNextUpdate = 0;
}
if (this._videoFrameRequestCallbackHandle === null) {
this._videoFrameRequestCallbackHandle = this.resource.requestVideoFrameCallback(
this._videoFrameRequestCallback
);
}
} else {
if (this._videoFrameRequestCallbackHandle !== null) {
this.resource.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle);
this._videoFrameRequestCallbackHandle = null;
}
if (!this._isConnectedToTicker) {
Ticker.Ticker.shared.add(this.updateFrame, this);
this._isConnectedToTicker = true;
this._msToNextUpdate = 0;
}
}
} else {
if (this._videoFrameRequestCallbackHandle !== null) {
this.resource.cancelVideoFrameCallback(this._videoFrameRequestCallbackHandle);
this._videoFrameRequestCallbackHandle = null;
}
if (this._isConnectedToTicker) {
Ticker.Ticker.shared.remove(this.updateFrame, this);
this._isConnectedToTicker = false;
this._msToNextUpdate = 0;
}
}
}
static test(resource) {
return globalThis.HTMLVideoElement && resource instanceof HTMLVideoElement;
}
};
_VideoSource.extension = Extensions.ExtensionType.TextureSource;
/** The default options for video sources. */
_VideoSource.defaultOptions = {
...TextureSource.TextureSource.defaultOptions,
/** If true, the video will start loading immediately. */
autoLoad: true,
/** If true, the video will start playing as soon as it is loaded. */
autoPlay: true,
/** The number of times a second to update the texture from the video. Leave at 0 to update at every render. */
updateFPS: 0,
/** If true, the video will be loaded with the `crossorigin` attribute. */
crossorigin: true,
/** If true, the video will loop when it ends. */
loop: false,
/** If true, the video will be muted. */
muted: true,
/** If true, the video will play inline. */
playsinline: true,
/** If true, the video will be preloaded. */
preload: false
};
/**
* Map of video MIME types that can't be directly derived from file extensions.
* @readonly
*/
_VideoSource.MIME_TYPES = {
ogv: "video/ogg",
mov: "video/quicktime",
m4v: "video/mp4"
};
let VideoSource = _VideoSource;
exports.VideoSource = VideoSource;
//# sourceMappingURL=VideoSource.js.map