import { NextRequest, NextResponse } from "next/server"; import { getDatabase } from "@/db"; import fs from "fs"; import path from "path"; export async function OPTIONS( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Range', 'Access-Control-Max-Age': '86400', }, }); } export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const db = getDatabase(); const searchParams = request.nextUrl.searchParams; const forceTranscode = searchParams.get('transcode') === 'true'; try { const videoId = parseInt(id); const video = db.prepare("SELECT * FROM media WHERE id = ? AND type = 'video'").get(videoId) as { path: string, codec_info: string, duration: number } | undefined; if (!video) { return NextResponse.json({ error: "Video not found" }, { status: 404 }); } // Parse codec info to determine if transcoding is needed let codecInfo = { needsTranscoding: false, duration: 0, codec: '', container: '' }; try { codecInfo = JSON.parse(video.codec_info || '{}'); } catch { // Fallback if codec info is invalid } // H.264 Direct Streaming Priority: Override database flag for H.264 content // According to memory: H.264-encoded content should attempt direct streaming first const isH264 = codecInfo.codec && ['h264', 'avc1', 'avc'].includes(codecInfo.codec.toLowerCase()); const shouldAttemptDirect = isH264 && !forceTranscode; // If H.264, bypass the needsTranscoding flag and attempt direct streaming const needsTranscoding = shouldAttemptDirect ? false : (forceTranscode || codecInfo.needsTranscoding || false); console.log(`[STREAM] Video ID: ${id}, Path: ${video.path}, Codec: ${codecInfo.codec}, Container: ${codecInfo.container}, Force Transcode: ${forceTranscode}, DB Needs Transcode: ${codecInfo.needsTranscoding}, Is H264: ${isH264}, Final Decision: ${needsTranscoding}`); if (needsTranscoding) { console.log(`[STREAM] Format requires local player for video ID: ${id}`); // TRANSCODING DISABLED: Return local player guidance instead of redirect return NextResponse.json({ error: 'Format not supported in browser', solution: 'local-player', message: 'This video format cannot be played directly in the browser. Please use a local video player.', directStreamUrl: `/api/stream/direct/${id}`, recommendedPlayers: ['vlc', 'iina', 'elmedia', 'potplayer'], action: 'use-local-player', streamInfo: { supportsRangeRequests: true, contentType: getContentType(video.path), authentication: 'none' }, helpUrl: '/help/local-players' }, { status: 415 }); // 415 Unsupported Media Type } const videoPath = video.path; if (!fs.existsSync(videoPath)) { return NextResponse.json({ error: "Video file not found" }, { status: 404 }); } const stat = fs.statSync(videoPath); const fileSize = stat.size; const range = request.headers.get("range"); if (range) { const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunksize = end - start + 1; const file = fs.createReadStream(videoPath, { start, end }); // Parse duration for progress bar let duration = 0; try { const codecInfo = JSON.parse(video.codec_info || '{}'); duration = codecInfo.duration || 0; } catch { duration = 0; } const headers = new Headers({ "Content-Range": `bytes ${start}-${end}/${fileSize}`, "Accept-Ranges": "bytes", "Content-Length": chunksize.toString(), "Content-Type": "video/mp4", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "Range, Content-Type", "X-Content-Duration": duration.toString(), }); return new Response(file as any, { status: 206, headers, }); } else { // Parse duration for progress bar let duration = 0; try { const codecInfo = JSON.parse(video.codec_info || '{}'); duration = codecInfo.duration || 0; } catch { duration = 0; } const headers = new Headers({ "Content-Length": fileSize.toString(), "Content-Type": "video/mp4", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "Range, Content-Type", "X-Content-Duration": duration.toString(), }); const file = fs.createReadStream(videoPath); return new Response(file as any, { status: 200, headers, }); } } catch (error) { console.error("Error streaming video:", error); return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } } // Helper function to determine content type based on file extension function getContentType(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); const contentTypes: Record = { '.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'video/ogg', '.ogv': 'video/ogg', '.m4v': 'video/x-m4v', '.ts': 'video/mp2t', '.m2ts': 'video/mp2t', '.mts': 'video/mp2t', '.mkv': 'video/x-matroska', '.avi': 'video/x-msvideo', '.wmv': 'video/x-ms-wmv', '.flv': 'video/x-flv', '.mov': 'video/quicktime', '.3gp': 'video/3gpp', '.vob': 'video/dvd' }; return contentTypes[ext] || 'video/mp4'; // Default fallback }