File size: 9,151 Bytes
6ce4ca6
 
3cdf7b9
 
6ce4ca6
 
 
 
 
3165745
ada77ab
6ce4ca6
 
 
 
 
 
 
 
 
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3165745
3cdf7b9
6ce4ca6
 
 
3cdf7b9
6ce4ca6
3cdf7b9
6ce4ca6
 
 
 
 
8173aa6
6ce4ca6
 
 
3cdf7b9
 
 
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cdf7b9
6ce4ca6
 
 
3cdf7b9
6ce4ca6
3cdf7b9
 
6ce4ca6
 
 
 
 
 
3cdf7b9
 
 
 
 
6ce4ca6
 
 
 
 
 
 
 
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
8173aa6
6ce4ca6
 
 
 
 
8173aa6
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8173aa6
6ce4ca6
 
 
3cdf7b9
 
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cdf7b9
 
 
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
 
3cdf7b9
6ce4ca6
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cdf7b9
 
 
6ce4ca6
 
 
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cdf7b9
 
 
6ce4ca6
 
 
 
 
 
 
 
 
 
3cdf7b9
 
6ce4ca6
 
 
3cdf7b9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
<script lang="ts">
	import * as Dialog from "@/components/ui/dialog";
	import { video } from "@robothub/transport-server-client";
	import type { video as videoTypes } from "@robothub/transport-server-client";
	import { Button } from "@/components/ui/button";
	import * as Card from "@/components/ui/card";
	import { Badge } from "@/components/ui/badge";
	import { toast } from "svelte-sonner";
	import { settings } from "$lib/runes/settings.svelte";
	import { videoManager } from "$lib/elements/video/VideoManager.svelte";
	import type { RemoteCompute } from "$lib/elements/compute//RemoteCompute.svelte";

	interface Props {
		workspaceId: string;
		open: boolean;
		compute: RemoteCompute;
	}

	let { open = $bindable(), compute, workspaceId }: Props = $props();

	let isConnecting = $state(false);
	let selectedCameraName = $state("front");
	let localStream: MediaStream | null = $state(null);
	let videoProducer: any = null;

	// Auto-refresh rooms when modal opens
	$effect(() => {
		if (open) {
			videoManager.refreshRooms(workspaceId);
		}
	});

	async function handleConnectLocalCamera() {
		if (!compute.hasSession) {
			toast.error("No Inference Session available. Create a session first.");
			return;
		}

		isConnecting = true;
		try {
			// Get user media
			const stream = await navigator.mediaDevices.getUserMedia({
				video: true,
				audio: false
			});
			localStream = stream;

			// Get the camera room ID for the selected camera
			const cameraRoomId = compute.sessionData?.camera_room_ids[selectedCameraName];
			if (!cameraRoomId) {
				throw new Error(`No room found for camera: ${selectedCameraName}`);
			}

			// Create video producer and connect to the camera room
			videoProducer = new video.VideoProducer(settings.transportServerUrl);

			// Connect to the EXISTING camera room (don't create new one)
			const participantId = `frontend-camera-${selectedCameraName}-${Date.now()}`;
			const success = await videoProducer.connect(workspaceId, cameraRoomId, participantId);

			if (!success) {
				throw new Error("Failed to connect to camera room");
			}

			// Start streaming
			await videoProducer.startCamera();

			toast.success(`Camera connected to Inference Session`, {
				description: `Local camera streaming to ${selectedCameraName} input`
			});
		} catch (error) {
			console.error("Camera connection error:", error);
			toast.error("Failed to connect camera", {
				description: error instanceof Error ? error.message : "Unknown error"
			});
		} finally {
			isConnecting = false;
		}
	}

	async function handleDisconnectCamera() {
		try {
			if (videoProducer) {
				await videoProducer.stopStreaming();
				await videoProducer.disconnect();
				videoProducer = null;
			}

			if (localStream) {
				localStream.getTracks().forEach((track) => track.stop());
				localStream = null;
			}

			toast.success("Camera disconnected");
		} catch (error) {
			console.error("Disconnect error:", error);
			toast.error("Error disconnecting camera");
		}
	}

	// Cleanup on modal close
	$effect(() => {
		return () => {
			if (!open) {
				handleDisconnectCamera();
			}
		};
	});
</script>

<Dialog.Root bind:open>
	<Dialog.Content
		class="max-h-[80vh] max-w-xl overflow-y-auto border-slate-600 bg-slate-900 text-slate-100"
	>
		<Dialog.Header class="pb-3">
			<Dialog.Title class="flex items-center gap-2 text-lg font-bold text-slate-100">
				<span class="icon-[mdi--video-input-component] size-5 text-green-400"></span>
				Video Input - {compute.name || "No Compute Selected"}
			</Dialog.Title>
			<Dialog.Description class="text-sm text-slate-400">
				Connect camera streams to provide visual input for AI inference
			</Dialog.Description>
		</Dialog.Header>

		<div class="space-y-4">
			<!-- Inference Session Status -->
			<div
				class="flex items-center justify-between rounded-lg border border-purple-500/30 bg-purple-900/20 p-3"
			>
				<div class="flex items-center gap-2">
					<span class="icon-[mdi--brain] size-4 text-purple-400"></span>
					<span class="text-sm font-medium text-purple-300">Inference Session</span>
				</div>
				{#if compute.hasSession}
					<Badge variant="default" class="bg-purple-600 text-xs">
						{compute.statusInfo.statusText}
					</Badge>
				{:else}
					<Badge variant="secondary" class="text-xs text-slate-400">No Session</Badge>
				{/if}
			</div>

			{#if !compute.hasSession}
				<Card.Root class="border-yellow-500/30 bg-yellow-500/5">
					<Card.Header>
						<Card.Title class="flex items-center gap-2 text-base text-yellow-200">
							<span class="icon-[mdi--alert] size-4"></span>
							Inference Session Required
						</Card.Title>
					</Card.Header>
					<Card.Content class="text-sm text-yellow-300">
						You need to create an Inference Session before connecting video inputs. The session
						defines which camera names are available for connection.
					</Card.Content>
				</Card.Root>
			{:else}
				<!-- Camera Selection and Connection -->
				<Card.Root class="border-green-500/30 bg-green-500/5">
					<Card.Header>
						<Card.Title class="flex items-center gap-2 text-base text-green-200">
							<span class="icon-[mdi--camera] size-4"></span>
							Camera Connection
						</Card.Title>
					</Card.Header>
					<Card.Content class="space-y-4">
						<!-- Available Cameras -->
						<div class="space-y-2">
							<div class="text-sm font-medium text-green-300">Available Camera Inputs:</div>
							<div class="grid grid-cols-2 gap-2">
								{#each compute.sessionConfig?.cameraNames || [] as cameraName}
									<button
										onclick={() => (selectedCameraName = cameraName)}
										class="rounded border p-2 text-left {selectedCameraName === cameraName
											? 'border-green-500 bg-green-500/20'
											: 'border-slate-600 bg-slate-800/50 hover:bg-slate-700/50'}"
									>
										<div class="text-sm font-medium">{cameraName}</div>
										<div class="text-xs text-slate-400">
											Room: {compute.sessionData?.camera_room_ids[cameraName]?.slice(-8)}
										</div>
									</button>
								{/each}
							</div>
						</div>

						<!-- Connection Status -->
						<div class="rounded-lg border border-green-500/30 bg-green-900/20 p-3">
							<div class="flex items-center justify-between">
								<div>
									<p class="text-sm font-medium text-green-300">
										Selected Camera: {selectedCameraName}
									</p>
									<p class="text-xs text-green-400/70">
										{localStream ? "Connected" : "Not Connected"}
									</p>
								</div>
								{#if !localStream}
									<Button
										variant="default"
										size="sm"
										onclick={handleConnectLocalCamera}
										disabled={isConnecting}
										class="bg-green-600 text-xs hover:bg-green-700 disabled:opacity-50"
									>
										{#if isConnecting}
											<span class="icon-[mdi--loading] mr-1 size-3 animate-spin"></span>
											Connecting...
										{:else}
											<span class="icon-[mdi--camera] mr-1 size-3"></span>
											Connect Camera
										{/if}
									</Button>
								{:else}
									<Button
										variant="destructive"
										size="sm"
										onclick={handleDisconnectCamera}
										class="text-xs"
									>
										<span class="icon-[mdi--close-circle] mr-1 size-3"></span>
										Disconnect
									</Button>
								{/if}
							</div>
						</div>

						<!-- Live Preview -->
						{#if localStream}
							<div class="space-y-2">
								<div class="text-sm font-medium text-green-300">Live Preview:</div>
								<div
									class="aspect-video overflow-hidden rounded border border-green-500/30 bg-black/50"
								>
									<video
										autoplay
										muted
										playsinline
										class="h-full w-full object-cover"
										onloadedmetadata={(e) => {
											const video = e.target as HTMLVideoElement;
											video.srcObject = localStream;
										}}
									></video>
								</div>
							</div>
						{/if}
					</Card.Content>
				</Card.Root>

				<!-- Session Camera Details -->
				<Card.Root class="border-blue-500/30 bg-blue-500/5">
					<Card.Header>
						<Card.Title class="flex items-center gap-2 text-base text-blue-200">
							<span class="icon-[mdi--information] size-4"></span>
							Session Camera Details
						</Card.Title>
					</Card.Header>
					<Card.Content>
						<div class="space-y-2 text-xs">
							{#each Object.entries(compute.sessionData?.camera_room_ids || {}) as [camera, roomId]}
								<div class="flex items-center justify-between rounded bg-slate-800/50 p-2">
									<span class="font-medium text-blue-300">{camera}</span>
									<span class="font-mono text-blue-200">{roomId}</span>
								</div>
							{/each}
						</div>
					</Card.Content>
				</Card.Root>
			{/if}

			<!-- Quick Info -->
			<div class="rounded border border-slate-700 bg-slate-800/30 p-2 text-xs text-slate-500">
				<span class="icon-[mdi--information] mr-1 size-3"></span>
				Video inputs stream camera data to the AI model for visual processing. Each camera connects to
				a dedicated room in the session.
			</div>
		</div>
	</Dialog.Content>
</Dialog.Root>