lipi-protocol
v0.1.0
Published
Type-safe protocol library for stylus-to-display annotation. The wire format used by Lipi (lipi.fly.dev) and any compatible client or server.
Downloads
150
Maintainers
Readme
lipi-protocol
Type-safe wire-format definitions for stylus-to-display annotation.
npm install lipi-protocolThis is the open protocol used by Lipi — beam pen strokes from any tablet to any screen, in real time. Use these types to build a Lipi-compatible client or server in any TypeScript / JavaScript environment.
The protocol is open. The hosted service (lipi.fly.dev) is the convenience layer. If you want to roll your own annotation stack on a different transport, hosting, or UI — this package gives you the canonical message shapes to interoperate.
Why this exists
Strokes are a primitive. Like payments (Stripe), like ink (PostScript), like packets (TCP). The right way to ship a primitive is to standardize the wire format and let anyone build clients, servers, and integrations against it.
This package is the wire-format specification, in TypeScript. No runtime dependencies. Works in browsers, Node, Workers, Deno, Bun. ~3KB minified.
What's in it
- Stroke events —
stroke_start,stroke_point,stroke_end,laser_*,undo,clear. Coordinates normalized [0..1] of the captured content. Pressure-aware. Apple-Pencil-compatible. - Signaling messages —
presence,offer/answer/ice(WebRTC),renegotiate,report(telemetry),viewer_event(stroke broadcast). - Role + room model —
display,controller,viewer. Room-keyed multi-tenancy. - URL helpers —
buildSignalingUrl,buildViewerUrl,parseRoomFromLocation,generateRoomKey. - Pressure envelope —
widthForPressure(base, p)matches the canonical Lipi renderer.
Quick start
Build a viewer (OBS Browser Source, transparent overlay)
import {
buildSignalingUrl,
isSignalingMessage,
isStrokeEvent,
type StrokeEvent,
} from "lipi-protocol";
const url = buildSignalingUrl({
host: "lipi.fly.dev",
role: "viewer",
room: "your-room-key",
});
const ws = new WebSocket(url);
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (!isSignalingMessage(msg)) return;
if (msg.type === "viewer_event" && isStrokeEvent(msg.payload)) {
renderStroke(msg.payload); // ← your renderer
}
};Render with the canonical pressure envelope
import { widthForPressure, type StrokeEvent } from "lipi-protocol";
function renderStroke(s: { points: { x: number; y: number; p?: number }[]; width: number; color: string }) {
for (let i = 1; i < s.points.length; i++) {
const a = s.points[i - 1]!;
const b = s.points[i]!;
const avgP = ((a.p ?? 0.5) + (b.p ?? 0.5)) / 2;
const w = widthForPressure(s.width, avgP);
// ctx.lineWidth = w; ctx.moveTo(a.x * W, a.y * H); ctx.lineTo(b.x * W, b.y * H); ctx.stroke();
}
}Generate room keys server-side
import { generateRoomKey } from "lipi-protocol";
const key = generateRoomKey(); // → "key_a3f8...2c1b"
const pretty = generateRoomKey("class_"); // → "class_..."Wire format at a glance
Connection URL
wss://lipi.fly.dev/?role=<display|controller|viewer>&room=<room-key>Stroke point
Normalized to the captured content's intrinsic dimensions:
{ x: number, y: number, p?: number } // all in [0..1]A stroke at {x: 0.5, y: 0.5} lands at the center of the content. Period. No matter what resolution the controller or display is rendering at. Alignment is mathematical, not heuristic.
Stroke start
{
type: "stroke_start",
color: "#E63916",
width: 4,
mode: "draw" | "highlight" | "laser",
point: { x, y, p }
}Subsequent stroke_point events extend the stroke. stroke_end commits it. undo pops the last committed stroke. clear drops them all.
Signaling
WebRTC offer/answer/ICE flows through the WebSocket between display and controller. Viewer-role peers receive a copy of every stroke event wrapped in { type: "viewer_event", payload: <StrokeEvent> } — they don't participate in WebRTC.
See src/messages.ts and src/strokes.ts for the full type definitions with JSDoc.
Compatibility
- TypeScript 5.0+
- Node 18+ (uses
globalThis.crypto) - All modern browsers
- Workers / Edge runtimes (no Node-specific imports)
License
MIT — build whatever you want with this. The package has zero runtime dependencies and adds no transitive surface to your app.
Related
- Lipi (hosted service) — siddhant-rajhans.github.io/lipi
- Issues — github.com/siddhant-rajhans/lipi-protocol/issues
If you build something interesting on this protocol, drop a link in a GitHub issue — I'll add it to a "built with" section.
