From f145cb19b2aa2eed6f363ca3d00b5f097aa10f5a Mon Sep 17 00:00:00 2001 From: Collins Ikechukwu Date: Sat, 27 Sep 2025 13:04:43 +0100 Subject: [PATCH 1/2] feat: implement wallet-based streaming api endpoints --- .husky/commit-msg | 9 +- app/api/v2/streaming/auth/session/route.ts | 102 ++ app/api/v2/streaming/auth/verify/route.ts | 77 ++ app/api/v2/streaming/health/route.ts | 135 +++ app/api/v2/streaming/playback/live/route.ts | 164 +++ .../playback/stream/[streamId]/route.ts | 115 +++ .../playback/wallet/[wallet]/route.ts | 102 ++ app/api/v2/streaming/streams/create/route.ts | 144 +++ app/api/v2/streaming/streams/delete/route.ts | 101 ++ app/api/v2/streaming/streams/start/route.ts | 107 ++ app/api/v2/streaming/streams/status/route.ts | 91 ++ app/api/v2/streaming/streams/stop/route.ts | 84 ++ .../v2/streaming/streams/terminate/route.ts | 99 ++ app/streaming-test/page.tsx | 9 + components/streaming/StreamingTestLink.tsx | 49 + components/streaming/StreamingTestUI.tsx | 967 ++++++++++++++++++ components/streaming/VideoPlayer.tsx | 220 ++++ components/ui/alert.tsx | 59 ++ components/ui/badge.tsx | 36 + components/ui/card.tsx | 83 ++ components/ui/tabs.tsx | 55 + lib/streaming/auth-utils.ts | 290 ++++++ lib/streaming/livepeer-service.ts | 395 +++++++ package.json | 3 +- scripts/create-streaming-tables.mjs | 205 ++++ scripts/update-streaming-schema-final.mjs | 138 +++ scripts/update-streaming-schema-final.sql | 340 ++++++ scripts/update-streaming-schema-simple.mjs | 138 +++ scripts/update-streaming-schema-simple.sql | 340 ++++++ scripts/update-streaming-schema-v2.mjs | 121 +++ scripts/update-streaming-schema-v2.sql | 83 ++ scripts/update-streaming-schema.mjs | 281 +++++ scripts/update-streaming-schema.sql | 469 +++++++++ 33 files changed, 5603 insertions(+), 8 deletions(-) create mode 100644 app/api/v2/streaming/auth/session/route.ts create mode 100644 app/api/v2/streaming/auth/verify/route.ts create mode 100644 app/api/v2/streaming/health/route.ts create mode 100644 app/api/v2/streaming/playback/live/route.ts create mode 100644 app/api/v2/streaming/playback/stream/[streamId]/route.ts create mode 100644 app/api/v2/streaming/playback/wallet/[wallet]/route.ts create mode 100644 app/api/v2/streaming/streams/create/route.ts create mode 100644 app/api/v2/streaming/streams/delete/route.ts create mode 100644 app/api/v2/streaming/streams/start/route.ts create mode 100644 app/api/v2/streaming/streams/status/route.ts create mode 100644 app/api/v2/streaming/streams/stop/route.ts create mode 100644 app/api/v2/streaming/streams/terminate/route.ts create mode 100644 app/streaming-test/page.tsx create mode 100644 components/streaming/StreamingTestLink.tsx create mode 100644 components/streaming/StreamingTestUI.tsx create mode 100644 components/streaming/VideoPlayer.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 lib/streaming/auth-utils.ts create mode 100644 lib/streaming/livepeer-service.ts create mode 100644 scripts/create-streaming-tables.mjs create mode 100644 scripts/update-streaming-schema-final.mjs create mode 100644 scripts/update-streaming-schema-final.sql create mode 100644 scripts/update-streaming-schema-simple.mjs create mode 100644 scripts/update-streaming-schema-simple.sql create mode 100755 scripts/update-streaming-schema-v2.mjs create mode 100644 scripts/update-streaming-schema-v2.sql create mode 100644 scripts/update-streaming-schema.mjs create mode 100644 scripts/update-streaming-schema.sql diff --git a/.husky/commit-msg b/.husky/commit-msg index 0b8383e..ad17965 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,9 +1,4 @@ #!/usr/bin/env sh -# Run our custom commit-msg hook first for better error messages -.git/hooks/commit-msg "$1" - -# If our custom hook passes, then run commitlint for additional validation -if [ $? -eq 0 ]; then - npx --no -- commitlint --edit $1 -fi +# Run commitlint for commit message validation +npx --no -- commitlint --edit $1 diff --git a/app/api/v2/streaming/auth/session/route.ts b/app/api/v2/streaming/auth/session/route.ts new file mode 100644 index 0000000..ce88aec --- /dev/null +++ b/app/api/v2/streaming/auth/session/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + authenticateWalletSimple, + hasActiveStream, + getUserStreamInfo, +} from "@/lib/streaming/auth-utils"; + +/** + * GET /api/v2/streaming/auth/session + * Get current streaming session status for authenticated wallet + */ +export async function GET(request: NextRequest) { + try { + const authResult = await authenticateWalletSimple(request); + + if (!authResult.isValid) { + return NextResponse.json( + { + error: authResult.error || "Authentication failed", + success: false, + }, + { status: 401 } + ); + } + + const streamInfo = await getUserStreamInfo(authResult.walletAddress); + const hasActive = await hasActiveStream(authResult.walletAddress); + + return NextResponse.json({ + success: true, + user: authResult.user, + walletAddress: authResult.walletAddress, + streamInfo: { + hasStream: streamInfo.hasStream, + isLive: streamInfo.isLive, + hasActiveStream: hasActive, + streamId: streamInfo.streamId, + playbackId: streamInfo.playbackId, + }, + }); + } catch (error) { + console.error("Session check error:", error); + return NextResponse.json( + { + error: "Internal server error", + success: false, + }, + { status: 500 } + ); + } +} + +/** + * POST /api/v2/streaming/auth/session + * Create or refresh streaming session + */ +export async function POST(request: NextRequest) { + try { + const authResult = await authenticateWalletSimple(request); + + if (!authResult.isValid) { + return NextResponse.json( + { + error: authResult.error || "Authentication failed", + success: false, + }, + { status: 401 } + ); + } + + // Check if user already has an active stream + const hasActive = await hasActiveStream(authResult.walletAddress); + + if (hasActive) { + return NextResponse.json( + { + error: "User already has an active stream", + success: false, + hasActiveStream: true, + }, + { status: 409 } + ); + } + + return NextResponse.json({ + success: true, + message: "Streaming session ready", + user: authResult.user, + walletAddress: authResult.walletAddress, + canCreateStream: true, + }); + } catch (error) { + console.error("Session creation error:", error); + return NextResponse.json( + { + error: "Internal server error", + success: false, + }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/auth/verify/route.ts b/app/api/v2/streaming/auth/verify/route.ts new file mode 100644 index 0000000..6cb243b --- /dev/null +++ b/app/api/v2/streaming/auth/verify/route.ts @@ -0,0 +1,77 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + authenticateWallet, + generateAuthMessage, +} from "@/lib/streaming/auth-utils"; + +/** + * POST /api/v2/streaming/auth/verify + * Verify wallet signature for streaming authentication + */ +export async function POST(request: NextRequest) { + try { + const authResult = await authenticateWallet(request); + + if (!authResult.isValid) { + return NextResponse.json( + { + error: authResult.error || "Authentication failed", + success: false, + }, + { status: 401 } + ); + } + + return NextResponse.json({ + success: true, + message: "Wallet authenticated successfully", + user: authResult.user, + walletAddress: authResult.walletAddress, + }); + } catch (error) { + console.error("Auth verification error:", error); + return NextResponse.json( + { + error: "Internal server error", + success: false, + }, + { status: 500 } + ); + } +} + +/** + * GET /api/v2/streaming/auth/verify + * Get authentication message for wallet signing + */ +export async function GET(request: NextRequest) { + try { + const walletAddress = + request.headers.get("x-wallet-address") || + request.headers.get("wallet-address"); + + if (!walletAddress) { + return NextResponse.json( + { error: "Wallet address is required" }, + { status: 400 } + ); + } + + const authMessage = generateAuthMessage(walletAddress); + const timestamp = Date.now(); + + return NextResponse.json({ + message: authMessage, + timestamp, + walletAddress, + instructions: + "Sign this message with your wallet to authenticate for streaming", + }); + } catch (error) { + console.error("Auth message generation error:", error); + return NextResponse.json( + { error: "Failed to generate authentication message" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/health/route.ts b/app/api/v2/streaming/health/route.ts new file mode 100644 index 0000000..224eed2 --- /dev/null +++ b/app/api/v2/streaming/health/route.ts @@ -0,0 +1,135 @@ +import { NextResponse } from "next/server"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; +import { sql } from "@vercel/postgres"; + +/** + * GET /api/v2/streaming/health + * Health check endpoint for the streaming service + */ +export async function GET() { + try { + const healthChecks: { + timestamp: string; + service: string; + version: string; + status: string; + checks: { + database: { status: string; message: string }; + livepeer: { status: string; message: string }; + environment: { status: string; message: string }; + }; + stats?: { + totalUsers: number; + totalStreams: number; + liveStreams: number; + }; + } = { + timestamp: new Date().toISOString(), + service: "StreamFi V2 Streaming API", + version: "1.0.0", + status: "healthy", + checks: { + database: { status: "unknown", message: "" }, + livepeer: { status: "unknown", message: "" }, + environment: { status: "unknown", message: "" }, + }, + }; + + // Check database connectivity + try { + await sql`SELECT 1`; + healthChecks.checks.database = { + status: "healthy", + message: "Database connection successful", + }; + } catch (error) { + healthChecks.checks.database = { + status: "unhealthy", + message: `Database connection failed: ${error instanceof Error ? error.message : "Unknown error"}`, + }; + healthChecks.status = "unhealthy"; + } + + // Check Livepeer service + try { + const isConfigured = await livepeerService.validateConfiguration(); + if (isConfigured) { + healthChecks.checks.livepeer = { + status: "healthy", + message: "Livepeer service is configured and accessible", + }; + } else { + healthChecks.checks.livepeer = { + status: "unhealthy", + message: "Livepeer service is not properly configured", + }; + healthChecks.status = "unhealthy"; + } + } catch (error) { + healthChecks.checks.livepeer = { + status: "unhealthy", + message: `Livepeer service error: ${error instanceof Error ? error.message : "Unknown error"}`, + }; + healthChecks.status = "unhealthy"; + } + + // Check environment variables + const requiredEnvVars = ["LIVEPEER_API_KEY", "DATABASE_URL"]; + const missingEnvVars = requiredEnvVars.filter( + envVar => !process.env[envVar] + ); + + if (missingEnvVars.length === 0) { + healthChecks.checks.environment = { + status: "healthy", + message: "All required environment variables are set", + }; + } else { + healthChecks.checks.environment = { + status: "unhealthy", + message: `Missing environment variables: ${missingEnvVars.join(", ")}`, + }; + healthChecks.status = "unhealthy"; + } + + // Get basic statistics + try { + const userCount = await sql`SELECT COUNT(*) as count FROM users`; + const streamCount = + await sql`SELECT COUNT(*) as count FROM users WHERE livepeer_stream_id_v2 IS NOT NULL`; + const liveCount = + await sql`SELECT COUNT(*) as count FROM users WHERE is_live_v2 = true`; + + healthChecks.stats = { + totalUsers: parseInt(userCount.rows[0]?.count || "0"), + totalStreams: parseInt(streamCount.rows[0]?.count || "0"), + liveStreams: parseInt(liveCount.rows[0]?.count || "0"), + }; + } catch { + // Stats are optional, don't fail the health check + healthChecks.stats = { + totalUsers: 0, + totalStreams: 0, + liveStreams: 0, + }; + } + + const statusCode = healthChecks.status === "healthy" ? 200 : 503; + + return NextResponse.json(healthChecks, { status: statusCode }); + } catch (error) { + console.error("Health check error:", error); + + return NextResponse.json( + { + timestamp: new Date().toISOString(), + service: "StreamFi V2 Streaming API", + version: "1.0.0", + status: "unhealthy", + error: "Health check failed", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 503 } + ); + } +} diff --git a/app/api/v2/streaming/playback/live/route.ts b/app/api/v2/streaming/playback/live/route.ts new file mode 100644 index 0000000..ed81646 --- /dev/null +++ b/app/api/v2/streaming/playback/live/route.ts @@ -0,0 +1,164 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * GET /api/v2/streaming/playback/live + * Get all currently live streams + */ +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const limit = parseInt(searchParams.get("limit") || "20"); + const offset = parseInt(searchParams.get("offset") || "0"); + const category = searchParams.get("category"); + const search = searchParams.get("search"); + + // Build query for live streams + let query = ` + SELECT + u.id, + u.username, + u.wallet, + u.livepeer_stream_id_v2, + u.playback_id_v2, + u.is_live_v2, + u.stream_started_at_v2, + u.stream_title_v2, + u.stream_description_v2, + u.stream_category_v2, + u.stream_tags_v2 + FROM users u + WHERE u.is_live_v2 = true + AND u.livepeer_stream_id_v2 IS NOT NULL + AND u.playback_id_v2 IS NOT NULL + `; + + const queryParams: any[] = []; + let paramCount = 1; + + // Add category filter + if (category) { + query += ` AND u.stream_category_v2 = $${paramCount}`; + queryParams.push(category); + paramCount++; + } + + // Add search filter + if (search) { + query += ` AND ( + u.username ILIKE $${paramCount} OR + u.stream_title_v2 ILIKE $${paramCount} OR + u.stream_description_v2 ILIKE $${paramCount} + )`; + queryParams.push(`%${search}%`); + paramCount++; + } + + // Add ordering and pagination + query += ` ORDER BY u.stream_started_at_v2 DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}`; + queryParams.push(limit, offset); + + const result = await sql.query(query, queryParams); + const streams = result.rows; + + // Get additional information for each stream + const enrichedStreams = await Promise.all( + streams.map(async stream => { + // Get stream health + let streamHealth = null; + try { + streamHealth = await livepeerService.getStreamHealth( + stream.livepeer_stream_id_v2 + ); + } catch (healthError) { + console.warn( + `Failed to get health for stream ${stream.livepeer_stream_id_v2}:`, + healthError + ); + } + + // Get playback info + let playbackInfo = null; + try { + playbackInfo = await livepeerService.getPlaybackInfo( + stream.playback_id_v2 + ); + } catch (playbackError) { + console.warn( + `Failed to get playback info for stream ${stream.playback_id_v2}:`, + playbackError + ); + } + + return { + streamId: stream.livepeer_stream_id_v2, + playbackId: stream.playback_id_v2, + isLive: stream.is_live_v2, + startedAt: stream.stream_started_at_v2, + title: stream.stream_title_v2 || "", + description: stream.stream_description_v2 || "", + category: stream.stream_category_v2 || "", + tags: stream.stream_tags_v2 || [], + creator: { + username: stream.username, + wallet: stream.wallet, + }, + playbackInfo: playbackInfo, + health: streamHealth, + }; + }) + ); + + // Get total count for pagination + let countQuery = ` + SELECT COUNT(*) as total + FROM users u + WHERE u.is_live_v2 = true + AND u.livepeer_stream_id_v2 IS NOT NULL + AND u.playback_id_v2 IS NOT NULL + `; + + const countParams: any[] = []; + let countParamCount = 1; + + if (category) { + countQuery += ` AND u.stream_category_v2 = $${countParamCount}`; + countParams.push(category); + countParamCount++; + } + + if (search) { + countQuery += ` AND ( + u.username ILIKE $${countParamCount} OR + u.stream_title_v2 ILIKE $${countParamCount} OR + u.stream_description_v2 ILIKE $${countParamCount} + )`; + countParams.push(`%${search}%`); + } + + const countResult = await sql.query(countQuery, countParams); + const total = parseInt(countResult.rows[0]?.total || "0"); + + return NextResponse.json({ + success: true, + streams: enrichedStreams, + pagination: { + total, + limit, + offset, + hasMore: offset + limit < total, + }, + filters: { + category: category || null, + search: search || null, + }, + }); + } catch (error) { + console.error("Live streams error:", error); + return NextResponse.json( + { error: "Failed to get live streams" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/playback/stream/[streamId]/route.ts b/app/api/v2/streaming/playback/stream/[streamId]/route.ts new file mode 100644 index 0000000..6637e3b --- /dev/null +++ b/app/api/v2/streaming/playback/stream/[streamId]/route.ts @@ -0,0 +1,115 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * GET /api/v2/streaming/playback/stream/[streamId] + * Get playback information for a specific stream ID + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ streamId: string }> } +) { + try { + const { streamId } = await params; + + if (!streamId) { + return NextResponse.json( + { error: "Stream ID is required" }, + { status: 400 } + ); + } + + // Get stream information from database + const userResult = await sql` + SELECT + u.id, + u.username, + u.wallet, + u.livepeer_stream_id_v2, + u.playback_id_v2, + u.is_live_v2, + u.stream_started_at_v2, + u.creator + FROM users u + WHERE u.livepeer_stream_id = ${streamId} + `; + + if (userResult.rows.length === 0) { + return NextResponse.json({ error: "Stream not found" }, { status: 404 }); + } + + const user = userResult.rows[0]; + const creatorData = user.creator || {}; + + // Get stream information from Livepeer + let livepeerStream; + try { + livepeerStream = await livepeerService.getStream(streamId); + } catch (streamError) { + console.error("Failed to get stream from Livepeer:", streamError); + return NextResponse.json( + { error: "Stream not available" }, + { status: 503 } + ); + } + + // Get playback information + let playbackInfo; + try { + playbackInfo = await livepeerService.getPlaybackInfo(user.playback_id_v2); + } catch (playbackError) { + console.error("Failed to get playback info:", playbackError); + return NextResponse.json( + { error: "Stream playback unavailable" }, + { status: 503 } + ); + } + + // Get stream health + let streamHealth = null; + try { + streamHealth = await livepeerService.getStreamHealth(streamId); + } catch (healthError) { + console.warn("Failed to get stream health:", healthError); + } + + return NextResponse.json({ + success: true, + stream: { + streamId: streamId, + playbackId: user.playback_id_v2, + isLive: user.is_live_v2, + isActive: livepeerStream.isActive, + startedAt: user.stream_started_at_v2, + lastSeen: livepeerStream.lastSeen, + title: creatorData.streamTitle || livepeerStream.name, + description: creatorData.description || "", + category: creatorData.category || "", + tags: creatorData.tags || [], + creator: { + username: user.username, + wallet: user.wallet, + }, + playback: { + hlsUrl: playbackInfo.hlsUrl, + rtmpUrl: playbackInfo.rtmpUrl, + dashUrl: playbackInfo.dashUrl, + }, + health: streamHealth, + livepeer: { + name: livepeerStream.name, + createdAt: livepeerStream.createdAt, + isActive: livepeerStream.isActive, + lastSeen: livepeerStream.lastSeen, + }, + }, + }); + } catch (error) { + console.error("Stream playback error:", error); + return NextResponse.json( + { error: "Failed to get stream playback information" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/playback/wallet/[wallet]/route.ts b/app/api/v2/streaming/playback/wallet/[wallet]/route.ts new file mode 100644 index 0000000..d6d198a --- /dev/null +++ b/app/api/v2/streaming/playback/wallet/[wallet]/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * GET /api/v2/streaming/playback/wallet/[wallet] + * Get playback information for a wallet address + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ wallet: string }> } +) { + try { + const { wallet } = await params; + + if (!wallet) { + return NextResponse.json( + { error: "Wallet address is required" }, + { status: 400 } + ); + } + + // Get user's stream information + const userResult = await sql` + SELECT + id, + username, + livepeer_stream_id_v2, + playback_id_v2, + is_live_v2, + stream_started_at_v2, + creator + FROM users + WHERE LOWER(wallet) = LOWER(${wallet}) + `; + + if (userResult.rows.length === 0) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const user = userResult.rows[0]; + const creatorData = user.creator || {}; + + if (!user.livepeer_stream_id_v2 || !user.playback_id_v2) { + return NextResponse.json( + { error: "No stream found for this wallet" }, + { status: 404 } + ); + } + + // Get playback information from Livepeer + let playbackInfo; + try { + playbackInfo = await livepeerService.getPlaybackInfo(user.playback_id_v2); + } catch (playbackError) { + console.error("Failed to get playback info:", playbackError); + return NextResponse.json( + { error: "Stream playback unavailable" }, + { status: 503 } + ); + } + + // Get stream health if stream is live + let streamHealth = null; + if (user.is_live_v2) { + try { + streamHealth = await livepeerService.getStreamHealth( + user.livepeer_stream_id_v2 + ); + } catch (healthError) { + console.warn("Failed to get stream health:", healthError); + } + } + + return NextResponse.json({ + success: true, + stream: { + streamId: user.livepeer_stream_id_v2, + playbackId: user.playback_id, + isLive: user.is_live_v2, + viewerCount: user.current_viewers_v2 || 0, + startedAt: user.stream_started_at_v2, + title: creatorData.streamTitle || "", + description: creatorData.description || "", + category: creatorData.category || "", + tags: creatorData.tags || [], + creator: { + username: user.username, + wallet: wallet, + }, + playbackInfo: playbackInfo, + health: streamHealth, + }, + }); + } catch (error) { + console.error("Playback info error:", error); + return NextResponse.json( + { error: "Failed to get playback information" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/streams/create/route.ts b/app/api/v2/streaming/streams/create/route.ts new file mode 100644 index 0000000..2238f28 --- /dev/null +++ b/app/api/v2/streaming/streams/create/route.ts @@ -0,0 +1,144 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { + authenticateWalletSimple, + hasActiveStream, +} from "@/lib/streaming/auth-utils"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * POST /api/v2/streaming/streams/create + * Create a new stream for authenticated wallet + */ +export async function POST(request: NextRequest) { + try { + // Authenticate wallet + const authResult = await authenticateWalletSimple(request); + if (!authResult.isValid) { + return NextResponse.json( + { error: authResult.error || "Authentication failed" }, + { status: 401 } + ); + } + + // Check if user already has an active stream + const hasActive = await hasActiveStream(authResult.walletAddress); + if (hasActive) { + return NextResponse.json( + { + error: + "User already has an active stream. Please stop the current stream first.", + }, + { status: 409 } + ); + } + + // Parse request body + const body = await request.json(); + const { title, description, category, tags, record = true } = body; + + // Validate required fields + if (!title || title.trim().length === 0) { + return NextResponse.json( + { error: "Stream title is required" }, + { status: 400 } + ); + } + + if (title.length > 100) { + return NextResponse.json( + { error: "Stream title must be 100 characters or less" }, + { status: 400 } + ); + } + + if (description && description.length > 500) { + return NextResponse.json( + { error: "Stream description must be 500 characters or less" }, + { status: 400 } + ); + } + + // Create stream with Livepeer + const streamData = await livepeerService.createStream( + authResult.walletAddress, + { + title: title.trim(), + description: description?.trim(), + category: category?.trim(), + tags: tags || [], + record, + } + ); + + // Update user record with stream information + const updatedCreator = { + streamTitle: title.trim(), + description: description?.trim() || "", + category: category?.trim() || "", + tags: tags || [], + lastUpdated: new Date().toISOString(), + }; + + await sql` + UPDATE users SET + livepeer_stream_id_v2 = ${streamData.streamId}, + playback_id_v2 = ${streamData.playbackId}, + streamkey = ${streamData.streamKey}, + creator = ${JSON.stringify(updatedCreator)}, + is_live_v2 = false, + updated_at = CURRENT_TIMESTAMP + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + `; + + // Create stream session record + const userResult = await sql` + SELECT id FROM users WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + `; + const userId = userResult.rows[0]?.id; + + if (userId) { + await sql` + INSERT INTO stream_sessions_v2 (user_id, livepeer_stream_id, playback_id, stream_key, started_at) + VALUES (${userId}, ${streamData.streamId}, ${streamData.playbackId}, ${streamData.streamKey}, CURRENT_TIMESTAMP) + `; + } + + return NextResponse.json( + { + success: true, + message: "Stream created successfully", + stream: { + streamId: streamData.streamId, + playbackId: streamData.playbackId, + streamKey: streamData.streamKey, + ingestUrl: streamData.ingestUrl, + rtmpUrl: streamData.rtmpUrl, + title: title.trim(), + description: description?.trim(), + category: category?.trim(), + tags: tags || [], + isActive: false, + createdAt: new Date().toISOString(), + }, + }, + { status: 201 } + ); + } catch (error) { + console.error("Stream creation error:", error); + + if (error instanceof Error) { + if (error.message.includes("Livepeer")) { + return NextResponse.json( + { error: "Streaming service unavailable. Please try again later." }, + { status: 503 } + ); + } + } + + return NextResponse.json( + { error: "Failed to create stream" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/streams/delete/route.ts b/app/api/v2/streaming/streams/delete/route.ts new file mode 100644 index 0000000..187bb6e --- /dev/null +++ b/app/api/v2/streaming/streams/delete/route.ts @@ -0,0 +1,101 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { + authenticateWalletSimple, + getUserStreamInfo, +} from "@/lib/streaming/auth-utils"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * DELETE /api/v2/streaming/streams/delete + * Delete stream for authenticated wallet + */ +export async function DELETE(request: NextRequest) { + try { + // Authenticate wallet + const authResult = await authenticateWalletSimple(request); + if (!authResult.isValid) { + return NextResponse.json( + { error: authResult.error || "Authentication failed" }, + { status: 401 } + ); + } + + // Get user's stream information + const streamInfo = await getUserStreamInfo(authResult.walletAddress); + + if (!streamInfo.hasStream) { + return NextResponse.json( + { error: "No stream found to delete" }, + { status: 404 } + ); + } + + // If stream is live, stop it first + if (streamInfo.isLive) { + await sql` + UPDATE users SET + is_live_v2 = false, + updated_at = CURRENT_TIMESTAMP + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + `; + } + + // Delete stream from Livepeer + if (streamInfo.streamId) { + try { + await livepeerService.deleteStream(streamInfo.streamId); + console.log( + "Livepeer stream deleted successfully:", + streamInfo.streamId + ); + } catch (livepeerError) { + console.error("Failed to delete Livepeer stream:", livepeerError); + // Continue with database cleanup even if Livepeer deletion fails + } + } + + // Clear stream data from user record + await sql` + UPDATE users SET + livepeer_stream_id_v2 = NULL, + playback_id_v2 = NULL, + streamkey = NULL, + is_live_v2 = false, + stream_started_at_v2 = NULL, + creator = jsonb_set(creator, '{streamTitle}', '""'), + updated_at = CURRENT_TIMESTAMP + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + `; + + // Mark stream sessions as ended + const userResult = await sql` + SELECT id FROM users WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + `; + const userId = userResult.rows[0]?.id; + + if (userId && streamInfo.streamId) { + await sql` + UPDATE stream_sessions_v2 + SET ended_at = CURRENT_TIMESTAMP + WHERE user_id = ${userId} AND livepeer_stream_id = ${streamInfo.streamId} + `; + } + + return NextResponse.json({ + success: true, + message: "Stream deleted successfully", + deletedStream: { + streamId: streamInfo.streamId, + playbackId: streamInfo.playbackId, + deletedAt: new Date().toISOString(), + }, + }); + } catch (error) { + console.error("Stream deletion error:", error); + return NextResponse.json( + { error: "Failed to delete stream" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/streams/start/route.ts b/app/api/v2/streaming/streams/start/route.ts new file mode 100644 index 0000000..a8504de --- /dev/null +++ b/app/api/v2/streaming/streams/start/route.ts @@ -0,0 +1,107 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { + authenticateWalletSimple, + getUserStreamInfo, +} from "@/lib/streaming/auth-utils"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * POST /api/v2/streaming/streams/start + * Start streaming for authenticated wallet + */ +export async function POST(request: NextRequest) { + try { + // Authenticate wallet + const authResult = await authenticateWalletSimple(request); + if (!authResult.isValid) { + return NextResponse.json( + { error: authResult.error || "Authentication failed" }, + { status: 401 } + ); + } + + // Get user's stream information + const streamInfo = await getUserStreamInfo(authResult.walletAddress); + + if (!streamInfo.hasStream) { + return NextResponse.json( + { error: "No stream found. Please create a stream first." }, + { status: 404 } + ); + } + + if (streamInfo.isLive) { + return NextResponse.json( + { error: "Stream is already live" }, + { status: 409 } + ); + } + + if (!streamInfo.streamId) { + return NextResponse.json( + { error: "Invalid stream configuration" }, + { status: 400 } + ); + } + + // Check stream health with Livepeer + try { + const health = await livepeerService.getStreamHealth(streamInfo.streamId); + console.log("Stream health check:", health); + } catch (healthError) { + console.warn("Stream health check failed:", healthError); + // Continue anyway - the stream might not be active yet + } + + // Update user record to mark stream as live + const result = await sql` + UPDATE users SET + is_live_v2 = true, + stream_started_at_v2 = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + RETURNING id, username, livepeer_stream_id_v2, playback_id_v2 + `; + + if (result.rows.length === 0) { + return NextResponse.json( + { error: "Failed to update stream status" }, + { status: 500 } + ); + } + + const updatedUser = result.rows[0]; + + // Update stream session + try { + await sql` + UPDATE stream_sessions_v2 + SET started_at = CURRENT_TIMESTAMP + WHERE user_id = ${updatedUser.id} AND livepeer_stream_id = ${updatedUser.livepeer_stream_id_v2} + `; + } catch (sessionError) { + console.error("Failed to update stream session:", sessionError); + // Continue - this is not critical + } + + return NextResponse.json({ + success: true, + message: "Stream started successfully", + stream: { + streamId: streamInfo.streamId, + playbackId: streamInfo.playbackId, + streamKey: streamInfo.streamKey, + isLive: true, + startedAt: new Date().toISOString(), + viewerCount: 0, + }, + }); + } catch (error) { + console.error("Stream start error:", error); + return NextResponse.json( + { error: "Failed to start stream" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/streams/status/route.ts b/app/api/v2/streaming/streams/status/route.ts new file mode 100644 index 0000000..d1f6751 --- /dev/null +++ b/app/api/v2/streaming/streams/status/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { + authenticateWalletSimple, + getUserStreamInfo, +} from "@/lib/streaming/auth-utils"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * GET /api/v2/streaming/streams/status + * Get stream status for authenticated wallet + */ +export async function GET(request: NextRequest) { + try { + // Authenticate wallet + const authResult = await authenticateWalletSimple(request); + if (!authResult.isValid) { + return NextResponse.json( + { error: authResult.error || "Authentication failed" }, + { status: 401 } + ); + } + + // Get user's stream information + const streamInfo = await getUserStreamInfo(authResult.walletAddress); + + if (!streamInfo.hasStream) { + return NextResponse.json({ + success: true, + hasStream: false, + message: "No stream found", + }); + } + + // Get additional stream data from database + const userResult = await sql` + SELECT + livepeer_stream_id_v2, + playback_id_v2, + streamkey, + is_live_v2, + stream_started_at_v2, + creator + FROM users + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + `; + + if (userResult.rows.length === 0) { + return NextResponse.json({ error: "User not found" }, { status: 404 }); + } + + const user = userResult.rows[0]; + const creatorData = user.creator || {}; + + // Get stream health from Livepeer if stream is active + let streamHealth = null; + if (streamInfo.streamId && streamInfo.isLive) { + try { + streamHealth = await livepeerService.getStreamHealth( + streamInfo.streamId + ); + } catch (healthError) { + console.warn("Failed to get stream health:", healthError); + } + } + + return NextResponse.json({ + success: true, + hasStream: true, + stream: { + streamId: streamInfo.streamId, + playbackId: streamInfo.playbackId, + streamKey: streamInfo.streamKey, + isLive: streamInfo.isLive, + viewerCount: user.current_viewers_v2 || 0, + startedAt: user.stream_started_at_v2, + title: creatorData.streamTitle || "", + description: creatorData.description || "", + category: creatorData.category || "", + tags: creatorData.tags || [], + health: streamHealth, + }, + }); + } catch (error) { + console.error("Stream status error:", error); + return NextResponse.json( + { error: "Failed to get stream status" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/streams/stop/route.ts b/app/api/v2/streaming/streams/stop/route.ts new file mode 100644 index 0000000..85ec102 --- /dev/null +++ b/app/api/v2/streaming/streams/stop/route.ts @@ -0,0 +1,84 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { + authenticateWalletSimple, + getUserStreamInfo, +} from "@/lib/streaming/auth-utils"; + +/** + * POST /api/v2/streaming/streams/stop + * Stop streaming for authenticated wallet + */ +export async function POST(request: NextRequest) { + try { + // Authenticate wallet + const authResult = await authenticateWalletSimple(request); + if (!authResult.isValid) { + return NextResponse.json( + { error: authResult.error || "Authentication failed" }, + { status: 401 } + ); + } + + // Get user's stream information + const streamInfo = await getUserStreamInfo(authResult.walletAddress); + + if (!streamInfo.hasStream) { + return NextResponse.json({ error: "No stream found" }, { status: 404 }); + } + + if (!streamInfo.isLive) { + return NextResponse.json( + { error: "Stream is not currently live" }, + { status: 409 } + ); + } + + // Update user record to mark stream as not live + const result = await sql` + UPDATE users SET + is_live_v2 = false, + updated_at = CURRENT_TIMESTAMP + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + RETURNING id, username, livepeer_stream_id_v2, stream_started_at_v2 + `; + + if (result.rows.length === 0) { + return NextResponse.json( + { error: "Failed to update stream status" }, + { status: 500 } + ); + } + + const updatedUser = result.rows[0]; + + // Update stream session to mark as ended + try { + await sql` + UPDATE stream_sessions_v2 + SET ended_at = CURRENT_TIMESTAMP + WHERE user_id = ${updatedUser.id} AND livepeer_stream_id = ${updatedUser.livepeer_stream_id_v2} + `; + } catch (sessionError) { + console.error("Failed to update stream session:", sessionError); + // Continue - this is not critical + } + + return NextResponse.json({ + success: true, + message: "Stream stopped successfully", + stream: { + streamId: streamInfo.streamId, + playbackId: streamInfo.playbackId, + isLive: false, + stoppedAt: new Date().toISOString(), + }, + }); + } catch (error) { + console.error("Stream stop error:", error); + return NextResponse.json( + { error: "Failed to stop stream" }, + { status: 500 } + ); + } +} diff --git a/app/api/v2/streaming/streams/terminate/route.ts b/app/api/v2/streaming/streams/terminate/route.ts new file mode 100644 index 0000000..548905e --- /dev/null +++ b/app/api/v2/streaming/streams/terminate/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from "next/server"; +import { sql } from "@vercel/postgres"; +import { + authenticateWalletSimple, + getUserStreamInfo, +} from "@/lib/streaming/auth-utils"; +import { livepeerService } from "@/lib/streaming/livepeer-service"; + +/** + * POST /api/v2/streaming/streams/terminate + * Terminate stream for authenticated wallet (stops stream but keeps it for later use) + */ +export async function POST(request: NextRequest) { + try { + // Authenticate wallet + const authResult = await authenticateWalletSimple(request); + if (!authResult.isValid) { + return NextResponse.json( + { error: authResult.error || "Authentication failed" }, + { status: 401 } + ); + } + + // Get user's stream information + const streamInfo = await getUserStreamInfo(authResult.walletAddress); + + if (!streamInfo.hasStream) { + return NextResponse.json({ error: "No stream found" }, { status: 404 }); + } + + if (!streamInfo.isLive) { + return NextResponse.json( + { error: "Stream is not currently live" }, + { status: 409 } + ); + } + + // Terminate stream with Livepeer + if (streamInfo.streamId) { + try { + await livepeerService.terminateStream(streamInfo.streamId); + console.log( + "Livepeer stream terminated successfully:", + streamInfo.streamId + ); + } catch (livepeerError) { + console.error("Failed to terminate Livepeer stream:", livepeerError); + // Continue with database update even if Livepeer termination fails + } + } + + // Update user record to mark stream as not live + const result = await sql` + UPDATE users SET + is_live_v2 = false, + updated_at = CURRENT_TIMESTAMP + WHERE LOWER(wallet) = LOWER(${authResult.walletAddress}) + RETURNING id, username, livepeer_stream_id_v2, stream_started_at_v2 + `; + + if (result.rows.length === 0) { + return NextResponse.json( + { error: "Failed to update stream status" }, + { status: 500 } + ); + } + + const updatedUser = result.rows[0]; + + // Update stream session to mark as ended + try { + await sql` + UPDATE stream_sessions_v2 + SET ended_at = CURRENT_TIMESTAMP + WHERE user_id = ${updatedUser.id} AND livepeer_stream_id = ${updatedUser.livepeer_stream_id_v2} + `; + } catch (sessionError) { + console.error("Failed to update stream session:", sessionError); + // Continue - this is not critical + } + + return NextResponse.json({ + success: true, + message: "Stream terminated successfully", + stream: { + streamId: streamInfo.streamId, + playbackId: streamInfo.playbackId, + isLive: false, + terminatedAt: new Date().toISOString(), + }, + }); + } catch (error) { + console.error("Stream termination error:", error); + return NextResponse.json( + { error: "Failed to terminate stream" }, + { status: 500 } + ); + } +} diff --git a/app/streaming-test/page.tsx b/app/streaming-test/page.tsx new file mode 100644 index 0000000..de597e0 --- /dev/null +++ b/app/streaming-test/page.tsx @@ -0,0 +1,9 @@ +import StreamingTestUI from "@/components/streaming/StreamingTestUI"; + +export default function StreamingTestPage() { + return ( +
+ +
+ ); +} diff --git a/components/streaming/StreamingTestLink.tsx b/components/streaming/StreamingTestLink.tsx new file mode 100644 index 0000000..32ecf79 --- /dev/null +++ b/components/streaming/StreamingTestLink.tsx @@ -0,0 +1,49 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Play, TestTube } from "lucide-react"; + +export default function StreamingTestLink() { + return ( + + + + + Streaming V2 Test + + + Test the new wallet-based streaming backend with Livepeer integration + + + +
+

+ This test interface allows you to: +

+
    +
  • • Test wallet authentication
  • +
  • • Create and manage streams
  • +
  • • Test video playback
  • +
  • • View live streams
  • +
  • • Monitor stream health
  • +
+ + + +
+
+
+ ); +} diff --git a/components/streaming/StreamingTestUI.tsx b/components/streaming/StreamingTestUI.tsx new file mode 100644 index 0000000..3076a5b --- /dev/null +++ b/components/streaming/StreamingTestUI.tsx @@ -0,0 +1,967 @@ +"use client"; + +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Loader2, Play, Square, Trash2, Users, Activity } from "lucide-react"; +import VideoPlayer from "./VideoPlayer"; + +interface StreamData { + streamId: string; + playbackId: string; + streamKey: string; + ingestUrl: string; + rtmpUrl: string; + title: string; + description?: string; + category?: string; + tags?: string[]; + isActive: boolean; + createdAt: string; + isHealthy?: boolean; + suspended?: boolean; + lastTerminatedAt?: number; + profiles?: Array<{ + name: string; + fps: number; + bitrate: number; + width: number; + height: number; + }>; +} + +interface StreamStatus { + hasStream: boolean; + isLive: boolean; + stream?: StreamData; + health?: { + isActive: boolean; + lastSeen?: string; + ingestRate?: number; + outgoingRate?: number; + viewerCount?: number; + }; +} + +interface LiveStream { + streamId: string; + playbackId: string; + isLive: boolean; + viewerCount: number; + startedAt: string; + title: string; + description?: string; + category?: string; + tags?: string[]; + creator: { + username: string; + wallet: string; + }; + playback: { + hlsUrl: string; + rtmpUrl?: string; + dashUrl?: string; + }; +} + +export default function StreamingTestUI() { + const [walletAddress, setWalletAddress] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + // Stream creation form + const [streamForm, setStreamForm] = useState({ + title: "", + description: "", + category: "", + tags: "", + record: true, + }); + + // Stream status + const [streamStatus, setStreamStatus] = useState({ + hasStream: false, + isLive: false, + }); + + // Live streams + const [liveStreams, setLiveStreams] = useState([]); + + // Playback info + const [playbackInfo, setPlaybackInfo] = useState(null); + + // Test wallet authentication + const testAuth = async () => { + if (!walletAddress.trim()) { + setError("Please enter a wallet address"); + return; + } + + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/v2/streaming/auth/session", { + headers: { + "x-wallet-address": walletAddress, + }, + }); + + const data = await response.json(); + + if (data.success) { + setSuccess("Authentication successful!"); + setStreamStatus(data.streamInfo); + } else { + setError(data.error || "Authentication failed"); + } + } catch { + setError("Failed to authenticate wallet"); + } finally { + setIsLoading(false); + } + }; + + // Create stream + const createStream = async () => { + if (!walletAddress.trim()) { + setError("Please enter a wallet address"); + return; + } + + if (!streamForm.title.trim()) { + setError("Please enter a stream title"); + return; + } + + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const tags = streamForm.tags + .split(",") + .map(tag => tag.trim()) + .filter(tag => tag); + + const response = await fetch("/api/v2/streaming/streams/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-wallet-address": walletAddress, + }, + body: JSON.stringify({ + title: streamForm.title, + description: streamForm.description, + category: streamForm.category, + tags: tags, + record: streamForm.record, + }), + }); + + const data = await response.json(); + + if (data.success) { + setSuccess("Stream created successfully!"); + setStreamStatus({ + hasStream: true, + isLive: false, + stream: data.stream, + }); + } else { + setError(data.error || "Failed to create stream"); + } + } catch { + setError("Failed to create stream"); + } finally { + setIsLoading(false); + } + }; + + // Start stream + const startStream = async () => { + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/v2/streaming/streams/start", { + method: "POST", + headers: { + "x-wallet-address": walletAddress, + }, + }); + + const data = await response.json(); + + if (data.success) { + setSuccess("Stream started successfully!"); + setStreamStatus(prev => ({ + ...prev, + isLive: true, + stream: data.stream, + })); + } else { + setError(data.error || "Failed to start stream"); + } + } catch { + setError("Failed to start stream"); + } finally { + setIsLoading(false); + } + }; + + // Stop stream + const stopStream = async () => { + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/v2/streaming/streams/stop", { + method: "POST", + headers: { + "x-wallet-address": walletAddress, + }, + }); + + const data = await response.json(); + + if (data.success) { + setSuccess("Stream stopped successfully!"); + setStreamStatus(prev => ({ + ...prev, + isLive: false, + })); + } else { + setError(data.error || "Failed to stop stream"); + } + } catch { + setError("Failed to stop stream"); + } finally { + setIsLoading(false); + } + }; + + // Terminate stream + const terminateStream = async () => { + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/v2/streaming/streams/terminate", { + method: "POST", + headers: { + "x-wallet-address": walletAddress, + }, + }); + + const data = await response.json(); + + if (data.success) { + setSuccess("Stream terminated successfully!"); + setStreamStatus(prev => ({ + ...prev, + isLive: false, + })); + } else { + setError(data.error || "Failed to terminate stream"); + } + } catch { + setError("Failed to terminate stream"); + } finally { + setIsLoading(false); + } + }; + + // Delete stream + const deleteStream = async () => { + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/v2/streaming/streams/delete", { + method: "DELETE", + headers: { + "x-wallet-address": walletAddress, + }, + }); + + const data = await response.json(); + + if (data.success) { + setSuccess("Stream deleted successfully!"); + setStreamStatus({ + hasStream: false, + isLive: false, + }); + } else { + setError(data.error || "Failed to delete stream"); + } + } catch { + setError("Failed to delete stream"); + } finally { + setIsLoading(false); + } + }; + + // Get stream status + const getStreamStatus = async () => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch("/api/v2/streaming/streams/status", { + headers: { + "x-wallet-address": walletAddress, + }, + }); + + const data = await response.json(); + + if (data.success) { + setStreamStatus({ + hasStream: data.hasStream, + isLive: data.stream?.isLive || false, + stream: data.stream, + health: data.stream?.health, + }); + } else { + setError(data.error || "Failed to get stream status"); + } + } catch { + setError("Failed to get stream status"); + } finally { + setIsLoading(false); + } + }; + + // Get live streams + const getLiveStreams = async () => { + setIsLoading(true); + setError(null); + + try { + const response = await fetch("/api/v2/streaming/playback/live"); + const data = await response.json(); + + if (data.success) { + setLiveStreams(data.streams); + } else { + setError(data.error || "Failed to get live streams"); + } + } catch { + setError("Failed to get live streams"); + } finally { + setIsLoading(false); + } + }; + + // Get playback info + const getPlaybackInfo = async () => { + if (!walletAddress.trim()) { + setError("Please enter a wallet address"); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await fetch( + `/api/v2/streaming/playback/wallet/${walletAddress}` + ); + const data = await response.json(); + + if (data.success) { + setPlaybackInfo(data.stream.playbackInfo); + console.log(data.stream.playbackInfo.playbackInfo); + } else { + setError(data.error || "Failed to get playback info"); + } + } catch { + setError("Failed to get playback info"); + } finally { + setIsLoading(false); + } + }; + + // Clear messages + const clearMessages = () => { + setError(null); + setSuccess(null); + }; + + return ( +
+
+

+ StreamFi V2 Streaming Test UI +

+

+ Test the new wallet-based streaming backend with Livepeer integration +

+
+ + {/* Wallet Input */} + + + Wallet Authentication + + Enter your wallet address to authenticate and test streaming + functionality + + + +
+ setWalletAddress(e.target.value)} + className="flex-1" + /> + +
+
+
+ + {/* Messages */} + {error && ( + + {error} + + )} + + {success && ( + + {success} + + )} + + + + Stream Management + Playback + Video Player + Live Streams + Status + + + {/* Stream Management Tab */} + + + + Create Stream + + Create a new stream for your wallet address + + + +
+ + + setStreamForm(prev => ({ ...prev, title: e.target.value })) + } + /> +
+ +
+ +