import { createReadStream } from "node:fs"; import { stat } from "node:fs/promises"; import { Readable } from "node:stream"; import { NextResponse } from "next/server"; import { db } from "@/lib/db"; import { getVideoPath } from "@/lib/storage"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; function parseRangeHeader(rangeHeader: string, size: number) { const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim()); if (!match) { return null; } const start = match[1] ? Number(match[1]) : 0; const end = match[2] ? Number(match[2]) : size - 1; if (!Number.isInteger(start) || !Number.isInteger(end)) { return null; } if (start < 0 || end < start || end >= size) { return null; } return { start, end }; } function baseHeaders(mimeType: string, size: number): HeadersInit { return { "Content-Type": mimeType, "Accept-Ranges": "bytes", "Content-Length": String(size), "Cache-Control": "public, max-age=0, must-revalidate", }; } export async function HEAD( _request: Request, context: RouteContext<"/api/media/videos/[videoId]">, ) { const { videoId } = await context.params; const video = await db.video.findUnique({ where: { id: videoId, }, select: { videoFilename: true, videoMimeType: true, }, }); if (!video) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } try { const fileStats = await stat(getVideoPath(video.videoFilename)); return new NextResponse(null, { status: 200, headers: baseHeaders(video.videoMimeType, fileStats.size), }); } catch { return NextResponse.json({ error: "Not found" }, { status: 404 }); } } export async function GET( request: Request, context: RouteContext<"/api/media/videos/[videoId]">, ) { const { videoId } = await context.params; const video = await db.video.findUnique({ where: { id: videoId, }, select: { videoFilename: true, videoMimeType: true, }, }); if (!video) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } const filePath = getVideoPath(video.videoFilename); let fileStats; try { fileStats = await stat(filePath); } catch { return NextResponse.json({ error: "Not found" }, { status: 404 }); } const rangeHeader = request.headers.get("range"); if (!rangeHeader) { const stream = createReadStream(filePath); return new Response(Readable.toWeb(stream) as ReadableStream, { status: 200, headers: baseHeaders(video.videoMimeType, fileStats.size), }); } const range = parseRangeHeader(rangeHeader, fileStats.size); if (!range) { return new NextResponse(null, { status: 416, headers: { "Content-Range": `bytes */${fileStats.size}`, }, }); } const stream = createReadStream(filePath, { start: range.start, end: range.end, }); const chunkSize = range.end - range.start + 1; return new Response(Readable.toWeb(stream) as ReadableStream, { status: 206, headers: { "Content-Type": video.videoMimeType, "Accept-Ranges": "bytes", "Content-Length": String(chunkSize), "Content-Range": `bytes ${range.start}-${range.end}/${fileStats.size}`, "Cache-Control": "public, max-age=0, must-revalidate", }, }); }