Skip to main content

Server Controlled Example

Production-ready example with backend API control and frontend display.

Recommended for Production

This architecture keeps your Kaltura Session secure and provides better separation of concerns.

Architecture

Frontend (React)  ←→  Backend (Express)  ←→  Kaltura API
Display only Control + Security Sessions

Backend (Express + Node.js)

server.js

import express from 'express';
import fetch from 'node-fetch';
import cors from 'cors';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
app.use(cors());
app.use(express.json());

const KS = process.env.AVATAR_KS;
const BASE_URL = process.env.AVATAR_BASE_URL || 'https://api.avatar.us.kaltura.ai/v1/avatar-session';

// Session storage (use Redis/database in production)
const sessions = new Map();

// Create session
app.post('/api/avatar/create-session', async (req, res) => {
try {
const { avatarId, voiceId } = req.body;

const response = await fetch(BASE_URL, {
method: 'POST',
headers: {
Authorization: `KS ${KS}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
visualConfig: { avatarId },
voiceConfig: voiceId ? { id: voiceId } : undefined,
}),
});

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

const data = await response.json();

sessions.set(data.sessionId, {
token: data.token,
createdAt: new Date(),
avatarId,
voiceId,
});

res.json({
sessionId: data.sessionId,
token: data.token,
});
} catch (error) {
console.error('Create session error:', error);
res.status(500).json({ error: 'Failed to create session' });
}
});

// Say text
app.post('/api/avatar/say-text', async (req, res) => {
try {
const { sessionId, token, text } = req.body;

if (!sessions.has(sessionId)) {
return res.status(404).json({ error: 'Session not found' });
}

if (!text || text.trim().length === 0) {
return res.status(400).json({ error: 'Text is required' });
}

const response = await fetch(`${BASE_URL}/${sessionId}/say-text`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: text.trim() }),
});

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

const data = await response.json();
res.json(data);
} catch (error) {
console.error('Say text error:', error);
res.status(500).json({ error: 'Failed to say text' });
}
});

// Interrupt
app.post('/api/avatar/interrupt', async (req, res) => {
try {
const { sessionId, token } = req.body;

if (!sessions.has(sessionId)) {
return res.status(404).json({ error: 'Session not found' });
}

const response = await fetch(`${BASE_URL}/${sessionId}/interrupt`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

const data = await response.json();
res.json(data);
} catch (error) {
console.error('Interrupt error:', error);
res.status(500).json({ error: 'Failed to interrupt' });
}
});

// End session
app.post('/api/avatar/end-session', async (req, res) => {
try {
const { sessionId, token } = req.body;

if (!sessions.has(sessionId)) {
return res.status(404).json({ error: 'Session not found' });
}

const response = await fetch(`${BASE_URL}/${sessionId}/end`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
});

if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}

sessions.delete(sessionId);

const data = await response.json();
res.json(data);
} catch (error) {
console.error('End session error:', error);
res.status(500).json({ error: 'Failed to end session' });
}
});

// Health check
app.get('/health', (req, res) => {
res.json({
status: 'ok',
activeSessions: sessions.size,
});
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Backend server running on port ${PORT}`);
});

.env

AVATAR_KS=your-api-key-here
AVATAR_BASE_URL=https://api.avatar.us.kaltura.ai/v1/avatar-session
PORT=3001

Frontend (React)

AvatarDemo.jsx

import { useState, useEffect, useRef } from 'react';
import { KalturaAvatarSession } from '@unisphere/models-sdk-js';

const BACKEND_URL = 'http://localhost:3001';
const AVATAR_BASE_URL = 'https://api.avatar.us.kaltura.ai/v1/avatar-session';

export default function AvatarDemo() {
const [session, setSession] = useState(null);
const [sessionId, setSessionId] = useState(null);
const [token, setToken] = useState(null);
const [status, setStatus] = useState('idle');
const [text, setText] = useState('Hello! How can I help you today?');
const [error, setError] = useState(null);

const containerRef = useRef(null);

const createSession = async () => {
try {
setStatus('creating');
setError(null);

// Call backend to create session
const response = await fetch(`${BACKEND_URL}/api/avatar/create-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
avatarId: 'avatar-123',
voiceId: 'voice-456',
}),
});

if (!response.ok) {
throw new Error('Failed to create session');
}

const data = await response.json();
setSessionId(data.sessionId);
setToken(data.token);

// Use Client SDK for display
const avatarSession = new KalturaAvatarSession(data.token, {
baseUrl: AVATAR_BASE_URL,
});

avatarSession.on('stateChange', (state) => {
console.log('State:', state);
if (state === 'READY') {
setStatus('ready');
}
});

avatarSession.on('error', (err) => {
console.error('Avatar error:', err);
setError(err.message);
setStatus('error');
});

// Create session and attach
await avatarSession.createSession({
avatarId: 'avatar-123',
videoContainerId: 'avatar-container',
});

setSession(avatarSession);

// Welcome message
await sayText('Hello! I am your avatar assistant.');
} catch (err) {
console.error('Create session error:', err);
setError(err.message);
setStatus('error');
}
};

const sayText = async (textToSay) => {
try {
// Call backend (not SDK)
await fetch(`${BACKEND_URL}/api/avatar/say-text`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId,
token,
text: textToSay || text,
}),
});
} catch (err) {
console.error('Say text error:', err);
setError(err.message);
}
};

const interrupt = async () => {
try {
await fetch(`${BACKEND_URL}/api/avatar/interrupt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, token }),
});
} catch (err) {
console.error('Interrupt error:', err);
setError(err.message);
}
};

const endSession = async () => {
try {
await fetch(`${BACKEND_URL}/api/avatar/end-session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId, token }),
});

if (session) {
await session.endSession();
}

setSession(null);
setSessionId(null);
setToken(null);
setStatus('idle');
} catch (err) {
console.error('End session error:', err);
setError(err.message);
}
};

// Cleanup on unmount
useEffect(() => {
return () => {
if (session) {
session.endSession();
}
};
}, [session]);

return (
<div style={{ maxWidth: '800px', margin: '50px auto', padding: '20px' }}>
<h1>Avatar Demo - Server Controlled</h1>

<div
style={{
padding: '12px',
borderRadius: '6px',
marginBottom: '20px',
background: status === 'ready' ? '#d4edda' : '#e9ecef',
}}
>
Status: {status}
{sessionId && ` (Session: ${sessionId})`}
</div>

{error && (
<div
style={{
padding: '12px',
borderRadius: '6px',
marginBottom: '20px',
background: '#f8d7da',
color: '#721c24',
}}
>
Error: {error}
</div>
)}

<div
id="avatar-container"
ref={containerRef}
style={{
width: '512px',
height: '512px',
border: '2px solid #333',
borderRadius: '12px',
overflow: 'hidden',
background: '#000',
margin: '20px 0',
}}
/>

<div style={{ marginBottom: '20px' }}>
<button
onClick={createSession}
disabled={status !== 'idle'}
style={{
padding: '12px 24px',
marginRight: '10px',
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: status === 'idle' ? 'pointer' : 'not-allowed',
}}
>
Start Avatar
</button>

<button
onClick={endSession}
disabled={status !== 'ready'}
style={{
padding: '12px 24px',
background: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: status === 'ready' ? 'pointer' : 'not-allowed',
}}
>
End Session
</button>
</div>

<textarea
value={text}
onChange={(e) => setText(e.target.value)}
style={{
width: '100%',
height: '100px',
padding: '10px',
marginBottom: '10px',
fontSize: '14px',
borderRadius: '6px',
}}
/>

<div>
<button
onClick={() => sayText()}
disabled={status !== 'ready'}
style={{
padding: '12px 24px',
marginRight: '10px',
background: '#007bff',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: status === 'ready' ? 'pointer' : 'not-allowed',
}}
>
Make Avatar Speak
</button>

<button
onClick={interrupt}
disabled={status !== 'ready'}
style={{
padding: '12px 24px',
background: '#ffc107',
color: 'black',
border: 'none',
borderRadius: '6px',
cursor: status === 'ready' ? 'pointer' : 'not-allowed',
}}
>
Interrupt
</button>
</div>
</div>
);
}

Running the Example

1. Backend

cd backend
npm install express node-fetch cors dotenv
node server.js

2. Frontend

cd frontend
npm install @unisphere/models-sdk-js
npm run dev

Key Benefits

Security: Kaltura Session stays on server ✅ Separation: Backend controls, frontend displays ✅ Validation: Backend validates all requests ✅ Audit: Backend can log all actions ✅ Scalable: Can manage multiple sessions

Next Steps