diff --git a/lib/famedlysdk.dart b/lib/famedlysdk.dart index 9930422..eb2890a 100644 --- a/lib/famedlysdk.dart +++ b/lib/famedlysdk.dart @@ -32,6 +32,7 @@ export 'package:famedlysdk/src/utils/mx_content.dart'; export 'package:famedlysdk/src/utils/profile.dart'; export 'package:famedlysdk/src/utils/push_rules.dart'; export 'package:famedlysdk/src/utils/states_map.dart'; +export 'package:famedlysdk/src/utils/turn_server_credentials.dart'; export 'package:famedlysdk/src/account_data.dart'; export 'package:famedlysdk/src/client.dart'; export 'package:famedlysdk/src/event.dart'; diff --git a/lib/src/client.dart b/lib/src/client.dart index 221b1e3..760230a 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -30,6 +30,7 @@ import 'package:famedlysdk/src/presence.dart'; import 'package:famedlysdk/src/store_api.dart'; import 'package:famedlysdk/src/sync/user_update.dart'; import 'package:famedlysdk/src/utils/matrix_file.dart'; +import 'package:famedlysdk/src/utils/turn_server_credentials.dart'; import 'package:pedantic/pedantic.dart'; import 'room.dart'; import 'event.dart'; @@ -400,6 +401,15 @@ class Client { return; } + /// Get credentials for the client to use when initiating calls. + Future getTurnServerCredentials() async { + final Map response = await this.jsonRequest( + type: HTTPType.GET, + action: "/client/r0/voip/turnServer", + ); + return TurnServerCredentials.fromJson(response); + } + /// Fetches the pushrules for the logged in user. /// These are needed for notifications on Android Future getPushrules() async { @@ -477,6 +487,18 @@ class Client { final StreamController onAccountData = StreamController.broadcast(); + /// Will be called on call invites. + final StreamController onCallInvite = StreamController.broadcast(); + + /// Will be called on call hangups. + final StreamController onCallHangup = StreamController.broadcast(); + + /// Will be called on call candidates. + final StreamController onCallCandidates = StreamController.broadcast(); + + /// Will be called on call answers. + final StreamController onCallAnswer = StreamController.broadcast(); + /// Matrix synchronisation is done with https long polling. This needs a /// timeout which is usually 30 seconds. int syncTimeoutSec = 30; @@ -894,6 +916,16 @@ class Client { _updateRoomsByEventUpdate(update); this.store?.storeEventUpdate(update); onEvent.add(update); + + if (event["type"] == "m.call.invite") { + onCallInvite.add(Event.fromJson(event, getRoomById(roomID))); + } else if (event["type"] == "m.call.hangup") { + onCallHangup.add(Event.fromJson(event, getRoomById(roomID))); + } else if (event["type"] == "m.call.answer") { + onCallAnswer.add(Event.fromJson(event, getRoomById(roomID))); + } else if (event["type"] == "m.call.candidates") { + onCallCandidates.add(Event.fromJson(event, getRoomById(roomID))); + } } } diff --git a/lib/src/event.dart b/lib/src/event.dart index d9b9b59..94cde99 100644 --- a/lib/src/event.dart +++ b/lib/src/event.dart @@ -220,6 +220,18 @@ class Event { return EventTypes.Sticker; case "m.room.message": return EventTypes.Message; + case "m.call.encrypted": + return EventTypes.Encrypted; + case "m.call.encryption": + return EventTypes.Encryption; + case "m.call.invite": + return EventTypes.CallInvite; + case "m.call.answer": + return EventTypes.CallAnswer; + case "m.call.candidates": + return EventTypes.CallCandidates; + case "m.call.hangup": + return EventTypes.CallHangup; } return EventTypes.Unknown; } @@ -394,5 +406,11 @@ enum EventTypes { RoomAvatar, GuestAccess, HistoryVisibility, + Encryption, + Encrypted, + CallInvite, + CallAnswer, + CallCandidates, + CallHangup, Unknown, } diff --git a/lib/src/room.dart b/lib/src/room.dart index 6b191e4..f6f3d8b 100644 --- a/lib/src/room.dart +++ b/lib/src/room.dart @@ -1048,6 +1048,103 @@ class Room { data: data, ); } + + /// This is sent by the caller when they wish to establish a call. + /// [callId] is a unique identifier for the call. + /// [version] is the version of the VoIP specification this message adheres to. This specification is version 0. + /// [lifetime] is the time in milliseconds that the invite is valid for. Once the invite age exceeds this value, + /// clients should discard it. They should also no longer show the call as awaiting an answer in the UI. + /// [type] The type of session description. Must be 'offer'. + /// [sdp] The SDP text of the session description. + Future inviteToCall(String callId, int lifetime, String sdp, + {String type = "offer", int version = 0, String txid}) async { + if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; + final Map response = await client.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/send/m.call.invite/$txid", + data: { + "call_id": callId, + "lifetime": lifetime, + "offer": {"sdp": sdp, "type": type}, + "version": version, + }, + ); + return response["event_id"]; + } + + /// This is sent by callers after sending an invite and by the callee after answering. + /// Its purpose is to give the other party additional ICE candidates to try using to communicate. + /// + /// [callId] The ID of the call this event relates to. + /// + /// [version] The version of the VoIP specification this messages adheres to. This specification is version 0. + /// + /// [candidates] Array of objects describing the candidates. Example: + /// + /// ``` + /// [ + /// { + /// "candidate": "candidate:863018703 1 udp 2122260223 10.9.64.156 43670 typ host generation 0", + /// "sdpMLineIndex": 0, + /// "sdpMid": "audio" + /// } + /// ], + /// ``` + Future sendCallCandidates( + String callId, + List> candidates, { + int version = 0, + String txid, + }) async { + if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; + final Map response = await client.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/send/m.call.candidates/$txid", + data: { + "call_id": callId, + "candidates": candidates, + "version": version, + }, + ); + return response["event_id"]; + } + + /// This event is sent by the callee when they wish to answer the call. + /// [callId] is a unique identifier for the call. + /// [version] is the version of the VoIP specification this message adheres to. This specification is version 0. + /// [type] The type of session description. Must be 'answer'. + /// [sdp] The SDP text of the session description. + Future answerCall(String callId, String sdp, + {String type = "answer", int version = 0, String txid}) async { + if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; + final Map response = await client.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/send/m.call.answer/$txid", + data: { + "call_id": callId, + "answer": {"sdp": sdp, "type": type}, + "version": version, + }, + ); + return response["event_id"]; + } + + /// This event is sent by the callee when they wish to answer the call. + /// [callId] The ID of the call this event relates to. + /// [version] is the version of the VoIP specification this message adheres to. This specification is version 0. + Future hangupCall(String callId, + {int version = 0, String txid}) async { + if (txid == null) txid = "txid${DateTime.now().millisecondsSinceEpoch}"; + final Map response = await client.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/rooms/$id/send/m.call.hangup/$txid", + data: { + "call_id": callId, + "version": version, + }, + ); + return response["event_id"]; + } } enum PushRuleState { notify, mentions_only, dont_notify } diff --git a/lib/src/utils/turn_server_credentials.dart b/lib/src/utils/turn_server_credentials.dart new file mode 100644 index 0000000..3efef57 --- /dev/null +++ b/lib/src/utils/turn_server_credentials.dart @@ -0,0 +1,23 @@ +/// Credentials for the client to use when initiating calls. +class TurnServerCredentials { + /// The username to use. + final String username; + + /// The password to use. + final String password; + + /// A list of TURN URIs + final List uris; + + /// The time-to-live in seconds + final int ttl; + + const TurnServerCredentials( + this.username, this.password, this.uris, this.ttl); + + TurnServerCredentials.fromJson(Map json) + : username = json['username'], + password = json['password'], + uris = json['uris'].cast(), + ttl = json['ttl']; +}