FaceTime-style ephemeral touch annotation for Jitsi calls. One participant draws on what they see — primarily a screen share — and the other sees the stroke appear in real time and fade a few seconds later. It's deliberately not a persistent whiteboard.
Ships as an Nx monorepo (React Native client, web/DOM renderer, shared protocol), published open source on npm, proven in production by Genius_Native's teacher↔student video sessions.
Core value: during a live call, one person can point at a specific spot on the other person's screen and the other person actually sees where they mean — on Android, iOS, and in the session recording.
This project is under active development. Current state, package by package:
| Package | npm | Status |
|---|---|---|
@vilancer/protocol |
Published (still at 0.0.1 — not yet re-published with Phase 3's additions below). Coordinate normalization, wire protocol schema/codec, transport, and a render-agnostic stroke-lifecycle store (start/move/end/clear, hold/fade timing, defensive caps, rate limiting, outbound move coalescing) — the shared foundation the client and web renderer build on. |
|
@vilancer/react-native |
— | Private, unpublished. Phase 5 shipped the real client: <ScribbleOverlay> (casing/core freehand strokes and tap-rings over react-native-svg, driven by react-native-gesture-handler's single-pointer Gesture.Pan() and a useAnimatedProps-based fade, per useScribbleSession's store) plus useScribbleSession (transport↔store orchestration, presence via AppState, D-04 host callbacks) sitting on top of Phase 4's shared render-policy math and a probing, never-throwing fromJitsiConference adapter parallel to the web package's own. Draw mode is a host-controlled prop that fully unmounts the gesture-capture surface when off (touches pass through untouched); a receiver can flip incoming annotations off instantly with a render-side filter, with no fade-out and no handshake. The local drawer's own ink dims to a fixed opacity while the other side's presence is unknown, so nobody draws into the void. A MemoryTransport-driven integration test proves one stroke crosses a simulated drawer/viewer pair end to end with no device, conference, or host app. Not yet integrated into a real host app or Genius_Native — that's Phase 6. |
@vilancer/web (DOM renderer) |
— | Private, unpublished. Real content shipped in Phase 4: a probing, never-throwing fromJitsiConference transport adapter over lib-jitsi-meet's data channel, a resize-reactive SVG renderer mounted over jitsi-meet's #largeVideo (one remote stroke renders end to end, proven by a jsdom-based three-assertion smoke test matching ARCHITECTURE.md's real-deployment contract), and a fully-tested try/catch-wrapped bootstrap entry point (duplicate-injection guard, silent mount-failure isolation, identifiable startup log). pnpm nx build web produces both dist/jitsi-scribble.js (ES) and dist/jitsi-scribble.iife.js (IIFE) — the actual browser-injectable bundle. Deployment (the real custom-config.js/nginx mount, a real Jibri recording) is a later phase's job. |
@jitsi-scribble/native-overlay |
— | Placeholder. Android out-of-app overlay (Expo module + config plugin) lands in a later phase. iOS gets a different, PiP-composited approach instead — see Platform notes. |
Only @vilancer/protocol is published today; the other packages are private until their
phases ship.
npm install @vilancer/protocolSubpath imports, so you only pull in what you use:
import { encode, decode } from '@vilancer/protocol/codec';
import { contentRect, mapTouchToContent } from '@vilancer/protocol/geometry';
import { MemoryTransport, createMemoryTransportPair } from '@vilancer/protocol/transport';
import { StrokeStore } from '@vilancer/protocol/core';
import type { StartFrame } from '@vilancer/protocol/schema';Don't import from the bare package root (@vilancer/protocol) on React Native unless you
also need /schema — see Privacy & platform notes for why.
Strokes are never persisted server-side. They're relayed peer-to-peer over the existing Jitsi
conference data channel for the duration of the call and then gone — no whiteboard history, no
server-side storage. If a transport outside the conference is ever added, it will be
wss:///TLS only.
Android and iOS parity is the goal, but iOS can't match Android's system-wide overlay.
Android uses a SYSTEM_ALERT_WINDOW overlay; iOS composites into the
AVPictureInPictureVideoCallViewController that RTCPIPView already hosts. This asymmetry is
a deliberate, documented product decision, not an oversight.
pnpm install
pnpm nx run-many -t typecheck lint testMIT