Octane gRPC API Skill
Expert knowledge for working with Octane's gRPC LiveLink API in octaneWebR.
Proto Files Location
All proto definitions live in:
code
server/proto/ ├── apinodesystem_3.proto # Node operations (create, delete, connect) ├── apiscenegraph_3.proto # Scene graph queries ├── apirendertarget_3.proto # Viewport/render operations ├── apimaterialdatabase_3.proto # LiveDB/LocalDB └── ...
gRPC Call Pattern
1. Find the Proto Method
bash
# Search for available methods grep -r "rpc MethodName" server/proto/ # Example: Find node creation grep -r "rpc Create" server/proto/apinodesystem_3.proto
2. Implement in Service
typescript
// services/octane/NodeService.ts
import { BaseService } from './BaseService';
export class NodeService extends BaseService {
async createNode(nodeType: string): Promise<number> {
try {
// Make gRPC call via fetch (proxy handles gRPC-Web conversion)
const response = await fetch(`${this.serverUrl}/api/node/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nodeType })
});
if (!response.ok) {
throw new Error(`Failed to create node: ${response.statusText}`);
}
const data = await response.json();
// Emit event for UI synchronization
this.emit('node:created', {
handle: data.handle,
type: nodeType
});
return data.handle;
} catch (error) {
console.error('[NodeService] createNode failed:', error);
throw error;
}
}
}
3. Expose in OctaneClient
typescript
// services/OctaneClient.ts
export class OctaneClient extends EventEmitter {
private _nodeService: NodeService;
constructor() {
super();
this._nodeService = new NodeService(this);
}
// Public API getter
public get node() {
return {
create: (type: string) => this._nodeService.createNode(type),
delete: (handle: number) => this._nodeService.deleteNode(handle),
connect: (parent: number, pin: number, child: number) =>
this._nodeService.connectPinByIndex(parent, pin, child)
};
}
}
4. Use in Component
typescript
// components/NodeGraph/index.tsx
import { useOctane } from '../../hooks/useOctane';
const NodeGraph: React.FC = () => {
const { client, connected } = useOctane();
const handleCreateNode = async (type: string) => {
if (!connected) return;
try {
const handle = await client.node.create(type);
console.log(`Created node: ${handle}`);
} catch (error) {
console.error('Failed to create node:', error);
}
};
return <button onClick={() => handleCreateNode('NT_DIFFUSE_MAT')}>
Create Material
</button>;
};
Common gRPC Operations
Node Operations
typescript
// Create node
const handle = await client.node.create('NT_CAMERA');
// Delete node
await client.node.delete(handle);
// Connect nodes
await client.node.connect(parentHandle, pinIndex, childHandle);
// Get node info
const info = await client.node.getInfo(handle);
// Get node parameters
const params = await client.node.getParameters(handle);
// Set parameter value
await client.node.setParameter(handle, 'paramName', value);
Scene Operations
typescript
// Get scene tree const tree = await client.scene.getTree(); // Refresh scene await client.scene.refresh(); // Get render target const renderTarget = await client.scene.getRenderTarget();
Viewport Operations
typescript
// Start interactive render await client.viewport.startRender(); // Stop render await client.viewport.stopRender(); // Get render statistics const stats = await client.viewport.getStats();
Service Layer Architecture
All services follow this pattern:
typescript
export class MyService extends BaseService {
// BaseService provides:
// - this.client: OctaneClient reference
// - this.serverUrl: API endpoint
// - this.emit(): Event emitter
async myMethod(param: Type): Promise<ResultType> {
try {
// 1. Validate input
if (!param) throw new Error('Param required');
// 2. Make gRPC call
const response = await fetch(`${this.serverUrl}/api/endpoint`, {
method: 'POST',
body: JSON.stringify({ param })
});
if (!response.ok) throw new Error(response.statusText);
const data = await response.json();
// 3. Emit event for UI updates
this.emit('event:name', { result: data });
// 4. Return result
return data;
} catch (error) {
console.error('[MyService] myMethod failed:', error);
throw error;
}
}
}
Event-Driven Communication
Services emit events for UI synchronization:
typescript
// In service
this.emit('node:created', { handle, type });
this.emit('node:deleted', { handle });
this.emit('node:replaced', { oldHandle, newHandle });
this.emit('scene:updated', { tree });
this.emit('render:started', {});
typescript
// In component
useEffect(() => {
const handleNodeCreated = (data: { handle: number, type: string }) => {
console.log('Node created:', data);
// Update UI state
};
client.on('node:created', handleNodeCreated);
return () => client.off('node:created', handleNodeCreated);
}, [client]);
Common Gotchas
1. Node Handles are Numbers
typescript
// ❌ Wrong
const handle: string = await client.node.create('NT_CAMERA');
// ✅ Correct
const handle: number = await client.node.create('NT_CAMERA');
2. Check Connection State
typescript
// ❌ Wrong - will fail if disconnected
await client.node.create('NT_CAMERA');
// ✅ Correct
if (!connected) {
console.warn('Not connected to Octane');
return;
}
await client.node.create('NT_CAMERA');
3. Pin Indices are 0-based
typescript
// Pins: [0: Diffuse, 1: Specular, 2: Roughness] await client.node.connect(materialHandle, 0, diffuseTextureHandle); // ✅ await client.node.connect(materialHandle, 1, diffuseTextureHandle); // ❌ Wrong pin!
4. Handle API Errors
typescript
// ❌ Wrong - error swallowed
const handle = await client.node.create('NT_INVALID_TYPE');
// ✅ Correct
try {
const handle = await client.node.create('NT_INVALID_TYPE');
} catch (error) {
console.error('Failed to create node:', error);
// Show user-friendly error
}
Proxy Server Pattern
The Vite plugin acts as a gRPC-Web proxy:
typescript
// vite-plugin-octane-grpc.ts
export function octaneGrpcPlugin(): Plugin {
return {
name: 'octane-grpc',
configureServer(server) {
server.middlewares.use('/api', async (req, res) => {
// Convert HTTP request to gRPC call
const grpcClient = new OctaneClient('host.docker.internal:51022');
const result = await grpcClient.callMethod(req.body);
res.json(result);
});
}
};
}
Client makes standard HTTP calls:
typescript
fetch('/api/node/create', { method: 'POST', body: JSON.stringify({ type }) })
Proxy converts to gRPC and calls Octane.
Debugging gRPC Calls
1. Check Server Logs
bash
# Look for gRPC connection status [Octane Client] Connected to Octane at host.docker.internal:51022 # Look for method calls [NodeService] Creating node: NT_CAMERA [NodeService] Node created: 42
2. Check Network Tab
- •Open browser DevTools → Network
- •Filter for
/api/ - •Check request payload and response
- •Look for 4xx/5xx errors
3. Check Proto Definition
bash
# Find the exact method signature grep -A 5 "rpc Create" server/proto/apinodesystem_3.proto
4. Test with cURL
bash
# Test endpoint directly
curl -X POST http://localhost:57341/api/node/create \
-H "Content-Type: application/json" \
-d '{"nodeType": "NT_CAMERA"}'
Recent Discoveries
Node Replacement Pattern (Jan 2025)
When replacing a node while maintaining connections:
typescript
async replaceNode(oldHandle: number, newType: string): Promise<number> {
// 1. Get parent connections FIRST (before deleting)
const parents = await this.getNodeParents(oldHandle);
// 2. Create new node
const newHandle = await this.createNode(newType);
// 3. Reconnect to parents
for (const { parentHandle, pinIndex } of parents) {
await this.connectPinByIndex(parentHandle, pinIndex, newHandle);
}
// 4. Delete old node LAST
await this.deleteNode(oldHandle);
return newHandle;
}
Key insight: Get parent connections before deletion, or they're lost!
When to Update This Skill
Add new knowledge when you:
- •Discover a new proto method
- •Find a clever gRPC pattern
- •Debug a tricky gRPC issue
- •Learn about undocumented API behavior
- •Create a useful helper for gRPC calls