Skip to content

Commit

Permalink
Add route for creating streams (#478)
Browse files Browse the repository at this point in the history
This PR adds:
- API route that creates a stream for given sandbox ID `POST
/stream/sandbox`
- Dynamic page for displaying the livestream video
`/stream/sandbox/[sandboxId]
  • Loading branch information
mlejva authored Nov 24, 2024
2 parents 8591054 + c3d5bda commit 2b51e88
Show file tree
Hide file tree
Showing 14 changed files with 682 additions and 362 deletions.
1 change: 0 additions & 1 deletion apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ const nextConfig = {
destination: 'https://app.posthog.com/:path*',
// BEWARE: setting basePath will break the analytics proxy
},
{ source: '/:path*', destination: '/_404/:path*' },
]
}
}
Expand Down
10 changes: 6 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"@headlessui/tailwindcss": "^0.2.0",
"@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.1.5",
"@mux/mux-node": "^9.0.1",
"@mux/mux-player-react": "^3.1.0",
"@next/mdx": "^14.2.5",
"@nivo/line": "^0.87.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
Expand All @@ -34,15 +36,13 @@
"@supabase/supabase-js": "^2.36.0",
"@tailwindcss/typography": "0.5.9",
"@types/node": "20.6.3",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.7",
"@types/react-highlight-words": "^0.16.4",
"@vercel/analytics": "^1.0.2",
"acorn": "^8.8.1",
"autoprefixer": "^10.4.7",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"e2b": "^0.16.2",
"e2b": "^1.0.5",
"fast-glob": "^3.3.0",
"fast-xml-parser": "^4.3.3",
"flexsearch": "^0.7.31",
Expand Down Expand Up @@ -79,9 +79,11 @@
"@nodelib/fs.walk": "^2.0.0",
"@stylistic/eslint-plugin-ts": "^1.6.2",
"@types/mdx": "^2.0.8",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"eslint-config-next": "14.2.5",
"knip": "^2.25.2",
"sharp": "^0.32.0",
"tailwind-scrollbar": "^3.0.5"
}
}
}
14 changes: 8 additions & 6 deletions apps/web/src/app/(dashboard)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { LayoutDashboard } from '@/components/LayoutDashboard'
import { FooterMain } from '@/components/Footer'
import { Toaster } from '@/components/ui/toaster'


export default async function Layout({ children }) {
return (
<LayoutDashboard>
{children}
<Toaster />
</LayoutDashboard>
<div className="h-full w-full">
<main className="w-full h-full flex flex-col">
{children}
<Toaster />
</main>
<FooterMain />
</div>
)
}
1 change: 1 addition & 0 deletions apps/web/src/app/(docs)/docs/api-reference/js-sdk/page.mdx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions apps/web/src/app/api/stream/sandbox/[sandboxId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { verifySandbox } from '@/lib/utils'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!

const supabase = createClient(supabaseUrl, supabaseServiceKey)

export async function GET(request: Request, { params }: { params: { sandboxId: string } }) {
const apiKey = request.headers.get('X-API-Key')
const sandboxId = params.sandboxId

if (!sandboxId) {
return NextResponse.json({ error: 'Missing sandbox ID' }, { status: 400 })
}

if (!apiKey) {
return NextResponse.json({ error: 'Missing E2B API Key' }, { status: 400 })
}

if (!(await verifySandbox(apiKey, sandboxId))) {
return NextResponse.json({ error: 'Invalid E2B API Key' }, { status: 401 })
}

const { data: stream, error } = await supabase
.from('sandbox_streams')
.select('token')
.eq('sandbox_id', sandboxId)
.single()

if (error) {
return NextResponse.json({ error: `Failed to retrieve stream - ${error.message}` }, { status: 500 })
}

if (!stream) {
return NextResponse.json({ error: `Stream not found for sandbox ${sandboxId}` }, { status: 404 })
}

return NextResponse.json({ token: stream.token }, { status: 200 })
}

76 changes: 76 additions & 0 deletions apps/web/src/app/api/stream/sandbox/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Mux from '@mux/mux-node'
import { NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { verifySandbox } from '@/lib/utils'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!

const supabase = createClient(supabaseUrl, supabaseServiceKey)

const mux = new Mux({
tokenId: process.env.MUX_TOKEN_ID,
tokenSecret: process.env.MUX_TOKEN_SECRET
})


// Create a new live stream and return the stream key
export async function POST(request: Request) {
const apiKey = request.headers.get('X-API-Key')
const { sandboxId } = await request.json()

if (!sandboxId) {
return NextResponse.json({ error: 'Missing sandboxId' }, { status: 400 })
}

if (!apiKey) {
return NextResponse.json({ error: 'Missing E2B API Key' }, { status: 400 })
}

if (!(await verifySandbox(apiKey, sandboxId))) {
return NextResponse.json({ error: 'Invalid E2B API Key' }, { status: 401 })
}

// Check if a stream already exists for the sandbox
const { data: existingStream, error: existingStreamError } = await supabase
.from('sandbox_streams')
.select('token')
.eq('sandbox_id', sandboxId)
.single()

if (existingStreamError && existingStreamError.code !== 'PGRST116') {
return NextResponse.json({ error: `Failed to check existing stream - ${existingStreamError.message}` }, { status: 500 })
}

if (existingStream) {
return NextResponse.json({ error: `Stream for the sandbox '${sandboxId}' already exists. There can be only one stream per sandbox.` }, { status: 400 })
}

// The stream doesn't exist yet, so create a new live stream
const liveStream = await mux.video.liveStreams.create({
latency_mode: 'low',
reconnect_window: 60,
playback_policy: ['public'],
new_asset_settings: { playback_policy: ['public'] },
})

if (!liveStream.playback_ids?.[0]?.id) {
return NextResponse.json({ error: 'Failed to create live stream' }, { status: 500 })
}

const { data, error }: { data: { token: string } | null, error: any } = await supabase
.from('sandbox_streams')
.insert([{ sandbox_id: sandboxId, playback_id: liveStream.playback_ids[0].id }])
.select('token')
.single()

if (error) {
return NextResponse.json({ error: `Failed to insert and retrieve token - ${error.message}` }, { status: 500 })
}

if (!data) {
return NextResponse.json({ error: 'Failed to insert and retrieve token - no data' }, { status: 500 })
}

return NextResponse.json({ streamKey: liveStream.stream_key, token: data.token }, { status: 201 })
}
4 changes: 0 additions & 4 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import { Section } from '@/components/SectionProvider'
import { Layout } from '@/components/Layout'

export const metadata: Metadata = {
// TODO: Add metadataBase
// metadataBase: ''
title: {
template: '%s - E2B',
default: 'E2B - Code Interpreting for AI apps',
Expand All @@ -39,7 +37,6 @@ declare global {
}
}


export default async function RootLayout({ children }) {
const pages = await glob('**/*.mdx', { cwd: 'src/app/(docs)/docs' })
const allSectionsEntries = (await Promise.all(
Expand All @@ -50,7 +47,6 @@ export default async function RootLayout({ children }) {
)) as Array<[string, Array<Section>]>
const allSections = Object.fromEntries(allSectionsEntries)


return (
<html
lang="en"
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Link from 'next/link'

export default function NotFound() {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100vh' }}>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/" style={{ textDecoration: 'underline', color: '#ff8800' }}>Return Home</Link>
</div>
)
}
66 changes: 66 additions & 0 deletions apps/web/src/app/stream/sandbox/[sandboxId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Suspense } from 'react'
import { createClient } from '@supabase/supabase-js'
import MuxPlayer from '@mux/mux-player-react'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!

const supabase = createClient(supabaseUrl, supabaseServiceKey)

interface SandboxStream {
sandboxId: string
playbackId: string
}

async function fetchStream(sandboxId: string, token: string): Promise<SandboxStream | null> {
const { data, error } = await supabase
.from('sandbox_streams')
.select('playback_id')
.eq('sandbox_id', sandboxId)
.eq('token', token)
.single()

if (error || !data) {
return null
}

return { sandboxId, playbackId: data.playback_id }
}

export default async function StreamPage({
params,
searchParams // Add searchParams to props
}: {
params: { sandboxId: string }
searchParams: { token?: string } // Add type for searchParams
}) {
const token = searchParams.token

if (!token) {
return <div>Missing token</div>
}

const stream = await fetchStream(params.sandboxId, token)

if (!stream) {
return <div>Stream not found</div>
}

return (
<Suspense fallback={<div className="h-full w-full flex items-center justify-center">Loading stream...</div>}>
<div className="flex justify-center max-h-[768px]">
<MuxPlayer
autoPlay
muted
playbackId={stream.playbackId}
themeProps={{ controlBarVertical: true, controlBarPlace: 'start start' }}
metadata={{
video_id: `sandbox-${stream.sandboxId}`,
video_title: 'Desktop Sandbox Stream',
}}
streamType="live"
/>
</div>
</Suspense>
)
}
18 changes: 0 additions & 18 deletions apps/web/src/components/LayoutDashboard.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions apps/web/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Sandbox } from 'e2b'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

// Verify that the sandbox exists and is associated with the API key
export async function verifySandbox(apiKey: string, sandboxId: string) {
const sandboxes = await Sandbox.list({ apiKey })
return sandboxes.some((sandbox) => sandbox.sandboxId === sandboxId)
}
15 changes: 0 additions & 15 deletions apps/web/src/pages/_404/[...path].tsx

This file was deleted.

Loading

0 comments on commit 2b51e88

Please sign in to comment.