@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
401 lines (400 loc) • 12.3 kB
JavaScript
"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;
}
}