videoTexture.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import { __decorate } from "../../tslib.es6.js";
  2. import { Observable } from "../../Misc/observable.js";
  3. import { Tools } from "../../Misc/tools.js";
  4. import { Logger } from "../../Misc/logger.js";
  5. import { Texture } from "../../Materials/Textures/texture.js";
  6. import "../../Engines/Extensions/engine.videoTexture.js";
  7. import "../../Engines/Extensions/engine.dynamicTexture.js";
  8. import { serialize } from "../../Misc/decorators.js";
  9. import { RegisterClass } from "../../Misc/typeStore.js";
  10. function removeSource(video) {
  11. // Remove any <source> elements, etc.
  12. while (video.firstChild) {
  13. video.removeChild(video.firstChild);
  14. }
  15. // detach srcObject
  16. video.srcObject = null;
  17. // Set a blank src (https://html.spec.whatwg.org/multipage/media.html#best-practices-for-authors-using-media-elements)
  18. video.src = "";
  19. // Prevent non-important errors maybe (https://twitter.com/beraliv/status/1205214277956775936)
  20. video.removeAttribute("src");
  21. }
  22. /**
  23. * If you want to display a video in your scene, this is the special texture for that.
  24. * This special texture works similar to other textures, with the exception of a few parameters.
  25. * @see https://doc.babylonjs.com/features/featuresDeepDive/materials/using/videoTexture
  26. */
  27. export class VideoTexture extends Texture {
  28. /**
  29. * Event triggered when a dom action is required by the user to play the video.
  30. * This happens due to recent changes in browser policies preventing video to auto start.
  31. */
  32. get onUserActionRequestedObservable() {
  33. if (!this._onUserActionRequestedObservable) {
  34. this._onUserActionRequestedObservable = new Observable();
  35. }
  36. return this._onUserActionRequestedObservable;
  37. }
  38. _processError(reason) {
  39. this._errorFound = true;
  40. if (this._onError) {
  41. this._onError(reason?.message);
  42. }
  43. else {
  44. Logger.Error(reason?.message);
  45. }
  46. }
  47. _handlePlay() {
  48. this._errorFound = false;
  49. this.video.play().catch((reason) => {
  50. if (reason?.name === "NotAllowedError") {
  51. if (this._onUserActionRequestedObservable && this._onUserActionRequestedObservable.hasObservers()) {
  52. this._onUserActionRequestedObservable.notifyObservers(this);
  53. return;
  54. }
  55. else if (!this.video.muted) {
  56. Logger.Warn("Unable to autoplay a video with sound. Trying again with muted turned true");
  57. this.video.muted = true;
  58. this._errorFound = false;
  59. this.video.play().catch((otherReason) => {
  60. this._processError(otherReason);
  61. });
  62. return;
  63. }
  64. }
  65. this._processError(reason);
  66. });
  67. }
  68. /**
  69. * Creates a video texture.
  70. * If you want to display a video in your scene, this is the special texture for that.
  71. * This special texture works similar to other textures, with the exception of a few parameters.
  72. * @see https://doc.babylonjs.com/features/featuresDeepDive/materials/using/videoTexture
  73. * @param name optional name, will detect from video source, if not defined
  74. * @param src can be used to provide an url, array of urls or an already setup HTML video element.
  75. * @param scene is obviously the current scene.
  76. * @param generateMipMaps can be used to turn on mipmaps (Can be expensive for videoTextures because they are often updated).
  77. * @param invertY is false by default but can be used to invert video on Y axis
  78. * @param samplingMode controls the sampling method and is set to TRILINEAR_SAMPLINGMODE by default
  79. * @param settings allows finer control over video usage
  80. * @param onError defines a callback triggered when an error occurred during the loading session
  81. * @param format defines the texture format to use (Engine.TEXTUREFORMAT_RGBA by default)
  82. */
  83. constructor(name, src, scene, generateMipMaps = false, invertY = false, samplingMode = Texture.TRILINEAR_SAMPLINGMODE, settings = {}, onError, format = 5) {
  84. super(null, scene, !generateMipMaps, invertY);
  85. this._externalTexture = null;
  86. this._onUserActionRequestedObservable = null;
  87. this._stillImageCaptured = false;
  88. this._displayingPosterTexture = false;
  89. this._frameId = -1;
  90. this._currentSrc = null;
  91. this._errorFound = false;
  92. /**
  93. * Serialize the flag to define this texture as a video texture
  94. */
  95. this.isVideo = true;
  96. this._resizeInternalTexture = () => {
  97. // Cleanup the old texture before replacing it
  98. if (this._texture != null) {
  99. this._texture.dispose();
  100. }
  101. if (!this._getEngine().needPOTTextures || (Tools.IsExponentOfTwo(this.video.videoWidth) && Tools.IsExponentOfTwo(this.video.videoHeight))) {
  102. this.wrapU = Texture.WRAP_ADDRESSMODE;
  103. this.wrapV = Texture.WRAP_ADDRESSMODE;
  104. }
  105. else {
  106. this.wrapU = Texture.CLAMP_ADDRESSMODE;
  107. this.wrapV = Texture.CLAMP_ADDRESSMODE;
  108. this._generateMipMaps = false;
  109. }
  110. this._texture = this._getEngine().createDynamicTexture(this.video.videoWidth, this.video.videoHeight, this._generateMipMaps, this.samplingMode);
  111. this._texture.format = this._format ?? 5;
  112. // Reset the frame ID and update the new texture to ensure it pulls in the current video frame
  113. this._frameId = -1;
  114. this._updateInternalTexture();
  115. };
  116. this._createInternalTexture = () => {
  117. if (this._texture != null) {
  118. if (this._displayingPosterTexture) {
  119. this._displayingPosterTexture = false;
  120. }
  121. else {
  122. return;
  123. }
  124. }
  125. this.video.addEventListener("resize", this._resizeInternalTexture);
  126. this._resizeInternalTexture();
  127. if (!this.video.autoplay && !this._settings.poster && !this._settings.independentVideoSource) {
  128. const oldHandler = this.video.onplaying;
  129. const oldMuted = this.video.muted;
  130. this.video.muted = true;
  131. this.video.onplaying = () => {
  132. this.video.muted = oldMuted;
  133. this.video.onplaying = oldHandler;
  134. this._updateInternalTexture();
  135. if (!this._errorFound) {
  136. this.video.pause();
  137. }
  138. if (this.onLoadObservable.hasObservers()) {
  139. this.onLoadObservable.notifyObservers(this);
  140. }
  141. };
  142. this._handlePlay();
  143. }
  144. else {
  145. this._updateInternalTexture();
  146. if (this.onLoadObservable.hasObservers()) {
  147. this.onLoadObservable.notifyObservers(this);
  148. }
  149. }
  150. };
  151. this._reset = () => {
  152. if (this._texture == null) {
  153. return;
  154. }
  155. if (!this._displayingPosterTexture) {
  156. this._texture.dispose();
  157. this._texture = null;
  158. }
  159. };
  160. this._updateInternalTexture = () => {
  161. if (this._texture == null) {
  162. return;
  163. }
  164. if (this.video.readyState < this.video.HAVE_CURRENT_DATA) {
  165. return;
  166. }
  167. if (this._displayingPosterTexture) {
  168. return;
  169. }
  170. const frameId = this.getScene().getFrameId();
  171. if (this._frameId === frameId) {
  172. return;
  173. }
  174. this._frameId = frameId;
  175. this._getEngine().updateVideoTexture(this._texture, this._externalTexture ? this._externalTexture : this.video, this._invertY);
  176. };
  177. this._settings = {
  178. autoPlay: true,
  179. loop: true,
  180. autoUpdateTexture: true,
  181. ...settings,
  182. };
  183. this._onError = onError;
  184. this._generateMipMaps = generateMipMaps;
  185. this._initialSamplingMode = samplingMode;
  186. this.autoUpdateTexture = this._settings.autoUpdateTexture;
  187. this._currentSrc = src;
  188. this.name = name || this._getName(src);
  189. this.video = this._getVideo(src);
  190. if (this._engine?.createExternalTexture) {
  191. this._externalTexture = this._engine.createExternalTexture(this.video);
  192. }
  193. if (!this._settings.independentVideoSource) {
  194. if (this._settings.poster) {
  195. this.video.poster = this._settings.poster;
  196. }
  197. if (this._settings.autoPlay !== undefined) {
  198. this.video.autoplay = this._settings.autoPlay;
  199. }
  200. if (this._settings.loop !== undefined) {
  201. this.video.loop = this._settings.loop;
  202. }
  203. if (this._settings.muted !== undefined) {
  204. this.video.muted = this._settings.muted;
  205. }
  206. this.video.setAttribute("playsinline", "");
  207. this.video.addEventListener("paused", this._updateInternalTexture);
  208. this.video.addEventListener("seeked", this._updateInternalTexture);
  209. this.video.addEventListener("loadeddata", this._updateInternalTexture);
  210. this.video.addEventListener("emptied", this._reset);
  211. if (this._settings.autoPlay) {
  212. this._handlePlay();
  213. }
  214. }
  215. this._createInternalTextureOnEvent = this._settings.poster && !this._settings.autoPlay ? "play" : "canplay";
  216. this.video.addEventListener(this._createInternalTextureOnEvent, this._createInternalTexture);
  217. this._format = format;
  218. const videoHasEnoughData = this.video.readyState >= this.video.HAVE_CURRENT_DATA;
  219. if (this._settings.poster && (!this._settings.autoPlay || !videoHasEnoughData)) {
  220. this._texture = this._getEngine().createTexture(this._settings.poster, false, !this.invertY, scene);
  221. this._displayingPosterTexture = true;
  222. }
  223. else if (videoHasEnoughData) {
  224. this._createInternalTexture();
  225. }
  226. }
  227. /**
  228. * Get the current class name of the video texture useful for serialization or dynamic coding.
  229. * @returns "VideoTexture"
  230. */
  231. getClassName() {
  232. return "VideoTexture";
  233. }
  234. _getName(src) {
  235. if (src instanceof HTMLVideoElement) {
  236. return src.currentSrc;
  237. }
  238. if (typeof src === "object") {
  239. return src.toString();
  240. }
  241. return src;
  242. }
  243. _getVideo(src) {
  244. if (src.isNative) {
  245. return src;
  246. }
  247. if (src instanceof HTMLVideoElement) {
  248. Tools.SetCorsBehavior(src.currentSrc, src);
  249. return src;
  250. }
  251. const video = document.createElement("video");
  252. if (typeof src === "string") {
  253. Tools.SetCorsBehavior(src, video);
  254. video.src = src;
  255. }
  256. else {
  257. Tools.SetCorsBehavior(src[0], video);
  258. src.forEach((url) => {
  259. const source = document.createElement("source");
  260. source.src = url;
  261. video.appendChild(source);
  262. });
  263. }
  264. this.onDisposeObservable.addOnce(() => {
  265. removeSource(video);
  266. });
  267. return video;
  268. }
  269. /**
  270. * @internal Internal method to initiate `update`.
  271. */
  272. _rebuild() {
  273. this.update();
  274. }
  275. /**
  276. * Update Texture in the `auto` mode. Does not do anything if `settings.autoUpdateTexture` is false.
  277. */
  278. update() {
  279. if (!this.autoUpdateTexture) {
  280. // Expecting user to call `updateTexture` manually
  281. return;
  282. }
  283. this.updateTexture(true);
  284. }
  285. /**
  286. * Update Texture in `manual` mode. Does not do anything if not visible or paused.
  287. * @param isVisible Visibility state, detected by user using `scene.getActiveMeshes()` or otherwise.
  288. */
  289. updateTexture(isVisible) {
  290. if (!isVisible) {
  291. return;
  292. }
  293. if (this.video.paused && this._stillImageCaptured) {
  294. return;
  295. }
  296. this._stillImageCaptured = true;
  297. this._updateInternalTexture();
  298. }
  299. /**
  300. * Get the underlying external texture (if supported by the current engine, else null)
  301. */
  302. get externalTexture() {
  303. return this._externalTexture;
  304. }
  305. /**
  306. * Change video content. Changing video instance or setting multiple urls (as in constructor) is not supported.
  307. * @param url New url.
  308. */
  309. updateURL(url) {
  310. this.video.src = url;
  311. this._currentSrc = url;
  312. }
  313. /**
  314. * Clones the texture.
  315. * @returns the cloned texture
  316. */
  317. clone() {
  318. return new VideoTexture(this.name, this._currentSrc, this.getScene(), this._generateMipMaps, this.invertY, this.samplingMode, this._settings);
  319. }
  320. /**
  321. * Dispose the texture and release its associated resources.
  322. */
  323. dispose() {
  324. super.dispose();
  325. this._currentSrc = null;
  326. if (this._onUserActionRequestedObservable) {
  327. this._onUserActionRequestedObservable.clear();
  328. this._onUserActionRequestedObservable = null;
  329. }
  330. this.video.removeEventListener(this._createInternalTextureOnEvent, this._createInternalTexture);
  331. if (!this._settings.independentVideoSource) {
  332. this.video.removeEventListener("paused", this._updateInternalTexture);
  333. this.video.removeEventListener("seeked", this._updateInternalTexture);
  334. this.video.removeEventListener("loadeddata", this._updateInternalTexture);
  335. this.video.removeEventListener("emptied", this._reset);
  336. this.video.removeEventListener("resize", this._resizeInternalTexture);
  337. this.video.pause();
  338. }
  339. this._externalTexture?.dispose();
  340. }
  341. /**
  342. * Creates a video texture straight from a stream.
  343. * @param scene Define the scene the texture should be created in
  344. * @param stream Define the stream the texture should be created from
  345. * @param constraints video constraints
  346. * @param invertY Defines if the video should be stored with invert Y set to true (true by default)
  347. * @returns The created video texture as a promise
  348. */
  349. static CreateFromStreamAsync(scene, stream, constraints, invertY = true) {
  350. const video = scene.getEngine().createVideoElement(constraints);
  351. if (scene.getEngine()._badOS) {
  352. // Yes... I know and I hope to remove it soon...
  353. document.body.appendChild(video);
  354. video.style.transform = "scale(0.0001, 0.0001)";
  355. video.style.opacity = "0";
  356. video.style.position = "fixed";
  357. video.style.bottom = "0px";
  358. video.style.right = "0px";
  359. }
  360. video.setAttribute("autoplay", "");
  361. video.setAttribute("muted", "true");
  362. video.setAttribute("playsinline", "");
  363. video.muted = true;
  364. if (video.isNative) {
  365. // No additional configuration needed for native
  366. }
  367. else if (video.mozSrcObject !== undefined) {
  368. // hack for Firefox < 19
  369. video.mozSrcObject = stream;
  370. }
  371. else {
  372. if (typeof video.srcObject == "object") {
  373. video.srcObject = stream;
  374. }
  375. else {
  376. // older API. See https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#using_object_urls_for_media_streams
  377. video.src = window.URL && window.URL.createObjectURL(stream);
  378. }
  379. }
  380. return new Promise((resolve) => {
  381. const onPlaying = () => {
  382. const videoTexture = new VideoTexture("video", video, scene, true, invertY, undefined, undefined, undefined, 4);
  383. if (scene.getEngine()._badOS) {
  384. videoTexture.onDisposeObservable.addOnce(() => {
  385. video.remove();
  386. });
  387. }
  388. videoTexture.onDisposeObservable.addOnce(() => {
  389. removeSource(video);
  390. });
  391. resolve(videoTexture);
  392. video.removeEventListener("playing", onPlaying);
  393. };
  394. video.addEventListener("playing", onPlaying);
  395. video.play();
  396. });
  397. }
  398. /**
  399. * Creates a video texture straight from your WebCam video feed.
  400. * @param scene Define the scene the texture should be created in
  401. * @param constraints Define the constraints to use to create the web cam feed from WebRTC
  402. * @param audioConstaints Define the audio constraints to use to create the web cam feed from WebRTC
  403. * @param invertY Defines if the video should be stored with invert Y set to true (true by default)
  404. * @returns The created video texture as a promise
  405. */
  406. static async CreateFromWebCamAsync(scene, constraints, audioConstaints = false, invertY = true) {
  407. if (navigator.mediaDevices) {
  408. const stream = await navigator.mediaDevices.getUserMedia({
  409. video: constraints,
  410. audio: audioConstaints,
  411. });
  412. const videoTexture = await this.CreateFromStreamAsync(scene, stream, constraints, invertY);
  413. videoTexture.onDisposeObservable.addOnce(() => {
  414. stream.getTracks().forEach((track) => {
  415. track.stop();
  416. });
  417. });
  418. return videoTexture;
  419. }
  420. return Promise.reject("No support for userMedia on this device");
  421. }
  422. /**
  423. * Creates a video texture straight from your WebCam video feed.
  424. * @param scene Defines the scene the texture should be created in
  425. * @param onReady Defines a callback to triggered once the texture will be ready
  426. * @param constraints Defines the constraints to use to create the web cam feed from WebRTC
  427. * @param audioConstaints Defines the audio constraints to use to create the web cam feed from WebRTC
  428. * @param invertY Defines if the video should be stored with invert Y set to true (true by default)
  429. */
  430. static CreateFromWebCam(scene, onReady, constraints, audioConstaints = false, invertY = true) {
  431. this.CreateFromWebCamAsync(scene, constraints, audioConstaints, invertY)
  432. .then(function (videoTexture) {
  433. if (onReady) {
  434. onReady(videoTexture);
  435. }
  436. })
  437. .catch(function (err) {
  438. Logger.Error(err.name);
  439. });
  440. }
  441. }
  442. __decorate([
  443. serialize("settings")
  444. ], VideoTexture.prototype, "_settings", void 0);
  445. __decorate([
  446. serialize("src")
  447. ], VideoTexture.prototype, "_currentSrc", void 0);
  448. __decorate([
  449. serialize()
  450. ], VideoTexture.prototype, "isVideo", void 0);
  451. Texture._CreateVideoTexture = (name, src, scene, generateMipMaps = false, invertY = false, samplingMode = Texture.TRILINEAR_SAMPLINGMODE, settings = {}, onError, format = 5) => {
  452. return new VideoTexture(name, src, scene, generateMipMaps, invertY, samplingMode, settings, onError, format);
  453. };
  454. // Some exporters relies on Tools.Instantiate
  455. RegisterClass("BABYLON.VideoTexture", VideoTexture);
  456. //# sourceMappingURL=videoTexture.js.map