@limrun/ui 0.14.3 → 0.15.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/components/remote-control.d.ts +7 -5
- package/dist/index.cjs +1 -1
- package/dist/index.js +1107 -1104
- package/package.json +1 -1
- package/src/components/remote-control.tsx +41 -28
package/package.json
CHANGED
|
@@ -176,8 +176,9 @@ interface RemoteControlProps {
|
|
|
176
176
|
* mode. Use it to render a richer status indicator (e.g.
|
|
177
177
|
* "Camera · 1920×1080 · 30 fps · FaceTime HD").
|
|
178
178
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
179
|
+
* iOS instances always fire this callback. Android instances fire it
|
|
180
|
+
* when the server supports camera injection (Android 15+); older
|
|
181
|
+
* Android instances have no camera-injector path and stay silent.
|
|
181
182
|
*/
|
|
182
183
|
onCameraDemandChange?: (active: boolean, granted?: boolean, camera?: CameraCaptureInfo) => void;
|
|
183
184
|
|
|
@@ -235,14 +236,15 @@ interface RemoteControlProps {
|
|
|
235
236
|
cameraAspect?: CameraAspect;
|
|
236
237
|
|
|
237
238
|
/**
|
|
238
|
-
*
|
|
239
|
+
* When true, capture the user's microphone via
|
|
239
240
|
* `getUserMedia({ audio: true })` and stream it live into the
|
|
240
|
-
*
|
|
241
|
+
* device's mock microphone. This is a manual override: apps
|
|
241
242
|
* that actively consume microphone input also trigger capture
|
|
242
243
|
* automatically, like camera demand. The component handles the
|
|
243
244
|
* browser permission prompt, WebRTC track attachment, and
|
|
244
245
|
* `microphoneStart`/`microphoneStop` signaling. Progress and failures
|
|
245
|
-
* are reported via `onMicrophoneStateChange`. Ignored on Android
|
|
246
|
+
* are reported via `onMicrophoneStateChange`. Ignored on Android
|
|
247
|
+
* servers without camera/mic injection support (Android 14 and older).
|
|
246
248
|
*/
|
|
247
249
|
microphoneEnabled?: boolean;
|
|
248
250
|
|
|
@@ -1931,7 +1933,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
1931
1933
|
// handler firing on both `connected` and `completed`) must not
|
|
1932
1934
|
// cancel an attach already in flight; only detach/teardown cancel.
|
|
1933
1935
|
const attachMicrophone = (): Promise<void> => {
|
|
1934
|
-
|
|
1936
|
+
// No platform gate: attachMicrophoneOp bails when the session never
|
|
1937
|
+
// allocated a sendonly mic transceiver (non-capable servers).
|
|
1935
1938
|
return runMicOp(() => attachMicrophoneOp());
|
|
1936
1939
|
};
|
|
1937
1940
|
|
|
@@ -2479,8 +2482,13 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2479
2482
|
updateStatus('WebSocket closed');
|
|
2480
2483
|
};
|
|
2481
2484
|
|
|
2482
|
-
// Request RTCConfiguration
|
|
2483
|
-
|
|
2485
|
+
// Request RTCConfiguration. The reply doubles as the capability
|
|
2486
|
+
// handshake: camera-capable Android servers advertise
|
|
2487
|
+
// `cameraSupported: true` alongside the configuration.
|
|
2488
|
+
const rtcConfigPromise = new Promise<{
|
|
2489
|
+
rtcConfiguration: RTCConfiguration;
|
|
2490
|
+
cameraSupported: boolean;
|
|
2491
|
+
}>((resolve, reject) => {
|
|
2484
2492
|
const timeoutId = window.setTimeout(() => reject(new Error('RTCConfiguration timeout')), 30000);
|
|
2485
2493
|
|
|
2486
2494
|
const messageHandler = (event: MessageEvent) => {
|
|
@@ -2489,7 +2497,10 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2489
2497
|
if (message.type === 'rtcConfiguration') {
|
|
2490
2498
|
window.clearTimeout(timeoutId);
|
|
2491
2499
|
ws.removeEventListener('message', messageHandler);
|
|
2492
|
-
resolve(
|
|
2500
|
+
resolve({
|
|
2501
|
+
rtcConfiguration: message.rtcConfiguration,
|
|
2502
|
+
cameraSupported: message.cameraSupported === true,
|
|
2503
|
+
});
|
|
2493
2504
|
}
|
|
2494
2505
|
} catch (e) {
|
|
2495
2506
|
window.clearTimeout(timeoutId);
|
|
@@ -2508,7 +2519,8 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2508
2519
|
);
|
|
2509
2520
|
});
|
|
2510
2521
|
|
|
2511
|
-
const rtcConfig
|
|
2522
|
+
const { rtcConfiguration: rtcConfig, cameraSupported: serverCameraSupported } =
|
|
2523
|
+
await rtcConfigPromise;
|
|
2512
2524
|
if (!isCurrentAttempt() || wsRef.current !== ws) {
|
|
2513
2525
|
return;
|
|
2514
2526
|
}
|
|
@@ -2518,17 +2530,20 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2518
2530
|
peerConnection.addTransceiver('audio', { direction: 'recvonly' });
|
|
2519
2531
|
const videoTransceiver = peerConnection.addTransceiver('video', { direction: 'recvonly' });
|
|
2520
2532
|
|
|
2533
|
+
// iOS always supports camera/mic injection; Android servers
|
|
2534
|
+
// advertise it through `cameraSupported` (Android 15 and newer).
|
|
2535
|
+
// Never offer the extra sendonly m-lines to non-capable Android
|
|
2536
|
+
// servers — older servers cannot negotiate them safely.
|
|
2537
|
+
const canSendMedia = platform === 'ios' || serverCameraSupported;
|
|
2538
|
+
|
|
2521
2539
|
// Pre-allocate a sendonly slot for the user's camera so the
|
|
2522
|
-
//
|
|
2540
|
+
// device can pull frames later without renegotiating SDP.
|
|
2523
2541
|
// Allocating up-front means we don't have to renegotiate the
|
|
2524
|
-
// SDP when the
|
|
2525
|
-
//
|
|
2526
|
-
//
|
|
2527
|
-
// `replaceTrack`.
|
|
2528
|
-
// iOS only: Android has no camera consumer, and the extra
|
|
2529
|
-
// m=video line crashes Android 14's scrcpy/libwebrtc.
|
|
2542
|
+
// SDP when the device app actually opens its camera session —
|
|
2543
|
+
// the codec/SSRC negotiation happens once, here, and turning
|
|
2544
|
+
// the camera on/off later is just a `replaceTrack`.
|
|
2530
2545
|
const outboundCameraTransceiver =
|
|
2531
|
-
|
|
2546
|
+
canSendMedia ?
|
|
2532
2547
|
peerConnection.addTransceiver('video', {
|
|
2533
2548
|
direction: 'sendonly',
|
|
2534
2549
|
})
|
|
@@ -2537,10 +2552,9 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2537
2552
|
|
|
2538
2553
|
// Same pre-allocation for the live microphone: a sendonly audio
|
|
2539
2554
|
// slot negotiated once at connection setup, so toggling the mic
|
|
2540
|
-
// later is just `replaceTrack` — no SDP renegotiation.
|
|
2541
|
-
// like the camera.
|
|
2555
|
+
// later is just `replaceTrack` — no SDP renegotiation.
|
|
2542
2556
|
const outboundMicTransceiver =
|
|
2543
|
-
|
|
2557
|
+
canSendMedia ?
|
|
2544
2558
|
peerConnection.addTransceiver('audio', {
|
|
2545
2559
|
direction: 'sendonly',
|
|
2546
2560
|
})
|
|
@@ -2934,12 +2948,11 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
2934
2948
|
pendingScreenshotRejectersRef.current.delete(message.id);
|
|
2935
2949
|
break;
|
|
2936
2950
|
case 'cameraRequest': {
|
|
2937
|
-
// Only
|
|
2938
|
-
// transceiver; without it there is nothing to
|
|
2939
|
-
// ignore camera requests entirely — no
|
|
2940
|
-
// no onCameraDemandChange callbacks.
|
|
2941
|
-
|
|
2942
|
-
if (platform !== 'ios' || !outboundCameraSenderRef.current) {
|
|
2951
|
+
// Only camera-capable sessions allocate the sendonly
|
|
2952
|
+
// outbound-camera transceiver; without it there is nothing to
|
|
2953
|
+
// feed the device, so ignore camera requests entirely — no
|
|
2954
|
+
// getUserMedia prompt and no onCameraDemandChange callbacks.
|
|
2955
|
+
if (!outboundCameraSenderRef.current) {
|
|
2943
2956
|
break;
|
|
2944
2957
|
}
|
|
2945
2958
|
const active = message.active === true;
|
|
@@ -3171,7 +3184,7 @@ export const RemoteControl = forwardRef<RemoteControlHandle, RemoteControlProps>
|
|
|
3171
3184
|
break;
|
|
3172
3185
|
}
|
|
3173
3186
|
case 'microphoneRequest': {
|
|
3174
|
-
if (
|
|
3187
|
+
if (!outboundMicSenderRef.current) {
|
|
3175
3188
|
break;
|
|
3176
3189
|
}
|
|
3177
3190
|
const active = message.active === true;
|