- 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
463 lines
15 KiB
PHP
463 lines
15 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');
|
|
|
|
/**
|
|
* Live Streaming System with SRS Integration
|
|
*/
|
|
class VLiveStreaming
|
|
{
|
|
private $logger;
|
|
private $db;
|
|
private $queue;
|
|
private $srsConfig;
|
|
|
|
// SRS default configuration
|
|
private $srsHost = 'localhost';
|
|
private $srsRtmpPort = 1935;
|
|
private $srsHttpPort = 8080;
|
|
private $srsApiPort = 1985;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->logger = VLogger::getInstance();
|
|
$this->db = VDatabase::getInstance();
|
|
$this->queue = new VQueueManager();
|
|
|
|
$this->loadSRSConfig();
|
|
}
|
|
|
|
/**
|
|
* Create live stream
|
|
* @param int $userId User ID
|
|
* @param array $streamData Stream configuration
|
|
* @return array Stream creation result
|
|
*/
|
|
public function createStream($userId, $streamData)
|
|
{
|
|
try {
|
|
$streamKey = $this->generateStreamKey();
|
|
|
|
$stream = [
|
|
'stream_key' => $streamKey,
|
|
'user_id' => $userId,
|
|
'title' => $streamData['title'] ?? 'Live Stream',
|
|
'description' => $streamData['description'] ?? '',
|
|
'category' => $streamData['category'] ?? 'general',
|
|
'privacy' => $streamData['privacy'] ?? 'public',
|
|
'status' => 'created',
|
|
'rtmp_url' => "rtmp://{$this->srsHost}:{$this->srsRtmpPort}/live/{$streamKey}",
|
|
'hls_url' => "http://{$this->srsHost}:{$this->srsHttpPort}/live/{$streamKey}.m3u8",
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$streamId = $this->db->doInsert('db_live_streams', $stream);
|
|
|
|
if (!$streamId) {
|
|
throw new Exception('Failed to create stream record');
|
|
}
|
|
|
|
$this->logger->info('Live stream created', [
|
|
'stream_id' => $streamId,
|
|
'stream_key' => $streamKey,
|
|
'user_id' => $userId
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'stream_id' => $streamId,
|
|
'stream_key' => $streamKey,
|
|
'rtmp_url' => $stream['rtmp_url'],
|
|
'hls_url' => $stream['hls_url'],
|
|
'stream_data' => $stream
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to create live stream', [
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start live stream
|
|
* @param string $streamKey Stream key
|
|
* @return array Start result
|
|
*/
|
|
public function startStream($streamKey)
|
|
{
|
|
try {
|
|
$stream = $this->getStreamByKey($streamKey);
|
|
|
|
if (!$stream) {
|
|
throw new Exception('Stream not found');
|
|
}
|
|
|
|
// Update stream status
|
|
$this->db->doUpdate('db_live_streams', 'id', [
|
|
'status' => 'live',
|
|
'started_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
], $stream['id']);
|
|
|
|
// Notify subscribers
|
|
$this->notifyStreamStart($stream);
|
|
|
|
$this->logger->info('Live stream started', [
|
|
'stream_key' => $streamKey,
|
|
'stream_id' => $stream['id']
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Stream started successfully'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to start stream', [
|
|
'stream_key' => $streamKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop live stream
|
|
* @param string $streamKey Stream key
|
|
* @param bool $saveRecording Whether to save recording
|
|
* @return array Stop result
|
|
*/
|
|
public function stopStream($streamKey, $saveRecording = true)
|
|
{
|
|
try {
|
|
$stream = $this->getStreamByKey($streamKey);
|
|
|
|
if (!$stream) {
|
|
throw new Exception('Stream not found');
|
|
}
|
|
|
|
// Update stream status
|
|
$updateData = [
|
|
'status' => 'ended',
|
|
'ended_at' => date('Y-m-d H:i:s'),
|
|
'updated_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
if ($saveRecording) {
|
|
// Queue recording processing job
|
|
$this->queue->enqueue('StreamRecordingJob', [
|
|
'stream_id' => $stream['id'],
|
|
'stream_key' => $streamKey,
|
|
'user_id' => $stream['user_id']
|
|
], 'video_processing', 0, 1);
|
|
|
|
$updateData['recording_queued'] = 1;
|
|
}
|
|
|
|
$this->db->doUpdate('db_live_streams', 'id', $updateData, $stream['id']);
|
|
|
|
$this->logger->info('Live stream stopped', [
|
|
'stream_key' => $streamKey,
|
|
'stream_id' => $stream['id'],
|
|
'save_recording' => $saveRecording
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Stream stopped successfully',
|
|
'recording_queued' => $saveRecording
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to stop stream', [
|
|
'stream_key' => $streamKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get live streams
|
|
* @param array $filters Filters
|
|
* @param int $limit Limit
|
|
* @param int $offset Offset
|
|
* @return array Streams
|
|
*/
|
|
public function getLiveStreams($filters = [], $limit = 20, $offset = 0)
|
|
{
|
|
try {
|
|
$whereClause = "WHERE 1=1";
|
|
$params = [];
|
|
|
|
if (isset($filters['status'])) {
|
|
$whereClause .= " AND status = ?";
|
|
$params[] = $filters['status'];
|
|
}
|
|
|
|
if (isset($filters['user_id'])) {
|
|
$whereClause .= " AND user_id = ?";
|
|
$params[] = $filters['user_id'];
|
|
}
|
|
|
|
if (isset($filters['category'])) {
|
|
$whereClause .= " AND category = ?";
|
|
$params[] = $filters['category'];
|
|
}
|
|
|
|
$query = "SELECT ls.*, au.usr_user, au.usr_avatar
|
|
FROM db_live_streams ls
|
|
JOIN db_accountuser au ON ls.user_id = au.usr_id
|
|
{$whereClause}
|
|
ORDER BY ls.created_at DESC
|
|
LIMIT ? OFFSET ?";
|
|
|
|
$params[] = $limit;
|
|
$params[] = $offset;
|
|
|
|
$result = $this->db->doQuery($query, $params);
|
|
|
|
$streams = [];
|
|
while ($row = $this->db->doFetch($result)) {
|
|
$streams[] = $this->formatStreamData($row);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'streams' => $streams,
|
|
'total' => $this->getStreamCount($filters)
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to get live streams', [
|
|
'filters' => $filters,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get stream statistics
|
|
* @param string $streamKey Stream key
|
|
* @return array Statistics
|
|
*/
|
|
public function getStreamStats($streamKey)
|
|
{
|
|
try {
|
|
$stream = $this->getStreamByKey($streamKey);
|
|
|
|
if (!$stream) {
|
|
throw new Exception('Stream not found');
|
|
}
|
|
|
|
// Get viewer count from SRS API
|
|
$viewerCount = $this->getSRSViewerCount($streamKey);
|
|
|
|
// Get chat message count
|
|
$chatCount = $this->getChatMessageCount($stream['id']);
|
|
|
|
// Calculate duration
|
|
$duration = 0;
|
|
if ($stream['started_at']) {
|
|
$endTime = $stream['ended_at'] ? strtotime($stream['ended_at']) : time();
|
|
$duration = $endTime - strtotime($stream['started_at']);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'stats' => [
|
|
'viewer_count' => $viewerCount,
|
|
'chat_messages' => $chatCount,
|
|
'duration' => $duration,
|
|
'status' => $stream['status'],
|
|
'started_at' => $stream['started_at'],
|
|
'ended_at' => $stream['ended_at']
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to get stream stats', [
|
|
'stream_key' => $streamKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
|
|
private function loadSRSConfig()
|
|
{
|
|
// Load SRS configuration from database or config file
|
|
$this->srsConfig = [
|
|
'host' => $this->srsHost,
|
|
'rtmp_port' => $this->srsRtmpPort,
|
|
'http_port' => $this->srsHttpPort,
|
|
'api_port' => $this->srsApiPort
|
|
];
|
|
}
|
|
|
|
private function generateStreamKey()
|
|
{
|
|
return uniqid('stream_', true) . '_' . time();
|
|
}
|
|
|
|
private function getStreamByKey($streamKey)
|
|
{
|
|
$query = "SELECT * FROM db_live_streams WHERE stream_key = ?";
|
|
$result = $this->db->doQuery($query, [$streamKey]);
|
|
|
|
return $this->db->doFetch($result);
|
|
}
|
|
|
|
private function notifyStreamStart($stream)
|
|
{
|
|
// Get user's followers
|
|
$query = "SELECT follower_id FROM db_user_follows WHERE user_id = ?";
|
|
$result = $this->db->doQuery($query, [$stream['user_id']]);
|
|
|
|
while ($row = $this->db->doFetch($result)) {
|
|
// Create notification
|
|
$notification = [
|
|
'user_id' => $row['follower_id'],
|
|
'type' => 'live_stream_started',
|
|
'title' => 'Live Stream Started',
|
|
'message' => "User is now live streaming: {$stream['title']}",
|
|
'data' => json_encode([
|
|
'stream_id' => $stream['id'],
|
|
'stream_key' => $stream['stream_key'],
|
|
'streamer' => $stream['user_id']
|
|
]),
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'read_status' => 0
|
|
];
|
|
|
|
$this->db->doInsert('db_notifications', $notification);
|
|
}
|
|
}
|
|
|
|
private function getSRSViewerCount($streamKey)
|
|
{
|
|
try {
|
|
// Call SRS API to get viewer count
|
|
$url = "http://{$this->srsHost}:{$this->srsApiPort}/api/v1/streams/";
|
|
$response = file_get_contents($url);
|
|
|
|
if ($response) {
|
|
$data = json_decode($response, true);
|
|
// Parse SRS response to get viewer count for specific stream
|
|
// This is a simplified implementation
|
|
return $data['streams'][$streamKey]['clients'] ?? 0;
|
|
}
|
|
|
|
return 0;
|
|
|
|
} catch (Exception $e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function getChatMessageCount($streamId)
|
|
{
|
|
try {
|
|
$query = "SELECT COUNT(*) as count FROM db_stream_chat WHERE stream_id = ?";
|
|
$result = $this->db->doQuery($query, [$streamId]);
|
|
$row = $this->db->doFetch($result);
|
|
|
|
return (int)($row['count'] ?? 0);
|
|
|
|
} catch (Exception $e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function getStreamCount($filters)
|
|
{
|
|
try {
|
|
$whereClause = "WHERE 1=1";
|
|
$params = [];
|
|
|
|
if (isset($filters['status'])) {
|
|
$whereClause .= " AND status = ?";
|
|
$params[] = $filters['status'];
|
|
}
|
|
|
|
if (isset($filters['user_id'])) {
|
|
$whereClause .= " AND user_id = ?";
|
|
$params[] = $filters['user_id'];
|
|
}
|
|
|
|
$query = "SELECT COUNT(*) as count FROM db_live_streams {$whereClause}";
|
|
$result = $this->db->doQuery($query, $params);
|
|
$row = $this->db->doFetch($result);
|
|
|
|
return (int)($row['count'] ?? 0);
|
|
|
|
} catch (Exception $e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
private function formatStreamData($row)
|
|
{
|
|
return [
|
|
'id' => $row['id'],
|
|
'stream_key' => $row['stream_key'],
|
|
'title' => $row['title'],
|
|
'description' => $row['description'],
|
|
'category' => $row['category'],
|
|
'privacy' => $row['privacy'],
|
|
'status' => $row['status'],
|
|
'rtmp_url' => $row['rtmp_url'],
|
|
'hls_url' => $row['hls_url'],
|
|
'created_at' => $row['created_at'],
|
|
'started_at' => $row['started_at'],
|
|
'ended_at' => $row['ended_at'],
|
|
'user' => [
|
|
'id' => $row['user_id'],
|
|
'username' => $row['usr_user'],
|
|
'avatar' => $row['usr_avatar']
|
|
]
|
|
];
|
|
}
|
|
} |