feat: Add comprehensive documentation suite and reorganize project structure
- 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
This commit is contained in:
748
f_core/f_classes/class.streaming.php
Normal file
748
f_core/f_classes/class.streaming.php
Normal file
@@ -0,0 +1,748 @@
|
||||
<?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';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user