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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ dist/
# IDE
.idea/
.vscode/
.cursor/hooks/
*.swp
*.swo

Expand Down
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,36 +71,41 @@ npm install
## How It Works

1. **Client** calls your server endpoint with the `avatarId`
2. **Server** uses your Runway API secret to create a session via `@runwayml/sdk`
3. **Server** returns connection credentials (token, URL) to the client
2. **Server** uses your Runway API secret to create a realtime session via `@runwayml/sdk`
3. **Server** polls until the session is ready, then returns `sessionId` and `sessionKey` to the client
4. **Client** establishes a WebRTC connection for real-time video/audio

This flow keeps your API secret secure on the server while enabling low-latency communication.

## Server Setup

Your server endpoint receives the `avatarId` and returns session credentials. Use `@runwayml/sdk` to create the session:
Your server endpoint receives the `avatarId` and returns session credentials. Use `@runwayml/sdk` to create and poll the session:

```ts
// /api/avatar/connect (Next.js App Router example)
import Runway from '@runwayml/sdk';

const runway = new Runway(); // Uses RUNWAYML_API_SECRET env var
const client = new Runway(); // Uses RUNWAYML_API_SECRET env var

export async function POST(req: Request) {
const { avatarId } = await req.json();

const session = await runway.realtime.sessions.create({
const { id: sessionId } = await client.realtimeSessions.create({
model: 'gwm1_avatars',
options: { avatar: avatarId },
avatar: { type: 'runway-preset', presetId: avatarId },
});

return Response.json({
sessionId: session.id,
serverUrl: session.url,
token: session.token,
roomName: session.room_name,
});
// Poll until the session is ready
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
const session = await client.realtimeSessions.retrieve(sessionId);
if (session.status === 'READY') {
return Response.json({ sessionId, sessionKey: session.sessionKey });
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
}

return Response.json({ error: 'Session creation timed out' }, { status: 504 });
}
```

Expand Down
2 changes: 1 addition & 1 deletion examples/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@runwayml/avatars-react": "latest",
"@runwayml/sdk": "^1.0.0",
"@runwayml/sdk": "^3.15.0",
"express": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
Expand Down
72 changes: 0 additions & 72 deletions examples/express/runway-realtime.ts

This file was deleted.

27 changes: 21 additions & 6 deletions examples/express/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import express from 'express';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import Runway from '@runwayml/sdk';
import { RunwayRealtime } from './runway-realtime';

const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();

const client = new Runway({ apiKey: process.env.RUNWAYML_API_SECRET });
const realtime = new RunwayRealtime(client);

app.use(express.json());
app.use(express.static(join(__dirname, 'dist')));
Expand All @@ -18,17 +16,34 @@ app.post('/api/avatar/connect', async (req, res) => {
const { avatarId, customAvatarId } = req.body;

const avatar = customAvatarId
? { type: 'custom' as const, customId: customAvatarId }
? { type: 'custom' as const, avatarId: customAvatarId }
: { type: 'runway-preset' as const, presetId: avatarId };

const { id: sessionId } = await realtime.create({
const { id: sessionId } = await client.realtimeSessions.create({
model: 'gwm1_avatars',
avatar,
});

const { sessionKey } = await realtime.waitForReady(sessionId);
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 1_000;
const deadline = Date.now() + TIMEOUT_MS;

res.json({ sessionId, sessionKey });
while (Date.now() < deadline) {
const session = await client.realtimeSessions.retrieve(sessionId);

if (session.status === 'READY') {
res.json({ sessionId, sessionKey: session.sessionKey });
return;
}

if (session.status === 'COMPLETED' || session.status === 'FAILED' || session.status === 'CANCELLED') {
throw new Error(`Session ${session.status.toLowerCase()} before becoming ready`);
}

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}

throw new Error('Session creation timed out');
} catch (error) {
console.error('Failed to create avatar session:', error);
res.status(500).json({ error: 'Failed to create avatar session' });
Expand Down
28 changes: 21 additions & 7 deletions examples/nextjs-server-actions/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Runway from '@runwayml/sdk';
import { RunwayRealtime } from '../runway-realtime';
import { AvatarPicker } from './avatar-picker';

const client = new Runway({ apiKey: process.env.RUNWAYML_API_SECRET });
const realtime = new RunwayRealtime(client);

async function createAvatarSession(
avatarId: string,
Expand All @@ -12,17 +10,33 @@ async function createAvatarSession(
'use server';

const avatar = options?.isCustom
? { type: 'custom' as const, customId: avatarId }
? { type: 'custom' as const, avatarId }
: { type: 'runway-preset' as const, presetId: avatarId };

const { id: sessionId } = await realtime.create({
const { id: sessionId } = await client.realtimeSessions.create({
model: 'gwm1_avatars',
avatar,
avatar: avatar as Runway.RealtimeSessionCreateParams['avatar'],
});

const { sessionKey } = await realtime.waitForReady(sessionId);
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 1_000;
const deadline = Date.now() + TIMEOUT_MS;

return { sessionId, sessionKey };
while (Date.now() < deadline) {
const session = await client.realtimeSessions.retrieve(sessionId);

if (session.status === 'READY') {
return { sessionId, sessionKey: session.sessionKey };
}

if (session.status === 'COMPLETED' || session.status === 'FAILED' || session.status === 'CANCELLED') {
throw new Error(`Session ${session.status.toLowerCase()} before becoming ready`);
}

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}

throw new Error('Session creation timed out');
}

export default function Page() {
Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs-server-actions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"@runwayml/avatars-react": "latest",
"@runwayml/sdk": "^1.0.0",
"@runwayml/sdk": "^3.15.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
Expand Down
72 changes: 0 additions & 72 deletions examples/nextjs-server-actions/runway-realtime.ts

This file was deleted.

34 changes: 24 additions & 10 deletions examples/nextjs-simple/app/api/avatar/connect/route.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import Runway from '@runwayml/sdk';
import { RunwayRealtime } from '../../../../runway-realtime';

const client = new Runway({ apiKey: process.env.RUNWAYML_API_SECRET });
const realtime = new RunwayRealtime(client);

export async function POST(req: Request) {
const { avatarId } = await req.json();

const avatar = { type: 'runway-preset' as const, presetId: avatarId }
// If you want to use a custom avatar, you can use the following code:
// const avatar = { type: 'custom' as const, avatarId: avatarId }

const { id: sessionId } = await realtime.create({
const { id: sessionId } = await client.realtimeSessions.create({
model: 'gwm1_avatars',
avatar,
avatar: { type: 'runway-preset', presetId: avatarId },
});

const { sessionKey } = await realtime.waitForReady(sessionId);
const session = await pollSessionUntilReady(sessionId);

return Response.json({ sessionId, sessionKey: session.sessionKey });
}

async function pollSessionUntilReady(sessionId: string) {
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 1_000;
const deadline = Date.now() + TIMEOUT_MS;

while (Date.now() < deadline) {
const session = await client.realtimeSessions.retrieve(sessionId);

if (session.status === 'READY') return session;

if (session.status === 'COMPLETED' || session.status === 'FAILED' || session.status === 'CANCELLED') {
throw new Error(`Session ${session.status.toLowerCase()} before becoming ready`);
}

await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}

return Response.json({ sessionId, sessionKey });
throw new Error('Session creation timed out');
}
2 changes: 1 addition & 1 deletion examples/nextjs-simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"dependencies": {
"@runwayml/avatars-react": "^0.7.2",
"@runwayml/sdk": "^1.0.0",
"@runwayml/sdk": "^3.15.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
Expand Down
Loading