diff --git a/.gitignore b/.gitignore index 5ea44cb..55477f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dist/ # IDE .idea/ .vscode/ +.cursor/hooks/ *.swp *.swo diff --git a/README.md b/README.md index d7f28c1..4c9edd1 100644 --- a/README.md +++ b/README.md @@ -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 }); } ``` diff --git a/examples/express/package.json b/examples/express/package.json index ab99e49..0d4c006 100644 --- a/examples/express/package.json +++ b/examples/express/package.json @@ -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" diff --git a/examples/express/runway-realtime.ts b/examples/express/runway-realtime.ts deleted file mode 100644 index 23305ce..0000000 --- a/examples/express/runway-realtime.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Temporary wrapper for Runway realtime sessions API - * Remove when @runwayml/sdk adds native support - */ - -import type Runway from '@runwayml/sdk'; - -export type AvatarConfig = - | { type: 'runway-preset'; presetId: string } - | { type: 'custom'; customId: string }; - -export interface CreateSessionOptions { - model: string; - avatar: AvatarConfig; -} - -export interface CreateSessionResponse { - id: string; -} - -export interface GetSessionResponse { - id: string; - status: 'NOT_READY' | 'READY' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; - sessionKey?: string; -} - -export class RunwayRealtime { - constructor(private client: Runway) {} - - async create(options: CreateSessionOptions): Promise { - return this.client.post('/v1/realtime_sessions', { - body: { model: options.model, avatar: options.avatar }, - }) as Promise; - } - - async get(sessionId: string): Promise { - return this.client.get( - `/v1/realtime_sessions/${sessionId}`, - ) as Promise; - } - - /** - * Helper to poll until session is ready - */ - async waitForReady( - sessionId: string, - options: { timeoutMs?: number; pollIntervalMs?: number } = {}, - ): Promise<{ sessionKey: string }> { - const { timeoutMs = 30 * 1000, pollIntervalMs = 1000 } = options; - const startTime = Date.now(); - - while (true) { - if (Date.now() - startTime > timeoutMs) { - throw new Error('Session creation timed out'); - } - - const session = await this.get(sessionId); - - if (session.status === 'READY' && session.sessionKey) { - return { sessionKey: session.sessionKey }; - } - - if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(session.status)) { - throw new Error( - `Session ${session.status.toLowerCase()} before becoming ready`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - } -} diff --git a/examples/express/server.ts b/examples/express/server.ts index 3e0186f..ef30a4b 100644 --- a/examples/express/server.ts +++ b/examples/express/server.ts @@ -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'))); @@ -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' }); diff --git a/examples/nextjs-server-actions/app/page.tsx b/examples/nextjs-server-actions/app/page.tsx index ba64fef..2aa0abe 100644 --- a/examples/nextjs-server-actions/app/page.tsx +++ b/examples/nextjs-server-actions/app/page.tsx @@ -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, @@ -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() { diff --git a/examples/nextjs-server-actions/package.json b/examples/nextjs-server-actions/package.json index 8401878..8a44886 100644 --- a/examples/nextjs-server-actions/package.json +++ b/examples/nextjs-server-actions/package.json @@ -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" diff --git a/examples/nextjs-server-actions/runway-realtime.ts b/examples/nextjs-server-actions/runway-realtime.ts deleted file mode 100644 index 23305ce..0000000 --- a/examples/nextjs-server-actions/runway-realtime.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Temporary wrapper for Runway realtime sessions API - * Remove when @runwayml/sdk adds native support - */ - -import type Runway from '@runwayml/sdk'; - -export type AvatarConfig = - | { type: 'runway-preset'; presetId: string } - | { type: 'custom'; customId: string }; - -export interface CreateSessionOptions { - model: string; - avatar: AvatarConfig; -} - -export interface CreateSessionResponse { - id: string; -} - -export interface GetSessionResponse { - id: string; - status: 'NOT_READY' | 'READY' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; - sessionKey?: string; -} - -export class RunwayRealtime { - constructor(private client: Runway) {} - - async create(options: CreateSessionOptions): Promise { - return this.client.post('/v1/realtime_sessions', { - body: { model: options.model, avatar: options.avatar }, - }) as Promise; - } - - async get(sessionId: string): Promise { - return this.client.get( - `/v1/realtime_sessions/${sessionId}`, - ) as Promise; - } - - /** - * Helper to poll until session is ready - */ - async waitForReady( - sessionId: string, - options: { timeoutMs?: number; pollIntervalMs?: number } = {}, - ): Promise<{ sessionKey: string }> { - const { timeoutMs = 30 * 1000, pollIntervalMs = 1000 } = options; - const startTime = Date.now(); - - while (true) { - if (Date.now() - startTime > timeoutMs) { - throw new Error('Session creation timed out'); - } - - const session = await this.get(sessionId); - - if (session.status === 'READY' && session.sessionKey) { - return { sessionKey: session.sessionKey }; - } - - if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(session.status)) { - throw new Error( - `Session ${session.status.toLowerCase()} before becoming ready`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - } -} diff --git a/examples/nextjs-simple/app/api/avatar/connect/route.ts b/examples/nextjs-simple/app/api/avatar/connect/route.ts index ded2834..df9088d 100644 --- a/examples/nextjs-simple/app/api/avatar/connect/route.ts +++ b/examples/nextjs-simple/app/api/avatar/connect/route.ts @@ -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'); } diff --git a/examples/nextjs-simple/package.json b/examples/nextjs-simple/package.json index fa84a57..11601fc 100644 --- a/examples/nextjs-simple/package.json +++ b/examples/nextjs-simple/package.json @@ -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" diff --git a/examples/nextjs-simple/runway-realtime.ts b/examples/nextjs-simple/runway-realtime.ts deleted file mode 100644 index b50da7c..0000000 --- a/examples/nextjs-simple/runway-realtime.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Temporary wrapper for Runway realtime sessions API - * Remove when @runwayml/sdk adds native support - */ - -import type Runway from '@runwayml/sdk'; - -export type AvatarConfig = - | { type: 'runway-preset'; presetId: string } - | { type: 'custom'; avatarId: string }; - -export interface CreateSessionOptions { - model: string; - avatar: AvatarConfig; -} - -export interface CreateSessionResponse { - id: string; -} - -export interface GetSessionResponse { - id: string; - status: 'NOT_READY' | 'READY' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; - sessionKey?: string; -} - -export class RunwayRealtime { - constructor(private client: Runway) {} - - async create(options: CreateSessionOptions): Promise { - return this.client.post('/v1/realtime_sessions', { - body: { model: options.model, avatar: options.avatar }, - }) as Promise; - } - - async get(sessionId: string): Promise { - return this.client.get( - `/v1/realtime_sessions/${sessionId}`, - ) as Promise; - } - - /** - * Helper to poll until session is ready - */ - async waitForReady( - sessionId: string, - options: { timeoutMs?: number; pollIntervalMs?: number } = {}, - ): Promise<{ sessionKey: string }> { - const { timeoutMs = 30 * 1000, pollIntervalMs = 1000 } = options; - const startTime = Date.now(); - - while (true) { - if (Date.now() - startTime > timeoutMs) { - throw new Error('Session creation timed out'); - } - - const session = await this.get(sessionId); - - if (session.status === 'READY' && session.sessionKey) { - return { sessionKey: session.sessionKey }; - } - - if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(session.status)) { - throw new Error( - `Session ${session.status.toLowerCase()} before becoming ready`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - } -} diff --git a/examples/nextjs/README.md b/examples/nextjs/README.md index 4497922..80eaf2d 100644 --- a/examples/nextjs/README.md +++ b/examples/nextjs/README.md @@ -46,7 +46,7 @@ export default function AvatarPage() { ### API Route -The API route creates a session with the Runway API and returns credentials: +The API route creates a realtime session with the Runway SDK and polls until it's ready: ```ts // app/api/avatar/connect/route.ts @@ -57,25 +57,22 @@ const client = new Runway({ apiKey: process.env.RUNWAYML_API_SECRET }); export async function POST(req: Request) { const { avatarId } = await req.json(); - // Create session - const { id } = await client.post('/v1/realtime_sessions', { - body: { - model: 'gwm1_avatars', - avatar: { type: 'runway-preset', presetId: avatarId }, - }, + const { id: sessionId } = await client.realtimeSessions.create({ + model: 'gwm1_avatars', + avatar: { type: 'runway-preset', presetId: avatarId }, }); - // Poll until ready - let status = 'NOT_READY'; - let sessionKey = ''; - while (status === 'NOT_READY') { - await new Promise((r) => setTimeout(r, 1000)); - const session = await client.get(`/v1/realtime_sessions/${id}`); - status = session.status; - if (status === 'READY') sessionKey = session.sessionKey; + // 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({ sessionId: id, sessionKey }); + return Response.json({ error: 'Session creation timed out' }, { status: 504 }); } ``` diff --git a/examples/nextjs/app/api/avatar/connect/route.ts b/examples/nextjs/app/api/avatar/connect/route.ts index d9b16fd..acfce45 100644 --- a/examples/nextjs/app/api/avatar/connect/route.ts +++ b/examples/nextjs/app/api/avatar/connect/route.ts @@ -1,8 +1,6 @@ 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, customAvatarId } = await req.json(); @@ -11,12 +9,32 @@ export async function POST(req: Request) { ? { 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 session = await pollSessionUntilReady(sessionId); - return Response.json({ sessionId, sessionKey }); + 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)); + } + + throw new Error('Session creation timed out'); } diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 60ad0ef..b8a4ea3 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -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" diff --git a/examples/nextjs/runway-realtime.ts b/examples/nextjs/runway-realtime.ts deleted file mode 100644 index b50da7c..0000000 --- a/examples/nextjs/runway-realtime.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Temporary wrapper for Runway realtime sessions API - * Remove when @runwayml/sdk adds native support - */ - -import type Runway from '@runwayml/sdk'; - -export type AvatarConfig = - | { type: 'runway-preset'; presetId: string } - | { type: 'custom'; avatarId: string }; - -export interface CreateSessionOptions { - model: string; - avatar: AvatarConfig; -} - -export interface CreateSessionResponse { - id: string; -} - -export interface GetSessionResponse { - id: string; - status: 'NOT_READY' | 'READY' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; - sessionKey?: string; -} - -export class RunwayRealtime { - constructor(private client: Runway) {} - - async create(options: CreateSessionOptions): Promise { - return this.client.post('/v1/realtime_sessions', { - body: { model: options.model, avatar: options.avatar }, - }) as Promise; - } - - async get(sessionId: string): Promise { - return this.client.get( - `/v1/realtime_sessions/${sessionId}`, - ) as Promise; - } - - /** - * Helper to poll until session is ready - */ - async waitForReady( - sessionId: string, - options: { timeoutMs?: number; pollIntervalMs?: number } = {}, - ): Promise<{ sessionKey: string }> { - const { timeoutMs = 30 * 1000, pollIntervalMs = 1000 } = options; - const startTime = Date.now(); - - while (true) { - if (Date.now() - startTime > timeoutMs) { - throw new Error('Session creation timed out'); - } - - const session = await this.get(sessionId); - - if (session.status === 'READY' && session.sessionKey) { - return { sessionKey: session.sessionKey }; - } - - if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(session.status)) { - throw new Error( - `Session ${session.status.toLowerCase()} before becoming ready`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - } -} diff --git a/examples/react-router/app/routes/api.avatar.connect.ts b/examples/react-router/app/routes/api.avatar.connect.ts index 18c1453..7c769da 100644 --- a/examples/react-router/app/routes/api.avatar.connect.ts +++ b/examples/react-router/app/routes/api.avatar.connect.ts @@ -1,25 +1,43 @@ import Runway from '@runwayml/sdk'; -import { RunwayRealtime } from '../../runway-realtime'; const client = new Runway({ apiKey: process.env.RUNWAYML_API_SECRET, baseURL: process.env.RUNWAYML_BASE_URL, }); -const realtime = new RunwayRealtime(client); export async function action({ request }: { request: Request }) { const { avatarId, customAvatarId } = await request.json(); 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 session = await pollSessionUntilReady(sessionId); - return Response.json({ sessionId, sessionKey }); + 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)); + } + + throw new Error('Session creation timed out'); } diff --git a/examples/react-router/package.json b/examples/react-router/package.json index 4137b1d..c258769 100644 --- a/examples/react-router/package.json +++ b/examples/react-router/package.json @@ -12,7 +12,7 @@ "@react-router/node": "^7.0.0", "@react-router/serve": "^7.0.0", "@runwayml/avatars-react": "latest", - "@runwayml/sdk": "^1.0.0", + "@runwayml/sdk": "^3.15.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.0.0", diff --git a/examples/react-router/runway-realtime.ts b/examples/react-router/runway-realtime.ts deleted file mode 100644 index 23305ce..0000000 --- a/examples/react-router/runway-realtime.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Temporary wrapper for Runway realtime sessions API - * Remove when @runwayml/sdk adds native support - */ - -import type Runway from '@runwayml/sdk'; - -export type AvatarConfig = - | { type: 'runway-preset'; presetId: string } - | { type: 'custom'; customId: string }; - -export interface CreateSessionOptions { - model: string; - avatar: AvatarConfig; -} - -export interface CreateSessionResponse { - id: string; -} - -export interface GetSessionResponse { - id: string; - status: 'NOT_READY' | 'READY' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; - sessionKey?: string; -} - -export class RunwayRealtime { - constructor(private client: Runway) {} - - async create(options: CreateSessionOptions): Promise { - return this.client.post('/v1/realtime_sessions', { - body: { model: options.model, avatar: options.avatar }, - }) as Promise; - } - - async get(sessionId: string): Promise { - return this.client.get( - `/v1/realtime_sessions/${sessionId}`, - ) as Promise; - } - - /** - * Helper to poll until session is ready - */ - async waitForReady( - sessionId: string, - options: { timeoutMs?: number; pollIntervalMs?: number } = {}, - ): Promise<{ sessionKey: string }> { - const { timeoutMs = 30 * 1000, pollIntervalMs = 1000 } = options; - const startTime = Date.now(); - - while (true) { - if (Date.now() - startTime > timeoutMs) { - throw new Error('Session creation timed out'); - } - - const session = await this.get(sessionId); - - if (session.status === 'READY' && session.sessionKey) { - return { sessionKey: session.sessionKey }; - } - - if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(session.status)) { - throw new Error( - `Session ${session.status.toLowerCase()} before becoming ready`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - } -} diff --git a/playground/app/api/avatar/connect/route.ts b/playground/app/api/avatar/connect/route.ts index 4b637ca..7658f5a 100644 --- a/playground/app/api/avatar/connect/route.ts +++ b/playground/app/api/avatar/connect/route.ts @@ -1,5 +1,4 @@ import Runway from '@runwayml/sdk'; -import { RunwayRealtime } from '../../../../runway-realtime'; export async function POST(req: Request) { const { avatarId, customAvatarId, apiKey } = await req.json(); @@ -13,20 +12,35 @@ export async function POST(req: Request) { try { const client = new Runway({ apiKey }); - const realtime = new RunwayRealtime(client); const avatar = 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; - return Response.json({ sessionId, sessionKey }); + while (Date.now() < deadline) { + const session = await client.realtimeSessions.retrieve(sessionId); + + if (session.status === 'READY') { + return Response.json({ 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'); } catch (error) { const message = error instanceof Error ? error.message : 'Failed to create session'; const isAuthError = message.toLowerCase().includes('unauthorized') || diff --git a/playground/app/page.tsx b/playground/app/page.tsx index 158daec..fe59ba8 100644 --- a/playground/app/page.tsx +++ b/playground/app/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { useCallback, useEffect, useState, Suspense } from 'react'; +import Image from 'next/image'; import { AvatarCall } from '@runwayml/avatars-react'; import '@runwayml/avatars-react/styles.css'; @@ -213,11 +214,11 @@ export default function Home() { disabled={!hasApiKey} title={hasApiKey ? `Start call with ${preset.name}` : 'Enter your API key first'} > - {preset.name}
diff --git a/playground/next.config.ts b/playground/next.config.ts index 4156c9f..f4664e4 100644 --- a/playground/next.config.ts +++ b/playground/next.config.ts @@ -3,6 +3,15 @@ import { resolve } from 'path'; const nextConfig: NextConfig = { outputFileTracingRoot: resolve(__dirname), + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'runway-static-assets.s3.us-east-1.amazonaws.com', + pathname: '/calliope-demo/**', + }, + ], + }, }; export default nextConfig; diff --git a/playground/package.json b/playground/package.json index aee99bb..75be795 100644 --- a/playground/package.json +++ b/playground/package.json @@ -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" diff --git a/playground/runway-realtime.ts b/playground/runway-realtime.ts deleted file mode 100644 index e647300..0000000 --- a/playground/runway-realtime.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Temporary wrapper for Runway realtime sessions API - * Remove when @runwayml/sdk adds native support - */ - -import type Runway from '@runwayml/sdk'; - -export type AvatarConfig = - | { type: 'runway-preset'; presetId: string } - | { type: 'custom'; avatarId: string }; - -export interface CreateSessionOptions { - model: string; - avatar: AvatarConfig; -} - -export interface CreateSessionResponse { - id: string; -} - -export interface GetSessionResponse { - id: string; - status: 'NOT_READY' | 'READY' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED'; - sessionKey?: string; -} - -export class RunwayRealtime { - constructor(private client: Runway) {} - - async create(options: CreateSessionOptions): Promise { - return this.client.post('/v1/realtime_sessions', { - body: { model: options.model, avatar: options.avatar }, - }) as Promise; - } - - async get(sessionId: string): Promise { - return this.client.get( - `/v1/realtime_sessions/${sessionId}`, - ) as Promise; - } - - async waitForReady( - sessionId: string, - options: { timeoutMs?: number; pollIntervalMs?: number } = {}, - ): Promise<{ sessionKey: string }> { - const { timeoutMs = 30000, pollIntervalMs = 1000 } = options; - const startTime = Date.now(); - - while (true) { - if (Date.now() - startTime > timeoutMs) { - throw new Error('Session creation timed out'); - } - - const session = await this.get(sessionId); - - if (session.status === 'READY' && session.sessionKey) { - return { sessionKey: session.sessionKey }; - } - - if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(session.status)) { - throw new Error( - `Session ${session.status.toLowerCase()} before becoming ready`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - } -}