Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 9383f719c9b37ff76411858f
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/watcher.ts
// A parent device watching one baby station: sends a connection offer, keeps the connection alive,
// reconnects on its own, and carries talk back, settings and alerts over the data channel.
import { set, subscribe, serverTimestamp } from "@bounded-sh/client";
import { StatsReader, gathered, iceConfig, randomId, type LinkStats } from "./lib/rtc";
import { parse, type FromBaby, type FromParent, type Sensitivity, type SoundName, type StationStatus } from "./lib/protocol";
export type LinkState =
| "connecting" // first try
| "live"
| "reconnecting" // the connection dropped; trying again
| "off" // the station was turned off
| "noanswer" // the station does not answer (closed, asleep or offline)
| "blocked" // the station answers, but the two devices cannot reach each other
| "removed"; // the station removed this device
export interface WatchSnapshot {
state: LinkState;
stationName: string;
status: StationStatus | null;
stats: LinkStats | null;
video: MediaStream | null;
audio: MediaStream | null;
talking: boolean;
}
const RETRY_MS = [1000, 2000, 4000, 8000, 15000, 30000];
const NOANSWER_RETRY_MS = [5000, 10000, 20000, 30000, 60000];
export class Watcher {
private pc: RTCPeerConnection | null = null;
private dc: RTCDataChannel | null = null;
private audioTx: RTCRtpTransceiver | null = null;
private talkTrack: MediaStreamTrack | null = null;
private talkWanted = false;
private session: string | null = null;
private offerId = "";
private answered = false;
private attempt = 0;
private failures = 0;
private timers: number[] = [];
private statsTimer: number | undefined;
private reader = new StatsReader();
private stops: (() => Promise<void>)[] = [];
private listeners = new Set<() => void>();
private stopped = false;
private snap: WatchSnapshot;
private state: LinkState = "connecting";
private stationName: string;
private status: StationStatus | null = null;
private stats: LinkStats | null = null;
private video: MediaStream | null = null;
private audio: MediaStream | null = null;
onLevel: ((v: number) => void) | null = null;
onAlert: ((v: number) => void) | null = null;
onName: ((name: string) => void) | null = null;
constructor(
readonly room: string,
readonly me: string,
name: string,
private prefs: { video: boolean; sensitivity: Sensitivity },
) {
this.stationName = name;
this.snap = this.build();
}
async start() {
this.stops.push(await subscribe(`rooms/${this.room}/joins/${this.me}`, { onData: (d: any) => { if (!d) this.removed(); } }));
this.stops.push(await subscribe(`rooms/${this.room}/links/${this.me}`, { onData: (d: any) => void this.onLink(d) }));
this.stops.push(await subscribe(`rooms/${this.room}`, { onData: (d: any) => this.onRoom(d) }));
const wake = () => { if (document.visibilityState === "visible" && ["reconnecting", "noanswer", "blocked"].includes(this.state)) this.retryNow(); };
document.addEventListener("visibilitychange", wake);
window.addEventListener("online", wake);
this.stops.push(async () => { document.removeEventListener("visibilitychange", wake); window.removeEventListener("online", wake); });
}
async stop() {
this.stopped = true;
this.clearTimers();
this.stopTalk();
this.closePc();
for (const s of this.stops) await s();
}
subscribe = (fn: () => void) => {
this.listeners.add(fn);
return () => { this.listeners.delete(fn); };
};
snapshot = () => this.snap;
retryNow() {
if (this.stopped || this.state === "removed") return;
this.attempt = 0;
void this.connect();
}
setVideo(on: boolean) {
this.prefs.video = on;
this.send({ t: "video", on });
}
setSensitivity(v: Sensitivity) {
this.prefs.sensitivity = v;
this.send({ t: "sensitivity", v });
}
playSound(name: SoundName | null, volume: number, minutes: number) {
this.send({ t: "sound", name, volume, minutes });
}
/** Push to talk: the microphone is on only while the button is held. */
async startTalk() {
if (this.talkTrack || !this.audioTx || this.state !== "live") return;
this.talkWanted = true;
const s = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } });
const track = s.getAudioTracks()[0];
if (!this.talkWanted || !this.audioTx) { track.stop(); return; }
this.talkTrack = track;
await this.audioTx.sender.replaceTrack(track);
this.send({ t: "talk", on: true });
this.changed();
}
stopTalk() {
this.talkWanted = false;
if (!this.talkTrack) return;
void this.audioTx?.sender.replaceTrack(null).catch(() => null);
this.talkTrack.stop();
this.talkTrack = null;
this.send({ t: "talk", on: false });
this.changed();
}
private send(m: FromParent) {
if (this.dc?.readyState === "open") this.dc.send(JSON.stringify(m));
}
private onRoom(doc: any) {
if (!doc || this.stopped || this.state === "removed") return;
if (doc.name && doc.name !== this.stationName) {
this.stationName = String(doc.name);
this.onName?.(this.stationName);
this.changed();
}
if (doc.online === false) {
this.session = String(doc.session || "");
this.clearTimers();
this.closePc();
this.setState("off");
return;
}
if (doc.session !== this.session || this.state === "off") {
this.session = String(doc.session || "");
this.attempt = 0;
this.failures = 0;
void this.connect();
}
}
private async onLink(doc: any) {
const pc = this.pc;
if (!doc || !pc || this.answered || doc.answerId !== this.offerId || pc.signalingState !== "have-local-offer") return;
this.answered = true;
this.clearTimers();
try {
await pc.setRemoteDescription(JSON.parse(String(doc.answer)));
} catch {
if (this.pc === pc) this.lost(false);
return;
}
// The station answered; if the devices still cannot reach each other, ICE fails or stalls.
this.later(() => { if (this.pc === pc && pc.connectionState !== "connected") this.lost(true); }, 20000);
}
private async connect() {
const config = await iceConfig();
if (this.stopped || this.state === "removed") return;
this.clearTimers();
this.closePc();
if (this.state !== "noanswer" && this.state !== "blocked") this.setState(this.attempt ? "reconnecting" : "connecting");
const pc = new RTCPeerConnection(config);
this.pc = pc;
const dc = pc.createDataChannel("monitor");
this.dc = dc;
pc.addTransceiver("video", { direction: "recvonly" });
// Sends nothing until push to talk puts the microphone on it.
this.audioTx = pc.addTransceiver("audio", { direction: "sendrecv" });
pc.ontrack = (e) => {
if (e.track.kind === "video") this.video = new MediaStream([e.track]);
else this.audio = new MediaStream([e.track]);
this.changed();
};
dc.onopen = () => this.send({ t: "hello", video: this.prefs.video, sensitivity: this.prefs.sensitivity });
dc.onmessage = (m) => this.onMessage(m.data);
dc.onclose = () => { if (this.pc === pc) this.lost(false); };
pc.onconnectionstatechange = () => {
if (this.pc !== pc) return;
const s = pc.connectionState;
if (s === "connected") {
this.attempt = 0;
this.failures = 0;
this.clearTimers();
this.setState("live");
this.watchStats(pc);
} else if (s === "failed") {
this.lost(true);
} else if (s === "disconnected") {
this.later(() => { if (this.pc === pc && pc.connectionState !== "connected") this.lost(false); }, 5000);
}
};
try {
await pc.setLocalDescription(await pc.createOffer());
await gathered(pc);
if (this.pc !== pc) return;
this.offerId = randomId(12);
this.answered = false;
await set(`rooms/${this.room}/links/${this.me}`, {
offer: JSON.stringify(pc.localDescription), offerId: this.offerId, session: this.session || "", answer: "", answerId: "", at: serverTimestamp(),
});
} catch {
if (this.pc === pc) this.lost(false);
return;
}
this.later(() => { if (this.pc === pc && !this.answered) this.noAnswer(); }, 12000);
}
private lost(iceFailed: boolean) {
if (this.stopped || this.state === "off" || this.state === "removed") return;
const wasAnswered = this.answered;
this.clearTimers();
this.closePc();
if (iceFailed && wasAnswered) this.failures++;
this.attempt++;
this.setState(this.failures >= 2 ? "blocked" : "reconnecting");
this.later(() => void this.connect(), this.failures >= 2 ? 30000 : RETRY_MS[Math.min(this.attempt - 1, RETRY_MS.length - 1)]);
}
private noAnswer() {
this.clearTimers();
this.closePc();
this.attempt++;
this.setState("noanswer");
this.later(() => void this.connect(), NOANSWER_RETRY_MS[Math.min(this.attempt - 1, NOANSWER_RETRY_MS.length - 1)]);
}
private removed() {
this.clearTimers();
this.stopTalk();
this.closePc();
this.setState("removed");
}
private onMessage(raw: unknown) {
const m = parse<FromBaby>(raw);
if (!m) return;
if (m.t === "level") this.onLevel?.(m.v);
else if (m.t === "alert") this.onAlert?.(m.v);
else if (m.t === "status") { this.status = m; this.changed(); }
}
private watchStats(pc: RTCPeerConnection) {
clearInterval(this.statsTimer);
const read = async () => {
if (this.pc !== pc) return clearInterval(this.statsTimer);
this.stats = await this.reader.read(pc).catch(() => this.stats);
this.changed();
};
void read();
this.statsTimer = window.setInterval(read, 3000);
}
private closePc() {
clearInterval(this.statsTimer);
if (this.talkTrack) this.stopTalk();
const pc = this.pc;
this.pc = null;
this.dc = null;
this.audioTx = null;
this.answered = false;
this.video = null;
this.audio = null;
this.stats = null;
pc?.close();
}
private later(fn: () => void, ms: number) {
this.timers.push(window.setTimeout(fn, ms));
}
private clearTimers() {
for (const t of this.timers) clearTimeout(t);
this.timers = [];
}
private setState(s: LinkState) {
this.state = s;
this.changed();
}
private changed() {
this.snap = this.build();
for (const fn of this.listeners) fn();
}
private build(): WatchSnapshot {
return {
state: this.state,
stationName: this.stationName,
status: this.state === "live" ? this.status : null,
stats: this.stats,
video: this.video,
audio: this.audio,
talking: Boolean(this.talkTrack),
};
}
}
Change history
Move store links to openapps.free
Release with preview
Bind agent address in policy
Use OpenApps service URLs and add contribution strip (retry)
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync