add: stylesheet
This commit is contained in:
Binary file not shown.
@@ -1,4 +1,16 @@
|
|||||||
import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot";
|
import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot";
|
||||||
|
import type { HandTrackingStatus } from "@/types/handTracking";
|
||||||
|
|
||||||
|
const STATUS_LABELS: Record<HandTrackingStatus, string> = {
|
||||||
|
idle: "Idle",
|
||||||
|
requesting_camera: "Requesting camera",
|
||||||
|
starting_camera: "Starting camera",
|
||||||
|
connecting_server: "Connecting server",
|
||||||
|
connecting: "Connecting",
|
||||||
|
connected: "Connected",
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
error: "Error",
|
||||||
|
};
|
||||||
|
|
||||||
export function HandTrackingOverlay(): React.JSX.Element | null {
|
export function HandTrackingOverlay(): React.JSX.Element | null {
|
||||||
const { hands, status, serverStatus, error } = useHandTrackingSnapshot();
|
const { hands, status, serverStatus, error } = useHandTrackingSnapshot();
|
||||||
@@ -12,7 +24,7 @@ export function HandTrackingOverlay(): React.JSX.Element | null {
|
|||||||
return (
|
return (
|
||||||
<aside className="hand-tracking-overlay" aria-label="Hand tracking status">
|
<aside className="hand-tracking-overlay" aria-label="Hand tracking status">
|
||||||
<strong>Hand tracking</strong>
|
<strong>Hand tracking</strong>
|
||||||
<span>Status: {status}</span>
|
<span>Status: {STATUS_LABELS[status]}</span>
|
||||||
{serverStatus ? <span>Server: {serverStatus}</span> : null}
|
{serverStatus ? <span>Server: {serverStatus}</span> : null}
|
||||||
<span>Hands: {hands.length}</span>
|
<span>Hands: {hands.length}</span>
|
||||||
<span>Pinch: {pinching ? "yes" : "no"}</span>
|
<span>Pinch: {pinching ? "yes" : "no"}</span>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const HAND_TRACKING_FRAME_WIDTH = 320;
|
|||||||
export const HAND_TRACKING_FRAME_HEIGHT = 240;
|
export const HAND_TRACKING_FRAME_HEIGHT = 240;
|
||||||
export const HAND_TRACKING_TARGET_FPS = 10;
|
export const HAND_TRACKING_TARGET_FPS = 10;
|
||||||
export const HAND_TRACKING_JPEG_QUALITY = 0.55;
|
export const HAND_TRACKING_JPEG_QUALITY = 0.55;
|
||||||
|
export const HAND_TRACKING_CAMERA_TIMEOUT_MS = 8_000;
|
||||||
export const HAND_TRACKING_RESPONSE_TIMEOUT_MS = 1_500;
|
export const HAND_TRACKING_RESPONSE_TIMEOUT_MS = 1_500;
|
||||||
|
|
||||||
export function getHandTrackingWsUrl(): string {
|
export function getHandTrackingWsUrl(): string {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
HAND_TRACKING_CAMERA_TIMEOUT_MS,
|
||||||
HAND_TRACKING_FRAME_HEIGHT,
|
HAND_TRACKING_FRAME_HEIGHT,
|
||||||
HAND_TRACKING_FRAME_WIDTH,
|
HAND_TRACKING_FRAME_WIDTH,
|
||||||
HAND_TRACKING_JPEG_QUALITY,
|
HAND_TRACKING_JPEG_QUALITY,
|
||||||
@@ -29,6 +30,32 @@ function getBase64Payload(dataUrl: string): string {
|
|||||||
return dataUrl.slice(dataUrl.indexOf(",") + 1);
|
return dataUrl.slice(dataUrl.indexOf(",") + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCameraStreamWithTimeout(
|
||||||
|
constraints: MediaStreamConstraints,
|
||||||
|
): Promise<MediaStream> {
|
||||||
|
let didTimeout = false;
|
||||||
|
const streamPromise = navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
didTimeout = true;
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"Camera request timed out. Restart Arc or check camera permissions for localhost:5173.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, HAND_TRACKING_CAMERA_TIMEOUT_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
streamPromise.then((stream) => {
|
||||||
|
if (didTimeout) {
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.race([streamPromise, timeoutPromise]);
|
||||||
|
}
|
||||||
|
|
||||||
export function useRemoteHandTracking({
|
export function useRemoteHandTracking({
|
||||||
enabled,
|
enabled,
|
||||||
websocketUrl = getHandTrackingWsUrl(),
|
websocketUrl = getHandTrackingWsUrl(),
|
||||||
@@ -116,13 +143,13 @@ export function useRemoteHandTracking({
|
|||||||
|
|
||||||
setSnapshot({
|
setSnapshot({
|
||||||
hands: [],
|
hands: [],
|
||||||
status: "connecting",
|
status: "requesting_camera",
|
||||||
serverStatus: null,
|
serverStatus: null,
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await getCameraStreamWithTimeout({
|
||||||
video: {
|
video: {
|
||||||
width: HAND_TRACKING_FRAME_WIDTH,
|
width: HAND_TRACKING_FRAME_WIDTH,
|
||||||
height: HAND_TRACKING_FRAME_HEIGHT,
|
height: HAND_TRACKING_FRAME_HEIGHT,
|
||||||
@@ -136,12 +163,27 @@ export function useRemoteHandTracking({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSnapshot((current) => ({
|
||||||
|
...current,
|
||||||
|
status: "starting_camera",
|
||||||
|
}));
|
||||||
|
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.playsInline = true;
|
video.playsInline = true;
|
||||||
video.srcObject = stream;
|
video.srcObject = stream;
|
||||||
await video.play();
|
await video.play();
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSnapshot((current) => ({
|
||||||
|
...current,
|
||||||
|
status: "connecting_server",
|
||||||
|
}));
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = HAND_TRACKING_FRAME_WIDTH;
|
canvas.width = HAND_TRACKING_FRAME_WIDTH;
|
||||||
canvas.height = HAND_TRACKING_FRAME_HEIGHT;
|
canvas.height = HAND_TRACKING_FRAME_HEIGHT;
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
font-family: Inter;
|
font-family: Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@@ -83,7 +83,7 @@ canvas {
|
|||||||
.hand-tracking-overlay {
|
.hand-tracking-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
bottom: 16px;
|
bottom: 132px;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export interface HandTrackingHand {
|
|||||||
|
|
||||||
export type HandTrackingStatus =
|
export type HandTrackingStatus =
|
||||||
| "idle"
|
| "idle"
|
||||||
|
| "requesting_camera"
|
||||||
|
| "starting_camera"
|
||||||
|
| "connecting_server"
|
||||||
| "connecting"
|
| "connecting"
|
||||||
| "connected"
|
| "connected"
|
||||||
| "disconnected"
|
| "disconnected"
|
||||||
|
|||||||
Reference in New Issue
Block a user