I’m building a video downloader application with Next.js and ytdl-core where users can download YouTube videos. Currently, the server handles the video download, which is inefficient for longer videos due to processing time and potential errors. I want the browser to handle the download directly to improve performance and reliability.
I wanna know if this is possible using Next.js. or if other popular websites have their own private libraries instead of open source library like ytdl-core.
Current Backend Code:
API Endpoint for video info:
export async function POST(req: NextRequest) {
const body = await req.json();
const url = body.url;
if (!ytdl.validateURL(url)) {
return new NextResponse(
JSON.stringify({
error: 'Invalid URL provided. Please provide a valid YouTube URL.',
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
try {
const info = await ytdl.getInfo(url);
const formats = ytdl.filterFormats(info.formats, 'audioandvideo');
return new NextResponse(
JSON.stringify({ videoDetails: info.videoDetails, formats }),
{
headers: { 'Content-Type': 'application/json' },
}
);
} catch (error) {
return new NextResponse(
JSON.stringify({ error: 'Failed to fetch video information' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
API Endpoint for downloading:
async function streamToBuffer(stream: Readable): Promise<Buffer> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
export async function POST(req: NextRequest) {
const body = await req.json();
const url: string = body.url;
const itag: number = parseInt(body.itag, 10);
if (!ytdl.validateURL(url)) {
return new NextResponse(
JSON.stringify({
error: 'Invalid URL provided. Please provide a valid YouTube URL.',
}),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
);
}
try {
const info = await ytdl.getInfo(url);
const format = info.formats.find((f) => f.itag === itag);
if (!format) {
return new NextResponse(
JSON.stringify({ error: 'Format not available' }),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
}
);
}
const videoStream = ytdl.downloadFromInfo(info, { format });
const videoBuffer = await streamToBuffer(videoStream);
const headers = new Headers({
'Content-Disposition': `attachment; filename="${info.videoDetails.title}.${format.container}"`,
'Content-Type': 'application/octet-stream', // or the specific media type
'Content-Length': videoBuffer.length.toString(),
});
return new NextResponse(videoBuffer, { headers });
} catch (error) {
return new NextResponse(
JSON.stringify({ error: 'Failed to download video' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
);
}
}
Current Frontend Code:
This function is to get the video information to display the title and the thumbnail in the ui:
const getVideoInformation = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/video-info', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: videoUrl }),
});
if (response.ok) {
const data = await response.json();
console.log(data);
setVideoDetails({
title: data.videoDetails.title,
thumbnail:
data.videoDetails.thumbnails[
data.videoDetails.thumbnails.length - 1
].url,
duration: formatDuration(data.videoDetails.lengthSeconds),
});
setAvailableFormats(
data.formats.map((format: any) => ({
itag: format.itag,
quality: format.qualityLabel,
container: format.container,
hasAudio: format.hasAudio,
hasVideo: format.hasVideo,
audioQuality: format.audioQuality,
url: format.url,
mimeType: format.mimeType,
}))
);
} else {
const errorResponse = await response.json();
console.error(
'Failed to fetch video information:',
errorResponse.error
);
toast.error(errorResponse.error || 'An unexpected error occurred');
}
} catch (error) {
console.error(error);
toast.error(
'Failed to connect to the service. Please check your network.'
);
} finally {
setIsLoading(false);
}
};
And this function is to download the video after selecting the desired format:
const downloadVideo = async () => {
if (!videoUrl) return;
if (!selectedItag) {
setIsFormatSelectedError(true);
toast.error('Please select a format to download');
return;
}
try {
setIsDownloading(true);
handleVideoProgress();
const response = await fetch('/api/download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: videoUrl, itag: selectedItag }),
});
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${videoDetails?.title}.${
availableFormats.find((format) => format.itag === selectedItag)
?.container
}`;
a.click();
window.URL.revokeObjectURL(url);
setProgressValue(100);
} else {
const errorResponse = await response.json(); // Parse the JSON error response
console.error('Failed to download video:', errorResponse.error);
toast.error(errorResponse.error || 'An unexpected error occurred');
}
} catch (error) {
console.error('Error downloading video:', error);
toast.error('Failed to download the video. Please try again.');
} finally {
setIsDownloading(false);
}
};
I attempted to use an anchor <a> tag with the href set to the video URL and the download attribute, but it only plays the video instead of prompting for download.
Thank you in advance!