nextav/src/app/api/stream/[id]/route.ts

170 lines
5.9 KiB
TypeScript

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<string, string> = {
'.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
}