Files
Alcea 357af53f2d Add files via upload
Allow direct stream (+seek) via ?direct=1 querystring

Otherwise download vid to cache folder
2025-09-05 20:33:19 +02:00

360 lines
12 KiB
PHP

<?php
set_time_limit(0); // prevent timeout for big files
@ob_implicit_flush(true);
@ob_end_flush();
$cacheDir = __DIR__ . '/cache';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
if (!isset($_GET['path'])) {
http_response_code(400);
echo "Missing 'path' parameter.";
exit;
}
$relativePath = ltrim($_GET['path'], '/');
$ext = strtolower(pathinfo($relativePath, PATHINFO_EXTENSION));
if ($ext !== 'mp4') {
http_response_code(403);
echo "Only .mp4 files are allowed.";
exit;
}
// FTP info
$server = isset($_GET['server']) ? $_GET['server'] : '192.168.0.1';
$username = isset($_GET['username']) ? $_GET['username'] : 'anonymous';
$password = isset($_GET['password']) ? $_GET['password'] : '';
$anonymous = isset($_GET['anonymous']) && $_GET['anonymous'] == '1';
if ($anonymous) {
$username = 'anonymous';
$password = 'anonymous@domain.com';
}
$cacheFile = $cacheDir . '/' . md5($server . $relativePath) . '.mp4';
// Handle direct play request: delete cache and stream from FTP directly
if (isset($_GET['direct']) && $_GET['direct'] == '1') {
if (file_exists($cacheFile)) {
@unlink($cacheFile);
}
// Stream directly from FTP without caching
streamFTPDirect($server, $username, $password, $relativePath);
exit;
}
// Function to stream file directly from FTP without caching
function streamFTPDirect($server, $username, $password, $remoteFile) {
// 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 size for content length
$size = ftp_size($ftp, $remoteFile);
if ($size <= 0) {
ftp_close($ftp);
http_response_code(404);
echo "Remote file not found or size unknown.";
exit;
}
// Handle Range header for partial content
$start = 0;
$end = $size - 1;
$length = $size;
$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 > $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 '<!DOCTYPE html><html><head><title>Downloading...</title><style>
#progress-container { width: 100%; background: #ddd; margin-bottom: 10px; }
#progress-bar { width: 0; height: 30px; background: #4caf50; text-align: center; color: white; }
body { font-family: Arial, sans-serif; padding: 20px; }
#video-player { width: 100%; max-width: 720px; display: none; margin-top: 20px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; margin-right: 10px; }
#stats { margin-top: 8px; font-size: 14px; color: #333; }
</style></head><body>';
echo "<h2>Downloading and caching video file...</h2>";
echo '<div style="margin-bottom:10px;">
<button id="direct-play-btn">Direct Play</button>
<button id="play-btn" disabled>Play Now</button>
</div>';
echo '<div id="progress-container"><div id="progress-bar">0%</div></div>';
echo '<div id="stats">Speed: 0 KB/s | Estimated time remaining: calculating...</div>';
echo '<video id="video-player" controls preload="auto"></video>';
echo '<script>
const progressBar = document.getElementById("progress-bar");
const playBtn = document.getElementById("play-btn");
const directBtn = document.getElementById("direct-play-btn");
const videoPlayer = document.getElementById("video-player");
const statsDiv = document.getElementById("stats");
let lastDownloaded = 0;
let lastTimestamp = Date.now();
function formatBytes(bytes) {
if (bytes < 1024) return bytes + " B/s";
else if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + " KB/s";
else return (bytes/(1024*1024)).toFixed(2) + " MB/s";
}
function formatTime(seconds) {
if (seconds === Infinity || isNaN(seconds)) return "calculating...";
let h = Math.floor(seconds / 3600);
let m = Math.floor((seconds % 3600) / 60);
let s = Math.floor(seconds % 60);
return (h > 0 ? h + "h " : "") + (m > 0 ? m + "m " : "") + s + "s";
}
function updateProgress(pct, downloadedBytes, totalBytes) {
progressBar.style.width = pct + "%";
progressBar.textContent = pct + "%";
const now = Date.now();
const timeDiff = (now - lastTimestamp) / 1000; // seconds
const bytesDiff = downloadedBytes - lastDownloaded;
if (timeDiff > 0) {
const speed = bytesDiff / timeDiff;
const bytesLeft = totalBytes - downloadedBytes;
const eta = bytesLeft / speed;
statsDiv.textContent = "Speed: " + formatBytes(speed) + " | Estimated time remaining: " + formatTime(eta);
}
lastDownloaded = downloadedBytes;
lastTimestamp = now;
if (pct > 0) {
playBtn.disabled = false; // enable play button once some progress
}
if (pct === 100) {
playBtn.textContent = "Play Full Video";
videoPlayer.src = "' . htmlspecialchars(basename($_SERVER['PHP_SELF'])) . '?path=' . rawurlencode($relativePath) . '&server=' . rawurlencode($server) . '&username=' . rawurlencode($username) . '&password=' . rawurlencode($password) . '";
videoPlayer.style.display = "block";
videoPlayer.play();
playBtn.style.display = "none"; // hide play button after auto-play
statsDiv.textContent = "Download complete.";
}
}
playBtn.addEventListener("click", () => {
videoPlayer.src = "' . htmlspecialchars(basename($_SERVER['PHP_SELF'])) . '?path=' . rawurlencode($relativePath) . '&server=' . rawurlencode($server) . '&username=' . rawurlencode($username) . '&password=' . rawurlencode($password) . '";
videoPlayer.style.display = "block";
videoPlayer.play();
});
directBtn.addEventListener("click", () => {
// Redirect with ?direct=1 query param added
const url = new URL(window.location.href);
url.searchParams.set("direct", "1");
window.location.href = url.toString();
});
</script>';
flush();
// Connect FTP
$ftp = ftp_connect($server);
if (!$ftp || !ftp_login($ftp, $username, $password)) {
http_response_code(500);
echo "<p style='color:red;'>FTP connection failed.</p></body></html>";
exit;
}
ftp_pasv($ftp, true);
// Get file size for progress calculation
$ftpSize = ftp_size($ftp, $relativePath);
if ($ftpSize <= 0) {
echo "<p style='color:red;'>Failed to get remote file size.</p></body></html>";
ftp_close($ftp);
exit;
}
$tmpFile = $cacheFile . '.part';
$fp = fopen($tmpFile, 'wb');
if (!$fp) {
echo "<p style='color:red;'>Failed to open local file for writing.</p></body></html>";
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 "<script>updateProgress($pct, $downloaded, $ftpSize);</script>";
flush();
$ret = ftp_nb_continue($ftp);
usleep(50000); // slight delay to reduce CPU load
}
if ($ret != FTP_FINISHED) {
echo "<p style='color:red;'>FTP download failed.</p></body></html>";
fclose($fp);
ftp_close($ftp);
unlink($tmpFile);
exit;
}
fclose($fp);
ftp_close($ftp);
rename($tmpFile, $cacheFile);
// Download complete, set progress bar to 100%
echo "<script>updateProgress(100, $ftpSize, $ftpSize);</script>";
echo "<p>Download complete! Preparing video for streaming...</p>";
echo '</body></html>';
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;