@broberg/call 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,7 @@ await call.setScreenShare(true); // the browser asks which s
59
59
  await call.setCamera(true, { deviceId }); // deviceId optional
60
60
  call.localMedia; // { camera, screenShare } — what you send now
61
61
  call.onLocalMedia((m) => setButtons(m)); // at once + on every change, INCLUDING the browser's own «stop sharing» button
62
- call.onVideos((videos) => renderTiles(videos)); // [{ id, identity, name, isLocal, source: "camera"|"screen_share", attach(el), detach(el) }]
62
+ call.onVideos((videos) => renderTiles(videos)); // [{ id, identity, name, isLocal, source: "camera"|"screen_share", attach(el), detach(el) }] — a turned-off (muted) camera is NOT in the list (0.2.1)
63
63
  videos[0].attach(videoEl); // sets autoplay + playsinline; your own picture is muted
64
64
  call.setAudioOnly(true); // weak connection: stop receiving others' video, keep sound
65
65
  ```
package/dist/client.cjs CHANGED
@@ -43,7 +43,8 @@ async function joinVoice({ url, token, publish = true }) {
43
43
  const v = localVideo(pub, room.localParticipant);
44
44
  if (v) local.push(v);
45
45
  }
46
- return [...local, ...remoteVideos.values()];
46
+ const remote = [...remoteVideos.values()].filter(({ pub }) => !pub.isMuted).map(({ video }) => video);
47
+ return [...local, ...remote];
47
48
  };
48
49
  const emitVideos = () => {
49
50
  if (left) return;
@@ -68,11 +69,11 @@ async function joinVoice({ url, token, publish = true }) {
68
69
  const ids = speakers.map((s) => s.identity);
69
70
  speakingCbs.forEach((cb) => cb(ids));
70
71
  }],
71
- [livekitClient.RoomEvent.TrackSubscribed, (track, _pub, p) => {
72
+ [livekitClient.RoomEvent.TrackSubscribed, (track, pub, p) => {
72
73
  if (track.kind === livekitClient.Track.Kind.Video) {
73
74
  const source = toVideoSource(track.source);
74
75
  if (!source) return;
75
- remoteVideos.set(track, videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track));
76
+ remoteVideos.set(track, { video: videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track), pub });
76
77
  emitVideos();
77
78
  return;
78
79
  }
@@ -98,7 +99,11 @@ async function joinVoice({ url, token, publish = true }) {
98
99
  // person ends a screen share with the BROWSER's stop button, which never
99
100
  // passes through setScreenShare.
100
101
  [livekitClient.RoomEvent.LocalTrackPublished, onLocalChange],
101
- [livekitClient.RoomEvent.LocalTrackUnpublished, onLocalChange]
102
+ [livekitClient.RoomEvent.LocalTrackUnpublished, onLocalChange],
103
+ // F104.10 — LiveKit turns a camera off by MUTING it, for you and for
104
+ // everyone else, so a mute is a picture going away (and unmute coming back).
105
+ [livekitClient.RoomEvent.TrackMuted, onLocalChange],
106
+ [livekitClient.RoomEvent.TrackUnmuted, onLocalChange]
102
107
  ];
103
108
  for (const [event, fn] of handlers) room.on(event, fn);
104
109
  try {
@@ -197,7 +202,7 @@ function toVideoSource(source) {
197
202
  }
198
203
  function localVideo(pub, p) {
199
204
  const source = toVideoSource(pub.source);
200
- if (!pub.track || !source) return void 0;
205
+ if (!pub.track || !source || pub.isMuted) return void 0;
201
206
  return videoOf(`${p.identity}:${pub.trackSid}`, p, true, source, pub.track);
202
207
  }
203
208
  function videoOf(id, p, isLocal, source, track) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"names":["Room","Track","RoomEvent"],"mappings":"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","file":"client.cjs","sourcesContent":["// @broberg/call/client — the browser's half of a call (F104.3; camera + screen share F104.7).\n//\n// A thin layer over livekit-client that does the four things every app would\n// otherwise get slightly wrong on its own: open the microphone with noise\n// suppression, echo cancellation and auto gain; actually PLAY the other people\n// (a subscribed audio track is silent until it is attached to an element);\n// keep \"mute everyone\" and per-person volume apart so unmuting restores what\n// each person was set to; and stop calling the app back after leave().\n// F104.7 adds camera and screen share: simulcast + adaptive stream + dynacast\n// so 2–10 people with video stay usable, the browser's own «stop sharing»\n// button is reported back to the app, and audio-only drops incoming video.\n// No node: imports.\n\nimport { Room, RoomEvent, Track } from \"livekit-client\";\nimport type { LocalTrackPublication, Participant, RemoteParticipant, RemoteTrack, RemoteTrackPublication } from \"livekit-client\";\n\nexport interface VoiceParticipant {\n identity: string;\n name: string;\n isLocal: boolean;\n}\n\n/** F104.7 — one video picture in the call: a camera or a shared screen, yours or someone else's. */\nexport interface CallVideo {\n /** Stable for as long as this picture exists — use it as a render key. */\n id: string;\n identity: string;\n name: string;\n isLocal: boolean;\n source: \"camera\" | \"screen_share\";\n /** Show it in a `<video>` element (autoplay + playsinline are set for you). */\n attach(el: HTMLVideoElement): void;\n detach(el: HTMLVideoElement): void;\n}\n\n/** F104.7 — what you are sending right now. */\nexport interface LocalMedia {\n camera: boolean;\n screenShare: boolean;\n}\n\nexport interface JoinOptions {\n /** The media server, e.g. `wss://voice.broberg.ai`. */\n url: string;\n /** A ticket from `createRoomToken` on the app's server. */\n token: string;\n /**\n * F104.4 — `false` joins as a listener: the microphone is never opened\n * (a listen-only ticket would be refused if it tried). Default `true`.\n */\n publish?: boolean;\n}\n\nexport interface VoiceCall {\n mute(): Promise<void>;\n unmute(): Promise<void>;\n readonly muted: boolean;\n /** Silence everyone else (true) or bring back each person's own volume (false). */\n setOutputMuted(muted: boolean): void;\n setInputDevice(deviceId: string): Promise<void>;\n setOutputDevice(deviceId: string): Promise<void>;\n /** 0–1 for one person. Kept while output is muted and applied on unmute. */\n setVolume(identity: string, volume: number): void;\n /** Identities of the people speaking right now, loudest first. Returns an unsubscribe. */\n onSpeaking(cb: (identities: string[]) => void): () => void;\n /** Everyone in the call, you included; called at once and on every join/leave. Returns an unsubscribe. */\n onParticipants(cb: (participants: VoiceParticipant[]) => void): () => void;\n /** F104.7 — turn your camera on or off. Needs a ticket with `\"camera\"` in `sources`. */\n setCamera(on: boolean, opts?: { deviceId?: string }): Promise<void>;\n /**\n * F104.7 — share your screen (the browser asks which screen/window/tab) or stop.\n * `audio: true` also offers the tab's sound (needs `\"screen_share_audio\"` in the ticket).\n * Stopping with the browser's own «stop sharing» button is reported through `onLocalMedia`.\n */\n setScreenShare(on: boolean, opts?: { audio?: boolean }): Promise<void>;\n readonly localMedia: LocalMedia;\n /** Called at once and whenever your camera/screen share turns on or off — also from the browser's own stop button. Returns an unsubscribe. */\n onLocalMedia(cb: (media: LocalMedia) => void): () => void;\n /** Every video picture in the call (yours included), at once and whenever one comes or goes. Returns an unsubscribe. */\n onVideos(cb: (videos: CallVideo[]) => void): () => void;\n /** F104.7 — `true` stops receiving other people's video (weak connection); sound is untouched. `false` brings it back. */\n setAudioOnly(on: boolean): void;\n readonly audioOnly: boolean;\n leave(): Promise<void>;\n}\n\n/** F104.7 — the room settings that keep 2–10 people with video usable. */\nexport const VIDEO_ROOM_OPTIONS = { adaptiveStream: true, dynacast: true, publishDefaults: { simulcast: true } } as const;\n\nexport const AUDIO_CAPTURE = { noiseSuppression: true, echoCancellation: true, autoGainControl: true } as const;\n\nexport async function joinVoice({ url, token, publish = true }: JoinOptions): Promise<VoiceCall> {\n const room = new Room({ audioCaptureDefaults: { ...AUDIO_CAPTURE }, ...VIDEO_ROOM_OPTIONS, publishDefaults: { ...VIDEO_ROOM_OPTIONS.publishDefaults } });\n const localMediaCbs = new Set<(m: LocalMedia) => void>();\n const videoCbs = new Set<(v: CallVideo[]) => void>();\n const remoteVideos = new Map<RemoteTrack, CallVideo>();\n let audioOnly = false;\n const speakingCbs = new Set<(ids: string[]) => void>();\n const participantCbs = new Set<(ps: VoiceParticipant[]) => void>();\n const volumes = new Map<string, number>();\n const audioEls = new Map<RemoteTrack, HTMLMediaElement[]>();\n let outputMuted = false;\n let muted = !publish;\n let left = false;\n\n const participants = (): VoiceParticipant[] => [\n toVoiceParticipant(room.localParticipant, true),\n ...[...room.remoteParticipants.values()].map((p) => toVoiceParticipant(p, false)),\n ];\n const emitParticipants = () => {\n if (left) return;\n const list = participants();\n participantCbs.forEach((cb) => cb(list));\n };\n const applyVolume = (p: RemoteParticipant) => p.setVolume(outputMuted ? 0 : (volumes.get(p.identity) ?? 1));\n\n const localMedia = (): LocalMedia => ({\n camera: room.localParticipant.isCameraEnabled,\n screenShare: room.localParticipant.isScreenShareEnabled,\n });\n const emitLocalMedia = () => {\n if (left) return;\n const m = localMedia();\n localMediaCbs.forEach((cb) => cb(m));\n };\n const videos = (): CallVideo[] => {\n const local: CallVideo[] = [];\n for (const pub of room.localParticipant.videoTrackPublications.values()) {\n const v = localVideo(pub, room.localParticipant);\n if (v) local.push(v);\n }\n return [...local, ...remoteVideos.values()];\n };\n const emitVideos = () => {\n if (left) return;\n const list = videos();\n videoCbs.forEach((cb) => cb(list));\n };\n const onLocalChange = () => { emitLocalMedia(); emitVideos(); };\n // Audio-only only ever touches VIDEO publications — sound keeps flowing.\n const applyAudioOnly = (pub: RemoteTrackPublication) => {\n if (pub.kind === Track.Kind.Video) pub.setSubscribed(!audioOnly);\n };\n\n const handlers: [RoomEvent, (...args: any[]) => void][] = [\n [RoomEvent.ParticipantConnected, (p: RemoteParticipant) => { applyVolume(p); emitParticipants(); }],\n [RoomEvent.ParticipantDisconnected, () => emitParticipants()],\n [RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n if (left) return;\n const ids = speakers.map((s) => s.identity);\n speakingCbs.forEach((cb) => cb(ids));\n }],\n [RoomEvent.TrackSubscribed, (track: RemoteTrack, _pub: unknown, p: RemoteParticipant) => {\n if (track.kind === Track.Kind.Video) {\n const source = toVideoSource(track.source);\n if (!source) return;\n remoteVideos.set(track, videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track));\n emitVideos();\n return;\n }\n if (track.kind !== Track.Kind.Audio) return;\n const el = track.attach();\n document.body.appendChild(el);\n audioEls.set(track, [el]);\n applyVolume(p);\n }],\n [RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n if (remoteVideos.delete(track)) {\n track.detach();\n emitVideos();\n return;\n }\n track.detach().forEach((el) => el.remove());\n audioEls.delete(track);\n }],\n [RoomEvent.TrackPublished, (pub: RemoteTrackPublication) => { if (audioOnly) applyAudioOnly(pub); }],\n // Fired for our own camera/screen going on or off — including when the\n // person ends a screen share with the BROWSER's stop button, which never\n // passes through setScreenShare.\n [RoomEvent.LocalTrackPublished, onLocalChange],\n [RoomEvent.LocalTrackUnpublished, onLocalChange],\n ];\n for (const [event, fn] of handlers) room.on(event, fn);\n\n try {\n await room.connect(url, token);\n if (publish) await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE });\n // joinVoice is called from a click; this is the gesture browsers require before audio may play.\n await room.startAudio();\n } catch (e) {\n // A refused microphone must not leave the person connected and listening\n // with no handle to leave — the caller only gets the rejection.\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n await room.disconnect();\n throw e;\n }\n\n return {\n get muted() { return muted; },\n async mute() { await room.localParticipant.setMicrophoneEnabled(false); muted = true; },\n async unmute() { await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE }); muted = false; },\n setOutputMuted(m) {\n outputMuted = m;\n room.remoteParticipants.forEach(applyVolume);\n },\n async setInputDevice(deviceId) { await room.switchActiveDevice(\"audioinput\", deviceId); },\n async setOutputDevice(deviceId) { await room.switchActiveDevice(\"audiooutput\", deviceId); },\n setVolume(identity, volume) {\n volumes.set(identity, Math.min(1, Math.max(0, volume)));\n const p = room.remoteParticipants.get(identity);\n if (p) applyVolume(p);\n },\n onSpeaking(cb) {\n speakingCbs.add(cb);\n return () => speakingCbs.delete(cb);\n },\n onParticipants(cb) {\n participantCbs.add(cb);\n if (!left) cb(participants());\n return () => participantCbs.delete(cb);\n },\n async setCamera(on, opts) {\n await room.localParticipant.setCameraEnabled(on, opts?.deviceId ? { deviceId: opts.deviceId } : undefined);\n onLocalChange();\n },\n async setScreenShare(on, opts) {\n await room.localParticipant.setScreenShareEnabled(on, on ? { audio: opts?.audio === true } : undefined);\n onLocalChange();\n },\n get localMedia() { return localMedia(); },\n onLocalMedia(cb) {\n localMediaCbs.add(cb);\n if (!left) cb(localMedia());\n return () => localMediaCbs.delete(cb);\n },\n onVideos(cb) {\n videoCbs.add(cb);\n if (!left) cb(videos());\n return () => videoCbs.delete(cb);\n },\n get audioOnly() { return audioOnly; },\n setAudioOnly(on) {\n audioOnly = on;\n room.remoteParticipants.forEach((p) => p.trackPublications.forEach(applyAudioOnly));\n },\n async leave() {\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n speakingCbs.clear();\n participantCbs.clear();\n localMediaCbs.clear();\n videoCbs.clear();\n remoteVideos.forEach((_v, track) => track.detach());\n remoteVideos.clear();\n audioEls.forEach((_els, track) => track.detach().forEach((el) => el.remove()));\n audioEls.clear();\n await room.disconnect();\n },\n };\n}\n\nfunction toVideoSource(source: Track.Source): CallVideo[\"source\"] | undefined {\n if (source === Track.Source.Camera) return \"camera\";\n if (source === Track.Source.ScreenShare) return \"screen_share\";\n return undefined;\n}\n\nfunction localVideo(pub: LocalTrackPublication, p: Participant): CallVideo | undefined {\n const source = toVideoSource(pub.source);\n if (!pub.track || !source) return undefined;\n return videoOf(`${p.identity}:${pub.trackSid}`, p, true, source, pub.track);\n}\n\nfunction videoOf(\n id: string,\n p: Participant,\n isLocal: boolean,\n source: CallVideo[\"source\"],\n track: { attach(el: HTMLMediaElement): HTMLMediaElement; detach(el: HTMLMediaElement): HTMLMediaElement },\n): CallVideo {\n return {\n id,\n identity: p.identity,\n name: p.name || p.identity,\n isLocal,\n source,\n attach(el) {\n el.autoplay = true;\n el.playsInline = true;\n // Your own camera would otherwise echo the room back through the page.\n if (isLocal) el.muted = true;\n track.attach(el);\n },\n detach(el) { track.detach(el); },\n };\n}\n\nfunction toVoiceParticipant(p: Participant, isLocal: boolean): VoiceParticipant {\n return { identity: p.identity, name: p.name || p.identity, isLocal };\n}\n"]}
1
+ {"version":3,"sources":["../src/client.ts"],"names":["Room","Track","RoomEvent"],"mappings":"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","file":"client.cjs","sourcesContent":["// @broberg/call/client — the browser's half of a call (F104.3; camera + screen share F104.7).\n//\n// A thin layer over livekit-client that does the four things every app would\n// otherwise get slightly wrong on its own: open the microphone with noise\n// suppression, echo cancellation and auto gain; actually PLAY the other people\n// (a subscribed audio track is silent until it is attached to an element);\n// keep \"mute everyone\" and per-person volume apart so unmuting restores what\n// each person was set to; and stop calling the app back after leave().\n// F104.7 adds camera and screen share: simulcast + adaptive stream + dynacast\n// so 2–10 people with video stay usable, the browser's own «stop sharing»\n// button is reported back to the app, and audio-only drops incoming video.\n// No node: imports.\n\nimport { Room, RoomEvent, Track } from \"livekit-client\";\nimport type { LocalTrackPublication, Participant, RemoteParticipant, RemoteTrack, RemoteTrackPublication } from \"livekit-client\";\n\nexport interface VoiceParticipant {\n identity: string;\n name: string;\n isLocal: boolean;\n}\n\n/** F104.7 — one video picture in the call: a camera or a shared screen, yours or someone else's. */\nexport interface CallVideo {\n /** Stable for as long as this picture exists — use it as a render key. */\n id: string;\n identity: string;\n name: string;\n isLocal: boolean;\n source: \"camera\" | \"screen_share\";\n /** Show it in a `<video>` element (autoplay + playsinline are set for you). */\n attach(el: HTMLVideoElement): void;\n detach(el: HTMLVideoElement): void;\n}\n\n/** F104.7 — what you are sending right now. */\nexport interface LocalMedia {\n camera: boolean;\n screenShare: boolean;\n}\n\nexport interface JoinOptions {\n /** The media server, e.g. `wss://voice.broberg.ai`. */\n url: string;\n /** A ticket from `createRoomToken` on the app's server. */\n token: string;\n /**\n * F104.4 — `false` joins as a listener: the microphone is never opened\n * (a listen-only ticket would be refused if it tried). Default `true`.\n */\n publish?: boolean;\n}\n\nexport interface VoiceCall {\n mute(): Promise<void>;\n unmute(): Promise<void>;\n readonly muted: boolean;\n /** Silence everyone else (true) or bring back each person's own volume (false). */\n setOutputMuted(muted: boolean): void;\n setInputDevice(deviceId: string): Promise<void>;\n setOutputDevice(deviceId: string): Promise<void>;\n /** 0–1 for one person. Kept while output is muted and applied on unmute. */\n setVolume(identity: string, volume: number): void;\n /** Identities of the people speaking right now, loudest first. Returns an unsubscribe. */\n onSpeaking(cb: (identities: string[]) => void): () => void;\n /** Everyone in the call, you included; called at once and on every join/leave. Returns an unsubscribe. */\n onParticipants(cb: (participants: VoiceParticipant[]) => void): () => void;\n /** F104.7 — turn your camera on or off. Needs a ticket with `\"camera\"` in `sources`. */\n setCamera(on: boolean, opts?: { deviceId?: string }): Promise<void>;\n /**\n * F104.7 — share your screen (the browser asks which screen/window/tab) or stop.\n * `audio: true` also offers the tab's sound (needs `\"screen_share_audio\"` in the ticket).\n * Stopping with the browser's own «stop sharing» button is reported through `onLocalMedia`.\n */\n setScreenShare(on: boolean, opts?: { audio?: boolean }): Promise<void>;\n readonly localMedia: LocalMedia;\n /** Called at once and whenever your camera/screen share turns on or off — also from the browser's own stop button. Returns an unsubscribe. */\n onLocalMedia(cb: (media: LocalMedia) => void): () => void;\n /** Every video picture in the call (yours included), at once and whenever one comes or goes. Returns an unsubscribe. */\n onVideos(cb: (videos: CallVideo[]) => void): () => void;\n /** F104.7 — `true` stops receiving other people's video (weak connection); sound is untouched. `false` brings it back. */\n setAudioOnly(on: boolean): void;\n readonly audioOnly: boolean;\n leave(): Promise<void>;\n}\n\n/** F104.7 — the room settings that keep 2–10 people with video usable. */\nexport const VIDEO_ROOM_OPTIONS = { adaptiveStream: true, dynacast: true, publishDefaults: { simulcast: true } } as const;\n\nexport const AUDIO_CAPTURE = { noiseSuppression: true, echoCancellation: true, autoGainControl: true } as const;\n\nexport async function joinVoice({ url, token, publish = true }: JoinOptions): Promise<VoiceCall> {\n const room = new Room({ audioCaptureDefaults: { ...AUDIO_CAPTURE }, ...VIDEO_ROOM_OPTIONS, publishDefaults: { ...VIDEO_ROOM_OPTIONS.publishDefaults } });\n const localMediaCbs = new Set<(m: LocalMedia) => void>();\n const videoCbs = new Set<(v: CallVideo[]) => void>();\n // F104.10 — kept WITH its publication: a muted camera stays subscribed, so\n // only the publication can say whether there is a picture right now.\n const remoteVideos = new Map<RemoteTrack, { video: CallVideo; pub: RemoteTrackPublication }>();\n let audioOnly = false;\n const speakingCbs = new Set<(ids: string[]) => void>();\n const participantCbs = new Set<(ps: VoiceParticipant[]) => void>();\n const volumes = new Map<string, number>();\n const audioEls = new Map<RemoteTrack, HTMLMediaElement[]>();\n let outputMuted = false;\n let muted = !publish;\n let left = false;\n\n const participants = (): VoiceParticipant[] => [\n toVoiceParticipant(room.localParticipant, true),\n ...[...room.remoteParticipants.values()].map((p) => toVoiceParticipant(p, false)),\n ];\n const emitParticipants = () => {\n if (left) return;\n const list = participants();\n participantCbs.forEach((cb) => cb(list));\n };\n const applyVolume = (p: RemoteParticipant) => p.setVolume(outputMuted ? 0 : (volumes.get(p.identity) ?? 1));\n\n const localMedia = (): LocalMedia => ({\n camera: room.localParticipant.isCameraEnabled,\n screenShare: room.localParticipant.isScreenShareEnabled,\n });\n const emitLocalMedia = () => {\n if (left) return;\n const m = localMedia();\n localMediaCbs.forEach((cb) => cb(m));\n };\n const videos = (): CallVideo[] => {\n const local: CallVideo[] = [];\n for (const pub of room.localParticipant.videoTrackPublications.values()) {\n const v = localVideo(pub, room.localParticipant);\n if (v) local.push(v);\n }\n const remote = [...remoteVideos.values()].filter(({ pub }) => !pub.isMuted).map(({ video }) => video);\n return [...local, ...remote];\n };\n const emitVideos = () => {\n if (left) return;\n const list = videos();\n videoCbs.forEach((cb) => cb(list));\n };\n const onLocalChange = () => { emitLocalMedia(); emitVideos(); };\n // Audio-only only ever touches VIDEO publications — sound keeps flowing.\n const applyAudioOnly = (pub: RemoteTrackPublication) => {\n if (pub.kind === Track.Kind.Video) pub.setSubscribed(!audioOnly);\n };\n\n const handlers: [RoomEvent, (...args: any[]) => void][] = [\n [RoomEvent.ParticipantConnected, (p: RemoteParticipant) => { applyVolume(p); emitParticipants(); }],\n [RoomEvent.ParticipantDisconnected, () => emitParticipants()],\n [RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n if (left) return;\n const ids = speakers.map((s) => s.identity);\n speakingCbs.forEach((cb) => cb(ids));\n }],\n [RoomEvent.TrackSubscribed, (track: RemoteTrack, pub: RemoteTrackPublication, p: RemoteParticipant) => {\n if (track.kind === Track.Kind.Video) {\n const source = toVideoSource(track.source);\n if (!source) return;\n remoteVideos.set(track, { video: videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track), pub });\n emitVideos();\n return;\n }\n if (track.kind !== Track.Kind.Audio) return;\n const el = track.attach();\n document.body.appendChild(el);\n audioEls.set(track, [el]);\n applyVolume(p);\n }],\n [RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n if (remoteVideos.delete(track)) {\n track.detach();\n emitVideos();\n return;\n }\n track.detach().forEach((el) => el.remove());\n audioEls.delete(track);\n }],\n [RoomEvent.TrackPublished, (pub: RemoteTrackPublication) => { if (audioOnly) applyAudioOnly(pub); }],\n // Fired for our own camera/screen going on or off — including when the\n // person ends a screen share with the BROWSER's stop button, which never\n // passes through setScreenShare.\n [RoomEvent.LocalTrackPublished, onLocalChange],\n [RoomEvent.LocalTrackUnpublished, onLocalChange],\n // F104.10 — LiveKit turns a camera off by MUTING it, for you and for\n // everyone else, so a mute is a picture going away (and unmute coming back).\n [RoomEvent.TrackMuted, onLocalChange],\n [RoomEvent.TrackUnmuted, onLocalChange],\n ];\n for (const [event, fn] of handlers) room.on(event, fn);\n\n try {\n await room.connect(url, token);\n if (publish) await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE });\n // joinVoice is called from a click; this is the gesture browsers require before audio may play.\n await room.startAudio();\n } catch (e) {\n // A refused microphone must not leave the person connected and listening\n // with no handle to leave — the caller only gets the rejection.\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n await room.disconnect();\n throw e;\n }\n\n return {\n get muted() { return muted; },\n async mute() { await room.localParticipant.setMicrophoneEnabled(false); muted = true; },\n async unmute() { await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE }); muted = false; },\n setOutputMuted(m) {\n outputMuted = m;\n room.remoteParticipants.forEach(applyVolume);\n },\n async setInputDevice(deviceId) { await room.switchActiveDevice(\"audioinput\", deviceId); },\n async setOutputDevice(deviceId) { await room.switchActiveDevice(\"audiooutput\", deviceId); },\n setVolume(identity, volume) {\n volumes.set(identity, Math.min(1, Math.max(0, volume)));\n const p = room.remoteParticipants.get(identity);\n if (p) applyVolume(p);\n },\n onSpeaking(cb) {\n speakingCbs.add(cb);\n return () => speakingCbs.delete(cb);\n },\n onParticipants(cb) {\n participantCbs.add(cb);\n if (!left) cb(participants());\n return () => participantCbs.delete(cb);\n },\n async setCamera(on, opts) {\n await room.localParticipant.setCameraEnabled(on, opts?.deviceId ? { deviceId: opts.deviceId } : undefined);\n onLocalChange();\n },\n async setScreenShare(on, opts) {\n await room.localParticipant.setScreenShareEnabled(on, on ? { audio: opts?.audio === true } : undefined);\n onLocalChange();\n },\n get localMedia() { return localMedia(); },\n onLocalMedia(cb) {\n localMediaCbs.add(cb);\n if (!left) cb(localMedia());\n return () => localMediaCbs.delete(cb);\n },\n onVideos(cb) {\n videoCbs.add(cb);\n if (!left) cb(videos());\n return () => videoCbs.delete(cb);\n },\n get audioOnly() { return audioOnly; },\n setAudioOnly(on) {\n audioOnly = on;\n room.remoteParticipants.forEach((p) => p.trackPublications.forEach(applyAudioOnly));\n },\n async leave() {\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n speakingCbs.clear();\n participantCbs.clear();\n localMediaCbs.clear();\n videoCbs.clear();\n remoteVideos.forEach((_v, track) => track.detach());\n remoteVideos.clear();\n audioEls.forEach((_els, track) => track.detach().forEach((el) => el.remove()));\n audioEls.clear();\n await room.disconnect();\n },\n };\n}\n\nfunction toVideoSource(source: Track.Source): CallVideo[\"source\"] | undefined {\n if (source === Track.Source.Camera) return \"camera\";\n if (source === Track.Source.ScreenShare) return \"screen_share\";\n return undefined;\n}\n\nfunction localVideo(pub: LocalTrackPublication, p: Participant): CallVideo | undefined {\n const source = toVideoSource(pub.source);\n if (!pub.track || !source || pub.isMuted) return undefined;\n return videoOf(`${p.identity}:${pub.trackSid}`, p, true, source, pub.track);\n}\n\nfunction videoOf(\n id: string,\n p: Participant,\n isLocal: boolean,\n source: CallVideo[\"source\"],\n track: { attach(el: HTMLMediaElement): HTMLMediaElement; detach(el: HTMLMediaElement): HTMLMediaElement },\n): CallVideo {\n return {\n id,\n identity: p.identity,\n name: p.name || p.identity,\n isLocal,\n source,\n attach(el) {\n el.autoplay = true;\n el.playsInline = true;\n // Your own camera would otherwise echo the room back through the page.\n if (isLocal) el.muted = true;\n track.attach(el);\n },\n detach(el) { track.detach(el); },\n };\n}\n\nfunction toVoiceParticipant(p: Participant, isLocal: boolean): VoiceParticipant {\n return { identity: p.identity, name: p.name || p.identity, isLocal };\n}\n"]}
package/dist/client.js CHANGED
@@ -41,7 +41,8 @@ async function joinVoice({ url, token, publish = true }) {
41
41
  const v = localVideo(pub, room.localParticipant);
42
42
  if (v) local.push(v);
43
43
  }
44
- return [...local, ...remoteVideos.values()];
44
+ const remote = [...remoteVideos.values()].filter(({ pub }) => !pub.isMuted).map(({ video }) => video);
45
+ return [...local, ...remote];
45
46
  };
46
47
  const emitVideos = () => {
47
48
  if (left) return;
@@ -66,11 +67,11 @@ async function joinVoice({ url, token, publish = true }) {
66
67
  const ids = speakers.map((s) => s.identity);
67
68
  speakingCbs.forEach((cb) => cb(ids));
68
69
  }],
69
- [RoomEvent.TrackSubscribed, (track, _pub, p) => {
70
+ [RoomEvent.TrackSubscribed, (track, pub, p) => {
70
71
  if (track.kind === Track.Kind.Video) {
71
72
  const source = toVideoSource(track.source);
72
73
  if (!source) return;
73
- remoteVideos.set(track, videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track));
74
+ remoteVideos.set(track, { video: videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track), pub });
74
75
  emitVideos();
75
76
  return;
76
77
  }
@@ -96,7 +97,11 @@ async function joinVoice({ url, token, publish = true }) {
96
97
  // person ends a screen share with the BROWSER's stop button, which never
97
98
  // passes through setScreenShare.
98
99
  [RoomEvent.LocalTrackPublished, onLocalChange],
99
- [RoomEvent.LocalTrackUnpublished, onLocalChange]
100
+ [RoomEvent.LocalTrackUnpublished, onLocalChange],
101
+ // F104.10 — LiveKit turns a camera off by MUTING it, for you and for
102
+ // everyone else, so a mute is a picture going away (and unmute coming back).
103
+ [RoomEvent.TrackMuted, onLocalChange],
104
+ [RoomEvent.TrackUnmuted, onLocalChange]
100
105
  ];
101
106
  for (const [event, fn] of handlers) room.on(event, fn);
102
107
  try {
@@ -195,7 +200,7 @@ function toVideoSource(source) {
195
200
  }
196
201
  function localVideo(pub, p) {
197
202
  const source = toVideoSource(pub.source);
198
- if (!pub.track || !source) return void 0;
203
+ if (!pub.track || !source || pub.isMuted) return void 0;
199
204
  return videoOf(`${p.identity}:${pub.trackSid}`, p, true, source, pub.track);
200
205
  }
201
206
  function videoOf(id, p, isLocal, source, track) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["// @broberg/call/client — the browser's half of a call (F104.3; camera + screen share F104.7).\n//\n// A thin layer over livekit-client that does the four things every app would\n// otherwise get slightly wrong on its own: open the microphone with noise\n// suppression, echo cancellation and auto gain; actually PLAY the other people\n// (a subscribed audio track is silent until it is attached to an element);\n// keep \"mute everyone\" and per-person volume apart so unmuting restores what\n// each person was set to; and stop calling the app back after leave().\n// F104.7 adds camera and screen share: simulcast + adaptive stream + dynacast\n// so 2–10 people with video stay usable, the browser's own «stop sharing»\n// button is reported back to the app, and audio-only drops incoming video.\n// No node: imports.\n\nimport { Room, RoomEvent, Track } from \"livekit-client\";\nimport type { LocalTrackPublication, Participant, RemoteParticipant, RemoteTrack, RemoteTrackPublication } from \"livekit-client\";\n\nexport interface VoiceParticipant {\n identity: string;\n name: string;\n isLocal: boolean;\n}\n\n/** F104.7 — one video picture in the call: a camera or a shared screen, yours or someone else's. */\nexport interface CallVideo {\n /** Stable for as long as this picture exists — use it as a render key. */\n id: string;\n identity: string;\n name: string;\n isLocal: boolean;\n source: \"camera\" | \"screen_share\";\n /** Show it in a `<video>` element (autoplay + playsinline are set for you). */\n attach(el: HTMLVideoElement): void;\n detach(el: HTMLVideoElement): void;\n}\n\n/** F104.7 — what you are sending right now. */\nexport interface LocalMedia {\n camera: boolean;\n screenShare: boolean;\n}\n\nexport interface JoinOptions {\n /** The media server, e.g. `wss://voice.broberg.ai`. */\n url: string;\n /** A ticket from `createRoomToken` on the app's server. */\n token: string;\n /**\n * F104.4 — `false` joins as a listener: the microphone is never opened\n * (a listen-only ticket would be refused if it tried). Default `true`.\n */\n publish?: boolean;\n}\n\nexport interface VoiceCall {\n mute(): Promise<void>;\n unmute(): Promise<void>;\n readonly muted: boolean;\n /** Silence everyone else (true) or bring back each person's own volume (false). */\n setOutputMuted(muted: boolean): void;\n setInputDevice(deviceId: string): Promise<void>;\n setOutputDevice(deviceId: string): Promise<void>;\n /** 0–1 for one person. Kept while output is muted and applied on unmute. */\n setVolume(identity: string, volume: number): void;\n /** Identities of the people speaking right now, loudest first. Returns an unsubscribe. */\n onSpeaking(cb: (identities: string[]) => void): () => void;\n /** Everyone in the call, you included; called at once and on every join/leave. Returns an unsubscribe. */\n onParticipants(cb: (participants: VoiceParticipant[]) => void): () => void;\n /** F104.7 — turn your camera on or off. Needs a ticket with `\"camera\"` in `sources`. */\n setCamera(on: boolean, opts?: { deviceId?: string }): Promise<void>;\n /**\n * F104.7 — share your screen (the browser asks which screen/window/tab) or stop.\n * `audio: true` also offers the tab's sound (needs `\"screen_share_audio\"` in the ticket).\n * Stopping with the browser's own «stop sharing» button is reported through `onLocalMedia`.\n */\n setScreenShare(on: boolean, opts?: { audio?: boolean }): Promise<void>;\n readonly localMedia: LocalMedia;\n /** Called at once and whenever your camera/screen share turns on or off — also from the browser's own stop button. Returns an unsubscribe. */\n onLocalMedia(cb: (media: LocalMedia) => void): () => void;\n /** Every video picture in the call (yours included), at once and whenever one comes or goes. Returns an unsubscribe. */\n onVideos(cb: (videos: CallVideo[]) => void): () => void;\n /** F104.7 — `true` stops receiving other people's video (weak connection); sound is untouched. `false` brings it back. */\n setAudioOnly(on: boolean): void;\n readonly audioOnly: boolean;\n leave(): Promise<void>;\n}\n\n/** F104.7 — the room settings that keep 2–10 people with video usable. */\nexport const VIDEO_ROOM_OPTIONS = { adaptiveStream: true, dynacast: true, publishDefaults: { simulcast: true } } as const;\n\nexport const AUDIO_CAPTURE = { noiseSuppression: true, echoCancellation: true, autoGainControl: true } as const;\n\nexport async function joinVoice({ url, token, publish = true }: JoinOptions): Promise<VoiceCall> {\n const room = new Room({ audioCaptureDefaults: { ...AUDIO_CAPTURE }, ...VIDEO_ROOM_OPTIONS, publishDefaults: { ...VIDEO_ROOM_OPTIONS.publishDefaults } });\n const localMediaCbs = new Set<(m: LocalMedia) => void>();\n const videoCbs = new Set<(v: CallVideo[]) => void>();\n const remoteVideos = new Map<RemoteTrack, CallVideo>();\n let audioOnly = false;\n const speakingCbs = new Set<(ids: string[]) => void>();\n const participantCbs = new Set<(ps: VoiceParticipant[]) => void>();\n const volumes = new Map<string, number>();\n const audioEls = new Map<RemoteTrack, HTMLMediaElement[]>();\n let outputMuted = false;\n let muted = !publish;\n let left = false;\n\n const participants = (): VoiceParticipant[] => [\n toVoiceParticipant(room.localParticipant, true),\n ...[...room.remoteParticipants.values()].map((p) => toVoiceParticipant(p, false)),\n ];\n const emitParticipants = () => {\n if (left) return;\n const list = participants();\n participantCbs.forEach((cb) => cb(list));\n };\n const applyVolume = (p: RemoteParticipant) => p.setVolume(outputMuted ? 0 : (volumes.get(p.identity) ?? 1));\n\n const localMedia = (): LocalMedia => ({\n camera: room.localParticipant.isCameraEnabled,\n screenShare: room.localParticipant.isScreenShareEnabled,\n });\n const emitLocalMedia = () => {\n if (left) return;\n const m = localMedia();\n localMediaCbs.forEach((cb) => cb(m));\n };\n const videos = (): CallVideo[] => {\n const local: CallVideo[] = [];\n for (const pub of room.localParticipant.videoTrackPublications.values()) {\n const v = localVideo(pub, room.localParticipant);\n if (v) local.push(v);\n }\n return [...local, ...remoteVideos.values()];\n };\n const emitVideos = () => {\n if (left) return;\n const list = videos();\n videoCbs.forEach((cb) => cb(list));\n };\n const onLocalChange = () => { emitLocalMedia(); emitVideos(); };\n // Audio-only only ever touches VIDEO publications — sound keeps flowing.\n const applyAudioOnly = (pub: RemoteTrackPublication) => {\n if (pub.kind === Track.Kind.Video) pub.setSubscribed(!audioOnly);\n };\n\n const handlers: [RoomEvent, (...args: any[]) => void][] = [\n [RoomEvent.ParticipantConnected, (p: RemoteParticipant) => { applyVolume(p); emitParticipants(); }],\n [RoomEvent.ParticipantDisconnected, () => emitParticipants()],\n [RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n if (left) return;\n const ids = speakers.map((s) => s.identity);\n speakingCbs.forEach((cb) => cb(ids));\n }],\n [RoomEvent.TrackSubscribed, (track: RemoteTrack, _pub: unknown, p: RemoteParticipant) => {\n if (track.kind === Track.Kind.Video) {\n const source = toVideoSource(track.source);\n if (!source) return;\n remoteVideos.set(track, videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track));\n emitVideos();\n return;\n }\n if (track.kind !== Track.Kind.Audio) return;\n const el = track.attach();\n document.body.appendChild(el);\n audioEls.set(track, [el]);\n applyVolume(p);\n }],\n [RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n if (remoteVideos.delete(track)) {\n track.detach();\n emitVideos();\n return;\n }\n track.detach().forEach((el) => el.remove());\n audioEls.delete(track);\n }],\n [RoomEvent.TrackPublished, (pub: RemoteTrackPublication) => { if (audioOnly) applyAudioOnly(pub); }],\n // Fired for our own camera/screen going on or off — including when the\n // person ends a screen share with the BROWSER's stop button, which never\n // passes through setScreenShare.\n [RoomEvent.LocalTrackPublished, onLocalChange],\n [RoomEvent.LocalTrackUnpublished, onLocalChange],\n ];\n for (const [event, fn] of handlers) room.on(event, fn);\n\n try {\n await room.connect(url, token);\n if (publish) await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE });\n // joinVoice is called from a click; this is the gesture browsers require before audio may play.\n await room.startAudio();\n } catch (e) {\n // A refused microphone must not leave the person connected and listening\n // with no handle to leave — the caller only gets the rejection.\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n await room.disconnect();\n throw e;\n }\n\n return {\n get muted() { return muted; },\n async mute() { await room.localParticipant.setMicrophoneEnabled(false); muted = true; },\n async unmute() { await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE }); muted = false; },\n setOutputMuted(m) {\n outputMuted = m;\n room.remoteParticipants.forEach(applyVolume);\n },\n async setInputDevice(deviceId) { await room.switchActiveDevice(\"audioinput\", deviceId); },\n async setOutputDevice(deviceId) { await room.switchActiveDevice(\"audiooutput\", deviceId); },\n setVolume(identity, volume) {\n volumes.set(identity, Math.min(1, Math.max(0, volume)));\n const p = room.remoteParticipants.get(identity);\n if (p) applyVolume(p);\n },\n onSpeaking(cb) {\n speakingCbs.add(cb);\n return () => speakingCbs.delete(cb);\n },\n onParticipants(cb) {\n participantCbs.add(cb);\n if (!left) cb(participants());\n return () => participantCbs.delete(cb);\n },\n async setCamera(on, opts) {\n await room.localParticipant.setCameraEnabled(on, opts?.deviceId ? { deviceId: opts.deviceId } : undefined);\n onLocalChange();\n },\n async setScreenShare(on, opts) {\n await room.localParticipant.setScreenShareEnabled(on, on ? { audio: opts?.audio === true } : undefined);\n onLocalChange();\n },\n get localMedia() { return localMedia(); },\n onLocalMedia(cb) {\n localMediaCbs.add(cb);\n if (!left) cb(localMedia());\n return () => localMediaCbs.delete(cb);\n },\n onVideos(cb) {\n videoCbs.add(cb);\n if (!left) cb(videos());\n return () => videoCbs.delete(cb);\n },\n get audioOnly() { return audioOnly; },\n setAudioOnly(on) {\n audioOnly = on;\n room.remoteParticipants.forEach((p) => p.trackPublications.forEach(applyAudioOnly));\n },\n async leave() {\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n speakingCbs.clear();\n participantCbs.clear();\n localMediaCbs.clear();\n videoCbs.clear();\n remoteVideos.forEach((_v, track) => track.detach());\n remoteVideos.clear();\n audioEls.forEach((_els, track) => track.detach().forEach((el) => el.remove()));\n audioEls.clear();\n await room.disconnect();\n },\n };\n}\n\nfunction toVideoSource(source: Track.Source): CallVideo[\"source\"] | undefined {\n if (source === Track.Source.Camera) return \"camera\";\n if (source === Track.Source.ScreenShare) return \"screen_share\";\n return undefined;\n}\n\nfunction localVideo(pub: LocalTrackPublication, p: Participant): CallVideo | undefined {\n const source = toVideoSource(pub.source);\n if (!pub.track || !source) return undefined;\n return videoOf(`${p.identity}:${pub.trackSid}`, p, true, source, pub.track);\n}\n\nfunction videoOf(\n id: string,\n p: Participant,\n isLocal: boolean,\n source: CallVideo[\"source\"],\n track: { attach(el: HTMLMediaElement): HTMLMediaElement; detach(el: HTMLMediaElement): HTMLMediaElement },\n): CallVideo {\n return {\n id,\n identity: p.identity,\n name: p.name || p.identity,\n isLocal,\n source,\n attach(el) {\n el.autoplay = true;\n el.playsInline = true;\n // Your own camera would otherwise echo the room back through the page.\n if (isLocal) el.muted = true;\n track.attach(el);\n },\n detach(el) { track.detach(el); },\n };\n}\n\nfunction toVoiceParticipant(p: Participant, isLocal: boolean): VoiceParticipant {\n return { identity: p.identity, name: p.name || p.identity, isLocal };\n}\n"]}
1
+ {"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["// @broberg/call/client — the browser's half of a call (F104.3; camera + screen share F104.7).\n//\n// A thin layer over livekit-client that does the four things every app would\n// otherwise get slightly wrong on its own: open the microphone with noise\n// suppression, echo cancellation and auto gain; actually PLAY the other people\n// (a subscribed audio track is silent until it is attached to an element);\n// keep \"mute everyone\" and per-person volume apart so unmuting restores what\n// each person was set to; and stop calling the app back after leave().\n// F104.7 adds camera and screen share: simulcast + adaptive stream + dynacast\n// so 2–10 people with video stay usable, the browser's own «stop sharing»\n// button is reported back to the app, and audio-only drops incoming video.\n// No node: imports.\n\nimport { Room, RoomEvent, Track } from \"livekit-client\";\nimport type { LocalTrackPublication, Participant, RemoteParticipant, RemoteTrack, RemoteTrackPublication } from \"livekit-client\";\n\nexport interface VoiceParticipant {\n identity: string;\n name: string;\n isLocal: boolean;\n}\n\n/** F104.7 — one video picture in the call: a camera or a shared screen, yours or someone else's. */\nexport interface CallVideo {\n /** Stable for as long as this picture exists — use it as a render key. */\n id: string;\n identity: string;\n name: string;\n isLocal: boolean;\n source: \"camera\" | \"screen_share\";\n /** Show it in a `<video>` element (autoplay + playsinline are set for you). */\n attach(el: HTMLVideoElement): void;\n detach(el: HTMLVideoElement): void;\n}\n\n/** F104.7 — what you are sending right now. */\nexport interface LocalMedia {\n camera: boolean;\n screenShare: boolean;\n}\n\nexport interface JoinOptions {\n /** The media server, e.g. `wss://voice.broberg.ai`. */\n url: string;\n /** A ticket from `createRoomToken` on the app's server. */\n token: string;\n /**\n * F104.4 — `false` joins as a listener: the microphone is never opened\n * (a listen-only ticket would be refused if it tried). Default `true`.\n */\n publish?: boolean;\n}\n\nexport interface VoiceCall {\n mute(): Promise<void>;\n unmute(): Promise<void>;\n readonly muted: boolean;\n /** Silence everyone else (true) or bring back each person's own volume (false). */\n setOutputMuted(muted: boolean): void;\n setInputDevice(deviceId: string): Promise<void>;\n setOutputDevice(deviceId: string): Promise<void>;\n /** 0–1 for one person. Kept while output is muted and applied on unmute. */\n setVolume(identity: string, volume: number): void;\n /** Identities of the people speaking right now, loudest first. Returns an unsubscribe. */\n onSpeaking(cb: (identities: string[]) => void): () => void;\n /** Everyone in the call, you included; called at once and on every join/leave. Returns an unsubscribe. */\n onParticipants(cb: (participants: VoiceParticipant[]) => void): () => void;\n /** F104.7 — turn your camera on or off. Needs a ticket with `\"camera\"` in `sources`. */\n setCamera(on: boolean, opts?: { deviceId?: string }): Promise<void>;\n /**\n * F104.7 — share your screen (the browser asks which screen/window/tab) or stop.\n * `audio: true` also offers the tab's sound (needs `\"screen_share_audio\"` in the ticket).\n * Stopping with the browser's own «stop sharing» button is reported through `onLocalMedia`.\n */\n setScreenShare(on: boolean, opts?: { audio?: boolean }): Promise<void>;\n readonly localMedia: LocalMedia;\n /** Called at once and whenever your camera/screen share turns on or off — also from the browser's own stop button. Returns an unsubscribe. */\n onLocalMedia(cb: (media: LocalMedia) => void): () => void;\n /** Every video picture in the call (yours included), at once and whenever one comes or goes. Returns an unsubscribe. */\n onVideos(cb: (videos: CallVideo[]) => void): () => void;\n /** F104.7 — `true` stops receiving other people's video (weak connection); sound is untouched. `false` brings it back. */\n setAudioOnly(on: boolean): void;\n readonly audioOnly: boolean;\n leave(): Promise<void>;\n}\n\n/** F104.7 — the room settings that keep 2–10 people with video usable. */\nexport const VIDEO_ROOM_OPTIONS = { adaptiveStream: true, dynacast: true, publishDefaults: { simulcast: true } } as const;\n\nexport const AUDIO_CAPTURE = { noiseSuppression: true, echoCancellation: true, autoGainControl: true } as const;\n\nexport async function joinVoice({ url, token, publish = true }: JoinOptions): Promise<VoiceCall> {\n const room = new Room({ audioCaptureDefaults: { ...AUDIO_CAPTURE }, ...VIDEO_ROOM_OPTIONS, publishDefaults: { ...VIDEO_ROOM_OPTIONS.publishDefaults } });\n const localMediaCbs = new Set<(m: LocalMedia) => void>();\n const videoCbs = new Set<(v: CallVideo[]) => void>();\n // F104.10 — kept WITH its publication: a muted camera stays subscribed, so\n // only the publication can say whether there is a picture right now.\n const remoteVideos = new Map<RemoteTrack, { video: CallVideo; pub: RemoteTrackPublication }>();\n let audioOnly = false;\n const speakingCbs = new Set<(ids: string[]) => void>();\n const participantCbs = new Set<(ps: VoiceParticipant[]) => void>();\n const volumes = new Map<string, number>();\n const audioEls = new Map<RemoteTrack, HTMLMediaElement[]>();\n let outputMuted = false;\n let muted = !publish;\n let left = false;\n\n const participants = (): VoiceParticipant[] => [\n toVoiceParticipant(room.localParticipant, true),\n ...[...room.remoteParticipants.values()].map((p) => toVoiceParticipant(p, false)),\n ];\n const emitParticipants = () => {\n if (left) return;\n const list = participants();\n participantCbs.forEach((cb) => cb(list));\n };\n const applyVolume = (p: RemoteParticipant) => p.setVolume(outputMuted ? 0 : (volumes.get(p.identity) ?? 1));\n\n const localMedia = (): LocalMedia => ({\n camera: room.localParticipant.isCameraEnabled,\n screenShare: room.localParticipant.isScreenShareEnabled,\n });\n const emitLocalMedia = () => {\n if (left) return;\n const m = localMedia();\n localMediaCbs.forEach((cb) => cb(m));\n };\n const videos = (): CallVideo[] => {\n const local: CallVideo[] = [];\n for (const pub of room.localParticipant.videoTrackPublications.values()) {\n const v = localVideo(pub, room.localParticipant);\n if (v) local.push(v);\n }\n const remote = [...remoteVideos.values()].filter(({ pub }) => !pub.isMuted).map(({ video }) => video);\n return [...local, ...remote];\n };\n const emitVideos = () => {\n if (left) return;\n const list = videos();\n videoCbs.forEach((cb) => cb(list));\n };\n const onLocalChange = () => { emitLocalMedia(); emitVideos(); };\n // Audio-only only ever touches VIDEO publications — sound keeps flowing.\n const applyAudioOnly = (pub: RemoteTrackPublication) => {\n if (pub.kind === Track.Kind.Video) pub.setSubscribed(!audioOnly);\n };\n\n const handlers: [RoomEvent, (...args: any[]) => void][] = [\n [RoomEvent.ParticipantConnected, (p: RemoteParticipant) => { applyVolume(p); emitParticipants(); }],\n [RoomEvent.ParticipantDisconnected, () => emitParticipants()],\n [RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n if (left) return;\n const ids = speakers.map((s) => s.identity);\n speakingCbs.forEach((cb) => cb(ids));\n }],\n [RoomEvent.TrackSubscribed, (track: RemoteTrack, pub: RemoteTrackPublication, p: RemoteParticipant) => {\n if (track.kind === Track.Kind.Video) {\n const source = toVideoSource(track.source);\n if (!source) return;\n remoteVideos.set(track, { video: videoOf(`${p.identity}:${track.sid ?? source}`, p, false, source, track), pub });\n emitVideos();\n return;\n }\n if (track.kind !== Track.Kind.Audio) return;\n const el = track.attach();\n document.body.appendChild(el);\n audioEls.set(track, [el]);\n applyVolume(p);\n }],\n [RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n if (remoteVideos.delete(track)) {\n track.detach();\n emitVideos();\n return;\n }\n track.detach().forEach((el) => el.remove());\n audioEls.delete(track);\n }],\n [RoomEvent.TrackPublished, (pub: RemoteTrackPublication) => { if (audioOnly) applyAudioOnly(pub); }],\n // Fired for our own camera/screen going on or off — including when the\n // person ends a screen share with the BROWSER's stop button, which never\n // passes through setScreenShare.\n [RoomEvent.LocalTrackPublished, onLocalChange],\n [RoomEvent.LocalTrackUnpublished, onLocalChange],\n // F104.10 — LiveKit turns a camera off by MUTING it, for you and for\n // everyone else, so a mute is a picture going away (and unmute coming back).\n [RoomEvent.TrackMuted, onLocalChange],\n [RoomEvent.TrackUnmuted, onLocalChange],\n ];\n for (const [event, fn] of handlers) room.on(event, fn);\n\n try {\n await room.connect(url, token);\n if (publish) await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE });\n // joinVoice is called from a click; this is the gesture browsers require before audio may play.\n await room.startAudio();\n } catch (e) {\n // A refused microphone must not leave the person connected and listening\n // with no handle to leave — the caller only gets the rejection.\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n await room.disconnect();\n throw e;\n }\n\n return {\n get muted() { return muted; },\n async mute() { await room.localParticipant.setMicrophoneEnabled(false); muted = true; },\n async unmute() { await room.localParticipant.setMicrophoneEnabled(true, { ...AUDIO_CAPTURE }); muted = false; },\n setOutputMuted(m) {\n outputMuted = m;\n room.remoteParticipants.forEach(applyVolume);\n },\n async setInputDevice(deviceId) { await room.switchActiveDevice(\"audioinput\", deviceId); },\n async setOutputDevice(deviceId) { await room.switchActiveDevice(\"audiooutput\", deviceId); },\n setVolume(identity, volume) {\n volumes.set(identity, Math.min(1, Math.max(0, volume)));\n const p = room.remoteParticipants.get(identity);\n if (p) applyVolume(p);\n },\n onSpeaking(cb) {\n speakingCbs.add(cb);\n return () => speakingCbs.delete(cb);\n },\n onParticipants(cb) {\n participantCbs.add(cb);\n if (!left) cb(participants());\n return () => participantCbs.delete(cb);\n },\n async setCamera(on, opts) {\n await room.localParticipant.setCameraEnabled(on, opts?.deviceId ? { deviceId: opts.deviceId } : undefined);\n onLocalChange();\n },\n async setScreenShare(on, opts) {\n await room.localParticipant.setScreenShareEnabled(on, on ? { audio: opts?.audio === true } : undefined);\n onLocalChange();\n },\n get localMedia() { return localMedia(); },\n onLocalMedia(cb) {\n localMediaCbs.add(cb);\n if (!left) cb(localMedia());\n return () => localMediaCbs.delete(cb);\n },\n onVideos(cb) {\n videoCbs.add(cb);\n if (!left) cb(videos());\n return () => videoCbs.delete(cb);\n },\n get audioOnly() { return audioOnly; },\n setAudioOnly(on) {\n audioOnly = on;\n room.remoteParticipants.forEach((p) => p.trackPublications.forEach(applyAudioOnly));\n },\n async leave() {\n left = true;\n for (const [event, fn] of handlers) room.off(event, fn);\n speakingCbs.clear();\n participantCbs.clear();\n localMediaCbs.clear();\n videoCbs.clear();\n remoteVideos.forEach((_v, track) => track.detach());\n remoteVideos.clear();\n audioEls.forEach((_els, track) => track.detach().forEach((el) => el.remove()));\n audioEls.clear();\n await room.disconnect();\n },\n };\n}\n\nfunction toVideoSource(source: Track.Source): CallVideo[\"source\"] | undefined {\n if (source === Track.Source.Camera) return \"camera\";\n if (source === Track.Source.ScreenShare) return \"screen_share\";\n return undefined;\n}\n\nfunction localVideo(pub: LocalTrackPublication, p: Participant): CallVideo | undefined {\n const source = toVideoSource(pub.source);\n if (!pub.track || !source || pub.isMuted) return undefined;\n return videoOf(`${p.identity}:${pub.trackSid}`, p, true, source, pub.track);\n}\n\nfunction videoOf(\n id: string,\n p: Participant,\n isLocal: boolean,\n source: CallVideo[\"source\"],\n track: { attach(el: HTMLMediaElement): HTMLMediaElement; detach(el: HTMLMediaElement): HTMLMediaElement },\n): CallVideo {\n return {\n id,\n identity: p.identity,\n name: p.name || p.identity,\n isLocal,\n source,\n attach(el) {\n el.autoplay = true;\n el.playsInline = true;\n // Your own camera would otherwise echo the room back through the page.\n if (isLocal) el.muted = true;\n track.attach(el);\n },\n detach(el) { track.detach(el); },\n };\n}\n\nfunction toVoiceParticipant(p: Participant, isLocal: boolean): VoiceParticipant {\n return { identity: p.identity, name: p.name || p.identity, isLocal };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@broberg/call",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Calls for 2–10 people on a self-hosted LiveKit SFU in the EU — audio, camera and screen share: the app's server mints a short-lived, room-bound ticket that allows exactly the sources it names (microphone only by default) and verifies LiveKit's signed webhooks; the browser joins with noise suppression, echo cancellation and auto gain, and gets mute, devices, per-person volume, who-is-speaking, the participant list, camera and screen share on/off (incl. the browser's own stop button), every video picture to attach, and an audio-only mode — with simulcast, adaptive stream and dynacast.",
5
5
  "type": "module",
6
6
  "license": "MIT",