Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@
'token' => '^[a-z0-9]{4,30}$',
],
],
[
'name' => 'Call#updateCallFlags',
'url' => '/api/{apiVersion}/call/{token}',
'verb' => 'PUT',
'requirements' => [
'apiVersion' => 'v(4)',
'token' => '^[a-z0-9]{4,30}$',
],
],
[
'name' => 'Call#leaveCall',
'url' => '/api/{apiVersion}/call/{token}',
Expand Down
20 changes: 20 additions & 0 deletions docs/call.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@
+ `404 Not Found` When the user did not join the conversation before
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator

## Update call flags

* Method: `PUT`
* Endpoint: `/call/{token}`
* Data:

field | type | Description
---|---|---
`flags` | int | Flags what streams are provided by the participant (see [Constants - Participant in-call flag](constants.md#participant-in-call-flag))

* Response:
- Status code:
+ `200 OK`
+ `400 Bad Request` When the user is not in the call
+ `400 Bad Request` When the flags do not contain "in call"
+ `403 Forbidden` When the conversation is read-only
+ `404 Not Found` When the conversation could not be found for the participant
+ `404 Not Found` When the user did not join the conversation before
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator

## Leave a call (but staying in the conversation for future calls and chat)

* Method: `DELETE`
Expand Down
22 changes: 22 additions & 0 deletions lib/Controller/CallController.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ public function joinCall(?int $flags): DataResponse {
return new DataResponse();
}

/**
* @PublicPage
* @RequireParticipant
*
* @param int flags
* @return DataResponse
*/
public function updateCallFlags(int $flags): DataResponse {
$session = $this->participant->getSession();
if (!$session instanceof Session) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

try {
$this->participantService->updateCallFlags($this->room, $this->participant, $flags);
} catch (\Exception $exception) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

return new DataResponse();
}

/**
* @PublicPage
* @RequireParticipant
Expand Down
2 changes: 2 additions & 0 deletions lib/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class Room {
public const EVENT_AFTER_GUESTS_CLEAN = self::class . '::postCleanGuests';
public const EVENT_BEFORE_SESSION_JOIN_CALL = self::class . '::preSessionJoinCall';
public const EVENT_AFTER_SESSION_JOIN_CALL = self::class . '::postSessionJoinCall';
public const EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS = self::class . '::preSessionUpdateCallFlags';
public const EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS = self::class . '::postSessionUpdateCallFlags';
public const EVENT_BEFORE_SESSION_LEAVE_CALL = self::class . '::preSessionLeaveCall';
public const EVENT_AFTER_SESSION_LEAVE_CALL = self::class . '::postSessionLeaveCall';
public const EVENT_BEFORE_SIGNALING_PROPERTIES = self::class . '::beforeSignalingProperties';
Expand Down
23 changes: 23 additions & 0 deletions lib/Service/ParticipantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,29 @@ public function changeInCall(Room $room, Participant $participant, int $flags):
}
}

public function updateCallFlags(Room $room, Participant $participant, int $flags): void {
$session = $participant->getSession();
if (!$session instanceof Session) {
return;
}

if (!($session->getInCall() & Participant::FLAG_IN_CALL)) {
throw new \Exception('Participant not in call');
}

if (!($flags & Participant::FLAG_IN_CALL)) {
throw new \InvalidArgumentException('Invalid flags');
}

$event = new ModifyParticipantEvent($room, $participant, 'inCall', $flags, $session->getInCall());
$this->dispatcher->dispatch(Room::EVENT_BEFORE_SESSION_UPDATE_CALL_FLAGS, $event);

$session->setInCall($flags);
$this->sessionMapper->update($session);

$this->dispatcher->dispatch(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $event);
}

public function markUsersAsMentioned(Room $room, array $userIds, int $messageId): void {
$query = $this->connection->getQueryBuilder();
$query->update('talk_attendees')
Expand Down
2 changes: 2 additions & 0 deletions lib/Signaling/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ protected static function registerInternalSignaling(IEventDispatcher $dispatcher
$dispatcher->addListener(Room::EVENT_AFTER_ROOM_CONNECT, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_GUEST_CONNECT, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener);
$dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, $listener);

Expand Down Expand Up @@ -241,6 +242,7 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher
}
};
$dispatcher->addListener(Room::EVENT_AFTER_SESSION_JOIN_CALL, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_SESSION_UPDATE_CALL_FLAGS, $listener);
$dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, $listener);

$dispatcher->addListener(Room::EVENT_AFTER_GUESTS_CLEAN, static function (RoomEvent $event) {
Expand Down
33 changes: 32 additions & 1 deletion src/utils/signaling.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,16 @@ Signaling.Base.prototype.leaveCurrentRoom = function() {
}
}

Signaling.Base.prototype.updateCurrentCallFlags = function(flags) {
return new Promise((resolve, reject) => {
if (this.currentCallToken) {
this.updateCallFlags(this.currentCallToken, flags).then(() => { resolve() }).catch(reason => { reject(reason) })
} else {
resolve()
}
})
}

Signaling.Base.prototype.leaveCurrentCall = function() {
return new Promise((resolve, reject) => {
if (this.currentCallToken) {
Expand Down Expand Up @@ -266,6 +276,27 @@ Signaling.Base.prototype._leaveCallSuccess = function(/* token */) {
// Override in subclasses if necessary.
}

Signaling.Base.prototype.updateCallFlags = function(token, flags) {
return new Promise((resolve, reject) => {
if (!token) {
reject(new Error())
return
}

axios.put(generateOcsUrl('apps/spreed/api/v4/call/{token}', { token }), {
flags: flags,
})
.then(function() {
this.currentCallFlags = flags
this._trigger('updateCallFlags', [token, flags])
resolve()
}.bind(this))
.catch(function() {
reject(new Error())
})
})
}

Signaling.Base.prototype.leaveCall = function(token, keepToken) {
return new Promise((resolve, reject) => {
if (!token) {
Expand Down Expand Up @@ -343,7 +374,7 @@ Signaling.Internal.prototype.forceReconnect = function(newSession, flags) {
// FIXME Naive reconnection routine; as the same session is kept peers
// must be explicitly ended before the reconnection is forced.
this.leaveCall(this.currentCallToken, true)
this.joinCall(this.currentCallToken)
this.joinCall(this.currentCallToken, this.currentCallFlags)
}

Signaling.Internal.prototype._sendMessageWithCallback = function(ev) {
Expand Down
56 changes: 50 additions & 6 deletions src/utils/webrtc/webrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,33 @@ export default function initWebRTC(signaling, _callParticipantCollection, _local
// is established, but forcing a reconnection should be done only
// once the connection was established.
if (peer.pc.iceConnectionState !== 'new' && peer.pc.iceConnectionState !== 'checking') {
forceReconnect(signaling)
// Update the media flags if needed, as the renegotiation could
// have been caused by tracks being added or removed.
const audioSender = peer.pc.getSenders().find((sender) => sender.track && sender.track.kind === 'audio')
const videoSender = peer.pc.getSenders().find((sender) => sender.track && sender.track.kind === 'video')

let flags = signaling.getCurrentCallFlags()
if (audioSender) {
flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO
} else {
flags &= ~PARTICIPANT.CALL_FLAG.WITH_AUDIO
}
if (videoSender) {
flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO
} else {
flags &= ~PARTICIPANT.CALL_FLAG.WITH_VIDEO
}

// Negotiation is expected to be needed only when a new track is
// added to or removed from a peer. Therefore if the HPB is used
// the negotiation will be needed in the own peer, but if the
// HPB is not used it will be needed in all peers. However, in
// that case as soon as the forced reconnection is triggered all
// the peers will be cleared, so in practice there will be just
// one forced reconnection even if there are several peers.
// FIXME: despite all of the above this is a dirty and ugly hack
// that should be fixed with proper renegotiation.
forceReconnect(signaling, flags)
}
})
}
Expand Down Expand Up @@ -942,16 +968,34 @@ export default function initWebRTC(signaling, _callParticipantCollection, _local
}
})

webrtc.on('localTrackReplaced', function(newTrack /*, oldTrack, stream */) {
// Device disabled, nothing to do here.
webrtc.on('localTrackReplaced', function(newTrack, oldTrack/*, stream */) {
// Device disabled, just update the call flags.
if (!newTrack) {
if (oldTrack && oldTrack.kind === 'audio') {
signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() & ~PARTICIPANT.CALL_FLAG.WITH_AUDIO)
} else if (oldTrack && oldTrack.kind === 'video') {
signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() & ~PARTICIPANT.CALL_FLAG.WITH_VIDEO)
}

return
}

// If the call was started with media the connections will be already
// established. If it has not started yet the connections will be
// established once started.
if (startedWithMedia || startedWithMedia === undefined) {
// established. The flags need to be updated if a device was enabled
// (but not if it was switched to another one).
if (startedWithMedia) {
if (newTrack.kind === 'audio' && !oldTrack) {
signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() | PARTICIPANT.CALL_FLAG.WITH_AUDIO)
} else if (newTrack.kind === 'video' && !oldTrack) {
signaling.updateCurrentCallFlags(signaling.getCurrentCallFlags() | PARTICIPANT.CALL_FLAG.WITH_VIDEO)
}

return
}

// If the call has not started with media yet the connections will be
// established once started, as well as the flags.
if (startedWithMedia === undefined) {
return
}

Expand Down
20 changes: 19 additions & 1 deletion tests/integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ public function userLoadsAttendeeIdsInRoom(string $user, string $identifier, str
}

protected function sortAttendees(array $a1, array $a2): int {
if ($a1['participantType'] !== $a2['participantType']) {
if (array_key_exists('participantType', $a1) && array_key_exists('participantType', $a2) && $a1['participantType'] !== $a2['participantType']) {
return $a1['participantType'] <=> $a2['participantType'];
}
if ($a1['actorType'] !== $a2['actorType']) {
Expand Down Expand Up @@ -1083,6 +1083,24 @@ public function userJoinsCall(string $user, string $identifier, int $statusCode,
}
}

/**
* @Then /^user "([^"]*)" updates call flags in room "([^"]*)" to "([^"]*)" with (\d+) \((v4)\)$/
*
* @param string $user
* @param string $identifier
* @param string $flags
* @param int $statusCode
* @param string $apiVersion
*/
public function userUpdatesCallFlagsInRoomTo(string $user, string $identifier, string $flags, int $statusCode, string $apiVersion): void {
$this->setCurrentUser($user);
$this->sendRequest(
'PUT', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier],
new TableNode([['flags', $flags]])
);
$this->assertStatusCode($this->response, $statusCode);
}

/**
* @Then /^user "([^"]*)" leaves call "([^"]*)" with (\d+) \((v4)\)$/
*
Expand Down
Loading