diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php
index 0678d895..8273720b 100644
--- a/src/app/Http/Controllers/API/V4/OpenViduController.php
+++ b/src/app/Http/Controllers/API/V4/OpenViduController.php
@@ -1,261 +1,388 @@
first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->requestAccept($reqid)) {
+ return $this->errorResponse(500, \trans('meet.session-request-accept-error'));
+ }
+
+ return response()->json(['status' => 'success']);
+ }
+
+ /**
+ * Denying the room join request.
+ *
+ * @param string $id Room identifier (name)
+ * @param string $reqid Request identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function denyJoinRequest($id, $reqid)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->requestDeny($reqid)) {
+ return $this->errorResponse(500, \trans('meet.session-request-deny-error'));
+ }
+
+ return response()->json(['status' => 'success']);
+ }
+
/**
* Close the room session.
*
* @param string $id Room identifier (name)
*
* @return \Illuminate\Http\JsonResponse
*/
public function closeRoom($id)
{
$room = Room::where('name', $id)->first();
// This isn't a room, bye bye
if (!$room) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
$user = Auth::guard()->user();
// Only the room owner can do it
if (!$user || $user->id != $room->user_id) {
return $this->errorResponse(403);
}
if (!$room->deleteSession()) {
return $this->errorResponse(500, \trans('meet.session-close-error'));
}
return response()->json([
'status' => 'success',
'message' => __('meet.session-close-success'),
]);
}
+ /**
+ * Accepting the room join request.
+ *
+ * @param string $id Room identifier (name)
+ * @param string $conn Connection identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function dismissConnection($id, $conn)
+ {
+ $room = Room::where('name', $id)->first();
+
+ // This isn't a room, bye bye
+ if (!$room) {
+ return $this->errorResponse(404, \trans('meet.room-not-found'));
+ }
+
+ $user = Auth::guard()->user();
+
+ // Only the room owner can do it
+ if (!$user || $user->id != $room->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ if (!$room->closeOVConnection($conn)) {
+ return $this->errorResponse(500, \trans('meet.session-dismiss-connection-error'));
+ }
+
+ return response()->json(['status' => 'success']);
+ }
+
/**
* Listing of rooms that belong to the current user.
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = Auth::guard()->user();
$rooms = Room::where('user_id', $user->id)->orderBy('name')->get();
if (count($rooms) == 0) {
// Create a room for the user (with a random and unique name)
while (true) {
$name = strtolower(\App\Utils::randStr(3, 3, '-'));
if (!Room::where('name', $name)->count()) {
break;
}
}
$room = Room::create([
'name' => $name,
'user_id' => $user->id
]);
$rooms = collect([$room]);
}
$result = [
'list' => $rooms,
'count' => count($rooms),
];
return response()->json($result);
}
/**
* Join the room session. Each room has one owner, and the room isn't open until the owner
* joins (and effectively creates the session).
*
* @param string $id Room identifier (name)
*
* @return \Illuminate\Http\JsonResponse
*/
public function joinRoom($id)
{
$room = Room::where('name', $id)->first();
// Room does not exist, or the owner is deleted
if (!$room || !$room->owner) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
// Check if there's still a valid beta entitlement for the room owner
$sku = \App\Sku::where('title', 'meet')->first();
if ($sku && !$room->owner->entitlements()->where('sku_id', $sku->id)->first()) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
$user = Auth::guard()->user();
$isOwner = $user && $user->id == $room->user_id;
// There's no existing session
if (!$room->hasSession()) {
// Participants can't join the room until the session is created by the owner
if (!$isOwner) {
return $this->errorResponse(423, \trans('meet.session-not-found'));
}
// The room owner can create the session on request
if (empty(request()->input('init'))) {
return $this->errorResponse(424, \trans('meet.session-not-found'));
}
$session = $room->createSession();
if (empty($session)) {
return $this->errorResponse(500, \trans('meet.session-create-error'));
}
}
$password = (string) $room->getSetting('password');
$config = [
'locked' => $room->getSetting('locked') === 'true',
'password' => $isOwner ? $password : '',
'requires_password' => !$isOwner && strlen($password),
];
// Validate room password
if (!$isOwner && strlen($password)) {
$request_password = request()->input('password');
if ($request_password !== $password) {
- // Note: We send the config to the client so it knows to display the password field
- $response = [
- 'config' => $config,
- 'message' => \trans('meet.session-password-error'),
- 'status' => 'error',
- ];
-
- return response()->json($response, 425);
+ return $this->errorResponse(425, \trans('meet.session-password-error'), ['config' => $config]);
+ }
+ }
+
+ // Handle locked room
+ if (!$isOwner && $config['locked']) {
+ $nickname = request()->input('nickname');
+ $picture = request()->input('picture');
+ $requestId = request()->input('requestId');
+
+ $request = $requestId ? $room->requestGet($requestId) : null;
+
+ $error = \trans('meet.session-room-locked-error');
+
+ // Request already has been processed (not accepted yet, but it could be denied)
+ if (empty($request['status']) || $request['status'] != Room::REQUEST_ACCEPTED) {
+ if (!$request) {
+ if (empty($nickname) || empty($requestId) || !preg_match('/^[a-z0-9]{8,32}$/i', $requestId)) {
+ return $this->errorResponse(426, $error, ['config' => $config]);
+ }
+
+ if (empty($picture)) {
+ $svg = file_get_contents(resource_path('images/user.svg'));
+ $picture = 'data:image/svg+xml;base64,' . base64_encode($svg);
+ } elseif (!preg_match('|^data:image/png;base64,[a-zA-Z0-9=+/]+$|', $picture)) {
+ return $this->errorResponse(426, $error, ['config' => $config]);
+ }
+
+ // TODO: Resize when big/make safe the user picture?
+
+ $request = ['nickname' => $nickname, 'requestId' => $requestId, 'picture' => $picture];
+
+ if (!$room->requestSave($requestId, $request)) {
+ // FIXME: should we use error code 500?
+ return $this->errorResponse(426, $error, ['config' => $config]);
+ }
+
+ // Send the request (signal) to the owner
+ $result = $room->signal('joinRequest', $request, Room::ROLE_MODERATOR);
+ }
+
+ return $this->errorResponse(427, $error, ['config' => $config]);
}
}
// Create session token for the current user/connection
- $response = $room->getSessionToken('PUBLISHER');
+ $response = $room->getSessionToken($isOwner ? Room::ROLE_MODERATOR : Room::ROLE_PUBLISHER);
if (empty($response)) {
return $this->errorResponse(500, \trans('meet.session-join-error'));
}
// Create session token for screen sharing connection
if (!empty(request()->input('screenShare'))) {
- $add_token = $room->getSessionToken('PUBLISHER');
+ $add_token = $room->getSessionToken(Room::ROLE_PUBLISHER);
$response['shareToken'] = $add_token['token'];
}
// Tell the UI who's the room owner
$response['owner'] = $isOwner;
// Append the room configuration
$response['config'] = $config;
return response()->json($response);
}
/**
* Set the domain configuration.
*
* @param string $id Room identifier (name)
*
* @return \Illuminate\Http\JsonResponse|void
*/
public function setRoomConfig($id)
{
$room = Room::where('name', $id)->first();
// Room does not exist, or the owner is deleted
if (!$room || !$room->owner) {
return $this->errorResponse(404);
}
$user = Auth::guard()->user();
// Only room owner can configure the room
if ($user->id != $room->user_id) {
return $this->errorResponse(403);
}
$input = request()->input();
$errors = [];
foreach ($input as $key => $value) {
switch ($key) {
case 'password':
if ($value === null || $value === '') {
$input[$key] = null;
} else {
// TODO: Do we have to validate the password in any way?
}
break;
case 'locked':
$input[$key] = $value ? 'true' : null;
break;
default:
$errors[$key] = \trans('meet.room-unsupported-option-error');
}
}
if (!empty($errors)) {
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
if (!empty($input)) {
$room->setSettings($input);
}
return response()->json([
'status' => 'success',
'message' => \trans('meet.room-setconfig-success'),
]);
}
/**
* Webhook as triggered from OpenVidu server
*
* @param \Illuminate\Http\Request $request The API request.
*
* @return \Illuminate\Http\Response The response
*/
public function webhook(Request $request)
{
\Log::debug($request->getContent());
switch ((string) $request->input('event')) {
case 'sessionDestroyed':
// When all participants left the room OpenVidu dispatches sessionDestroyed
// event. We'll remove the session reference from the database.
$sessionId = $request->input('sessionId');
$room = Room::where('session_id', $sessionId)->first();
if ($room) {
$room->session_id = null;
$room->save();
}
break;
}
return response('Success', 200);
}
}
diff --git a/src/app/Http/Controllers/Controller.php b/src/app/Http/Controllers/Controller.php
index 25ed87c3..9e957987 100644
--- a/src/app/Http/Controllers/Controller.php
+++ b/src/app/Http/Controllers/Controller.php
@@ -1,44 +1,49 @@
"Bad request",
401 => "Unauthorized",
403 => "Access denied",
404 => "Not found",
422 => "Input validation error",
405 => "Method not allowed",
500 => "Internal server error",
];
$response = [
'status' => 'error',
'message' => $message ?: (isset($errors[$code]) ? $errors[$code] : "Server error"),
];
+ if (!empty($data)) {
+ $response = $response + $data;
+ }
+
return response()->json($response, $code);
}
}
diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php
index 3579fab5..02187684 100644
--- a/src/app/OpenVidu/Room.php
+++ b/src/app/OpenVidu/Room.php
@@ -1,174 +1,361 @@
false, // No exceptions from Guzzle
'base_uri' => \config('openvidu.api_url'),
'verify' => \config('openvidu.api_verify_tls'),
'auth' => [
\config('openvidu.api_username'),
\config('openvidu.api_password')
]
]
);
}
return self::$client;
}
+ /**
+ * Destroy a OpenVidu connection
+ *
+ * @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
*/
public function createSession(): ?array
{
$response = $this->client()->request(
'POST',
"sessions",
[
'json' => [
'mediaMode' => 'ROUTED',
'recordingMode' => 'MANUAL'
]
]
);
if ($response->getStatusCode() !== 200) {
$this->session_id = null;
$this->save();
+ return null;
}
$session = json_decode($response->getBody(), true);
$this->session_id = $session['id'];
$this->save();
return $session;
}
/**
* Delete a OpenVidu session
*
* @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;
}
/**
* Create a OpenVidu session (connection) token
*
* @return array|null Token data on success, NULL otherwise
*/
- public function getSessionToken($role = 'PUBLISHER'): ?array
+ public function getSessionToken($role = self::ROLE_PUBLISHER): ?array
{
$response = $this->client()->request(
'POST',
'tokens',
[
'json' => [
'session' => $this->session_id,
'role' => $role
]
]
);
if ($response->getStatusCode() == 200) {
$json = json_decode($response->getBody(), true);
return $json;
}
return null;
}
/**
* Check if the room has an active session
*
* @return bool True when the session exists, False otherwise
*/
public function hasSession(): bool
{
if (!$this->session_id) {
return false;
}
$response = $this->client()->request('GET', "sessions/{$this->session_id}");
return $response->getStatusCode() == 200;
}
/**
* The room owner.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function owner()
{
return $this->belongsTo('\App\User', 'user_id', 'id');
}
+ /**
+ * Accept the join request.
+ *
+ * @param string $id Request identifier
+ *
+ * @return bool True on success, False on failure
+ */
+ public function requestAccept(string $id): bool
+ {
+ $request = Cache::get($this->session_id . '-' . $id);
+
+ if ($request) {
+ $request['status'] = self::REQUEST_ACCEPTED;
+
+ return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
+ }
+
+ return false;
+ }
+
+ /**
+ * Deny the join request.
+ *
+ * @param string $id Request identifier
+ *
+ * @return bool True on success, False on failure
+ */
+ public function requestDeny(string $id): bool
+ {
+ $request = Cache::get($this->session_id . '-' . $id);
+
+ if ($request) {
+ $request['status'] = self::REQUEST_DENIED;
+
+ return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the join request data.
+ *
+ * @param string $id Request identifier
+ *
+ * @return array|null Request data (e.g. nickname, status, picture?)
+ */
+ public function requestGet(string $id): ?array
+ {
+ return Cache::get($this->session_id . '-' . $id);
+ }
+
+ /**
+ * Save the join request.
+ *
+ * @param string $id Request identifier
+ * @param array $request Request data
+ *
+ * @return bool True on success, False on failure
+ */
+ public function requestSave(string $id, array $request): bool
+ {
+ // We don't really need the picture in the cache
+ // As we use this cache for the request status only
+ unset($request['picture']);
+
+ return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
+ }
+
/**
* Any (additional) properties of this room.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function settings()
{
return $this->hasMany('App\OpenVidu\RoomSetting', 'room_id');
}
+
+ /**
+ * Send a OpenVidu signal to the session participants (connections)
+ *
+ * @param string $name Signal name (type)
+ * @param array $data Signal data array
+ * @param array|string $target List of target connections, Null for all connections.
+ * It can be also a participant role.
+ *
+ * @return bool True on success, False on failure
+ * @throws \Exception if session does not exist
+ */
+ public function signal(string $name, array $data = [], $target = null): bool
+ {
+ if (!$this->session_id) {
+ throw new \Exception("The room session does not exist");
+ }
+
+ $post = [
+ 'session' => $this->session_id,
+ 'type' => $name,
+ 'data' => $data ? json_encode($data) : '',
+ ];
+
+ // Get connection IDs by participant role
+ if (is_string($target)) {
+ // TODO: We should probably store this in our database/redis. I foresee a use-case
+ // for such a connections store on our side, e.g. keeping participant
+ // metadata, e.g. selected language, extra roles like a "language interpreter", etc.
+
+ $response = $this->client()->request('GET', 'sessions/' . $this->session_id);
+
+ if ($response->getStatusCode() !== 200) {
+ return false;
+ }
+
+ $json = json_decode($response->getBody(), true);
+ $connections = [];
+
+ foreach ($json['connections']['content'] as $connection) {
+ if ($connection['role'] === $target) {
+ $connections[] = $connection['id'];
+ break;
+ }
+ }
+
+ if (empty($connections)) {
+ return false;
+ }
+
+ $target = $connections;
+ }
+
+ if (!empty($target)) {
+ $post['to'] = $target;
+ }
+
+ $response = $this->client()->request('POST', 'signal', ['json' => $post]);
+
+ return $response->getStatusCode() == 200;
+ }
}
diff --git a/src/resources/images/user.svg b/src/resources/images/user.svg
new file mode 100644
index 00000000..c976281c
--- /dev/null
+++ b/src/resources/images/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/resources/js/meet/app.js b/src/resources/js/meet/app.js
index d01ad7d3..7373f529 100644
--- a/src/resources/js/meet/app.js
+++ b/src/resources/js/meet/app.js
@@ -1,964 +1,988 @@
import anchorme from 'anchorme'
import { library } from '@fortawesome/fontawesome-svg-core'
import { OpenVidu } from 'openvidu-browser'
function Meet(container)
{
let OV // OpenVidu object to initialize a session
let session // Session object where the user will connect
let publisher // Publisher object which the user will publish
let audioActive = false // True if the audio track of the publisher is active
let videoActive = false // True if the video track of the publisher is active
let numOfVideos = 0 // Keeps track of the number of videos that are being shown
let audioSource = '' // Currently selected microphone
let videoSource = '' // Currently selected camera
let sessionData // Room session metadata
+ let role // Current user role
let screenOV // OpenVidu object to initialize a screen sharing session
let screenSession // Session object where the user will connect for screen sharing
let screenPublisher // Publisher object which the user will publish the screen sharing
let publisherDefaults = {
publishAudio: true, // Whether to start publishing with your audio unmuted or not
publishVideo: true, // Whether to start publishing with your video enabled or not
resolution: '640x480', // The resolution of your video
frameRate: 30, // The frame rate of your video
mirror: true // Whether to mirror your local video or not
}
let cameras = [] // List of user video devices
let microphones = [] // List of user audio devices
let connections = {} // Connected users in the session
let containerWidth
let containerHeight
let chatCount = 0
let volumeElement
let setupProps
OV = new OpenVidu()
screenOV = new OpenVidu()
// If there's anything to do, do it here.
//OV.setAdvancedConfiguration(config)
// Disable all logging except errors
// OV.enableProdMode()
// Disconnect participant when browser's window close
window.addEventListener('beforeunload', () => {
leaveRoom()
})
window.addEventListener('resize', resize)
// Public methods
this.isScreenSharingSupported = isScreenSharingSupported
this.joinRoom = joinRoom
this.leaveRoom = leaveRoom
this.setup = setup
this.setupSetAudioDevice = setupSetAudioDevice
this.setupSetVideoDevice = setupSetVideoDevice
this.switchAudio = switchAudio
this.switchScreen = switchScreen
this.switchVideo = switchVideo
this.updateSession = updateSession
/**
* Join the room session
*
* @param data Session metadata and event handlers (session, token, shareToken, nickname,
- * chatElement, menuElement, onDestroy)
+ * chatElement, menuElement, onDestroy, onJoinRequest)
*/
function joinRoom(data) {
resize();
volumeMeterStop()
data.params = {
nickname: data.nickname, // user nickname
// avatar: undefined // avatar image
}
sessionData = data
// Init a session
session = OV.initSession()
// Handle connection creation events
session.on('connectionCreated', event => {
// Ignore the current user connection
if (event.connection.role) {
+ role = event.connection.role
return
}
// This is the first event executed when a user joins in.
// We'll create the video wrapper here, which will be re-used
// in 'streamCreated' event handler.
// Note: For a user with no cam/mic enabled streamCreated event
// is not being dispatched at all
// TODO: We may consider placing users with no video enabled
// in a separate place, so they do not fill the precious
// screen estate
let connectionId = event.connection.connectionId
let metadata = JSON.parse(event.connection.data)
+ metadata.connId = connectionId
let wrapper = videoWrapperCreate(container, metadata)
connections[connectionId] = {
element: wrapper
}
updateLayout()
// Send the current user status to the connecting user
// otherwise e.g. nickname might be not up to date
signalUserUpdate(event.connection)
})
session.on('connectionDestroyed', event => {
let conn = connections[event.connection.connectionId]
if (conn) {
$(conn.element).remove()
numOfVideos--
updateLayout()
delete connections[event.connection.connectionId]
}
})
// On every new Stream received...
session.on('streamCreated', event => {
let connection = event.stream.connection
let connectionId = connection.connectionId
let metadata = JSON.parse(connection.data)
let wrapper = connections[connectionId].element
let props = {
// Prepend the video element so it is always before the watermark element
insertMode: 'PREPEND'
}
// Subscribe to the Stream to receive it
let subscriber = session.subscribe(event.stream, wrapper, props);
subscriber.on('videoElementCreated', event => {
$(event.element).prop({
tabindex: -1
})
updateLayout()
})
/*
subscriber.on('videoElementDestroyed', event => {
})
*/
// Update the wrapper controls/status
videoWrapperUpdate(wrapper, event.stream)
})
/*
session.on('streamDestroyed', event => {
})
*/
// Handle session disconnection events
session.on('sessionDisconnected', event => {
if (data.onDestroy) {
data.onDestroy(event)
}
updateLayout()
})
// Handle signals from all participants
session.on('signal', signalEventHandler)
// Connect with the token
session.connect(data.token, data.params)
.then(() => {
let params = { publisher: true, audioActive, videoActive }
let wrapper = videoWrapperCreate(container, Object.assign({}, data.params, params))
publisher.on('videoElementCreated', event => {
$(event.element).prop({
muted: true, // Mute local video to avoid feedback
disablePictureInPicture: true, // this does not work in Firefox
tabindex: -1
})
updateLayout()
})
publisher.createVideoElement(wrapper, 'PREPEND')
sessionData.wrapper = wrapper
// Publish the stream
session.publish(publisher)
})
.catch(error => {
console.error('There was an error connecting to the session: ', error.message);
})
// Prepare the chat
setupChat()
}
/**
* Leave the room (disconnect)
*/
function leaveRoom() {
if (publisher) {
volumeMeterStop()
// FIXME: We have to unpublish streams only if there's no session yet
if (!session && audioActive) {
publisher.publishAudio(false)
}
if (!session && videoActive) {
publisher.publishVideo(false)
}
publisher = null
}
if (session) {
session.disconnect();
session = null
}
if (screenSession) {
screenSession.disconnect();
screenSession = null
}
}
/**
* Sets the audio and video devices for the session.
* This will ask user for permission to access media devices.
*
* @param props Setup properties (videoElement, volumeElement, onSuccess, onError)
*/
function setup(props) {
setupProps = props
publisher = OV.initPublisher(undefined, publisherDefaults)
publisher.once('accessDenied', error => {
props.onError(error)
})
publisher.once('accessAllowed', async () => {
let mediaStream = publisher.stream.getMediaStream()
let videoStream = mediaStream.getVideoTracks()[0]
let audioStream = mediaStream.getAudioTracks()[0]
audioActive = !!audioStream
videoActive = !!videoStream
volumeElement = props.volumeElement
publisher.addVideoElement(props.videoElement)
volumeMeterStart()
const devices = await OV.getDevices()
devices.forEach(device => {
// device's props: deviceId, kind, label
if (device.kind == 'videoinput') {
cameras.push(device)
if (videoStream && videoStream.label == device.label) {
videoSource = device.deviceId
}
} else if (device.kind == 'audioinput') {
microphones.push(device)
if (audioStream && audioStream.label == device.label) {
audioSource = device.deviceId
}
}
})
props.onSuccess({
microphones,
cameras,
audioSource,
videoSource,
audioActive,
videoActive
})
})
}
/**
* Change the publisher audio device
*
* @param deviceId Device identifier string
*/
async function setupSetAudioDevice(deviceId) {
if (!deviceId) {
publisher.publishAudio(false)
volumeMeterStop()
audioActive = false
} else if (deviceId == audioSource) {
publisher.publishAudio(true)
volumeMeterStart()
audioActive = true
} else {
const mediaStream = publisher.stream.mediaStream
const oldTrack = mediaStream.getAudioTracks()[0]
let properties = Object.assign({}, publisherDefaults, {
publishAudio: true,
publishVideo: videoActive,
audioSource: deviceId,
videoSource: videoSource
})
volumeMeterStop()
// Note: We're not using publisher.replaceTrack() as it wasn't working for me
// Stop and remove the old track
if (oldTrack) {
oldTrack.stop()
mediaStream.removeTrack(oldTrack)
}
// TODO: Handle errors
await OV.getUserMedia(properties)
.then(async (newMediaStream) => {
publisher.stream.mediaStream = newMediaStream
volumeMeterStart()
audioActive = true
audioSource = deviceId
})
}
return audioActive
}
/**
* Change the publisher video device
*
* @param deviceId Device identifier string
*/
async function setupSetVideoDevice(deviceId) {
if (!deviceId) {
publisher.publishVideo(false)
videoActive = false
} else if (deviceId == videoSource) {
publisher.publishVideo(true)
videoActive = true
} else {
const mediaStream = publisher.stream.mediaStream
const oldTrack = mediaStream.getAudioTracks()[0]
let properties = Object.assign({}, publisherDefaults, {
publishAudio: audioActive,
publishVideo: true,
audioSource: audioSource,
videoSource: deviceId
})
volumeMeterStop()
// Stop and remove the old track
if (oldTrack) {
oldTrack.stop()
mediaStream.removeTrack(oldTrack)
}
// TODO: Handle errors
await OV.getUserMedia(properties)
.then(async (newMediaStream) => {
publisher.stream.mediaStream = newMediaStream
volumeMeterStart()
videoActive = true
videoSource = deviceId
})
}
return videoActive
}
/**
* Setup the chat UI
*/
function setupChat() {
// The UI elements are created in the vue template
// Here we add a logic for how they work
const textarea = $(sessionData.chatElement).find('textarea')
const button = $(sessionData.menuElement).find('.link-chat')
textarea.on('keydown', e => {
if (e.keyCode == 13 && !e.shiftKey) {
if (textarea.val().length) {
signalChat(textarea.val())
textarea.val('')
}
return false
}
})
// Add an element for the count of unread messages on the chat button
button.append('')
.on('click', () => {
button.find('.badge').text('')
chatCount = 0
})
}
/**
* Signal events handler
*/
function signalEventHandler(signal) {
let conn, data
- let connId = signal.from.connectionId
+ let connId = signal.from ? signal.from.connectionId : null
switch (signal.type) {
case 'signal:userChanged':
if (conn = connections[connId]) {
data = JSON.parse(signal.data)
videoWrapperUpdate(conn.element, data)
nicknameUpdate(data.nickname, connId)
}
break
case 'signal:chat':
data = JSON.parse(signal.data)
data.id = connId
pushChatMessage(data)
break
+
+ case 'signal:joinRequest':
+ if (sessionData.onJoinRequest) {
+ sessionData.onJoinRequest(JSON.parse(signal.data))
+ }
+ break;
}
}
/**
* Send the chat message to other participants
*
* @param message Message string
*/
function signalChat(message) {
let data = {
nickname: sessionData.params.nickname,
message
}
session.signal({
data: JSON.stringify(data),
type: 'chat'
})
}
/**
* Add a message to the chat
*
* @param data Object with a message, nickname, id (of the connection, empty for self)
*/
function pushChatMessage(data) {
let message = $('').text(data.message).text() // make the message secure
// Format the message, convert emails and urls to links
message = anchorme({
input: message,
options: {
attributes: {
target: "_blank"
},
// any link above 20 characters will be truncated
// to 20 characters and ellipses at the end
truncate: 20,
// characters will be taken out of the middle
middleTruncation: true
}
// TODO: anchorme is extensible, we could support
// github/phabricator's markup e.g. backticks for code samples
})
message = message.replace(/\r?\n/, ' ')
// Display the message
let isSelf = data.id == publisher.stream.connection.connectionId
let chat = $(sessionData.chatElement).find('.chat')
let box = chat.find('.message').last()
message = $('
').html(message)
message.find('a').attr('rel', 'noreferrer')
if (box.length && box.data('id') == data.id) {
// A message from the same user as the last message, no new box needed
message.appendTo(box)
} else {
box = $('
').data('id', data.id)
.append($('
').text(data.nickname || ''))
.append(message)
.appendTo(chat)
if (isSelf) {
box.addClass('self')
}
}
// Count unread messages
if (!$(sessionData.chatElement).is('.open')) {
if (!isSelf) {
chatCount++
}
} else {
chatCount = 0
}
$(sessionData.menuElement).find('.link-chat .badge').text(chatCount ? chatCount : '')
}
/**
* Send the user properties update signal to other participants
*
* @param connection Optional connection to which the signal will be sent
* If not specified the signal is sent to all participants
*/
function signalUserUpdate(connection) {
let data = {
audioActive,
videoActive,
nickname: sessionData.params.nickname
}
// Note: StreamPropertyChangedEvent might be more standard way
// to propagate the audio/video state change to other users.
// It looks there's no other way to propagate nickname changes.
session.signal({
data: JSON.stringify(data),
type: 'userChanged',
to: connection ? [connection] : undefined
})
// The same nickname for screen sharing session
if (screenSession) {
data.audioActive = false
data.videoActive = true
screenSession.signal({
data: JSON.stringify(data),
type: 'userChanged',
to: connection ? [connection] : undefined
})
}
}
/**
* Mute/Unmute audio for current session publisher
*/
function switchAudio() {
// TODO: If user has no devices or denied access to them in the setup,
// the button will just not work. Find a way to make it working
// after user unlocks his devices. For now he has to refresh
// the page and join the room again.
if (microphones.length) {
try {
publisher.publishAudio(!audioActive)
audioActive = !audioActive
videoWrapperUpdate(sessionData.wrapper, { audioActive })
signalUserUpdate()
} catch (e) {
console.error(e)
}
}
return audioActive
}
/**
* Mute/Unmute video for current session publisher
*/
function switchVideo() {
// TODO: If user has no devices or denied access to them in the setup,
// the button will just not work. Find a way to make it working
// after user unlocks his devices. For now he has to refresh
// the page and join the room again.
if (cameras.length) {
try {
publisher.publishVideo(!videoActive)
videoActive = !videoActive
videoWrapperUpdate(sessionData.wrapper, { videoActive })
signalUserUpdate()
} catch (e) {
console.error(e)
}
}
return videoActive
}
/**
* Switch on/off screen sharing
*/
function switchScreen(callback) {
if (screenPublisher) {
screenSession.disconnect()
screenSession = null
screenPublisher = null
if (callback) {
// Note: Disconnecting invalidates the token. The callback should request
// a new token for the next screen sharing session.
callback(false)
}
return
}
screenConnect(callback)
}
/**
* Detect if screen sharing is supported by the browser
*/
function isScreenSharingSupported() {
return !!OV.checkScreenSharingCapabilities();
}
/**
* Update nickname in chat
*
* @param nickname Nickname
* @param connectionId Connection identifier of the user
*/
function nicknameUpdate(nickname, connectionId) {
if (connectionId) {
$(sessionData.chatElement).find('.chat').find('.message').each(function() {
let elem = $(this)
if (elem.data('id') == connectionId) {
elem.find('.nickname').text(nickname || '')
}
})
}
}
/**
* Create a