|
@@ -1,44 +1,127 @@
|
|
|
module BABYLON {
|
|
|
+ /**
|
|
|
+ * Settings for finer control over video usage
|
|
|
+ */
|
|
|
+ export interface VideoTextureSettings {
|
|
|
+ /**
|
|
|
+ * Applies `autoplay` to video, if specified
|
|
|
+ */
|
|
|
+ autoPlay?: boolean;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Applies `loop` to video, if specified
|
|
|
+ */
|
|
|
+ loop?: boolean;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Automatically updates internal texture from video at every frame in the render loop
|
|
|
+ */
|
|
|
+ autoUpdateTexture: boolean;
|
|
|
+ }
|
|
|
+
|
|
|
+ const getName = (src: string | string[] | HTMLVideoElement): string => {
|
|
|
+ if (src instanceof HTMLVideoElement) {
|
|
|
+ return src.currentSrc;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof src === "object") {
|
|
|
+ return src.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ return src;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getVideo = (src: string | string[] | HTMLVideoElement): HTMLVideoElement => {
|
|
|
+ if (src instanceof HTMLVideoElement) {
|
|
|
+ return src;
|
|
|
+ }
|
|
|
+ const video: HTMLVideoElement = document.createElement("video");
|
|
|
+ if (typeof src === "string") {
|
|
|
+ video.src = src;
|
|
|
+ } else {
|
|
|
+ src.forEach(url => {
|
|
|
+ const source = document.createElement("source");
|
|
|
+ source.src = url;
|
|
|
+ video.appendChild(source);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return video;
|
|
|
+ };
|
|
|
+
|
|
|
export class VideoTexture extends Texture {
|
|
|
- public video: HTMLVideoElement;
|
|
|
+ /**
|
|
|
+ * Tells whether textures will be updated automatically or user is required to call `updateTexture` manually
|
|
|
+ */
|
|
|
+ public readonly autoUpdateTexture: boolean;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * The video instance used by the texture internally
|
|
|
+ */
|
|
|
+ public readonly video: HTMLVideoElement;
|
|
|
|
|
|
- private _autoLaunch = true;
|
|
|
- private _lastUpdate: number;
|
|
|
- private _generateMipMaps: boolean
|
|
|
- private _setTextureReady: () => void;
|
|
|
+ private _generateMipMaps: boolean;
|
|
|
private _engine: Engine;
|
|
|
|
|
|
/**
|
|
|
* Creates a video texture.
|
|
|
- * Sample : https://doc.babylonjs.com/tutorials/01._Advanced_Texturing
|
|
|
- * @param {Array} urlsOrVideo can be used to provide an array of urls or an already setup HTML video element.
|
|
|
+ * Sample : https://doc.babylonjs.com/how_to/video_texture
|
|
|
+ * @param {string | null} name optional name, will detect from video source, if not defined
|
|
|
+ * @param {(string | string[] | HTMLVideoElement)} src can be used to provide an url, array of urls or an already setup HTML video element.
|
|
|
* @param {BABYLON.Scene} scene is obviously the current scene.
|
|
|
* @param {boolean} generateMipMaps can be used to turn on mipmaps (Can be expensive for videoTextures because they are often updated).
|
|
|
* @param {boolean} invertY is false by default but can be used to invert video on Y axis
|
|
|
* @param {number} samplingMode controls the sampling method and is set to TRILINEAR_SAMPLINGMODE by default
|
|
|
+ * @param {VideoTextureSettings} [settings] allows finer control over video usage
|
|
|
*/
|
|
|
- constructor(name: string, urlsOrVideo: string[] | HTMLVideoElement, scene: Scene, generateMipMaps = false, invertY = false, samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE) {
|
|
|
+ constructor(
|
|
|
+ name: Nullable<string>,
|
|
|
+ src: string | string[] | HTMLVideoElement,
|
|
|
+ scene: Nullable<Scene>,
|
|
|
+ generateMipMaps = false,
|
|
|
+ invertY = false,
|
|
|
+ samplingMode: number = Texture.TRILINEAR_SAMPLINGMODE,
|
|
|
+ settings: VideoTextureSettings = {
|
|
|
+ autoPlay: true,
|
|
|
+ loop: true,
|
|
|
+ autoUpdateTexture: true,
|
|
|
+ }
|
|
|
+ ) {
|
|
|
super(null, scene, !generateMipMaps, invertY);
|
|
|
|
|
|
- var urls: Nullable<string[]> = null;
|
|
|
- this.name = name;
|
|
|
+ this._engine = this.getScene()!.getEngine();
|
|
|
+ this._generateMipMaps = generateMipMaps;
|
|
|
+ this._samplingMode = samplingMode;
|
|
|
+ this.autoUpdateTexture = settings.autoUpdateTexture;
|
|
|
|
|
|
- if (urlsOrVideo instanceof HTMLVideoElement) {
|
|
|
- this.video = <any>urlsOrVideo;
|
|
|
- } else {
|
|
|
- urls = urlsOrVideo;
|
|
|
+ this.name = name || getName(src);
|
|
|
+ this.video = getVideo(src);
|
|
|
|
|
|
- this.video = document.createElement("video");
|
|
|
- this.video.autoplay = false;
|
|
|
- this.video.loop = true;
|
|
|
- Tools.SetCorsBehavior(urls, this.video);
|
|
|
+ if (settings.autoPlay !== undefined) {
|
|
|
+ this.video.autoplay = settings.autoPlay;
|
|
|
+ }
|
|
|
+ if (settings.loop !== undefined) {
|
|
|
+ this.video.loop = settings.loop;
|
|
|
}
|
|
|
|
|
|
- this._engine = (<Scene>this.getScene()).getEngine();
|
|
|
- this._generateMipMaps = generateMipMaps;
|
|
|
- this._samplingMode = samplingMode;
|
|
|
+ this.video.addEventListener("canplay", this._createInternalTexture);
|
|
|
+ this.video.addEventListener("paused", this._updateInternalTexture);
|
|
|
+ this.video.addEventListener("seeked", this._updateInternalTexture);
|
|
|
+ this.video.addEventListener("emptied", this.reset);
|
|
|
+
|
|
|
+ if (this.video.readyState >= this.video.HAVE_CURRENT_DATA) {
|
|
|
+ this._createInternalTexture();
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (!this._engine.needPOTTextures || (Tools.IsExponentOfTwo(this.video.videoWidth) && Tools.IsExponentOfTwo(this.video.videoHeight))) {
|
|
|
+ private _createInternalTexture = (): void => {
|
|
|
+ if (this._texture != null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ !this._engine.needPOTTextures ||
|
|
|
+ (Tools.IsExponentOfTwo(this.video.videoWidth) && Tools.IsExponentOfTwo(this.video.videoHeight))
|
|
|
+ ) {
|
|
|
this.wrapU = Texture.WRAP_ADDRESSMODE;
|
|
|
this.wrapV = Texture.WRAP_ADDRESSMODE;
|
|
|
} else {
|
|
@@ -47,110 +130,146 @@
|
|
|
this._generateMipMaps = false;
|
|
|
}
|
|
|
|
|
|
- if (urls) {
|
|
|
- this.video.addEventListener("canplay", () => {
|
|
|
- if (this._texture === undefined){
|
|
|
- this._createTexture();
|
|
|
- }
|
|
|
- });
|
|
|
- urls.forEach(url => {
|
|
|
- var source = document.createElement("source");
|
|
|
- source.src = url;
|
|
|
- this.video.appendChild(source);
|
|
|
- });
|
|
|
- } else {
|
|
|
- this._createTexture();
|
|
|
- }
|
|
|
+ this._texture = this._engine.createDynamicTexture(
|
|
|
+ this.video.videoWidth,
|
|
|
+ this.video.videoHeight,
|
|
|
+ this._generateMipMaps,
|
|
|
+ this._samplingMode
|
|
|
+ );
|
|
|
+ this._texture.width;
|
|
|
|
|
|
- this._lastUpdate = Tools.Now;
|
|
|
- }
|
|
|
-
|
|
|
- private __setTextureReady(): void {
|
|
|
- if (this._texture) {
|
|
|
- this._texture.isReady = true;
|
|
|
- }
|
|
|
- }
|
|
|
+ this._updateInternalTexture();
|
|
|
|
|
|
- private _createTexture(): void {
|
|
|
- this._texture = this._engine.createDynamicTexture(this.video.videoWidth, this.video.videoHeight, this._generateMipMaps, this._samplingMode);
|
|
|
+ this._texture.isReady = true;
|
|
|
+ };
|
|
|
|
|
|
- if (this._autoLaunch) {
|
|
|
- this._autoLaunch = false;
|
|
|
- this.video.play();
|
|
|
+ private reset = (): void => {
|
|
|
+ if (this._texture == null) {
|
|
|
+ return;
|
|
|
}
|
|
|
- this._setTextureReady = this.__setTextureReady.bind(this);
|
|
|
- this.video.addEventListener("playing", this._setTextureReady);
|
|
|
- }
|
|
|
-
|
|
|
+ this._texture.dispose();
|
|
|
+ this._texture = null;
|
|
|
+ };
|
|
|
|
|
|
+ /**
|
|
|
+ * Internal method to initiate `update`.
|
|
|
+ */
|
|
|
public _rebuild(): void {
|
|
|
this.update();
|
|
|
}
|
|
|
|
|
|
- public update(): boolean {
|
|
|
- var now = Tools.Now;
|
|
|
+ /**
|
|
|
+ * Update Texture in the `auto` mode. Does not do anything if `settings.autoUpdateTexture` is false.
|
|
|
+ */
|
|
|
+ public update(): void {
|
|
|
+ if (!this.autoUpdateTexture) {
|
|
|
+ // Expecting user to call `updateTexture` manually
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (now - this._lastUpdate < 15 || this.video.readyState !== this.video.HAVE_ENOUGH_DATA) {
|
|
|
- return false;
|
|
|
+ this.updateTexture(true);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Update Texture in `manual` mode. Does not do anything if not visible or paused.
|
|
|
+ * @param isVisible Visibility state, detected by user using `scene.getActiveMeshes()` or othervise.
|
|
|
+ */
|
|
|
+ public updateTexture(isVisible: boolean): void {
|
|
|
+ if (!isVisible) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.video.paused) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this._updateInternalTexture();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected _updateInternalTexture = (e?: Event): void => {
|
|
|
+ if (this._texture == null || !this._texture.isReady) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (this.video.readyState < this.video.HAVE_CURRENT_DATA) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- this._lastUpdate = now;
|
|
|
this._engine.updateVideoTexture(this._texture, this.video, this._invertY);
|
|
|
- return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Change video content. Changing video instance or setting multiple urls (as in constructor) is not supported.
|
|
|
+ * @param url New url.
|
|
|
+ */
|
|
|
+ public updateURL(url: string): void {
|
|
|
+ this.video.src = url;
|
|
|
}
|
|
|
|
|
|
public dispose(): void {
|
|
|
super.dispose();
|
|
|
- this.video.removeEventListener("playing", this._setTextureReady);
|
|
|
+ this.video.removeEventListener("canplay", this._createInternalTexture);
|
|
|
+ this.video.removeEventListener("paused", this._updateInternalTexture);
|
|
|
+ this.video.removeEventListener("seeked", this._updateInternalTexture);
|
|
|
+ this.video.removeEventListener("emptied", this.reset);
|
|
|
}
|
|
|
|
|
|
- public static CreateFromWebCam(scene: Scene, onReady: (videoTexture: VideoTexture) => void, constraints: {
|
|
|
- minWidth: number,
|
|
|
- maxWidth: number,
|
|
|
- minHeight: number,
|
|
|
- maxHeight: number,
|
|
|
- deviceId: string
|
|
|
- }): void {
|
|
|
+ public static CreateFromWebCam(
|
|
|
+ scene: Scene,
|
|
|
+ onReady: (videoTexture: VideoTexture) => void,
|
|
|
+ constraints: {
|
|
|
+ minWidth: number;
|
|
|
+ maxWidth: number;
|
|
|
+ minHeight: number;
|
|
|
+ maxHeight: number;
|
|
|
+ deviceId: string;
|
|
|
+ }
|
|
|
+ ): void {
|
|
|
var video = document.createElement("video");
|
|
|
var constraintsDeviceId;
|
|
|
if (constraints && constraints.deviceId) {
|
|
|
constraintsDeviceId = {
|
|
|
- exact: constraints.deviceId
|
|
|
- }
|
|
|
+ exact: constraints.deviceId,
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
|
|
+ navigator.getUserMedia =
|
|
|
+ navigator.getUserMedia ||
|
|
|
+ navigator.webkitGetUserMedia ||
|
|
|
+ navigator.mozGetUserMedia ||
|
|
|
+ navigator.msGetUserMedia;
|
|
|
window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
|
|
|
|
|
|
if (navigator.getUserMedia) {
|
|
|
- navigator.getUserMedia({
|
|
|
- video: {
|
|
|
- deviceId: constraintsDeviceId,
|
|
|
- width: {
|
|
|
- min: (constraints && constraints.minWidth) || 256,
|
|
|
- max: (constraints && constraints.maxWidth) || 640
|
|
|
+ navigator.getUserMedia(
|
|
|
+ {
|
|
|
+ video: {
|
|
|
+ deviceId: constraintsDeviceId,
|
|
|
+ width: {
|
|
|
+ min: (constraints && constraints.minWidth) || 256,
|
|
|
+ max: (constraints && constraints.maxWidth) || 640,
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ min: (constraints && constraints.minHeight) || 256,
|
|
|
+ max: (constraints && constraints.maxHeight) || 480,
|
|
|
+ },
|
|
|
},
|
|
|
- height: {
|
|
|
- min: (constraints && constraints.minHeight) || 256,
|
|
|
- max: (constraints && constraints.maxHeight) || 480
|
|
|
+ },
|
|
|
+ (stream: any) => {
|
|
|
+ if (video.mozSrcObject !== undefined) {
|
|
|
+ // hack for Firefox < 19
|
|
|
+ video.mozSrcObject = stream;
|
|
|
+ } else {
|
|
|
+ video.src = (window.URL && window.URL.createObjectURL(stream)) || stream;
|
|
|
}
|
|
|
- }
|
|
|
- }, (stream: any) => {
|
|
|
-
|
|
|
- if (video.mozSrcObject !== undefined) { // hack for Firefox < 19
|
|
|
- video.mozSrcObject = stream;
|
|
|
- } else {
|
|
|
- video.src = (window.URL && window.URL.createObjectURL(stream)) || stream;
|
|
|
- }
|
|
|
|
|
|
- video.play();
|
|
|
+ video.play();
|
|
|
|
|
|
- if (onReady) {
|
|
|
- onReady(new VideoTexture("video", video, scene, true, true));
|
|
|
+ if (onReady) {
|
|
|
+ onReady(new VideoTexture("video", video, scene, true, true));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ function(e: MediaStreamError) {
|
|
|
+ Tools.Error(e.name);
|
|
|
}
|
|
|
- }, function (e: MediaStreamError) {
|
|
|
- Tools.Error(e.name);
|
|
|
- });
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
}
|