@jgengine/react 0.7.0 → 0.8.0

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/dist/voice.js ADDED
@@ -0,0 +1,130 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { createPushToTalk, } from "@jgengine/core/multiplayer/voiceContract";
4
+ /**
5
+ * Mic capture + push-to-talk + channel roster over the VoiceTransport
6
+ * signaling seam. Transmission gates the captured tracks' `enabled` flag; the
7
+ * media plane that actually moves audio bytes (WebRTC/SFU) stays behind the
8
+ * transport, host-supplied. Call once per voice channel and hand the returned
9
+ * state to the voice components.
10
+ */
11
+ export function useVoice(options) {
12
+ const transport = options?.transport;
13
+ const channelId = options?.channelId ?? "voice";
14
+ const resolveRoutes = options?.resolveRoutes;
15
+ const requestedMode = options?.mode ?? "hold";
16
+ const [transmitting, setTransmitting] = useState(false);
17
+ const [version, setVersion] = useState(0);
18
+ const bump = useCallback(() => setVersion((current) => current + 1), []);
19
+ void version;
20
+ const pttRef = useRef(null);
21
+ if (pttRef.current === null) {
22
+ pttRef.current = createPushToTalk({ mode: requestedMode, onChange: setTransmitting });
23
+ }
24
+ const ptt = pttRef.current;
25
+ const [micStream, setMicStream] = useState(null);
26
+ const [micError, setMicError] = useState(null);
27
+ const micRef = useRef(null);
28
+ const getUserMedia = options?.getUserMedia ??
29
+ (typeof navigator !== "undefined" && navigator.mediaDevices !== undefined
30
+ ? (constraints) => navigator.mediaDevices.getUserMedia(constraints)
31
+ : undefined);
32
+ const supported = getUserMedia !== undefined;
33
+ const requestMic = useCallback(async () => {
34
+ if (getUserMedia === undefined) {
35
+ setMicError("microphone capture not supported");
36
+ return false;
37
+ }
38
+ try {
39
+ const stream = await getUserMedia({ audio: true });
40
+ micRef.current = stream;
41
+ setMicStream(stream);
42
+ setMicError(null);
43
+ void transport?.publish(channelId, stream.id);
44
+ return true;
45
+ }
46
+ catch (error) {
47
+ setMicError(error instanceof Error ? error.message : "microphone permission denied");
48
+ return false;
49
+ }
50
+ }, [getUserMedia, transport, channelId]);
51
+ useEffect(() => {
52
+ const stream = micRef.current;
53
+ if (stream === null)
54
+ return;
55
+ for (const track of stream.getAudioTracks())
56
+ track.enabled = transmitting;
57
+ }, [transmitting, micStream]);
58
+ useEffect(() => {
59
+ return () => {
60
+ const stream = micRef.current;
61
+ if (stream !== null)
62
+ for (const track of stream.getTracks())
63
+ track.stop();
64
+ };
65
+ }, []);
66
+ const [participants, setParticipants] = useState([]);
67
+ useEffect(() => {
68
+ if (transport === undefined)
69
+ return undefined;
70
+ void transport.join(channelId, micRef.current?.id);
71
+ const unsubscribe = transport.subscribers(channelId, setParticipants);
72
+ return () => {
73
+ unsubscribe();
74
+ void transport.leave(channelId);
75
+ };
76
+ }, [transport, channelId]);
77
+ const routes = resolveRoutes?.() ?? [];
78
+ return useMemo(() => ({
79
+ supported,
80
+ micStream,
81
+ micError,
82
+ requestMic,
83
+ transmitting,
84
+ status: ptt.status(),
85
+ mode: ptt.mode(),
86
+ setMode(mode) {
87
+ ptt.setMode(mode);
88
+ bump();
89
+ },
90
+ muted: ptt.muted(),
91
+ setMuted(muted) {
92
+ ptt.setMuted(muted);
93
+ bump();
94
+ },
95
+ keyDown() {
96
+ ptt.keyDown();
97
+ bump();
98
+ },
99
+ keyUp() {
100
+ ptt.keyUp();
101
+ bump();
102
+ },
103
+ participants,
104
+ routes,
105
+ gainFor(userId) {
106
+ let gain = 0;
107
+ for (const route of routes) {
108
+ if (route.fromUserId === userId && route.gain > gain)
109
+ gain = route.gain;
110
+ }
111
+ return gain;
112
+ },
113
+ }), [supported, micStream, micError, requestMic, transmitting, participants, routes, ptt, bump]);
114
+ }
115
+ export function PushToTalkButton({ voice, className, children, }) {
116
+ return (_jsx("button", { type: "button", className: className, "data-push-to-talk": true, "data-transmitting": voice.transmitting, "data-status": voice.status, onPointerDown: () => voice.keyDown(), onPointerUp: () => voice.keyUp(), onPointerLeave: () => voice.keyUp(), children: children ?? (voice.transmitting ? "Transmitting" : "Push to talk") }));
117
+ }
118
+ export function MicToggle({ voice, className, mutedLabel, unmutedLabel, }) {
119
+ return (_jsx("button", { type: "button", className: className, "data-mic-toggle": true, "data-muted": voice.muted, "aria-pressed": voice.muted, onClick: () => voice.setMuted(!voice.muted), children: voice.muted ? (mutedLabel ?? "Unmute") : (unmutedLabel ?? "Mute") }));
120
+ }
121
+ export function SpeakingIndicator({ voice, userId, className, threshold = 0.01, children, }) {
122
+ const gain = voice.gainFor(userId);
123
+ return (_jsx("span", { className: className, "data-speaking": gain > threshold, "data-gain": gain.toFixed(3), "data-user": userId, children: children }));
124
+ }
125
+ export function VoiceRoster({ voice, className, participantClassName, renderParticipant, }) {
126
+ return (_jsx("div", { className: className, "data-voice-roster": true, children: voice.participants.map((participant) => {
127
+ const gain = voice.gainFor(participant.userId);
128
+ return (_jsx("div", { className: participantClassName, "data-voice-participant": participant.userId, "data-published": participant.streamId !== undefined, "data-speaking": gain > 0.01, children: renderParticipant !== undefined ? renderParticipant(participant, gain) : participant.userId }, participant.userId));
129
+ }) }));
130
+ }