$size - 1) $end = $size - 1; $length = $end - $start + 1; $status = 206; } // Set headers if (function_exists('http_response_code')) { http_response_code($status); } else { header($_SERVER["SERVER_PROTOCOL"] . " " . $status); } header("Content-Type: video/mp4"); header("Accept-Ranges: bytes"); header("Content-Length: $length"); header('Content-Disposition: inline; filename="' . basename($remoteFile) . '"'); if ($status === 206) { header("Content-Range: bytes $start-$end/$size"); } // Open a temporary local stream for output buffering // Use ftp_nb_fget to fetch from FTP in chunks and output directly $tmpStream = fopen('php://output', 'wb'); if (!$tmpStream) { ftp_close($ftp); http_response_code(500); echo "Failed to open output stream."; exit; } // ftp_nb_fget doesn't support offset, so we have to download full or partial file manually // We'll download full file into php://output but skip bytes before $start by reading and discarding // Unfortunately, FTP protocol itself doesn't support partial retrieval starting at offset // so we have to download and skip bytes, or use ftp_raw to send REST command (some FTP servers support it) // We'll try REST command ftp_raw($ftp, "TYPE I"); // Switch to binary mode // Attempt REST command for offset $restResp = ftp_raw($ftp, "REST $start"); if (strpos(implode("\n", $restResp), '350') === false) { // REST not supported or failed - fallback to downloading entire file and skipping in PHP $start = 0; // ignore range start - will download entire file $length = $size; $status = 200; } $ret = ftp_nb_fget($ftp, $tmpStream, $remoteFile, FTP_BINARY); $bytesSent = 0; while ($ret == FTP_MOREDATA) { flush(); $ret = ftp_nb_continue($ftp); } fclose($tmpStream); ftp_close($ftp); } // If cached file does NOT exist, do caching download with progress page and play button if (!file_exists($cacheFile)) { // Show progress bar page with play button, video, speed and ETR display and direct play button echo 'Downloading...'; echo "

Downloading and caching video file...

"; echo '
'; echo '
0%
'; echo '
Speed: 0 KB/s | Estimated time remaining: calculating...
'; echo ''; echo ''; flush(); // Connect FTP $ftp = ftp_connect($server); if (!$ftp || !ftp_login($ftp, $username, $password)) { http_response_code(500); echo "

FTP connection failed.

"; exit; } ftp_pasv($ftp, true); // Get file size for progress calculation $ftpSize = ftp_size($ftp, $relativePath); if ($ftpSize <= 0) { echo "

Failed to get remote file size.

"; ftp_close($ftp); exit; } $tmpFile = $cacheFile . '.part'; $fp = fopen($tmpFile, 'wb'); if (!$fp) { echo "

Failed to open local file for writing.

"; ftp_close($ftp); exit; } // Use ftp_nb_fget for non-blocking download to track progress $ret = ftp_nb_fget($ftp, $fp, $relativePath, FTP_BINARY); $downloaded = 0; while ($ret == FTP_MOREDATA) { clearstatcache(true, $tmpFile); $downloaded = filesize($tmpFile); $pct = min(100, round(($downloaded / $ftpSize) * 100)); echo ""; flush(); $ret = ftp_nb_continue($ftp); usleep(50000); // slight delay to reduce CPU load } if ($ret != FTP_FINISHED) { echo "

FTP download failed.

"; fclose($fp); ftp_close($ftp); unlink($tmpFile); exit; } fclose($fp); ftp_close($ftp); rename($tmpFile, $cacheFile); // Download complete, set progress bar to 100% echo ""; echo "

Download complete! Preparing video for streaming...

"; echo ''; flush(); // Give user 1 second to see 100% before redirecting to self without caching phase sleep(1); // Redirect to self to serve cached video directly header("Location: " . strtok($_SERVER["REQUEST_URI"], '?') . '?' . $_SERVER['QUERY_STRING']); exit; } // Serve cached file with Range support $filesize = filesize($cacheFile); $start = 0; $end = $filesize - 1; $length = $filesize; $status = 200; if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) { $start = intval($matches[1]); if (!empty($matches[2])) { $end = intval($matches[2]); } if ($end > $filesize - 1) { $end = $filesize - 1; } $length = $end - $start + 1; $status = 206; } $fp = fopen($cacheFile, 'rb'); if ($fp === false) { http_response_code(500); echo "Failed to open cached file."; exit; } fseek($fp, $start); if (function_exists('http_response_code')) { http_response_code($status); } else { header($_SERVER["SERVER_PROTOCOL"] . " " . $status); } header("Content-Type: video/mp4"); header("Content-Length: $length"); header("Accept-Ranges: bytes"); header('Content-Disposition: inline; filename="' . basename($relativePath) . '"'); if ($status === 206) { header("Content-Range: bytes $start-$end/$filesize"); } // Stream the file $buffer = 8192; $bytesSent = 0; while (!feof($fp) && $bytesSent < $length) { $read = min($buffer, $length - $bytesSent); $data = fread($fp, $read); if ($data === false) { break; } echo $data; flush(); $bytesSent += strlen($data); } fclose($fp); exit;