import { Avatar } from './avatar.js';
import { CameraHelper } from '../core/camera-helper.js';
import { MediaHelper } from '../core/media-helper.js';
import { VRSPACEUI } from "../ui/vrspace-ui.js";
/**
A disc that shows video stream. Until streaming starts, altText is displayed on the cylinder.
It can be extended, and new class provided to WorldManager factory.
*/
export class VideoAvatar extends Avatar {
constructor( scene, callback, customOptions ) {
super(scene);
this.video = true;
this.callback = callback;
this.deviceId = null;
this.radius = 1;
this.altText = "N/A";
this.altImage = null;
this.textStyle = "bold 64px monospace";
this.textColor = "black";
this.backColor = "white";
this.maxWidth = 640;
this.maxHeight = 640;
/** Should show() start video? */
this.autoStart = true;
/** Should own video avatar be attached to hud? */
this.autoAttach = true;
this.attached = false;
this.displaying="NONE";
if ( customOptions ) {
for(var c of Object.keys(customOptions)) {
this[c] = customOptions[c];
}
}
}
/**
Show the avatar. Used for both own and remote avatars.
*/
async show() {
if ( ! this.mesh ) {
if ( this.autoAttach ) {
this.cameraTracker = () => this.cameraChanged();
}
this.mesh = BABYLON.MeshBuilder.CreateDisc("VideoAvatar", {radius:this.radius}, this.scene);
//mesh.visibility = 0.95;
this.mesh.billboardMode = BABYLON.Mesh.BILLBOARDMODE_ALL;
this.mesh.position = new BABYLON.Vector3( 0, this.radius, 0);
this.mesh.material = new BABYLON.StandardMaterial("WebCamMat", this.scene);
this.mesh.material.emissiveColor = new BABYLON.Color3.White();
this.mesh.material.specularColor = new BABYLON.Color3.Black();
// used for collision detection (3rd person view)
this.mesh.ellipsoid = new BABYLON.Vector3(this.radius, this.radius, this.radius);
this.mesh.avatar = this; // CHECKME
// glow layer may make the texture invisible, needd to turn of glow for the mesh
if ( this.scene.effectLayers ) {
this.scene.effectLayers.forEach( (layer) => {
if ( 'GlowLayer' === layer.getClassName() ) {
layer.addExcludedMesh(this.mesh);
}
});
}
// display alt text before video texture loads:
this.displayAlt();
if ( this.autoStart ) {
await this.displayVideo();
}
}
}
/** dispose of everything */
dispose() {
super.dispose();
if ( this.mesh ) {
if ( this.mesh.parent && !this.attached) {
// if attached, this could dispose of camera
// otherwise dispose of parent mesh created by avatar loader
this.mesh.parent.dispose();
}
if ( this.mesh.material ) {
if ( this.mesh.material.diffuseTexture ) {
this.mesh.material.diffuseTexture.dispose();
}
this.mesh.material.dispose();
}
this.mesh.dispose();
delete this.mesh;
}
if ( this.cameraTracker ) {
CameraHelper.getInstance(this.scene).removeCameraListener(this.cameraTracker);
}
this.attached = false;
}
isEnabled() {
return Object.hasOwn(this,"mesh");
}
/**
Display and optionally set altText.
*/
displayAltText(text) {
this.displaying="TEXT";
if ( text ) {
this.altText = text;
}
if ( this.mesh.material.diffuseTexture ) {
this.mesh.material.diffuseTexture.dispose();
}
this.mesh.material.diffuseTexture = new BABYLON.DynamicTexture("WebCamTexture", {width:128, height:128}, this.scene);
this.mesh.material.diffuseTexture.drawText(this.altText, null, null, this.textStyle, this.textColor, this.backColor, false, true);
}
/**
Display and optionally set altImage
@param image path to the image file
*/
displayImage(image) {
this.displaying="IMAGE";
if ( image ) {
this.altImage = image;
}
if ( this.mesh.material.diffuseTexture ) {
this.mesh.material.diffuseTexture.dispose();
}
this.mesh.material.diffuseTexture = new BABYLON.Texture(this.altImage, this.scene, null, false);
}
/** Displays altImage if available, altText otherwise */
displayAlt() {
if ( this.altImage ) {
this.displayImage();
} else {
this.displayAltText();
}
}
/**
Display video from given device, used for own avatar.
*/
async displayVideo(deviceId) {
if ( this.displaying === "VIDEO" ) {
return;
}
this.deviceId = MediaHelper.selectVideoInput(deviceId);
if ( this.deviceId ) {
BABYLON.VideoTexture.CreateFromWebCamAsync(this.scene, { maxWidth: this.maxWidth, maxHeight: this.maxHeight, deviceId: this.deviceId }).then( (texture) => {
if ( this.mesh.material.diffuseTexture ) {
this.mesh.material.diffuseTexture.dispose();
}
this.mesh.material.diffuseTexture = texture;
this.displaying="VIDEO";
if ( this.callback ) {
this.callback();
}
});
}
}
/**
Create and display VideoTexture from given MediaStream.
*/
displayStream( mediaStream ) {
if ( mediaStream ) {
// CHECKME: otherwise error?
BABYLON.VideoTexture.CreateFromStreamAsync(this.scene, mediaStream).then( (texture) => {
if ( this.mesh.material.diffuseTexture ) {
this.mesh.material.diffuseTexture.dispose();
}
this.mesh.material.diffuseTexture = texture;
this.displaying="STREAM";
});
}
}
/**
Rescale own avatar and attach to current camera at given position
@param position where to put the avatar, by default it goes to the top left corner
*/
attachToCamera( position ) {
if (this.mesh && !this.attached) {
this.mesh.billboardMode = BABYLON.Mesh.BILLBOARDMODE_NONE;
this.mesh.parent = this.camera;
if ( position ) {
this.mesh.position = position;
} else {
this.windowResized = () => this.moveToCorner();
window.addEventListener("resize", this.windowResized);
this.moveToCorner();
var scale = (this.radius/2)/20; // 5cm size
this.mesh.scaling = new BABYLON.Vector3(scale, scale, scale);
}
this.attached = true;
this.cameraChanged();
CameraHelper.getInstance(this.scene).addCameraListener(this.cameraTracker);
}
}
/**
* @private
*/
moveToCorner() {
if ( this.mesh ) {
this.mesh.position = new BABYLON.Vector3(-VRSPACEUI.hud.scaling() * .2 * this.scene.getEngine().getAspectRatio(this.scene.activeCamera) + 0.05, - VRSPACEUI.hud.vertical(), 0.5);
}
}
/** Rescale own avatar and detach from camera */
detachFromCamera() {
if ( this.attached && this.mesh ) {
if ( this.windowResized ) {
window.removeEventListener("remove", this.windowResized);
delete this.windowResized;
}
this.mesh.billboardMode = BABYLON.Mesh.BILLBOARDMODE_Y;
this.mesh.position = this.camera.position; // CHECKME: must be the same
console.log("Mesh position: "+this.mesh.position);
this.mesh.scaling = new BABYLON.Vector3(1, 1, 1);
CameraHelper.getInstance(this.scene).removeCameraListener(this.cameraTracker);
this.mesh.parent = null;
this.attached = false;
}
}
/** Called when active camera changes/avatar attaches to camera */
cameraChanged() {
if ( this.autoAttach && this.attached && this.mesh ) {
console.log("Camera changed: "+this.scene.activeCamera.getClassName()+" new position "+this.scene.activeCamera.position);
if ( this.scene.activeCamera.getClassName() == 'UniversalCamera' ) {
this.camera = this.scene.activeCamera;
this.attached = true;
this.mesh.parent = this.camera;
}
}
}
getUrl() {
return "video";
}
basePosition() {
if ( this.mesh.parent ) {
// CHECKME this is used by avatarController and world serializer
return new BABYLON.Vector3(this.mesh.parent.position.x, this.mesh.parent.position.y, this.mesh.parent.position.z);
}
return new BABYLON.Vector3(this.mesh.position.x, this.mesh.position.y-this.radius, this.mesh.position.z);
}
topPositionRelative() {
return new BABYLON.Vector3(0, this.userHeight-this.radius, 0);
}
baseMesh() {
return this.mesh;
}
/** Remote emoji event routed by WorldManager. Video avatar looks the oposite way, so this just blows the particles to the opposite direction */
async emoji(client, direction=3) {
super.emoji(client, -direction);
}
/**
* Handles name change network event
*/
setName(name) {
super.setName(name);
if ( name ) {
this.altText = name;
} else {
this.altText = "N/A";
}
}
}