// /api/videos/hls/[...path]/route.ts // This endpoint serves HLS playlists and segments from the HLS directory import { NextResponse } from 'next/server'; import * as fs from 'fs'; import * as path from 'path'; import { checkVideoAccess, getViewer } from '@/lib/video-access'; // Path to HLS directory (should match UPLOADS_DIR/hls) const HLS_DIR = path.join(process.env.UPLOADS_DIR || '/uploads', 'hls'); export async function GET( request: Request, context: { params: Promise<{ path?: string[] }> } ) { try { const params = await context.params; const pathSegments = params?.path || []; if (pathSegments.length === 0) { return NextResponse.json( { error: 'Invalid HLS request' }, { status: 400 } ); } // Signed-in viewers only, and only for videos they may watch. // The first path segment is the video id. const viewer = await getViewer(); if (!viewer) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const access = await checkVideoAccess(viewer, pathSegments[0]); if (!access.ok) { return NextResponse.json({ error: access.error }, { status: access.status }); } // Construct the file path and validate it const requestedPath = path.join(HLS_DIR, ...pathSegments); // Security: prevent directory traversal attacks if (!requestedPath.startsWith(HLS_DIR + path.sep)) { return NextResponse.json( { error: 'Invalid request' }, { status: 403 } ); } // Check if file exists if (!fs.existsSync(requestedPath)) { console.log(`[HLS] File not found: ${requestedPath}`); return NextResponse.json( { error: 'Not found' }, { status: 404 } ); } // Read the file const fileContent = await fs.promises.readFile(requestedPath); // Determine content type based on file extension let contentType = 'application/octet-stream'; if (requestedPath.endsWith('.m3u8')) { contentType = 'application/vnd.apple.mpegurl'; } else if (requestedPath.endsWith('.ts')) { contentType = 'video/mp2t'; } else if (requestedPath.endsWith('.mp4')) { contentType = 'video/mp4'; } // Return the file with appropriate headers return new NextResponse(fileContent, { status: 200, headers: { 'Content-Type': contentType, 'Cache-Control': 'private, max-age=3600', // Viewer's browser only, 1 hour 'Accept-Ranges': 'bytes', }, }); } catch (error) { console.error('[HLS] Error serving HLS file:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } } // HEAD request support for playlist validation export async function HEAD( request: Request, context: { params: Promise<{ path?: string[] }> } ) { try { const params = await context.params; const pathSegments = params?.path || []; if (pathSegments.length === 0) { return new NextResponse(null, { status: 400 }); } const viewer = await getViewer(); if (!viewer) return new NextResponse(null, { status: 401 }); const access = await checkVideoAccess(viewer, pathSegments[0]); if (!access.ok) return new NextResponse(null, { status: access.status }); const requestedPath = path.join(HLS_DIR, ...pathSegments); // Security: prevent directory traversal attacks if (!requestedPath.startsWith(HLS_DIR + path.sep)) { return new NextResponse(null, { status: 403 }); } // Check if file exists if (!fs.existsSync(requestedPath)) { return new NextResponse(null, { status: 404 }); } return new NextResponse(null, { status: 200 }); } catch (error) { console.error('[HLS] Error in HEAD request:', error); return new NextResponse(null, { status: 500 }); } }