|
|
|
@@ -24,12 +24,12 @@ npm registry** (`code.lotusguild.org`) — zero new infra.
|
|
|
|
|
|
|
|
|
|
### 9.1 Version → tag → commit mapping (LOCKED)
|
|
|
|
|
|
|
|
|
|
| Source | Value |
|
|
|
|
|
| :-- | :-- |
|
|
|
|
|
| cinny `package.json` pin | `@element-hq/element-call-embedded@0.20.1` |
|
|
|
|
|
| Bundle self-report (`VITE_APP_VERSION`/`appVersion`) | `embedded-v0.20.1` |
|
|
|
|
|
| npm registry `gitHead` for 0.20.1 | `2d74c48151d9edc01c65a22a91478aac81bf24d0` |
|
|
|
|
|
| GitHub tag `v0.20.1` → commit | `2d74c48…` ✅ **same commit** |
|
|
|
|
|
| Source | Value |
|
|
|
|
|
| :--------------------------------------------------- | :----------------------------------------- |
|
|
|
|
|
| cinny `package.json` pin | `@element-hq/element-call-embedded@0.20.1` |
|
|
|
|
|
| Bundle self-report (`VITE_APP_VERSION`/`appVersion`) | `embedded-v0.20.1` |
|
|
|
|
|
| npm registry `gitHead` for 0.20.1 | `2d74c48151d9edc01c65a22a91478aac81bf24d0` |
|
|
|
|
|
| GitHub tag `v0.20.1` → commit | `2d74c48…` ✅ **same commit** |
|
|
|
|
|
|
|
|
|
|
→ **Fork from upstream tag `v0.20.1` (commit `2d74c48`).** The embedded package
|
|
|
|
|
version equals the element-call release tag; repo `package.json` version is
|
|
|
|
@@ -49,13 +49,13 @@ upstream EC's own, not ours.
|
|
|
|
|
- **Node `24`** (`.node-version`), **pnpm `10.33.0`** (`packageManager` field,
|
|
|
|
|
via corepack).
|
|
|
|
|
- Build: **`pnpm run build:embedded`** = `vite build --config
|
|
|
|
|
vite-embedded.config.ts` with `NODE_OPTIONS=--max-old-space-size=16384`.
|
|
|
|
|
vite-embedded.config.ts` with `NODE_OPTIONS=--max-old-space-size=16384`.
|
|
|
|
|
- Output dir is **repo-root `dist/`**; CI stages it into **`embedded/web/dist`**
|
|
|
|
|
(the `embedded/web/` dir holds the publish template: `package.json`, README,
|
|
|
|
|
both LICENSE files).
|
|
|
|
|
- Publish workflow upstream = `.github/workflows/publish-embedded-packages.yaml`:
|
|
|
|
|
builds → `npm version <tag> --no-git-tag-version` → `npm publish --provenance
|
|
|
|
|
--access public` to npmjs as `@element-hq/element-call-embedded`. (Also
|
|
|
|
|
--access public` to npmjs as `@element-hq/element-call-embedded`. (Also
|
|
|
|
|
Android/Maven + iOS/SwiftPM — irrelevant; we are web-only.)
|
|
|
|
|
|
|
|
|
|
### 9.4 Build reproduction — PARITY CONFIRMED
|
|
|
|
@@ -68,8 +68,8 @@ shipped npm dist:
|
|
|
|
|
wasm, audio, JSON locale files, and `IndexedDBWorker`.
|
|
|
|
|
- **Only 5 JS chunks differ** (`index`, `pako.esm`, `polyfill-force`,
|
|
|
|
|
`rust-crypto`, `spa`) — **cause isolated to the version define**: our local
|
|
|
|
|
build baked `appVersion:\`dev\`` (because `VITE_APP_VERSION` was unset) vs the
|
|
|
|
|
npm build's `appVersion:\`embedded-v0.20.1\``. `index.html` is identical modulo
|
|
|
|
|
build baked `appVersion:\`dev\``(because`VITE_APP_VERSION`was unset) vs the
|
|
|
|
|
npm build's`appVersion:\`embedded-v0.20.1\``. `index.html` is identical modulo
|
|
|
|
|
the hashed asset filenames. **Benign** — our CI sets the version from the git
|
|
|
|
|
tag, so a tagged CI build will match.
|
|
|
|
|
|
|
|
|
@@ -95,7 +95,7 @@ to the Gitea npm registry (needs `secrets.GITEA_NPM_TOKEN`).
|
|
|
|
|
`package.json` dep + `vite.config.js` `viteStaticCopy` src. `npm install`
|
|
|
|
|
swapped the package (resolved from Gitea), `npm run build` succeeded,
|
|
|
|
|
`dist/public/element-call/` populated, bundle reports `appVersion:
|
|
|
|
|
embedded-v0.20.1`, **denoise shim injected + all denoise assets copied**
|
|
|
|
|
embedded-v0.20.1`, **denoise shim injected + all denoise assets copied**
|
|
|
|
|
(injection layer unchanged). **These cinny edits are staged in the working
|
|
|
|
|
tree, NOT committed/pushed** — pushing triggers CI → desktop → deploy, so it's
|
|
|
|
|
gated on the §D live test (see §10).
|
|
|
|
@@ -390,7 +390,7 @@ re-homed into the fork.
|
|
|
|
|
EC is **same-origin** today (self-hosted under our domain;
|
|
|
|
|
`iframe.sandbox` includes `allow-same-origin`; we read `contentDocument`), and
|
|
|
|
|
**as of 2026-06-29 we own the fork's source** (`@lotusguild/element-call-embedded`).
|
|
|
|
|
The _practical_ point it made still holds *until we ship the audio-inject API*:
|
|
|
|
|
The _practical_ point it made still holds _until we ship the audio-inject API_:
|
|
|
|
|
**LiveKit's `LocalAudioTrack` lives in EC's module scope**, not on `window`, so
|
|
|
|
|
cinny can't reach it even same-origin — which is why the in-call soundboard had
|
|
|
|
|
to be local-playback-only. **The fork removes this wall:** EC can expose a real
|
|
|
|
@@ -441,13 +441,15 @@ go live:
|
|
|
|
|
## 11. Phase 2 — implementation seams (mapped 2026-06-29)
|
|
|
|
|
|
|
|
|
|
The exact integration points for each Phase 2 item, found by reading the EC fork
|
|
|
|
|
+ cinny source. **All of these are media-path / in-call features that cannot be
|
|
|
|
|
functionally verified without a live Matrix + LiveKit call** — implement each as
|
|
|
|
|
a minimal, **feature-flagged, additive** diff (no behavior change unless cinny
|
|
|
|
|
opts in), build-verify the fork (`pnpm build:embedded`, ~15s) AND cinny
|
|
|
|
|
(`npm run build`), then gate shipping on `LOTUS_TESTING.md` §D.
|
|
|
|
|
|
|
|
|
|
- cinny source. **All of these are media-path / in-call features that cannot be
|
|
|
|
|
functionally verified without a live Matrix + LiveKit call** — implement each as
|
|
|
|
|
a minimal, **feature-flagged, additive** diff (no behavior change unless cinny
|
|
|
|
|
opts in), build-verify the fork (`pnpm build:embedded`, ~15s) AND cinny
|
|
|
|
|
(`npm run build`), then gate shipping on `LOTUS_TESTING.md` §D.
|
|
|
|
|
|
|
|
|
|
**Shared widget channel (the backbone for #2/#3/#4/#7):**
|
|
|
|
|
|
|
|
|
|
- EC→cinny: `widget.api.transport.send("io.lotus.<x>", data)` (see
|
|
|
|
|
`element-call/src/widget.ts`).
|
|
|
|
|
- cinny→EC actions: add the action name to the `lazyActions` allow-list in
|
|
|
|
@@ -464,43 +466,43 @@ opts in), build-verify the fork (`pnpm build:embedded`, ~15s) AND cinny
|
|
|
|
|
Mount in `room/InCallView.tsx` via `useEffect` guarded by `widget !== null`.
|
|
|
|
|
cinny: `listenAction("io.lotus.call_state")` in `CallEmbed.ts`, feed
|
|
|
|
|
`hooks/useCallSpeakers.ts` → delete its `contentDocument` `[data-muted]` /
|
|
|
|
|
`[data-video-fit]` scrape. *Additive, low risk.*
|
|
|
|
|
`[data-video-fit]` scrape. _Additive, low risk._
|
|
|
|
|
|
|
|
|
|
**#4 spotlight/focus** — EC: add `io.lotus.focus_participant` to the `lazyActions`
|
|
|
|
|
list (`widget.ts`), drive `vm`'s spotlight (`spotlightSpeaker$` /
|
|
|
|
|
`spotlight$` in `CallViewModel.ts:898/1001`) to pin a given identity, coexisting
|
|
|
|
|
with `hasRemoteScreenShares$` (L1008). cinny: replace
|
|
|
|
|
`CallControl.ts` `focusCameraParticipant` `.click()` walk with
|
|
|
|
|
`transport.send("io.lotus.focus_participant", {userId})`. *Additive, low risk.*
|
|
|
|
|
`transport.send("io.lotus.focus_participant", {userId})`. _Additive, low risk._
|
|
|
|
|
|
|
|
|
|
**#3 audio-inject** — EC: add `io.lotus.inject_audio` action; mix an
|
|
|
|
|
`AudioBufferSourceNode` into the published mic track. The local publish path is
|
|
|
|
|
`state/CallViewModel/localMember/Publisher.ts` + `LocalMember.ts` (LiveKit
|
|
|
|
|
`localParticipant`); create a `MediaStreamAudioDestinationNode`, mix mic + clip,
|
|
|
|
|
`replaceTrack`. cinny soundboard calls the action instead of local-only playback.
|
|
|
|
|
*Medium; touches publish path → live-test carefully.*
|
|
|
|
|
_Medium; touches publish path → live-test carefully._
|
|
|
|
|
|
|
|
|
|
**#1 denoise-in-source** — replace the cinny `lotusDenoise()` `getUserMedia`
|
|
|
|
|
monkeypatch with a real processing stage in EC's mic capture
|
|
|
|
|
(`Publisher.ts`/`LocalMember.ts`; note EC has a `TrackProcessorContext` +
|
|
|
|
|
`BlurBackgroundTransformer` precedent in `livekit/`). EC re-runs it on every
|
|
|
|
|
(re)publish → fixes A7. Remove `vite.config.js` `lotusDenoise()` + URL params in
|
|
|
|
|
`CallEmbed.ts`; move `denoise/` assets into the fork. *Highest value, highest
|
|
|
|
|
risk — most live testing.*
|
|
|
|
|
`CallEmbed.ts`; move `denoise/` assets into the fork. _Highest value, highest
|
|
|
|
|
risk — most live testing._
|
|
|
|
|
|
|
|
|
|
**#5 theming** — add a Lotus/TDS theme in EC's theme system (`src/useTheme.ts` +
|
|
|
|
|
EC theme tokens / CSS); driven by the existing `setTheme()` channel cinny already
|
|
|
|
|
calls (`CallEmbed.ts:277`). Bake transparent background. Delete cinny's
|
|
|
|
|
`applyStyles()` injection + `background:none !important`. *Medium.*
|
|
|
|
|
`applyStyles()` injection + `background:none !important`. _Medium._
|
|
|
|
|
|
|
|
|
|
**#6 in-call decorations** — render the decoration APNG in EC's tile component
|
|
|
|
|
(`tile/GridTile.tsx`); pass slugs via widget member data. cinny already has the
|
|
|
|
|
decoration data + `AvatarDecoration` (lobby `CallMemberCard.tsx`). *Medium-Large.*
|
|
|
|
|
decoration data + `AvatarDecoration` (lobby `CallMemberCard.tsx`). _Medium-Large._
|
|
|
|
|
|
|
|
|
|
**#7 quality controls** — set audio `maxBitrate` via
|
|
|
|
|
`RTCRtpSender.setParameters` and screenshare `getDisplayMedia` constraints in
|
|
|
|
|
EC's publish path (`Publisher.ts`); configurable via `config.json` / a widget
|
|
|
|
|
message. Keep the server `voice-limit-guard` as enforcement. *Medium.*
|
|
|
|
|
message. Keep the server `voice-limit-guard` as enforcement. _Medium._
|
|
|
|
|
|
|
|
|
|
**Rollback:** revert the 4 cinny files (restores `@element-hq/...@0.20.1` from
|
|
|
|
|
npmjs). The fork repo/package can stay; nothing else depends on it until pushed.
|
|
|
|
@@ -529,15 +531,15 @@ Fork modules live under `element-call/src/lotus/*`; mounts are `useEffect`s in
|
|
|
|
|
`src/room/InCallView.tsx`. Custom widget actions are in `src/lotus/lotusActions.ts`
|
|
|
|
|
(toWidget ones allow-listed in `src/widget.ts`).
|
|
|
|
|
|
|
|
|
|
| # | Feature | Enable via | EC module |
|
|
|
|
|
| :- | :- | :- | :- |
|
|
|
|
|
| 2 | Speaker/mute/camera state → host | URL `lotusCallState=1` | `lotusCallState.ts` (sends `io.lotus.call_state`) |
|
|
|
|
|
| 4 | Focus/spotlight a participant (works during screenshare) | action `io.lotus.focus_participant {userId|null}` | `lotusFocus.ts` + `CallViewModel` spotlight override |
|
|
|
|
|
| 3 | Soundboard audio-inject (heard by peers) | URL `lotusAudioInject=1` + action `io.lotus.inject_audio {url,volume?}` | `lotusAudioInject.ts` |
|
|
|
|
|
| 7 | Audio/screenshare quality caps | action `io.lotus.set_quality {audioMaxBitrate?,screenshareMaxBitrate?,screenshareMaxFramerate?}` | `lotusQuality.ts` |
|
|
|
|
|
| 5 | Transparent bg + Lotus theme | URL `lotusTransparent=1` / `lotusTheme=1` | `useTheme.ts` + `index.css` |
|
|
|
|
|
| 6 | In-call avatar decorations | action `io.lotus.decorations {decorations:{userId:url}}` | `lotusDecorations.ts` + `MediaView.tsx` |
|
|
|
|
|
| 1 | ML denoise in-source (fixes A7) | URL **`lotusDenoiseSource=1`** (+`lotusModel`,`lotusGate`,`lotusGateThreshold`,`lotusDenoiseBase`) — deliberately NOT the existing `lotusDenoise=ml` (that drives the host shim; reusing it would double-process) | `lotusDenoise.ts` + `lotusDenoiseProcessor.ts` |
|
|
|
|
|
| # | Feature | Enable via | EC module |
|
|
|
|
|
| :-- | :------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | ---------------------------------------------------- |
|
|
|
|
|
| 2 | Speaker/mute/camera state → host | URL `lotusCallState=1` | `lotusCallState.ts` (sends `io.lotus.call_state`) |
|
|
|
|
|
| 4 | Focus/spotlight a participant (works during screenshare) | action `io.lotus.focus_participant {userId | null}` | `lotusFocus.ts` + `CallViewModel` spotlight override |
|
|
|
|
|
| 3 | Soundboard audio-inject (heard by peers) | URL `lotusAudioInject=1` + action `io.lotus.inject_audio {url,volume?}` | `lotusAudioInject.ts` |
|
|
|
|
|
| 7 | Audio/screenshare quality caps | action `io.lotus.set_quality {audioMaxBitrate?,screenshareMaxBitrate?,screenshareMaxFramerate?}` | `lotusQuality.ts` |
|
|
|
|
|
| 5 | Transparent bg + Lotus theme | URL `lotusTransparent=1` / `lotusTheme=1` | `useTheme.ts` + `index.css` |
|
|
|
|
|
| 6 | In-call avatar decorations | action `io.lotus.decorations {decorations:{userId:url}}` | `lotusDecorations.ts` + `MediaView.tsx` |
|
|
|
|
|
| 1 | ML denoise in-source (fixes A7) | URL **`lotusDenoiseSource=1`** (+`lotusModel`,`lotusGate`,`lotusGateThreshold`,`lotusDenoiseBase`) — deliberately NOT the existing `lotusDenoise=ml` (that drives the host shim; reusing it would double-process) | `lotusDenoise.ts` + `lotusDenoiseProcessor.ts` |
|
|
|
|
|
|
|
|
|
|
**Security hardening applied** (holistic audit): `lotusDenoiseBase` forced
|
|
|
|
|
same-origin before `audioWorklet.addModule` (was an arbitrary-code-load vector
|
|
|
|
@@ -561,7 +563,7 @@ The EC side is additive and dormant until cinny opts in. Host work needed (in
|
|
|
|
|
> `deepfilternet` selections silently fall back to rnnoise (now logged). Restrict
|
|
|
|
|
> the embedded-call model picker to rnnoise/speex, or implement the others in
|
|
|
|
|
> `lotusDenoiseProcessor.ts`. **F4** — cinny sends `lotusNativeNS`, which the
|
|
|
|
|
> fork ignores; drop it or wire it in. **F7** — no widget *capability* changes
|
|
|
|
|
> fork ignores; drop it or wire it in. **F7** — no widget _capability_ changes
|
|
|
|
|
> needed; custom actions bypass capability checks.
|
|
|
|
|
|
|
|
|
|
1. **Set the URL flags** on the widget iframe params (the `URLSearchParams` in
|
|
|
|
@@ -649,4 +651,5 @@ shim). This enables a low-risk incremental rollout even without a staging env:
|
|
|
|
|
**Blocker to step 1:** publishing the `lotus` branch needs a Gitea npm token
|
|
|
|
|
(the admin token used for the `0.20.1` parity publish was deleted). Either
|
|
|
|
|
provide a token for a manual `npm publish`, or stand up the Gitea Actions runner
|
|
|
|
|
+ `GITEA_NPM_TOKEN` secret so a `v0.20.1-lotus.1` tag auto-publishes.
|
|
|
|
|
|
|
|
|
|
- `GITEA_NPM_TOKEN` secret so a `v0.20.1-lotus.1` tag auto-publishes.
|
|
|
|
|