Skip to content

Instantly share code, notes, and snippets.

@antpb
Created November 29, 2020 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save antpb/33b20cad6e0bae7b7c9d0d0b929f494c to your computer and use it in GitHub Desktop.
Save antpb/33b20cad6e0bae7b7c9d0d0b929f494c to your computer and use it in GitHub Desktop.
Spoke/src/editor/objects/Video.js
import {
VideoTexture,
LinearFilter,
sRGBEncoding,
PlaneBufferGeometry,
MeshBasicMaterial,
DoubleSide,
Mesh,
SphereBufferGeometry,
RGBAFormat
} from "three";
import { RethrownError } from "../utils/errors";
import Hls from "hls.js/dist/hls.light";
import isHLS from "../utils/isHLS";
import AudioSource from "./AudioSource";
import bannerScreenFileUrl from "../../assets/banners_screen.glb";
import { GLTFLoader } from "../gltf/GLTFLoader";
export const VideoProjection = {
Flat: "flat",
BannerScreen: "banner-screen",
Equirectangular360: "360-equirectangular"
};
export default class Video extends AudioSource {
constructor(audioListener) {
super(audioListener, "video");
this._videoTexture = new VideoTexture(this.el);
this._videoTexture.minFilter = LinearFilter;
this._videoTexture.encoding = sRGBEncoding;
this._texture = this._videoTexture;
this.bannerObject = null;
this.bannerScreenObject = null;
this.performanceScreenObject = null;
this.logoScreenObject = null;
const geometry = new PlaneBufferGeometry();
const material = new MeshBasicMaterial();
material.map = this._texture;
material.side = DoubleSide;
this._mesh = new Mesh(geometry, material);
this._mesh.name = "VideoMesh";
this.add(this._mesh);
this._projection = "flat";
this.hls = null;
}
loadVideo(src, contentType) {
return new Promise((resolve, reject) => {
const _isHLS = isHLS(src, contentType);
if (_isHLS) {
if (!this.hls) {
this.hls = new Hls();
}
this.hls.loadSource(src);
this.hls.attachMedia(this.el);
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.hls.startLoad(-1);
});
} else {
this.el.src = src;
}
let cleanup = null;
const onLoadedMetadata = () => {
cleanup();
resolve(this._videoTexture);
};
const onError = error => {
cleanup();
reject(new RethrownError(`Error loading video "${this.el.src}"`, error));
};
cleanup = () => {
this.el.removeEventListener("loadeddata", onLoadedMetadata);
this.el.removeEventListener("error", onError);
};
if (_isHLS) {
this.hls.on(Hls.Events.ERROR, onError);
}
this.el.addEventListener("loadeddata", onLoadedMetadata);
this.el.addEventListener("error", onError);
});
}
get projection() {
return this._projection;
}
set projection(projection) {
if (projection === this._projection) {
return;
}
const material = new MeshBasicMaterial();
let geometry;
if (projection === "360-equirectangular") {
geometry = new SphereBufferGeometry(1, 64, 32);
// invert the geometry on the x-axis so that all of the faces point inward
geometry.scale(-1, 1, 1);
} else if (projection === "banner") {
if (this.bannerObject) {
geometry = this.bannerObject.scene.children[0].geometry;
} else {
geometry = new PlaneBufferGeometry();
}
// invert the geometry on the x-axis so that all of the faces point inward
geometry.scale(.001, .001, .001);
material.side = DoubleSide;
} else if (projection === "banner-screen") {
if (this.bannerScreenObject) {
geometry = this.bannerScreenObject.scene.children[0].geometry;
} else {
geometry = new PlaneBufferGeometry();
}
geometry.scale(.001, .001, .001);
material.side = DoubleSide;
} else if (projection === "performance-screen") {
if (this.performanceScreenObject) {
geometry = this.performanceScreenObject.scene.children[0].geometry;
} else {
geometry = new PlaneBufferGeometry();
}
// invert the geometry on the x-axis so that all of the faces point inward
geometry.scale(.001, .001, .001);
material.side = DoubleSide;
} else if (projection === "logo-screen") {
if (this.logoScreenObject) {
geometry = this.logoScreenObject.scene.children[0].geometry;
} else {
geometry = new PlaneBufferGeometry();
}
// invert the geometry on the x-axis so that all of the faces point inward
geometry.scale(.001, .001, .001);
material.side = DoubleSide;
} else {
geometry = new PlaneBufferGeometry();
material.side = DoubleSide;
}
material.map = this._texture;
this._projection = projection;
const nextMesh = new Mesh(geometry, material);
nextMesh.name = "VideoMesh";
const meshIndex = this.children.indexOf(this._mesh);
if (meshIndex === -1) {
this.add(nextMesh);
} else {
this.children.splice(meshIndex, 1, nextMesh);
nextMesh.parent = this;
}
this._mesh = nextMesh;
this.onResize();
}
async loadBannersScreen() {
if (this.bannerScreenObject) {
return Promise.resolve(this.bannerScreenObject);
}
const bannerScreenGltf = await new GLTFLoader(bannerScreenFileUrl).loadGLTF();
this.bannerScreenObject = bannerScreenGltf;
return this.bannerScreenObject;
}
async load(src, contentType) {
this.bannerScreenObject = await this.loadBannersScreen();
this._mesh.visible = false;
this._texture = await this.loadVideo(src, contentType);
this.onResize();
this.audioSource = this.audioListener.context.createMediaElementSource(this.el);
this.audio.setNodeSource(this.audioSource);
if (this._texture.format === RGBAFormat) {
this._mesh.material.transparent = true;
}
this._mesh.material.map = this._texture;
this._mesh.material.needsUpdate = true;
this._mesh.visible = true;
return this;
}
onResize() {
if (this.projection === VideoProjection.Flat) {
const ratio = (this.el.videoHeight || 1.0) / (this.el.videoWidth || 1.0);
const width = Math.min(1.0, 1.0 / ratio);
const height = Math.min(1.0, ratio);
this._mesh.scale.set(width, height, 1);
}
}
clone(recursive) {
return new this.constructor(this.audioListener).copy(this, recursive);
}
copy(source, recursive = true) {
super.copy(source, false);
if (recursive) {
for (let i = 0; i < source.children.length; i++) {
const child = source.children[i];
if (child !== source.audio && child !== source._mesh) {
this.add(child.clone());
}
}
}
this.projection = source.projection;
return this;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment