The Problem
When I first launched the site, all resources—HTML, CSS, JS, and MP3 files—were served from a single server. While this worked fine for a small number of users, it quickly became a bottleneck as traffic grew. Streaming large MP3 files consumed significant bandwidth, and I risked exceeding my server's data limits, leading to slowdowns or even downtime.
To solve this, I needed a way to:
- Offload the heavy lifting of serving MP3 files.
- Ensure fast delivery of static assets (HTML, CSS, JS).
- Scale seamlessly to handle thousands of concurrent streams.
Here’s how I tackled the problem:
- Separate Hosting for MP3 Files
I moved all MP3 files to a dedicated VPS running NGINX, optimized for serving static files. This VPS handles only MP3 streaming, freeing up my main server to focus on delivering the website itself. NGINX is a beast when it comes to serving static content efficiently, and it supports features like range requests (more on that later). - CDN for Static Assets
To ensure the website loads as quickly as possible, I integrated a CDN (Content Delivery Network) for all static assets. This means that HTML, CSS, JS, and images are served from edge servers closest to the user, reducing latency and improving load times. - Adaptive Streaming Logic
I wrote a custom PHP script to handle MP3 streaming from the VPS. This script supports range requests, which are essential for seamless streaming. It also includes adaptive buffering to optimize the user experience, especially for users with slower connections.
This setup directly improves the user experience in several ways:
- Faster Streaming: By offloading MP3 delivery to a dedicated VPS, users experience minimal buffering and faster start times.
- Scalability: The hybrid setup ensures that the site can handle thousands of concurrent streams without breaking a sweat.
- Reliability: Even during traffic spikes, the site remains responsive because the main server isn’t bogged down by MP3 delivery.
- Global Reach: The CDN ensures that users worldwide get the fastest possible load times for the website itself.
For the tech nerds out there (like me!), here’s a breakdown of the key components:
1. MP3 Streaming Script
The heart of the system is a PHP script that fetches MP3 files from the VPS and streams them to the user.
Here’s how it works:
- Range Requests: The script supports HTTP range requests, allowing users to skip to different parts of the audio file without downloading the entire file.
- Adaptive Buffering: The script starts with a smaller buffer (32KB) to ensure quick playback initiation. Once enough data is streamed, it switches to a larger buffer (128KB) for smoother playback.
- CORS Headers: To ensure security, the script includes CORS headers that restrict access to the MP3 files to requests originating from my site.
Code: Select all
<?php
// Get and sanitize the file parameter
$file = isset($_GET['file']) ? basename($_GET['file']) : '';
if (empty($file)) {
header('HTTP/1.0 400 Bad Request');
exit('No file specified.');
}
// Define the base URL and construct full URL
$vpsBaseUrl = 'https://[VPS ADDRESS]/mp3s/';
$fileUrl = $vpsBaseUrl . rawurlencode($file);
// Function to get file size with error handling
function getRemoteFileSize($url) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_NOBODY => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_TIMEOUT => 10, // Increased timeout for slower connections
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentLength = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($ch);
if ($httpCode == 200 && $contentLength > 0) {
return (int)$contentLength;
}
return false;
}
// Get file size and handle errors
$filesize = getRemoteFileSize($fileUrl);
if ($filesize === false) {
header('HTTP/1.0 404 Not Found');
exit('File not found or size could not be determined.');
}
// Set CORS headers
header('Access-Control-Allow-Origin: https://v2melody.com');
header('Access-Control-Allow-Methods: GET, HEAD, OPTIONS');
header('Access-Control-Allow-Headers: Range');
// Handle OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(0);
}
// Initialize streaming variables
$start = 0;
$end = $filesize - 1;
$length = $filesize;
// Determine if this is an initial request or a range request
$isInitialRequest = !isset($_SERVER['HTTP_RANGE']);
// Handle range requests
if (!$isInitialRequest) {
$range = sscanf($_SERVER['HTTP_RANGE'], 'bytes=%d-%d');
$start = (int)$range[0];
$end = (isset($range[1]) && $range[1] > 0) ? (int)$range[1] : $filesize - 1;
$length = $end - $start + 1;
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $start-$end/$filesize");
} else {
header('HTTP/1.1 200 OK');
}
// Set streaming headers
header('Content-Type: audio/mpeg');
header('Accept-Ranges: bytes');
header("Content-Length: $length");
// Adjust cache headers based on request type
if ($isInitialRequest) {
header('Cache-Control: no-cache, must-revalidate');
} else {
header('Cache-Control: max-age=2592000, public');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2592000) . ' GMT');
}
// Initialize adaptive buffer tracking
$bytesStreamed = 0;
$initialChunkSize = 131072; // 128KB for quick start, increased for better initial performance
$normalChunkSize = 262144; // 256KB for normal streaming
$currentChunkSize = $initialChunkSize;
$adaptiveThreshold = 524288; // 512KB threshold for switching to larger chunks
// Stream the file
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $fileUrl,
CURLOPT_RANGE => "$start-$end",
CURLOPT_RETURNTRANSFER => false,
CURLOPT_WRITEFUNCTION => function($ch, $data) use (&$bytesStreamed, &$currentChunkSize, $initialChunkSize, $normalChunkSize, $adaptiveThreshold) {
$bytesStreamed += strlen($data);
// Write the current chunk
echo $data;
// Adjust buffer size based on amount streamed
if ($bytesStreamed >= $adaptiveThreshold && $currentChunkSize == $initialChunkSize) {
$currentChunkSize = $normalChunkSize;
curl_setopt($ch, CURLOPT_BUFFERSIZE, $currentChunkSize);
}
// Flush frequently during initial buffering
if ($bytesStreamed < $adaptiveThreshold) {
flush(); // Flush after each chunk during initial load
} else {
// Flush less frequently once we're streaming normally
if ($bytesStreamed % ($normalChunkSize * 2) == 0) {
flush();
}
}
return strlen($data);
},
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_BUFFERSIZE => $currentChunkSize,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 300,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
CURLOPT_FORBID_REUSE => false,
CURLOPT_FRESH_CONNECT => false,
CURLOPT_LOW_SPEED_LIMIT => 1, // Consider setting a low speed limit to detect slow connections
CURLOPT_LOW_SPEED_TIME => 20, // And a time threshold for this
]);
// Start streaming with an initial small buffer to ensure quick response
curl_setopt($ch, CURLOPT_BUFFERSIZE, $initialChunkSize);
$success = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$success || ($httpCode != 200 && $httpCode != 206)) {
header('HTTP/1.0 500 Internal Server Error');
error_log("Failed to stream file: " . $fileUrl . ", HTTP Code: " . $httpCode);
exit('Error streaming file from server.');
}
?>
2. NGINX Configuration on the VPS
On the VPS, I configured NGINX to serve MP3 files efficiently. Key optimizations include:
- Cache Control Headers: Ensures that MP3 files are cached by the user’s browser, reducing repeated downloads.
- HTTP/2 Support: Improves performance by allowing multiplexed requests over a single connection.
- Adaptive Bitrate Streaming: Dynamically adjusting audio quality based on the user’s connection speed.
The CDN caches all static assets (HTML, CSS, JS, images) and serves them from edge servers. This reduces the load on the main server and ensures fast delivery worldwide.
Performance Metrics
Since implementing this setup:
- Stream Start Time: Reduced from ~2.5 seconds to under 1 second.
- Concurrent Streams: The system now handles 10,000+ concurrent streams without breaking a sweat.
- Server Load: The main server’s CPU and bandwidth usage dropped by over 60%.
I’m always looking for ways to optimize further. Some ideas I’m exploring:
- Edge Caching for MP3s: Using a CDN to cache MP3 files closer to users.
- HTTP/3 Support: Taking advantage of QUIC for even faster streaming. (Wasted a lot of time trying to implement this but failed and won't be going down this road for a long time yet!)
- Enable Gzip Compression: Reduces file size for faster transfers.
I want to emphasize that v2melody.com is a self-made site, built entirely from scratch by yours truly. As someone who had to learn everything along the way, I’m proud of what I’ve accomplished, but I’m also aware that there’s always room for improvement. I’m sure there are things that could be done better by those cleverer than I, and I’d love to hear your suggestions!
If you have any ideas for improving my code, optimizing the site further, or just general advice, please don’t hesitate to share. I’m here to learn and grow, and I’m excited to hear what this amazing community has to offer.
Final Thoughts
This hybrid setup has been a game-changer for my site, and I’m thrilled with the results. If you’re running a similar service, I highly recommend considering a similar approach. It’s a bit more expensive than a single-server setup, but the performance gains are well worth it. That said, I did explore other options, like AWS, but the costs quickly spiraled out of control—especially since v2melody.com is completely free to use. It’s no wonder there aren’t many free streaming services out there; the infrastructure costs can be brutal!
This experience has taught me a lot about balancing performance, scalability, and cost. I’m always looking for ways to optimize further, so I’d love to hear your thoughts or suggestions for improvements. Let’s geek out in the comments!
TL;DR:
- Moved MP3 files to a dedicated VPS with NGINX.
- Used a CDN for static assets.
- Wrote a custom PHP script for adaptive MP3 streaming.
- Result: Faster streaming, better scalability, and happier users.