@ksp-gonogo/kerbcast-react 1.11.0-rc.4 → 1.11.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/README.md +19 -4
- package/dist/CameraFeed.js +77 -39
- package/dist/KerbalFaceFeed.js +2 -2
- package/dist/hooks/useReportDisplaySize.d.ts +46 -5
- package/dist/hooks/useReportDisplaySize.js +186 -31
- package/package.json +2 -2
- package/src/CameraFeed.test.tsx +247 -6
- package/src/CameraFeed.tsx +337 -289
- package/src/KerbalFaceFeed.tsx +2 -2
- package/src/hooks/useReportDisplaySize.loop.test.tsx +290 -0
- package/src/hooks/useReportDisplaySize.test.tsx +14 -11
- package/src/hooks/useReportDisplaySize.ts +230 -32
package/src/CameraFeed.tsx
CHANGED
|
@@ -38,7 +38,7 @@ import { useKerbcastCameras } from "./hooks/useKerbcastCameras";
|
|
|
38
38
|
import { useKerbcastInFlight } from "./hooks/useKerbcastInFlight";
|
|
39
39
|
import { useKerbcastStream } from "./hooks/useKerbcastStream";
|
|
40
40
|
import { useRecordings } from "./hooks/useRecordings";
|
|
41
|
-
import { useReportDisplaySize } from "./hooks/useReportDisplaySize";
|
|
41
|
+
import { DEFAULT_FEED_ASPECT, useReportDisplaySize } from "./hooks/useReportDisplaySize";
|
|
42
42
|
import { useTargetBracketsPreference } from "./hooks/useTargetBracketsPreference";
|
|
43
43
|
import { StandbyIcon } from "./StandbyIcon";
|
|
44
44
|
import { isCameraDestroyed } from "./lifecycle";
|
|
@@ -740,10 +740,36 @@ const CameraFeedInner = forwardRef<CameraFeedHandle, CameraFeedProps>(
|
|
|
740
740
|
// -------------------------------------------------------------------------
|
|
741
741
|
// Display-size reporting (auto-resolution). The feed self-measures its
|
|
742
742
|
// rendered box and reports it per-consumer; the sidecar maxes across
|
|
743
|
-
// consumers.
|
|
744
|
-
//
|
|
743
|
+
// consumers. Auto picks the resolution, never the shape: the request and
|
|
744
|
+
// the feed's own box keep the camera's configured capture aspect (16:9
|
|
745
|
+
// when unknown). Disabled by `renderSize="none"`. The video and the
|
|
746
|
+
// sidecar's render size let it ignore a resize that only follows the
|
|
747
|
+
// stream. See useReportDisplaySize for debounce / bucketing / the guard.
|
|
745
748
|
// -------------------------------------------------------------------------
|
|
746
|
-
|
|
749
|
+
const baseW = camera?.maxWidth ?? 0;
|
|
750
|
+
const baseH = camera?.maxHeight ?? 0;
|
|
751
|
+
const hasBase = baseW > 0 && baseH > 0;
|
|
752
|
+
const feedAspect = hasBase ? baseW / baseH : DEFAULT_FEED_ASPECT;
|
|
753
|
+
const feedShape = useMemo(
|
|
754
|
+
() =>
|
|
755
|
+
({
|
|
756
|
+
"--kc-feed-w": hasBase ? baseW : 16,
|
|
757
|
+
"--kc-feed-h": hasBase ? baseH : 9,
|
|
758
|
+
}) as React.CSSProperties,
|
|
759
|
+
[hasBase, baseW, baseH],
|
|
760
|
+
);
|
|
761
|
+
const renderedW = camera?.renderWidth ?? 0;
|
|
762
|
+
const renderedH = camera?.renderHeight ?? 0;
|
|
763
|
+
const renderedSize = useMemo(
|
|
764
|
+
() => ({ width: renderedW, height: renderedH }),
|
|
765
|
+
[renderedW, renderedH],
|
|
766
|
+
);
|
|
767
|
+
useReportDisplaySize(flightId, wrapRef, {
|
|
768
|
+
enabled: renderSize === "auto",
|
|
769
|
+
aspect: feedAspect,
|
|
770
|
+
videoRef,
|
|
771
|
+
streamSize: renderedSize,
|
|
772
|
+
});
|
|
747
773
|
|
|
748
774
|
// -------------------------------------------------------------------------
|
|
749
775
|
// UI state
|
|
@@ -1232,307 +1258,309 @@ const CameraFeedInner = forwardRef<CameraFeedHandle, CameraFeedProps>(
|
|
|
1232
1258
|
) : null;
|
|
1233
1259
|
|
|
1234
1260
|
return (
|
|
1235
|
-
<
|
|
1236
|
-
{
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
<CameraMenuItem
|
|
1267
|
-
type="button"
|
|
1268
|
-
role="menuitemradio"
|
|
1269
|
-
aria-checked={camera.viewerQuality == null}
|
|
1270
|
-
$selected={camera.viewerQuality == null}
|
|
1271
|
-
onClick={() => selectQuality(null)}
|
|
1261
|
+
<FeedBox style={feedShape}>
|
|
1262
|
+
<Stage ref={wrapRef} $pinned={chromePinned}>
|
|
1263
|
+
{flightId === null ? (
|
|
1264
|
+
<>
|
|
1265
|
+
<Empty>{emptyMessage}</Empty>
|
|
1266
|
+
{topOverlay}
|
|
1267
|
+
{actionBar}
|
|
1268
|
+
</>
|
|
1269
|
+
) : (
|
|
1270
|
+
<>
|
|
1271
|
+
<StyledVideo
|
|
1272
|
+
ref={attachVideo}
|
|
1273
|
+
autoPlay
|
|
1274
|
+
playsInline
|
|
1275
|
+
muted
|
|
1276
|
+
controls={false}
|
|
1277
|
+
onClick={() => setChromePinned((v) => !v)}
|
|
1278
|
+
/>
|
|
1279
|
+
{topOverlay}
|
|
1280
|
+
{actionBar}
|
|
1281
|
+
{qualityAvailable &&
|
|
1282
|
+
qualityMenu.open &&
|
|
1283
|
+
qualityMenu.position &&
|
|
1284
|
+
camera &&
|
|
1285
|
+
createPortal(
|
|
1286
|
+
<QualityMenu
|
|
1287
|
+
ref={qualityMenu.menuRef}
|
|
1288
|
+
id={qualityMenuId}
|
|
1289
|
+
role="menu"
|
|
1290
|
+
aria-label="Quality"
|
|
1291
|
+
style={qualityMenu.position}
|
|
1272
1292
|
>
|
|
1273
|
-
Auto
|
|
1274
|
-
</CameraMenuItem>
|
|
1275
|
-
{QUALITY_PRESETS.map(({ preset, label, scale }) => (
|
|
1276
1293
|
<CameraMenuItem
|
|
1277
|
-
key={preset}
|
|
1278
1294
|
type="button"
|
|
1279
1295
|
role="menuitemradio"
|
|
1280
|
-
aria-checked={camera.viewerQuality
|
|
1281
|
-
$selected={camera.viewerQuality
|
|
1282
|
-
onClick={() => selectQuality(
|
|
1296
|
+
aria-checked={camera.viewerQuality == null}
|
|
1297
|
+
$selected={camera.viewerQuality == null}
|
|
1298
|
+
onClick={() => selectQuality(null)}
|
|
1283
1299
|
>
|
|
1284
|
-
|
|
1285
|
-
{presetDim(camera.operatorHeight, scale)})
|
|
1300
|
+
Auto
|
|
1286
1301
|
</CameraMenuItem>
|
|
1287
|
-
|
|
1288
|
-
<QualityMeta role="status">
|
|
1289
|
-
now {camera.renderWidth}×{camera.renderHeight}
|
|
1290
|
-
{qualityThrottled ? " · throttled" : ""}
|
|
1291
|
-
</QualityMeta>
|
|
1292
|
-
</QualityMenu>,
|
|
1293
|
-
document.body,
|
|
1294
|
-
)}
|
|
1295
|
-
{trackingAvailable &&
|
|
1296
|
-
trackingMenu.open &&
|
|
1297
|
-
trackingMenu.position &&
|
|
1298
|
-
camera &&
|
|
1299
|
-
createPortal(
|
|
1300
|
-
<QualityMenu
|
|
1301
|
-
ref={trackingMenu.menuRef}
|
|
1302
|
-
id={trackingMenuId}
|
|
1303
|
-
role="menu"
|
|
1304
|
-
aria-label="Auto-track"
|
|
1305
|
-
style={trackingMenu.position}
|
|
1306
|
-
>
|
|
1307
|
-
<CameraMenuItem
|
|
1308
|
-
type="button"
|
|
1309
|
-
role="menuitemradio"
|
|
1310
|
-
aria-checked={trackMode === TrackMode.ActiveVessel}
|
|
1311
|
-
$selected={trackMode === TrackMode.ActiveVessel}
|
|
1312
|
-
onClick={() => selectTrack(TrackMode.ActiveVessel)}
|
|
1313
|
-
>
|
|
1314
|
-
Track active vessel
|
|
1315
|
-
</CameraMenuItem>
|
|
1316
|
-
<CameraMenuItem
|
|
1317
|
-
type="button"
|
|
1318
|
-
role="menuitemradio"
|
|
1319
|
-
aria-checked={trackMode === TrackMode.Target}
|
|
1320
|
-
$selected={trackMode === TrackMode.Target}
|
|
1321
|
-
onClick={() => selectTrack(TrackMode.Target)}
|
|
1322
|
-
>
|
|
1323
|
-
Track target
|
|
1324
|
-
</CameraMenuItem>
|
|
1325
|
-
</QualityMenu>,
|
|
1326
|
-
document.body,
|
|
1327
|
-
)}
|
|
1328
|
-
{commandTargetAvailable &&
|
|
1329
|
-
commandTargetMenu.open &&
|
|
1330
|
-
commandTargetMenu.position &&
|
|
1331
|
-
createPortal(
|
|
1332
|
-
<CameraMenu
|
|
1333
|
-
ref={commandTargetMenu.menuRef}
|
|
1334
|
-
id={commandTargetMenuId}
|
|
1335
|
-
role="menu"
|
|
1336
|
-
aria-label="Target"
|
|
1337
|
-
style={commandTargetMenu.position}
|
|
1338
|
-
>
|
|
1339
|
-
{commandTargets.map((t) => {
|
|
1340
|
-
const eligible = t.eligibility === CommandTargetEligibility.Ok;
|
|
1341
|
-
const current = t.persistentId === commandTargetId;
|
|
1342
|
-
return (
|
|
1302
|
+
{QUALITY_PRESETS.map(({ preset, label, scale }) => (
|
|
1343
1303
|
<CameraMenuItem
|
|
1344
|
-
key={
|
|
1304
|
+
key={preset}
|
|
1345
1305
|
type="button"
|
|
1346
1306
|
role="menuitemradio"
|
|
1347
|
-
aria-checked={
|
|
1348
|
-
$selected={
|
|
1349
|
-
|
|
1350
|
-
title={eligible ? undefined : t.eligibilityText}
|
|
1351
|
-
onClick={() => selectCommandTarget(t.persistentId)}
|
|
1307
|
+
aria-checked={camera.viewerQuality === preset}
|
|
1308
|
+
$selected={camera.viewerQuality === preset}
|
|
1309
|
+
onClick={() => selectQuality(preset)}
|
|
1352
1310
|
>
|
|
1353
|
-
{
|
|
1354
|
-
{
|
|
1355
|
-
{eligible ? "" : ` · ${t.eligibilityText}`}
|
|
1311
|
+
{label} ({presetDim(camera.operatorWidth, scale)}×
|
|
1312
|
+
{presetDim(camera.operatorHeight, scale)})
|
|
1356
1313
|
</CameraMenuItem>
|
|
1357
|
-
)
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1314
|
+
))}
|
|
1315
|
+
<QualityMeta role="status">
|
|
1316
|
+
now {camera.renderWidth}×{camera.renderHeight}
|
|
1317
|
+
{qualityThrottled ? " · throttled" : ""}
|
|
1318
|
+
</QualityMeta>
|
|
1319
|
+
</QualityMenu>,
|
|
1320
|
+
document.body,
|
|
1321
|
+
)}
|
|
1322
|
+
{trackingAvailable &&
|
|
1323
|
+
trackingMenu.open &&
|
|
1324
|
+
trackingMenu.position &&
|
|
1325
|
+
camera &&
|
|
1326
|
+
createPortal(
|
|
1327
|
+
<QualityMenu
|
|
1328
|
+
ref={trackingMenu.menuRef}
|
|
1329
|
+
id={trackingMenuId}
|
|
1330
|
+
role="menu"
|
|
1331
|
+
aria-label="Auto-track"
|
|
1332
|
+
style={trackingMenu.position}
|
|
1333
|
+
>
|
|
1334
|
+
<CameraMenuItem
|
|
1335
|
+
type="button"
|
|
1336
|
+
role="menuitemradio"
|
|
1337
|
+
aria-checked={trackMode === TrackMode.ActiveVessel}
|
|
1338
|
+
$selected={trackMode === TrackMode.ActiveVessel}
|
|
1339
|
+
onClick={() => selectTrack(TrackMode.ActiveVessel)}
|
|
1340
|
+
>
|
|
1341
|
+
Track active vessel
|
|
1342
|
+
</CameraMenuItem>
|
|
1343
|
+
<CameraMenuItem
|
|
1344
|
+
type="button"
|
|
1345
|
+
role="menuitemradio"
|
|
1346
|
+
aria-checked={trackMode === TrackMode.Target}
|
|
1347
|
+
$selected={trackMode === TrackMode.Target}
|
|
1348
|
+
onClick={() => selectTrack(TrackMode.Target)}
|
|
1349
|
+
>
|
|
1350
|
+
Track target
|
|
1351
|
+
</CameraMenuItem>
|
|
1352
|
+
</QualityMenu>,
|
|
1353
|
+
document.body,
|
|
1354
|
+
)}
|
|
1355
|
+
{commandTargetAvailable &&
|
|
1356
|
+
commandTargetMenu.open &&
|
|
1357
|
+
commandTargetMenu.position &&
|
|
1358
|
+
createPortal(
|
|
1359
|
+
<CameraMenu
|
|
1360
|
+
ref={commandTargetMenu.menuRef}
|
|
1361
|
+
id={commandTargetMenuId}
|
|
1362
|
+
role="menu"
|
|
1363
|
+
aria-label="Target"
|
|
1364
|
+
style={commandTargetMenu.position}
|
|
1365
|
+
>
|
|
1366
|
+
{commandTargets.map((t) => {
|
|
1367
|
+
const eligible = t.eligibility === CommandTargetEligibility.Ok;
|
|
1368
|
+
const current = t.persistentId === commandTargetId;
|
|
1369
|
+
return (
|
|
1370
|
+
<CameraMenuItem
|
|
1371
|
+
key={t.persistentId}
|
|
1372
|
+
type="button"
|
|
1373
|
+
role="menuitemradio"
|
|
1374
|
+
aria-checked={current}
|
|
1375
|
+
$selected={current}
|
|
1376
|
+
disabled={!eligible}
|
|
1377
|
+
title={eligible ? undefined : t.eligibilityText}
|
|
1378
|
+
onClick={() => selectCommandTarget(t.persistentId)}
|
|
1379
|
+
>
|
|
1380
|
+
{t.vesselName}
|
|
1381
|
+
{t.rangeM != null ? ` · ${formatCommandRange(t.rangeM)}` : ""}
|
|
1382
|
+
{eligible ? "" : ` · ${t.eligibilityText}`}
|
|
1383
|
+
</CameraMenuItem>
|
|
1384
|
+
);
|
|
1385
|
+
})}
|
|
1386
|
+
</CameraMenu>,
|
|
1387
|
+
document.body,
|
|
1388
|
+
)}
|
|
1389
|
+
{commandState &&
|
|
1390
|
+
!outOfFlight &&
|
|
1391
|
+
(
|
|
1392
|
+
<>
|
|
1393
|
+
{commandState.blank && <CommandBlank reason={commandState.blank} />}
|
|
1394
|
+
{targetBracketsAvailable && bracketsOn && (
|
|
1395
|
+
<CommandBrackets
|
|
1396
|
+
state={commandState}
|
|
1397
|
+
displayUt={commandDisplayUt?.ut ?? commandTrack!.ut}
|
|
1398
|
+
utSource={commandDisplayUt?.source ?? null}
|
|
1399
|
+
video={videoEl}
|
|
1400
|
+
fallbackSize={
|
|
1401
|
+
camera ? { width: camera.renderWidth, height: camera.renderHeight } : null
|
|
1402
|
+
}
|
|
1403
|
+
overlay={targetBracketOverlay}
|
|
1404
|
+
/>
|
|
1405
|
+
)}
|
|
1406
|
+
</>
|
|
1407
|
+
)}
|
|
1408
|
+
{outOfFlight ? (
|
|
1409
|
+
showStandbyIcon && (
|
|
1410
|
+
<StandbyOverlay role="status" aria-label="Standby, no active flight">
|
|
1411
|
+
<StandbyIconWrap>
|
|
1412
|
+
<StandbyIcon size={40} />
|
|
1413
|
+
</StandbyIconWrap>
|
|
1414
|
+
</StandbyOverlay>
|
|
1415
|
+
)
|
|
1416
|
+
) : (
|
|
1417
|
+
isDestroyed && (
|
|
1418
|
+
<SignalLostOverlay role="status" aria-label="Signal lost">
|
|
1419
|
+
<SignalLostText $animated={effectiveShowStatic}>
|
|
1420
|
+
SIGNAL LOST
|
|
1421
|
+
</SignalLostText>
|
|
1422
|
+
</SignalLostOverlay>
|
|
1423
|
+
)
|
|
1361
1424
|
)}
|
|
1362
|
-
|
|
1363
|
-
!outOfFlight &&
|
|
1364
|
-
(
|
|
1425
|
+
{!outOfFlight && !effectiveShowStatic && isStale && !isDestroyed && (
|
|
1365
1426
|
<>
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
<
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
utSource={commandDisplayUt?.source ?? null}
|
|
1372
|
-
video={videoEl}
|
|
1373
|
-
fallbackSize={
|
|
1374
|
-
camera ? { width: camera.renderWidth, height: camera.renderHeight } : null
|
|
1375
|
-
}
|
|
1376
|
-
overlay={targetBracketOverlay}
|
|
1377
|
-
/>
|
|
1378
|
-
)}
|
|
1427
|
+
<StaleScrim aria-hidden="true" />
|
|
1428
|
+
<StaleBadge role="status" aria-label="Feed stale">
|
|
1429
|
+
<StaleIcon />
|
|
1430
|
+
Stale
|
|
1431
|
+
</StaleBadge>
|
|
1379
1432
|
</>
|
|
1380
1433
|
)}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
controllerRef.current?.setFovSliderDragging(false);
|
|
1458
|
-
}}
|
|
1459
|
-
/>
|
|
1460
|
-
<ZoomButton
|
|
1461
|
-
type="button"
|
|
1462
|
-
aria-label="Zoom out"
|
|
1463
|
-
$pos="bottom"
|
|
1464
|
-
disabled={manualDisabled}
|
|
1465
|
-
onPointerDown={() => controllerRef.current?.setZoomRate(-1)}
|
|
1466
|
-
onPointerUp={() => controllerRef.current?.setZoomRate(0)}
|
|
1467
|
-
onPointerLeave={() => controllerRef.current?.setZoomRate(0)}
|
|
1468
|
-
onPointerCancel={() => controllerRef.current?.setZoomRate(0)}
|
|
1469
|
-
onClick={(e) => {
|
|
1470
|
-
if (e.detail === 0) controllerRef.current?.nudgeZoom(1);
|
|
1471
|
-
}}
|
|
1472
|
-
>
|
|
1473
|
-
−
|
|
1474
|
-
</ZoomButton>
|
|
1475
|
-
</ZoomControlsWrap>
|
|
1476
|
-
)}
|
|
1477
|
-
{showPan && (
|
|
1478
|
-
<PanControl
|
|
1479
|
-
role="group"
|
|
1480
|
-
aria-label="Pan camera"
|
|
1481
|
-
$disabled={manualDisabled}
|
|
1482
|
-
aria-hidden={manualDisabled}
|
|
1483
|
-
>
|
|
1484
|
-
<PanArrow
|
|
1485
|
-
type="button"
|
|
1486
|
-
$dir="up"
|
|
1487
|
-
aria-label="Pan up"
|
|
1488
|
-
disabled={manualDisabled || !supportsPitch}
|
|
1489
|
-
onClick={() => controllerRef.current?.nudgePan(0, 1)}
|
|
1490
|
-
>
|
|
1491
|
-
▲
|
|
1492
|
-
</PanArrow>
|
|
1493
|
-
<PanArrow
|
|
1494
|
-
type="button"
|
|
1495
|
-
$dir="down"
|
|
1496
|
-
aria-label="Pan down"
|
|
1497
|
-
disabled={manualDisabled || !supportsPitch}
|
|
1498
|
-
onClick={() => controllerRef.current?.nudgePan(0, -1)}
|
|
1499
|
-
>
|
|
1500
|
-
▼
|
|
1501
|
-
</PanArrow>
|
|
1502
|
-
<PanArrow
|
|
1503
|
-
type="button"
|
|
1504
|
-
$dir="left"
|
|
1505
|
-
aria-label="Pan left"
|
|
1506
|
-
disabled={manualDisabled}
|
|
1507
|
-
onClick={() => controllerRef.current?.nudgePan(-1, 0)}
|
|
1508
|
-
>
|
|
1509
|
-
◀
|
|
1510
|
-
</PanArrow>
|
|
1511
|
-
<PanArrow
|
|
1512
|
-
type="button"
|
|
1513
|
-
$dir="right"
|
|
1514
|
-
aria-label="Pan right"
|
|
1515
|
-
disabled={manualDisabled}
|
|
1516
|
-
onClick={() => controllerRef.current?.nudgePan(1, 0)}
|
|
1434
|
+
{recordingActive && armingActive && (
|
|
1435
|
+
<ArmingBadge role="status" aria-label="Arming">
|
|
1436
|
+
<ArmingDot aria-hidden="true" />
|
|
1437
|
+
ARMING
|
|
1438
|
+
</ArmingBadge>
|
|
1439
|
+
)}
|
|
1440
|
+
{recordingActive && !armingActive && (
|
|
1441
|
+
<RecBadge role="status" aria-label="Recording">
|
|
1442
|
+
<RecDot aria-hidden="true" />
|
|
1443
|
+
REC {formatElapsed(recordingElapsedMs)}
|
|
1444
|
+
</RecBadge>
|
|
1445
|
+
)}
|
|
1446
|
+
{showZoom && (
|
|
1447
|
+
<ZoomControlsWrap $disabled={manualDisabled} aria-hidden={manualDisabled}>
|
|
1448
|
+
<ZoomButton
|
|
1449
|
+
type="button"
|
|
1450
|
+
aria-label="Zoom in"
|
|
1451
|
+
$pos="top"
|
|
1452
|
+
disabled={manualDisabled}
|
|
1453
|
+
onPointerDown={() =>
|
|
1454
|
+
controllerRef.current?.setZoomRate(1)
|
|
1455
|
+
}
|
|
1456
|
+
onPointerUp={() => controllerRef.current?.setZoomRate(0)}
|
|
1457
|
+
onPointerLeave={() => controllerRef.current?.setZoomRate(0)}
|
|
1458
|
+
onPointerCancel={() => controllerRef.current?.setZoomRate(0)}
|
|
1459
|
+
onClick={(e) => {
|
|
1460
|
+
if (e.detail === 0) controllerRef.current?.nudgeZoom(-1);
|
|
1461
|
+
}}
|
|
1462
|
+
>
|
|
1463
|
+
+
|
|
1464
|
+
</ZoomButton>
|
|
1465
|
+
<FovSlider
|
|
1466
|
+
type="range"
|
|
1467
|
+
aria-label="Zoom"
|
|
1468
|
+
min={camera.fovMin}
|
|
1469
|
+
max={camera.fovMax}
|
|
1470
|
+
step={0.5}
|
|
1471
|
+
value={sliderFov}
|
|
1472
|
+
disabled={manualDisabled}
|
|
1473
|
+
onChange={(e) => {
|
|
1474
|
+
const v = Number(e.target.value);
|
|
1475
|
+
controllerRef.current?.fovSliderInput(v);
|
|
1476
|
+
}}
|
|
1477
|
+
onPointerDown={() => {
|
|
1478
|
+
controllerRef.current?.setFovSliderDragging(true);
|
|
1479
|
+
}}
|
|
1480
|
+
onPointerUp={() => {
|
|
1481
|
+
controllerRef.current?.setFovSliderDragging(false);
|
|
1482
|
+
}}
|
|
1483
|
+
onPointerCancel={() => {
|
|
1484
|
+
controllerRef.current?.setFovSliderDragging(false);
|
|
1485
|
+
}}
|
|
1486
|
+
/>
|
|
1487
|
+
<ZoomButton
|
|
1488
|
+
type="button"
|
|
1489
|
+
aria-label="Zoom out"
|
|
1490
|
+
$pos="bottom"
|
|
1491
|
+
disabled={manualDisabled}
|
|
1492
|
+
onPointerDown={() => controllerRef.current?.setZoomRate(-1)}
|
|
1493
|
+
onPointerUp={() => controllerRef.current?.setZoomRate(0)}
|
|
1494
|
+
onPointerLeave={() => controllerRef.current?.setZoomRate(0)}
|
|
1495
|
+
onPointerCancel={() => controllerRef.current?.setZoomRate(0)}
|
|
1496
|
+
onClick={(e) => {
|
|
1497
|
+
if (e.detail === 0) controllerRef.current?.nudgeZoom(1);
|
|
1498
|
+
}}
|
|
1499
|
+
>
|
|
1500
|
+
−
|
|
1501
|
+
</ZoomButton>
|
|
1502
|
+
</ZoomControlsWrap>
|
|
1503
|
+
)}
|
|
1504
|
+
{showPan && (
|
|
1505
|
+
<PanControl
|
|
1506
|
+
role="group"
|
|
1507
|
+
aria-label="Pan camera"
|
|
1508
|
+
$disabled={manualDisabled}
|
|
1509
|
+
aria-hidden={manualDisabled}
|
|
1517
1510
|
>
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1511
|
+
<PanArrow
|
|
1512
|
+
type="button"
|
|
1513
|
+
$dir="up"
|
|
1514
|
+
aria-label="Pan up"
|
|
1515
|
+
disabled={manualDisabled || !supportsPitch}
|
|
1516
|
+
onClick={() => controllerRef.current?.nudgePan(0, 1)}
|
|
1517
|
+
>
|
|
1518
|
+
▲
|
|
1519
|
+
</PanArrow>
|
|
1520
|
+
<PanArrow
|
|
1521
|
+
type="button"
|
|
1522
|
+
$dir="down"
|
|
1523
|
+
aria-label="Pan down"
|
|
1524
|
+
disabled={manualDisabled || !supportsPitch}
|
|
1525
|
+
onClick={() => controllerRef.current?.nudgePan(0, -1)}
|
|
1526
|
+
>
|
|
1527
|
+
▼
|
|
1528
|
+
</PanArrow>
|
|
1529
|
+
<PanArrow
|
|
1530
|
+
type="button"
|
|
1531
|
+
$dir="left"
|
|
1532
|
+
aria-label="Pan left"
|
|
1533
|
+
disabled={manualDisabled}
|
|
1534
|
+
onClick={() => controllerRef.current?.nudgePan(-1, 0)}
|
|
1535
|
+
>
|
|
1536
|
+
◀
|
|
1537
|
+
</PanArrow>
|
|
1538
|
+
<PanArrow
|
|
1539
|
+
type="button"
|
|
1540
|
+
$dir="right"
|
|
1541
|
+
aria-label="Pan right"
|
|
1542
|
+
disabled={manualDisabled}
|
|
1543
|
+
onClick={() => controllerRef.current?.nudgePan(1, 0)}
|
|
1544
|
+
>
|
|
1545
|
+
▶
|
|
1546
|
+
</PanArrow>
|
|
1547
|
+
<PanBall
|
|
1548
|
+
aria-hidden="true"
|
|
1549
|
+
title="Drag to pan"
|
|
1550
|
+
onPointerDown={manualDisabled ? undefined : handleBallDown}
|
|
1551
|
+
onPointerMove={manualDisabled ? undefined : handleBallMove}
|
|
1552
|
+
onPointerUp={manualDisabled ? undefined : handleBallUp}
|
|
1553
|
+
onPointerCancel={manualDisabled ? undefined : handleBallUp}
|
|
1554
|
+
style={{
|
|
1555
|
+
transform: `translate(${ballPos.x}px, ${ballPos.y}px)`,
|
|
1556
|
+
}}
|
|
1557
|
+
/>
|
|
1558
|
+
</PanControl>
|
|
1559
|
+
)}
|
|
1560
|
+
</>
|
|
1561
|
+
)}
|
|
1562
|
+
</Stage>
|
|
1563
|
+
</FeedBox>
|
|
1536
1564
|
);
|
|
1537
1565
|
},
|
|
1538
1566
|
);
|
|
@@ -2147,7 +2175,27 @@ const Panel = styled.div`
|
|
|
2147
2175
|
overflow: hidden;
|
|
2148
2176
|
`;
|
|
2149
2177
|
|
|
2178
|
+
/*
|
|
2179
|
+
* Fills the host and centres the Stage at the camera's configured aspect
|
|
2180
|
+
* (--kc-feed-w / --kc-feed-h), so the picture fills the Stage and the host's
|
|
2181
|
+
* spare room sits outside it.
|
|
2182
|
+
*/
|
|
2183
|
+
const FeedBox = styled.div`
|
|
2184
|
+
width: 100%;
|
|
2185
|
+
height: 100%;
|
|
2186
|
+
min-width: 0;
|
|
2187
|
+
min-height: 0;
|
|
2188
|
+
container-type: size;
|
|
2189
|
+
display: flex;
|
|
2190
|
+
align-items: center;
|
|
2191
|
+
justify-content: center;
|
|
2192
|
+
`;
|
|
2193
|
+
|
|
2150
2194
|
const Stage = styled(Panel)<{ $pinned: boolean }>`
|
|
2195
|
+
width: min(100cqw, 100cqh * var(--kc-feed-w) / var(--kc-feed-h));
|
|
2196
|
+
height: auto;
|
|
2197
|
+
aspect-ratio: var(--kc-feed-w) / var(--kc-feed-h);
|
|
2198
|
+
flex: none;
|
|
2151
2199
|
padding: 0;
|
|
2152
2200
|
gap: 0;
|
|
2153
2201
|
position: relative;
|