@limrun/ui 0.14.2 → 0.14.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.
- package/dist/components/remote-control.d.ts +9 -12
- package/dist/index.cjs +1 -1
- package/dist/index.js +589 -573
- package/package.json +1 -1
- package/src/components/remote-control.tsx +51 -30
package/package.json
CHANGED
|
@@ -237,23 +237,20 @@ interface RemoteControlProps {
|
|
|
237
237
|
/**
|
|
238
238
|
* iOS only: when true, capture the user's microphone via
|
|
239
239
|
* `getUserMedia({ audio: true })` and stream it live into the
|
|
240
|
-
* simulator's mock microphone.
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* signaling to the host. Progress and failures are reported via
|
|
247
|
-
* `onMicrophoneStateChange`. Ignored on Android instances.
|
|
240
|
+
* simulator's mock microphone. This is a manual override: iOS apps
|
|
241
|
+
* that actively consume microphone input also trigger capture
|
|
242
|
+
* automatically, like camera demand. The component handles the
|
|
243
|
+
* browser permission prompt, WebRTC track attachment, and
|
|
244
|
+
* `microphoneStart`/`microphoneStop` signaling. Progress and failures
|
|
245
|
+
* are reported via `onMicrophoneStateChange`. Ignored on Android.
|
|
248
246
|
*/
|
|
249
247
|
microphoneEnabled?: boolean;
|
|
250
248
|
|
|
251
249
|
/**
|
|
252
250
|
* Fires whenever the live-microphone pipeline changes state:
|
|
253
|
-
* after
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* the mic button's on/off/error state.
|
|
251
|
+
* after a manual or guest-demanded attempt resolves, when the host
|
|
252
|
+
* acknowledges or rejects the stream, when the browser revokes the
|
|
253
|
+
* track mid-stream, and when capture stops.
|
|
257
254
|
*/
|
|
258
255
|
onMicrophoneStateChange?: (state: MicrophoneState) => void;
|
|
259
256
|
}
|
|
@@ -642,14 +639,11 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
642
639
|
packetsSent?: number;
|
|
643
640
|
packetsLost?: number;
|
|
644
641
|
} | null>(null);
|
|
645
|
-
//
|
|
646
|
-
//
|
|
647
|
-
// track rides a pre-allocated sendonly audio transceiver so
|
|
648
|
-
// enabling/disabling is just a `replaceTrack`, plus a
|
|
649
|
-
// `microphoneStart`/`microphoneStop` WS signal so the host's
|
|
650
|
-
// audio state machine switches its source.
|
|
642
|
+
// Outbound microphone state (iOS only). The manual prop and guest-driven
|
|
643
|
+
// microphoneRequest signals share one pre-allocated audio transceiver.
|
|
651
644
|
const microphoneEnabledRef = useRef(microphoneEnabled);
|
|
652
645
|
microphoneEnabledRef.current = microphoneEnabled;
|
|
646
|
+
const microphoneDemandActiveRef = useRef(false);
|
|
653
647
|
const onMicrophoneStateChangeRef = useRef(onMicrophoneStateChange);
|
|
654
648
|
onMicrophoneStateChangeRef.current = onMicrophoneStateChange;
|
|
655
649
|
const outboundMicSenderRef = useRef<RTCRtpSender | null>(null);
|
|
@@ -1921,6 +1915,14 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
1921
1915
|
return next;
|
|
1922
1916
|
};
|
|
1923
1917
|
|
|
1918
|
+
const isMicrophoneWanted = () => microphoneEnabledRef.current || microphoneDemandActiveRef.current;
|
|
1919
|
+
|
|
1920
|
+
const sendMicrophoneStart = (ws: WebSocket) => {
|
|
1921
|
+
const id = `ui-mic-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1922
|
+
micStartPendingIdRef.current = id;
|
|
1923
|
+
ws.send(JSON.stringify({ type: 'microphoneStart', id }));
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1924
1926
|
// Capture the user's microphone and stream it to the simulator's
|
|
1925
1927
|
// mock microphone. Safe to call redundantly: bails when already
|
|
1926
1928
|
// attached, when the connection isn't up yet (the ICE-connected
|
|
@@ -1935,7 +1937,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
1935
1937
|
|
|
1936
1938
|
const attachMicrophoneOp = async () => {
|
|
1937
1939
|
const generation = micGenerationRef.current;
|
|
1938
|
-
const isCurrent = () => generation === micGenerationRef.current &&
|
|
1940
|
+
const isCurrent = () => generation === micGenerationRef.current && isMicrophoneWanted();
|
|
1939
1941
|
const reportMicError = (error: string) => {
|
|
1940
1942
|
safeInvoke('onMicrophoneStateChange', onMicrophoneStateChangeRef.current, {
|
|
1941
1943
|
active: false,
|
|
@@ -2007,9 +2009,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2007
2009
|
if (outboundMicStreamRef.current !== stream) return;
|
|
2008
2010
|
void detachMicrophone('microphone capture ended by the browser');
|
|
2009
2011
|
};
|
|
2010
|
-
|
|
2011
|
-
micStartPendingIdRef.current = id;
|
|
2012
|
-
ws.send(JSON.stringify({ type: 'microphoneStart', id }));
|
|
2012
|
+
sendMicrophoneStart(ws);
|
|
2013
2013
|
safeInvoke('onMicrophoneStateChange', onMicrophoneStateChangeRef.current, {
|
|
2014
2014
|
active: true,
|
|
2015
2015
|
label: audioTrack.label || undefined,
|
|
@@ -2203,10 +2203,10 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2203
2203
|
stopCameraStatsPoller();
|
|
2204
2204
|
stopOutboundLocalStream();
|
|
2205
2205
|
outboundCameraSenderRef.current = null;
|
|
2206
|
-
// Same for the live microphone.
|
|
2207
|
-
//
|
|
2208
|
-
// is still on, the post-reconnect hook re-attaches.
|
|
2206
|
+
// Same for the live microphone. Manual enablement survives reconnect;
|
|
2207
|
+
// guest demand is replayed by the host after it assigns the new session.
|
|
2209
2208
|
++micGenerationRef.current;
|
|
2209
|
+
microphoneDemandActiveRef.current = false;
|
|
2210
2210
|
micStartPendingIdRef.current = null;
|
|
2211
2211
|
if (outboundMicStreamRef.current) {
|
|
2212
2212
|
stopMediaStream(outboundMicStreamRef.current);
|
|
@@ -2770,7 +2770,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2770
2770
|
clearIceDisconnectedGrace();
|
|
2771
2771
|
// Re-attach the live microphone across reconnects (or apply a
|
|
2772
2772
|
// prop that was flipped on before the connection was up).
|
|
2773
|
-
if (
|
|
2773
|
+
if (isMicrophoneWanted() && !outboundMicStreamRef.current) {
|
|
2774
2774
|
void attachMicrophone();
|
|
2775
2775
|
}
|
|
2776
2776
|
return;
|
|
@@ -3170,6 +3170,29 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
3170
3170
|
startCameraStatsPoller();
|
|
3171
3171
|
break;
|
|
3172
3172
|
}
|
|
3173
|
+
case 'microphoneRequest': {
|
|
3174
|
+
if (platform !== 'ios' || !outboundMicSenderRef.current) {
|
|
3175
|
+
break;
|
|
3176
|
+
}
|
|
3177
|
+
const active = message.active === true;
|
|
3178
|
+
microphoneDemandActiveRef.current = active;
|
|
3179
|
+
// eslint-disable-next-line no-console
|
|
3180
|
+
console.info('[RemoteControl] microphoneRequest received, active=', active);
|
|
3181
|
+
if (!active) {
|
|
3182
|
+
if (!microphoneEnabledRef.current) {
|
|
3183
|
+
await detachMicrophone();
|
|
3184
|
+
}
|
|
3185
|
+
break;
|
|
3186
|
+
}
|
|
3187
|
+
if (outboundMicStreamRef.current) {
|
|
3188
|
+
// A manual stream may already be attached. Re-select it on the
|
|
3189
|
+
// host because file playback can have replaced its source.
|
|
3190
|
+
sendMicrophoneStart(ws);
|
|
3191
|
+
break;
|
|
3192
|
+
}
|
|
3193
|
+
await attachMicrophone();
|
|
3194
|
+
break;
|
|
3195
|
+
}
|
|
3173
3196
|
case 'microphoneStartResult': {
|
|
3174
3197
|
if (micStartPendingIdRef.current === null || message.id !== micStartPendingIdRef.current) {
|
|
3175
3198
|
break;
|
|
@@ -3312,11 +3335,9 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
3312
3335
|
}
|
|
3313
3336
|
}, [cameraAspect]);
|
|
3314
3337
|
|
|
3315
|
-
// Drive the
|
|
3316
|
-
// When flipped on before the connection is up, attachMicrophone
|
|
3317
|
-
// bails and the ICE-connected handler picks it up instead.
|
|
3338
|
+
// Drive the manual override without interrupting guest-driven demand.
|
|
3318
3339
|
useEffect(() => {
|
|
3319
|
-
if (
|
|
3340
|
+
if (isMicrophoneWanted()) {
|
|
3320
3341
|
void attachMicrophone();
|
|
3321
3342
|
} else {
|
|
3322
3343
|
void detachMicrophone();
|