UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

401 lines (400 loc) 12.3 kB
"use strict"; import { ParamEvent } from "./../../poly/ParamEvent"; import { VideoTexture } from "three"; import { TypedCopNode } from "./_Base"; import { NodeParamsConfig, ParamConfig } from "../utils/params/ParamsConfig"; import { isUrlVideo } from "../../../core/FileTypeController"; import { TextureParamsController, TextureParamConfig } from "./utils/TextureParamsController"; import { isBooleanTrue } from "../../../core/BooleanValue"; import { InputCloneMode } from "../../poly/InputCloneMode"; import { CopType } from "../../poly/registers/nodes/types/Cop"; import { FileTypeCheckCopParamConfig } from "./utils/CheckFileType"; import { Poly } from "../../Poly"; import { CoreVideoTextureLoader } from "../../../core/loader/texture/Video"; import { VideoEvent, VIDEO_EVENTS } from "../../../core/VideoEvent"; import { EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT } from "../../../core/loader/FileExtensionRegister"; import { isHTMLVideoElementLoaded } from "../../../core/DomUtils"; import { NodeContext } from "../../poly/NodeContext"; export var VideoMode = /* @__PURE__ */ ((VideoMode2) => { VideoMode2["FROM_URLS"] = "From Urls"; VideoMode2["FROM_HTML_ELEMENT"] = "From HTML Element"; return VideoMode2; })(VideoMode || {}); const VIDEO_MODES = ["From Urls" /* FROM_URLS */, "From HTML Element" /* FROM_HTML_ELEMENT */]; const VIDEO_MODE_ENTRIES = VIDEO_MODES.map((name, value) => ({ name, value })); const VIDEO_MODE_FROM_URLS = VIDEO_MODES.indexOf("From Urls" /* FROM_URLS */); const VIDEO_MODE_FROM_HTML_ELEMENT = VIDEO_MODES.indexOf("From HTML Element" /* FROM_HTML_ELEMENT */); function VideoCopParamConfig(Base) { return class Mixin extends Base { constructor() { super(...arguments); /** @param mode */ this.mode = ParamConfig.INTEGER(VIDEO_MODE_FROM_URLS, { menu: { entries: VIDEO_MODE_ENTRIES } }); /** @param number of video files to fetch */ this.urlsCount = ParamConfig.INTEGER(2, { range: [1, 3], rangeLocked: [true, true], visibleIf: { mode: VIDEO_MODE_FROM_URLS } }); /** @param url to fetch the video from */ this.url1 = ParamConfig.STRING("", { fileBrowse: { extensions: EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT[NodeContext.COP][CopType.VIDEO] } }); /** @param url to fetch the video from */ this.url2 = ParamConfig.STRING("", { fileBrowse: { extensions: EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT[NodeContext.COP][CopType.VIDEO] }, visibleIf: [ { mode: VIDEO_MODE_FROM_URLS, urlsCount: 2 }, { mode: VIDEO_MODE_FROM_URLS, urlsCount: 3 } ] }); /** @param url to fetch the video from */ this.url3 = ParamConfig.STRING("", { fileBrowse: { extensions: EXTENSIONS_BY_NODE_TYPE_BY_CONTEXT[NodeContext.COP][CopType.VIDEO] }, visibleIf: { mode: VIDEO_MODE_FROM_URLS, urlsCount: 3 } }); /** @param selector */ this.selector = ParamConfig.STRING("", { visibleIf: { mode: VIDEO_MODE_FROM_HTML_ELEMENT } }); /** @param reload the video */ this.reload = ParamConfig.BUTTON(null, { callback: (node, param) => { VideoCopNode.PARAM_CALLBACK_reload(node, param); } }); /** @param play the video */ this.play = ParamConfig.BOOLEAN(1, { cook: false, callback: (node) => { VideoCopNode.PARAM_CALLBACK_videoUpdatePlay(node); } }); /** @param set the video muted attribute */ this.muted = ParamConfig.BOOLEAN(1, { cook: false, callback: (node) => { VideoCopNode.PARAM_CALLBACK_videoUpdateMuted(node); } }); /** @param set the video loop attribute */ this.loop = ParamConfig.BOOLEAN(1, { cook: false, callback: (node) => { VideoCopNode.PARAM_CALLBACK_videoUpdateLoop(node); } }); /** @param set the video time */ this.videoTime = ParamConfig.FLOAT(0, { cook: false // do not use videoTime, as calling "this._video.currentTime =" every frame is really expensive }); /** @param seek the video at the time specified in videoTime */ this.setVideoTime = ParamConfig.BUTTON(null, { cook: false, callback: (node) => { VideoCopNode.PARAM_CALLBACK_videoUpdateTime(node); } }); } }; } class VideoCopParamsConfig extends FileTypeCheckCopParamConfig( TextureParamConfig(VideoCopParamConfig(NodeParamsConfig)) ) { } const ParamsConfig = new VideoCopParamsConfig(); export class VideoCopNode extends TypedCopNode { constructor() { super(...arguments); this.paramsConfig = ParamsConfig; // private _data_texture_controller: DataTextureController | undefined; this.textureParamsController = new TextureParamsController(this); this._videoBoundEvents = { play: this._onVideoEventPlay.bind(this), pause: this._onVideoEventPause.bind(this), timeupdate: this._onVideoEventTimeUpdate.bind(this), volumechange: this._onVideoEventVolumeChange.bind(this) }; } static type() { return CopType.VIDEO; } HTMLVideoElement() { return this._video; } initializeNode() { this.io.inputs.setCount(0, 1); this.io.inputs.initInputsClonedState(InputCloneMode.NEVER); } dispose() { super.dispose(); this._disposeHTMLVideoElement(); Poly.blobs.clearBlobsForNode(this); } _disposeHTMLVideoElement() { var _a; if (this._video) { this._removeVideoEvents(this._video); (_a = this._video.parentElement) == null ? void 0 : _a.removeChild(this._video); } } setMode(mode) { this.p.mode.set(VIDEO_MODES.indexOf(mode)); } mode() { return VIDEO_MODES[this.pv.mode]; } async cook(input_contents) { const mode = this.mode(); if (mode == "From Urls" /* FROM_URLS */ && isBooleanTrue(this.pv.checkFileType)) { const setUrlNotVideoError = (param) => { this.states.error.set(`url from param ${param.name()} is not a video ('${param.value}')`); }; if (!isUrlVideo(this.pv.url1)) { return setUrlNotVideoError(this.p.url1); } const urlsCount = this.pv.urlsCount; if (urlsCount >= 2) { if (!isUrlVideo(this.pv.url2)) { return setUrlNotVideoError(this.p.url2); } } if (urlsCount >= 3) { if (!isUrlVideo(this.pv.url3)) { return setUrlNotVideoError(this.p.url3); } } } const texture = mode == "From Urls" /* FROM_URLS */ ? await this._loadTexture() : await this._videoTextureFromSelector(); if (texture) { this._disposeHTMLVideoElement(); this._video = texture.image; if (this._video) { this._addVideoEvents(this._video); } const inputTexture = input_contents[0]; if (inputTexture) { TextureParamsController.copyTextureAttributes(texture, inputTexture); } this.videoUpdateLoop(); this.videoUpdateMuted(); this.videoUpdatePlay(); this.videoUpdateTime(); await this.textureParamsController.update(texture); this.setTexture(texture); } else { this.cookController.endCook(); } } _addVideoEvents(video) { for (const eventName of VIDEO_EVENTS) { video.addEventListener(eventName, this._videoBoundEvents[eventName]); } } _removeVideoEvents(video) { for (const eventName of VIDEO_EVENTS) { video.removeEventListener(eventName, this._videoBoundEvents[eventName]); } } _onVideoEvent(eventName) { this.dispatchEvent({ type: eventName }); } _onVideoEventPlay() { this._onVideoEvent(VideoEvent.PLAY); } _onVideoEventPause() { this._onVideoEvent(VideoEvent.PAUSE); } _onVideoEventTimeUpdate() { this._onVideoEvent(VideoEvent.TIME_UPDATE); } _onVideoEventVolumeChange() { this._onVideoEvent(VideoEvent.VOLUME_CHANGE); } videoStatePlaying() { return this._video ? !this._video.paused : false; } videoStateMuted() { return this._video ? this._video.muted : true; } videoDuration() { var _a; return ((_a = this._video) == null ? void 0 : _a.duration) || 0; } videoCurrentTime() { var _a; return ((_a = this._video) == null ? void 0 : _a.currentTime) || 0; } // // // VIDEO STATE // // // // time static PARAM_CALLBACK_videoUpdateTime(node) { node.videoUpdateTime(); } async videoUpdateTime() { if (this._video) { const param = this.p.videoTime; if (param.isDirty()) { await param.compute(); } this._videoUpdateTime(this._video); } } async _videoUpdateTime(video) { video.currentTime = this.pv.videoTime; } // play static PARAM_CALLBACK_videoUpdatePlay(node) { node.videoUpdatePlay(); } videoUpdatePlay() { if (this._video) { this._videoUpdatePlay(this._video); } } _videoUpdatePlay(video) { if (isBooleanTrue(this.pv.play)) { video.play(); } else { video.pause(); } } // muted static PARAM_CALLBACK_videoUpdateMuted(node) { node.videoUpdateMuted(); } videoUpdateMuted() { if (this._video) { this._videoUpdateMuted(this._video); } } _videoUpdateMuted(video) { video.muted = isBooleanTrue(this.pv.muted); } // loop static PARAM_CALLBACK_videoUpdateLoop(node) { node.videoUpdateLoop(); } videoUpdateLoop() { if (this._video) { this._videoUpdateLoop(this._video); } } _videoUpdateLoop(video) { video.loop = isBooleanTrue(this.pv.loop); } // // VIDEO CREATE / GET // // // private _createVideoElement(){ // const mode = VIDEO_MODES[this.pv.mode] // switch(mode){ // case VideoMode.FROM_URLS:{ // return this._videoElementFromUrls() // } // case VideoMode.FROM_HTML_ELEMENT:{ // return this._videoElementFromSelector() // } // } // TypeAssert.unreachable(mode) // } // private _videoElementFromUrls(){ // const _createVideoTag=()=>{ // const video = document.createElement('video') // this._videoUpdateLoop(video) // this._videoUpdateMuted(video) // video.setAttribute('crossOrigin', 'anonymous'); // video.setAttribute('autoplay', `${true}`); // to ensure it loads // return video // } // const _addUrls=(video:HTMLVideoElement, urls:string[])=>{ // for(let url of urls){ // const source = document.createElement('source') // source.src = url; // video.append(source) // } // } // return _addUrls(_createVideoTag(),this._urlParams().map(p=>p.value)) // } async _videoTextureFromSelector() { const selector = this.pv.selector; const element = document.querySelector(selector); if (!element) { return; } if (!(element instanceof HTMLVideoElement)) { this.states.error.set(`element found with selector '${selector}' is not a video`); return; } const texture = new VideoTexture(element); return new Promise((resolve) => { if (isHTMLVideoElementLoaded(element)) { resolve(texture); } element.onloadedmetadata = () => { resolve(texture); }; }); } // // // UTILS // // urlParams() { const urlsCount = this.pv.urlsCount; switch (urlsCount) { case 1: { return [this.p.url1]; } case 2: { return [this.p.url1, this.p.url2]; } case 3: { return [this.p.url1, this.p.url2, this.p.url3]; } } return []; } _urlsToLoad() { return this.urlParams().map((p) => p.value); } static PARAM_CALLBACK_reload(node, param) { node.paramCallbackReload(); } paramCallbackReload() { const urlParams = this.urlParams(); for (const urlParam of urlParams) { urlParam.setDirty(); urlParam.emit(ParamEvent.ASSET_RELOAD_REQUEST); } } async _loadTexture() { const urls = this._urlsToLoad(); let texture = null; const loader = new CoreVideoTextureLoader(urls, this); try { texture = await loader.loadVideo(); if (texture) { texture.matrixAutoUpdate = false; } } catch (e) { } if (!texture) { this.states.error.set(`could not load video from textures '${urls.join(",")}'`); } return texture; } }