357af53f2d
Allow direct stream (+seek) via ?direct=1 querystring Otherwise download vid to cache folder
360 lines
12 KiB
PHP
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;
|