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:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View 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';
}
}
}