# Colloquy clip player — build your own, no repo required > Everything needed to put a recorded Colloquy of Mobiles simulation on your own > page. The runtime is one self-contained ES module served with > `Access-Control-Allow-Origin: *`, so you can import it straight from this site: > no install, no build step, no copy of the source, no API key. This file is the > whole brief — an agent should not need anything else. Base URL: `https://colloquyscenestudio.aroughidea.com` ## Instructions **If you are a person:** paste this into your coding agent and you are done. ```text Add a Colloquy of Mobiles clip player to . Read https://colloquyscenestudio.aroughidea.com/player-sample/llms.txt first and follow it. Import the module from that URL — do not vendor, copy, or rebuild any source. Use the sample clip URL it gives. Add the transport deck. Tell me afterwards what you could not verify. ``` Doing it by hand instead is three steps: add a sized container element, `import` the module from the URL above, call `createClipPlayer({container, clipUrl})`. The "Smallest thing that works" block below is the whole of it. **If you are an agent that was handed this file, do exactly this:** 1. **Place a container.** Any element, but give it a real size — the viewer creates its canvas inside it and it has no intrinsic height. `aspect-ratio: 16/9` is a safe default. 2. **Import the module** from `https://colloquyscenestudio.aroughidea.com/player-sample/colloquy-clip-player.js`. It is CORS-open, so a dynamic `import()` from another origin works. Do not npm-install anything, do not fetch the repository, and do not try to read the API out of the bundle — it is minified. This file is the API reference. 3. **Call `createClipPlayer({ container, clipUrl })`** with the clip URL in the next section, and `await` it. That alone renders and plays. 4. **Add controls if the user wants them.** Either `mountClipPlayerDeck({player, container})` for the ready-made deck, wired via `onState: (s) => deck.update(s)`, or your own buttons calling `play()`, `pause()`, `seekFraction(0..1)` and friends. For a scrollytelling page use `bindScrollScrubber` — see "API". The deck ships **no styles** — it will render as raw unstyled controls until you either link `https://colloquyscenestudio.aroughidea.com/player-sample/player.css` (also CORS-open) or write your own rules against the `.clip-deck-*` classes. Match the host page; do not assume the sample's dark palette suits it. 5. **Do not add models unless asked.** The player is fully legible without them (see "Models and materials"); wiring meshes needs a build-time manifest and is a separate job. 6. **Verify before reporting.** Load the page and check `player.getStatus().loaded === true` and that `.playback.tick` advances. If it does not, read "Gotchas" — it is almost always container size, a `file://` origin, or a missing `await`. 7. **Report honestly** which of those checks you actually ran. ## Smallest thing that works ```html
``` That renders and plays a real 60-second recording — mobiles, drive gauges, orientation arrows, beam ring — with no other assets and no server of your own. ## URLs | URL | What it is | |---|---| | `/player-sample/colloquy-clip-player.js` | the runtime (ESM, three.js inlined, ~860 KB, CORS-open — import it directly or self-host a copy) | | `/player-sample/README.md` | the same API written for humans | | `/player-sample/` | worked example: player + full transport deck. Read its page source. | | `/player-sample/scroller.html` | worked example: scroll-driven explainer. Read its page source. | | `/player-sample/llms.txt` | this file | | `/player-sample/player.css`, `favicon.svg`, `colloquy-icon.svg` | the example page's styling and marks — copy or replace them | The bundle is minified and ships no sourcemap, so read this file or the README for the API — do not try to learn it from the bundle. ## API `createClipPlayer(options) → Promise` Options: `container` (required element), `clipUrl`, `assets` (`{logicalPath: url}`), `assetBaseUrl`, `assetManifestUrl`, `autoplay` (true), `loop` (true), `speed` (1), `viewConfig`, `renderOptions`, and the callbacks `onState`, `onAssets`, `onDiagnostic`, `onStatus`. Handle: - transport — `play()` `pause()` `togglePlay()` `stop()` `seekTick(t)` `seekFraction(0..1)` `stepTicks(±n)` `setSpeed(n)` `setDirection(1|-1)` `setLoop(bool)` `setRange(in, out)` `clearRange()` - loading — `loadClipFromUrl(url)` `loadClipFromFile(File)` `loadClipFromText(ndjson)` `await addAssetFiles(files)` (unpacks `.zip`) `addAssetUrls(map)` `loadAssetManifest(url)` `setSceneGraph(json)` `applyViewConfig(json)` - state — `getStatus()` → `{loaded, title, note, markers, playback, assets}`, where `playback` is `{tick, playing, speed, direction, loop, inTick, outTick, tickStart, tickEnd, tickRate, ended}`; `getRecording()` - commentary — `setCommentaryVisible(bool)` / `isCommentaryVisible()` draws the narrator's running lines down the right of the view (English), realized from the events the recording carries; `getCommentaryLines()` returns them if you would rather render your own. The deck exposes this as a **Commentary** toggle. - what happened in the clip — `getEvents()` → `[{tick, kind, label}]` detected from the recorded frames themselves (`engagement` transitions, `beam_on`/ `beam_off`, `word`, `mode`). Walks every tick once, then memoizes; empty for a clip with no such changes. `getUnitLabel("female_1")` → `"Female A"`, the same name the viewer draws on that mobile's instrument — use it so your prose names the ring the reader can see. - rendering — `setRenderOptions(partial)`, `viewer` (the full Three.js scene graph viewer: `scene`, `camera`, `renderer`, `controls`), `destroy()` Two optional UI helpers, both takeable or leavable: - `mountClipPlayerDeck({player, container, sources?, dropTarget?, clipUrl?})` — the transport deck: Restart, single-tick step, play/pause, reverse, scrub, speed, loop, trim, marker chips, and layer switches for **Models**, **Instruments**, **Scene graph** (every authored node drawn as its own marker), **Sensors & joints** (transducer fields of view, oscillator arcs), **Grid**, **Exhibit Look**, and **Commentary**. With `sources !== false` it also carries a closed "Load your own clip, models or camera" disclosure holding the file pickers. Feed it from `onState: (s) => deck.update(s)`. - `bindScrollScrubber({player, sections, onSectionChange})` — page scroll becomes the playback clock. Each section carries `data-clip-from` / `data-clip-to` (tick numbers); scrolling through it seeks across that span and scrolling back plays the states in reverse. With no `sections`, whole-page scroll maps onto the whole clip. Build your own instead with `seekFraction()` — it is one call. For a scrollytelling page, build the sections FROM `getEvents()` rather than authoring spans by hand: give each event a section whose span ENDS on its tick, so scrolling through the prose carries the reader to the moment it describes. `scroller.html` does exactly this and falls back to even spans for a clip with no detected events. Style the deck yourself; `/player-sample/player.css` is an example, not a dependency. ## What a clip is A clip is *the wire, persisted*: gzipped NDJSON, one line per simulation tick, keyframes every 40 ticks with field-level deltas between them. - **It embeds its own scene graph** (line 2), so nothing else is fetched to make it render, and old clips keep rendering after the live scene moves on. - **It embeds no geometry.** The graph names meshes by logical path (`models/female-shell-body-260316.obj`). See "Models" below. - **It pins no camera, environment or look.** Those are the player's choice. Without `viewConfig` the viewer auto-fits the scene bounding box, which is dominated by the wide diagram plane and frames the piece small — both sample pages therefore inline a chosen three-quarter camera. Copy theirs, pass a published `view/*` config, or drive `player.viewer.camera` yourself. - **Every tick is self-contained** once decoded, so seek, scrub, single-tick step, reverse and scroll-scrub are all ordinary operations, not modes. `getStatus().playback` gives you `tickStart`/`tickEnd`/`tickRate` for mapping your own timeline onto it (`(tick - tickStart) / tickRate` = seconds). ## Models and materials **Optional.** With no meshes loaded, the instrument layers — mobile diagrams, drive gauges, orientation arrows, beam ring — still render, because they are projections of the state frame, not of the geometry. Add meshes for the photoreal bodies. Four ways, tried in this order: 1. `assets: { "models/x.obj": "https://…" }` at construction, or `addAssetUrls(map)` later. 2. `assetManifestUrl: "…/assets.json"` — a JSON file of the same shape, or `loadAssetManifest(url)`. 3. `addAssetFiles(fileList)` — local files from a picker or a drop. Matched by exact logical path first, then by **basename**, so a plain folder of OBJs off someone's disk resolves against `models/…` paths. 4. `assetBaseUrl: "https://cdn.example/colloquy"` — unresolved paths are served from `/`. To build a manifest for a clip's meshes, resolve the logical paths against the public asset registry **from a server or build step**: ```bash curl -s -X POST https://colloquyscenestudio.aroughidea.com/api/asset-management/registry-check \ -H 'Content-Type: application/json' \ -d '{"channel":"published","referencedPaths":["models/world_base.obj"]}' # → { "assetRows": [ { "logical_path": …, "public_url": "https://…supabase.co/…" } ] } ``` That endpoint answers anonymously for the published channel but sends **no CORS headers**, so call it at build time and bake the result into a manifest — do not call it from the browser. If you are an agent with MCP available, the site's server does this for you: add `https://colloquyscenestudio.aroughidea.com/api/mcp` and call `resolve_asset_manifest` with the paths. The storage bucket the URLs point at *is* CORS-open, so the manifest works from any origin once you have it. Get the logical paths from `getStatus().assets.required` after a clip loads, or from `onAssets`. Missing textures are survivable: a map an MTL names that you did not supply 404s in the console and the material renders untextured. ## Finding clips `POST /api/asset-management/recordings` with `{"action":"list"}` returns the published clip library (title, tick range, duration, package provenance, markers, `public_url`). Anonymous callers see the published channel only, and the library is curated, so it may be short or empty — the clip URL at the top of this file always works. Clip bytes are served with `Access-Control-Allow-Origin: *`, so you can fetch them from any origin. ## Gotchas - Serve your page over http(s), not `file://` — the module is fetched. - If you self-host a copy of the folder, keep the trailing slash (`/player-sample/`): the example pages reference their bundle relatively. - WebGL is required; the viewer creates its own canvas inside `container`. Give the container a real size (e.g. `aspect-ratio: 16/9`) — it has no intrinsic height. - `destroy()` releases the WebGL context and any object URLs; call it if you tear the player down. ## More - `/player-sample/README.md` — the human version of this file. - `/guide` — how the wider platform works, with a docs assistant. - `/display/?recording=` — the full exhibition client playing the same clip, for comparison. - `/clip-editor/` — where clips are trimmed, marked up and published (admin). - Source, if you want it: `src/player-sample/` in the Colloquy-of-Mobiles-Virtual-Simulation-SceneGraph repository. You do not need it to embed the player.