Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/backend/setup.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"hw": "jetson",
"hw": "rk3588",
"ceracoder_path": "./mocks/ceracoder/",
"ceracoder_config": "/tmp/ceracoder.conf",
"srtla_path": "./mocks/srtla/",
Expand Down
20 changes: 19 additions & 1 deletion apps/backend/src/modules/streaming/pipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,13 @@ export function searchPipelines(id: string): Pipeline | null {
// Pipeline list in the format needed by the frontend
type PipelineResponseEntry = Pick<
Pipeline,
"name" | "description" | "supportsAudio" | "supportsResolutionOverride" | "supportsFramerateOverride"
| "name"
| "description"
| "supportsAudio"
| "supportsResolutionOverride"
| "supportsFramerateOverride"
| "defaultResolution"
| "defaultFramerate"
>;

export function getPipelineList() {
Expand All @@ -135,11 +141,23 @@ export function getPipelineList() {
supportsAudio: pipeline.supportsAudio,
supportsResolutionOverride: pipeline.supportsResolutionOverride,
supportsFramerateOverride: pipeline.supportsFramerateOverride,
defaultResolution: pipeline.defaultResolution,
defaultFramerate: pipeline.defaultFramerate,
};
}
return list;
}

/**
* Get pipelines message with hardware info (for WebSocket broadcast)
*/
export function getPipelinesMessage() {
return {
hardware: getEffectiveHardware(),
pipelines: getPipelineList(),
};
}

/**
* Generate a pipeline file with overrides
*/
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/modules/ui/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { netIfBuildMsg } from "../network/network-interfaces.ts";
import { buildRelaysMsg, getRelays } from "../remote/remote-relays.ts";
import { getAudioDevices } from "../streaming/audio.ts";
import { AUDIO_CODECS } from "@ceralive/ceracoder";
import { getPipelineList } from "../streaming/pipelines.ts";
import { getPipelinesMessage } from "../streaming/pipelines.ts";
import { getIsStreaming } from "../streaming/streaming.ts";
import { getRevisions } from "../system/revisions.ts";
import { getSensors } from "../system/sensors.ts";
Expand Down Expand Up @@ -65,7 +65,7 @@ export function sendStatus(conn: WebSocket) {
export function sendInitialStatus(conn: WebSocket) {
const config = getConfig();
conn.send(buildMsg("config", config));
conn.send(buildMsg("pipelines", getPipelineList()));
conn.send(buildMsg("pipelines", getPipelinesMessage()));
if (getRelays()) conn.send(buildMsg("relays", buildRelaysMsg()));
sendStatus(conn);
conn.send(buildMsg("netif", netIfBuildMsg()));
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/rpc/procedures/status.procedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "../../modules/remote/remote-relays.ts";
import { getAudioDevices } from "../../modules/streaming/audio.ts";
import { AUDIO_CODECS } from "@ceralive/ceracoder";
import { getPipelineList } from "../../modules/streaming/pipelines.ts";
import { getPipelinesMessage } from "../../modules/streaming/pipelines.ts";
import { getIsStreaming } from "../../modules/streaming/streaming.ts";
import { getRevisions } from "../../modules/system/revisions.ts";
import { getSensors } from "../../modules/system/sensors.ts";
Expand Down Expand Up @@ -71,7 +71,7 @@ export function buildInitialStatus() {
const config = getConfig();
return {
config,
pipelines: getPipelineList(),
pipelines: getPipelinesMessage(),
relays: getRelays() ? buildRelaysMsg() : null,
status: {
is_streaming: getIsStreaming(),
Expand Down
14 changes: 9 additions & 5 deletions apps/backend/src/rpc/procedures/streaming.procedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
bitrateOutputSchema,
configMessageSchema,
getMockHardwareOutputSchema,
pipelinesSchema,
pipelinesMessageSchema,
setMockHardwareInputSchema,
setMockHardwareOutputSchema,
streamingConfigInputSchema,
Expand All @@ -25,6 +25,7 @@ import {
getEffectiveHardware,
getMockHardware,
getPipelineList,
getPipelinesMessage,
initPipelines,
setMockHardware,
VALID_HARDWARE_TYPES,
Expand Down Expand Up @@ -88,12 +89,15 @@ export const setBitrateProcedure = authedProcedure
});

/**
* Get pipelines procedure
* Get pipelines procedure - returns pipelines with hardware info
*/
export const getPipelinesProcedure = authedProcedure
.output(pipelinesSchema)
.output(pipelinesMessageSchema)
.handler(() => {
return getPipelineList();
return {
hardware: getEffectiveHardware(),
pipelines: getPipelineList(),
};
});

/**
Expand Down Expand Up @@ -149,7 +153,7 @@ export const setMockHardwareProcedure = authedProcedure
if (success) {
// Reload pipelines and broadcast to all clients
initPipelines();
broadcastMsg("pipelines", getPipelineList());
broadcastMsg("pipelines", getPipelinesMessage());
return {
success: true,
hardware: input.hardware,
Expand Down
15 changes: 15 additions & 0 deletions apps/frontend/docs/DEVTOOLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ The DevTools tab displays real-time information about the current development en
- Test toast queue management
- **Usage**: Click various buttons to trigger different toast notifications

#### 🔧 Mock Hardware Switcher
- **Purpose**: Switch between hardware profiles to test different pipeline configurations
- **Features**:
- **Hardware Profiles**: Switch between Jetson, RK3588, N100, and Generic (software)
- **Live Reload**: Pipelines update immediately and broadcast to all connected clients
- **Quick Switch Buttons**: Color-coded buttons for rapid hardware switching
- **Dropdown Selector**: Detailed view with hardware descriptions
- **State Display**: Shows effective hardware and current mock override
- **Hardware Types**:
- 🟢 **NVIDIA Jetson**: NVIDIA nvenc hardware encoding
- 🟠 **Rockchip RK3588**: Rockchip MPP hardware encoding (supports 4K)
- 🔵 **Intel N100**: Intel VAAPI hardware encoding
- ⚪ **Generic**: Software x264/x265 encoding
- **Usage**: Select a hardware profile to reload pipelines for that platform. The encoder settings will update to show available video sources for the selected hardware.

### 📊 System Information

Real-time system and browser information display:
Expand Down
106 changes: 47 additions & 59 deletions apps/frontend/src/lib/components/dev-tools/hardware-switcher.svelte
Original file line number Diff line number Diff line change
@@ -1,51 +1,55 @@
<script lang="ts">
import { Cpu, Loader2, RefreshCw } from '@lucide/svelte';
import { toast } from 'svelte-sonner';
import {
type HardwareType,
HARDWARE_LABELS,
HARDWARE_DESCRIPTIONS,
HARDWARE_COLORS,
hardwareTypeSchema,
} from '@ceraui/rpc/schemas';

import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import { rpc } from '$lib/rpc';
import { rpc, getIsConnected } from '$lib/rpc';

type HardwareType = 'jetson' | 'n100' | 'rk3588';
// All available hardware types from schema
const ALL_HARDWARE_TYPES = hardwareTypeSchema.options as HardwareType[];

// State
let selectedHardware = $state<HardwareType | null>(null);
let effectiveHardware = $state<string>('unknown');
let availableHardware = $state<HardwareType[]>(['jetson', 'n100', 'rk3588']);
let effectiveHardware = $state<string>('loading...');
let availableHardware = $state<HardwareType[]>(ALL_HARDWARE_TYPES);
let isLoading = $state(false);
let isInitialized = $state(false);

// Hardware type display info
const hardwareInfo: Record<HardwareType, { name: string; description: string; color: string }> = {
jetson: {
name: 'NVIDIA Jetson',
description: 'NVIDIA nvenc hardware encoding',
color: 'text-green-600 dark:text-green-400',
},
n100: {
name: 'Intel N100',
description: 'Intel VAAPI hardware encoding',
color: 'text-blue-600 dark:text-blue-400',
},
rk3588: {
name: 'Rockchip RK3588',
description: 'Rockchip MPP hardware encoding (supports 4K)',
color: 'text-orange-600 dark:text-orange-400',
},
};
let loadError = $state<string | null>(null);

// Load current hardware state on mount
async function loadHardwareState() {
isLoading = true;
loadError = null;
try {
// Wait for connection if not connected
const isConnected = getIsConnected();
if (!isConnected) {
effectiveHardware = 'connecting...';
// Retry after a short delay
await new Promise(resolve => setTimeout(resolve, 1000));
}

const state = await rpc.streaming.getMockHardware();
selectedHardware = state.hardware;
effectiveHardware = state.effectiveHardware;
availableHardware = state.availableHardware;
isInitialized = true;
} catch (_error) {
toast.error('Failed to load hardware state');
} catch (error) {
loadError = error instanceof Error ? error.message : 'Failed to load';
effectiveHardware = 'error';
console.error('Failed to load hardware state:', error);
} finally {
isLoading = false;
}
}

Expand All @@ -59,7 +63,7 @@ async function switchHardware(hardware: HardwareType) {
if (result.success) {
selectedHardware = result.hardware ?? null;
effectiveHardware = hardware;
toast.success(`Switched to ${hardwareInfo[hardware].name}`, {
toast.success(`Switched to ${HARDWARE_LABELS[hardware]}`, {
description: 'Pipelines reloaded and broadcast to all clients',
});
} else {
Expand Down Expand Up @@ -107,13 +111,21 @@ $effect(() => {
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-muted-foreground">Effective Hardware:</span>
<span class="ml-2 font-mono font-medium">{effectiveHardware}</span>
<span class="ml-2 font-mono font-medium" class:text-yellow-500={effectiveHardware === 'loading...' || effectiveHardware === 'connecting...'} class:text-red-500={effectiveHardware === 'error'}>
{#if isLoading && !isInitialized}
<Loader2 class="inline h-3 w-3 animate-spin mr-1" />
{/if}
{effectiveHardware}
</span>
</div>
<div>
<span class="text-muted-foreground">Mock Override:</span>
<span class="ml-2 font-mono font-medium">{selectedHardware ?? 'None'}</span>
</div>
</div>
{#if loadError}
<div class="mt-2 text-xs text-red-500">{loadError}</div>
{/if}
</div>

<!-- Hardware Selector -->
Expand All @@ -131,16 +143,8 @@ $effect(() => {
Switching...
{:else if selectedHardware}
<div class="flex items-center gap-2">
<div
class={`h-2 w-2 rounded-full ${
selectedHardware === 'jetson'
? 'bg-green-500'
: selectedHardware === 'n100'
? 'bg-blue-500'
: 'bg-orange-500'
}`}
></div>
{hardwareInfo[selectedHardware].name}
<div class={`h-2 w-2 rounded-full ${HARDWARE_COLORS[selectedHardware].bg}`}></div>
{HARDWARE_LABELS[selectedHardware]}
</div>
{:else}
Select hardware...
Expand All @@ -151,18 +155,10 @@ $effect(() => {
{#each availableHardware as hw}
<Select.Item value={hw}>
<div class="flex items-center gap-2">
<div
class={`h-2 w-2 rounded-full ${
hw === 'jetson'
? 'bg-green-500'
: hw === 'n100'
? 'bg-blue-500'
: 'bg-orange-500'
}`}
></div>
<div class={`h-2 w-2 rounded-full ${HARDWARE_COLORS[hw].bg}`}></div>
<div>
<div class="font-medium">{hardwareInfo[hw].name}</div>
<div class="text-muted-foreground text-xs">{hardwareInfo[hw].description}</div>
<div class="font-medium">{HARDWARE_LABELS[hw]}</div>
<div class="text-muted-foreground text-xs">{HARDWARE_DESCRIPTIONS[hw]}</div>
</div>
</div>
</Select.Item>
Expand All @@ -175,18 +171,10 @@ $effect(() => {
<!-- Quick Switch Buttons -->
<div class="space-y-2">
<Label class="text-sm font-medium">Quick Switch</Label>
<div class="grid grid-cols-3 gap-2">
<div class="grid grid-cols-2 gap-2">
{#each availableHardware as hw}
<Button
class={`${
selectedHardware === hw
? hw === 'jetson'
? 'border-green-500 bg-green-500/20'
: hw === 'n100'
? 'border-blue-500 bg-blue-500/20'
: 'border-orange-500 bg-orange-500/20'
: ''
}`}
class={selectedHardware === hw ? HARDWARE_COLORS[hw].border : ''}
disabled={isLoading}
onclick={() => switchHardware(hw)}
size="sm"
Expand All @@ -195,7 +183,7 @@ $effect(() => {
{#if isLoading && selectedHardware === hw}
<Loader2 class="mr-1 h-3 w-3 animate-spin" />
{/if}
<span class={hardwareInfo[hw].color}>{hardwareInfo[hw].name.split(' ')[0]}</span>
<span class={HARDWARE_COLORS[hw].text}>{HARDWARE_LABELS[hw].split(' ')[0]}</span>
</Button>
{/each}
</div>
Expand Down
Loading