- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
749 lines
24 KiB
PHP
749 lines
24 KiB
PHP
<?php
|
|
/*******************************************************************************************************************
|
|
| Software Name : EasyStream
|
|
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
|
| Software Author : (c) Sami Ahmed
|
|
|*******************************************************************************************************************
|
|
|
|
|
|*******************************************************************************************************************
|
|
| This source file is subject to the EasyStream Proprietary License Agreement.
|
|
|
|
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
|
|*******************************************************************************************************************
|
|
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
|
|*******************************************************************************************************************/
|
|
|
|
defined('_ISVALID') or header('Location: /error');
|
|
|
|
/**
|
|
* Video Streaming and Delivery System
|
|
*/
|
|
class VStreaming
|
|
{
|
|
private $logger;
|
|
private $db;
|
|
private $baseUrl;
|
|
private $streamingPath;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->logger = VLogger::getInstance();
|
|
$this->db = VDatabase::getInstance();
|
|
$this->baseUrl = $this->getBaseUrl();
|
|
$this->streamingPath = _FPATH . 'f_data/processed/';
|
|
}
|
|
|
|
/**
|
|
* Get video streaming URLs for a video
|
|
* @param string $videoKey Video key
|
|
* @param string $userId User ID (for access control)
|
|
* @return array Streaming URLs and metadata
|
|
*/
|
|
public function getVideoStreams($videoKey, $userId = null)
|
|
{
|
|
try {
|
|
// Get video information
|
|
$video = $this->getVideoInfo($videoKey);
|
|
|
|
if (!$video) {
|
|
throw new Exception('Video not found');
|
|
}
|
|
|
|
// Check access permissions
|
|
if (!$this->checkVideoAccess($video, $userId)) {
|
|
throw new Exception('Access denied');
|
|
}
|
|
|
|
// Get available formats
|
|
$availableFormats = json_decode($video['available_formats'] ?? '[]', true);
|
|
$streams = [];
|
|
|
|
// Progressive download streams
|
|
foreach ($availableFormats as $format) {
|
|
if ($format === 'hls' || $format === 'thumbnails' || $format === 'preview') {
|
|
continue;
|
|
}
|
|
|
|
$streamUrl = $this->getProgressiveStreamUrl($videoKey, $format);
|
|
if ($streamUrl) {
|
|
$streams['progressive'][$format] = [
|
|
'url' => $streamUrl,
|
|
'format' => $format,
|
|
'type' => 'video/mp4',
|
|
'quality' => $this->getQualityLabel($format)
|
|
];
|
|
}
|
|
}
|
|
|
|
// HLS adaptive streaming
|
|
if ($video['hls_available']) {
|
|
$hlsUrl = $this->getHLSStreamUrl($videoKey);
|
|
if ($hlsUrl) {
|
|
$streams['hls'] = [
|
|
'url' => $hlsUrl,
|
|
'type' => 'application/x-mpegURL',
|
|
'adaptive' => true
|
|
];
|
|
}
|
|
}
|
|
|
|
// Thumbnails
|
|
$thumbnails = $this->getThumbnailUrls($videoKey);
|
|
|
|
// Track view
|
|
$this->trackView($videoKey, $userId);
|
|
|
|
return [
|
|
'success' => true,
|
|
'video_key' => $videoKey,
|
|
'streams' => $streams,
|
|
'thumbnails' => $thumbnails,
|
|
'duration' => (float)$video['duration'],
|
|
'title' => $video['file_title'],
|
|
'description' => $video['file_description']
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to get video streams', [
|
|
'video_key' => $videoKey,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stream video file with range support
|
|
* @param string $videoKey Video key
|
|
* @param string $format Video format
|
|
* @param string $userId User ID (for access control)
|
|
*/
|
|
public function streamVideo($videoKey, $format, $userId = null)
|
|
{
|
|
try {
|
|
// Get video information
|
|
$video = $this->getVideoInfo($videoKey);
|
|
|
|
if (!$video) {
|
|
http_response_code(404);
|
|
exit('Video not found');
|
|
}
|
|
|
|
// Check access permissions
|
|
if (!$this->checkVideoAccess($video, $userId)) {
|
|
http_response_code(403);
|
|
exit('Access denied');
|
|
}
|
|
|
|
// Get file path
|
|
$filePath = $this->getVideoFilePath($videoKey, $format);
|
|
|
|
if (!$filePath || !file_exists($filePath)) {
|
|
http_response_code(404);
|
|
exit('Video file not found');
|
|
}
|
|
|
|
// Track analytics
|
|
$this->trackStreamRequest($videoKey, $format, $userId);
|
|
|
|
// Stream the file with range support
|
|
$this->streamFileWithRanges($filePath, $this->getMimeType($format));
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Video streaming failed', [
|
|
'video_key' => $videoKey,
|
|
'format' => $format,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
http_response_code(500);
|
|
exit('Streaming error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stream HLS playlist or segment
|
|
* @param string $videoKey Video key
|
|
* @param string $path HLS path (playlist or segment)
|
|
* @param string $userId User ID (for access control)
|
|
*/
|
|
public function streamHLS($videoKey, $path, $userId = null)
|
|
{
|
|
try {
|
|
// Get video information
|
|
$video = $this->getVideoInfo($videoKey);
|
|
|
|
if (!$video || !$video['hls_available']) {
|
|
http_response_code(404);
|
|
exit('HLS not available');
|
|
}
|
|
|
|
// Check access permissions
|
|
if (!$this->checkVideoAccess($video, $userId)) {
|
|
http_response_code(403);
|
|
exit('Access denied');
|
|
}
|
|
|
|
$sanitizedPath = $this->sanitizeHLSPath($path);
|
|
if (!$sanitizedPath) {
|
|
http_response_code(400);
|
|
exit('Invalid HLS path');
|
|
}
|
|
|
|
$hlsRoot = $this->streamingPath . $videoKey . '/hls/';
|
|
$hlsPath = $hlsRoot . $sanitizedPath;
|
|
|
|
if (!file_exists($hlsPath) || !is_file($hlsPath)) {
|
|
http_response_code(404);
|
|
exit('HLS file not found');
|
|
}
|
|
|
|
$realRoot = realpath($hlsRoot);
|
|
$realFile = realpath($hlsPath);
|
|
if ($realRoot === false || $realFile === false || strpos($realFile, $realRoot) !== 0) {
|
|
http_response_code(403);
|
|
exit('Invalid HLS path');
|
|
}
|
|
|
|
$extension = strtolower(pathinfo($sanitizedPath, PATHINFO_EXTENSION));
|
|
|
|
if ($extension === 'm3u8') {
|
|
$playlist = file_get_contents($hlsPath);
|
|
if ($playlist === false) {
|
|
throw new Exception('Failed to read HLS playlist');
|
|
}
|
|
|
|
$playlist = $this->rewritePlaylistUrls($playlist, $videoKey, $sanitizedPath);
|
|
|
|
header('Content-Type: application/vnd.apple.mpegurl');
|
|
header('Cache-Control: no-cache, max-age=0');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Content-Length: ' . strlen($playlist));
|
|
|
|
if (basename($sanitizedPath) === 'master.m3u8') {
|
|
$this->trackStreamRequest($videoKey, 'hls_master', $userId);
|
|
}
|
|
|
|
echo $playlist;
|
|
exit;
|
|
}
|
|
|
|
$contentType = 'application/octet-stream';
|
|
if ($extension === 'ts') {
|
|
$contentType = 'video/MP2T';
|
|
} elseif ($extension === 'mp4') {
|
|
$contentType = 'video/mp4';
|
|
}
|
|
|
|
header('Content-Type: ' . $contentType);
|
|
header('Content-Length: ' . filesize($hlsPath));
|
|
header('Cache-Control: max-age=30');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
readfile($hlsPath);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('HLS streaming failed', [
|
|
'video_key' => $videoKey,
|
|
'path' => $path,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
http_response_code(500);
|
|
exit('HLS streaming error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get video analytics data
|
|
* @param string $videoKey Video key
|
|
* @return array Analytics data
|
|
*/
|
|
public function getVideoAnalytics($videoKey)
|
|
{
|
|
try {
|
|
$query = "SELECT
|
|
COUNT(*) as total_views,
|
|
COUNT(DISTINCT user_id) as unique_viewers,
|
|
AVG(watch_duration) as avg_watch_duration,
|
|
SUM(watch_duration) as total_watch_time,
|
|
DATE(created_at) as view_date,
|
|
COUNT(*) as daily_views
|
|
FROM db_video_analytics
|
|
WHERE video_key = ?
|
|
GROUP BY DATE(created_at)
|
|
ORDER BY view_date DESC
|
|
LIMIT 30";
|
|
|
|
$result = $this->db->doQuery($query, [$videoKey]);
|
|
|
|
$analytics = [];
|
|
while ($row = $this->db->doFetch($result)) {
|
|
$analytics[] = $row;
|
|
}
|
|
|
|
// Get overall stats
|
|
$overallQuery = "SELECT
|
|
COUNT(*) as total_views,
|
|
COUNT(DISTINCT user_id) as unique_viewers,
|
|
AVG(watch_duration) as avg_watch_duration,
|
|
SUM(watch_duration) as total_watch_time
|
|
FROM db_video_analytics
|
|
WHERE video_key = ?";
|
|
|
|
$overallResult = $this->db->doQuery($overallQuery, [$videoKey]);
|
|
$overallStats = $this->db->doFetch($overallResult);
|
|
|
|
return [
|
|
'success' => true,
|
|
'video_key' => $videoKey,
|
|
'overall_stats' => $overallStats,
|
|
'daily_analytics' => $analytics
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to get video analytics', [
|
|
'video_key' => $videoKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update watch progress for a user
|
|
* @param string $videoKey Video key
|
|
* @param string $userId User ID
|
|
* @param float $currentTime Current playback time
|
|
* @param float $duration Video duration
|
|
*/
|
|
public function updateWatchProgress($videoKey, $userId, $currentTime, $duration)
|
|
{
|
|
try {
|
|
if (!$userId) {
|
|
return; // Skip for anonymous users
|
|
}
|
|
|
|
$watchData = [
|
|
'user_id' => $userId,
|
|
'video_key' => $videoKey,
|
|
'current_time' => $currentTime,
|
|
'duration' => $duration,
|
|
'watch_percentage' => ($duration > 0) ? ($currentTime / $duration) * 100 : 0,
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
// Check if record exists
|
|
$existingQuery = "SELECT id FROM db_watch_progress WHERE user_id = ? AND video_key = ?";
|
|
$existingResult = $this->db->doQuery($existingQuery, [$userId, $videoKey]);
|
|
$existing = $this->db->doFetch($existingResult);
|
|
|
|
if ($existing) {
|
|
// Update existing record
|
|
$this->db->doUpdate('db_watch_progress', 'id', $watchData, $existing['id']);
|
|
} else {
|
|
// Create new record
|
|
$watchData['created_at'] = date('Y-m-d H:i:s');
|
|
$this->db->doInsert('db_watch_progress', $watchData);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to update watch progress', [
|
|
'video_key' => $videoKey,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user's watch progress for a video
|
|
* @param string $videoKey Video key
|
|
* @param string $userId User ID
|
|
* @return array|null Watch progress data
|
|
*/
|
|
public function getWatchProgress($videoKey, $userId)
|
|
{
|
|
if (!$userId) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$query = "SELECT * FROM db_watch_progress WHERE user_id = ? AND video_key = ?";
|
|
$result = $this->db->doQuery($query, [$userId, $videoKey]);
|
|
|
|
return $this->db->doFetch($result);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to get watch progress', [
|
|
'video_key' => $videoKey,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
|
|
private function getVideoInfo($videoKey)
|
|
{
|
|
$query = "SELECT * FROM db_videofiles WHERE file_key = ? AND file_active = 1";
|
|
$result = $this->db->doQuery($query, [$videoKey]);
|
|
|
|
return $this->db->doFetch($result);
|
|
}
|
|
|
|
private function checkVideoAccess($video, $userId)
|
|
{
|
|
// Check if video is public
|
|
if ($video['privacy'] === 'public') {
|
|
return true;
|
|
}
|
|
|
|
// Check if user owns the video
|
|
if ($userId && $video['usr_id'] == $userId) {
|
|
return true;
|
|
}
|
|
|
|
// Check if video is unlisted and user has direct link
|
|
if ($video['privacy'] === 'unlisted') {
|
|
return true;
|
|
}
|
|
|
|
// Private videos require ownership or special permissions
|
|
if ($video['privacy'] === 'private') {
|
|
return $userId && $video['usr_id'] == $userId;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function getProgressiveStreamUrl($videoKey, $format)
|
|
{
|
|
$filePath = $this->getVideoFilePath($videoKey, $format);
|
|
|
|
if ($filePath && file_exists($filePath)) {
|
|
return $this->baseUrl . '/stream.php?v=' . urlencode($videoKey) . '&f=' . urlencode($format);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getHLSStreamUrl($videoKey)
|
|
{
|
|
$playlistPath = $this->streamingPath . $videoKey . '/hls/master.m3u8';
|
|
|
|
if (file_exists($playlistPath)) {
|
|
return $this->baseUrl . '/stream.php?v=' . urlencode($videoKey) . '&hls=master.m3u8';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getThumbnailUrls($videoKey)
|
|
{
|
|
$thumbnailDir = $this->streamingPath . $videoKey . '/thumbnails/';
|
|
$thumbnails = [];
|
|
|
|
// Main thumbnail
|
|
$mainThumb = $thumbnailDir . $videoKey . '_thumb.jpg';
|
|
if (file_exists($mainThumb)) {
|
|
$thumbnails['main'] = $this->baseUrl . '/f_data/processed/' . $videoKey . '/thumbnails/' . $videoKey . '_thumb.jpg';
|
|
}
|
|
|
|
// Preview thumbnails
|
|
$previewThumbs = [];
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$previewThumb = $thumbnailDir . $videoKey . "_preview_{$i}.jpg";
|
|
if (file_exists($previewThumb)) {
|
|
$previewThumbs[] = $this->baseUrl . '/f_data/processed/' . $videoKey . '/thumbnails/' . $videoKey . "_preview_{$i}.jpg";
|
|
}
|
|
}
|
|
$thumbnails['preview'] = $previewThumbs;
|
|
|
|
return $thumbnails;
|
|
}
|
|
|
|
private function getVideoFilePath($videoKey, $format)
|
|
{
|
|
$safeFormat = $this->sanitizeFormat($format);
|
|
if (!$safeFormat) {
|
|
return null;
|
|
}
|
|
|
|
return $this->streamingPath . $videoKey . '/' . $videoKey . '_' . $safeFormat . '.mp4';
|
|
}
|
|
|
|
private function trackView($videoKey, $userId)
|
|
{
|
|
try {
|
|
$viewData = [
|
|
'video_key' => $videoKey,
|
|
'user_id' => $userId,
|
|
'ip_address' => $this->getRealIpAddress(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->db->doInsert('db_video_views', $viewData);
|
|
|
|
// Update video view count
|
|
$this->db->doQuery("UPDATE db_videofiles SET file_views = file_views + 1 WHERE file_key = ?", [$videoKey]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to track view', [
|
|
'video_key' => $videoKey,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function trackStreamRequest($videoKey, $format, $userId)
|
|
{
|
|
try {
|
|
$analyticsData = [
|
|
'video_key' => $videoKey,
|
|
'user_id' => $userId,
|
|
'format' => $format,
|
|
'ip_address' => $this->getRealIpAddress(),
|
|
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->db->doInsert('db_video_analytics', $analyticsData);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to track stream request', [
|
|
'video_key' => $videoKey,
|
|
'format' => $format,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function streamFileWithRanges($filePath, $contentType)
|
|
{
|
|
$fileSize = filesize($filePath);
|
|
$start = 0;
|
|
$end = $fileSize - 1;
|
|
|
|
// Handle range requests
|
|
if (isset($_SERVER['HTTP_RANGE'])) {
|
|
$range = $_SERVER['HTTP_RANGE'];
|
|
if (preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
|
|
$start = intval($matches[1]);
|
|
if (!empty($matches[2])) {
|
|
$end = intval($matches[2]);
|
|
}
|
|
}
|
|
|
|
http_response_code(206); // Partial Content
|
|
header('Accept-Ranges: bytes');
|
|
header("Content-Range: bytes $start-$end/$fileSize");
|
|
} else {
|
|
http_response_code(200);
|
|
}
|
|
|
|
// Set headers
|
|
header('Content-Type: ' . $contentType);
|
|
header('Content-Length: ' . ($end - $start + 1));
|
|
header('Cache-Control: max-age=3600');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
// Stream the file
|
|
$handle = fopen($filePath, 'rb');
|
|
fseek($handle, $start);
|
|
|
|
$chunkSize = 8192;
|
|
$bytesRemaining = $end - $start + 1;
|
|
|
|
while ($bytesRemaining > 0 && !feof($handle)) {
|
|
$bytesToRead = min($chunkSize, $bytesRemaining);
|
|
echo fread($handle, $bytesToRead);
|
|
$bytesRemaining -= $bytesToRead;
|
|
|
|
if (ob_get_level()) {
|
|
ob_flush();
|
|
}
|
|
flush();
|
|
}
|
|
|
|
fclose($handle);
|
|
}
|
|
|
|
private function getMimeType($format)
|
|
{
|
|
switch (strtolower($format)) {
|
|
case 'mp4':
|
|
case '1080p':
|
|
case '720p':
|
|
case '480p':
|
|
case '360p':
|
|
case '240p':
|
|
return 'video/mp4';
|
|
case 'webm':
|
|
return 'video/webm';
|
|
case 'ogv':
|
|
return 'video/ogg';
|
|
default:
|
|
return 'video/mp4';
|
|
}
|
|
}
|
|
|
|
private function getQualityLabel($format)
|
|
{
|
|
$labels = [
|
|
'1080p' => '1080p HD',
|
|
'720p' => '720p HD',
|
|
'480p' => '480p',
|
|
'360p' => '360p',
|
|
'240p' => '240p'
|
|
];
|
|
|
|
return $labels[$format] ?? $format;
|
|
}
|
|
|
|
private function sanitizeFormat($format)
|
|
{
|
|
$format = trim((string)$format);
|
|
if ($format === '') {
|
|
return null;
|
|
}
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $format)) {
|
|
return null;
|
|
}
|
|
|
|
return $format;
|
|
}
|
|
|
|
private function sanitizeHLSPath($path)
|
|
{
|
|
$path = str_replace('\\', '/', (string)$path);
|
|
$path = trim($path, '/');
|
|
|
|
if ($path === '') {
|
|
return null;
|
|
}
|
|
|
|
$segments = explode('/', $path);
|
|
$cleanSegments = [];
|
|
|
|
foreach ($segments as $segment) {
|
|
if ($segment === '' || $segment === '.' || $segment === '..') {
|
|
return null;
|
|
}
|
|
|
|
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $segment)) {
|
|
return null;
|
|
}
|
|
|
|
$cleanSegments[] = $segment;
|
|
}
|
|
|
|
return implode('/', $cleanSegments);
|
|
}
|
|
|
|
private function rewritePlaylistUrls($playlist, $videoKey, $currentPath)
|
|
{
|
|
$lines = preg_split('/\r\n|\r|\n/', $playlist);
|
|
$baseDir = trim(str_replace('\\', '/', dirname($currentPath)), '/');
|
|
|
|
foreach ($lines as $index => $line) {
|
|
$trimmed = trim($line);
|
|
|
|
if ($trimmed === '' || strpos($trimmed, '#') === 0) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/^https?:\/\//i', $trimmed)) {
|
|
continue;
|
|
}
|
|
|
|
if (!preg_match('/\.(m3u8|ts)$/i', $trimmed)) {
|
|
continue;
|
|
}
|
|
|
|
$resolved = $this->normalizeHLSRelativePath($baseDir, $trimmed);
|
|
$safePath = $this->sanitizeHLSPath($resolved);
|
|
|
|
if (!$safePath) {
|
|
continue;
|
|
}
|
|
|
|
$lines[$index] = '/stream.php?v=' . rawurlencode($videoKey) . '&hls=' . $this->encodeHLSPath($safePath);
|
|
}
|
|
|
|
return implode("\n", $lines);
|
|
}
|
|
|
|
private function normalizeHLSRelativePath($baseDir, $target)
|
|
{
|
|
$baseDir = trim($baseDir, '/');
|
|
$target = str_replace('\\', '/', (string)$target);
|
|
|
|
if (strpos($target, '/') === 0) {
|
|
$target = ltrim($target, '/');
|
|
} elseif ($baseDir !== '') {
|
|
$target = $baseDir . '/' . $target;
|
|
}
|
|
|
|
$parts = [];
|
|
foreach (explode('/', $target) as $part) {
|
|
if ($part === '' || $part === '.') {
|
|
continue;
|
|
}
|
|
if ($part === '..') {
|
|
array_pop($parts);
|
|
continue;
|
|
}
|
|
$parts[] = $part;
|
|
}
|
|
|
|
return implode('/', $parts);
|
|
}
|
|
|
|
private function encodeHLSPath($path)
|
|
{
|
|
$segments = explode('/', $path);
|
|
$encodedSegments = array_map('rawurlencode', $segments);
|
|
|
|
return implode('/', $encodedSegments);
|
|
}
|
|
|
|
private function getBaseUrl()
|
|
{
|
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
|
|
|
return $protocol . '://' . $host;
|
|
}
|
|
|
|
private function getRealIpAddress()
|
|
{
|
|
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
|
return $_SERVER['HTTP_CLIENT_IP'];
|
|
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
return $_SERVER['HTTP_X_FORWARDED_FOR'];
|
|
} else {
|
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
|
}
|
|
}
|
|
}
|