Skip to content

Commit 487e241

Browse files
committed
Video calibration
1 parent 95daec6 commit 487e241

70 files changed

Lines changed: 8551 additions & 78 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

gui/electron/main/index.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,12 @@ import { writeFileSync } from 'node:fs';
3232
import { spawn } from 'node:child_process';
3333
import { discordPresence } from './presence';
3434
import { options } from './cli';
35-
import { ServerStatusEvent } from 'electron/preload/interface';
35+
import { ServerStatusEvent, WebcamOfferRequest } from 'electron/preload/interface';
3636
import { mkdir } from 'node:fs/promises';
3737
import { MenuItem } from 'electron/main';
3838

39-
40-
app.setPath('userData', getGuiDataFolder())
41-
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'))
39+
app.setPath('userData', getGuiDataFolder());
40+
app.setPath('sessionData', join(getGuiDataFolder(), 'electron'));
4241

4342
// Register custom protocol to handle asset paths with leading slashes
4443
protocol.registerSchemesAsPrivileged([
@@ -55,6 +54,13 @@ protocol.registerSchemesAsPrivileged([
5554

5655
let mainWindow: BrowserWindow | null = null;
5756

57+
function buildWebcamOfferUrl(host: string, port: number) {
58+
const normalizedHost =
59+
host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
60+
61+
return `http://${normalizedHost}:${port}/offer`;
62+
}
63+
5864
handleIpc(IPC_CHANNELS.GH_FETCH, async (e, options) => {
5965
if (options.type === 'fw-releases') {
6066
return fetch(
@@ -153,6 +159,31 @@ handleIpc(IPC_CHANNELS.DISCORD_PRESENCE, async (e, options) => {
153159
}
154160
});
155161

162+
handleIpc(IPC_CHANNELS.WEBCAM_OFFER, async (e, request: WebcamOfferRequest) => {
163+
const response = await fetch(buildWebcamOfferUrl(request.host, request.port), {
164+
method: 'POST',
165+
headers: {
166+
'Content-Type': 'application/json',
167+
},
168+
body: JSON.stringify({
169+
sdp: request.sdp,
170+
}),
171+
});
172+
173+
if (!response.ok) {
174+
throw new Error(`Offer request failed with status ${response.status}`);
175+
}
176+
177+
const body = (await response.json()) as { sdp?: unknown };
178+
if (typeof body.sdp !== 'string' || body.sdp.length === 0) {
179+
throw new Error('Webcam response did not contain an SDP answer');
180+
}
181+
182+
return {
183+
sdp: body.sdp,
184+
};
185+
});
186+
156187
handleIpc(IPC_CHANNELS.OPEN_FILE, (e, folder) => {
157188
const requestedPath = path.resolve(folder);
158189

@@ -339,8 +370,7 @@ function createWindow() {
339370
menu.append(new MenuItem({ label: 'Copy', role: 'copy' }));
340371
menu.append(new MenuItem({ label: 'Paste', role: 'paste' }));
341372

342-
if (mainWindow)
343-
menu.popup({ window: mainWindow });
373+
if (mainWindow) menu.popup({ window: mainWindow });
344374
});
345375
}
346376

gui/electron/preload/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
3131
setTranslations: () => {},
3232
openDialog: (options) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_DIALOG, options),
3333
saveDialog: (options) => ipcRenderer.invoke(IPC_CHANNELS.SAVE_DIALOG, options),
34-
openConfigFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'config')),
35-
openLogsFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'logs')),
34+
openConfigFolder: async () =>
35+
ipcRenderer.invoke(
36+
IPC_CHANNELS.OPEN_FILE,
37+
await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'config')
38+
),
39+
openLogsFolder: async () =>
40+
ipcRenderer.invoke(
41+
IPC_CHANNELS.OPEN_FILE,
42+
await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'logs')
43+
),
3644
openFile: (path) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, path),
3745
ghGet: (req) => ipcRenderer.invoke(IPC_CHANNELS.GH_FETCH, req),
38-
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options)
46+
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options),
47+
webcamOffer: (request) => ipcRenderer.invoke(IPC_CHANNELS.WEBCAM_OFFER, request),
3948
} satisfies IElectronAPI);

gui/electron/preload/interface.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,15 @@ export type GHReturn = {
3434
| null;
3535
};
3636

37-
export type DiscordPresence = { enable: false } | { enable: true, activity: string }
37+
export type DiscordPresence = { enable: false } | { enable: true; activity: string };
38+
export type WebcamOfferRequest = {
39+
host: string;
40+
port: number;
41+
sdp: string;
42+
};
43+
export type WebcamOfferResponse = {
44+
sdp: string;
45+
};
3846

3947
export interface IElectronAPI {
4048
onServerStatus: (cb: (data: ServerStatusEvent) => void) => () => void;
@@ -55,6 +63,7 @@ export interface IElectronAPI {
5563
openFile: (path: string) => void;
5664
ghGet: <T extends GHGet>(options: T) => Promise<GHReturn[T['type']]>;
5765
setPresence: (options: DiscordPresence) => void;
66+
webcamOffer: (request: WebcamOfferRequest) => Promise<WebcamOfferResponse>;
5867
}
5968

6069
declare global {

gui/electron/shared.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import {
44
SaveDialogOptions,
55
SaveDialogReturnValue,
66
} from 'electron';
7-
import { DiscordPresence, GHGet, GHReturn, OSStats } from './preload/interface';
7+
import {
8+
DiscordPresence,
9+
GHGet,
10+
GHReturn,
11+
OSStats,
12+
WebcamOfferRequest,
13+
WebcamOfferResponse,
14+
} from './preload/interface';
815

916
export const IPC_CHANNELS = {
1017
SERVER_STATUS: 'server-status',
@@ -19,7 +26,8 @@ export const IPC_CHANNELS = {
1926
OPEN_FILE: 'open-file',
2027
GET_FOLDER: 'get-folder',
2128
GH_FETCH: 'gh-fetch',
22-
DISCORD_PRESENCE: 'discord-presence'
29+
DISCORD_PRESENCE: 'discord-presence',
30+
WEBCAM_OFFER: 'webcam-offer',
2331
} as const;
2432

2533
export interface IpcInvokeMap {
@@ -46,4 +54,7 @@ export interface IpcInvokeMap {
4654
options: T
4755
) => Promise<GHReturn[T['type']]>;
4856
[IPC_CHANNELS.DISCORD_PRESENCE]: (options: DiscordPresence) => void;
57+
[IPC_CHANNELS.WEBCAM_OFFER]: (
58+
request: WebcamOfferRequest
59+
) => Promise<WebcamOfferResponse>;
4960
}

gui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
},
1818
"scripts": {
1919
"start": "vite --force",
20+
"dev:clean": "rd /s /q \"node_modules/.vite\" && pnpm run gui",
2021
"gui": "electron-vite dev --config electron.vite.config.ts --watch",
2122
"build": "electron-vite build --config electron.vite.config.ts",
2223
"package": "electron-builder",

gui/public/i18n/en/translation.ftl

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,44 @@ navbar-home = Home
273273
navbar-body_proportions = Body Proportions
274274
navbar-trackers_assign = Tracker Assignment
275275
navbar-mounting = Mounting Calibration
276+
navbar-video_calibration = Video Calibration
276277
navbar-onboarding = Setup Wizard
277278
navbar-settings = Settings
278279
navbar-connect_trackers = Connect Trackers
279280
281+
## Video calibration
282+
video-calibration-loading = Looking for a webcam...
283+
video-calibration-connecting = Connecting to the webcam video stream...
284+
video-calibration-no_webcam = No webcam is currently available.
285+
video-calibration-error = Could not start the webcam video stream.
286+
video-calibration-sidebar-title = Video Calibration
287+
video-calibration-sidebar-description = Start calibration and monitor its progress here.
288+
video-calibration-sidebar-description-active = Current progress is shown under Status below.
289+
video-calibration-start = Start Calibration
290+
video-calibration-status-label = Status
291+
video-calibration-status-idle = Waiting to start calibration.
292+
video-calibration-status-starting = Waiting for calibration progress...
293+
video-calibration-status-calibrate_camera = Calibrating camera
294+
video-calibration-status-capture_forward_pose = Capture forward pose
295+
video-calibration-status-capture_bent_over_pose = Capture bent-over pose
296+
video-calibration-status-calibrate_trackers = Calibrating trackers
297+
video-calibration-status-calibrate_skeleton_offsets = Calibrating skeleton offsets
298+
video-calibration-status-done = Calibration complete
299+
video-calibration-instruction-calibrate_camera = Move your right controller in a ∞ loop
300+
video-calibration-instruction-capture_forward_pose = Stand straight and face forward
301+
video-calibration-instruction-capture_bent_over_pose = Carefully lean forward
302+
video-calibration-instruction-calibrate_trackers = Walk around in a small circle
303+
video-calibration-camera-label = Camera
304+
video-calibration-camera-available = Camera data available
305+
video-calibration-camera-unavailable = Camera data is not available yet.
306+
video-calibration-camera-resolution = Resolution
307+
video-calibration-camera-focal_length = Focal length
308+
video-calibration-camera-principal_point = Principal point
309+
video-calibration-done-trackers = Done trackers
310+
video-calibration-pending-trackers = Pending trackers
311+
video-calibration-none = None
312+
video-calibration-error-label = Error
313+
280314
## Biovision hierarchy recording
281315
bvh-start_recording = Record BVH
282316
bvh-stop_recording = Save BVH recording

gui/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { ChecklistPage } from './components/tracking-checklist/TrackingChecklist
5353
import { ElectronContextC, provideElectron } from './hooks/electron';
5454
import { AppLocalizationProvider } from './i18n/config';
5555
import { openUrl } from './hooks/crossplatform';
56+
import { VideoCalibrationPage } from './components/video-calibration/VideoCalibrationPage';
5657

5758
export const GH_REPO = 'SlimeVR/SlimeVR-Server';
5859
export const VersionContext = createContext('');
@@ -104,6 +105,10 @@ function Layout() {
104105
</MainLayout>
105106
}
106107
/>
108+
<Route
109+
path="/video-calibration"
110+
element={<VideoCalibrationPage isMobile={isMobile} />}
111+
/>
107112
<Route
108113
path="/tracker/:trackernum/:deviceid"
109114
element={

gui/src/components/MainLayout.scss

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@
1717
/ var(--navbar-w) calc(100% - var(--navbar-w) - var(--right-section-w)) var(--right-section-w);
1818
}
1919

20+
&.full.no-toolbar {
21+
grid-template:
22+
't t t' var(--topbar-h)
23+
'n c s' calc(100% - var(--topbar-h))
24+
/ var(--navbar-w) calc(100% - var(--navbar-w) - var(--right-section-w)) var(--right-section-w);
25+
}
26+
2027
@screen nsm {
2128
--right-section-w: 40%;
2229
}
@@ -58,6 +65,15 @@
5865
/ 100%;
5966
}
6067

68+
&.full.no-toolbar {
69+
grid-template:
70+
't' var(--topbar-h)
71+
'l' var(--checklist-h)
72+
'c' calc(100% - var(--topbar-h) - var(--checklist-h) - var(--navbar-h))
73+
'n' calc(var(--navbar-h))
74+
/ 100%;
75+
}
76+
6177
grid-template:
6278
't' var(--topbar-h)
6379
'c' calc(100% - var(--topbar-h) - var(--navbar-h))

gui/src/components/MainLayout.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ export function MainLayout({
2020
background = true,
2121
full = false,
2222
isMobile = undefined,
23+
rightSidebar,
24+
showToolbar = true,
25+
scrollContent = true,
2326
}: {
2427
children: ReactNode;
2528
background?: boolean;
2629
isMobile?: boolean;
27-
showToolbarSettings?: boolean;
2830
full?: boolean;
31+
rightSidebar?: ReactNode;
32+
showToolbar?: boolean;
33+
scrollContent?: boolean;
2934
}) {
3035
const { completion } = useTrackingChecklist();
3136
const { sendRPCPacket } = useWebsocketAPI();
@@ -65,6 +70,7 @@ export function MainLayout({
6570
return (
6671
<div
6772
className={classNames('main-layout w-full h-screen', full && 'full', {
73+
'no-toolbar': full && !showToolbar,
6874
'checklist-ok': completion === 'complete',
6975
})}
7076
>
@@ -78,7 +84,8 @@ export function MainLayout({
7884
<div
7985
style={{ gridArea: 'c' }}
8086
className={classNames(
81-
'overflow-y-auto mr-2 my-2 mobile:m-0',
87+
scrollContent ? 'overflow-y-auto' : 'overflow-hidden',
88+
'mr-2 my-2 mobile:m-0',
8289
'flex flex-col rounded-md',
8390
background && 'bg-background-70',
8491
{ 'rounded-t-none': !isMobile && full }
@@ -89,14 +96,14 @@ export function MainLayout({
8996
{full && isMobile && completion !== 'complete' && (
9097
<TrackingChecklistMobile />
9198
)}
92-
{full && (
99+
{full && showToolbar && (
93100
<div style={{ gridArea: 'b' }}>
94101
<Toolbar />
95102
</div>
96103
)}
97104
{!isMobile && full && (
98105
<div style={{ gridArea: 's' }} className="mr-2">
99-
<Sidebar />
106+
{rightSidebar || <Sidebar />}
100107
</div>
101108
)}
102109
</div>

gui/src/components/Navbar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useBreakpoint } from '@/hooks/breakpoint';
99
import { HomeIcon } from './commons/icon/HomeIcon';
1010
import { SkiIcon } from './commons/icon/SkiIcon';
1111
import { WifiIcon } from './commons/icon/WifiIcon';
12+
import { EyeIcon } from './commons/icon/EyeIcon';
1213

1314
export function NavButton({
1415
to,
@@ -94,6 +95,9 @@ export function MainLinks() {
9495
>
9596
{l10n.getString('navbar-body_proportions')}
9697
</NavButton>
98+
<NavButton to="/video-calibration" icon={<EyeIcon />}>
99+
{l10n.getString('navbar-video_calibration')}
100+
</NavButton>
97101
<NavButton
98102
to="/onboarding/wifi-creds"
99103
icon={<WifiIcon value={1} disabled variant="navbar" />}

0 commit comments

Comments
 (0)