From af2df06c48d6a61221a507364fdc033268afad6c Mon Sep 17 00:00:00 2001 From: Greg Fodor <gfodor@gmail.com> Date: Wed, 8 Aug 2018 23:49:20 +0000 Subject: [PATCH] Add telemetry call for object spawning --- src/components/media-loader.js | 2 + src/components/super-spawner.js | 3 +- src/hub.js | 25 ++++-- src/object-types.js | 86 ++++++++++++++++++++ src/react-components/create-object-dialog.js | 4 +- src/utils/hub-channel.js | 13 +++ src/utils/media-utils.js | 10 ++- 7 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 src/object-types.js diff --git a/src/components/media-loader.js b/src/components/media-loader.js index 81303a07d..b840a7ce8 100644 --- a/src/components/media-loader.js +++ b/src/components/media-loader.js @@ -67,6 +67,8 @@ AFRAME.registerComponent("media-loader", { const { raw, origin, contentType } = await resolveMedia(url, token); + this.el.emit("media_resolved", { src: this.data.src, raw, origin, contentType }); + if (token) { if (this.blobURL) { URL.revokeObjectURL(this.blobURL); diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js index a3d2df913..f760a29e2 100644 --- a/src/components/super-spawner.js +++ b/src/components/super-spawner.js @@ -1,5 +1,6 @@ import { addMedia } from "../utils/media-utils"; import { waitForEvent } from "../utils/async-utils"; +import { ObjectContentOrigins } from "../object-types"; let nextGrabId = 0; /** @@ -83,7 +84,7 @@ AFRAME.registerComponent("super-spawner", { const thisGrabId = nextGrabId++; this.heldEntities.set(hand, thisGrabId); - const entity = addMedia(this.data.src); + const entity = addMedia(this.data.src, ObjectContentOrigins.SPAWNER); entity.object3D.position.copy( this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.object3D.position ); diff --git a/src/hub.js b/src/hub.js index db26ba4d3..2892cd0b0 100644 --- a/src/hub.js +++ b/src/hub.js @@ -25,6 +25,8 @@ import joystick_dpad4 from "./behaviours/joystick-dpad4"; import msft_mr_axis_with_deadzone from "./behaviours/msft-mr-axis-with-deadzone"; import { PressedMove } from "./activators/pressedmove"; import { ReverseY } from "./activators/reversey"; +import { ObjectContentOrigins } from "./object-types"; + import "./activators/shortpress"; import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future @@ -297,8 +299,9 @@ const onReady = async () => { }); const offset = { x: 0, y: 0, z: -1.5 }; - const spawnMediaInfrontOfPlayer = src => { - const entity = addMedia(src, true); + const spawnMediaInfrontOfPlayer = (src, contentOrigin) => { + const entity = addMedia(src, contentOrigin, true); + entity.setAttribute("offset-relative-to", { target: "#player-camera", offset @@ -306,7 +309,15 @@ const onReady = async () => { }; scene.addEventListener("add_media", e => { - spawnMediaInfrontOfPlayer(e.detail); + const origin = e.detail instanceof File ? ObjectContentOrigins.FILE : ObjectContentOrigins.URL; + + spawnMediaInfrontOfPlayer(e.detail, origin); + }); + + scene.addEventListener("object_spawned", e => { + if (hubChannel) { + hubChannel.sendObjectSpawnedEvent(e.detail.objectType); + } }); if (qsTruthy("mediaTools")) { @@ -316,10 +327,10 @@ const onReady = async () => { const url = e.clipboardData.getData("text"); const files = e.clipboardData.files && e.clipboardData.files; if (url) { - spawnMediaInfrontOfPlayer(url); + spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL); } else { for (const file of files) { - spawnMediaInfrontOfPlayer(file); + spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.CLIPBOARD); } } }); @@ -333,10 +344,10 @@ const onReady = async () => { const url = e.dataTransfer.getData("url"); const files = e.dataTransfer.files; if (url) { - spawnMediaInfrontOfPlayer(url); + spawnMediaInfrontOfPlayer(url, ObjectContentOrigins.URL); } else { for (const file of files) { - spawnMediaInfrontOfPlayer(file); + spawnMediaInfrontOfPlayer(file, ObjectContentOrigins.FILE); } } }); diff --git a/src/object-types.js b/src/object-types.js new file mode 100644 index 000000000..529df1252 --- /dev/null +++ b/src/object-types.js @@ -0,0 +1,86 @@ +// Enumeration of spawned object content origins. URL means the content is +// fetched from a URL, FILE means it was an uploaded/dropped file, and +// CLIPBOARD means it was raw data pasted in from the clipboard, SPAWNER means +// it was copied over from a spawner. +export const ObjectContentOrigins = { + URL: 1, + FILE: 2, + CLIPBOARD: 3, + SPAWNER: 4 +}; + +// Enumeration of spawnable object types, used for telemetry, which encapsulates +// both the origin of the content for the object and also the type of content +// contained in the object. +const ObjectTypes = { + URL_IMAGE: 0, + URL_VIDEO: 1, + URL_MODEL: 2, + URL_PDF: 3, + URL_AUDIO: 4, + //URL_TEXT: 5, + //URL_TELEPORTER: 6, + FILE_IMAGE: 8, + FILE_VIDEO: 9, + FILE_MODEL: 10, + FILE_PDF: 11, + FILE_AUDIO: 12, + //FILE_TEXT: 13, + CLIPBOARD_IMAGE: 16, + CLIPBOARD_VIDEO: 17, + CLIPBOARD_MODEL: 18, + CLIPBOARD_PDF: 19, + CLIPBOARD_AUDIO: 20, + //CLIPBOARD_TEXT: 21, + SPAWNER_IMAGE: 24, + SPAWNER_VIDEO: 25, + SPAWNER_MODEL: 26, + SPAWNER_PDF: 27, + SPAWNER_AUDIO: 28, + //SPAWNER_TEXT: 29, + //DRAWING: 30, + UNKNOWN: 31 +}; + +// Given an origin and three object type values for URL, FILE, and CLIPBOARD +// origins respectively, return the appropriate one +function objectTypeForOrigin(origin, urlType, fileType, clipboardType, spawnerType) { + if (origin === ObjectContentOrigins.URL) { + return urlType; + } else if (origin === ObjectContentOrigins.FILE) { + return fileType; + } else if (origin === ObjectContentOrigins.CLIPBOARD) { + return clipboardType; + } else { + return spawnerType; + } +} + +// Lookup table of mime-type prefixes to the set of object types that we should use +// for objects spawned matching their underlying Content-Type. +const objectTypeMimePrefixLookupMap = { + "image/": [ObjectTypes.URL_IMAGE, ObjectTypes.FILE_IMAGE, ObjectTypes.CLIPBOARD_IMAGE, ObjectTypes.SPAWNER_IMAGE], + "model/": [ObjectTypes.URL_MODEL, ObjectTypes.FILE_MODEL, ObjectTypes.CLIPBOARD_MODEL, ObjectTypes.SPAWNER_MODEL], + "application/x-zip-compressed": [ + ObjectTypes.URL_MODEL, + ObjectTypes.FILE_MODEL, + ObjectTypes.CLIPBOARD_MODEL, + ObjectTypes.SPAWNER_MODEL + ], + "video/": [ObjectTypes.URL_VIDEO, ObjectTypes.FILE_VIDEO, ObjectTypes.CLIPBOARD_VIDEO, ObjectTypes.SPAWNER_VIDEO], + "audio/": [ObjectTypes.URL_AUDIO, ObjectTypes.FILE_AUDIO, ObjectTypes.CLIPBOARD_AUDIO, ObjectTypes.SPAWNER_AUDIO], + "application/pdf": [ObjectTypes.URL_PDF, ObjectTypes.FILE_PDF, ObjectTypes.CLIPBOARD_PDF, ObjectTypes.SPAWNER_PDF] +}; + +// Given an content origin and the resolved mime type of a piece of content, return +// the ObjectType, if any, for that content. +export function objectTypeForOriginAndContentType(origin, contentType) { + for (const prefix in objectTypeMimePrefixLookupMap) { + if (contentType.startsWith(prefix)) { + const types = objectTypeMimePrefixLookupMap[prefix]; + return objectTypeForOrigin(origin, ...types); + } + } + + return ObjectTypes.UNKNOWN; +} diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js index 6b314cfa7..caf31f378 100644 --- a/src/react-components/create-object-dialog.js +++ b/src/react-components/create-object-dialog.js @@ -50,7 +50,8 @@ export default class CreateObjectDialog extends Component { onUrlChange = e => { let attributionImage = this.state.attributionImage; if (e.target && e.target.value && e.target.validity.valid) { - attributionImage = attributionHostnames[new URL(e.target.value).hostname]; + const url = new URL(e.target.value); + attributionImage = attributionHostnames[url.hostname]; } this.setState({ url: e.target && e.target.value, @@ -98,6 +99,7 @@ export default class CreateObjectDialog extends Component { className={cx(styles.leftSideOfInput)} placeholder="Image/Video/glTF URL" onChange={this.onUrlChange} + type="url" value={this.state.url} /> ); diff --git a/src/utils/hub-channel.js b/src/utils/hub-channel.js index d8b87730a..86940a091 100644 --- a/src/utils/hub-channel.js +++ b/src/utils/hub-channel.js @@ -74,6 +74,19 @@ export default class HubChannel { return entryTimingFlags; }; + sendObjectSpawnedEvent = objectType => { + if (!this.channel) { + console.warn("No phoenix channel initialized before object spawn."); + return; + } + + const spawnEvent = { + object_type: objectType + }; + + this.channel.push("events:object_spawned", spawnEvent); + }; + disconnect = () => { if (this.channel) { this.channel.socket.disconnect(); diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js index dc7d57730..3948f5fc4 100644 --- a/src/utils/media-utils.js +++ b/src/utils/media-utils.js @@ -1,3 +1,5 @@ +import { objectTypeForOriginAndContentType } from "../object-types"; + const whitelistedHosts = [/^.*\.reticulum\.io$/, /^.*hubs\.mozilla\.com$/, /^hubs\.local$/]; const isHostWhitelisted = hostname => !!whitelistedHosts.filter(r => r.test(hostname)).length; let mediaAPIEndpoint = "/api/v1/media"; @@ -51,7 +53,7 @@ export const upload = file => { }; let interactableId = 0; -export const addMedia = (src, resize = false) => { +export const addMedia = (src, contentOrigin, resize = false) => { const scene = AFRAME.scenes[0]; const entity = document.createElement("a-entity"); @@ -71,5 +73,11 @@ export const addMedia = (src, resize = false) => { entity.setAttribute("media-loader", { src: "error" }); }); } + + entity.addEventListener("media_resolved", ({ detail }) => { + const objectType = objectTypeForOriginAndContentType(contentOrigin, detail.contentType); + scene.emit("object_spawned", { objectType }); + }); + return entity; }; -- GitLab