@urth/metatell-sdk/objects lets a plugin read, spawn, move, update, pin, and
remove managed room objects (media and models) without touching A-Frame or NAF
directly.
Experimental. This API was added in 0.0.24 and requires the metatell host to install its room-object bridge. Without it, reads return empty results and writes reject with an
ObjectOperationErrorwhosecodeis"host_unavailable". UsewaitUntilReady("objects")when you need to check support first.
list() and get() are synchronous snapshots. Each returned object is a copy,
so mutating it does not change the room.
import { get, list } from "@urth/metatell-sdk/objects";
const all = list(); // RoomObject[] ([] when the host is not ready)
const one = get("object-id"); // RoomObject | undefined
interface RoomObject {
id: string;
type: "image" | "video" | "audio" | "pdf" | "model" | "primitive" | "unknown";
src?: string;
pdfIndex?: number;
transform: ObjectTransform;
pinned: boolean;
ownerId?: string;
mine: boolean; // true when the current client owns the object
}
interface ObjectTransform {
position: { x: number; y: number; z: number };
quaternion: { x: number; y: number; z: number; w: number };
scale: { x: number; y: number; z: number };
}
ObjectTransform always carries scale and has no Euler rotation, unlike the
TransformData used by the avatar and camera streams.
Each subscription returns an Unsubscribe function; call it on teardown.
import {
onAdded,
onMoved,
onRemoved,
onUpdated,
} from "@urth/metatell-sdk/objects";
const offs = [
onAdded((object) => console.log("added", object.id, object.type)),
onUpdated((object) => console.log("updated", object.id, object.pinned)),
onMoved(({ id, transform }) => console.log("moved", id, transform.position)),
onRemoved(({ id }) => console.log("removed", id)),
];
function teardown() {
for (const off of offs) off();
}
onAdded / onRemoved — an object enters or leaves the room.onUpdated — a property other than the transform changes (for example
src, pdfIndex, or pinned).onMoved — the object's transform changes.spawn() resolves with the created RoomObject.
import { spawn } from "@urth/metatell-sdk/objects";
const image = await spawn({
kind: "media",
src: "https://example.com/poster.png",
position: { x: 0, y: 1.5, z: -2 },
pinned: true,
});
EntitySpec is a union discriminated by kind. All kinds accept the optional
position, quaternion, scale, and pinned fields.
kind |
Fields |
|---|---|
media |
src: string | File, optional contentType |
model |
src: string | File, optional lipSync: { sessionId } |
primitive |
shape: "box" | "sphere" | "cylinder" | "plane", optional color, size |
The current metatell host does not support primitive yet and rejects it with
SpawnRejectedError.
import { move, pin, remove, setProperty } from "@urth/metatell-sdk/objects";
await move(image.id, { position: { x: 1, y: 1.5, z: -2 } });
await setProperty(image.id, { src: "https://example.com/next.png" });
await pin(image.id, false);
await remove(image.id);
move(id, transform) accepts a Partial<ObjectTransform>; omitted parts are
left unchanged.setProperty(id, props) sets replicated properties: src, pdfIndex (PDF
objects only), and lipSync.pin(id, pinned) pins or unpins an object so it persists across sessions.Before move, setProperty, pin, or remove, the host checks the room's
permissions and takes network ownership of the object. It rejects with
PermissionDeniedError or NotOwnerError when either step fails.
Writes resolve once the host confirms the operation. The resulting change is
delivered separately through onUpdated / onMoved / onRemoved.
Every write accepts an optional last argument:
interface WriteOptions {
signal?: AbortSignal; // abort the wait; rejects with signal.reason
timeoutMs?: number; // default 15000
}
Aborting or timing out stops waiting for the result. It does not undo an operation the host has already applied.
lipSync binds a VRM model's mouth (the aa expression) to a participant's
voice. sessionId is that participant's session id, as returned by
getSessionId() in /user or RemoteUser.sessionId in
/users. The binding replicates to all clients.
import { setProperty, spawn } from "@urth/metatell-sdk/objects";
import { getSessionId } from "@urth/metatell-sdk/user";
const sessionId = getSessionId();
if (sessionId) {
const avatar = await spawn({
kind: "model",
src: "https://example.com/guide.vrm",
lipSync: { sessionId },
});
// later: remove the binding
await setProperty(avatar.id, { lipSync: null });
}
Writes reject with error classes exported from @urth/metatell-sdk/objects, so
instanceof checks work inside your plugin bundle:
| Error | Meaning |
|---|---|
PermissionDeniedError |
The room's permissions do not allow the operation. |
ObjectNotFoundError |
No object with that id (error.id). |
NotOwnerError |
The host could not take ownership of the object (error.id). |
SpawnRejectedError |
The host rejected the spec or property (for example, an unsupported kind). |
ObjectOperationError |
Any other failure. error.code is "host_unavailable" when no host bridge is installed and "unknown" for timeouts and other failures. |
import {
NotOwnerError,
PermissionDeniedError,
move,
} from "@urth/metatell-sdk/objects";
try {
await move("object-id", { scale: { x: 2, y: 2, z: 2 } });
} catch (e) {
if (e instanceof PermissionDeniedError) {
// the user lacks the required room permission
} else if (e instanceof NotOwnerError) {
// someone else controls the object
}
}