Join GitHub today
GitHub is home to over 20 million developers working together to host and review code, manage projects, and build software together.
Add call events and request of turn servers #110
Conversation
mariogrip
added some commits
Nov 7, 2017
KitsuneRal
self-requested a review
Nov 7, 2017
KitsuneRal
self-assigned this
Nov 7, 2017
KitsuneRal
added
the
enhancement
label
Nov 7, 2017
KitsuneRal
added this to Backlog
in Roadmap
via automation
Nov 7, 2017
KitsuneRal
moved this from Backlog
to Version 0.2 - To Do
in Roadmap
Nov 7, 2017
KitsuneRal
requested changes
Nov 7, 2017
•
Overall, I can't express my appreciation that you've taken on this task. VoIP has been a very much requested feature, and the Librem 5 phone might very much use this code as well (if they choose to use libqmatrixclient, that is...). So even though I have a lot of comments on your code, please accept my huge thanks for your effort, to begin with. Now to the critique :)
| @@ -96,6 +96,7 @@ namespace QMatrixClient | ||
| */ | ||
| ForgetRoomJob* forgetRoom(const QString& id); | ||
| + |
| + | ||
| + private: | ||
| + int _lifetime; | ||
| + QJsonObject _offer; |
KitsuneRal
Nov 7, 2017
Owner
I'm not sure how much it's worth it to store QJsonObject here. I'd probably simply store the SDP text instead (we only have type=="offer" anyway). In general, we try to not expose JSON to the clients because both the JSON processing engine and even the transfer protocol (JSON, that is) may, even though in theory yet, change for something else (Qt JSON -> RapidJson, or JSON -> COAP, e.g.).
| @@ -42,6 +42,7 @@ namespace QMatrixClient | ||
| RoomName = RoomStateEventBase + 1, | ||
| RoomAliases, RoomCanonicalAlias, RoomMember, RoomTopic, | ||
| RoomAvatar, RoomEncryption, | ||
| + CallInvite, CallCandidates, CallAnswer, CallHangup, |
KitsuneRal
Nov 7, 2017
•
Owner
I think it makes sense to use another bit (pick your favourite from the higher byte) to indicate that an event is a call event. Doesn't change much so far; but clients might want to check if an event is a call event to adorn it with a relevant icon, e.g. See the code for Event::isStateEvent(). On a related note, you should move the new events to the room non-state events block (before RoomStateEventBase).
| +class TurnServerJob::Private | ||
| +{ | ||
| + public: | ||
| + QJsonObject _turnObject; |
KitsuneRal
Nov 7, 2017
Owner
Same as the comment above, please unroll _turnObject into actual data stored in it.
mariogrip
Nov 7, 2017
This also gets passed directly to the "javascript" side, thats why i store it as a jsonobj. Does it make sense to spit it up to then assemble it again?
KitsuneRal
Nov 7, 2017
Owner
Nope, in that case let it stay as it is now; we can parse it in another accessor when/if there comes the need.
| +{ | ||
| + CallInviteEvent rme(callId, lifetime, sdp); | ||
| + connection()->callApi<SendEventJob>(id(), rme); | ||
| +} |
KitsuneRal
Nov 7, 2017
•
Owner
The following two points apply to all the Room methods you're introducing:
- You're replicating
Room::postMessage()here. Consider just passing a created event topostMessage instead(see line 611 above). - As also mentioned below,
Room::inviteCall()must only actually put a call event if the room has exactly two members. Specific way to do validation is up to you; I prefer clients to apply such validations on their side so I'd put aQ_ASSERThere (potentially blowing up a misbehaving client in debug mode). At the very least, there should be aqCWarning(MAIN)instead of sending a message event whenmemberCount() != 2.
mariogrip
Nov 7, 2017
The reason why i created separated function is so they can be called directly from QML, but i can move it to the client side (uMatriks) if this is not the correct place.
KitsuneRal
Nov 7, 2017
Owner
I meant a bit different thing: the functions are in place; but their bodies can be made one-liners calling postMessage instead of the current two-liners.
| + case EventType::CallHangup: { | ||
| + emit callEvent(this, event); | ||
| + break; | ||
| + } |
KitsuneRal
Nov 7, 2017
•
Owner
This is not the right place to catch m.call events - The Spec defines them as message events, so you have to put this code in doAddNewMessages() and doAddHistoricalMessages() instead of processStateEvents(). What's worse, in order for clients to not go through hoops to correctly deal with messages you have to do some extra checking:
- At least in
doAddHistoricalMessages()you should make sure that clients are not notified about already finished calls (or at least are notified in a more intelligent way). Note that historical message events come in reverse, hangup first, and moreover you might have a hangup event in one chunk and invite/candidates events in another chunk.doAddNewMessages()may benefit from the same logic but since call events come in natural order in this case, the standard logic for a missed call on the client side will perfectly work. - Since The Spec is very strict that
m.callevents MUST only be used in 1:1 rooms, it makes sense to only emit the signal ifmemberCount() == 2.
KitsuneRal
Nov 7, 2017
•
Owner
One more thing here: by notifying about an event of one of several types with one common signal, you effectively mandate clients to put a switch statement in their code, to find out what exactly happened. Since that signal carries a RoomEvent, clients don't have even a single chance to process the call event uniformly (RoomEvent is too generic a type). So I'd really consider separating callEvent() into separate signals, maaybe only aggregating CallInvite and CallCandidates (and I'm not sure even that is proper).
mariogrip
Nov 7, 2017
The reason why i choose room to place this event, is so client don't need to listen to each room to be able to catch incoming calls. This is even more useful if clients want add a "listen for call" background services for mobile devices.
But I do agree on 1:1 room check, I have done this on the client side for uMatriks but it does make sense to add this here, I'll add that tomorrow.
KitsuneRal
Nov 7, 2017
Owner
I have no problem with Room being in the signal (although, unfortunately, you will anyway either have to put this signal to Connection - and listen for this signal from each connection object - or listen to Room::callEvent() from each room). I understand that calling is kind of a special case because it consumes much more attention of the end users (normally - all attention); so probably it really makes sense to make a separate signal on at least the level of Connection, or even setup a separate callback (although we mostly use direct connections that are only marginally heavier than callbacks).
My point in the original comment, though, was about passing a generic RoomEvent through a bottleneck of a single signal signature. When I get that RoomEvent in my slot on the client side, too many things have to be clarified before I can do anything sensible. Is that RoomEvent a call event at all (should I put a Q_ASSERT(evt->isCallEvent()) in each client?)? Should I even care about a hangup event if I don't have active calls? Why should I care about (probably invalid) call answer event if I didn't place a call invite (note that I might have several devices and the call answer will arrive to all of them)? That's why I'd prefer to have 4 signals instead of a single one; and only m.call.invite is worth that "globalization" I mentioned in the first paragraph.
| + void inviteCall(const QString& callId, const int& lifetime, | ||
| + const QString& sdp); | ||
| + void callCandidates(const QString& callId, | ||
| + const QJsonArray& candidates); |
KitsuneRal
Nov 7, 2017
•
Owner
As mentioned above, please try to not use QJsonArray unless you actually intend to pass some opaque JSON object. In this particular case, CS API describes the element contents explicitly so just pass a QVector of supplementary "call candidate" structures. To save effort on converting a QVector to/from QJsonArray, you might consider using facility functions from converters.h.
mariogrip
Nov 7, 2017
Using QJsonArray makes it easier to pass to directly to WebRTC handler which is written JavaScript, since all WebRTC will probably be handled by a browser (qwebengine or oxide) this makes sense.
KitsuneRal
Nov 7, 2017
Owner
Would be great to see doc comments next to the declarations, by the way :) I'm not very consistent at putting them around, but 0.2 has an explicit goal about documenting the whole public API of the library, so these comments will have to arrive sooner or later.
| + const QString& sdp); | ||
| + void callCandidates(const QString& callId, | ||
| + const QJsonArray& candidates); | ||
| + void answerCall(const QString& callId, const int& lifetime, |
KitsuneRal
Nov 7, 2017
Owner
const int&? Plain int will be more than enough :) The whole const-ref thing is a bit extraneous in case of Qt strings but we use it because it's more universal and also it still saves on refcounting. For int this just doesn't apply.
| + const QString& sdp); | ||
| + void answerCall(const QString& callId, const QString& sdp); | ||
| + void hangupCall(const QString& callId); | ||
| + |
KitsuneRal
Nov 7, 2017
•
Owner
Looking at those callIds in each call, I start to think that we might want to have a separate "1:1 call" class with its own lifecycle. The "call candidate" structure definition mentioned above would land inside that class as well.
mariogrip
added some commits
Nov 7, 2017
| + CallInvite = 0x2100, | ||
| + CallCandidates = CallInvite + 1, | ||
| + CallAnswer = CallCandidates + 1, | ||
| + CallHangup = CallAnswer + 1, |
KitsuneRal
Nov 7, 2017
Owner
If you use 0x2100 for calls, push Type::Reserved to 0x4000 or something like that. Or instead, something like 0x1100 would fit much better (because 0x2100 does not have the 0x1000 "room event" bit - we don't have any trouble with it right now because we don't have Event::isRoomEvent() but who knows what will come).
| @@ -52,6 +58,7 @@ namespace QMatrixClient | ||
| private: | ||
| int _lifetime; | ||
| QJsonObject _answer; | ||
| + QString _sdp; |
|
Also note that Travis CI is unhappy now - as disappointing as it is, but Qt 5.2.1 doesn't have a |
mariogrip
added some commits
Nov 7, 2017
|
That won't fly either. You now have |
|
|
mariogrip
added some commits
Nov 8, 2017
mariogrip
commented
Nov 8, 2017
|
There we go, sorry for the confusion. Would it be ok that i can move the call things to it's own class (and add doc comments) later on? That way i can push my changes for uMatriks, I also have started briefly on a low power "mode" that only listed for the bare minimal to get push notifications (for both call and messages) |
mariogrip
commented
Nov 8, 2017
|
Here is the implementation on the client side mariogrip/uMatriks@a1f2cff it's still little of a mess, but it works really well! :) |
|
I'm very much puzzled why you treat call events as state events. Having not tried it from the working code, I wonder if Riot also sends those in |
|
I mean, if Riot does send those as state events, then the spec is damn lying that those are message events. As for the code in uMatriks - wow, that's a lot. I was thinking to borrow a thing or two for Quaternion but it's apparently not easy. Kudos. |
|
Aside from |
|
Ok, I at least understood the reason why putting this code into
|
mariogrip commentedNov 7, 2017
This adds support for call events and request of turn servers. Needed for call support in uMatriks
This handles the WebRTC negotiation and call signaling between clients.
This follows https://matrix.org/docs/spec/client_server/r0.2.0.html#voice-over-ip
The WebRTC part not handled here, that must be handled by client. Example of this implemented will be in uMatriks once pushed.