├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── composer.json ├── config.example.php ├── logs └── .gitkeep ├── make_keychain.php ├── server.php └── src ├── Call.php ├── Client.php ├── Controller.php ├── Exceptions ├── ClientIsAlreadyInException.php ├── ClientIsBannedException.php ├── ClientIsNotBannedException.php ├── ClientIsNotInTheRoomException.php ├── ClientIsNotOwnerException.php ├── RoomIsFullException.php └── RoomWrongPasswordException.php ├── Mapping.php └── Room.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.paypal.me/JavierSL'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | config.php 3 | composer.lock 4 | logs 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPPeerServer 2 | PHP Socket.io server for WebRTC signaling. 3 | 4 | Client: https://github.com/jsanahuja/peer-client 5 | 6 | Demo: https://peer-demo.sowecms.com/ 7 | 8 | ## Install 9 | Clone both client and server and install dependencies. 10 | ``` 11 | git clone https://github.com/jsanahuja/php-peer-server.git 12 | git clone https://github.com/jsanahuja/peer-client.git 13 | cd php-peer-server 14 | composer install 15 | cd ../peer-client 16 | npm install 17 | npm run build 18 | ``` 19 | 20 | Create your server configuration file 21 | ``` 22 | cd ../php-peer-server 23 | cp config.example.php config.php 24 | ``` 25 | Generate your Keychain private key: 26 | ``` 27 | php make_keychain.php 28 | ``` 29 | > Result example: 1949e2278ea5767099e592ae8dd15677ecf851efd177a1f0fb1d44e8e79231f5 30 | 31 | Place the result in the constant `KEYCHAIN_PK` defined in the `config.php` file we just created. Set also the absolute path to your `CERT_CA` and `CERT_KEY` files. You can play with the other constant values but it should be good to go. Note that if you want to change the `PORT` you will have to change it in the `peer-client` too. 32 | 33 | Start the server as a daemon 34 | ``` 35 | php server.php start -d 36 | ``` 37 | 38 | Do not forget to open/whitelist the `PORT` or the server won't be accessible. For example with `firewalld` you would do: 39 | ``` 40 | firewall-cmd --zone=public --permanent --add-port=8999/tcp 41 | firewall-cmd --reload 42 | ``` 43 | 44 | Note: The HTTP server should be pointing to the `peer-client` folder. 45 | 46 | ## Management 47 | 48 | Here you can see a list of the available commands: https://github.com/walkor/Workerman#available-commands 49 | 50 | ## Server actions 51 | 52 | The following is the list of events you can send to the server. For example `socket.emit('join', 'a68ca609389b6ba7f0766b9ed1bfd8ca')` 53 | 54 | > `create(name, password)` creates a room. 55 | 56 | > `join(roomId, password)` joins a room. 57 | 58 | > `leave()` leaves the current room. 59 | 60 | > `kick(userId)` kicks an user from the room. Can only be used by the creator. 61 | 62 | > `ban(userId)` bans an user from the room. Can only be used by the creator. 63 | 64 | > `unban(userId)` unbans an user from the room. Can only be used by the creator. 65 | 66 | > `message(msg)` sends a message to the current room. 67 | 68 | > `toggle(resource)` toggles a resource. The resources are 'video', 'microphone' and 'audio'. 69 | 70 | > `candidate(callId, candidate)` sends a candidate. 71 | 72 | > `offer(callId, offer)` sends an offer. Must have been requested with the `call` event. 73 | 74 | > `answer(callId, answer)` sends an answer. Must have been requested with the `offer` event. 75 | 76 | ## Server responses 77 | 78 | The following are the events the server will trigger. 79 | 80 | > `created(roomId)` confirms you created a room. 81 | 82 | > `joined(roomId)` confirms you joined the room. 83 | 84 | > `left(roomId)` confirms you left the room. 85 | 86 | > `kicked` notifies that you have been kicked from the room. 87 | 88 | > `banned` notifies that you have been banned from the room. 89 | 90 | > `unbanned` notifies that you have been unbanned from a room. 91 | 92 | > `call(callId)` requests an offer for the call 93 | 94 | > `offer(callId, offer)` sends and offer and requires an answer 95 | 96 | > `answer(callId, answer)` sends an answer to the offer 97 | 98 | > `hangup(callId)` notifies that the call was closed 99 | 100 | Also the error events: 101 | 102 | > `join_alreadyin` notifies you can't join the room because you're already in. 103 | 104 | > `join_wrongpass` notifies you can't join the room because the password provided is wrong. 105 | 106 | > `join_full` notifies you can't join the room because it is full. 107 | 108 | > `join_banned` notifies you can't join the room because you are banned. 109 | 110 | > `kick_noprivileges` notifies you don't have enough privileges to kick in the current room. 111 | 112 | > `kick_notin` notifies that the user you tried to kick is no longer in the room. 113 | 114 | > `ban_noprivileges` notifies you don't have enough privileges to ban in the current room. 115 | 116 | > `ban_already` notifies that the user you tried to ban is already banned. 117 | 118 | > `unban_noprivileges` notifies you don't have enough privileges to unban in the current room. 119 | 120 | > `unban_notbanned` notifies you have tried to unban an user that wasn't banned. 121 | 122 | And the room events: 123 | 124 | > `r_message(userId, message)` someone in your room sent a text message in your room. 125 | 126 | > `r_resource(userId, resource, status)` someone in your room toggled the status of one of its resources. 127 | 128 | > `r_joined(userId)` an user joined your room. 129 | 130 | > `r_left(userId)` an user left your room. 131 | 132 | > `r_kicked(userId)` an user was kicked out of your room. 133 | 134 | > `r_banned(userId)` an user from your room was banned. 135 | 136 | > `r_unbanned(userId)` an user has been unbanned. 137 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sowe/peerserver", 3 | "description": "PHP Socket.io server for WebRTC signaling", 4 | "type": "project", 5 | "license": "MIT", 6 | "version": "0.0.10", 7 | "authors": [ 8 | { 9 | "name": "Javier Sanahuja", 10 | "email": "bannss1@gmail.com" 11 | } 12 | ], 13 | "minimum-stability": "stable", 14 | "require": { 15 | "workerman/phpsocket.io": "^1.1", 16 | "monolog/monolog": "^2.0", 17 | "sowe/framework": "^1.3" 18 | }, 19 | "autoload": { 20 | "files": [ 21 | "config.php" 22 | ], 23 | "psr-4": { 24 | "Sowe\\PHPPeerServer\\": "src/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config.example.php: -------------------------------------------------------------------------------- 1 | setFormatter($formatter); 26 | $logger->pushHandler($stream); 27 | $handler = new ErrorHandler($logger); 28 | $handler->registerErrorHandler([], false); 29 | $handler->registerExceptionHandler(); 30 | $handler->registerFatalHandler(); 31 | 32 | $controller = new Controller($logger); 33 | $controller->bind(); 34 | 35 | Worker::runAll(); 36 | 37 | -------------------------------------------------------------------------------- /src/Call.php: -------------------------------------------------------------------------------- 1 | id = $room->getId() . $client1->getId() . $client2->getId(); 18 | $this->client1 = $client1; 19 | $this->client2 = $client2; 20 | 21 | $this->init(); 22 | } 23 | 24 | /** 25 | * Call workflow 26 | */ 27 | public function init(){ 28 | $this->client1->getSocket()->emit("call", $this->id); 29 | } 30 | 31 | public function offer($offer){ 32 | $this->client2->getSocket()->emit("offer", $this->id, $offer); 33 | } 34 | 35 | public function answer($answer){ 36 | $this->client1->getSocket()->emit("answer", $this->id, $answer); 37 | } 38 | 39 | public function hangup(){ 40 | $this->client1->getSocket()->emit("hangup", $this->id); 41 | $this->client2->getSocket()->emit("hangup", $this->id); 42 | } 43 | 44 | public function candidate($client, $candidate){ 45 | if($this->client1->equals($client)){ 46 | $this->client2->getSocket()->emit("candidate", $this->id, $candidate); 47 | }else if($this->client2->equals($client)){ 48 | $this->client1->getSocket()->emit("candidate", $this->id, $candidate); 49 | } 50 | } 51 | 52 | /** 53 | * Helpers 54 | */ 55 | public function getId(){ 56 | return $this->id; 57 | } 58 | 59 | public function clientCanOffer(Client $client){ 60 | return $this->client1->equals($client); 61 | } 62 | 63 | public function clientCanAnswer(Client $client){ 64 | return $this->client2->equals($client); 65 | } 66 | 67 | public function contains(Client $client){ 68 | return $this->client1->equals($client) || 69 | $this->client2->equals($client); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | id = $id; 16 | $this->socket = $socket; 17 | $this->room = false; 18 | 19 | $this->resources = [ 20 | "video" => false, 21 | "microphone" => false, 22 | "audio" => false 23 | ]; 24 | 25 | $this->candidates = []; 26 | } 27 | 28 | public function getId(){ 29 | return $this->id; 30 | } 31 | 32 | public function getPublicInfo(){ 33 | return [ 34 | "id" => $this->id 35 | ]; 36 | } 37 | 38 | public function getSocket(){ 39 | return $this->socket; 40 | } 41 | 42 | public function equals(Client $other){ 43 | return $this->id === $other->getId(); 44 | } 45 | 46 | /** 47 | * Resources 48 | */ 49 | public function toggleResource($resource){ 50 | if(isset($this->resources[$resource])){ 51 | $this->resources[$resource] = !$this->resources[$resource]; 52 | return true; 53 | } 54 | return false; 55 | } 56 | 57 | public function getResource($resource){ 58 | if(isset($this->resources[$resource])){ 59 | return $this->resources[$resource]; 60 | } 61 | } 62 | 63 | public function getResources(){ 64 | return $this->resources; 65 | } 66 | 67 | /** 68 | * Room 69 | */ 70 | public function getRoom(){ 71 | return $this->room; 72 | } 73 | 74 | public function setRoom($room){ 75 | if($this->room !== false){ 76 | $this->socket->leave($this->room->getId()); 77 | } 78 | if($room !== false){ 79 | $this->socket->join($room->getId()); 80 | } 81 | $this->room = $room; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 32 | $this->clients = new Mapping(); 33 | $this->rooms = new Mapping(); 34 | 35 | self::$instance = $this; 36 | } 37 | 38 | public function getClient($socket){ 39 | $client = $socket->ppsClient; 40 | if($this->clients->hasKey($client->getId())){ 41 | return $client; 42 | } 43 | return false; 44 | } 45 | 46 | public function handleException($client, $exception){ 47 | $this->logger->error(__FUNCTION__.":".__LINE__ .":" . $client->getId() . ": " . $exception->getMessage()); 48 | } 49 | 50 | public function unsetRoom($room){ 51 | $this->rooms->remove($room); 52 | $this->logger->info(__FUNCTION__.":".__LINE__ .": room killed " . $room->getId()); 53 | } 54 | 55 | public function roomChanged($room){ 56 | $this->logger->info(__FUNCTION__.":".__LINE__ .": room changed " . $room->getId()); 57 | // @TODO: update clients info 58 | } 59 | 60 | /** 61 | * Actions 62 | */ 63 | public function connect($socket){ 64 | $client = new Client($socket->id, $socket); 65 | $socket->ppsClient = $client; 66 | $this->clients->add($client); 67 | 68 | $this->logger->info(__FUNCTION__.":".__LINE__ .":". $client->getId() .": connected (ONLINE: " . sizeof($this->clients) . ") (IP: " . $socket->conn->remoteAddress . ")"); 69 | } 70 | 71 | public function disconnect($client, $reason){ 72 | $this->leaveRoom($client); 73 | $this->clients->remove($client); 74 | 75 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . ": disconnected (ONLINE: " . sizeof($this->clients) . ") (Reason: " . $reason . ")"); 76 | } 77 | 78 | public function message($client, $message){ 79 | if(!empty($message)){ 80 | $room = $client->getRoom(); 81 | if($room !== false){ 82 | $room->getSocket($this->io)->emit("r_message", $client->getPublicInfo(), $message); 83 | 84 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " message in " . $room->getId() . ": " . $message); 85 | } 86 | } 87 | } 88 | 89 | public function toggleResource($client, $resource){ 90 | if($client->toggleResource($resource)){ 91 | $room = $client->getRoom(); 92 | if($room !== false){ 93 | $room->getSocket($this->io)->emit("r_resource", $client->getPublicInfo(), $client->getResources()); 94 | } 95 | } 96 | } 97 | 98 | 99 | /** 100 | * Call management 101 | */ 102 | public function candidate($client, $callId, $candidate){ 103 | $room = $client->getRoom(); 104 | if($room !== false){ 105 | if($room->candidate($client, $callId, $candidate)){ 106 | // Candidate sent 107 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " sent a candidate " . $callId); 108 | return; 109 | } 110 | } 111 | $this->logger->warning(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " tried to send a candidate " . $callId); 112 | } 113 | 114 | public function offer(Client $client, $callId, $offer){ 115 | $room = $client->getRoom(); 116 | if($room !== false){ 117 | if($room->offer($client, $callId, $offer)){ 118 | // Offered 119 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " offered " . $callId); 120 | return; 121 | } 122 | } 123 | $this->logger->warning(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " tried to offer " . $callId); 124 | } 125 | 126 | public function answer(Client $client, $callId, $answer){ 127 | $room = $client->getRoom(); 128 | if($room !== false){ 129 | if($room->answer($client, $callId, $answer)){ 130 | // Answered 131 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " answered " . $callId); 132 | return; 133 | } 134 | } 135 | $this->logger->warning(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " tried to answer " . $callId); 136 | } 137 | 138 | /** 139 | * Room management 140 | */ 141 | public function createRoom($client, $name, $password){ 142 | do{ 143 | $roomId = bin2hex(random_bytes(ROOM_HASH_LENGTH)); 144 | }while($this->rooms->hasKey($roomId)); 145 | 146 | $currentRoom = $client->getRoom(); 147 | if($currentRoom !== false){ 148 | $this->leaveRoom($client); 149 | } 150 | 151 | $this->rooms->add(new Room($roomId, $name, $password, $client)); 152 | $client->getSocket()->emit("created", $client->getRoom()->getPublicInfo()); 153 | 154 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " created the room " . $roomId); 155 | } 156 | 157 | public function joinRoom($client, $roomId, $password){ 158 | $currentRoom = $client->getRoom(); 159 | $room = $this->rooms->get($roomId); 160 | 161 | if($room !== false){ 162 | if($currentRoom !== false){ 163 | if($currentRoom->equals($room)){ 164 | // Already in this room 165 | $this->logger->warning(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " tried to rejoin " . $roomId); 166 | 167 | $client->getSocket()->emit("join_alreadyin"); 168 | return; 169 | }else{ 170 | // Leaving previous room 171 | $this->leaveRoom($client); 172 | } 173 | } 174 | 175 | try{ 176 | $room->join($client, $password); 177 | // Joined 178 | $client->getSocket()->emit("joined", $room->getPublicInfo()); 179 | $room->getSocket($this->io)->emit("r_joined", $client->getPublicInfo()); 180 | 181 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " joined " . $roomId); 182 | }catch(RoomWrongPasswordException $e){ 183 | // Wrong room password 184 | $client->getSocket()->emit("join_wrongpass"); 185 | 186 | $this->logger->error(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " join failed (wrong password) " . $roomId); 187 | }catch(RoomIsFullException $e){ 188 | // Room is full 189 | $client->getSocket()->emit("join_full"); 190 | 191 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " join failed (full) " . $roomId); 192 | }catch(ClientIsBannedException $e){ 193 | // Client is banned 194 | $client->getSocket()->emit("join_banned"); 195 | 196 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " join failed (banned) " . $roomId); 197 | }catch(ClientIsAlreadyInException $e){ 198 | // Shouldnt happen because we're checking it earlier 199 | $this->logger->error(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " ClientIsAlreadyInException " . $roomId . ". Report this error."); 200 | } 201 | } 202 | } 203 | 204 | public function leaveRoom($client){ 205 | $room = $client->getRoom(); 206 | if($room !== false){ 207 | $room->leave($client); 208 | $client->getSocket()->emit("left", $room->getPublicInfo()); 209 | $room->getSocket($this->io)->emit("r_left", $client->getPublicInfo()); 210 | 211 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " left " . $room->getId()); 212 | } 213 | } 214 | 215 | public function kickFromRoom($client, $userId){ 216 | $room = $client->getRoom(); 217 | $clientToKick = $this->clients->get($userId); 218 | if($room !== false && $clientToKick !== false){ 219 | try{ 220 | $room->kick($client, $clientToKick); 221 | // Kicked 222 | $clientToKick->getSocket()->emit("kicked", $room->getPublicInfo()); 223 | $room->getSocket($this->io)->emit("r_kicked", $clientToKick->getPublicInfo()); 224 | 225 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " kicked " . $clientToKick->getId() . " from " . $room->getId()); 226 | }catch(ClientIsNotOwnerException $e){ 227 | // Client is not the owner 228 | $client->getSocket()->emit("kick_noprivileges"); 229 | 230 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " failed kicking (privileges) " . $clientToKick->getId() . " from " . $room->getId()); 231 | }catch(ClientIsNotInTheRoomException $e){ 232 | // ClientToKick is no longuer in the room 233 | $client->getSocket()->emit("kick_notin"); 234 | 235 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " failed kicking (not in) " . $clientToKick->getId() . " from " . $room->getId()); 236 | } 237 | } 238 | } 239 | 240 | public function banFromRoom($client, $userId){ 241 | $room = $client->getRoom(); 242 | $clientToBan = $this->clients->get($userId); 243 | if($room !== false && $clientToBan !== false){ 244 | try{ 245 | $room->ban($client, $clientToBan); 246 | // Banned 247 | $clientToBan->getSocket()->emit("banned", $room->getPublicInfo()); 248 | $room->getSocket($this->io)->emit("r_banned", $clientToBan->getPublicInfo()); 249 | 250 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " banned " . $clientToBan->getId() . " from " . $room->getId()); 251 | }catch(ClientIsNotOwnerException $e){ 252 | // Client is not the owner 253 | $client->getSocket()->emit("ban_noprivileges"); 254 | 255 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " failed banning (privileges) " . $clientToBan->getId() . " from " . $room->getId()); 256 | }catch(ClientIsBannedException $e){ 257 | // ClientToBan is already banned 258 | $client->getSocket()->emit("ban_already"); 259 | 260 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " failed banning (already) " . $clientToBan->getId() . " from " . $room->getId()); 261 | } 262 | } 263 | } 264 | 265 | public function unbanFromRoom($client, $userId){ 266 | $room = $client->getRoom(); 267 | $clientToUnban = $this->clients->get($userId); 268 | if($room !== false && $clientToUnban !== false){ 269 | try{ 270 | $room->unban($client, $clientToUnban); 271 | $clientToUnban->getSocket()->emit("unbanned", $room->getPublicInfo()); 272 | $room->getSocket($this->io)->emit("r_unbanned", $clientToUnban->getPublicInfo()); 273 | 274 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " unbanned " . $clientToUnban->getId() . " from " . $room->getId()); 275 | }catch(ClientIsNotOwnerException $e){ 276 | // Client is not the owner 277 | $client->getSocket()->emit("unban_noprivileges"); 278 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " failed unbanning (privileges) " . $clientToUnban->getId() . " from " . $room->getId()); 279 | }catch(ClientIsNotBannedException $e){ 280 | // ClientToUnban is not banned 281 | $client->getSocket()->emit("unban_notbanned"); 282 | $this->logger->info(__FUNCTION__.":".__LINE__ .":" . $client->getId() . " failed unbanning (not banned) " . $clientToUnban->getId() . " from " . $room->getId()); 283 | } 284 | } 285 | } 286 | 287 | /** 288 | * Binding 289 | */ 290 | public function bind(){ 291 | $this->io = new SocketIO(PORT, array( 292 | 'ssl' => array( 293 | 'local_cert' => CERT_CA, 294 | 'local_pk' => CERT_KEY, 295 | 'verify_peer' => false, 296 | 'allow_self_signed' => true, 297 | 'verify_peer_name' => false 298 | ) 299 | )); 300 | $this->io->on('workerStart', function ($socket){ 301 | $this->logger->info("Server starting..."); 302 | }); 303 | $this->io->on('workerStop', function ($socket){ 304 | $this->logger->info("Server stopping..."); 305 | }); 306 | $this->io->on('connection', function ($socket) { 307 | $this->connect($socket); 308 | 309 | $socket->on("error", function ($exception) use ($socket) { 310 | $client = $this->getClient($socket); 311 | if($client !== false){ 312 | $this->handleException($client, $exception); 313 | } 314 | }); 315 | 316 | $socket->on("disconnect", function ($reason) use ($socket) { 317 | $client = $this->getClient($socket); 318 | if($client !== false){ 319 | if($reason == $client->getId()){ 320 | $reason = "leaving"; 321 | } 322 | $this->disconnect($client, $reason); 323 | } 324 | }); 325 | 326 | $socket->on("message", function($message) use ($socket) { 327 | $client = $this->getClient($socket); 328 | if($client !== false){ 329 | $this->message($client, $message); 330 | } 331 | }); 332 | 333 | $socket->on("toggle", function($resource) use ($socket) { 334 | $client = $this->getClient($socket); 335 | if($client !== false){ 336 | $this->toggleResource($client, $resource); 337 | } 338 | }); 339 | 340 | $socket->on("create", function($name, $password) use ($socket) { 341 | $client = $this->getClient($socket); 342 | if($client !== false){ 343 | $this->createRoom($client, $name, $password); 344 | } 345 | }); 346 | 347 | $socket->on("join", function($roomId, $password) use ($socket) { 348 | $client = $this->getClient($socket); 349 | if($client !== false){ 350 | $this->joinRoom($client, $roomId, $password); 351 | } 352 | }); 353 | 354 | $socket->on("leave", function() use ($socket) { 355 | $client = $this->getClient($socket); 356 | if($client !== false){ 357 | $this->leaveRoom($client); 358 | } 359 | }); 360 | 361 | $socket->on("kick", function($userId) use ($socket) { 362 | $client = $this->getClient($socket); 363 | if($client !== false){ 364 | $this->kickFromRoom($client, $userId); 365 | } 366 | }); 367 | 368 | $socket->on("ban", function($userId) use ($socket) { 369 | $client = $this->getClient($socket); 370 | if($client !== false){ 371 | $this->banFromRoom($client, $userId); 372 | } 373 | }); 374 | 375 | $socket->on("unban", function($userId) use ($socket) { 376 | $client = $this->getClient($socket); 377 | if($client !== false){ 378 | $this->unbanFromRoom($client, $userId); 379 | } 380 | }); 381 | 382 | /** 383 | * Calls 384 | */ 385 | $socket->on("candidate", function($callId, $candidate) use ($socket) { 386 | $client = $this->getClient($socket); 387 | if($client !== false){ 388 | $this->candidate($client, $callId, $candidate); 389 | } 390 | }); 391 | 392 | $socket->on("offer", function($callId, $offer) use ($socket) { 393 | $client = $this->getClient($socket); 394 | if($client !== false){ 395 | $this->offer($client, $callId, $offer); 396 | } 397 | }); 398 | 399 | $socket->on("answer", function($callId, $answer) use ($socket) { 400 | $client = $this->getClient($socket); 401 | if($client !== false){ 402 | $this->answer($client, $callId, $answer); 403 | } 404 | }); 405 | }); 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/Exceptions/ClientIsAlreadyInException.php: -------------------------------------------------------------------------------- 1 | elements = []; 11 | } 12 | 13 | public function add($element){ 14 | $this->elements[$element->getId()] = $element; 15 | } 16 | 17 | public function remove($element){ 18 | if (isset($this->elements[$element->getId()])) { 19 | unset($this->elements[$element->getId()]); 20 | } 21 | } 22 | 23 | public function contains($element){ 24 | return isset($this->elements[$element->getId()]); 25 | } 26 | 27 | public function hasKey($key){ 28 | return isset($this->elements[$key]); 29 | } 30 | 31 | public function get($id){ 32 | if (isset($this->elements[$id])) { 33 | return $this->elements[$id]; 34 | } 35 | return false; 36 | } 37 | 38 | public function keys(){ 39 | return array_keys($this->elements); 40 | } 41 | 42 | public function values(){ 43 | return array_values($this->elements); 44 | } 45 | 46 | // Countable 47 | public function count(){ 48 | return count($this->elements); 49 | } 50 | 51 | // IteratorAggregate 52 | public function getIterator(){ 53 | return new \ArrayIterator($this->elements); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Room.php: -------------------------------------------------------------------------------- 1 | id = $id; 29 | $this->name = $name; 30 | $this->password = Keychain::hash($password); 31 | $this->clients = new Mapping(); 32 | $this->banned = new Mapping(); 33 | $this->owner = $owner; 34 | $this->calls = new Mapping(); 35 | 36 | $this->join($owner, $password); 37 | } 38 | 39 | public function getId(){ 40 | return $this->id; 41 | } 42 | 43 | public function getPublicInfo(){ 44 | return [ 45 | "name" => $this->name, 46 | "id" => $this->id 47 | ]; 48 | } 49 | 50 | public function getSocket($io){ 51 | return $io->to($this->id); 52 | } 53 | 54 | public function equals(Room $other){ 55 | return $this->id === $other->getId(); 56 | } 57 | 58 | /** 59 | * Calls 60 | */ 61 | public function createCalls(Client $client){ 62 | foreach($this->clients as $c){ 63 | $this->calls->add(new Call($this, $c, $client)); 64 | } 65 | } 66 | 67 | public function removeCalls(Client $client){ 68 | foreach($this->calls as $call){ 69 | if($call->contains($client)){ 70 | $call->hangup(); 71 | $this->calls->remove($call); 72 | } 73 | } 74 | } 75 | 76 | public function candidate(Client $client, $callId, $candidate){ 77 | $call = $this->calls->get($callId); 78 | if($call !== false && $call->contains($client)){ 79 | $call->candidate($client, $candidate); 80 | return true; 81 | } 82 | return false; 83 | } 84 | 85 | public function offer(Client $client, $callId, $offer){ 86 | $call = $this->calls->get($callId); 87 | if($call !== false && $call->clientCanOffer($client)){ 88 | $call->offer($offer); 89 | return true; 90 | } 91 | return false; 92 | } 93 | 94 | public function answer(Client $client, $callId, $answer){ 95 | $call = $this->calls->get($callId); 96 | if($call !== false && $call->clientCanAnswer($client)){ 97 | $call->answer($answer); 98 | return true; 99 | } 100 | return false; 101 | } 102 | 103 | /** 104 | * Status helpers 105 | */ 106 | public function isOwner(Client $client){ 107 | return $this->owner->equals($client); 108 | } 109 | 110 | public function authorize($password){ 111 | return Keychain::hash_verify($this->password, $password); 112 | } 113 | 114 | public function isMember(Client $client){ 115 | return $this->clients->contains($client); 116 | } 117 | 118 | public function isBanned(Client $client){ 119 | return $this->banned->contains($client); 120 | } 121 | 122 | public function isFull(){ 123 | return sizeof($this->clients) >= ROOM_MAX_CLIENTS; 124 | } 125 | 126 | /** 127 | * Actions 128 | */ 129 | public function join(Client $client, $password){ 130 | if($this->isMember($client)){ 131 | throw new ClientIsAlreadyInException(); 132 | } 133 | if(!$this->authorize($password)){ 134 | throw new RoomWrongPasswordException(); 135 | } 136 | if($this->isFull()){ 137 | throw new RoomIsFullException(); 138 | } 139 | if($this->isBanned($client)){ 140 | throw new ClientIsBannedException(); 141 | } 142 | 143 | $this->createCalls($client); 144 | $this->clients->add($client); 145 | $client->setRoom($this); 146 | } 147 | 148 | public function leave(Client $client){ 149 | if($this->isOwner($client)){ 150 | if(sizeof($this->clients) > 1){ 151 | foreach($this->clients as $newOwner){ 152 | if(!$client->equals($newOwner)){ 153 | $this->owner = $newOwner; 154 | Controller::getInstance()->roomChanged($this); 155 | break; 156 | } 157 | } 158 | }else{ 159 | Controller::getInstance()->unsetRoom($this); 160 | } 161 | } 162 | $this->clients->remove($client); 163 | $this->removeCalls($client); 164 | $client->setRoom(false); 165 | } 166 | 167 | public function kick(Client $client, Client $clientToKick){ 168 | if(!$this->isOwner($client)){ 169 | throw new ClientIsNotOwnerException(); 170 | } 171 | if(!$this->isMember($clientToKick)){ 172 | throw new ClientIsNotInTheRoomException(); 173 | } 174 | $this->clients->remove($clientToKick); 175 | $this->removeCalls($clientToKick); 176 | $client->setRoom(false); 177 | } 178 | 179 | public function ban(Client $client, Client $clientToBan){ 180 | if(!$this->isOwner($client)){ 181 | throw new ClientIsNotOwnerException(); 182 | } 183 | if($this->isBanned($client)){ 184 | throw new ClientIsBannedException(); 185 | } 186 | 187 | $this->banned->add($clientToBan); 188 | 189 | if(!$this->isMember($clientToBan)){ 190 | $this->clients->remove($clientToBan); 191 | $this->removeCalls($clientToBan); 192 | $client->setRoom(false); 193 | } 194 | } 195 | 196 | public function unban(Client $client, Client $clientToUnban){ 197 | if(!$this->isOwner($client)){ 198 | throw new ClientIsNotOwnerException(); 199 | } 200 | if(!$this->isBanned($client)){ 201 | throw new ClientIsNotBannedException(); 202 | } 203 | $this->banned->remove($clientToUnban); 204 | } 205 | 206 | } --------------------------------------------------------------------------------