Changeset View
Changeset View
Standalone View
Standalone View
src/app/Meet/Room.php
- This file was moved from src/app/OpenVidu/Room.php.
<?php | <?php | ||||
namespace App\OpenVidu; | namespace App\Meet; | ||||
use App\Traits\SettingsTrait; | use App\Traits\SettingsTrait; | ||||
use Illuminate\Database\Eloquent\Model; | use Illuminate\Database\Eloquent\Model; | ||||
use Illuminate\Support\Facades\Cache; | use Illuminate\Support\Facades\Cache; | ||||
/** | /** | ||||
* The eloquent definition of a Room. | * The eloquent definition of a Room. | ||||
* | * | ||||
* @property int $id Room identifier | * @property int $id Room identifier | ||||
* @property string $name Room name | * @property string $name Room name | ||||
* @property int $user_id Room owner | * @property int $user_id Room owner | ||||
* @property ?string $session_id OpenVidu session identifier | * @property ?string $session_id Meet session identifier | ||||
*/ | */ | ||||
class Room extends Model | class Room extends Model | ||||
{ | { | ||||
use SettingsTrait; | use SettingsTrait; | ||||
public const ROLE_SUBSCRIBER = 1 << 0; | public const ROLE_SUBSCRIBER = 1 << 0; | ||||
public const ROLE_PUBLISHER = 1 << 1; | public const ROLE_PUBLISHER = 1 << 1; | ||||
public const ROLE_MODERATOR = 1 << 2; | public const ROLE_MODERATOR = 1 << 2; | ||||
Show All 14 Lines | class Room extends Model | ||||
protected $table = 'openvidu_rooms'; | protected $table = 'openvidu_rooms'; | ||||
/** @var \GuzzleHttp\Client|null HTTP client instance */ | /** @var \GuzzleHttp\Client|null HTTP client instance */ | ||||
private static $client = null; | private static $client = null; | ||||
/** | /** | ||||
* Creates HTTP client for connections to OpenVidu server | * Creates HTTP client for connections to Meet server | ||||
* | * | ||||
* @return \GuzzleHttp\Client HTTP client instance | * @return \GuzzleHttp\Client HTTP client instance | ||||
*/ | */ | ||||
private function client() | private function client() | ||||
{ | { | ||||
if (!self::$client) { | if (!self::$client) { | ||||
self::$client = new \GuzzleHttp\Client( | self::$client = new \GuzzleHttp\Client( | ||||
[ | [ | ||||
'http_errors' => false, // No exceptions from Guzzle | 'http_errors' => false, // No exceptions from Guzzle | ||||
'base_uri' => \config('openvidu.api_url'), | 'base_uri' => \config('meet.api_url'), | ||||
'verify' => \config('openvidu.api_verify_tls'), | 'verify' => \config('meet.api_verify_tls'), | ||||
'auth' => [ | 'headers' => [ | ||||
\config('openvidu.api_username'), | 'X-Auth-Token' => \config('meet.api_token'), | ||||
\config('openvidu.api_password') | |||||
], | ], | ||||
'connect_timeout' => 10, | |||||
'timeout' => 10, | |||||
'on_stats' => function (\GuzzleHttp\TransferStats $stats) { | 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { | ||||
$threshold = \config('logging.slow_log'); | $threshold = \config('logging.slow_log'); | ||||
if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { | if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { | ||||
$url = $stats->getEffectiveUri(); | $url = $stats->getEffectiveUri(); | ||||
$method = $stats->getRequest()->getMethod(); | $method = $stats->getRequest()->getMethod(); | ||||
\Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); | \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); | ||||
} | } | ||||
}, | }, | ||||
] | ] | ||||
); | ); | ||||
} | } | ||||
return self::$client; | return self::$client; | ||||
} | } | ||||
/** | /** | ||||
* Destroy a OpenVidu connection | * Create a Meet session | ||||
* | |||||
* @param string $conn Connection identifier | |||||
* | |||||
* @return bool True on success, False otherwise | |||||
* @throws \Exception if session does not exist | |||||
*/ | |||||
public function closeOVConnection($conn): bool | |||||
{ | |||||
if (!$this->session_id) { | |||||
throw new \Exception("The room session does not exist"); | |||||
} | |||||
$url = 'sessions/' . $this->session_id . '/connection/' . urlencode($conn); | |||||
$response = $this->client()->request('DELETE', $url); | |||||
return $response->getStatusCode() == 204; | |||||
} | |||||
/** | |||||
* Fetch a OpenVidu connection information. | |||||
* | |||||
* @param string $conn Connection identifier | |||||
* | |||||
* @return ?array Connection data on success, Null otherwise | |||||
* @throws \Exception if session does not exist | |||||
*/ | |||||
public function getOVConnection($conn): ?array | |||||
{ | |||||
// Note: getOVConnection() not getConnection() because Eloquent\Model::getConnection() exists | |||||
// TODO: Maybe use some other name? getParticipant? | |||||
if (!$this->session_id) { | |||||
throw new \Exception("The room session does not exist"); | |||||
} | |||||
$url = 'sessions/' . $this->session_id . '/connection/' . urlencode($conn); | |||||
$response = $this->client()->request('GET', $url); | |||||
if ($response->getStatusCode() == 200) { | |||||
return json_decode($response->getBody(), true); | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* Create a OpenVidu session | |||||
* | * | ||||
* @return array|null Session data on success, NULL otherwise | * @return array|null Session data on success, NULL otherwise | ||||
*/ | */ | ||||
public function createSession(): ?array | public function createSession(): ?array | ||||
{ | { | ||||
$response = $this->client()->request( | $params = [ | ||||
'POST', | 'json' => [ /* request params here */ ] | ||||
"sessions", | ]; | ||||
[ | |||||
'json' => [ | $response = $this->client()->request('POST', "sessions", $params); | ||||
'mediaMode' => 'ROUTED', | |||||
'recordingMode' => 'MANUAL' | |||||
] | |||||
] | |||||
); | |||||
if ($response->getStatusCode() !== 200) { | if ($response->getStatusCode() !== 200) { | ||||
$this->logError("Failed to create the meet session", $response); | |||||
$this->session_id = null; | $this->session_id = null; | ||||
$this->save(); | $this->save(); | ||||
return null; | return null; | ||||
} | } | ||||
$session = json_decode($response->getBody(), true); | $session = json_decode($response->getBody(), true); | ||||
$this->session_id = $session['id']; | $this->session_id = $session['id']; | ||||
$this->save(); | $this->save(); | ||||
return $session; | return $session; | ||||
} | } | ||||
/** | /** | ||||
* Delete a OpenVidu session | * Create a Meet session (connection) token | ||||
* | |||||
* @return bool | |||||
*/ | |||||
public function deleteSession(): bool | |||||
{ | |||||
if (!$this->session_id) { | |||||
return true; | |||||
} | |||||
$response = $this->client()->request( | |||||
'DELETE', | |||||
"sessions/" . $this->session_id, | |||||
); | |||||
if ($response->getStatusCode() == 204) { | |||||
$this->session_id = null; | |||||
$this->save(); | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* Returns metadata for every connection in a session. | |||||
* | |||||
* @return array Connections metadata, indexed by connection identifier | |||||
* @throws \Exception if session does not exist | |||||
*/ | |||||
public function getSessionConnections(): array | |||||
{ | |||||
if (!$this->session_id) { | |||||
throw new \Exception("The room session does not exist"); | |||||
} | |||||
return Connection::where('session_id', $this->session_id) | |||||
// Ignore screen sharing connection for now | |||||
->whereRaw("(role & " . self::ROLE_SCREEN . ") = 0") | |||||
->get() | |||||
->keyBy('id') | |||||
->map(function ($item) { | |||||
// Warning: Make sure to not return all metadata here as it might contain sensitive data. | |||||
return [ | |||||
'role' => $item->role, | |||||
'hand' => $item->metadata['hand'] ?? 0, | |||||
'language' => $item->metadata['language'] ?? null, | |||||
]; | |||||
}) | |||||
// Sort by order in the queue, so UI can re-build the existing queue in order | |||||
->sort(function ($a, $b) { | |||||
return $a['hand'] <=> $b['hand']; | |||||
}) | |||||
->all(); | |||||
} | |||||
/** | |||||
* Create a OpenVidu session (connection) token | |||||
* | * | ||||
* @param int $role User role (see self::ROLE_* constants) | * @param int $role User role (see self::ROLE_* constants) | ||||
* | * | ||||
* @return array|null Token data on success, NULL otherwise | * @return array|null Token data on success, NULL otherwise | ||||
* @throws \Exception if session does not exist | * @throws \Exception if session does not exist | ||||
*/ | */ | ||||
public function getSessionToken($role = self::ROLE_SUBSCRIBER): ?array | public function getSessionToken($role = self::ROLE_SUBSCRIBER): ?array | ||||
{ | { | ||||
if (!$this->session_id) { | if (!$this->session_id) { | ||||
throw new \Exception("The room session does not exist"); | throw new \Exception("The room session does not exist"); | ||||
} | } | ||||
// FIXME: Looks like passing the role in 'data' param is the only way | |||||
// to make it visible for everyone in a room. So, for example we can | |||||
// handle/style subscribers/publishers/moderators differently on the | |||||
// client-side. Is this a security issue? | |||||
$data = ['role' => $role]; | |||||
$url = 'sessions/' . $this->session_id . '/connection'; | $url = 'sessions/' . $this->session_id . '/connection'; | ||||
$post = [ | $post = [ | ||||
'json' => [ | 'json' => [ | ||||
'role' => self::OV_ROLE_PUBLISHER, | 'role' => $role, | ||||
'data' => json_encode($data) | |||||
] | ] | ||||
]; | ]; | ||||
$response = $this->client()->request('POST', $url, $post); | $response = $this->client()->request('POST', $url, $post); | ||||
if ($response->getStatusCode() == 200) { | if ($response->getStatusCode() == 200) { | ||||
$json = json_decode($response->getBody(), true); | $json = json_decode($response->getBody(), true); | ||||
$authToken = base64_encode($json['id'] . ':' . \random_bytes(16)); | |||||
// Extract the 'token' part of the token, it will be used to authenticate the connection. | |||||
// It will be needed in next iterations e.g. to authenticate moderators that aren't | |||||
// Kolab4 users (or are just not logged in to Kolab4). | |||||
// FIXME: we could as well generate our own token for auth purposes | |||||
parse_str(parse_url($json['token'], PHP_URL_QUERY), $url); | |||||
// Create the connection reference in our database | |||||
$conn = new Connection(); | |||||
$conn->id = $json['id']; | |||||
$conn->session_id = $this->session_id; | |||||
$conn->room_id = $this->id; | |||||
$conn->role = $role; | |||||
$conn->metadata = ['token' => $url['token'], 'authToken' => $authToken]; | |||||
$conn->save(); | |||||
return [ | return [ | ||||
'session' => $this->session_id, | |||||
'token' => $json['token'], | 'token' => $json['token'], | ||||
'authToken' => $authToken, | |||||
'connectionId' => $json['id'], | |||||
'role' => $role, | 'role' => $role, | ||||
]; | ]; | ||||
} | } | ||||
$this->logError("Failed to create the meet peer connection", $response); | |||||
return null; | return null; | ||||
} | } | ||||
/** | /** | ||||
* Check if the room has an active session | * Check if the room has an active session | ||||
* | * | ||||
* @return bool True when the session exists, False otherwise | * @return bool True when the session exists, False otherwise | ||||
*/ | */ | ||||
public function hasSession(): bool | public function hasSession(): bool | ||||
{ | { | ||||
if (!$this->session_id) { | if (!$this->session_id) { | ||||
return false; | return false; | ||||
} | } | ||||
$response = $this->client()->request('GET', "sessions/{$this->session_id}"); | $response = $this->client()->request('GET', "sessions/{$this->session_id}"); | ||||
$this->logError("Failed to check that a meet session exists", $response); | |||||
return $response->getStatusCode() == 200; | return $response->getStatusCode() == 200; | ||||
} | } | ||||
/** | /** | ||||
* The room owner. | * The room owner. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo | ||||
*/ | */ | ||||
▲ Show 20 Lines • Show All 73 Lines • ▼ Show 20 Lines | class Room extends Model | ||||
/** | /** | ||||
* Any (additional) properties of this room. | * Any (additional) properties of this room. | ||||
* | * | ||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany | * @return \Illuminate\Database\Eloquent\Relations\HasMany | ||||
*/ | */ | ||||
public function settings() | public function settings() | ||||
{ | { | ||||
return $this->hasMany('App\OpenVidu\RoomSetting', 'room_id'); | return $this->hasMany('App\Meet\RoomSetting', 'room_id'); | ||||
} | } | ||||
/** | /** | ||||
* Send a OpenVidu signal to the session participants (connections) | * Send a signal to the Meet session participants (peers) | ||||
* | * | ||||
* @param string $name Signal name (type) | * @param string $name Signal name (type) | ||||
* @param array $data Signal data array | * @param array $data Signal data array | ||||
* @param null|int|string[] $target List of target connections, Null for all connections. | * @param int $target Limit targets by their participant role | ||||
* It can be also a participant role. | |||||
* | * | ||||
* @return bool True on success, False on failure | * @return bool True on success, False on failure | ||||
* @throws \Exception if session does not exist | * @throws \Exception if session does not exist | ||||
*/ | */ | ||||
public function signal(string $name, array $data = [], $target = null): bool | public function signal(string $name, array $data = [], $target = null): bool | ||||
{ | { | ||||
if (!$this->session_id) { | if (!$this->session_id) { | ||||
throw new \Exception("The room session does not exist"); | throw new \Exception("The room session does not exist"); | ||||
} | } | ||||
$post = [ | $post = [ | ||||
'session' => $this->session_id, | 'roomId' => $this->session_id, | ||||
'type' => $name, | 'type' => $name, | ||||
'data' => $data ? json_encode($data) : '', | 'role' => $target, | ||||
'data' => $data, | |||||
]; | ]; | ||||
// Get connection IDs by participant role | $response = $this->client()->request('POST', 'signal', ['json' => $post]); | ||||
if (is_int($target)) { | |||||
$connections = Connection::where('session_id', $this->session_id) | |||||
->whereRaw("(role & $target)") | |||||
->pluck('id') | |||||
->all(); | |||||
if (empty($connections)) { | $this->logError("Failed to send a signal to the meet session", $response); | ||||
return false; | |||||
} | |||||
$target = $connections; | return $response->getStatusCode() == 200; | ||||
} | } | ||||
if (!empty($target)) { | /** | ||||
$post['to'] = $target; | * Log an error for a failed request to the meet server | ||||
* | |||||
* @param string $str The error string | |||||
* @param object $response Guzzle client response | |||||
*/ | |||||
private function logError(string $str, $response) | |||||
{ | |||||
$code = $response->getStatusCode(); | |||||
if ($code != 200) { | |||||
\Log::error("$str [$code]"); | |||||
} | } | ||||
$response = $this->client()->request('POST', 'signal', ['json' => $post]); | |||||
return $response->getStatusCode() == 200; | |||||
} | } | ||||
} | } |