-
Notifications
You must be signed in to change notification settings - Fork 477
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add route for creating streams (#478)
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
Showing
14 changed files
with
682 additions
and
362 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
5 changes: 5 additions & 0 deletions
5
apps/web/src/app/(docs)/docs/api-reference/js-sdk/v1.0.5/page.mdx
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.