diff --git a/.gitignore b/.gitignore index 353da704..216c1d16 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ pubspec.lock doc/api/ .DS_Store example/lib/generated_plugin_registrant.dart + +.flutter-plugins +.flutter-plugins-dependencies diff --git a/example/.gitignore b/example/.gitignore index 2ddde2a5..3929287a 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -71,3 +71,5 @@ !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +.flutter-plugins-dependencies \ No newline at end of file diff --git a/example/lib/src/callscreen.dart b/example/lib/src/callscreen.dart index e2e9cf1d..5269768c 100644 --- a/example/lib/src/callscreen.dart +++ b/example/lib/src/callscreen.dart @@ -581,4 +581,9 @@ class _MyCallScreenWidget extends State void onNewMessage(SIPMessageRequest msg) { // NO OP } + + @override + void onNewNotify(Notify ntf) { + // TODO: implement onNewNotify + } } diff --git a/example/lib/src/dialpad.dart b/example/lib/src/dialpad.dart index 8b12ce20..b891a1d7 100644 --- a/example/lib/src/dialpad.dart +++ b/example/lib/src/dialpad.dart @@ -311,4 +311,9 @@ class _MyDialPadWidget extends State receivedMsg = msgBody; }); } + + @override + void onNewNotify(Notify ntf) { + // TODO: implement onNewNotify + } } diff --git a/example/lib/src/register.dart b/example/lib/src/register.dart index 0e18339f..f49f73af 100644 --- a/example/lib/src/register.dart +++ b/example/lib/src/register.dart @@ -293,4 +293,9 @@ class _MyRegisterWidget extends State void onNewMessage(SIPMessageRequest msg) { // NO OP } + + @override + void onNewNotify(Notify ntf) { + // TODO: implement onNewNotify + } } diff --git a/lib/src/constants.dart b/lib/src/constants.dart index 878354ee..cda2f969 100644 --- a/lib/src/constants.dart +++ b/lib/src/constants.dart @@ -209,7 +209,7 @@ Map REASON_PHRASE = { }; const String ALLOWED_METHODS = - 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO'; + 'INVITE,ACK,CANCEL,BYE,UPDATE,MESSAGE,OPTIONS,REFER,INFO,NOTIFY'; const String ACCEPTED_BODY_TYPES = 'application/sdp, application/dtmf-relay'; const int MAX_FORWARDS = 69; const int SESSION_EXPIRES = 90; diff --git a/lib/src/dialog.dart b/lib/src/dialog.dart index 78b6ec3d..baff98f8 100644 --- a/lib/src/dialog.dart +++ b/lib/src/dialog.dart @@ -36,7 +36,7 @@ class Id { // RFC 3261 12.1. class Dialog { - Dialog(RTCSession owner, dynamic message, String type, [int? state]) { + Dialog(Owner owner, dynamic message, String type, [int? state]) { state = state ?? DialogStatus.STATUS_CONFIRMED; _owner = owner; _ua = owner.ua; @@ -91,7 +91,7 @@ class Dialog { '$type dialog created with status ${_state == DialogStatus.STATUS_EARLY ? 'EARLY' : 'CONFIRMED'}'); } - RTCSession? _owner; + Owner? _owner; UA? _ua; bool uac_pending_reply = false; bool uas_pending_reply = false; @@ -108,7 +108,7 @@ class Dialog { UA? get ua => _ua; Id? get id => _id; - RTCSession? get owner => _owner; + Owner? get owner => _owner; void update(dynamic message, String type) { _state = DialogStatus.STATUS_CONFIRMED; @@ -263,3 +263,10 @@ class Dialog { return true; } } + +abstract class Owner { + Function(IncomingRequest) get receiveRequest; + int? get status; + int get TerminatedCode; + UA? get ua; +} diff --git a/lib/src/dialog/request_sender.dart b/lib/src/dialog/request_sender.dart index f0240684..46fd1e0a 100644 --- a/lib/src/dialog/request_sender.dart +++ b/lib/src/dialog/request_sender.dart @@ -91,7 +91,7 @@ class DialogRequestSender { _request.cseq = _dialog.local_seqnum!.toInt(); _reattemptTimer = setTimeout(() { // TODO(cloudwebrtc): look at dialog state instead. - if (_dialog.owner!.status != RTCSession.C.STATUS_TERMINATED) { + if (_dialog.owner!.status != _dialog.owner!.TerminatedCode) { _reattempt = true; _request_sender.send(); } diff --git a/lib/src/event_manager/internal_events.dart b/lib/src/event_manager/internal_events.dart index e9031101..31f8b4dc 100644 --- a/lib/src/event_manager/internal_events.dart +++ b/lib/src/event_manager/internal_events.dart @@ -161,3 +161,8 @@ class EventOnErrorResponse extends EventType { EventOnErrorResponse({this.response}); IncomingMessage? response; } + +class EventOnNewSubscribe extends EventType { + EventOnNewSubscribe({this.request}); + IncomingRequest? request; +} diff --git a/lib/src/event_manager/notifier_events.dart b/lib/src/event_manager/notifier_events.dart new file mode 100644 index 00000000..ef6e3e09 --- /dev/null +++ b/lib/src/event_manager/notifier_events.dart @@ -0,0 +1,18 @@ +import 'package:sip_ua/src/sip_message.dart'; + +import 'events.dart'; + +class EventTerminated extends EventType { + EventTerminated({this.terminationCode, this.sendFinalNotify}); + int? terminationCode; + bool? sendFinalNotify; +} + +class EventSubscribe extends EventType { + EventSubscribe( + {this.isUnsubscribe, this.request, this.body, this.content_type}); + bool? isUnsubscribe; + IncomingRequest? request; + String? body; + String? content_type; +} diff --git a/lib/src/event_manager/subscriber_events.dart b/lib/src/event_manager/subscriber_events.dart new file mode 100644 index 00000000..099e7365 --- /dev/null +++ b/lib/src/event_manager/subscriber_events.dart @@ -0,0 +1,23 @@ +import '../sip_message.dart'; +import 'events.dart'; + +class EventTerminated extends EventType { + EventTerminated({this.TerminationCode, this.reason, this.retryAfter}); + int? TerminationCode; + String? reason; + int? retryAfter; +} + +class EventPending extends EventType {} + +class EventActive extends EventType {} + +class EventNotify extends EventType { + EventNotify({this.isFinal, this.request, this.body, this.contentType}); + bool? isFinal; + IncomingRequest? request; + String? body; + String? contentType; +} + +class EventAccepted extends EventType {} diff --git a/lib/src/grammar_parser.dart b/lib/src/grammar_parser.dart index 5383dc1c..2bafb875 100644 --- a/lib/src/grammar_parser.dart +++ b/lib/src/grammar_parser.dart @@ -17093,7 +17093,7 @@ class GrammarParser { _failure(_expect85); } if (success) { - final $1 = $$; + final $1 = $$[2]; final $start = startPos1; var pos0 = _startPos; $$ = ((offset, expires) { @@ -22438,7 +22438,7 @@ class GrammarParser { if (!success && _cursor > _testing) { _failure(_expect115); } - return $$; + return data; } dynamic parse_Supported() { diff --git a/lib/src/rtc_session.dart b/lib/src/rtc_session.dart index 085dcb75..b2e40153 100644 --- a/lib/src/rtc_session.dart +++ b/lib/src/rtc_session.dart @@ -64,7 +64,7 @@ class RFC4028Timers { Timer? timer; } -class RTCSession extends EventManager { +class RTCSession extends EventManager implements Owner { RTCSession(UA? ua) { logger.debug('new'); @@ -195,6 +195,9 @@ class RTCSession extends EventManager { RTCPeerConnection? get connection => _connection; + @override + int get TerminatedCode => C.STATUS_TERMINATED; + RTCDTMFSender get dtmfSender => _connection!.createDtmfSender(_localMediaStream!.getAudioTracks()[0]); diff --git a/lib/src/sip_ua_helper.dart b/lib/src/sip_ua_helper.dart index 93aef7ca..74683e68 100644 --- a/lib/src/sip_ua_helper.dart +++ b/lib/src/sip_ua_helper.dart @@ -3,14 +3,17 @@ import 'dart:async'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:logger/logger.dart'; -import 'package:sip_ua/src/rtc_session/refer_subscriber.dart'; import 'config.dart'; import 'constants.dart' as DartSIP_C; import 'event_manager/event_manager.dart'; +import 'event_manager/subscriber_events.dart'; import 'logger.dart'; import 'message.dart'; import 'rtc_session.dart'; +import 'rtc_session/refer_subscriber.dart'; +import 'sip_message.dart'; import 'stack_trace_nj.dart'; +import 'subscriber.dart'; import 'transports/websocket_interface.dart'; import 'ua.dart'; @@ -328,6 +331,16 @@ class SIPUAHelper extends EventManager { return _ua!.sendMessage(target, body, options); } + void subscribe(String target, String event, String contentType) { + Subscriber s = _ua!.subscribe(target, event, contentType); + + s.on(EventNotify(), (EventNotify event) { + _notifyNotifyListeners(event); + }); + + s.subscribe(); + } + void terminateSessions(Map options) { _ua!.terminateSessions(options as Map); } @@ -372,6 +385,12 @@ class SIPUAHelper extends EventManager { listener.onNewMessage(msg); } } + + void _notifyNotifyListeners(EventNotify event) { + for (SipUaHelperListener listener in _sipUaHelperListeners) { + listener.onNewNotify(Notify(request: event.request)); + } + } } enum CallStateEnum { @@ -422,7 +441,7 @@ class Call { void hangup([Map? options]) { assert(_session != null, 'ERROR(hangup): rtc session is invalid!'); - _session.terminate(options as Map?); + _session.terminate(options); } void hold() { @@ -584,8 +603,14 @@ abstract class SipUaHelperListener { void transportStateChanged(TransportState state); void registrationStateChanged(RegistrationState state); void callStateChanged(Call call, CallState state); - //For SIP messaga coming + //For SIP message coming void onNewMessage(SIPMessageRequest msg); + void onNewNotify(Notify ntf); +} + +class Notify { + Notify({this.request}); + IncomingRequest? request; } class RegisterParams { diff --git a/lib/src/subscriber.dart b/lib/src/subscriber.dart new file mode 100644 index 00000000..c48dd8d5 --- /dev/null +++ b/lib/src/subscriber.dart @@ -0,0 +1,560 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; + +import 'constants.dart'; +import 'dialog.dart'; +import 'event_manager/event_manager.dart'; +import 'event_manager/internal_events.dart'; +import 'event_manager/subscriber_events.dart'; +import 'exceptions.dart'; +import 'grammar.dart'; +import 'logger.dart'; +import 'request_sender.dart'; +import 'sip_message.dart'; +import 'timers.dart'; +import 'ua.dart'; +import 'utils.dart'; + +/** + * Termination codes. + */ +class C { + // Termination codes. + static const int SUBSCRIBE_RESPONSE_TIMEOUT = 0; + static const int SUBSCRIBE_TRANSPORT_ERROR = 1; + static const int SUBSCRIBE_NON_OK_RESPONSE = 2; + static const int SUBSCRIBE_BAD_OK_RESPONSE = 3; + static const int SUBSCRIBE_FAILED_AUTHENTICATION = 4; + static const int UNSUBSCRIBE_TIMEOUT = 5; + static const int RECEIVE_FINAL_NOTIFY = 6; + static const int RECEIVE_BAD_NOTIFY = 7; + + // Subscriber states. + static const int STATE_PENDING = 0; + static const int STATE_ACTIVE = 1; + static const int STATE_TERMINATED = 2; + static const int STATE_INIT = 3; + static const int STATE_NOTIFY_WAIT = 4; +} + +class Subscriber extends EventManager implements Owner { + String? _id; + + final String _target; + + late int _expires; + + String? _contentType; + + late Map _params; + + late int _state; + + late Dialog? _dialog; + + DateTime? _expires_timestamp; + + Timer? _expires_timer; + + late bool _terminated; + + Timer? _unsubscribe_timeout_timer; + + late Map _data; + + late String _event_name; + + num? _event_id; + + late List _headers; + + late List> _queue; + + @override + late Function(IncomingRequest p1) receiveRequest; + + @override + UA ua; + + Subscriber(this.ua, this._target, String eventName, String accept, + [int expires = 900, + String? contentType, + String? allowEvents, + Map requestParams = const {}, + List extraHeaders = const []]) { + logger.debug('new'); + + _expires = expires; + + // Used to subscribe with body. + _contentType = contentType; + + _params = Map.from(requestParams); + + _params['from_tag'] = newTag(); + + _params['to_tag'] = null; + + _params['call_id'] = createRandomToken(20); + + //if (_params['cseq'] == null) { + // _params['cseq'] = Math.floor((Math.random() * 10000) + 1); + //} + + _state = C.STATE_INIT; + + _dialog = null; + + _expires_timer = null; + + _expires_timestamp = null; + + _terminated = false; + + _unsubscribe_timeout_timer = null; + + dynamic parsed = Grammar.parse(eventName, 'Event'); + + if (parsed == -1) { + throw TypeError('eventName - wrong format'); + } + + _event_name = parsed.event; + // this._event_id = parsed.params && parsed.params.id; + _event_id = parsed.params['id']; + + String eventValue = _event_name; + + if (_event_id != null) { + eventValue += ';id=$_event_id'; + } + + _headers = cloneArray(extraHeaders); + + _headers.addAll(['Event: $eventValue', 'Expires: $_expires']); + + if (!_headers.any((dynamic element) => element.startsWith('Contact'))) { + String contact = 'Contact: ${ua.contact.toString()}'; + + _headers.add(contact); + } + + if (allowEvents != null) { + _headers.add('Allow-Events: $allowEvents'); + } + + receiveRequest = receiveNotifyRequest; + + // To enqueue subscribes created before receive initial subscribe OK. + _queue = >[]; + } + String? get id => _id; + + int? get status => _state; + + @override + int get TerminatedCode => C.STATE_TERMINATED; + + @override + void onRequestTimeout() { + _dialogTerminated(C.SUBSCRIBE_RESPONSE_TIMEOUT); + } + + /** + * User API + */ + + @override + void onTransportError() { + _dialogTerminated(C.SUBSCRIBE_TRANSPORT_ERROR); + } + + /** + * Dialog callback. + */ + void receiveNotifyRequest(IncomingRequest request) { + if (request.method != SipMethod.NOTIFY) { + logger.warn('received non-NOTIFY request'); + request.reply(405); + + return; + } + + // RFC 6665 8.2.1. Check if event header matches. + dynamic eventHeader = request.parseHeader('Event'); + + if (eventHeader == null) { + logger.warn('missed Event header'); + request.reply(400); + _dialogTerminated(C.RECEIVE_BAD_NOTIFY); + + return; + } + + dynamic eventName = eventHeader.event; + num? eventId = eventHeader.params['id']; + + if (eventName != _event_name || eventId != _event_id) { + logger.warn('Event header does not match SUBSCRIBE'); + request.reply(489); + _dialogTerminated(C.RECEIVE_BAD_NOTIFY); + + return; + } + + // Process Subscription-State header. + dynamic subsState = request.parseHeader('subscription-state'); + + if (subsState == null) { + logger.warn('missed Subscription-State header'); + request.reply(400); + _dialogTerminated(C.RECEIVE_BAD_NOTIFY); + + return; + } + + request.reply(200); + + int newState = _stateStringToNumber(subsState.state); + int prevState = _state; + + if (prevState != C.STATE_TERMINATED && newState != C.STATE_TERMINATED) { + _state = newState; + + if (subsState.expires != null) { + int expires = subsState.expires; + DateTime expiresTimestamp = + DateTime.now().add(Duration(milliseconds: expires * 1000)); + int maxTimeDeviation = 2000; + + // Expiration time is shorter and the difference is not too small. + if (_expires_timestamp!.difference(expiresTimestamp) > + Duration(milliseconds: maxTimeDeviation)) { + logger.debug('update sending re-SUBSCRIBE time'); + + _scheduleSubscribe(expires); + } + } + } + + if (prevState != C.STATE_PENDING && newState == C.STATE_PENDING) { + logger.debug('emit "pending"'); + emit(EventPending()); + } else if (prevState != C.STATE_ACTIVE && newState == C.STATE_ACTIVE) { + logger.debug('emit "active"'); + emit(EventActive()); + } + + String? body = request.body; + + // Check if the notify is final. + bool isFinal = newState == C.STATE_TERMINATED; + + // Notify event fired only for notify with body. + if (body != null) { + dynamic contentType = request.getHeader('content-type'); + + logger.debug('emit "notify"'); + emit(EventNotify( + isFinal: isFinal, + request: request, + body: body, + contentType: contentType)); + } + + if (isFinal) { + dynamic reason = subsState.reason; + dynamic retryAfter = null; + + if (subsState.params && subsState.params['retry-after'] != null) { + retryAfter = parseInt(subsState.params['retry-after'], 10); + } + + _dialogTerminated(C.RECEIVE_FINAL_NOTIFY, reason, retryAfter); + } + } + + /** + * Send the initial (non-fetch) and subsequent subscribe. + * @param {string} body - subscribe request body. + */ + void subscribe([String? target, String? body]) { + logger.debug('subscribe()'); + + if (_state == C.STATE_INIT) { + _sendInitialSubscribe(body, _headers); + } else { + _sendSubsequentSubscribe(body, _headers); + } + } + + /** + * terminate. + * Send un-subscribe or fetch-subscribe (with Expires: 0). + * @param {string} body - un-subscribe request body + */ + void terminate(String? body) { + logger.debug('terminate()'); + + // Prevent duplication un-subscribe sending. + if (_terminated) { + return; + } + _terminated = true; + + // Set header Expires: 0. + List headers = _headers.map((dynamic header) { + return header.startsWith('Expires') ? 'Expires: 0' : header; + }).toList(); + + if (_state == C.STATE_INIT) { + // fetch-subscribe - initial subscribe with Expires: 0. + _sendInitialSubscribe(body, headers); + } else { + _sendSubsequentSubscribe(body, headers); + } + + // Waiting for the final notify for a while. + int final_notify_timeout = 30000; + + _unsubscribe_timeout_timer = setTimeout(() { + _dialogTerminated(C.UNSUBSCRIBE_TIMEOUT); + }, final_notify_timeout); + } + + void _dialogTerminated(int terminationCode, + [String? reason, int? retryAfter]) { + // To prevent duplicate emit terminated event. + if (_state == C.STATE_TERMINATED) { + return; + } + + _state = C.STATE_TERMINATED; + + // Clear timers. + clearTimeout(_expires_timer); + clearTimeout(_unsubscribe_timeout_timer); + + if (_dialog != null) { + _dialog!.terminate(); + _dialog = null; + } + + logger.debug('emit "terminated" code=$terminationCode'); + emit(EventTerminated( + TerminationCode: terminationCode, + reason: reason, + retryAfter: retryAfter)); + } + + void _handlePresence(EventNotify event) { + emit(event); + } + + void _receiveSubscribeResponse(IncomingResponse? response) { + if (response == null) { + throw ArgumentError('Incoming response was null'); + } + + if (response.status_code >= 200 && response.status_code! < 300) { + // Create dialog + if (_dialog == null) { + _id = response.call_id!; + try { + Dialog dialog = Dialog(this, response, 'UAC'); + _dialog = dialog; + } catch (e) { + logger.warn(e.toString()); + _dialogTerminated(C.SUBSCRIBE_BAD_OK_RESPONSE); + + return; + } + + logger.debug('emit "accepted"'); + emit(EventAccepted()); + + // Subsequent subscribes saved in the queue until dialog created. + for (Map sub in _queue) { + logger.debug('dequeue subscribe'); + + _sendSubsequentSubscribe(sub['body'], sub['headers']); + } + } else { + ua.destroySubscriber(this); + _id = response.call_id!; + ua.newSubscriber(sub: this); + } + + ua.newSubscriber(sub: this); + + // Check expires value. + String? expires_value = response.getHeader('Expires'); + + if (expires_value != null && + expires_value == '' && + expires_value == '0') { + logger.warn('response without Expires header'); + + // RFC 6665 3.1.1 subscribe OK response must contain Expires header. + // Use workaround expires value. + expires_value = '900'; + } + + int? expires = parseInt(expires_value!, 10); + + if (expires! > 0) { + _scheduleSubscribe(expires); + } + } else if (response.status_code == 401 || response.status_code == 407) { + _dialogTerminated(C.SUBSCRIBE_FAILED_AUTHENTICATION); + } else if (response.status_code >= 300) { + _dialogTerminated(C.SUBSCRIBE_NON_OK_RESPONSE); + } + } + + void _scheduleSubscribe(int expires) { + /* + If the expires time is less than 140 seconds we do not support Chrome intensive timer throttling mode. + In this case, the re-subscribe is sent 5 seconds before the subscription expiration. + + When Chrome is in intensive timer throttling mode, in the worst case, + the timer will be 60 seconds late. + We give the server 10 seconds to make sure it will execute the command even if it is heavily loaded. + As a result, we order the time no later than 70 seconds before the subscription expiration. + Resulting time calculated as half time interval + (half interval - 70) * random. + + E.g. expires is 140, re-subscribe will be ordered to send in 70 seconds. + expires is 600, re-subscribe will be ordered to send in 300 + (0 .. 230) seconds. + */ + + num timeout = expires / 2; + + _expires_timestamp = DateTime.now().add(Duration(seconds: expires)); + + logger.debug('next SUBSCRIBE will be sent in $timeout sec'); + + clearTimeout(_expires_timer); + + _expires_timer = setTimeout(() { + _expires_timer = null; + _sendSubsequentSubscribe(null, _headers); + }, timeout.toInt() * 1000); + } + + /** + * Private API. + */ + void _sendInitialSubscribe(String? body, List headers) { + if (body != null) { + if (_contentType == null) { + throw TypeError('content_type is undefined'); + } + + headers = headers.slice(0); + headers.add('Content-Type: $_contentType'); + } + + _state = C.STATE_NOTIFY_WAIT; + + OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, + ua.normalizeTarget(_target), ua, _params, headers, body); + + EventManager handler = EventManager(); + + handler.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { + _receiveSubscribeResponse(response.response); + }); + + handler.on(EventOnRequestTimeout(), (EventOnRequestTimeout timeout) { + onRequestTimeout(); + }); + + handler.on(EventOnTransportError(), (EventOnTransportError event) { + onTransportError(); + }); + + RequestSender request_sender = RequestSender(ua, request, handler); + + request_sender.send(); + } + + void _sendSubsequentSubscribe(String? body, List headers) { + if (_state == C.STATE_TERMINATED) { + return; + } + + if (_dialog == null) { + logger.debug('enqueue subscribe'); + + _queue.add({'body': body, 'headers': headers.slice(0)}); + + return; + } + + if (body != null) { + if (_contentType == null) { + throw TypeError('content_type is undefined'); + } + + headers = headers.slice(0); + headers.add('Content-Type: $_contentType'); + } + + var manager = EventManager(); + manager.on(EventOnReceiveResponse(), (EventOnReceiveResponse response) { + _receiveSubscribeResponse(response.response); + }); + + manager.on(EventOnRequestTimeout(), (EventOnRequestTimeout timeout) { + onRequestTimeout(); + }); + + manager.on(EventOnTransportError(), (EventOnTransportError event) { + onTransportError(); + }); + + OutgoingRequest request = OutgoingRequest(SipMethod.SUBSCRIBE, + ua.normalizeTarget(_target), ua, _params, headers, body); + + RequestSender request_sender = RequestSender(ua, request, manager); + + request_sender.send(); + + var s = _dialog!.sendRequest(SipMethod.SUBSCRIBE, { + 'body': body, + 'extraHeaders': headers, + 'eventHandlers': manager, + }); + } + + int _stateStringToNumber(String? strState) { + switch (strState) { + case 'pending': + return C.STATE_PENDING; + case 'active': + return C.STATE_ACTIVE; + case 'terminated': + return C.STATE_TERMINATED; + case 'init': + return C.STATE_INIT; + case 'notify_wait': + return C.STATE_NOTIFY_WAIT; + default: + throw TypeError('wrong state value'); + } + } + + /** + * Expose C object. + */ + static C getC() { + return C(); + } +} + +class SubscriptionId { + String target; + String event; + SubscriptionId(this.target, this.event); +} diff --git a/lib/src/ua.dart b/lib/src/ua.dart index bf338acb..99c1569e 100644 --- a/lib/src/ua.dart +++ b/lib/src/ua.dart @@ -1,6 +1,7 @@ import 'dart:async'; -import 'package:sip_ua/src/data.dart'; +import 'data.dart'; +import 'subscriber.dart'; import 'config.dart' as config; import 'config.dart'; import 'constants.dart' as DartSIP_C; @@ -122,8 +123,11 @@ class UA extends EventManager { // Initialize registrator. _registrator = Registrator(this); + + _subscribers = {}; } + late Map _subscribers; Map? _cache; Settings? _configuration; DynamicSettings? _dynConfiguration; @@ -205,6 +209,25 @@ class UA extends EventManager { _registrator.unregister(all); } + /** + * Create subscriber instance + */ + Subscriber subscribe( + String target, + String eventName, + String accept, [ + int expires = 900, + String? contentType, + String? allowEvents, + Map requestParams = const {}, + List extraHeaders = const [], + ]) { + logger.debug('subscribe()'); + + return Subscriber(this, target, eventName, accept, expires, contentType, + allowEvents, requestParams, extraHeaders); + } + /** * Get the Registrator instance. */ @@ -309,6 +332,19 @@ class UA extends EventManager { } }); + // Run _terminate on ever subscription + _subscribers.forEach((String? key, _) { + if (_subscribers.containsKey(key)) { + logger.debug('closing subscription $key'); + try { + Subscriber subscriber = _subscribers[key]!; + subscriber.terminate(null); + } catch (error, s) { + Log.e(error.toString(), null, s); + } + } + }); + // Run _close_ on every applicant. for (Message message in _applicants) { try { @@ -418,6 +454,20 @@ class UA extends EventManager { emit(EventTransactionDestroyed(transaction: transaction)); } + /** + * Subscriber + */ + void newSubscriber({required Subscriber sub}) { + _subscribers[sub.id] = sub; + } + + /** + * Subscriber destroyed. + */ + void destroySubscriber(Subscriber sub) { + _subscribers.remove(sub.id); + } + /** * Dialog */ @@ -562,6 +612,12 @@ class UA extends EventManager { if (request.to_tag != null && !hasListeners(EventNewRTCSession())) { request.reply(405); + return; + } + } else if (method == SipMethod.SUBSCRIBE) { + if (listeners['newSubscribe']?.length == 0) { + request.reply(405); + return; } } @@ -580,7 +636,7 @@ class UA extends EventManager { dialog = _findDialog( replaces.call_id, replaces.from_tag!, replaces.to_tag!); if (dialog != null) { - session = dialog.owner; + session = dialog.owner as RTCSession?; if (!session!.isEnded()) { session.receiveRequest(request); } else { @@ -622,6 +678,9 @@ class UA extends EventManager { emit(EventSipEvent(request: request)); request.reply(200); break; + case SipMethod.SUBSCRIBE: + emit(EventOnNewSubscribe(request: request)); + break; default: request.reply(405); break; @@ -635,10 +694,10 @@ class UA extends EventManager { if (dialog != null) { dialog.receiveRequest(request); } else if (method == SipMethod.NOTIFY) { - session = - _findSession(request.call_id!, request.from_tag, request.to_tag); - if (session != null) { - session.receiveRequest(request); + Subscriber? sub = _findSubscriber( + request.call_id!, request.from_tag!, request.to_tag!); + if (sub != null) { + sub.receiveRequest(request); } else { logger .debug('received NOTIFY request for a non existent subscription'); @@ -661,6 +720,13 @@ class UA extends EventManager { // Utils. // ============ + Subscriber? _findSubscriber(String call_id, String from_tag, String to_tag) { + String id = call_id; + Subscriber? sub = _subscribers[id]; + + return sub; + } + /** * Get the session to which the request belongs to, if any. */