170 lines
5.9 KiB
TypeScript
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
|
|
} |