@burdenoff/microfe-comradecircle 2026.1025.2 → 2026.1025.3

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.
Files changed (49) hide show
  1. package/dist/comradecircle/components/LiveStagePanel.d.ts +10 -0
  2. package/dist/comradecircle/components/LiveStagePanel.d.ts.map +1 -0
  3. package/dist/comradecircle/components/LiveStagePanel.js +263 -0
  4. package/dist/comradecircle/components/LiveStagePanel.js.map +1 -0
  5. package/dist/comradecircle/components/MediaTile.d.ts +16 -0
  6. package/dist/comradecircle/components/MediaTile.d.ts.map +1 -0
  7. package/dist/comradecircle/components/MediaTile.js +74 -0
  8. package/dist/comradecircle/components/MediaTile.js.map +1 -0
  9. package/dist/comradecircle/components/VoiceChannelPanel.d.ts +10 -0
  10. package/dist/comradecircle/components/VoiceChannelPanel.d.ts.map +1 -0
  11. package/dist/comradecircle/components/VoiceChannelPanel.js +123 -0
  12. package/dist/comradecircle/components/VoiceChannelPanel.js.map +1 -0
  13. package/dist/comradecircle/hooks/index.d.ts +3 -0
  14. package/dist/comradecircle/hooks/index.d.ts.map +1 -1
  15. package/dist/comradecircle/hooks/index.js +3 -0
  16. package/dist/comradecircle/hooks/useLiveStage.d.ts +20 -0
  17. package/dist/comradecircle/hooks/useLiveStage.d.ts.map +1 -0
  18. package/dist/comradecircle/hooks/useLiveStage.js +160 -0
  19. package/dist/comradecircle/hooks/useLiveStage.js.map +1 -0
  20. package/dist/comradecircle/hooks/useVoiceApi.d.ts +36 -0
  21. package/dist/comradecircle/hooks/useVoiceApi.d.ts.map +1 -0
  22. package/dist/comradecircle/hooks/useVoiceApi.js +113 -0
  23. package/dist/comradecircle/hooks/useVoiceApi.js.map +1 -0
  24. package/dist/comradecircle/hooks/useVoiceCall.d.ts +21 -0
  25. package/dist/comradecircle/hooks/useVoiceCall.d.ts.map +1 -0
  26. package/dist/comradecircle/hooks/useVoiceCall.js +154 -0
  27. package/dist/comradecircle/hooks/useVoiceCall.js.map +1 -0
  28. package/dist/comradecircle/lib/graphqlStream.d.ts +31 -0
  29. package/dist/comradecircle/lib/graphqlStream.d.ts.map +1 -0
  30. package/dist/comradecircle/lib/graphqlStream.js +112 -0
  31. package/dist/comradecircle/lib/graphqlStream.js.map +1 -0
  32. package/dist/comradecircle/lib/rtc/liveStage.d.ts +37 -0
  33. package/dist/comradecircle/lib/rtc/liveStage.d.ts.map +1 -0
  34. package/dist/comradecircle/lib/rtc/liveStage.js +97 -0
  35. package/dist/comradecircle/lib/rtc/liveStage.js.map +1 -0
  36. package/dist/comradecircle/lib/rtc/peerLink.d.ts +50 -0
  37. package/dist/comradecircle/lib/rtc/peerLink.d.ts.map +1 -0
  38. package/dist/comradecircle/lib/rtc/peerLink.js +110 -0
  39. package/dist/comradecircle/lib/rtc/peerLink.js.map +1 -0
  40. package/dist/comradecircle/lib/rtc/voiceMesh.d.ts +41 -0
  41. package/dist/comradecircle/lib/rtc/voiceMesh.d.ts.map +1 -0
  42. package/dist/comradecircle/lib/rtc/voiceMesh.js +101 -0
  43. package/dist/comradecircle/lib/rtc/voiceMesh.js.map +1 -0
  44. package/dist/comradecircle/pages/circles/CircleDetailPage.d.ts.map +1 -1
  45. package/dist/comradecircle/pages/circles/CircleDetailPage.js +299 -286
  46. package/dist/comradecircle/pages/circles/CircleDetailPage.js.map +1 -1
  47. package/dist/comradecircle/types/index.d.ts +51 -0
  48. package/dist/comradecircle/types/index.d.ts.map +1 -1
  49. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLiveStage.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/hooks/useLiveStage.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,UAAU,CAAC;AAU5D,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AACnD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,kEAAkE;IAClE,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,eAAe,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB;AAyBD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,CAmN7D"}
@@ -0,0 +1,160 @@
1
+ import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
2
+ import "../context/index.js";
3
+ import { LIVE_STREAM_FIELDS as t, VOICE_KEY as n, useComRtcConfiguration as r, useComStartLiveStream as i, useLiveStreamActions as a } from "./useVoiceApi.js";
4
+ import { subscribeGraphqlStream as o } from "../lib/graphqlStream.js";
5
+ import { toRtcConfiguration as s } from "../lib/rtc/peerLink.js";
6
+ import { describeMediaError as c } from "./useVoiceCall.js";
7
+ import { StageHost as l, StageViewer as u } from "../lib/rtc/liveStage.js";
8
+ import { useCallback as d, useEffect as f, useRef as p, useState as m } from "react";
9
+ import { useQueryClient as h } from "@tanstack/react-query";
10
+ //#region src/comradecircle/hooks/useLiveStage.ts
11
+ async function g(e) {
12
+ if (!navigator.mediaDevices?.getUserMedia) throw Error("This browser does not support live streaming.");
13
+ if (e === "camera") return navigator.mediaDevices.getUserMedia({
14
+ audio: {
15
+ echoCancellation: !0,
16
+ noiseSuppression: !0
17
+ },
18
+ video: {
19
+ width: { ideal: 1280 },
20
+ height: { ideal: 720 }
21
+ }
22
+ });
23
+ if (!navigator.mediaDevices.getDisplayMedia) throw Error("Screen sharing is not supported on this device.");
24
+ let t = await navigator.mediaDevices.getDisplayMedia({
25
+ video: !0,
26
+ audio: !1
27
+ });
28
+ try {
29
+ let e = await navigator.mediaDevices.getUserMedia({ audio: !0 });
30
+ return new MediaStream([...t.getVideoTracks(), ...e.getAudioTracks()]);
31
+ } catch {
32
+ return t;
33
+ }
34
+ }
35
+ function _(_) {
36
+ let { currentUser: v, apiGatewayUrl: y } = e(), b = v?.id ?? "", x = h(), S = r(), C = a(), w = i(), [T, E] = m("none"), [D, O] = m(!1), [k, A] = m(null), [j, M] = m(null), [N, P] = m(null), [F, I] = m(0), [L, R] = m(null), z = p([]), B = p(null), V = p(null), H = d(() => {
37
+ z.current.forEach((e) => e()), z.current = [], B.current?.getTracks().forEach((e) => e.stop()), B.current = null, M(null), P(null), I(0);
38
+ }, []), U = d(() => x.invalidateQueries({ queryKey: n.liveStreams(_) }), [_, x]), W = d(async (e) => {
39
+ let t = V.current;
40
+ V.current = null, H(), t?.role === "host" && await C.end(t.streamId).catch(() => {}), t?.role === "viewer" && await C.leave(t.streamId).catch(() => {}), E("none"), A(null), R(e), U();
41
+ }, [
42
+ C,
43
+ U,
44
+ H
45
+ ]), G = d((e, n, r) => {
46
+ z.current.push(o({
47
+ query: "subscription ComLiveStreamSignal($streamId: ID!) {\n comLiveStreamSignal(streamId: $streamId) { fromMemberId toMemberId signalType payload }\n }",
48
+ variables: { streamId: e },
49
+ operationName: "ComLiveStreamSignal",
50
+ baseUrl: y,
51
+ onOpen: r,
52
+ onData: (e) => n(e.comLiveStreamSignal),
53
+ onError: (e) => void W(e[0]?.message ?? "Lost the stream connection.")
54
+ }), o({
55
+ query: `subscription ComLiveStreamUpdated($streamId: ID!) {
56
+ comLiveStreamUpdated(streamId: $streamId) { ${t} }
57
+ }`,
58
+ variables: { streamId: e },
59
+ operationName: "ComLiveStreamUpdated",
60
+ baseUrl: y,
61
+ onData: (e) => {
62
+ let t = e.comLiveStreamUpdated;
63
+ A(t), t.status === "ENDED" && W(V.current?.role === "viewer" ? "The live stream has ended." : null);
64
+ }
65
+ }));
66
+ }, [y, W]), K = d((e, t) => {
67
+ let n = setInterval(() => {
68
+ C.heartbeat(e).then((e) => {
69
+ A(e), e.status === "ENDED" && W("The live stream has ended.");
70
+ }).catch(() => void W("You were disconnected from the live stream."));
71
+ }, Math.max(5, t) * 1e3);
72
+ z.current.push(() => clearInterval(n));
73
+ }, [C, W]), q = d(async (e, t) => {
74
+ if (!b || V.current) return;
75
+ O(!0), R(null);
76
+ let n;
77
+ try {
78
+ n = await g(t);
79
+ } catch (e) {
80
+ R(c(e)), O(!1);
81
+ return;
82
+ }
83
+ B.current = n, M(n);
84
+ try {
85
+ let t = S.data ?? (await S.refetch()).data;
86
+ if (!t) throw Error("Could not load streaming configuration.");
87
+ let r = await w.mutateAsync({
88
+ circleId: _,
89
+ title: e
90
+ });
91
+ V.current = {
92
+ role: "host",
93
+ streamId: r.id
94
+ }, A(r), E("host");
95
+ let i = new l(s(t.iceServers), n, (e, t, n) => C.signal(r.id, e, t, n), I);
96
+ z.current.push(() => i.dispose()), G(r.id, (e) => void i.handleSignal(e)), K(r.id, t.heartbeatIntervalSeconds), n.getVideoTracks().forEach((e) => {
97
+ e.addEventListener("ended", () => void W(null));
98
+ });
99
+ } catch (e) {
100
+ await W(e instanceof Error ? e.message : "Could not start the live stream.");
101
+ } finally {
102
+ O(!1);
103
+ }
104
+ }, [
105
+ C,
106
+ _,
107
+ W,
108
+ G,
109
+ S,
110
+ b,
111
+ w,
112
+ K
113
+ ]), J = d(async (e) => {
114
+ if (!(!b || V.current)) {
115
+ O(!0), R(null);
116
+ try {
117
+ let t = S.data ?? (await S.refetch()).data;
118
+ if (!t) throw Error("Could not load streaming configuration.");
119
+ let n = await C.join(e.id);
120
+ V.current = {
121
+ role: "viewer",
122
+ streamId: n.id
123
+ }, A(n), E("viewer");
124
+ let r = new u(n.startedBy, s(t.iceServers), (e, t, r) => C.signal(n.id, e, t, r), M, P);
125
+ z.current.push(() => r.dispose()), G(n.id, (e) => void r.handleSignal(e), () => r.markSignalingReady()), K(n.id, t.heartbeatIntervalSeconds);
126
+ } catch (e) {
127
+ await W(e instanceof Error ? e.message : "Could not join the live stream.");
128
+ } finally {
129
+ O(!1);
130
+ }
131
+ }
132
+ }, [
133
+ C,
134
+ W,
135
+ G,
136
+ S,
137
+ b,
138
+ K
139
+ ]), Y = d(() => W(null), [W]), X = d(() => R(null), []);
140
+ return f(() => () => {
141
+ let e = V.current;
142
+ V.current = null, H(), e?.role === "host" && C.end(e.streamId).catch(() => {}), e?.role === "viewer" && C.leave(e.streamId).catch(() => {});
143
+ }, [C, H]), {
144
+ role: T,
145
+ busy: D,
146
+ stream: k,
147
+ media: j,
148
+ connectionState: N,
149
+ connectedViewers: F,
150
+ error: L,
151
+ goLive: q,
152
+ watch: J,
153
+ stop: Y,
154
+ clearError: X
155
+ };
156
+ }
157
+ //#endregion
158
+ export { _ as useLiveStage };
159
+
160
+ //# sourceMappingURL=useLiveStage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLiveStage.js","names":[],"sources":["../../../src/comradecircle/hooks/useLiveStage.ts"],"sourcesContent":["/**\n * useLiveStage — host or watch a circle's live stage (BOFF-4450).\n *\n * Host: capture camera or screen plus microphone → `comStartLiveStream` →\n * answer each viewer's offer with the local tracks (StageHost) → heartbeat to\n * keep the stream LIVE → `comEndLiveStream`. If the browser's screen-share\n * picker is stopped, the stream ends.\n *\n * Viewer: `comJoinLiveStream` → open the signal inbox → dial the host with a\n * receive-only connection (StageViewer) → heartbeat to keep the seat → leave.\n * Both sides follow `comLiveStreamUpdated` for status and viewer count, and a\n * viewer drops out automatically when the stream ends.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { useComradeCircle } from '../context';\nimport { subscribeGraphqlStream } from '../lib/graphqlStream';\nimport { toRtcConfiguration } from '../lib/rtc/peerLink';\nimport { StageHost, StageViewer } from '../lib/rtc/liveStage';\nimport type { ComLiveStream, ComRtcSignal } from '../types';\nimport { describeMediaError } from './useVoiceCall';\nimport {\n LIVE_STREAM_FIELDS,\n VOICE_KEY,\n useComRtcConfiguration,\n useComStartLiveStream,\n useLiveStreamActions,\n} from './useVoiceApi';\n\nexport type StageRole = 'none' | 'host' | 'viewer';\nexport type StageSource = 'camera' | 'screen';\n\nexport interface LiveStageState {\n role: StageRole;\n busy: boolean;\n stream: ComLiveStream | null;\n /** Host: own preview. Viewer: the host's media once connected. */\n media: MediaStream | null;\n connectionState: RTCPeerConnectionState | null;\n connectedViewers: number;\n error: string | null;\n goLive: (title: string, source: StageSource) => Promise<void>;\n watch: (stream: ComLiveStream) => Promise<void>;\n stop: () => Promise<void>;\n clearError: () => void;\n}\n\nasync function captureStageMedia(source: StageSource): Promise<MediaStream> {\n if (!navigator.mediaDevices?.getUserMedia) {\n throw new Error('This browser does not support live streaming.');\n }\n if (source === 'camera') {\n return navigator.mediaDevices.getUserMedia({\n audio: { echoCancellation: true, noiseSuppression: true },\n video: { width: { ideal: 1280 }, height: { ideal: 720 } },\n });\n }\n if (!navigator.mediaDevices.getDisplayMedia) {\n throw new Error('Screen sharing is not supported on this device.');\n }\n const screen = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: false });\n try {\n const mic = await navigator.mediaDevices.getUserMedia({ audio: true });\n return new MediaStream([...screen.getVideoTracks(), ...mic.getAudioTracks()]);\n } catch {\n // Streaming the screen without narration is still useful.\n return screen;\n }\n}\n\nexport function useLiveStage(circleId: string): LiveStageState {\n const { currentUser, apiGatewayUrl } = useComradeCircle();\n const selfId = currentUser?.id ?? '';\n const qc = useQueryClient();\n const rtc = useComRtcConfiguration();\n const actions = useLiveStreamActions();\n const start = useComStartLiveStream();\n\n const [role, setRole] = useState<StageRole>('none');\n const [busy, setBusy] = useState(false);\n const [stream, setStream] = useState<ComLiveStream | null>(null);\n const [media, setMedia] = useState<MediaStream | null>(null);\n const [connectionState, setConnectionState] = useState<RTCPeerConnectionState | null>(null);\n const [connectedViewers, setConnectedViewers] = useState(0);\n const [error, setError] = useState<string | null>(null);\n\n const disposersRef = useRef<Array<() => void>>([]);\n const localRef = useRef<MediaStream | null>(null);\n const activeRef = useRef<{ role: StageRole; streamId: string } | null>(null);\n\n const teardown = useCallback(() => {\n disposersRef.current.forEach((dispose) => dispose());\n disposersRef.current = [];\n localRef.current?.getTracks().forEach((t) => t.stop());\n localRef.current = null;\n setMedia(null);\n setConnectionState(null);\n setConnectedViewers(0);\n }, []);\n\n const refreshList = useCallback(\n () => qc.invalidateQueries({ queryKey: VOICE_KEY.liveStreams(circleId) }),\n [circleId, qc]\n );\n\n const finish = useCallback(\n async (message: string | null) => {\n const active = activeRef.current;\n activeRef.current = null;\n teardown();\n if (active?.role === 'host') await actions.end(active.streamId).catch(() => {});\n if (active?.role === 'viewer') await actions.leave(active.streamId).catch(() => {});\n setRole('none');\n setStream(null);\n setError(message);\n void refreshList();\n },\n [actions, refreshList, teardown]\n );\n\n const followStream = useCallback(\n (streamId: string, onSignal: (signal: ComRtcSignal) => void, onReady?: () => void) => {\n disposersRef.current.push(\n subscribeGraphqlStream<{ comLiveStreamSignal: ComRtcSignal }>({\n query: `subscription ComLiveStreamSignal($streamId: ID!) {\n comLiveStreamSignal(streamId: $streamId) { fromMemberId toMemberId signalType payload }\n }`,\n variables: { streamId },\n operationName: 'ComLiveStreamSignal',\n baseUrl: apiGatewayUrl,\n onOpen: onReady,\n onData: (data) => onSignal(data.comLiveStreamSignal),\n onError: (errors) => void finish(errors[0]?.message ?? 'Lost the stream connection.'),\n }),\n subscribeGraphqlStream<{ comLiveStreamUpdated: ComLiveStream }>({\n query: `subscription ComLiveStreamUpdated($streamId: ID!) {\n comLiveStreamUpdated(streamId: $streamId) { ${LIVE_STREAM_FIELDS} }\n }`,\n variables: { streamId },\n operationName: 'ComLiveStreamUpdated',\n baseUrl: apiGatewayUrl,\n onData: (data) => {\n const next = data.comLiveStreamUpdated;\n setStream(next);\n if (next.status === 'ENDED') {\n void finish(\n activeRef.current?.role === 'viewer' ? 'The live stream has ended.' : null\n );\n }\n },\n })\n );\n },\n [apiGatewayUrl, finish]\n );\n\n const startHeartbeat = useCallback(\n (streamId: string, intervalSeconds: number) => {\n const beat = setInterval(\n () => {\n void actions\n .heartbeat(streamId)\n .then((s) => {\n setStream(s);\n if (s.status === 'ENDED') void finish('The live stream has ended.');\n })\n .catch(() => void finish('You were disconnected from the live stream.'));\n },\n Math.max(5, intervalSeconds) * 1000\n );\n disposersRef.current.push(() => clearInterval(beat));\n },\n [actions, finish]\n );\n\n const goLive = useCallback(\n async (title: string, source: StageSource) => {\n if (!selfId || activeRef.current) return;\n setBusy(true);\n setError(null);\n let local: MediaStream;\n try {\n local = await captureStageMedia(source);\n } catch (err) {\n setError(describeMediaError(err));\n setBusy(false);\n return;\n }\n localRef.current = local;\n setMedia(local);\n try {\n const config = rtc.data ?? (await rtc.refetch()).data;\n if (!config) throw new Error('Could not load streaming configuration.');\n const created = await start.mutateAsync({ circleId, title });\n activeRef.current = { role: 'host', streamId: created.id };\n setStream(created);\n setRole('host');\n const host = new StageHost(\n toRtcConfiguration(config.iceServers),\n local,\n (to, type, payload) => actions.signal(created.id, to, type, payload),\n setConnectedViewers\n );\n disposersRef.current.push(() => host.dispose());\n followStream(created.id, (signal) => void host.handleSignal(signal));\n startHeartbeat(created.id, config.heartbeatIntervalSeconds);\n // Stopping the browser's share picker ends the broadcast.\n local.getVideoTracks().forEach((track) => {\n track.addEventListener('ended', () => void finish(null));\n });\n } catch (err) {\n await finish(err instanceof Error ? err.message : 'Could not start the live stream.');\n } finally {\n setBusy(false);\n }\n },\n [actions, circleId, finish, followStream, rtc, selfId, start, startHeartbeat]\n );\n\n const watch = useCallback(\n async (target: ComLiveStream) => {\n if (!selfId || activeRef.current) return;\n setBusy(true);\n setError(null);\n try {\n const config = rtc.data ?? (await rtc.refetch()).data;\n if (!config) throw new Error('Could not load streaming configuration.');\n const joined = await actions.join(target.id);\n activeRef.current = { role: 'viewer', streamId: joined.id };\n setStream(joined);\n setRole('viewer');\n const viewer = new StageViewer(\n joined.startedBy,\n toRtcConfiguration(config.iceServers),\n (to, type, payload) => actions.signal(joined.id, to, type, payload),\n setMedia,\n setConnectionState\n );\n disposersRef.current.push(() => viewer.dispose());\n followStream(\n joined.id,\n (signal) => void viewer.handleSignal(signal),\n () => viewer.markSignalingReady()\n );\n startHeartbeat(joined.id, config.heartbeatIntervalSeconds);\n } catch (err) {\n await finish(err instanceof Error ? err.message : 'Could not join the live stream.');\n } finally {\n setBusy(false);\n }\n },\n [actions, finish, followStream, rtc, selfId, startHeartbeat]\n );\n\n const stop = useCallback(() => finish(null), [finish]);\n const clearError = useCallback(() => setError(null), []);\n\n useEffect(\n () => () => {\n const active = activeRef.current;\n activeRef.current = null;\n teardown();\n if (active?.role === 'host') void actions.end(active.streamId).catch(() => {});\n if (active?.role === 'viewer') void actions.leave(active.streamId).catch(() => {});\n },\n [actions, teardown]\n );\n\n return {\n role,\n busy,\n stream,\n media,\n connectionState,\n connectedViewers,\n error,\n goLive,\n watch,\n stop,\n clearError,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import { ComLiveStream, ComRtcConfiguration, ComVoiceParticipant, ComVoiceSession, ComVoiceSignalType } from '../types';
2
+
3
+ export declare const VOICE_PARTICIPANT_FIELDS = "id sessionId memberId muted joinedAt leftAt";
4
+ export declare const VOICE_SESSION_FIELDS = "\n id circleId channelId status startedBy startedAt endedAt participantCount\n participants { id sessionId memberId muted joinedAt leftAt }\n";
5
+ export declare const LIVE_STREAM_FIELDS = "id circleId title status startedBy startedAt endedAt viewerCount createdAt";
6
+ export declare const VOICE_KEY: {
7
+ session: (channelId: string) => readonly ["comradecircle", "voiceSession", string];
8
+ rtc: readonly ["comradecircle", "rtcConfiguration"];
9
+ liveStreams: (circleId: string) => readonly ["comradecircle", "liveStreams", string];
10
+ };
11
+ /** The channel's ACTIVE voice session (or null), refreshed every 15s while idle. */
12
+ export declare const useComVoiceSession: (channelId: string | undefined, refetchMs?: number | false) => import('@tanstack/react-query').UseQueryResult<ComVoiceSession | null, Error>;
13
+ export declare const useComRtcConfiguration: (enabled?: boolean) => import('@tanstack/react-query').UseQueryResult<ComRtcConfiguration, Error>;
14
+ /** Imperative voice calls used by the call controller (not cached). */
15
+ export declare const useVoiceActions: () => {
16
+ join: (channelId: string) => Promise<ComVoiceSession>;
17
+ leave: (sessionId: string) => Promise<boolean>;
18
+ heartbeat: (sessionId: string) => Promise<ComVoiceSession>;
19
+ setMute: (sessionId: string, muted: boolean) => Promise<ComVoiceParticipant>;
20
+ signal: (sessionId: string, toMemberId: string, signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;
21
+ };
22
+ /** LIVE streams in a circle (at most one by construction), refreshed every 20s. */
23
+ export declare const useComLiveStreams: (circleId: string | undefined) => import('@tanstack/react-query').UseQueryResult<ComLiveStream[], Error>;
24
+ export declare const useComStartLiveStream: () => import('@tanstack/react-query').UseMutationResult<ComLiveStream, Error, {
25
+ circleId: string;
26
+ title: string;
27
+ }, unknown>;
28
+ /** Imperative live-stream calls used by the stage controller. */
29
+ export declare const useLiveStreamActions: () => {
30
+ end: (streamId: string) => Promise<ComLiveStream>;
31
+ join: (streamId: string) => Promise<ComLiveStream>;
32
+ leave: (streamId: string) => Promise<ComLiveStream>;
33
+ heartbeat: (streamId: string) => Promise<ComLiveStream>;
34
+ signal: (streamId: string, toMemberId: string, signalType: ComVoiceSignalType, payload: unknown) => Promise<void>;
35
+ };
36
+ //# sourceMappingURL=useVoiceApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVoiceApi.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/hooks/useVoiceApi.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,aAAa,EACb,mBAAmB,EACnB,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EACnB,MAAM,UAAU,CAAC;AAGlB,eAAO,MAAM,wBAAwB,gDAAgD,CAAC;AACtF,eAAO,MAAM,oBAAoB,oJAGhC,CAAC;AACF,eAAO,MAAM,kBAAkB,+EAC+C,CAAC;AAE/E,eAAO,MAAM,SAAS;yBACC,MAAM;;4BAEH,MAAM;CAC/B,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,kBAAkB,GAC7B,WAAW,MAAM,GAAG,SAAS,EAC7B,YAAW,MAAM,GAAG,KAAc,kFAkBnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,iBAAc,+EAoBpD,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,eAAe;sBAOA,MAAM;uBAUL,MAAM;2BAQF,MAAM;yBAUR,MAAM,SAAS,OAAO;wBAWpC,MAAM,cACL,MAAM,cACN,kBAAkB,WACrB,OAAO;CA9CrB,CAAC;AA2DF,mFAAmF;AACnF,eAAO,MAAM,iBAAiB,GAAI,UAAU,MAAM,GAAG,SAAS,2EAiB7D,CAAC;AAEF,eAAO,MAAM,qBAAqB;cAIsB,MAAM;WAAS,MAAM;WAa5E,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,oBAAoB;oBASP,MAAM;qBAOL,MAAM;sBAOL,MAAM;0BAOF,MAAM;uBAQtB,MAAM,cACJ,MAAM,cACN,kBAAkB,WACrB,OAAO;CAtCrB,CAAC"}
@@ -0,0 +1,113 @@
1
+ import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
2
+ import "../context/index.js";
3
+ import { comQuery as t } from "./useComradeCircleApi.js";
4
+ import { useMemo as n } from "react";
5
+ import { useMutation as r, useQuery as i, useQueryClient as a } from "@tanstack/react-query";
6
+ //#region src/comradecircle/hooks/useVoiceApi.ts
7
+ var o = "id sessionId memberId muted joinedAt leftAt", s = `
8
+ id circleId channelId status startedBy startedAt endedAt participantCount
9
+ participants { ${o} }
10
+ `, c = "id circleId title status startedBy startedAt endedAt viewerCount createdAt", l = {
11
+ session: (e) => [
12
+ "comradecircle",
13
+ "voiceSession",
14
+ e
15
+ ],
16
+ rtc: ["comradecircle", "rtcConfiguration"],
17
+ liveStreams: (e) => [
18
+ "comradecircle",
19
+ "liveStreams",
20
+ e
21
+ ]
22
+ }, u = (n, r = 15e3) => {
23
+ let { apiGatewayUrl: a } = e();
24
+ return i({
25
+ queryKey: l.session(n ?? ""),
26
+ enabled: !!n,
27
+ refetchInterval: r,
28
+ queryFn: async () => (await t(`query ComVoiceSession($channelId: ID!) {
29
+ comVoiceSession(channelId: $channelId) { ${s} }
30
+ }`, { channelId: n }, a)).comVoiceSession
31
+ });
32
+ }, d = (n = !0) => {
33
+ let { apiGatewayUrl: r } = e();
34
+ return i({
35
+ queryKey: l.rtc,
36
+ enabled: n,
37
+ staleTime: 10 * 6e4,
38
+ queryFn: async () => (await t("query ComRtcConfiguration {\n comRtcConfiguration {\n iceServers { urls username credential }\n maxMeshParticipants maxStreamViewers heartbeatIntervalSeconds\n }\n }", {}, r)).comRtcConfiguration
39
+ });
40
+ }, f = () => {
41
+ let { apiGatewayUrl: t } = e();
42
+ return n(() => p(t), [t]);
43
+ }, p = (e) => ({
44
+ join: async (n) => (await t(`mutation ComJoinVoiceChannel($channelId: ID!) {
45
+ comJoinVoiceChannel(channelId: $channelId) { ${s} }
46
+ }`, { channelId: n }, e)).comJoinVoiceChannel,
47
+ leave: async (n) => (await t("mutation ComLeaveVoiceChannel($sessionId: ID!) { comLeaveVoiceChannel(sessionId: $sessionId) }", { sessionId: n }, e)).comLeaveVoiceChannel,
48
+ heartbeat: async (n) => (await t(`mutation ComVoiceHeartbeat($sessionId: ID!) {
49
+ comVoiceHeartbeat(sessionId: $sessionId) { ${s} }
50
+ }`, { sessionId: n }, e)).comVoiceHeartbeat,
51
+ setMute: async (n, r) => (await t(`mutation ComSetVoiceMute($sessionId: ID!, $muted: Boolean!) {
52
+ comSetVoiceMute(sessionId: $sessionId, muted: $muted) { ${o} }
53
+ }`, {
54
+ sessionId: n,
55
+ muted: r
56
+ }, e)).comSetVoiceMute,
57
+ signal: async (n, r, i, a) => {
58
+ await t("mutation ComSendVoiceSignal($sessionId: ID!, $toMemberId: ID!, $signalType: ComVoiceSignalType!, $payload: JSON!) {\n comSendVoiceSignal(sessionId: $sessionId, toMemberId: $toMemberId, signalType: $signalType, payload: $payload)\n }", {
59
+ sessionId: n,
60
+ toMemberId: r,
61
+ signalType: i,
62
+ payload: a
63
+ }, e);
64
+ }
65
+ }), m = (n) => {
66
+ let { apiGatewayUrl: r } = e();
67
+ return i({
68
+ queryKey: l.liveStreams(n ?? ""),
69
+ enabled: !!n,
70
+ refetchInterval: 2e4,
71
+ queryFn: async () => (await t(`query ComLiveStreams($input: ComListLiveStreamsInput!) {
72
+ comLiveStreams(input: $input) { items { ${c} } }
73
+ }`, { input: {
74
+ circleId: n,
75
+ status: "LIVE",
76
+ pagination: { limit: 5 }
77
+ } }, r)).comLiveStreams.items
78
+ });
79
+ }, h = () => {
80
+ let { apiGatewayUrl: n } = e(), i = a();
81
+ return r({
82
+ mutationFn: async ({ circleId: e, title: r }) => (await t(`mutation ComStartLiveStream($circleId: ID!, $title: String!) {
83
+ comStartLiveStream(circleId: $circleId, title: $title) { ${c} }
84
+ }`, {
85
+ circleId: e,
86
+ title: r
87
+ }, n)).comStartLiveStream,
88
+ onSuccess: (e) => i.invalidateQueries({ queryKey: l.liveStreams(e.circleId) })
89
+ });
90
+ }, g = () => {
91
+ let { apiGatewayUrl: t } = e();
92
+ return n(() => _(t), [t]);
93
+ }, _ = (e) => {
94
+ let n = async (n, r) => t(n, r, e);
95
+ return {
96
+ end: async (e) => (await n(`mutation ComEndLiveStream($streamId: ID!) { comEndLiveStream(streamId: $streamId) { ${c} } }`, { streamId: e })).comEndLiveStream,
97
+ join: async (e) => (await n(`mutation ComJoinLiveStream($streamId: ID!) { comJoinLiveStream(streamId: $streamId) { ${c} } }`, { streamId: e })).comJoinLiveStream,
98
+ leave: async (e) => (await n(`mutation ComLeaveLiveStream($streamId: ID!) { comLeaveLiveStream(streamId: $streamId) { ${c} } }`, { streamId: e })).comLeaveLiveStream,
99
+ heartbeat: async (e) => (await n(`mutation ComLiveStreamHeartbeat($streamId: ID!) { comLiveStreamHeartbeat(streamId: $streamId) { ${c} } }`, { streamId: e })).comLiveStreamHeartbeat,
100
+ signal: async (e, t, r, i) => {
101
+ await n("mutation ComSendLiveStreamSignal($streamId: ID!, $toMemberId: ID!, $signalType: ComVoiceSignalType!, $payload: JSON!) {\n comSendLiveStreamSignal(streamId: $streamId, toMemberId: $toMemberId, signalType: $signalType, payload: $payload)\n }", {
102
+ streamId: e,
103
+ toMemberId: t,
104
+ signalType: r,
105
+ payload: i
106
+ });
107
+ }
108
+ };
109
+ };
110
+ //#endregion
111
+ export { c as LIVE_STREAM_FIELDS, l as VOICE_KEY, o as VOICE_PARTICIPANT_FIELDS, s as VOICE_SESSION_FIELDS, m as useComLiveStreams, d as useComRtcConfiguration, h as useComStartLiveStream, u as useComVoiceSession, g as useLiveStreamActions, f as useVoiceActions };
112
+
113
+ //# sourceMappingURL=useVoiceApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVoiceApi.js","names":[],"sources":["../../../src/comradecircle/hooks/useVoiceApi.ts"],"sourcesContent":["/**\n * Voice/video channel and live-stream API hooks (BOFF-4450).\n *\n * Thin React-Query wrappers over the `wspace-comradecircle-svc` voice module\n * (`comVoiceSession`, `comJoinVoiceChannel`, `comLiveStreams`, ...). The\n * real-time halves — roster changes, stream updates and WebRTC signaling —\n * are SSE subscriptions opened by `useVoiceChannel` / `useLiveStage`.\n */\nimport { useMemo } from 'react';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\n\nimport { useComradeCircle } from '../context';\nimport type {\n ComLiveStream,\n ComRtcConfiguration,\n ComVoiceParticipant,\n ComVoiceSession,\n ComVoiceSignalType,\n} from '../types';\nimport { comQuery } from './useComradeCircleApi';\n\nexport const VOICE_PARTICIPANT_FIELDS = 'id sessionId memberId muted joinedAt leftAt';\nexport const VOICE_SESSION_FIELDS = `\n id circleId channelId status startedBy startedAt endedAt participantCount\n participants { ${VOICE_PARTICIPANT_FIELDS} }\n`;\nexport const LIVE_STREAM_FIELDS =\n 'id circleId title status startedBy startedAt endedAt viewerCount createdAt';\n\nexport const VOICE_KEY = {\n session: (channelId: string) => ['comradecircle', 'voiceSession', channelId] as const,\n rtc: ['comradecircle', 'rtcConfiguration'] as const,\n liveStreams: (circleId: string) => ['comradecircle', 'liveStreams', circleId] as const,\n};\n\n/** The channel's ACTIVE voice session (or null), refreshed every 15s while idle. */\nexport const useComVoiceSession = (\n channelId: string | undefined,\n refetchMs: number | false = 15_000\n) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: VOICE_KEY.session(channelId ?? ''),\n enabled: Boolean(channelId),\n refetchInterval: refetchMs,\n queryFn: async () => {\n const result = await comQuery<{ comVoiceSession: ComVoiceSession | null }>(\n `query ComVoiceSession($channelId: ID!) {\n comVoiceSession(channelId: $channelId) { ${VOICE_SESSION_FIELDS} }\n }`,\n { channelId },\n apiGatewayUrl\n );\n return result.comVoiceSession;\n },\n });\n};\n\nexport const useComRtcConfiguration = (enabled = true) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: VOICE_KEY.rtc,\n enabled,\n staleTime: 10 * 60_000,\n queryFn: async () => {\n const result = await comQuery<{ comRtcConfiguration: ComRtcConfiguration }>(\n `query ComRtcConfiguration {\n comRtcConfiguration {\n iceServers { urls username credential }\n maxMeshParticipants maxStreamViewers heartbeatIntervalSeconds\n }\n }`,\n {},\n apiGatewayUrl\n );\n return result.comRtcConfiguration;\n },\n });\n};\n\n/** Imperative voice calls used by the call controller (not cached). */\nexport const useVoiceActions = () => {\n const { apiGatewayUrl } = useComradeCircle();\n return useMemo(() => voiceActions(apiGatewayUrl), [apiGatewayUrl]);\n};\n\nconst voiceActions = (apiGatewayUrl: string | undefined) => {\n return {\n join: async (channelId: string) =>\n (\n await comQuery<{ comJoinVoiceChannel: ComVoiceSession }>(\n `mutation ComJoinVoiceChannel($channelId: ID!) {\n comJoinVoiceChannel(channelId: $channelId) { ${VOICE_SESSION_FIELDS} }\n }`,\n { channelId },\n apiGatewayUrl\n )\n ).comJoinVoiceChannel,\n leave: async (sessionId: string) =>\n (\n await comQuery<{ comLeaveVoiceChannel: boolean }>(\n `mutation ComLeaveVoiceChannel($sessionId: ID!) { comLeaveVoiceChannel(sessionId: $sessionId) }`,\n { sessionId },\n apiGatewayUrl\n )\n ).comLeaveVoiceChannel,\n heartbeat: async (sessionId: string) =>\n (\n await comQuery<{ comVoiceHeartbeat: ComVoiceSession }>(\n `mutation ComVoiceHeartbeat($sessionId: ID!) {\n comVoiceHeartbeat(sessionId: $sessionId) { ${VOICE_SESSION_FIELDS} }\n }`,\n { sessionId },\n apiGatewayUrl\n )\n ).comVoiceHeartbeat,\n setMute: async (sessionId: string, muted: boolean) =>\n (\n await comQuery<{ comSetVoiceMute: ComVoiceParticipant }>(\n `mutation ComSetVoiceMute($sessionId: ID!, $muted: Boolean!) {\n comSetVoiceMute(sessionId: $sessionId, muted: $muted) { ${VOICE_PARTICIPANT_FIELDS} }\n }`,\n { sessionId, muted },\n apiGatewayUrl\n )\n ).comSetVoiceMute,\n signal: async (\n sessionId: string,\n toMemberId: string,\n signalType: ComVoiceSignalType,\n payload: unknown\n ) => {\n await comQuery<{ comSendVoiceSignal: boolean }>(\n `mutation ComSendVoiceSignal($sessionId: ID!, $toMemberId: ID!, $signalType: ComVoiceSignalType!, $payload: JSON!) {\n comSendVoiceSignal(sessionId: $sessionId, toMemberId: $toMemberId, signalType: $signalType, payload: $payload)\n }`,\n { sessionId, toMemberId, signalType, payload },\n apiGatewayUrl\n );\n },\n };\n};\n\n/** LIVE streams in a circle (at most one by construction), refreshed every 20s. */\nexport const useComLiveStreams = (circleId: string | undefined) => {\n const { apiGatewayUrl } = useComradeCircle();\n return useQuery({\n queryKey: VOICE_KEY.liveStreams(circleId ?? ''),\n enabled: Boolean(circleId),\n refetchInterval: 20_000,\n queryFn: async () => {\n const result = await comQuery<{ comLiveStreams: { items: ComLiveStream[] } }>(\n `query ComLiveStreams($input: ComListLiveStreamsInput!) {\n comLiveStreams(input: $input) { items { ${LIVE_STREAM_FIELDS} } }\n }`,\n { input: { circleId, status: 'LIVE', pagination: { limit: 5 } } },\n apiGatewayUrl\n );\n return result.comLiveStreams.items;\n },\n });\n};\n\nexport const useComStartLiveStream = () => {\n const { apiGatewayUrl } = useComradeCircle();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async ({ circleId, title }: { circleId: string; title: string }) =>\n (\n await comQuery<{ comStartLiveStream: ComLiveStream }>(\n `mutation ComStartLiveStream($circleId: ID!, $title: String!) {\n comStartLiveStream(circleId: $circleId, title: $title) { ${LIVE_STREAM_FIELDS} }\n }`,\n { circleId, title },\n apiGatewayUrl\n )\n ).comStartLiveStream,\n onSuccess: (stream) =>\n qc.invalidateQueries({ queryKey: VOICE_KEY.liveStreams(stream.circleId) }),\n });\n};\n\n/** Imperative live-stream calls used by the stage controller. */\nexport const useLiveStreamActions = () => {\n const { apiGatewayUrl } = useComradeCircle();\n return useMemo(() => liveStreamActions(apiGatewayUrl), [apiGatewayUrl]);\n};\n\nconst liveStreamActions = (apiGatewayUrl: string | undefined) => {\n const call = async <T>(query: string, variables: Record<string, unknown>) =>\n comQuery<T>(query, variables, apiGatewayUrl);\n return {\n end: async (streamId: string) =>\n (\n await call<{ comEndLiveStream: ComLiveStream }>(\n `mutation ComEndLiveStream($streamId: ID!) { comEndLiveStream(streamId: $streamId) { ${LIVE_STREAM_FIELDS} } }`,\n { streamId }\n )\n ).comEndLiveStream,\n join: async (streamId: string) =>\n (\n await call<{ comJoinLiveStream: ComLiveStream }>(\n `mutation ComJoinLiveStream($streamId: ID!) { comJoinLiveStream(streamId: $streamId) { ${LIVE_STREAM_FIELDS} } }`,\n { streamId }\n )\n ).comJoinLiveStream,\n leave: async (streamId: string) =>\n (\n await call<{ comLeaveLiveStream: ComLiveStream }>(\n `mutation ComLeaveLiveStream($streamId: ID!) { comLeaveLiveStream(streamId: $streamId) { ${LIVE_STREAM_FIELDS} } }`,\n { streamId }\n )\n ).comLeaveLiveStream,\n heartbeat: async (streamId: string) =>\n (\n await call<{ comLiveStreamHeartbeat: ComLiveStream }>(\n `mutation ComLiveStreamHeartbeat($streamId: ID!) { comLiveStreamHeartbeat(streamId: $streamId) { ${LIVE_STREAM_FIELDS} } }`,\n { streamId }\n )\n ).comLiveStreamHeartbeat,\n signal: async (\n streamId: string,\n toMemberId: string,\n signalType: ComVoiceSignalType,\n payload: unknown\n ) => {\n await call<{ comSendLiveStreamSignal: boolean }>(\n `mutation ComSendLiveStreamSignal($streamId: ID!, $toMemberId: ID!, $signalType: ComVoiceSignalType!, $payload: JSON!) {\n comSendLiveStreamSignal(streamId: $streamId, toMemberId: $toMemberId, signalType: $signalType, payload: $payload)\n }`,\n { streamId, toMemberId, signalType, payload }\n );\n },\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { ComVoiceSession } from '../types';
2
+
3
+ export type VoiceCallStatus = 'idle' | 'joining' | 'connected' | 'leaving' | 'error';
4
+ export interface VoiceCallState {
5
+ status: VoiceCallStatus;
6
+ session: ComVoiceSession | null;
7
+ localStream: MediaStream | null;
8
+ remoteStreams: Map<string, MediaStream>;
9
+ peerStates: Map<string, RTCPeerConnectionState>;
10
+ muted: boolean;
11
+ videoOn: boolean;
12
+ error: string | null;
13
+ join: (withVideo: boolean) => Promise<void>;
14
+ leave: () => Promise<void>;
15
+ toggleMute: () => Promise<void>;
16
+ toggleVideo: () => Promise<void>;
17
+ }
18
+ /** Map a getUserMedia / API failure to a user-facing reason. */
19
+ export declare function describeMediaError(err: unknown): string;
20
+ export declare function useVoiceCall(channelId: string): VoiceCallState;
21
+ //# sourceMappingURL=useVoiceCall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVoiceCall.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/hooks/useVoiceCall.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,UAAU,CAAC;AAQ9D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAErF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,GAAG,IAAI,CAAC;IAChC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACxC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;IAChD,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAYvD;AAMD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAwN9D"}
@@ -0,0 +1,154 @@
1
+ import { useComradeCircle as e } from "../context/ComradeCircleContext.js";
2
+ import "../context/index.js";
3
+ import { VOICE_KEY as t, VOICE_SESSION_FIELDS as n, useComRtcConfiguration as r, useVoiceActions as i } from "./useVoiceApi.js";
4
+ import { subscribeGraphqlStream as a } from "../lib/graphqlStream.js";
5
+ import { toRtcConfiguration as o } from "../lib/rtc/peerLink.js";
6
+ import { VoiceMesh as s } from "../lib/rtc/voiceMesh.js";
7
+ import { useCallback as c, useEffect as l, useRef as u, useState as d } from "react";
8
+ import { useQueryClient as f } from "@tanstack/react-query";
9
+ //#region src/comradecircle/hooks/useVoiceCall.ts
10
+ function p(e) {
11
+ let t = e instanceof DOMException ? e.name : "";
12
+ return t === "NotAllowedError" || t === "SecurityError" ? "Microphone or camera access was blocked. Allow it in your browser settings and try again." : t === "NotFoundError" || t === "OverconstrainedError" ? "No microphone or camera was found on this device." : t === "NotReadableError" ? "Your microphone or camera is being used by another application." : e instanceof Error ? e.message : "Something went wrong joining the call.";
13
+ }
14
+ function m(e) {
15
+ e?.getTracks().forEach((e) => e.stop());
16
+ }
17
+ function h(h) {
18
+ let { currentUser: g, apiGatewayUrl: _ } = e(), v = g?.id ?? "", y = i(), b = f(), x = r(), [S, C] = d("idle"), [w, T] = d(null), [E, D] = d(null), [O, k] = d(/* @__PURE__ */ new Map()), [A, j] = d(/* @__PURE__ */ new Map()), [M, N] = d(!1), [P, F] = d(!1), [I, L] = d(null), R = u(null), z = u(null), B = u(null), V = u([]), H = c((e) => {
19
+ T(e), e && R.current?.updateRoster(e.participants);
20
+ }, []), U = c(() => {
21
+ V.current.forEach((e) => e()), V.current = [], R.current?.dispose(), R.current = null, m(z.current), z.current = null, D(null), k(/* @__PURE__ */ new Map()), j(/* @__PURE__ */ new Map());
22
+ }, []), W = c((e) => {
23
+ let t = B.current;
24
+ B.current = null, U(), t && y.leave(t).catch(() => {}), T(null), L(e), C("error");
25
+ }, [y, U]), G = c(async (e) => {
26
+ if (!v || B.current) return;
27
+ L(null), C("joining");
28
+ let t;
29
+ try {
30
+ if (!navigator.mediaDevices?.getUserMedia) throw Error("This browser does not support voice and video calls.");
31
+ t = await navigator.mediaDevices.getUserMedia({
32
+ audio: {
33
+ echoCancellation: !0,
34
+ noiseSuppression: !0
35
+ },
36
+ video: e ? {
37
+ width: { ideal: 640 },
38
+ height: { ideal: 360 }
39
+ } : !1
40
+ });
41
+ } catch (e) {
42
+ L(p(e)), C("error");
43
+ return;
44
+ }
45
+ z.current = t, D(t), F(e), N(!1);
46
+ try {
47
+ let e = x.data ?? (await x.refetch()).data;
48
+ if (!e) throw Error("Could not load call configuration.");
49
+ let r = await y.join(h);
50
+ B.current = r.id;
51
+ let i = new s({
52
+ selfId: v,
53
+ rtcConfig: o(e.iceServers),
54
+ localStream: t,
55
+ sendSignal: (e, t, n) => y.signal(r.id, e, t, n),
56
+ onRemoteStreamsChange: k,
57
+ onPeerStatesChange: j
58
+ });
59
+ R.current = i, H(r), V.current.push(a({
60
+ query: "subscription ComVoiceSignal($sessionId: ID!) {\n comVoiceSignal(sessionId: $sessionId) { fromMemberId toMemberId signalType payload }\n }",
61
+ variables: { sessionId: r.id },
62
+ operationName: "ComVoiceSignal",
63
+ baseUrl: _,
64
+ onOpen: () => i.markSignalingReady(),
65
+ onData: (e) => void i.handleSignal(e.comVoiceSignal),
66
+ onError: (e) => W(e[0]?.message ?? "Lost the call connection.")
67
+ }), a({
68
+ query: `subscription ComVoiceParticipantsChanged($sessionId: ID!) {
69
+ comVoiceParticipantsChanged(sessionId: $sessionId) { ${n} }
70
+ }`,
71
+ variables: { sessionId: r.id },
72
+ operationName: "ComVoiceParticipantsChanged",
73
+ baseUrl: _,
74
+ onData: (e) => H(e.comVoiceParticipantsChanged)
75
+ }));
76
+ let c = setInterval(() => {
77
+ y.heartbeat(r.id).then(H).catch(() => W("You were disconnected from the call."));
78
+ }, Math.max(5, e.heartbeatIntervalSeconds) * 1e3);
79
+ V.current.push(() => clearInterval(c)), C("connected");
80
+ } catch (e) {
81
+ W(e instanceof Error ? e.message : "Could not join the call.");
82
+ }
83
+ }, [
84
+ y,
85
+ _,
86
+ H,
87
+ h,
88
+ W,
89
+ x,
90
+ v
91
+ ]), K = c(async () => {
92
+ let e = B.current;
93
+ B.current = null, C("leaving"), U();
94
+ try {
95
+ e && await y.leave(e);
96
+ } catch {}
97
+ T(null), C("idle"), b.invalidateQueries({ queryKey: t.session(h) });
98
+ }, [
99
+ y,
100
+ h,
101
+ b,
102
+ U
103
+ ]), q = c(async () => {
104
+ let e = z.current, t = B.current;
105
+ if (!e || !t) return;
106
+ let n = !M;
107
+ e.getAudioTracks().forEach((e) => {
108
+ e.enabled = !n;
109
+ }), N(n);
110
+ try {
111
+ await y.setMute(t, n);
112
+ } catch {}
113
+ }, [y, M]), J = c(async () => {
114
+ let e = z.current;
115
+ if (e) {
116
+ if (P) {
117
+ e.getVideoTracks().forEach((e) => e.stop());
118
+ let t = new MediaStream(e.getAudioTracks());
119
+ z.current = t, D(t), R.current?.setLocalStream(t), F(!1);
120
+ return;
121
+ }
122
+ try {
123
+ let t = await navigator.mediaDevices.getUserMedia({ video: {
124
+ width: { ideal: 640 },
125
+ height: { ideal: 360 }
126
+ } }), n = new MediaStream([...e.getAudioTracks(), ...t.getVideoTracks()]);
127
+ z.current = n, D(n), R.current?.setLocalStream(n), F(!0);
128
+ } catch (e) {
129
+ L(p(e));
130
+ }
131
+ }
132
+ }, [P]);
133
+ return l(() => () => {
134
+ let e = B.current;
135
+ B.current = null, U(), e && y.leave(e).catch(() => {});
136
+ }, [y, U]), {
137
+ status: S,
138
+ session: w,
139
+ localStream: E,
140
+ remoteStreams: O,
141
+ peerStates: A,
142
+ muted: M,
143
+ videoOn: P,
144
+ error: I,
145
+ join: G,
146
+ leave: K,
147
+ toggleMute: q,
148
+ toggleVideo: J
149
+ };
150
+ }
151
+ //#endregion
152
+ export { p as describeMediaError, h as useVoiceCall };
153
+
154
+ //# sourceMappingURL=useVoiceCall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVoiceCall.js","names":[],"sources":["../../../src/comradecircle/hooks/useVoiceCall.ts"],"sourcesContent":["/**\n * useVoiceCall — joins a VOICE channel's mesh call and keeps it alive\n * (BOFF-4450).\n *\n * Lifecycle: capture the microphone (and optionally the camera) → join the\n * session → open the roster and signal SSE subscriptions → dial earlier\n * participants once our signal inbox is listening → heartbeat on the\n * server-advertised interval (which also refreshes the roster) → leave on\n * demand or unmount. Media never touches the server: it flows peer to peer;\n * the server only relays negotiation messages.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\n\nimport { useComradeCircle } from '../context';\nimport { subscribeGraphqlStream } from '../lib/graphqlStream';\nimport { toRtcConfiguration } from '../lib/rtc/peerLink';\nimport { VoiceMesh } from '../lib/rtc/voiceMesh';\nimport type { ComRtcSignal, ComVoiceSession } from '../types';\nimport {\n VOICE_KEY,\n VOICE_SESSION_FIELDS,\n useComRtcConfiguration,\n useVoiceActions,\n} from './useVoiceApi';\n\nexport type VoiceCallStatus = 'idle' | 'joining' | 'connected' | 'leaving' | 'error';\n\nexport interface VoiceCallState {\n status: VoiceCallStatus;\n session: ComVoiceSession | null;\n localStream: MediaStream | null;\n remoteStreams: Map<string, MediaStream>;\n peerStates: Map<string, RTCPeerConnectionState>;\n muted: boolean;\n videoOn: boolean;\n error: string | null;\n join: (withVideo: boolean) => Promise<void>;\n leave: () => Promise<void>;\n toggleMute: () => Promise<void>;\n toggleVideo: () => Promise<void>;\n}\n\n/** Map a getUserMedia / API failure to a user-facing reason. */\nexport function describeMediaError(err: unknown): string {\n const name = err instanceof DOMException ? err.name : '';\n if (name === 'NotAllowedError' || name === 'SecurityError') {\n return 'Microphone or camera access was blocked. Allow it in your browser settings and try again.';\n }\n if (name === 'NotFoundError' || name === 'OverconstrainedError') {\n return 'No microphone or camera was found on this device.';\n }\n if (name === 'NotReadableError') {\n return 'Your microphone or camera is being used by another application.';\n }\n return err instanceof Error ? err.message : 'Something went wrong joining the call.';\n}\n\nfunction stopStream(stream: MediaStream | null): void {\n stream?.getTracks().forEach((t) => t.stop());\n}\n\nexport function useVoiceCall(channelId: string): VoiceCallState {\n const { currentUser, apiGatewayUrl } = useComradeCircle();\n const selfId = currentUser?.id ?? '';\n const actions = useVoiceActions();\n const qc = useQueryClient();\n const rtc = useComRtcConfiguration();\n\n const [status, setStatus] = useState<VoiceCallStatus>('idle');\n const [session, setSession] = useState<ComVoiceSession | null>(null);\n const [localStream, setLocalStream] = useState<MediaStream | null>(null);\n const [remoteStreams, setRemoteStreams] = useState<Map<string, MediaStream>>(new Map());\n const [peerStates, setPeerStates] = useState<Map<string, RTCPeerConnectionState>>(new Map());\n const [muted, setMuted] = useState(false);\n const [videoOn, setVideoOn] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const meshRef = useRef<VoiceMesh | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const sessionIdRef = useRef<string | null>(null);\n const disposersRef = useRef<Array<() => void>>([]);\n\n const applySession = useCallback((next: ComVoiceSession | null) => {\n setSession(next);\n if (next) meshRef.current?.updateRoster(next.participants);\n }, []);\n\n const teardown = useCallback(() => {\n disposersRef.current.forEach((dispose) => dispose());\n disposersRef.current = [];\n meshRef.current?.dispose();\n meshRef.current = null;\n stopStream(streamRef.current);\n streamRef.current = null;\n setLocalStream(null);\n setRemoteStreams(new Map());\n setPeerStates(new Map());\n }, []);\n\n const fail = useCallback(\n (message: string) => {\n const sessionId = sessionIdRef.current;\n sessionIdRef.current = null;\n teardown();\n if (sessionId) void actions.leave(sessionId).catch(() => {});\n setSession(null);\n setError(message);\n setStatus('error');\n },\n [actions, teardown]\n );\n\n const join = useCallback(\n async (withVideo: boolean) => {\n if (!selfId || sessionIdRef.current) return;\n setError(null);\n setStatus('joining');\n let media: MediaStream;\n try {\n if (!navigator.mediaDevices?.getUserMedia) {\n throw new Error('This browser does not support voice and video calls.');\n }\n media = await navigator.mediaDevices.getUserMedia({\n audio: { echoCancellation: true, noiseSuppression: true },\n video: withVideo ? { width: { ideal: 640 }, height: { ideal: 360 } } : false,\n });\n } catch (err) {\n setError(describeMediaError(err));\n setStatus('error');\n return;\n }\n streamRef.current = media;\n setLocalStream(media);\n setVideoOn(withVideo);\n setMuted(false);\n\n try {\n const config = rtc.data ?? (await rtc.refetch()).data;\n if (!config) throw new Error('Could not load call configuration.');\n const joined = await actions.join(channelId);\n sessionIdRef.current = joined.id;\n\n const mesh = new VoiceMesh({\n selfId,\n rtcConfig: toRtcConfiguration(config.iceServers),\n localStream: media,\n sendSignal: (to, type, payload) => actions.signal(joined.id, to, type, payload),\n onRemoteStreamsChange: setRemoteStreams,\n onPeerStatesChange: setPeerStates,\n });\n meshRef.current = mesh;\n applySession(joined);\n\n disposersRef.current.push(\n subscribeGraphqlStream<{ comVoiceSignal: ComRtcSignal }>({\n query: `subscription ComVoiceSignal($sessionId: ID!) {\n comVoiceSignal(sessionId: $sessionId) { fromMemberId toMemberId signalType payload }\n }`,\n variables: { sessionId: joined.id },\n operationName: 'ComVoiceSignal',\n baseUrl: apiGatewayUrl,\n onOpen: () => mesh.markSignalingReady(),\n onData: (data) => void mesh.handleSignal(data.comVoiceSignal),\n onError: (errors) => fail(errors[0]?.message ?? 'Lost the call connection.'),\n }),\n subscribeGraphqlStream<{ comVoiceParticipantsChanged: ComVoiceSession }>({\n query: `subscription ComVoiceParticipantsChanged($sessionId: ID!) {\n comVoiceParticipantsChanged(sessionId: $sessionId) { ${VOICE_SESSION_FIELDS} }\n }`,\n variables: { sessionId: joined.id },\n operationName: 'ComVoiceParticipantsChanged',\n baseUrl: apiGatewayUrl,\n onData: (data) => applySession(data.comVoiceParticipantsChanged),\n })\n );\n\n const beat = setInterval(\n () => {\n void actions\n .heartbeat(joined.id)\n .then(applySession)\n .catch(() => fail('You were disconnected from the call.'));\n },\n Math.max(5, config.heartbeatIntervalSeconds) * 1000\n );\n disposersRef.current.push(() => clearInterval(beat));\n setStatus('connected');\n } catch (err) {\n fail(err instanceof Error ? err.message : 'Could not join the call.');\n }\n },\n [actions, apiGatewayUrl, applySession, channelId, fail, rtc, selfId]\n );\n\n const leave = useCallback(async () => {\n const sessionId = sessionIdRef.current;\n sessionIdRef.current = null;\n setStatus('leaving');\n teardown();\n try {\n if (sessionId) await actions.leave(sessionId);\n } catch {\n // Already gone (reaped or ended) — nothing left to clean up server-side.\n }\n setSession(null);\n setStatus('idle');\n void qc.invalidateQueries({ queryKey: VOICE_KEY.session(channelId) });\n }, [actions, channelId, qc, teardown]);\n\n const toggleMute = useCallback(async () => {\n const stream = streamRef.current;\n const sessionId = sessionIdRef.current;\n if (!stream || !sessionId) return;\n const next = !muted;\n stream.getAudioTracks().forEach((t) => {\n t.enabled = !next;\n });\n setMuted(next);\n try {\n // The roster subscription broadcasts the new flag to everyone (us included).\n await actions.setMute(sessionId, next);\n } catch {\n // The local track is already muted; the server flag catches up on the next toggle.\n }\n }, [actions, muted]);\n\n const toggleVideo = useCallback(async () => {\n const current = streamRef.current;\n if (!current) return;\n if (videoOn) {\n current.getVideoTracks().forEach((t) => t.stop());\n const audioOnly = new MediaStream(current.getAudioTracks());\n streamRef.current = audioOnly;\n setLocalStream(audioOnly);\n meshRef.current?.setLocalStream(audioOnly);\n setVideoOn(false);\n return;\n }\n try {\n const cam = await navigator.mediaDevices.getUserMedia({\n video: { width: { ideal: 640 }, height: { ideal: 360 } },\n });\n const combined = new MediaStream([...current.getAudioTracks(), ...cam.getVideoTracks()]);\n streamRef.current = combined;\n setLocalStream(combined);\n meshRef.current?.setLocalStream(combined);\n setVideoOn(true);\n } catch (err) {\n setError(describeMediaError(err));\n }\n }, [videoOn]);\n\n // Leave the call when the channel view unmounts (navigating away).\n useEffect(\n () => () => {\n const sessionId = sessionIdRef.current;\n sessionIdRef.current = null;\n teardown();\n if (sessionId) void actions.leave(sessionId).catch(() => {});\n },\n [actions, teardown]\n );\n\n return {\n status,\n session,\n localStream,\n remoteStreams,\n peerStates,\n muted,\n videoOn,\n error,\n join,\n leave,\n toggleMute,\n toggleVideo,\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ export interface GraphqlStreamError {
2
+ message: string;
3
+ code?: string;
4
+ }
5
+ export interface GraphqlStreamOptions<TData> {
6
+ query: string;
7
+ variables: Record<string, unknown>;
8
+ operationName: string;
9
+ /** Workspace gateway base URL override (from the ComradeCircle context). */
10
+ baseUrl?: string;
11
+ onData: (data: TData) => void;
12
+ /** Called once the gateway has accepted the subscription (response headers received). */
13
+ onOpen?: () => void;
14
+ /** GraphQL errors end the stream; the caller decides what to do next. */
15
+ onError?: (errors: GraphqlStreamError[]) => void;
16
+ }
17
+ export interface SseEvent {
18
+ event: string;
19
+ data: string;
20
+ }
21
+ /**
22
+ * Incremental SSE parser. Feed it decoded text chunks; it returns every
23
+ * complete event and keeps the unterminated tail for the next call.
24
+ */
25
+ export declare function createSseParser(): (chunk: string) => SseEvent[];
26
+ /**
27
+ * Open a subscription. Returns a disposer that aborts the stream and stops
28
+ * any pending reconnect.
29
+ */
30
+ export declare function subscribeGraphqlStream<TData>(options: GraphqlStreamOptions<TData>): () => void;
31
+ //# sourceMappingURL=graphqlStream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphqlStream.d.ts","sourceRoot":"","sources":["../../../src/comradecircle/lib/graphqlStream.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB,CAAC,KAAK;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,aAAa,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,yEAAyE;IACzE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,CAAC,KAAK,EAAE,MAAM,KAAK,QAAQ,EAAE,CAsB/D;AAoCD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,oBAAoB,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,CA2F9F"}