diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
index c0d76fa5..0e9557e2 100644
--- a/.github/workflows/dart.yml
+++ b/.github/workflows/dart.yml
@@ -3,9 +3,9 @@ name: Build
on:
push:
- branches: [ master ]
+ branches: [ main ]
pull_request:
- branches: [ master ]
+ branches: [ main ]
jobs:
test:
@@ -22,7 +22,7 @@ jobs:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
- channel: 'stable'
+ flutter-version: '3.27.1'
- name: Install project dependencies
run: flutter pub get
- name: Dart Format Check
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 5c9d8574..c19c1aee 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -23,8 +23,8 @@ jobs:
run: flutter analyze
- name: Dart Test Check
run: flutter test
- #- name: Check Publish Warnings
- # run: dart pub publish --dry-run
+ - name: Check Publish Warnings
+ run: dart pub publish --dry-run
- name: Publish
uses: k-paxian/dart-package-publisher@v1.5.1
with:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e5fe8c4..736418b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,29 @@
# Changelog
--------------------------------------------
+[1.1.0] -2025-09-26
+
+* Bump flutter-webrtc to 1.2.0
+* Docs(readme): replace pub.dartlang.org with pub.dev; normalize macOS; update Flutter docs link (#555)
+* Allow setting call-id (#552)
+* Extend configuration for ice gathering process (#550)
+* Setting for call statistics logs (#549)
+* Session-timers fix (#545)
+* Fix: persist 'ws_uri' in `_saveSettings` to enable proper registration (#542)
+* Sip message stateless reply reason fixes #539
+* Chore: removal of unneccesary pushToMaster work flow and import sorting
+* Fix bug for rfc2833. (#534)
+* Sessions keys toList added (#514)
+* Build fixes
+
+[1.0.1] -2024.12.19
+
+* make sure the session terminates by @victortive in https://github.com/flutter-webrtc/dart-sip-ua/pull/485
+* Hold and video upgrade fixed by @mikaelwills in https://github.com/flutter-webrtc/dart-sip-ua/pull/503
+* sip_ua_helper: add sendOptions by @eschmidbauer in https://github.com/flutter-webrtc/dart-sip-ua/pull/476
+* Update callscreen.dart by @HVaidehi in https://github.com/flutter-webrtc/dart-sip-ua/pull/427
+
+
[1.0.0] - 2024.08.24
* allow to change UA uri in runtime (#425)
diff --git a/README.md b/README.md
index f934e45e..3d375545 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
# dart-sip-ua
-[](https://opencollective.com/flutter-webrtc) [](https://pub.dartlang.org/packages/sip_ua) [](https://join.slack.com/t/flutterwebrtc/shared_invite/zt-q83o7y1s-FExGLWEvtkPKM8ku_F8cEQ)
+[](https://opencollective.com/flutter-webrtc) [](https://pub.dev/packages/sip_ua) [](https://join.slack.com/t/flutterwebrtc/shared_invite/zt-q83o7y1s-FExGLWEvtkPKM8ku_F8cEQ)
A dart-lang version of the SIP UA stack, ported from [JsSIP](https://github.com/versatica/JsSIP).
## Overview
- Use pure [dart-lang](https://dart.dev)
-- SIP over WebSocket (use real SIP in your flutter mobile, [desktop](https://flutter.dev/desktop), [web](https://flutter.dev/web) apps)
+- SIP over WebSocket && TCP (use real SIP in your flutter mobile, [desktop](https://flutter.dev/desktop), [web](https://flutter.dev/web) apps)
- Audio/video calls ([flutter-webrtc](https://github.com/cloudwebrtc/flutter-webrtc)) and instant messaging
-- Support with standard SIP servers such as OpenSIPS, Kamailio, Asterisk and FreeSWITCH.
+- Support with standard SIP servers such as OpenSIPS, Kamailio, Asterisk, 3CX and FreeSWITCH.
- Support RFC2833 or INFO to send DTMF.
## Currently supported platforms
@@ -50,9 +50,47 @@ Register with SIP server:
- [Asterisk](https://github.com/flutter-webrtc/dockers/tree/main/asterisk)
- FreeSWITCH
- OpenSIPS
+- 3CX
- Kamailio
- or add your server example.
+## FAQ's OR ISSUES
+
+
+expand
+
+## Server not configured for DTLS/SRTP
+
+WEBRTC_SET_REMOTE_DESCRIPTION_ERROR: Failed to set remote offer sdp: Called with SDP without DTLS fingerprint.
+
+Your server is not sending a DTLS fingerprint inside the SDP when inviting the sip_ua client to start a call.
+
+WebRTC uses encryption by Default, all WebRTC communications (audio, video, and data) are encrypted using DTLS and SRTP, ensuring secure communication. Your PBX must be configured to use DTLS/SRTP when calling sip_ua.
+
+
+## Why isn't there a UDP connection option?
+
+This package uses a WS or TCP connection for the signalling processs to initiate or terminate a session (sip messages).
+Once the session is connected WebRTC transmits the actual media (audio/video) over UDP.
+
+If anyone actually still wants to use UDP for the signalling process, feel free to submit a PR with the large amount of work needed to set it up, packet order checking, error checking, reliability timeouts, flow control, security etc etc.
+
+## SIP/2.0 488 Not acceptable here
+
+The codecs on your PBX server don't match the codecs used by WebRTC
+
+- **opus** (payload type 111, 48kHz, 2 channels)
+- **red** (payload type 63, 48kHz, 2 channels)
+- **G722** (payload type 9, 8kHz, 1 channel)
+- **ILBC** (payload type 102, 8kHz, 1 channel)
+- **PCMU** (payload type 0, 8kHz, 1 channel)
+- **PCMA** (payload type 8, 8kHz, 1 channel)
+- **CN** (payload type 13, 8kHz, 1 channel)
+- **telephone-event** (payload type 110, 48kHz, 1 channel for wideband, 8000Hz, 1 channel for narrowband)
+
+
+
+
## NOTE
Thanks to the original authors of [JsSIP](https://github.com/versatica/JsSIP) for providing the JS version, which makes it possible to port the [dart-lang](https://dart.dev).
- [José Luis Millán](https://github.com/jmillan)
@@ -69,6 +107,7 @@ The project is inseparable from the contributors of the community.
- [Robert Sutton](https://github.com/rlsutton1) - Contributor
- [Gavin Henry](https://github.com/ghenry) - Contributor
- [Perondas](https://github.com/Perondas) - Contributor
+- [Mikael Wills](https://github.com/mikaelwills) - Contributor
## License
dart-sip-ua is released under the [MIT license](https://github.com/cloudwebrtc/dart-sip-ua/blob/master/LICENSE).
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 3a1f08c9..93b8d111 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -45,7 +45,6 @@ analyzer:
library_prefixes: ignore
unused_field: ignore
avoid_init_to_null: ignore
- prefer_is_empty: ignore
unused_element: ignore
curly_braces_in_flow_control_structures: ignore
unnecessary_null_in_if_null_operators: ignore
@@ -53,7 +52,6 @@ analyzer:
avoid_types_as_parameter_names: ignore
empty_catches: ignore
unawaited_futures: ignore
- use_rethrow_when_possible: ignore
unused_import: ignore
must_be_immutable: ignore
todo: ignore
diff --git a/example/.metadata b/example/.metadata
new file mode 100644
index 00000000..1101d3f3
--- /dev/null
+++ b/example/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2"
+ channel: "[user-branch]"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: android
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: ios
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: linux
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: macos
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: web
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ - platform: windows
+ create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+ base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/example/android/.gitignore b/example/android/.gitignore
index 6f568019..be3943c9 100644
--- a/example/android/.gitignore
+++ b/example/android/.gitignore
@@ -5,9 +5,10 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
+.cxx/
# Remember to never publicly share your keystore.
-# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
deleted file mode 100644
index 721f4f4d..00000000
--- a/example/android/app/build.gradle
+++ /dev/null
@@ -1,58 +0,0 @@
-plugins {
- id "com.android.application"
- id "kotlin-android"
- // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
- id "dev.flutter.flutter-gradle-plugin"
-}
-
-def localProperties = new Properties()
-def localPropertiesFile = rootProject.file("local.properties")
-if (localPropertiesFile.exists()) {
- localPropertiesFile.withReader("UTF-8") { reader ->
- localProperties.load(reader)
- }
-}
-
-def flutterVersionCode = localProperties.getProperty("flutter.versionCode")
-if (flutterVersionCode == null) {
- flutterVersionCode = "1"
-}
-
-def flutterVersionName = localProperties.getProperty("flutter.versionName")
-if (flutterVersionName == null) {
- flutterVersionName = "1.0"
-}
-
-android {
- namespace = "com.github.cloudwebrtc.dart_sip_ua_example"
- compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
-
- defaultConfig {
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
- applicationId = "com.github.cloudwebrtc.dart_sip_ua_example"
- // You can update the following values to match your application needs.
- // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
- minSdk = flutter.minSdkVersion
- targetSdk = flutter.targetSdkVersion
- versionCode = flutterVersionCode.toInteger()
- versionName = flutterVersionName
- }
-
- buildTypes {
- release {
- // TODO: Add your own signing config for the release build.
- // Signing with the debug keys for now, so `flutter run --release` works.
- signingConfig = signingConfigs.debug
- }
- }
-}
-
-flutter {
- source = "../.."
-}
diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts
new file mode 100644
index 00000000..dadc2379
--- /dev/null
+++ b/example/android/app/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+android {
+ namespace = "com.github.cloudwebrtc.dart_sip_ua_example"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.github.cloudwebrtc.dart_sip_ua_example"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
diff --git a/example/android/app/src/main/java/com/github/cloudwebrtc/dart_sip_ua_example/MainActivity.java b/example/android/app/src/main/java/com/github/cloudwebrtc/dart_sip_ua_example/MainActivity.java
deleted file mode 100644
index 2aa4a9c5..00000000
--- a/example/android/app/src/main/java/com/github/cloudwebrtc/dart_sip_ua_example/MainActivity.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.github.cloudwebrtc.dart_sip_ua_example;
-
-import io.flutter.embedding.android.FlutterActivity;
-
-public class MainActivity extends FlutterActivity {
-}
diff --git a/example/android/app/src/main/kotlin/com/github/cloudwebrtc/dart_sip_ua_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/github/cloudwebrtc/dart_sip_ua_example/MainActivity.kt
new file mode 100644
index 00000000..cf3b481c
--- /dev/null
+++ b/example/android/app/src/main/kotlin/com/github/cloudwebrtc/dart_sip_ua_example/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.github.cloudwebrtc.dart_sip_ua_example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/example/android/build.gradle b/example/android/build.gradle
deleted file mode 100644
index d2ffbffa..00000000
--- a/example/android/build.gradle
+++ /dev/null
@@ -1,18 +0,0 @@
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-rootProject.buildDir = "../build"
-subprojects {
- project.buildDir = "${rootProject.buildDir}/${project.name}"
-}
-subprojects {
- project.evaluationDependsOn(":app")
-}
-
-tasks.register("clean", Delete) {
- delete rootProject.buildDir
-}
diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts
new file mode 100644
index 00000000..dbee657b
--- /dev/null
+++ b/example/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
index 3b5b324f..f018a618 100644
--- a/example/android/gradle.properties
+++ b/example/android/gradle.properties
@@ -1,3 +1,3 @@
-org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index e1ca574e..ac3b4792 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
deleted file mode 100644
index 536165d3..00000000
--- a/example/android/settings.gradle
+++ /dev/null
@@ -1,25 +0,0 @@
-pluginManagement {
- def flutterSdkPath = {
- def properties = new Properties()
- file("local.properties").withInputStream { properties.load(it) }
- def flutterSdkPath = properties.getProperty("flutter.sdk")
- assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
- return flutterSdkPath
- }()
-
- includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
-
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-}
-
-plugins {
- id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "7.3.0" apply false
- id "org.jetbrains.kotlin.android" version "1.7.10" apply false
-}
-
-include ":app"
diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts
new file mode 100644
index 00000000..fb605bc8
--- /dev/null
+++ b/example/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.9.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.1.0" apply false
+}
+
+include(":app")
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 7c569640..1dc6cf76 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 12.0
+ 13.0
diff --git a/example/ios/Podfile b/example/ios/Podfile
index c9339a03..620e46eb 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '12.0'
+# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -28,6 +28,8 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe
flutter_ios_podfile_setup
target 'Runner' do
+ use_frameworks!
+
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index a50780a3..e2ebb6e6 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -8,17 +8,16 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
- 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 331C80F3294D02FB00263BE5 /* RunnerTests.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
- 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
- 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
- 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */ = {
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
@@ -43,16 +42,15 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
- 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 331C80F3294D02FB00263BE5 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
- 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
- 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
- 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
@@ -60,13 +58,6 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
- 331C80EE294D02FB00263BE5 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -77,10 +68,10 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
- 331C80F2294D02FB00263BE5 /* RunnerTests */ = {
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
- 331C80F3294D02FB00263BE5 /* RunnerTests.m */,
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "";
@@ -101,8 +92,8 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
- 331C80F2294D02FB00263BE5 /* RunnerTests */,
97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "";
};
@@ -110,7 +101,7 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
- 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "";
@@ -118,46 +109,36 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
- 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
- 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
- 97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "";
};
- 97C146F11CF9000F007C117D /* Supporting Files */ = {
- isa = PBXGroup;
- children = (
- 97C146F21CF9000F007C117D /* main.m */,
- );
- name = "Supporting Files";
- sourceTree = "";
- };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
- 331C80F0294D02FB00263BE5 /* RunnerTests */ = {
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
- buildConfigurationList = 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
- 331C80ED294D02FB00263BE5 /* Sources */,
- 331C80EE294D02FB00263BE5 /* Frameworks */,
- 331C80EF294D02FB00263BE5 /* Resources */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
- 331C80F6294D02FB00263BE5 /* PBXTargetDependency */,
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
- productReference = 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
@@ -190,12 +171,13 @@
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
- 331C80F0294D02FB00263BE5 = {
+ 331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
};
};
};
@@ -213,13 +195,13 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
- 331C80F0294D02FB00263BE5 /* RunnerTests */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
- 331C80EF294D02FB00263BE5 /* Resources */ = {
+ 331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -274,11 +256,11 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
- 331C80ED294D02FB00263BE5 /* Sources */ = {
+ 331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */,
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -286,8 +268,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
- 97C146F31CF9000F007C117D /* main.m in Sources */,
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -295,10 +276,10 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
- 331C80F6294D02FB00263BE5 /* PBXTargetDependency */ = {
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
- targetProxy = 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
@@ -365,7 +346,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -379,8 +360,8 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 954G8NSFLG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -389,45 +370,55 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
- 331C80F8294D02FB00263BE5 /* Debug */ = {
+ 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
- 331C80F9294D02FB00263BE5 /* Release */ = {
+ 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
- 331C80FA294D02FB00263BE5 /* Profile */ = {
+ 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
@@ -481,7 +472,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -532,10 +523,12 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
@@ -546,8 +539,8 @@
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 954G8NSFLG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -556,6 +549,9 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
@@ -565,8 +561,8 @@
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 954G8NSFLG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -575,6 +571,8 @@
);
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
@@ -582,12 +580,12 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
- 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
- 331C80F8294D02FB00263BE5 /* Debug */,
- 331C80F9294D02FB00263BE5 /* Release */,
- 331C80FA294D02FB00263BE5 /* Profile */,
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3d0fb007..e3773d42 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
@@ -54,11 +55,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
+ enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h
deleted file mode 100644
index 36e21bbf..00000000
--- a/example/ios/Runner/AppDelegate.h
+++ /dev/null
@@ -1,6 +0,0 @@
-#import
-#import
-
-@interface AppDelegate : FlutterAppDelegate
-
-@end
diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m
deleted file mode 100644
index 70e83933..00000000
--- a/example/ios/Runner/AppDelegate.m
+++ /dev/null
@@ -1,13 +0,0 @@
-#import "AppDelegate.h"
-#import "GeneratedPluginRegistrant.h"
-
-@implementation AppDelegate
-
-- (BOOL)application:(UIApplication *)application
- didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
- [GeneratedPluginRegistrant registerWithRegistry:self];
- // Override point for customization after application launch.
- return [super application:application didFinishLaunchingWithOptions:launchOptions];
-}
-
-@end
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 00000000..62666446
--- /dev/null
+++ b/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 00000000..308a2a56
--- /dev/null
+++ b/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m
deleted file mode 100644
index dff6597e..00000000
--- a/example/ios/Runner/main.m
+++ /dev/null
@@ -1,9 +0,0 @@
-#import
-#import
-#import "AppDelegate.h"
-
-int main(int argc, char* argv[]) {
- @autoreleasepool {
- return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- }
-}
diff --git a/example/ios/RunnerTests/RunnerTests.m b/example/ios/RunnerTests/RunnerTests.m
deleted file mode 100644
index 6d8b0bde..00000000
--- a/example/ios/RunnerTests/RunnerTests.m
+++ /dev/null
@@ -1,16 +0,0 @@
-#import
-#import
-#import
-
-@interface RunnerTests : XCTestCase
-
-@end
-
-@implementation RunnerTests
-
-- (void)testExample {
- // If you add code to the Runner application, consider adding tests here.
- // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
-}
-
-@end
diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 00000000..86a7c3b1
--- /dev/null
+++ b/example/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 5ed459a3..c9f91f6f 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,4 +1,5 @@
import 'package:dart_sip_ua_example/src/theme_provider.dart';
+import 'package:dart_sip_ua_example/src/user_state/sip_user_cubit.dart';
import 'package:flutter/foundation.dart'
show debugDefaultTargetPlatformOverride;
import 'package:flutter/material.dart';
@@ -60,11 +61,18 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'Flutter Demo',
- theme: Provider.of(context).currentTheme,
- initialRoute: '/',
- onGenerateRoute: _onGenerateRoute,
+ return MultiProvider(
+ providers: [
+ Provider.value(value: _helper),
+ Provider(
+ create: (context) => SipUserCubit(sipHelper: _helper)),
+ ],
+ child: MaterialApp(
+ title: 'Flutter Demo',
+ theme: Provider.of(context).currentTheme,
+ initialRoute: '/',
+ onGenerateRoute: _onGenerateRoute,
+ ),
);
}
}
diff --git a/example/lib/src/callscreen.dart b/example/lib/src/callscreen.dart
index 976c0307..9fbb7c14 100644
--- a/example/lib/src/callscreen.dart
+++ b/example/lib/src/callscreen.dart
@@ -34,7 +34,7 @@ class _MyCallScreenWidget extends State
bool _speakerOn = false;
bool _hold = false;
bool _mirror = true;
- String? _holdOriginator;
+ Originator? _holdOriginator;
bool _callConfirmed = false;
CallStateEnum _state = CallStateEnum.NONE;
@@ -47,7 +47,7 @@ class _MyCallScreenWidget extends State
String? get remoteIdentity => call!.remote_identity;
- String get direction => call!.direction;
+ Direction? get direction => call!.direction;
Call? get call => widget._call;
@@ -177,16 +177,19 @@ class _MyCallScreenWidget extends State
void _handleStreams(CallState event) async {
MediaStream? stream = event.stream;
- if (event.originator == 'local') {
+ if (event.originator == Originator.local) {
if (_localRenderer != null) {
_localRenderer!.srcObject = stream;
}
- if (!kIsWeb && !WebRTC.platformIsDesktop) {
+
+ if (!kIsWeb &&
+ !WebRTC.platformIsDesktop &&
+ event.stream?.getAudioTracks().isNotEmpty == true) {
event.stream?.getAudioTracks().first.enableSpeakerphone(false);
}
_localStream = stream;
}
- if (event.originator == 'remote') {
+ if (event.originator == Originator.remote) {
if (_remoteRenderer != null) {
_remoteRenderer!.srcObject = stream;
}
@@ -227,6 +230,7 @@ class _MyCallScreenWidget extends State
'minFrameRate': '30',
},
'facingMode': 'user',
+ 'optional': [],
}
: false
};
@@ -239,6 +243,9 @@ class _MyCallScreenWidget extends State
await navigator.mediaDevices.getUserMedia(mediaConstraints);
mediaStream.addTrack(userStream.getAudioTracks()[0], addToNative: true);
} else {
+ if (!remoteHasVideo) {
+ mediaConstraints['video'] = false;
+ }
mediaStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
}
@@ -334,10 +341,14 @@ class _MyCallScreenWidget extends State
call!.voiceOnly = false;
});
helper!.renegotiate(
- call: call!, voiceOnly: false, done: (incomingMessage) {});
+ call: call!,
+ voiceOnly: false,
+ done: (IncomingMessage? incomingMessage) {});
} else {
helper!.renegotiate(
- call: call!, voiceOnly: true, done: (incomingMessage) {});
+ call: call!,
+ voiceOnly: true,
+ done: (IncomingMessage? incomingMessage) {});
}
}
@@ -412,7 +423,7 @@ class _MyCallScreenWidget extends State
switch (_state) {
case CallStateEnum.NONE:
case CallStateEnum.CONNECTING:
- if (direction == 'INCOMING') {
+ if (direction == Direction.incoming) {
basicActions.add(ActionButton(
title: "Accept",
fillColor: Colors.green,
@@ -597,7 +608,7 @@ class _MyCallScreenWidget extends State
child: Text(
(voiceOnly ? 'VOICE CALL' : 'VIDEO CALL') +
(_hold
- ? ' PAUSED BY ${_holdOriginator!.toUpperCase()}'
+ ? ' PAUSED BY ${_holdOriginator!.name}'
: ''),
style: TextStyle(fontSize: 24, color: textColor),
),
@@ -644,7 +655,7 @@ class _MyCallScreenWidget extends State
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
- title: Text('[$direction] ${EnumHelper.getName(_state)}'),
+ title: Text('[$direction] ${_state.name}'),
),
body: _buildContent(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
diff --git a/example/lib/src/dialpad.dart b/example/lib/src/dialpad.dart
index 79db8747..fdd357fd 100644
--- a/example/lib/src/dialpad.dart
+++ b/example/lib/src/dialpad.dart
@@ -1,7 +1,9 @@
import 'package:dart_sip_ua_example/src/theme_provider.dart';
+import 'package:dart_sip_ua_example/src/user_state/sip_user_cubit.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
+import 'package:logger/logger.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -24,6 +26,9 @@ class _MyDialPadWidget extends State
SIPUAHelper? get helper => widget._helper;
TextEditingController? _textController;
late SharedPreferences _preferences;
+ late SipUserCubit currentUserCubit;
+
+ final Logger _logger = Logger();
String? receivedMsg;
@@ -172,7 +177,7 @@ class _MyDialPadWidget extends State
List _buildDialPad() {
Color? textFieldColor =
- Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.5);
+ Theme.of(context).textTheme.bodyMedium?.color?.withValues(alpha: 0.5);
Color? textFieldFill =
Theme.of(context).buttonTheme.colorScheme?.surfaceContainerLowest;
return [
@@ -190,15 +195,15 @@ class _MyDialPadWidget extends State
filled: true,
fillColor: textFieldFill,
border: OutlineInputBorder(
- borderSide: BorderSide(color: Colors.blue.withOpacity(0.5)),
+ borderSide: BorderSide(color: Colors.blue.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(5),
),
enabledBorder: OutlineInputBorder(
- borderSide: BorderSide(color: Colors.blue.withOpacity(0.5)),
+ borderSide: BorderSide(color: Colors.blue.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(5),
),
focusedBorder: OutlineInputBorder(
- borderSide: BorderSide(color: Colors.blue.withOpacity(0.5)),
+ borderSide: BorderSide(color: Colors.blue.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(5),
),
),
@@ -241,6 +246,8 @@ class _MyDialPadWidget extends State
Color? textColor = Theme.of(context).textTheme.bodyMedium?.color;
Color? iconColor = Theme.of(context).iconTheme.color;
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+ currentUserCubit = context.watch();
+
return Scaffold(
appBar: AppBar(
title: Text("Dart SIP UA Demo"),
@@ -321,7 +328,7 @@ class _MyDialPadWidget extends State
SizedBox(height: 8),
Center(
child: Text(
- 'Register Status: ${EnumHelper.getName(helper!.registerState.state)}',
+ 'Register Status: ${helper!.registerState.state?.name ?? ''}',
style: TextStyle(fontSize: 18, color: textColor),
),
),
@@ -345,7 +352,9 @@ class _MyDialPadWidget extends State
@override
void registrationStateChanged(RegistrationState state) {
- setState(() {});
+ setState(() {
+ _logger.i("Registration state: ${state.state?.name}");
+ });
}
@override
@@ -353,11 +362,27 @@ class _MyDialPadWidget extends State
@override
void callStateChanged(Call call, CallState callState) {
- if (callState.state == CallStateEnum.CALL_INITIATION) {
- Navigator.pushNamed(context, '/callscreen', arguments: call);
+ switch (callState.state) {
+ case CallStateEnum.CALL_INITIATION:
+ Navigator.pushNamed(context, '/callscreen', arguments: call);
+ break;
+ case CallStateEnum.FAILED:
+ reRegisterWithCurrentUser();
+ break;
+ case CallStateEnum.ENDED:
+ reRegisterWithCurrentUser();
+ break;
+ default:
}
}
+ void reRegisterWithCurrentUser() async {
+ if (currentUserCubit.state == null) return;
+ if (helper!.registered) await helper!.unregister();
+ _logger.i("Re-registering");
+ currentUserCubit.register(currentUserCubit.state!);
+ }
+
@override
void onNewMessage(SIPMessageRequest msg) {
//Save the incoming message to DB
diff --git a/example/lib/src/register.dart b/example/lib/src/register.dart
index a9f0bc98..c96332d7 100644
--- a/example/lib/src/register.dart
+++ b/example/lib/src/register.dart
@@ -1,4 +1,7 @@
+import 'package:dart_sip_ua_example/src/user_state/sip_user.dart';
+import 'package:dart_sip_ua_example/src/user_state/sip_user_cubit.dart';
import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sip_ua/sip_ua.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
@@ -32,6 +35,8 @@ class _MyRegisterWidget extends State
SIPUAHelper? get helper => widget._helper;
+ late SipUserCubit currentUser;
+
@override
void initState() {
super.initState();
@@ -98,47 +103,44 @@ class _MyRegisterWidget extends State
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
- title: Text('$alertFieldName is empty'),
- content: Text('Please enter $alertFieldName!'),
- actions: [
- TextButton(
- child: Text('Ok'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- ),
- ],
- );
+ title: Text('$alertFieldName is empty'),
+ content: Text('Please enter $alertFieldName!'),
+ actions: [
+ TextButton(
+ child: Text('Ok'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ ]);
},
);
}
- void _handleSave(BuildContext context) {
+ void _register(BuildContext context) {
if (_wsUriController.text == '') {
_alert(context, "WebSocket URL");
} else if (_sipUriController.text == '') {
_alert(context, "SIP URI");
}
- UaSettings settings = UaSettings();
-
- settings.port = _portController.text;
- settings.webSocketSettings.extraHeaders = _wsExtraHeaders;
- settings.webSocketSettings.allowBadCertificate = true;
- //settings.webSocketSettings.userAgent = 'Dart/2.8 (dart:io) for OpenSIPS.';
- settings.tcpSocketSettings.allowBadCertificate = true;
- settings.transportType = _selectedTransport;
- settings.uri = _sipUriController.text;
- settings.webSocketUrl = _wsUriController.text;
- settings.host = _sipUriController.text.split('@')[1];
- settings.authorizationUser = _authorizationUserController.text;
- settings.password = _passwordController.text;
- settings.displayName = _displayNameController.text;
- settings.userAgent = 'Dart SIP Client v1.0.0';
- settings.dtmfMode = DtmfMode.RFC2833;
- settings.contact_uri = 'sip:${_sipUriController.text}';
+ _saveSettings();
- helper!.start(settings);
+ currentUser.register(SipUser(
+ wsUrl: _wsUriController.text,
+ //this is the websocket url which was missing in the original code hence it
+ //was showing null in the register method of sip_user_cubit.dart and always
+ //redirected to 'wss://tryit.jssip.net:10443', present inside sip_ua_helper.dart
+ //161: uaSettings.webSocketUrl ?? 'wss://tryit.jssip.net:10443',
+ //this will help people trying out the example to register with the correct url
+ //without changing the code in sip_ua_helper.dart
+ selectedTransport: _selectedTransport,
+ wsExtraHeaders: _wsExtraHeaders,
+ sipUri: _sipUriController.text,
+ port: _portController.text,
+ displayName: _displayNameController.text,
+ password: _passwordController.text,
+ authUser: _authorizationUserController.text));
}
@override
@@ -146,22 +148,45 @@ class _MyRegisterWidget extends State
Color? textColor = Theme.of(context).textTheme.bodyMedium?.color;
Color? textFieldFill =
Theme.of(context).buttonTheme.colorScheme?.surfaceContainerLowest;
+ currentUser = context.watch();
+
OutlineInputBorder border = OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(5),
);
Color? textLabelColor =
- Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.5);
+ Theme.of(context).textTheme.bodyMedium?.color?.withValues(alpha: 0.5);
return Scaffold(
appBar: AppBar(
title: Text("SIP Account"),
),
+ bottomNavigationBar: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ child: SizedBox(
+ height: 40,
+ child: ElevatedButton(
+ child: Text('Register'),
+ onPressed: () => _register(context),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
children: [
Center(
child: Text(
- 'Register Status: ${EnumHelper.getName(_registerState.state)}',
+ 'Register Status: ${_registerState.state?.name ?? ''}',
style: TextStyle(fontSize: 18, color: textColor),
),
),
@@ -281,11 +306,6 @@ class _MyRegisterWidget extends State
],
),
],
- const SizedBox(height: 20),
- ElevatedButton(
- child: Text('Register'),
- onPressed: () => _handleSave(context),
- ),
],
),
);
diff --git a/example/lib/src/user_state/sip_user.dart b/example/lib/src/user_state/sip_user.dart
new file mode 100644
index 00000000..5aed56fc
--- /dev/null
+++ b/example/lib/src/user_state/sip_user.dart
@@ -0,0 +1,51 @@
+import 'package:sip_ua/sip_ua.dart';
+
+class SipUser {
+ final String port;
+ final String displayName;
+ final String? wsUrl;
+ final String? sipUri;
+ final String password;
+ final String authUser;
+ final TransportType selectedTransport;
+ final Map? wsExtraHeaders;
+
+ SipUser({
+ required this.port,
+ required this.displayName,
+ required this.password,
+ required this.authUser,
+ required this.selectedTransport,
+ this.wsExtraHeaders,
+ this.wsUrl,
+ this.sipUri,
+ });
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ return other is SipUser &&
+ other.port == port &&
+ other.displayName == displayName &&
+ other.wsUrl == wsUrl &&
+ other.sipUri == sipUri &&
+ other.selectedTransport == selectedTransport &&
+ other.wsExtraHeaders == wsExtraHeaders &&
+ other.password == password &&
+ other.authUser == authUser;
+ }
+
+ @override
+ int get hashCode {
+ return Object.hashAll([
+ port,
+ displayName,
+ wsUrl,
+ sipUri,
+ password,
+ wsExtraHeaders,
+ selectedTransport,
+ authUser,
+ ]);
+ }
+}
diff --git a/example/lib/src/user_state/sip_user_cubit.dart b/example/lib/src/user_state/sip_user_cubit.dart
new file mode 100644
index 00000000..eddc61db
--- /dev/null
+++ b/example/lib/src/user_state/sip_user_cubit.dart
@@ -0,0 +1,31 @@
+import 'package:bloc/bloc.dart';
+import 'package:dart_sip_ua_example/src/user_state/sip_user.dart';
+import 'package:sip_ua/sip_ua.dart';
+
+class SipUserCubit extends Cubit {
+ final SIPUAHelper sipHelper;
+ SipUserCubit({required this.sipHelper}) : super(null);
+
+
+ void register(SipUser user) {
+ UaSettings settings = UaSettings();
+ settings.port = user.port;
+ settings.webSocketSettings.extraHeaders = user.wsExtraHeaders ?? {};
+ settings.webSocketSettings.allowBadCertificate = true;
+ //settings.webSocketSettings.userAgent = 'Dart/2.8 (dart:io) for OpenSIPS.';
+ settings.tcpSocketSettings.allowBadCertificate = true;
+ settings.transportType = user.selectedTransport;
+ settings.uri = user.sipUri;
+ settings.webSocketUrl = user.wsUrl;
+ settings.host = user.sipUri?.split('@')[1];
+ settings.authorizationUser = user.authUser;
+ settings.password = user.password;
+ settings.displayName = user.displayName;
+ settings.userAgent = 'Dart SIP Client v1.0.0';
+ settings.dtmfMode = DtmfMode.RFC2833;
+ settings.contact_uri = 'sip:${user.sipUri}';
+
+ emit(user);
+ sipHelper.start(settings);
+ }
+}
diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt
index 0eb7fd0f..f7843d93 100644
--- a/example/linux/CMakeLists.txt
+++ b/example/linux/CMakeLists.txt
@@ -1,5 +1,5 @@
# Project-level configuration.
-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.13)
project(runner LANGUAGES CXX)
# The name of the executable created for the application. Change this to change
@@ -54,25 +54,8 @@ add_subdirectory(${FLUTTER_MANAGED_DIR})
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
-add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
-
-# Define the application target. To change its name, change BINARY_NAME above,
-# not the value here, or `flutter run` will no longer work.
-#
-# Any new source files that you add to the application should be added here.
-add_executable(${BINARY_NAME}
- "main.cc"
- "my_application.cc"
- "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
-)
-
-# Apply the standard set of build settings. This can be removed for applications
-# that need different build settings.
-apply_standard_settings(${BINARY_NAME})
-
-# Add dependency libraries. Add any application-specific dependencies here.
-target_link_libraries(${BINARY_NAME} PRIVATE flutter)
-target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+# Application build; see runner/CMakeLists.txt.
+add_subdirectory("runner")
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)
diff --git a/example/linux/runner/CMakeLists.txt b/example/linux/runner/CMakeLists.txt
new file mode 100644
index 00000000..e97dabc7
--- /dev/null
+++ b/example/linux/runner/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 3.13)
+project(runner LANGUAGES CXX)
+
+# Define the application target. To change its name, change BINARY_NAME in the
+# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
+# work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add preprocessor definitions for the application ID.
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
diff --git a/example/linux/main.cc b/example/linux/runner/main.cc
similarity index 100%
rename from example/linux/main.cc
rename to example/linux/runner/main.cc
diff --git a/example/linux/my_application.cc b/example/linux/runner/my_application.cc
similarity index 82%
rename from example/linux/my_application.cc
rename to example/linux/runner/my_application.cc
index 9cc6c558..ff70ee17 100644
--- a/example/linux/my_application.cc
+++ b/example/linux/runner/my_application.cc
@@ -14,6 +14,12 @@ struct _MyApplication {
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+// Called when first Flutter frame received.
+static void first_frame_cb(MyApplication* self, FlView *view)
+{
+ gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
+}
+
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
@@ -48,15 +54,23 @@ static void my_application_activate(GApplication* application) {
}
gtk_window_set_default_size(window, 1280, 720);
- gtk_widget_show(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
FlView* view = fl_view_new(project);
+ GdkRGBA background_color;
+ // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent.
+ gdk_rgba_parse(&background_color, "#000000");
+ fl_view_set_background_color(view, &background_color);
gtk_widget_show(GTK_WIDGET(view));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+ // Show the window when Flutter renders.
+ // Requires the view to be realized so we can start rendering.
+ g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self);
+ gtk_widget_realize(GTK_WIDGET(view));
+
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_grab_focus(GTK_WIDGET(view));
@@ -117,6 +131,12 @@ static void my_application_class_init(MyApplicationClass* klass) {
static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() {
+ // Set the program name to the application ID, which helps various systems
+ // like GTK and desktop environments map this running application to its
+ // corresponding .desktop file. This ensures better integration by allowing
+ // the application to be recognized beyond its binary name.
+ g_set_prgname(APPLICATION_ID);
+
return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE,
diff --git a/example/linux/my_application.h b/example/linux/runner/my_application.h
similarity index 100%
rename from example/linux/my_application.h
rename to example/linux/runner/my_application.h
diff --git a/example/macos/Podfile b/example/macos/Podfile
index c795730d..ff5ddb3b 100644
--- a/example/macos/Podfile
+++ b/example/macos/Podfile
@@ -1,4 +1,4 @@
-platform :osx, '10.14'
+platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -28,7 +28,6 @@ flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
- use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj
index 4fc25999..91bbd10b 100644
--- a/example/macos/Runner.xcodeproj/project.pbxproj
+++ b/example/macos/Runner.xcodeproj/project.pbxproj
@@ -64,7 +64,7 @@
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
- 33CC10ED2044A3C60003C045 /* dart_sip_ua_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dart_sip_ua_example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10ED2044A3C60003C045 /* dart_sip_ua_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = dart_sip_ua_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
@@ -461,7 +461,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -478,6 +478,17 @@
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
+ ENABLE_APP_SANDBOX = YES;
+ ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
+ ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
+ ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = YES;
+ ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
+ ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
+ ENABLE_RESOURCE_ACCESS_CAMERA = YES;
+ ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
+ ENABLE_RESOURCE_ACCESS_LOCATION = NO;
+ ENABLE_RESOURCE_ACCESS_PRINTING = NO;
+ ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -543,7 +554,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -593,7 +604,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- MACOSX_DEPLOYMENT_TARGET = 10.14;
+ MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@@ -610,6 +621,17 @@
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
+ ENABLE_APP_SANDBOX = YES;
+ ENABLE_INCOMING_NETWORK_CONNECTIONS = NO;
+ ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
+ ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = YES;
+ ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO;
+ ENABLE_RESOURCE_ACCESS_CALENDARS = NO;
+ ENABLE_RESOURCE_ACCESS_CAMERA = YES;
+ ENABLE_RESOURCE_ACCESS_CONTACTS = NO;
+ ENABLE_RESOURCE_ACCESS_LOCATION = NO;
+ ENABLE_RESOURCE_ACCESS_PRINTING = NO;
+ ENABLE_RESOURCE_ACCESS_USB = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index d7739d4e..041a88dd 100644
--- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -59,6 +59,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
+ enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift
index d53ef643..b3c17614 100644
--- a/example/macos/Runner/AppDelegate.swift
+++ b/example/macos/Runner/AppDelegate.swift
@@ -1,9 +1,13 @@
import Cocoa
import FlutterMacOS
-@NSApplicationMain
+@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
+
+ override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
+ return true
+ }
}
diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig
index 3f865707..01b99e08 100644
--- a/example/macos/Runner/Configs/AppInfo.xcconfig
+++ b/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -11,4 +11,4 @@ PRODUCT_NAME = dart_sip_ua_example
PRODUCT_BUNDLE_IDENTIFIER = com.github.cloudwebrtc.dartSipUaExample
// The copyright displayed in application information
-PRODUCT_COPYRIGHT = Copyright © 2024 com.github.cloudwebrtc. All rights reserved.
+PRODUCT_COPYRIGHT = Copyright © 2025 com.github.cloudwebrtc. All rights reserved.
diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements
index ab97dbc1..dddb8a30 100644
--- a/example/macos/Runner/DebugProfile.entitlements
+++ b/example/macos/Runner/DebugProfile.entitlements
@@ -8,11 +8,5 @@
com.apple.security.network.server
- com.apple.security.device.camera
-
- com.apple.security.device.microphone
-
- com.apple.security.network.client
-
diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements
index 84c4851e..852fa1a4 100644
--- a/example/macos/Runner/Release.entitlements
+++ b/example/macos/Runner/Release.entitlements
@@ -4,11 +4,5 @@
com.apple.security.app-sandbox
- com.apple.security.device.camera
-
- com.apple.security.device.microphone
-
- com.apple.security.network.client
-
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 1e834d69..e8679f6f 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -15,24 +15,24 @@ version: 1.0.0+1
publish_to: none
environment:
- sdk: ">=2.12.0 <3.0.0"
+ sdk: ">=2.15.0 <3.0.0"
flutter: ">=1.10.0"
dependencies:
+ bloc: ^9.0.0
flutter:
sdk: flutter
sip_ua:
path: ../
shared_preferences: ^2.2.0
permission_handler: ^11.1.0
- flutter_webrtc: ^0.10.4
provider: 6.1.2
- logger: ^2.4.0
+ logger: ^2.5.0
dev_dependencies:
flutter_test:
sdk: flutter
- lints: ^3.0.0
+ lints: ^4.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
index 4e8e53f4..900418a1 100644
--- a/example/test/widget_test.dart
+++ b/example/test/widget_test.dart
@@ -13,7 +13,7 @@ import 'package:dart_sip_ua_example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
- await tester.pumpWidget(const MyApp());
+ await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
diff --git a/example/web/index.html b/example/web/index.html
index ea6e27f9..b11d2d84 100644
--- a/example/web/index.html
+++ b/example/web/index.html
@@ -21,7 +21,7 @@
-
+
diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc
index 55598ef5..282226a5 100644
--- a/example/windows/runner/Runner.rc
+++ b/example/windows/runner/Runner.rc
@@ -93,7 +93,7 @@ BEGIN
VALUE "FileDescription", "dart_sip_ua_example" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "dart_sip_ua_example" "\0"
- VALUE "LegalCopyright", "Copyright (C) 2024 com.github.cloudwebrtc. All rights reserved." "\0"
+ VALUE "LegalCopyright", "Copyright (C) 2025 com.github.cloudwebrtc. All rights reserved." "\0"
VALUE "OriginalFilename", "dart_sip_ua_example.exe" "\0"
VALUE "ProductName", "dart_sip_ua_example" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0"
diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest
index a42ea768..153653e8 100644
--- a/example/windows/runner/runner.exe.manifest
+++ b/example/windows/runner/runner.exe.manifest
@@ -9,12 +9,6 @@
-
-
-
-
-
-
diff --git a/lib/sip_ua.dart b/lib/sip_ua.dart
index c7f2efe7..7c37f449 100644
--- a/lib/sip_ua.dart
+++ b/lib/sip_ua.dart
@@ -1,7 +1,5 @@
-export 'src/enum_helper.dart';
+export 'src/enums.dart';
export 'src/sip_message.dart';
export 'src/sip_ua_helper.dart';
export 'src/transport_type.dart';
export 'src/uri.dart';
-
-
diff --git a/lib/src/config.dart b/lib/src/config.dart
index d9849d85..7dc05c9c 100644
--- a/lib/src/config.dart
+++ b/lib/src/config.dart
@@ -1,13 +1,11 @@
-import 'package:sip_ua/sip_ua.dart';
-import 'package:sip_ua/src/transports/socket_interface.dart';
-import 'package:sip_ua/src/transports/tcp_socket.dart';
+import '../sip_ua.dart';
import 'constants.dart' as DartSIP_C;
import 'constants.dart';
import 'exceptions.dart' as Exceptions;
import 'grammar.dart';
import 'logger.dart';
+import 'transports/socket_interface.dart';
import 'transports/web_socket.dart';
-import 'uri.dart';
import 'utils.dart' as Utils;
// Default settings.
@@ -20,8 +18,8 @@ class Settings {
// SIP account.
String? display_name;
- dynamic uri;
- dynamic contact_uri;
+ URI? uri;
+ URI? contact_uri;
String user_agent = DartSIP_C.USER_AGENT;
// SIP instance id (GRUU).
@@ -66,6 +64,9 @@ class Settings {
/// ICE Gathering Timeout (in millisecond).
int ice_gathering_timeout = 500;
+ /// Call statistics in the log
+ bool log_call_statistics = false;
+
bool terminateOnAudioMediaPortZero = false;
/// Sip Message Delay (in millisecond) ( default 0 ).
@@ -86,7 +87,7 @@ class Checks {
* List of Objects and Socket: [{socket: socket1}, socket2]
*/
List copy = [];
- if (sockets is List && sockets!.length > 0) {
+ if (sockets is List && sockets!.isNotEmpty) {
for (SIPUASocketInterface socket in sockets) {
copy.add(socket);
}
@@ -97,21 +98,14 @@ class Checks {
dst!.sockets = copy;
},
'uri': (Settings src, Settings? dst) {
- dynamic uri = src.uri;
if (src.uri == null && dst!.uri == null) {
throw Exceptions.ConfigurationError('uri', null);
}
- if (!uri.contains(RegExp(r'^sip:', caseSensitive: false))) {
- uri = '${DartSIP_C.SIP}:$uri';
- }
- dynamic parsed = URI.parse(uri);
- if (parsed == null) {
- throw Exceptions.ConfigurationError('uri', parsed);
- } else if (parsed.user == null) {
- throw Exceptions.ConfigurationError('uri', parsed);
- } else {
- dst!.uri = parsed;
+ URI uri = src.uri!;
+ if (!uri.toString().contains(RegExp(r'^sip:', caseSensitive: false))) {
+ uri.scheme = DartSIP_C.SIP;
}
+ dst!.uri = uri;
},
'transport_type': (Settings src, Settings? dst) {
dynamic transportType = src.transportType;
@@ -264,6 +258,9 @@ class Checks {
},
'ice_gathering_timeout': (Settings src, Settings? dst) {
dst!.ice_gathering_timeout = src.ice_gathering_timeout;
+ },
+ 'log_call_statistics': (Settings src, Settings? dst) {
+ dst!.log_call_statistics = src.log_call_statistics;
}
};
}
@@ -287,6 +284,6 @@ void load(Settings src, Settings? dst) {
});
} catch (e) {
logger.e('Failed to load config: ${e.toString()}');
- throw e;
+ rethrow;
}
}
diff --git a/lib/src/enum_helper.dart b/lib/src/enum_helper.dart
deleted file mode 100644
index 55f8949e..00000000
--- a/lib/src/enum_helper.dart
+++ /dev/null
@@ -1,43 +0,0 @@
-import 'package:recase/recase.dart';
-
-///
-/// Provides a collection of methods that help when working with
-/// enums.
-///
-class EnumHelper {
- static T getByIndex(List values, int index) {
- return values.elementAt(index - 1);
- }
-
- static int getIndexOf(List values, T value) {
- return values.indexOf(value);
- }
-
- ///
- /// Returns the Enum name without the enum class.
- /// e.g. DayName.Wednesday becomes Wednesday.
- /// By default we recase the value to Title Case.
- /// You can pass an alternate method to control the format.
- ///
- static String getName(T enumValue,
- {String Function(String value) recase = reCase}) {
- String name = enumValue.toString();
- int period = name.indexOf('.');
-
- return recase(name.substring(period + 1));
- }
-
- static String reCase(String value) {
- return ReCase(value).titleCase;
- }
-
- static T getEnum(String enumName, List values) {
- String cleanedName = reCase(enumName);
- for (int i = 0; i < values.length; i++) {
- if (cleanedName == getName(values[i])) {
- return values[i];
- }
- }
- throw Exception("$cleanedName doesn't exist in the list of enums $values");
- }
-}
diff --git a/lib/src/enums.dart b/lib/src/enums.dart
new file mode 100644
index 00000000..814b6f8a
--- /dev/null
+++ b/lib/src/enums.dart
@@ -0,0 +1,39 @@
+/// Defines the direction of a communication,
+/// indicating whether it is outgoing or incoming.
+///
+/// Used to specify the flow of calls or messages.
+enum Direction {
+ /// Represents an outgoing call or message.
+ outgoing,
+
+ /// Represents an incoming call or message.
+ incoming
+}
+
+/// Identifies the originator of a communication,
+/// specifying whether the initiator is local or remote.
+///
+/// This is useful for determining who started the call or message.
+enum Originator {
+ /// Represents the user of this device initiated the communication.
+ local,
+
+ /// Represents the communication was initiated by someone else.
+ remote,
+
+ /// Represents that the communication was initiated by the system (e.g., automated processes).
+ system,
+}
+
+/// Represents the type of SDP (Session Description Protocol) message
+/// used in a communication session.
+///
+/// SDP messages are exchanged between peers during the setup of a media connection.
+enum SdpType {
+ /// Represents an SDP offer, which is the initial proposal sent to set up a media session.
+ offer,
+
+ /// Represents an SDP answer, which is the response to an SDP offer,
+ /// confirming or adjusting the session parameters.
+ answer
+}
diff --git a/lib/src/event_manager/call_events.dart b/lib/src/event_manager/call_events.dart
index 0270a8cc..feb13750 100644
--- a/lib/src/event_manager/call_events.dart
+++ b/lib/src/event_manager/call_events.dart
@@ -1,5 +1,6 @@
import 'package:flutter_webrtc/flutter_webrtc.dart';
+import '../enums.dart';
import '../rtc_session.dart';
import '../sip_message.dart';
import 'events.dart';
@@ -11,9 +12,10 @@ class CallEvent extends EventType {
}
class EventNewRTCSession extends CallEvent {
- EventNewRTCSession({RTCSession? session, String? originator, dynamic request})
+ EventNewRTCSession(
+ {RTCSession? session, Originator? originator, dynamic request})
: super(session);
- String? originator;
+ Originator? originator;
dynamic request;
}
@@ -25,7 +27,7 @@ class EventCallEnded extends CallEvent {
EventCallEnded(
{RTCSession? session, this.originator, this.cause, this.request})
: super(session);
- String? originator;
+ Originator? originator;
ErrorCause? cause;
IncomingRequest? request;
}
@@ -34,7 +36,7 @@ class EventCallProgress extends CallEvent {
EventCallProgress(
{RTCSession? session, this.originator, this.response, this.cause})
: super(session);
- String? originator;
+ Originator? originator;
dynamic response;
ErrorCause? cause;
}
@@ -42,18 +44,19 @@ class EventCallProgress extends CallEvent {
class EventCallConfirmed extends CallEvent {
EventCallConfirmed({RTCSession? session, this.originator, this.ack})
: super(session);
- String? originator;
+ Originator? originator;
dynamic ack;
}
class EventCallHold extends CallEvent {
EventCallHold({RTCSession? session, this.originator}) : super(session);
- String? originator;
+ Originator? originator;
}
class EventCallUnhold extends CallEvent {
- EventCallUnhold({RTCSession? session, String? originator}) : super(session);
- String? originator;
+ EventCallUnhold({RTCSession? session, Originator? originator})
+ : super(session);
+ Originator? originator;
}
class EventCallMuted extends CallEvent {
@@ -73,7 +76,7 @@ class EventCallUnmuted extends CallEvent {
class EventCallAccepted extends CallEvent {
EventCallAccepted({RTCSession? session, this.originator, this.response})
: super(session);
- String? originator;
+ Originator? originator;
dynamic response;
}
@@ -89,7 +92,7 @@ class EventCallFailed extends CallEvent {
this.status_line})
: super(session);
dynamic response;
- String? originator;
+ Originator? originator;
ErrorCause? cause;
dynamic request;
String? status_line;
@@ -98,7 +101,7 @@ class EventCallFailed extends CallEvent {
class EventStream extends CallEvent {
EventStream({RTCSession? session, this.originator, this.stream})
: super(session);
- String? originator;
+ Originator? originator;
MediaStream? stream;
}
diff --git a/lib/src/event_manager/internal_events.dart b/lib/src/event_manager/internal_events.dart
index 0d2f3ed8..9b5adad2 100644
--- a/lib/src/event_manager/internal_events.dart
+++ b/lib/src/event_manager/internal_events.dart
@@ -1,5 +1,6 @@
import 'package:flutter_webrtc/flutter_webrtc.dart';
+import '../enums.dart';
import '../rtc_session.dart' show RTCSession;
import '../rtc_session/dtmf.dart';
import '../rtc_session/info.dart';
@@ -33,8 +34,8 @@ class EventOnAuthenticated extends EventType {
class EventSdp extends EventType {
EventSdp({this.originator, this.type, this.sdp});
- String? originator;
- String? type;
+ Originator? originator;
+ SdpType? type;
String? sdp;
}
@@ -55,7 +56,7 @@ class EventSetLocalDescriptionFailed extends EventType {
class EventFailedUnderScore extends EventType {
EventFailedUnderScore({this.originator, this.cause});
- String? originator;
+ Originator? originator;
ErrorCause? cause;
}
@@ -66,14 +67,14 @@ class EventGetUserMediaFailed extends EventType {
class EventNewDTMF extends EventType {
EventNewDTMF({this.originator, this.request, this.dtmf});
- String? originator;
+ Originator? originator;
dynamic request;
DTMF? dtmf;
}
class EventNewInfo extends EventType {
EventNewInfo({this.originator, this.request, this.info});
- String? originator;
+ Originator? originator;
dynamic request;
Info? info;
}
@@ -127,7 +128,7 @@ class EventOnFialed extends EventType {}
class EventSucceeded extends EventType {
EventSucceeded({this.response, this.originator});
- String? originator;
+ Originator? originator;
IncomingMessage? response;
}
diff --git a/lib/src/event_manager/message_events.dart b/lib/src/event_manager/message_events.dart
index ccc15b68..445f61f9 100644
--- a/lib/src/event_manager/message_events.dart
+++ b/lib/src/event_manager/message_events.dart
@@ -1,9 +1,10 @@
+import '../enums.dart';
import '../message.dart';
import 'events.dart';
class EventNewMessage extends EventType {
EventNewMessage({this.message, this.originator, this.request});
dynamic request;
- String? originator;
+ Originator? originator;
Message? message;
}
diff --git a/lib/src/event_manager/notifier_events.dart b/lib/src/event_manager/notifier_events.dart
index 945e735b..d1ef620d 100644
--- a/lib/src/event_manager/notifier_events.dart
+++ b/lib/src/event_manager/notifier_events.dart
@@ -1,4 +1,4 @@
-import 'package:sip_ua/src/sip_message.dart';
+import '../sip_message.dart';
import 'events.dart';
class EventTerminated extends EventType {
diff --git a/lib/src/event_manager/options_events.dart b/lib/src/event_manager/options_events.dart
index 668c2b5d..2bfe65a7 100644
--- a/lib/src/event_manager/options_events.dart
+++ b/lib/src/event_manager/options_events.dart
@@ -1,9 +1,10 @@
+import '../enums.dart';
import '../options.dart';
import 'events.dart';
class EventNewOptions extends EventType {
EventNewOptions({this.message, this.originator, this.request});
dynamic request;
- String? originator;
+ Originator? originator;
Options? message;
}
diff --git a/lib/src/event_manager/transport_events.dart b/lib/src/event_manager/transport_events.dart
index 881a9d2d..83bc9f1b 100644
--- a/lib/src/event_manager/transport_events.dart
+++ b/lib/src/event_manager/transport_events.dart
@@ -1,4 +1,4 @@
-import 'package:sip_ua/src/transports/socket_interface.dart';
+import '../transports/socket_interface.dart';
import '../transports/web_socket.dart';
import 'events.dart';
diff --git a/lib/src/logger.dart b/lib/src/logger.dart
index d8989695..21169d23 100644
--- a/lib/src/logger.dart
+++ b/lib/src/logger.dart
@@ -1,7 +1,6 @@
import 'package:intl/intl.dart';
import 'package:logger/logger.dart';
-import 'enum_helper.dart';
import 'stack_trace_nj.dart';
Logger logger = Log();
@@ -41,8 +40,7 @@ class MyLogPrinter extends LogPrinter {
@override
List log(LogEvent event) {
- if (EnumHelper.getIndexOf(Level.values, Log._loggingLevel) >
- EnumHelper.getIndexOf(Level.values, event.level)) {
+ if (Log._loggingLevel.index > event.level.index) {
// don't log events where the log level is set higher
return [];
}
diff --git a/lib/src/message.dart b/lib/src/message.dart
index 2aeffe49..c8f6092d 100644
--- a/lib/src/message.dart
+++ b/lib/src/message.dart
@@ -1,10 +1,11 @@
-import 'package:sip_ua/src/name_addr_header.dart';
import 'constants.dart' as DartSIP_C;
import 'constants.dart';
+import 'enums.dart';
import 'event_manager/event_manager.dart';
import 'event_manager/internal_events.dart';
import 'exceptions.dart' as Exceptions;
import 'logger.dart';
+import 'name_addr_header.dart';
import 'request_sender.dart';
import 'sip_message.dart';
import 'ua.dart';
@@ -17,14 +18,14 @@ class Message extends EventManager with Applicant {
final UA _ua;
dynamic _request;
bool _closed = false;
- String? _direction;
+ Direction? _direction;
NameAddrHeader? _local_identity;
NameAddrHeader? _remote_identity;
// Whether an incoming message has been replied.
bool _is_replied = false;
// Custom message empty object for high level use.
final Map _data = {};
- String? get direction => _direction;
+ Direction? get direction => _direction;
NameAddrHeader? get local_identity => _local_identity;
@@ -76,7 +77,7 @@ class Message extends EventManager with Applicant {
RequestSender request_sender = RequestSender(_ua, _request, handlers);
- _newMessage('local', _request);
+ _newMessage(Originator.local, _request);
request_sender.send();
}
@@ -84,7 +85,7 @@ class Message extends EventManager with Applicant {
void init_incoming(IncomingRequest request) {
_request = request;
- _newMessage('remote', request);
+ _newMessage(Originator.remote, request);
// Reply with a 200 OK if the user didn't reply.
if (!_is_replied) {
@@ -103,7 +104,7 @@ class Message extends EventManager with Applicant {
List extraHeaders = Utils.cloneArray(options['extraHeaders']);
String? body = options['body'];
- if (_direction != 'incoming') {
+ if (_direction != Direction.incoming) {
throw Exceptions.NotSupportedError(
'"accept" not supported for outgoing Message');
}
@@ -126,7 +127,7 @@ class Message extends EventManager with Applicant {
List extraHeaders = Utils.cloneArray(options['extraHeaders']);
String? body = options['body'];
- if (_direction != 'incoming') {
+ if (_direction != Direction.incoming) {
throw Exceptions.NotSupportedError(
'"reject" not supported for outgoing Message');
}
@@ -151,10 +152,11 @@ class Message extends EventManager with Applicant {
// Ignore provisional responses.
} else if (RegExp(r'^2[0-9]{2}$')
.hasMatch(response.status_code.toString())) {
- _succeeded('remote', response);
+ _succeeded(Originator.remote, response);
} else {
String cause = Utils.sipErrorCause(response.status_code);
- _failed('remote', response.status_code, cause, response.reason_phrase);
+ _failed(Originator.remote, response.status_code, cause,
+ response.reason_phrase);
}
}
@@ -162,15 +164,15 @@ class Message extends EventManager with Applicant {
if (_closed) {
return;
}
- _failed(
- 'system', 408, DartSIP_C.CausesType.REQUEST_TIMEOUT, 'Request Timeout');
+ _failed(Originator.system, 408, DartSIP_C.CausesType.REQUEST_TIMEOUT,
+ 'Request Timeout');
}
void _onTransportError() {
if (_closed) {
return;
}
- _failed('system', 500, DartSIP_C.CausesType.CONNECTION_ERROR,
+ _failed(Originator.system, 500, DartSIP_C.CausesType.CONNECTION_ERROR,
'Transport Error');
}
@@ -184,13 +186,13 @@ class Message extends EventManager with Applicant {
* Internal Callbacks
*/
- void _newMessage(String originator, dynamic request) {
- if (originator == 'remote') {
- _direction = 'incoming';
+ void _newMessage(Originator originator, dynamic request) {
+ if (originator == Originator.remote) {
+ _direction = Direction.incoming;
_local_identity = request.to;
_remote_identity = request.from;
- } else if (originator == 'local') {
- _direction = 'outgoing';
+ } else if (originator == Originator.local) {
+ _direction = Direction.outgoing;
_local_identity = request.from;
_remote_identity = request.to;
}
@@ -198,7 +200,7 @@ class Message extends EventManager with Applicant {
_ua.newMessage(this, originator, request);
}
- void _failed(String originator, int? status_code, String cause,
+ void _failed(Originator originator, int? status_code, String cause,
String? reason_phrase) {
logger.d('MESSAGE failed');
close();
@@ -211,7 +213,7 @@ class Message extends EventManager with Applicant {
reason_phrase: reason_phrase)));
}
- void _succeeded(String originator, IncomingResponse? response) {
+ void _succeeded(Originator originator, IncomingResponse? response) {
logger.d('MESSAGE succeeded');
close();
diff --git a/lib/src/name_addr_header.dart b/lib/src/name_addr_header.dart
index a2924071..8e934490 100644
--- a/lib/src/name_addr_header.dart
+++ b/lib/src/name_addr_header.dart
@@ -89,7 +89,7 @@ class NameAddrHeader {
@override
String toString() {
- String body = (_display_name != null && _display_name!.length > 0)
+ String body = (_display_name != null && _display_name!.isNotEmpty)
? '"${_quote(_display_name!)}" '
: '';
diff --git a/lib/src/options.dart b/lib/src/options.dart
index cff29729..e8e1ece6 100644
--- a/lib/src/options.dart
+++ b/lib/src/options.dart
@@ -1,3 +1,4 @@
+import 'package:sip_ua/src/enums.dart';
import 'package:sip_ua/src/name_addr_header.dart';
import 'constants.dart' as DartSIP_C;
import 'constants.dart';
@@ -17,14 +18,14 @@ class Options extends EventManager with Applicant {
final UA _ua;
dynamic _request;
bool _closed = false;
- String? _direction;
+ Direction? _direction;
NameAddrHeader? _local_identity;
NameAddrHeader? _remote_identity;
// Whether an incoming Options has been replied.
bool _is_replied = false;
// Custom Options empty object for high level use.
final Map _data = {};
- String? get direction => _direction;
+ Direction? get direction => _direction;
NameAddrHeader? get local_identity => _local_identity;
@@ -75,7 +76,7 @@ class Options extends EventManager with Applicant {
RequestSender request_sender = RequestSender(_ua, _request, handlers);
- _newOptions('local', _request);
+ _newOptions(Originator.local, _request);
request_sender.send();
}
@@ -83,7 +84,7 @@ class Options extends EventManager with Applicant {
void init_incoming(IncomingRequest request) {
_request = request;
- _newOptions('remote', request);
+ _newOptions(Originator.remote, request);
// Reply with a 200 OK if the user didn't reply.
if (!_is_replied) {
@@ -102,7 +103,7 @@ class Options extends EventManager with Applicant {
List extraHeaders = Utils.cloneArray(options['extraHeaders']);
String? body = options['body'];
- if (_direction != 'incoming') {
+ if (_direction != Direction.incoming) {
throw Exceptions.NotSupportedError(
'"accept" not supported for outgoing Options');
}
@@ -125,7 +126,7 @@ class Options extends EventManager with Applicant {
List extraHeaders = Utils.cloneArray(options['extraHeaders']);
String? body = options['body'];
- if (_direction != 'incoming') {
+ if (_direction != Direction.incoming) {
throw Exceptions.NotSupportedError(
'"reject" not supported for outgoing Options');
}
@@ -149,10 +150,11 @@ class Options extends EventManager with Applicant {
if (RegExp(r'^1[0-9]{2}$').hasMatch(response!.status_code)) {
// Ignore provisional responses.
} else if (RegExp(r'^2[0-9]{2}$').hasMatch(response.status_code)) {
- _succeeded('remote', response);
+ _succeeded(Originator.remote, response);
} else {
String cause = Utils.sipErrorCause(response.status_code);
- _failed('remote', response.status_code, cause, response.reason_phrase);
+ _failed(Originator.remote, response.status_code, cause,
+ response.reason_phrase);
}
}
@@ -160,15 +162,15 @@ class Options extends EventManager with Applicant {
if (_closed != null) {
return;
}
- _failed(
- 'system', 408, DartSIP_C.CausesType.REQUEST_TIMEOUT, 'Request Timeout');
+ _failed(Originator.system, 408, DartSIP_C.CausesType.REQUEST_TIMEOUT,
+ 'Request Timeout');
}
void _onTransportError() {
if (_closed != null) {
return;
}
- _failed('system', 500, DartSIP_C.CausesType.CONNECTION_ERROR,
+ _failed(Originator.system, 500, DartSIP_C.CausesType.CONNECTION_ERROR,
'Transport Error');
}
@@ -182,13 +184,13 @@ class Options extends EventManager with Applicant {
* Internal Callbacks
*/
- void _newOptions(String originator, dynamic request) {
- if (originator == 'remote') {
- _direction = 'incoming';
+ void _newOptions(Originator originator, dynamic request) {
+ if (originator == Originator.remote) {
+ _direction = Direction.incoming;
_local_identity = request.to;
_remote_identity = request.from;
- } else if (originator == 'local') {
- _direction = 'outgoing';
+ } else if (originator == Originator.local) {
+ _direction = Direction.outgoing;
_local_identity = request.from;
_remote_identity = request.to;
}
@@ -196,7 +198,7 @@ class Options extends EventManager with Applicant {
_ua.newOptions(this, originator, request);
}
- void _failed(String originator, int? status_code, String cause,
+ void _failed(Originator originator, int? status_code, String cause,
String? reason_phrase) {
logger.d('OPTIONS failed');
close();
@@ -209,7 +211,7 @@ class Options extends EventManager with Applicant {
reason_phrase: reason_phrase)));
}
- void _succeeded(String originator, IncomingResponse? response) {
+ void _succeeded(Originator originator, IncomingResponse? response) {
logger.d('OPTIONS succeeded');
close();
diff --git a/lib/src/registrator.dart b/lib/src/registrator.dart
index 4de8deb9..96a84b36 100644
--- a/lib/src/registrator.dart
+++ b/lib/src/registrator.dart
@@ -1,6 +1,5 @@
import 'dart:async';
-import 'package:sip_ua/src/socket_transport.dart';
import 'constants.dart' as DartSIP_C;
import 'constants.dart';
import 'event_manager/event_manager.dart';
@@ -10,6 +9,7 @@ import 'logger.dart';
import 'name_addr_header.dart';
import 'request_sender.dart';
import 'sip_message.dart';
+import 'socket_transport.dart';
import 'timers.dart';
import 'ua.dart';
import 'uri.dart';
@@ -206,7 +206,7 @@ class Registrator {
expires ??= _expires;
- expires = num.tryParse(expires) ?? 0;
+ expires = num.tryParse(expires.toString()) ?? 0;
if (expires < MIN_REGISTER_EXPIRES) {
expires = MIN_REGISTER_EXPIRES;
@@ -274,11 +274,11 @@ class Registrator {
request_sender.send();
}
- void unregister(bool unregister_all) {
+ Future unregister(bool unregister_all) async {
if (_registered == false) {
logger.d('already unregistered');
- return;
+ return true;
}
_registered = false;
@@ -312,11 +312,14 @@ class Registrator {
extraHeaders);
EventManager handlers = EventManager();
+ Completer completer = Completer();
handlers.on(EventOnRequestTimeout(), (EventOnRequestTimeout value) {
_unregistered(null, DartSIP_C.CausesType.REQUEST_TIMEOUT);
+ completer.complete(false);
});
handlers.on(EventOnTransportError(), (EventOnTransportError value) {
_unregistered(null, DartSIP_C.CausesType.CONNECTION_ERROR);
+ completer.complete(false);
});
handlers.on(EventOnAuthenticated(), (EventOnAuthenticated response) {
// Increase the CSeq on authentication.
@@ -327,17 +330,20 @@ class Registrator {
String status_code = event.response!.status_code.toString();
if (utils.test2XX(status_code)) {
_unregistered(event.response);
+ completer.complete(true);
} else if (utils.test1XX(status_code)) {
// Ignore provisional responses.
} else {
String cause = utils.sipErrorCause(event.response!.status_code);
_unregistered(event.response, cause);
+ completer.complete(true);
}
});
RequestSender request_sender = RequestSender(_ua, request, handlers);
request_sender.send();
+ return completer.future;
}
void close() {
diff --git a/lib/src/request_sender.dart b/lib/src/request_sender.dart
index 1d2f53cc..42ec8cb8 100644
--- a/lib/src/request_sender.dart
+++ b/lib/src/request_sender.dart
@@ -9,7 +9,6 @@ import 'transactions/ack_client.dart';
import 'transactions/invite_client.dart';
import 'transactions/non_invite_client.dart';
import 'transactions/transaction_base.dart';
-import 'ua.dart' as UAC;
import 'ua.dart';
// Default event handlers.
@@ -25,7 +24,7 @@ class RequestSender {
_staled = false;
// If ua is in closing process or even closed just allow sending Bye and ACK.
- if (ua.status == UAC.C.STATUS_USER_CLOSED &&
+ if (ua.status == UAStatus.userClosed &&
(_method != SipMethod.BYE || _method != SipMethod.ACK)) {
_eventHandlers.emit(EventOnTransportError());
}
diff --git a/lib/src/rtc_session.dart b/lib/src/rtc_session.dart
index 9fc9da52..e8d9872b 100644
--- a/lib/src/rtc_session.dart
+++ b/lib/src/rtc_session.dart
@@ -1,12 +1,10 @@
import 'dart:async';
-import 'dart:convert';
-import 'package:crypto/crypto.dart';
+import 'package:collection/collection.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
-import 'package:sdp_transform/sdp_transform.dart';
-import 'package:sip_ua/sip_ua.dart';
+import '../sip_ua.dart';
import 'constants.dart' as DartSIP_C;
import 'constants.dart';
import 'dialog.dart';
@@ -22,25 +20,22 @@ import 'rtc_session/info.dart' as RTCSession_Info;
import 'rtc_session/info.dart';
import 'rtc_session/refer_notifier.dart';
import 'rtc_session/refer_subscriber.dart';
-import 'sip_message.dart';
import 'timers.dart';
import 'transactions/transaction_base.dart';
import 'ua.dart';
-import 'uri.dart';
import 'utils.dart' as utils;
-class C {
- // RTCSession states.
- static const int STATUS_NULL = 0;
- static const int STATUS_INVITE_SENT = 1;
- static const int STATUS_1XX_RECEIVED = 2;
- static const int STATUS_INVITE_RECEIVED = 3;
- static const int STATUS_WAITING_FOR_ANSWER = 4;
- static const int STATUS_ANSWERED = 5;
- static const int STATUS_WAITING_FOR_ACK = 6;
- static const int STATUS_CANCELED = 7;
- static const int STATUS_TERMINATED = 8;
- static const int STATUS_CONFIRMED = 9;
+enum RtcSessionState {
+ none, // STATUS_NULL
+ inviteSent, // STATUS_INVITE_SENT
+ provisionalResponse, // STATUS_1XX_RECEIVED
+ inviteReceived, // STATUS_INVITE_RECEIVED
+ waitingForAnswer, // STATUS_WAITING_FOR_ANSWER
+ answered, // STATUS_ANSWERED
+ waitingForAck, // STATUS_WAITING_FOR_ACK
+ canceled, // STATUS_CANCELED
+ terminated, // STATUS_TERMINATED
+ confirmed, // STATUS_CONFIRMED
}
/**
@@ -86,7 +81,7 @@ class RTCSession extends EventManager implements Owner {
final UA _ua;
String? _id;
- int _status = C.STATUS_NULL;
+ RtcSessionState _state = RtcSessionState.none;
Dialog? _dialog;
final Map _earlyDialogs = {};
String? _contact;
@@ -98,6 +93,9 @@ class RTCSession extends EventManager implements Owner {
// The RTCPeerConnection instance (public attribute).
RTCPeerConnection? _connection;
+ // RTPSender List
+ final List _senders = [];
+
// Incoming/Outgoing request being currently processed.
dynamic _request;
@@ -122,11 +120,14 @@ class RTCSession extends EventManager implements Owner {
// Flag to indicate PeerConnection ready for actions.
bool _rtcReady = true;
+ Timer? _iceDisconnectTimer;
+ bool _isAttemptingIceRestart = false;
+
// SIP Timers.
final SIPTimers _timers = SIPTimers();
// Session info.
- String? _direction;
+ Direction? _direction;
NameAddrHeader? _local_identity;
NameAddrHeader? _remote_identity;
DateTime? _start_time;
@@ -169,14 +170,16 @@ class RTCSession extends EventManager implements Owner {
RTCPeerConnection? get connection => _connection;
@override
- int get TerminatedCode => C.STATUS_TERMINATED;
+ int get TerminatedCode => RtcSessionState.terminated.index;
- RTCDTMFSender get dtmfSender =>
- _connection!.createDtmfSender(_localMediaStream!.getAudioTracks()[0]);
+ RTCDTMFSender? get dtmfSender => _senders
+ .firstWhereOrNull((RTCRtpSender item) =>
+ item.track != null && item.track!.kind == 'audio')
+ ?.dtmfSender;
String? get contact => _contact;
- String? get direction => _direction;
+ Direction? get direction => _direction;
NameAddrHeader? get local_identity => _local_identity;
@@ -190,15 +193,17 @@ class RTCSession extends EventManager implements Owner {
UA get ua => _ua;
@override
- int get status => _status;
+ int get status => _state.index;
+
+ RtcSessionState get state => _state;
bool isInProgress() {
- switch (_status) {
- case C.STATUS_NULL:
- case C.STATUS_INVITE_SENT:
- case C.STATUS_1XX_RECEIVED:
- case C.STATUS_INVITE_RECEIVED:
- case C.STATUS_WAITING_FOR_ANSWER:
+ switch (_state) {
+ case RtcSessionState.none:
+ case RtcSessionState.inviteSent:
+ case RtcSessionState.provisionalResponse:
+ case RtcSessionState.inviteReceived:
+ case RtcSessionState.waitingForAnswer:
return true;
default:
return false;
@@ -206,10 +211,10 @@ class RTCSession extends EventManager implements Owner {
}
bool isEstablished() {
- switch (_status) {
- case C.STATUS_ANSWERED:
- case C.STATUS_WAITING_FOR_ACK:
- case C.STATUS_CONFIRMED:
+ switch (_state) {
+ case RtcSessionState.answered:
+ case RtcSessionState.waitingForAck:
+ case RtcSessionState.confirmed:
return true;
default:
return false;
@@ -217,9 +222,9 @@ class RTCSession extends EventManager implements Owner {
}
bool isEnded() {
- switch (_status) {
- case C.STATUS_CANCELED:
- case C.STATUS_TERMINATED:
+ switch (_state) {
+ case RtcSessionState.canceled:
+ case RtcSessionState.terminated:
return true;
default:
return false;
@@ -264,8 +269,8 @@ class RTCSession extends EventManager implements Owner {
}
// Check Session Status.
- if (_status != C.STATUS_NULL) {
- throw Exceptions.InvalidStateError(_status);
+ if (_state != RtcSessionState.none) {
+ throw Exceptions.InvalidStateError(_state.name);
}
// Check WebRTC support.
@@ -301,7 +306,9 @@ class RTCSession extends EventManager implements Owner {
// Set anonymous property.
bool anonymous = options['anonymous'] ?? false;
Map requestParams = {
- 'from_tag': _from_tag
+ 'from_tag': _from_tag,
+ 'to_display_name': options['to_display_name'] ?? '',
+ 'call_id': options['call_id'] ?? null,
};
_ua.contact!.anonymous = anonymous;
_ua.contact!.outbound = true;
@@ -314,7 +321,7 @@ class RTCSession extends EventManager implements Owner {
requestParams['from_display_name'] = options['from_display_name'] ?? '';
requestParams['from_uri'] = URI.parse(options['from_uri']);
extraHeaders
- .add('P-Preferred-Identity: ${_ua.configuration.uri.toString()}');
+ .add('P-Preferred-Identity: ${_ua.configuration.uri.toString()}');
}
if (anonymous) {
@@ -340,7 +347,7 @@ class RTCSession extends EventManager implements Owner {
await _createRTCConnection(pcConfig, rtcConstraints);
// Set internal properties.
- _direction = 'outgoing';
+ _direction = Direction.outgoing;
_local_identity = _request.from;
_remote_identity = _request.to;
@@ -349,7 +356,7 @@ class RTCSession extends EventManager implements Owner {
initCallback(this);
}
- _newRTCSession('local', _request);
+ _newRTCSession(Originator.local, _request);
await _sendInitialRequest(
pcConfig, mediaConstraints, rtcOfferConstraints, mediaStream);
}
@@ -368,7 +375,7 @@ class RTCSession extends EventManager implements Owner {
}
// Session parameter initialization.
- _status = C.STATUS_INVITE_RECEIVED;
+ _state = RtcSessionState.inviteReceived;
_from_tag = request.from_tag;
_id = request.call_id! + _from_tag!;
_request = request;
@@ -404,13 +411,13 @@ class RTCSession extends EventManager implements Owner {
_late_sdp = true;
}
- _status = C.STATUS_WAITING_FOR_ANSWER;
+ _state = RtcSessionState.waitingForAnswer;
// Set userNoAnswerTimer.
_timers.userNoAnswerTimer = setTimeout(() {
request.reply(408);
- _failed('local', null, null, null, 408, DartSIP_C.CausesType.NO_ANSWER,
- 'No Answer');
+ _failed(Originator.local, null, null, null, 408,
+ DartSIP_C.CausesType.NO_ANSWER, 'No Answer');
}, _ua.configuration.no_answer_timeout);
/* Set expiresTimer
@@ -418,16 +425,16 @@ class RTCSession extends EventManager implements Owner {
*/
if (expires != null) {
_timers.expiresTimer = setTimeout(() {
- if (_status == C.STATUS_WAITING_FOR_ANSWER) {
+ if (_state == RtcSessionState.waitingForAnswer) {
request.reply(487);
- _failed('system', null, null, null, 487, DartSIP_C.CausesType.EXPIRES,
- 'Timeout');
+ _failed(Originator.system, null, null, null, 487,
+ DartSIP_C.CausesType.EXPIRES, 'Timeout');
}
}, expires);
}
// Set internal properties.
- _direction = 'incoming';
+ _direction = Direction.incoming;
_local_identity = request.to;
_remote_identity = request.from;
@@ -437,10 +444,10 @@ class RTCSession extends EventManager implements Owner {
}
// Fire 'newRTCSession' event.
- _newRTCSession('remote', request);
+ _newRTCSession(Originator.remote, request);
// The user may have rejected the call in the 'newRTCSession' event.
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
@@ -449,7 +456,7 @@ class RTCSession extends EventManager implements Owner {
// Fire 'progress' event.
// TODO(cloudwebrtc): Document that 'response' field in 'progress' event is null for incoming calls.
- _progress('local', null);
+ _progress(Originator.local, null);
}
/**
@@ -487,14 +494,14 @@ class RTCSession extends EventManager implements Owner {
data = options['data'] ?? data;
// Check Session Direction and Status.
- if (_direction != 'incoming') {
+ if (_direction != Direction.incoming) {
throw Exceptions.NotSupportedError(
'"answer" not supported for outgoing RTCSession');
}
// Check Session status.
- if (_status != C.STATUS_WAITING_FOR_ANSWER) {
- throw Exceptions.InvalidStateError(_status);
+ if (_state != RtcSessionState.waitingForAnswer) {
+ throw Exceptions.InvalidStateError(_state.name);
}
// Session Timers.
@@ -508,7 +515,7 @@ class RTCSession extends EventManager implements Owner {
}
}
- _status = C.STATUS_ANSWERED;
+ _state = RtcSessionState.answered;
// An error on dialog creation will fire 'failed' event.
if (!_createDialog(request, 'UAS')) {
@@ -588,7 +595,8 @@ class RTCSession extends EventManager implements Owner {
// A local MediaStream is given, use it.
if (mediaStream != null) {
stream = mediaStream;
- emit(EventStream(session: this, originator: 'local', stream: stream));
+ emit(EventStream(
+ session: this, originator: Originator.local, stream: stream));
}
// Audio and/or video requested, prompt getUserMedia.
else if (mediaConstraints['audio'] != null ||
@@ -596,14 +604,15 @@ class RTCSession extends EventManager implements Owner {
_localMediaStreamLocallyGenerated = true;
try {
stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
- emit(EventStream(session: this, originator: 'local', stream: stream));
+ emit(EventStream(
+ session: this, originator: Originator.local, stream: stream));
} catch (error) {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
request.reply(480);
_failed(
- 'local',
+ Originator.local,
null,
null,
null,
@@ -616,7 +625,7 @@ class RTCSession extends EventManager implements Owner {
}
}
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
@@ -626,8 +635,9 @@ class RTCSession extends EventManager implements Owner {
if (stream != null) {
switch (sdpSemantics) {
case 'unified-plan':
- stream.getTracks().forEach((MediaStreamTrack track) {
- _connection!.addTrack(track, stream!);
+ stream.getTracks().forEach((MediaStreamTrack track) async {
+ RTCRtpSender sender = await _connection!.addTrack(track, stream!);
+ _senders.add(sender);
});
break;
case 'plan-b':
@@ -644,15 +654,17 @@ class RTCSession extends EventManager implements Owner {
logger.d('emit "sdp"');
final String? processedSDP = _sdpOfferToWebRTC(request.body);
- emit(EventSdp(originator: 'remote', type: 'offer', sdp: processedSDP));
+ emit(EventSdp(
+ originator: Originator.remote, type: SdpType.offer, sdp: processedSDP));
- RTCSessionDescription offer = RTCSessionDescription(processedSDP, 'offer');
+ RTCSessionDescription offer =
+ RTCSessionDescription(processedSDP, SdpType.offer.name);
try {
await _connection!.setRemoteDescription(offer);
} catch (error) {
request.reply(488);
_failed(
- 'system',
+ Originator.system,
null,
null,
null,
@@ -667,7 +679,7 @@ class RTCSession extends EventManager implements Owner {
}
// Create local description.
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
@@ -676,16 +688,18 @@ class RTCSession extends EventManager implements Owner {
RTCSessionDescription desc;
try {
if (!_late_sdp) {
- desc = await _createLocalDescription('answer', rtcAnswerConstraints);
+ desc =
+ await _createLocalDescription(SdpType.answer, rtcAnswerConstraints);
} else {
- desc = await _createLocalDescription('offer', _rtcOfferConstraints);
+ desc =
+ await _createLocalDescription(SdpType.offer, _rtcOfferConstraints);
}
} catch (e) {
request.reply(500);
throw Exceptions.TypeError('_createLocalDescription() failed');
}
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
@@ -693,16 +707,16 @@ class RTCSession extends EventManager implements Owner {
try {
_handleSessionTimersInIncomingRequest(request, extraHeaders);
request.reply(200, null, extraHeaders, desc.sdp, () {
- _status = C.STATUS_WAITING_FOR_ACK;
+ _state = RtcSessionState.waitingForAck;
_setInvite2xxTimer(request, desc.sdp);
_setACKTimer();
- _accepted('local');
+ _accepted(Originator.local);
}, () {
- _failed('system', null, null, null, 500,
+ _failed(Originator.system, null, null, null, 500,
DartSIP_C.CausesType.CONNECTION_ERROR, 'Transport Error');
});
} catch (error, s) {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
logger.e('Failed to answer(): ${error.toString()}',
@@ -713,7 +727,7 @@ class RTCSession extends EventManager implements Owner {
/**
* Terminate the call.
*/
- void terminate([Map? options]) {
+ void terminate([Map? options]) async {
logger.d('terminate()');
options = options ?? {};
@@ -730,15 +744,15 @@ class RTCSession extends EventManager implements Owner {
String? reason_phrase = options['reason_phrase'] as String?;
// Check Session Status.
- if (_status == C.STATUS_TERMINATED) {
- throw Exceptions.InvalidStateError(_status);
+ if (_state == RtcSessionState.terminated) {
+ throw Exceptions.InvalidStateError(_state.name);
}
- switch (_status) {
+ switch (_state) {
// - UAC -
- case C.STATUS_NULL:
- case C.STATUS_INVITE_SENT:
- case C.STATUS_1XX_RECEIVED:
+ case RtcSessionState.none:
+ case RtcSessionState.inviteSent:
+ case RtcSessionState.provisionalResponse:
logger.d('canceling session');
if (status_code != null && (status_code < 200 || status_code >= 700)) {
@@ -749,23 +763,24 @@ class RTCSession extends EventManager implements Owner {
}
// Check Session Status.
- if (_status == C.STATUS_NULL || _status == C.STATUS_INVITE_SENT) {
+ if (_state == RtcSessionState.none ||
+ _state == RtcSessionState.inviteSent) {
_is_canceled = true;
_cancel_reason = cancel_reason;
- } else if (_status == C.STATUS_1XX_RECEIVED) {
+ } else if (_state == RtcSessionState.provisionalResponse) {
_request.cancel(cancel_reason ?? '');
}
- _status = C.STATUS_CANCELED;
+ _state = RtcSessionState.canceled;
cancel_reason = cancel_reason ?? 'Canceled by local';
status_code = status_code ?? 100;
- _failed('local', null, null, null, status_code,
+ _failed(Originator.local, null, null, null, status_code,
DartSIP_C.CausesType.CANCELED, cancel_reason);
- break;
+ break;
// - UAS -
- case C.STATUS_WAITING_FOR_ANSWER:
- case C.STATUS_ANSWERED:
+ case RtcSessionState.waitingForAnswer:
+ case RtcSessionState.answered:
logger.d('rejecting session');
status_code = status_code ?? 480;
@@ -776,12 +791,11 @@ class RTCSession extends EventManager implements Owner {
}
_request.reply(status_code, reason_phrase, extraHeaders, body);
- _failed('local', null, null, null, status_code,
+ _failed(Originator.local, null, null, null, status_code,
DartSIP_C.CausesType.REJECTED, reason_phrase);
break;
-
- case C.STATUS_WAITING_FOR_ACK:
- case C.STATUS_CONFIRMED:
+ case RtcSessionState.waitingForAck:
+ case RtcSessionState.confirmed:
logger.d('terminating session');
reason_phrase = options['reason_phrase'] as String? ??
@@ -801,8 +815,8 @@ class RTCSession extends EventManager implements Owner {
* until it has received an ACK for its 2xx response or until the server
* transaction times out."
*/
- if (_status == C.STATUS_WAITING_FOR_ACK &&
- _direction == 'incoming' &&
+ if (_state == RtcSessionState.waitingForAck &&
+ _direction == Direction.incoming &&
_request.server_transaction.state != TransactionState.TERMINATED) {
/// Save the dialog for later restoration.
Dialog dialog = _dialog!;
@@ -810,10 +824,10 @@ class RTCSession extends EventManager implements Owner {
// Send the BYE as soon as the ACK is received...
receiveRequest = (IncomingMessage request) {
if (request.method == SipMethod.ACK) {
- sendRequest(SipMethod.BYE, {
- 'extraHeaders': extraHeaders,
- 'body': body
- });
+ sendRequest(
+ SipMethod.BYE,
+ {'extraHeaders': extraHeaders, 'body': body},
+ );
dialog.terminate();
}
};
@@ -831,13 +845,17 @@ class RTCSession extends EventManager implements Owner {
}
});
+ //write call statistics to the log
+ await _logCallStat();
+
_ended(
- 'local',
+ Originator.local,
null,
ErrorCause(
- cause: cause as String?,
- status_code: status_code,
- reason_phrase: reason_phrase));
+ cause: cause as String?,
+ status_code: status_code,
+ reason_phrase: reason_phrase,
+ ));
// Restore the dialog into 'this' in order to be able to send the in-dialog BYE :-).
_dialog = dialog;
@@ -849,14 +867,21 @@ class RTCSession extends EventManager implements Owner {
{'extraHeaders': extraHeaders, 'body': body});
reason_phrase = reason_phrase ?? 'Terminated by local';
status_code = status_code ?? 200;
+
+ //write call statistics to the log
+ await _logCallStat();
+
_ended(
- 'local',
+ Originator.local,
null,
ErrorCause(
cause: cause as String?,
status_code: status_code,
reason_phrase: reason_phrase));
}
+ break;
+ default:
+ break;
}
}
@@ -879,8 +904,9 @@ class RTCSession extends EventManager implements Owner {
}
// Check Session Status.
- if (_status != C.STATUS_CONFIRMED && _status != C.STATUS_WAITING_FOR_ACK) {
- throw Exceptions.InvalidStateError(_status);
+ if (_state != RtcSessionState.confirmed &&
+ _state != RtcSessionState.waitingForAck) {
+ throw Exceptions.InvalidStateError(_state);
}
// Convert to string.
@@ -933,7 +959,7 @@ class RTCSession extends EventManager implements Owner {
if (tone == ',') {
// queue the delay
dtmfFuture = dtmfFuture.then((_) async {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
await Future.delayed(Duration(milliseconds: 2000), () {});
@@ -941,7 +967,7 @@ class RTCSession extends EventManager implements Owner {
} else {
// queue playing the tone
dtmfFuture = dtmfFuture.then((_) async {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
@@ -966,8 +992,9 @@ class RTCSession extends EventManager implements Owner {
logger.d('sendInfo()');
// Check Session Status.
- if (_status != C.STATUS_CONFIRMED && _status != C.STATUS_WAITING_FOR_ACK) {
- throw Exceptions.InvalidStateError(_status);
+ if (_state != RtcSessionState.confirmed &&
+ _state != RtcSessionState.waitingForAck) {
+ throw Exceptions.InvalidStateError(_state);
}
RTCSession_Info.Info info = RTCSession_Info.Info(this);
@@ -985,13 +1012,10 @@ class RTCSession extends EventManager implements Owner {
if (!_audioMuted && audio) {
_audioMuted = true;
changed = true;
- _toggleMuteAudio(true);
}
-
if (!_videoMuted && video) {
_videoMuted = true;
changed = true;
- _toggleMuteVideo(true);
}
if (changed) {
@@ -1032,12 +1056,13 @@ class RTCSession extends EventManager implements Owner {
/**
* Hold
*/
- bool hold([Map? options, Function? done]) {
+ bool hold([Map? options, Function(IncomingMessage?)? done]) {
logger.d('hold()');
options = options ?? {};
- if (_status != C.STATUS_WAITING_FOR_ACK && _status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.waitingForAck &&
+ _state != RtcSessionState.confirmed) {
return false;
}
@@ -1050,13 +1075,13 @@ class RTCSession extends EventManager implements Owner {
}
_localHold = true;
- _onhold('local');
+ _onhold(Originator.local);
EventManager handlers = EventManager();
handlers.on(EventSucceeded(), (EventSucceeded event) {
if (done != null) {
- done();
+ done(event.response);
}
});
handlers.on(EventCallFailed(), (EventCallFailed event) {
@@ -1083,12 +1108,14 @@ class RTCSession extends EventManager implements Owner {
return true;
}
- bool unhold([Map? options, Function? done]) {
+ bool unhold(
+ [Map? options, Function(IncomingMessage?)? done]) {
logger.d('unhold()');
options = options ?? {};
- if (_status != C.STATUS_WAITING_FOR_ACK && _status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.waitingForAck &&
+ _state != RtcSessionState.confirmed) {
return false;
}
@@ -1101,12 +1128,12 @@ class RTCSession extends EventManager implements Owner {
}
_localHold = false;
- _onunhold('local');
+ _onunhold(Originator.local);
EventManager handlers = EventManager();
handlers.on(EventSucceeded(), (EventSucceeded event) {
if (done != null) {
- done();
+ done(event.response);
}
});
handlers.on(EventCallFailed(), (EventCallFailed event) {
@@ -1135,8 +1162,8 @@ class RTCSession extends EventManager implements Owner {
bool renegotiate(
{Map? options,
- Function(IncomingMessage)? done,
- bool useUpdate = false}) {
+ bool useUpdate = false,
+ Function(IncomingMessage?)? done}) {
logger.d('renegotiate()');
options = options ?? {};
@@ -1151,10 +1178,20 @@ class RTCSession extends EventManager implements Owner {
dynamic sdpSemantics =
options['pcConfig']?['sdpSemantics'] ?? 'unified-plan';
- if (_status != C.STATUS_WAITING_FOR_ACK && _status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.waitingForAck &&
+ _state != RtcSessionState.confirmed) {
return false;
}
+ bool? upgradeToVideo;
+ try {
+ upgradeToVideo = (options['mediaConstraints']?['video'] != false ||
+ options['mediaConstraints']?['mandatory']?['video'] != null) &&
+ rtcOfferConstraints?['offerToReceiveVideo'] == null;
+ } catch (e) {
+ logger.w('Failed to determine upgrade to video: $e');
+ }
+
if (!_isReadyToReOffer()) {
return false;
}
@@ -1172,10 +1209,9 @@ class RTCSession extends EventManager implements Owner {
'reason_phrase': 'Media Renegotiation Failed'
});
});
-
_setLocalMediaStatus();
- if (useUpdate) {
+ if (options['useUpdate'] != null) {
_sendUpdate({
'sdpOffer': true,
'eventHandlers': handlers,
@@ -1183,13 +1219,21 @@ class RTCSession extends EventManager implements Owner {
'extraHeaders': options['extraHeaders']
});
} else {
- _sendReinvite({
- 'eventHandlers': handlers,
- 'sdpSemantics': sdpSemantics,
- 'rtcOfferConstraints': rtcOfferConstraints,
- 'mediaConstraints': mediaConstraints,
- 'extraHeaders': options['extraHeaders']
- });
+ if (upgradeToVideo ?? false) {
+ _sendVideoUpgradeReinvite({
+ 'eventHandlers': handlers,
+ 'sdpSemantics': sdpSemantics,
+ 'rtcOfferConstraints': rtcOfferConstraints,
+ 'mediaConstraints': mediaConstraints,
+ 'extraHeaders': options['extraHeaders']
+ });
+ } else {
+ _sendReinvite({
+ 'eventHandlers': handlers,
+ 'rtcOfferConstraints': rtcOfferConstraints,
+ 'extraHeaders': options['extraHeaders']
+ });
+ }
}
return true;
@@ -1205,7 +1249,8 @@ class RTCSession extends EventManager implements Owner {
dynamic originalTarget = target;
- if (_status != C.STATUS_WAITING_FOR_ACK && _status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.waitingForAck &&
+ _state != RtcSessionState.confirmed) {
return null;
}
@@ -1267,22 +1312,22 @@ class RTCSession extends EventManager implements Owner {
* Terminate the whole session in case the user didn't accept (or yet send the answer)
* nor reject the request opening the session.
*/
- if (_status == C.STATUS_WAITING_FOR_ANSWER ||
- _status == C.STATUS_ANSWERED) {
- _status = C.STATUS_CANCELED;
+ if (_state == RtcSessionState.waitingForAnswer ||
+ _state == RtcSessionState.answered) {
+ _state = RtcSessionState.canceled;
_request.reply(487);
- _failed('remote', null, request, null, 487,
+ _failed(Originator.remote, null, request, null, 487,
DartSIP_C.CausesType.CANCELED, request.reason_phrase);
}
} else {
// Requests arriving here are in-dialog requests.
switch (request.method) {
case SipMethod.ACK:
- if (_status != C.STATUS_WAITING_FOR_ACK) {
+ if (_state != RtcSessionState.waitingForAck) {
return;
}
// Update signaling status.
- _status = C.STATUS_CONFIRMED;
+ _state = RtcSessionState.confirmed;
clearTimeout(_timers.ackTimer);
clearTimeout(_timers.invite2xxTimer);
@@ -1297,10 +1342,12 @@ class RTCSession extends EventManager implements Owner {
logger.d('emit "sdp"');
emit(EventSdp(
- originator: 'remote', type: 'answer', sdp: request.body));
+ originator: Originator.remote,
+ type: SdpType.answer,
+ sdp: request.body));
RTCSessionDescription answer =
- RTCSessionDescription(request.body, 'answer');
+ RTCSessionDescription(request.body, SdpType.answer.name);
try {
await _connection!.setRemoteDescription(answer);
} catch (error) {
@@ -1314,24 +1361,32 @@ class RTCSession extends EventManager implements Owner {
}
}
if (!_is_confirmed) {
- _confirmed('remote', request);
+ _confirmed(Originator.remote, request);
}
break;
case SipMethod.BYE:
- if (_status == C.STATUS_CONFIRMED) {
+ if (_state == RtcSessionState.confirmed) {
request.reply(200);
+
+ //write call statistics to the log
+ await _logCallStat();
+
_ended(
- 'remote',
+ Originator.remote,
request,
ErrorCause(
cause: DartSIP_C.CausesType.BYE,
status_code: 200,
reason_phrase: 'BYE Received'));
- } else if (_status == C.STATUS_INVITE_RECEIVED) {
+ } else if (_state == RtcSessionState.inviteReceived) {
request.reply(200);
_request.reply(487, 'BYE Received');
+
+ //write call statistics to the log
+ await _logCallStat();
+
_ended(
- 'remote',
+ Originator.remote,
request,
ErrorCause(
cause: DartSIP_C.CausesType.BYE,
@@ -1342,7 +1397,7 @@ class RTCSession extends EventManager implements Owner {
}
break;
case SipMethod.INVITE:
- if (_status == C.STATUS_CONFIRMED) {
+ if (_state == RtcSessionState.confirmed) {
if (request.hasHeader('replaces')) {
_receiveReplaces(request);
} else {
@@ -1353,11 +1408,11 @@ class RTCSession extends EventManager implements Owner {
}
break;
case SipMethod.INFO:
- if (_status == C.STATUS_1XX_RECEIVED ||
- _status == C.STATUS_WAITING_FOR_ANSWER ||
- _status == C.STATUS_ANSWERED ||
- _status == C.STATUS_WAITING_FOR_ACK ||
- _status == C.STATUS_CONFIRMED) {
+ if (_state == RtcSessionState.provisionalResponse ||
+ _state == RtcSessionState.waitingForAnswer ||
+ _state == RtcSessionState.answered ||
+ _state == RtcSessionState.waitingForAck ||
+ _state == RtcSessionState.confirmed) {
String? contentType = request.getHeader('content-type');
if (contentType != null &&
contentType.contains(RegExp(r'^application\/dtmf-relay',
@@ -1373,21 +1428,21 @@ class RTCSession extends EventManager implements Owner {
}
break;
case SipMethod.UPDATE:
- if (_status == C.STATUS_CONFIRMED) {
+ if (_state == RtcSessionState.confirmed) {
_receiveUpdate(request);
} else {
request.reply(403, 'Wrong Status');
}
break;
case SipMethod.REFER:
- if (_status == C.STATUS_CONFIRMED) {
+ if (_state == RtcSessionState.confirmed) {
_receiveRefer(request);
} else {
request.reply(403, 'Wrong Status');
}
break;
case SipMethod.NOTIFY:
- if (_status == C.STATUS_CONFIRMED) {
+ if (_state == RtcSessionState.confirmed) {
_receiveNotify(request);
} else {
request.reply(403, 'Wrong Status');
@@ -1404,7 +1459,7 @@ class RTCSession extends EventManager implements Owner {
*/
void onTransportError() {
logger.e('onTransportError()');
- if (_status != C.STATUS_TERMINATED) {
+ if (_state != RtcSessionState.terminated) {
terminate({
'status_code': 500,
'reason_phrase': DartSIP_C.CausesType.CONNECTION_ERROR,
@@ -1416,7 +1471,7 @@ class RTCSession extends EventManager implements Owner {
void onRequestTimeout() {
logger.e('onRequestTimeout()');
- if (_status != C.STATUS_TERMINATED) {
+ if (_state != RtcSessionState.terminated) {
terminate({
'status_code': 408,
'reason_phrase': DartSIP_C.CausesType.REQUEST_TIMEOUT,
@@ -1428,7 +1483,7 @@ class RTCSession extends EventManager implements Owner {
void onDialogError() {
logger.e('onDialogError()');
- if (_status != C.STATUS_TERMINATED) {
+ if (_state != RtcSessionState.terminated) {
terminate({
'status_code': 500,
'reason_phrase': DartSIP_C.CausesType.DIALOG_ERROR,
@@ -1438,14 +1493,14 @@ class RTCSession extends EventManager implements Owner {
}
// Called from DTMF handler.
- void newDTMF(String originator, DTMF dtmf, dynamic request) {
+ void newDTMF(Originator originator, DTMF dtmf, dynamic request) {
logger.d('newDTMF()');
emit(EventNewDTMF(originator: originator, dtmf: dtmf, request: request));
}
// Called from Info handler.
- void newInfo(String originator, Info info, dynamic request) {
+ void newInfo(Originator originator, Info info, dynamic request) {
logger.d('newInfo()');
emit(EventNewInfo(originator: originator, info: info, request: request));
@@ -1482,10 +1537,10 @@ class RTCSession extends EventManager implements Owner {
void _close() async {
logger.d('close()');
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
- _status = C.STATUS_TERMINATED;
+ _state = RtcSessionState.terminated;
// Terminate RTC.
if (_connection != null) {
try {
@@ -1512,6 +1567,8 @@ class RTCSession extends EventManager implements Owner {
clearTimeout(_timers.invite2xxTimer);
clearTimeout(_timers.userNoAnswerTimer);
+ _iceDisconnectTimer?.cancel();
+
// Clear Session Timers.
clearTimeout(_sessionTimers.timer);
@@ -1546,7 +1603,7 @@ class RTCSession extends EventManager implements Owner {
int timeout = Timers.T1;
void invite2xxRetransmission() {
- if (_status != C.STATUS_WAITING_FOR_ACK) {
+ if (_state != RtcSessionState.waitingForAck) {
return;
}
request.reply(200, null, ['Contact: $_contact'], body);
@@ -1569,13 +1626,13 @@ class RTCSession extends EventManager implements Owner {
*/
void _setACKTimer() {
_timers.ackTimer = setTimeout(() {
- if (_status == C.STATUS_WAITING_FOR_ACK) {
+ if (_state == RtcSessionState.waitingForAck) {
logger.d('no ACK received, terminating the session');
clearTimeout(_timers.invite2xxTimer);
sendRequest(SipMethod.BYE);
_ended(
- 'remote',
+ Originator.remote,
null,
ErrorCause(
cause: DartSIP_C.CausesType.NO_ACK,
@@ -1592,19 +1649,6 @@ class RTCSession extends EventManager implements Owner {
'optional': [],
};
offerConstraints['mandatory']['IceRestart'] = true;
-
- EventManager handlers = EventManager();
- handlers.on(EventSucceeded(), (EventSucceeded event) async {
- logger.d('ICE Restart was successful');
- });
- handlers.on(EventCallFailed(), (EventCallFailed event) {
- terminate({
- 'cause': DartSIP_C.CausesType.WEBRTC_ERROR,
- 'status_code': 500,
- 'reason_phrase': 'Media Renegotiation Failed'
- });
- });
- offerConstraints['eventHandlers'] = handlers;
renegotiate(options: offerConstraints);
}
@@ -1612,19 +1656,79 @@ class RTCSession extends EventManager implements Owner {
Map rtcConstraints) async {
_connection = await createPeerConnection(pcConfig, rtcConstraints);
_connection!.onIceConnectionState = (RTCIceConnectionState state) {
- // TODO(cloudwebrtc): Do more with different states.
+ if (_state == RtcSessionState.terminated ||
+ _state == RtcSessionState.canceled) {
+ logger.d(
+ 'ICE State change ignored, SIP session already terminated/canceled.');
+ _iceDisconnectTimer?.cancel();
+ return;
+ }
+
if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) {
+ logger.e('ICE Connection State Failed.');
+ _iceDisconnectTimer?.cancel();
terminate({
'cause': DartSIP_C.CausesType.RTP_TIMEOUT,
'status_code': 408,
- 'reason_phrase': DartSIP_C.CausesType.RTP_TIMEOUT
+ 'reason_phrase': 'ICE Connection Failed'
});
} else if (state ==
RTCIceConnectionState.RTCIceConnectionStateDisconnected) {
- _iceRestart();
+ logger.w('ICE Connection State Disconnected.');
+ if (_iceDisconnectTimer == null && !_isAttemptingIceRestart) {
+ logger.i('Starting ICE disconnect timer...');
+ _iceDisconnectTimer = Timer(const Duration(seconds: 20), () {
+ logger.w('ICE disconnect timer fired!');
+ if (_connection?.iceConnectionState ==
+ RTCIceConnectionState.RTCIceConnectionStateDisconnected &&
+ _state != RtcSessionState.terminated &&
+ _state != RtcSessionState.canceled &&
+ !_isAttemptingIceRestart) {
+ logger.i('Attempting ICE restart after timeout...');
+ _isAttemptingIceRestart = true;
+ _iceRestart();
+ } else {
+ logger.i('ICE restart aborted (state changed during timer).');
+ }
+ _iceDisconnectTimer = null;
+ });
+ } else {
+ logger.d(
+ 'ICE disconnect timer not started (already running or attempting restart).');
+ }
+ } else if (state ==
+ RTCIceConnectionState.RTCIceConnectionStateConnected ||
+ state == RTCIceConnectionState.RTCIceConnectionStateCompleted) {
+ // If connection recovers, cancel timer and reset flag
+ if (_iceDisconnectTimer != null || _isAttemptingIceRestart) {
+ logger.i(
+ 'ICE Connection State Connected/Completed. Canceling timer/resetting flag.');
+ _iceDisconnectTimer?.cancel();
+ _isAttemptingIceRestart = false;
+ } else {
+ logger.i('ICE Connection State Connected/Completed.');
+ }
+ } else if (state == RTCIceConnectionState.RTCIceConnectionStateClosed) {
+ // Connection closed locally, usually via _connection.close() called by terminate()
+ logger.i('ICE Connection State Closed.'); // Use logger.i
+ _iceDisconnectTimer?.cancel(); // Ensure timer is cancelled
+ // Ensure *SIP* session state reflects closure if not already set by terminate()
+ if (_state != RtcSessionState.terminated &&
+ _state != RtcSessionState.canceled) {
+ logger.w(
+ 'ICE closed but SIP session state was not terminal. Terminating SIP session now.');
+ terminate({
+ 'cause': DartSIP_C.CausesType.WEBRTC_ERROR,
+ 'status_code': 487,
+ 'reason_phrase': 'ICE Connection Closed'
+ });
+ }
+ } else if (state == RTCIceConnectionState.RTCIceConnectionStateChecking) {
+ logger.d('ICE Connection State Checking...'); // Use logger.d
+ } else if (state == RTCIceConnectionState.RTCIceConnectionStateNew) {
+ logger.d('ICE Connection State New.'); // Use logger.d
}
};
-
// In future versions, unified-plan will be used by default
String? sdpSemantics = 'unified-plan';
if (pcConfig['sdpSemantics'] != null) {
@@ -1636,14 +1740,16 @@ class RTCSession extends EventManager implements Owner {
_connection!.onTrack = (RTCTrackEvent event) {
if (event.streams.isNotEmpty) {
emit(EventStream(
- session: this, originator: 'remote', stream: event.streams[0]));
+ session: this,
+ originator: Originator.remote,
+ stream: event.streams[0]));
}
};
break;
case 'plan-b':
_connection!.onAddStream = (MediaStream stream) {
- emit(
- EventStream(session: this, originator: 'remote', stream: stream));
+ emit(EventStream(
+ session: this, originator: Originator.remote, stream: stream));
};
break;
}
@@ -1654,7 +1760,7 @@ class RTCSession extends EventManager implements Owner {
}
Future _createLocalDescription(
- String type, Map? constraints) async {
+ SdpType type, Map? constraints) async {
logger.d('createLocalDescription()');
_iceGatheringState ??= RTCIceGatheringState.RTCIceGatheringStateNew;
Completer completer =
@@ -1670,16 +1776,16 @@ class RTCSession extends EventManager implements Owner {
modifiers = constraints['offerModifiers'] ??
Function(RTCSessionDescription)>[];
- constraints.remove('offerModifiers');
+ constraints['offerModifiers'] = null;
- if (type != 'offer' && type != 'answer') {
+ if (type != SdpType.offer && type != SdpType.answer) {
completer.completeError(Exceptions.TypeError(
'createLocalDescription() | invalid type "$type"'));
}
_rtcReady = false;
late RTCSessionDescription desc;
- if (type == 'offer') {
+ if (type == SdpType.offer) {
try {
desc = await _connection!.createOffer(constraints);
} catch (error) {
@@ -1708,7 +1814,7 @@ class RTCSession extends EventManager implements Owner {
}
Future ready() async {
- if (!finished && _status != C.STATUS_TERMINATED) {
+ if (!finished && _state != RtcSessionState.terminated) {
finished = true;
_connection!.onIceCandidate = null;
_connection!.onIceGatheringState = null;
@@ -1716,7 +1822,8 @@ class RTCSession extends EventManager implements Owner {
_rtcReady = true;
RTCSessionDescription? desc = await _connection!.getLocalDescription();
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'local', type: type, sdp: desc!.sdp));
+ emit(
+ EventSdp(originator: Originator.local, type: type, sdp: desc!.sdp));
completer.complete(desc);
}
}
@@ -1763,7 +1870,7 @@ class RTCSession extends EventManager implements Owner {
_rtcReady = true;
RTCSessionDescription? desc = await _connection!.getLocalDescription();
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'local', type: type, sdp: desc!.sdp));
+ emit(EventSdp(originator: Originator.local, type: type, sdp: desc!.sdp));
return desc;
}
@@ -1789,7 +1896,7 @@ class RTCSession extends EventManager implements Owner {
} catch (error) {
logger.d('$error');
_failed(
- 'remote',
+ Originator.remote,
message,
null,
null,
@@ -1822,7 +1929,7 @@ class RTCSession extends EventManager implements Owner {
} catch (error) {
logger.d(error.toString());
_failed(
- 'remote',
+ Originator.remote,
message,
null,
null,
@@ -1849,7 +1956,7 @@ class RTCSession extends EventManager implements Owner {
}
request.reply(200, null, extraHeaders, sdp, () {
- _status = C.STATUS_WAITING_FOR_ACK;
+ _state = RtcSessionState.waitingForAck;
_setInvite2xxTimer(request, sdp);
_setACKTimer();
});
@@ -1868,7 +1975,7 @@ class RTCSession extends EventManager implements Owner {
try {
RTCSessionDescription desc =
- await _createLocalDescription('offer', _rtcOfferConstraints);
+ await _createLocalDescription(SdpType.offer, _rtcOfferConstraints);
sendAnswer(desc.sdp);
} catch (_) {
request.reply(500);
@@ -1888,14 +1995,15 @@ class RTCSession extends EventManager implements Owner {
String? reason_phrase = options['reason_phrase'];
List extraHeaders = utils.cloneArray(options['extraHeaders']);
- if (_status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.confirmed) {
return false;
}
if (status_code < 300 || status_code >= 700) {
throw Exceptions.TypeError('Invalid status_code: $status_code');
}
- print('Rejecting with status code: $status_code, reason: $reason_phrase');
+ logger.i(
+ 'Rejecting with status code: $status_code, reason: $reason_phrase');
request.reply(status_code, reason_phrase, extraHeaders);
return true;
}
@@ -1905,7 +2013,7 @@ class RTCSession extends EventManager implements Owner {
Future acceptReInvite(dynamic options) async {
try {
// Send answer.
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return false;
}
sendAnswer(desc.sdp);
@@ -1953,7 +2061,7 @@ class RTCSession extends EventManager implements Owner {
String reason_phrase = options['reason_phrase'] ?? '';
List extraHeaders = utils.cloneArray(options['extraHeaders']);
- if (_status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.confirmed) {
return false;
}
@@ -1995,7 +2103,7 @@ class RTCSession extends EventManager implements Owner {
try {
RTCSessionDescription desc = await _processInDialogSdpOffer(request);
- if (_status == C.STATUS_TERMINATED) return;
+ if (_state == RtcSessionState.terminated) return;
// Send answer.
sendAnswer(desc.sdp);
} catch (error) {
@@ -2013,17 +2121,16 @@ class RTCSession extends EventManager implements Owner {
bool upgradeToVideo = false;
if (sdp != null) {
List mediaList = sdp['media'];
- for (Map m in mediaList) {
- if (holdMediaTypes.indexOf(m['type']) == -1) {
- continue;
- }
-
- if (m['type'] == 'video') {
- upgradeToVideo = true;
- }
-
- String direction = m['direction'] ?? sdp['direction'] ?? 'sendrecv';
+ // Loop media list items for video upgrade
+ for (Map media in mediaList) {
+ if (holdMediaTypes.indexOf(media['type']) == -1) continue;
+ if (media['type'] == 'video') upgradeToVideo = true;
+ }
+ // Loop media list items for hold
+ for (Map media in mediaList) {
+ if (holdMediaTypes.indexOf(media['type']) == -1) continue;
+ String direction = media['direction'] ?? sdp['direction'] ?? 'sendrecv';
if (direction == 'sendonly' || direction == 'inactive') {
hold = true;
}
@@ -2046,28 +2153,48 @@ class RTCSession extends EventManager implements Owner {
'facingMode': 'user',
}
};
- MediaStream localStream =
- await navigator.mediaDevices.getUserMedia(mediaConstraints);
- if (localStream.getVideoTracks().isEmpty) {
- logger.w(
- 'Remote wants to upgrade to video but failed to get local video');
- }
- for (MediaStreamTrack track in localStream.getTracks()) {
- if (track.kind == 'video') {
- _connection!.addTrack(track, localStream);
+ bool hasCamera = false;
+ try {
+ List devices =
+ await navigator.mediaDevices.enumerateDevices();
+ for (MediaDeviceInfo device in devices) {
+ if (device.kind == 'videoinput') hasCamera = true;
+ }
+ } catch (e) {
+ logger.w('Failed to enumerate devices: $e');
+ }
+ if (hasCamera) {
+ MediaStream localStream =
+ await navigator.mediaDevices.getUserMedia(mediaConstraints);
+ if (localStream.getVideoTracks().isEmpty) {
+ logger.w(
+ 'Remote wants to upgrade to video but failed to get local video');
+ }
+ for (MediaStreamTrack track in localStream.getTracks()) {
+ if (track.kind == 'video') {
+ _connection!.addTrack(track, localStream);
+ _localMediaStream?.addTrack(track);
+ }
}
+ emit(EventStream(
+ session: this,
+ originator: Originator.local,
+ stream: _localMediaStream));
+ } else {
+ logger.w(
+ 'Remote wants to upgrade to video but no camera available to send');
}
- emit(
- EventStream(session: this, originator: 'local', stream: localStream));
}
logger.d('emit "sdp"');
final String? processedSDP = _sdpOfferToWebRTC(request.body);
- emit(EventSdp(originator: 'remote', type: 'offer', sdp: processedSDP));
+ emit(EventSdp(
+ originator: Originator.remote, type: SdpType.offer, sdp: processedSDP));
- RTCSessionDescription offer = RTCSessionDescription(processedSDP, 'offer');
+ RTCSessionDescription offer =
+ RTCSessionDescription(processedSDP, SdpType.offer.name);
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
try {
@@ -2083,26 +2210,27 @@ class RTCSession extends EventManager implements Owner {
'peerconnection.setRemoteDescription() failed');
}
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
if (_remoteHold == true && hold == false) {
_remoteHold = false;
- _onunhold('remote');
+ _onunhold(Originator.remote);
} else if (_remoteHold == false && hold == true) {
_remoteHold = true;
- _onhold('remote');
+ _onhold(Originator.remote);
}
// Create local description.
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
try {
- return await _createLocalDescription('answer', _rtcAnswerConstraints);
+ return await _createLocalDescription(
+ SdpType.answer, _rtcAnswerConstraints);
} catch (_) {
request.reply(500);
throw Exceptions.TypeError('_createLocalDescription() failed');
@@ -2137,8 +2265,8 @@ class RTCSession extends EventManager implements Owner {
InitSuccessCallback? initCallback, Map options) {
initCallback = (initCallback is Function) ? initCallback : null;
- if (_status != C.STATUS_WAITING_FOR_ACK &&
- _status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.waitingForAck &&
+ _state != RtcSessionState.confirmed) {
return false;
}
@@ -2246,8 +2374,8 @@ class RTCSession extends EventManager implements Owner {
logger.d('receiveReplaces()');
bool accept(InitSuccessCallback initCallback) {
- if (_status != C.STATUS_WAITING_FOR_ACK &&
- _status != C.STATUS_CONFIRMED) {
+ if (_state != RtcSessionState.waitingForAck &&
+ _state != RtcSessionState.confirmed) {
return false;
}
@@ -2314,20 +2442,22 @@ class RTCSession extends EventManager implements Owner {
// A stream is given, var the app set events such as 'peerconnection' and 'connecting'.
if (mediaStream != null) {
stream = mediaStream;
- emit(EventStream(session: this, originator: 'local', stream: stream));
+ emit(EventStream(
+ session: this, originator: Originator.local, stream: stream));
} // Request for user media access.
else if (mediaConstraints['audio'] != null ||
mediaConstraints['video'] != null) {
_localMediaStreamLocallyGenerated = true;
try {
stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
- emit(EventStream(session: this, originator: 'local', stream: stream));
+ emit(EventStream(
+ session: this, originator: Originator.local, stream: stream));
} catch (error) {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
_failed(
- 'local',
+ Originator.local,
null,
null,
null,
@@ -2336,11 +2466,11 @@ class RTCSession extends EventManager implements Owner {
'User Denied Media Access');
logger.e('emit "getusermediafailed" [error:${error.toString()}]');
emit(EventGetUserMediaFailed(exception: error));
- throw error;
+ rethrow;
}
}
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
@@ -2349,8 +2479,9 @@ class RTCSession extends EventManager implements Owner {
if (stream != null) {
switch (sdpSemantics) {
case 'unified-plan':
- stream.getTracks().forEach((MediaStreamTrack track) {
- _connection!.addTrack(track, stream!);
+ stream.getTracks().forEach((MediaStreamTrack track) async {
+ RTCRtpSender sender = await _connection!.addTrack(track, stream!);
+ _senders.add(sender);
});
break;
case 'plan-b':
@@ -2366,13 +2497,13 @@ class RTCSession extends EventManager implements Owner {
_connecting(_request);
try {
RTCSessionDescription desc =
- await _createLocalDescription('offer', rtcOfferConstraints);
- if (_is_canceled || _status == C.STATUS_TERMINATED) {
+ await _createLocalDescription(SdpType.offer, rtcOfferConstraints);
+ if (_is_canceled || _state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
_request.body = desc.sdp;
- _status = C.STATUS_INVITE_SENT;
+ _state = RtcSessionState.inviteSent;
logger.d('emit "sending" [request]');
@@ -2382,19 +2513,19 @@ class RTCSession extends EventManager implements Owner {
request_sender.send();
} catch (error, s) {
logger.e(error.toString(), error: error, stackTrace: s);
- _failed('local', null, null, null, 500, DartSIP_C.CausesType.WEBRTC_ERROR,
- 'Can\'t create local SDP');
- if (_status == C.STATUS_TERMINATED) {
+ _failed(Originator.local, null, null, null, 500,
+ DartSIP_C.CausesType.WEBRTC_ERROR, 'Can\'t create local SDP');
+ if (_state == RtcSessionState.terminated) {
return;
}
logger.e('Failed to _sendInitialRequest: ${error.toString()}');
- throw error;
+ rethrow;
}
}
/// Reception of Response for Initial INVITE
void _receiveInviteResponse(IncomingResponse? response) async {
- logger.d('receiveInviteResponse() current status: $_status ');
+ logger.d('receiveInviteResponse() current status: $_state ');
if (response == null) {
logger.d('No response received');
@@ -2439,7 +2570,8 @@ class RTCSession extends EventManager implements Owner {
return;
}
- if (_status != C.STATUS_INVITE_SENT && _status != C.STATUS_1XX_RECEIVED) {
+ if (_state != RtcSessionState.inviteSent &&
+ _state != RtcSessionState.provisionalResponse) {
return;
}
@@ -2447,7 +2579,7 @@ class RTCSession extends EventManager implements Owner {
if (utils.test100(status_code)) {
// 100 trying
- _status = C.STATUS_1XX_RECEIVED;
+ _state = RtcSessionState.provisionalResponse;
} else if (utils.test1XX(status_code)) {
// 1XX
// Do nothing with 1xx responses without To tag.
@@ -2464,18 +2596,21 @@ class RTCSession extends EventManager implements Owner {
}
}
- _status = C.STATUS_1XX_RECEIVED;
- _progress('remote', response, int.parse(status_code));
+ _state = RtcSessionState.provisionalResponse;
+ _progress(Originator.remote, response, int.parse(status_code));
if (response.body == null || response.body!.isEmpty) {
return;
}
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'remote', type: 'answer', sdp: response.body));
+ emit(EventSdp(
+ originator: Originator.remote,
+ type: SdpType.answer,
+ sdp: response.body));
RTCSessionDescription answer =
- RTCSessionDescription(response.body, 'answer');
+ RTCSessionDescription(response.body, SdpType.answer.name);
try {
await _connection!.setRemoteDescription(answer);
@@ -2485,9 +2620,12 @@ class RTCSession extends EventManager implements Owner {
emit(EventSetRemoteDescriptionFailed(exception: error));
}
} else if (utils.test2XX(status_code)) {
+ // 2XX
+ _state = RtcSessionState.confirmed;
+
if (response.body == null || response.body!.isEmpty) {
_acceptAndTerminate(response, 400, DartSIP_C.CausesType.MISSING_SDP);
- _failed('remote', null, null, response, 400,
+ _failed(Originator.remote, null, null, response, 400,
DartSIP_C.CausesType.BAD_MEDIA_DESCRIPTION, 'Missing SDP');
return;
}
@@ -2496,7 +2634,7 @@ class RTCSession extends EventManager implements Owner {
if (mediaPort == 0 && _ua.configuration.terminateOnAudioMediaPortZero) {
_acceptAndTerminate(response, 400, DartSIP_C.CausesType.MISSING_SDP);
- _failed('remote', null, null, response, 400,
+ _failed(Originator.remote, null, null, response, 400,
DartSIP_C.CausesType.BAD_MEDIA_DESCRIPTION, 'Media port is zero');
return;
}
@@ -2506,13 +2644,14 @@ class RTCSession extends EventManager implements Owner {
return;
}
- _status = C.STATUS_CONFIRMED;
-
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'remote', type: 'answer', sdp: response.body));
+ emit(EventSdp(
+ originator: Originator.remote,
+ type: SdpType.answer,
+ sdp: response.body));
RTCSessionDescription answer =
- RTCSessionDescription(response.body, 'answer');
+ RTCSessionDescription(response.body, SdpType.answer.name);
// Be ready for 200 with SDP after a 180/183 with SDP.
// We created a SDP 'answer' for it, so check the current signaling state.
@@ -2527,7 +2666,7 @@ class RTCSession extends EventManager implements Owner {
} catch (error) {
_acceptAndTerminate(response, 500, error.toString());
_failed(
- 'local',
+ Originator.local,
null,
null,
response,
@@ -2541,12 +2680,12 @@ class RTCSession extends EventManager implements Owner {
await _connection!.setRemoteDescription(answer);
// Handle Session Timers.
_handleSessionTimersInIncomingResponse(response);
- _accepted('remote', response);
+ _accepted(Originator.remote, response);
OutgoingRequest ack = sendRequest(SipMethod.ACK);
- _confirmed('local', ack);
+ _confirmed(Originator.local, ack);
} catch (error) {
_acceptAndTerminate(response, 488, 'Not Acceptable Here');
- _failed('remote', null, null, response, 488,
+ _failed(Originator.remote, null, null, response, 488,
DartSIP_C.CausesType.BAD_MEDIA_DESCRIPTION, 'Not Acceptable Here');
logger.e(
'emit "peerconnection:setremotedescriptionfailed" [error:${error.toString()}]');
@@ -2554,8 +2693,8 @@ class RTCSession extends EventManager implements Owner {
}
} else {
String cause = utils.sipErrorCause(response.status_code);
- _failed('remote', null, null, response, response.status_code, cause,
- response.reason_phrase);
+ _failed(Originator.remote, null, null, response, response.status_code,
+ cause, response.reason_phrase);
}
}
@@ -2567,6 +2706,118 @@ class RTCSession extends EventManager implements Owner {
options = options ?? {};
+ List extraHeaders = options['extraHeaders'] != null
+ ? utils.cloneArray(options['extraHeaders'])
+ : [];
+ EventManager eventHandlers = options['eventHandlers'] ?? EventManager();
+ Map? rtcOfferConstraints =
+ options['rtcOfferConstraints'] ?? _rtcOfferConstraints;
+
+ bool succeeded = false;
+
+ extraHeaders.add('Contact: $_contact');
+ extraHeaders.add('Content-Type: application/sdp');
+
+ // Session Timers.
+ if (_sessionTimers.running) {
+ extraHeaders.add(
+ 'Session-Expires: ${_sessionTimers.currentExpires};refresher=${_sessionTimers.refresher ? 'uac' : 'uas'}');
+ }
+
+ void onFailed([dynamic response]) {
+ eventHandlers.emit(EventCallFailed(session: this, response: response));
+ }
+
+ void onSucceeded(IncomingResponse? response) async {
+ if (_state == RtcSessionState.terminated) {
+ return;
+ }
+
+ sendRequest(SipMethod.ACK);
+
+ // If it is a 2XX retransmission exit now.
+ if (succeeded != null) {
+ return;
+ }
+
+ // Handle Session Timers.
+ _handleSessionTimersInIncomingResponse(response);
+
+ // Must have SDP answer.
+ if (response!.body == null || response.body!.isEmpty) {
+ onFailed();
+ return;
+ } else if (response.getHeader('Content-Type') != 'application/sdp') {
+ onFailed();
+ return;
+ }
+
+ logger.d('emit "sdp"');
+ emit(EventSdp(
+ originator: Originator.remote,
+ type: SdpType.answer,
+ sdp: response.body,
+ ));
+
+ RTCSessionDescription answer =
+ RTCSessionDescription(response.body, SdpType.answer.name);
+
+ try {
+ await _connection!.setRemoteDescription(answer);
+ eventHandlers.emit(EventSucceeded(response: response));
+ } catch (error) {
+ onFailed();
+ logger.e(
+ 'emit "peerconnection:setremotedescriptionfailed" [error:${error.toString()}]');
+ emit(EventSetRemoteDescriptionFailed(exception: error));
+ }
+ }
+
+ try {
+ RTCSessionDescription desc =
+ await _createLocalDescription(SdpType.offer, rtcOfferConstraints);
+ String? sdp = _mangleOffer(desc.sdp);
+ logger.d('emit "sdp"');
+ emit(EventSdp(
+ originator: Originator.local, type: SdpType.offer, sdp: sdp));
+
+ EventManager handlers = EventManager();
+ handlers.on(EventOnSuccessResponse(), (EventOnSuccessResponse event) {
+ onSucceeded(event.response as IncomingResponse?);
+ succeeded = true;
+ });
+ handlers.on(EventOnErrorResponse(), (EventOnErrorResponse event) {
+ onFailed(event.response);
+ });
+ handlers.on(EventOnTransportError(), (EventOnTransportError event) {
+ onTransportError(); // Do nothing because session ends.
+ });
+ handlers.on(EventOnRequestTimeout(), (EventOnRequestTimeout event) {
+ onRequestTimeout(); // Do nothing because session ends.
+ });
+ handlers.on(EventOnDialogError(), (EventOnDialogError event) {
+ onDialogError(); // Do nothing because session ends.
+ });
+
+ sendRequest(SipMethod.INVITE, {
+ 'extraHeaders': extraHeaders,
+ 'body': sdp,
+ 'eventHandlers': handlers
+ });
+ } catch (e, s) {
+ logger.e(e.toString(), error: e, stackTrace: s);
+ onFailed();
+ }
+ }
+
+ /**
+ * Send Re-INVITE
+ */
+ void _sendVideoUpgradeReinvite([Map? options]) async {
+ logger.d('sendVideoUpgradeReinvite()');
+
+ options = options ?? {};
+
List extraHeaders = options['extraHeaders'] != null
? utils.cloneArray(options['extraHeaders'])
: [];
@@ -2577,26 +2828,22 @@ class RTCSession extends EventManager implements Owner {
Map mediaConstraints =
options['mediaConstraints'] ?? {};
+ mediaConstraints['audio'] = false;
+
dynamic sdpSemantics =
options['pcConfig']?['sdpSemantics'] ?? 'unified-plan';
- bool hasVideo = (options['mediaConstraints']?['video'] ?? false) != false;
-
try {
MediaStream localStream =
await navigator.mediaDevices.getUserMedia(mediaConstraints);
_localMediaStreamLocallyGenerated = true;
- _localMediaStream = localStream;
-
- emit(
- EventStream(session: this, originator: 'local', stream: localStream));
switch (sdpSemantics) {
case 'unified-plan':
localStream.getTracks().forEach((MediaStreamTrack track) {
- if (track.kind == 'video' && hasVideo) {
+ if (track.kind == 'video')
_connection!.addTrack(track, localStream);
- }
+ _localMediaStream?.addTrack(track);
});
break;
case 'plan-b':
@@ -2606,13 +2853,18 @@ class RTCSession extends EventManager implements Owner {
logger.e('Unkown sdp semantics $sdpSemantics');
throw Exceptions.NotReadyError('Unkown sdp semantics $sdpSemantics');
}
+
+ emit(EventStream(
+ session: this,
+ originator: Originator.local,
+ stream: _localMediaStream));
} catch (error) {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
throw Exceptions.InvalidStateError('terminated');
}
request.reply(480);
_failed(
- 'local',
+ Originator.local,
null,
null,
null,
@@ -2640,7 +2892,7 @@ class RTCSession extends EventManager implements Owner {
}
void onSucceeded(IncomingResponse? response) async {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
@@ -2664,10 +2916,13 @@ class RTCSession extends EventManager implements Owner {
}
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'remote', type: 'answer', sdp: response.body));
+ emit(EventSdp(
+ originator: Originator.remote,
+ type: SdpType.answer,
+ sdp: response.body));
RTCSessionDescription answer =
- RTCSessionDescription(response.body, 'answer');
+ RTCSessionDescription(response.body, SdpType.answer.name);
try {
await _connection!.setRemoteDescription(answer);
@@ -2682,10 +2937,11 @@ class RTCSession extends EventManager implements Owner {
try {
RTCSessionDescription desc =
- await _createLocalDescription('offer', rtcOfferConstraints);
+ await _createLocalDescription(SdpType.offer, rtcOfferConstraints);
String? sdp = _mangleOffer(desc.sdp);
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'local', type: 'offer', sdp: sdp));
+ emit(EventSdp(
+ originator: Originator.local, type: SdpType.offer, sdp: sdp));
EventManager handlers = EventManager();
handlers.on(EventOnSuccessResponse(), (EventOnSuccessResponse event) {
@@ -2747,7 +3003,7 @@ class RTCSession extends EventManager implements Owner {
}
void onSucceeded(IncomingResponse? response) async {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
@@ -2770,11 +3026,13 @@ class RTCSession extends EventManager implements Owner {
}
logger.d('emit "sdp"');
- emit(
- EventSdp(originator: 'remote', type: 'answer', sdp: response.body));
+ emit(EventSdp(
+ originator: Originator.remote,
+ type: SdpType.answer,
+ sdp: response.body));
RTCSessionDescription answer =
- RTCSessionDescription(response.body, 'answer');
+ RTCSessionDescription(response.body, SdpType.answer.name);
try {
await _connection!.setRemoteDescription(answer);
@@ -2796,11 +3054,12 @@ class RTCSession extends EventManager implements Owner {
extraHeaders.add('Content-Type: application/sdp');
try {
RTCSessionDescription desc =
- await _createLocalDescription('offer', rtcOfferConstraints);
+ await _createLocalDescription(SdpType.offer, rtcOfferConstraints);
String? sdp = _mangleOffer(desc.sdp);
logger.d('emit "sdp"');
- emit(EventSdp(originator: 'local', type: 'offer', sdp: sdp));
+ emit(EventSdp(
+ originator: Originator.local, type: SdpType.offer, sdp: sdp));
EventManager handlers = EventManager();
handlers.on(EventOnSuccessResponse(), (EventOnSuccessResponse event) {
@@ -2877,7 +3136,7 @@ class RTCSession extends EventManager implements Owner {
}
// Update session status.
- _status = C.STATUS_TERMINATED;
+ _state = RtcSessionState.terminated;
}
/**
@@ -2937,7 +3196,7 @@ class RTCSession extends EventManager implements Owner {
}
/// SDP offers may contain text media channels. e.g. Older clients using linphone.
- ///
+ ///
/// WebRTC does not support text media channels, so remove them.
String? _sdpOfferToWebRTC(String? sdpInput) {
if (sdpInput == null) {
@@ -2960,20 +3219,16 @@ class RTCSession extends EventManager implements Owner {
void _setLocalMediaStatus() {
bool enableAudio = true, enableVideo = true;
-
if (_localHold || _remoteHold) {
enableAudio = false;
enableVideo = false;
}
-
if (_audioMuted) {
enableAudio = false;
}
-
if (_videoMuted) {
enableVideo = false;
}
-
_toggleMuteAudio(!enableAudio);
_toggleMuteVideo(!enableVideo);
}
@@ -3042,24 +3297,25 @@ class RTCSession extends EventManager implements Owner {
// I'm the refresher.
if (_sessionTimers.refresher) {
- _sessionTimers.timer = setTimeout(() {
- if (_status == C.STATUS_TERMINATED) {
- return;
- }
-
- logger.d('runSessionTimer() | sending session refresh request');
-
- if (_sessionTimers.refreshMethod == SipMethod.UPDATE) {
- _sendUpdate();
- } else {
- _sendReinvite();
- }
- }, expires! * 500); // Half the given interval (as the RFC states).
+ final int delayMs = expires! * 500;
+ _sessionTimers.timer = Timer.periodic(
+ Duration(milliseconds: delayMs),
+ (_) {
+ if (_state == RtcSessionState.terminated) return;
+ logger.d(
+ 'runSessionTimer() | sending session refresh request with expires=$expires, delayMs=$delayMs');
+ if (_sessionTimers.refreshMethod == SipMethod.UPDATE) {
+ _sendUpdate();
+ } else {
+ _sendReinvite();
+ }
+ },
+ );
}
// I'm not the refresher.
else {
_sessionTimers.timer = setTimeout(() {
- if (_status == C.STATUS_TERMINATED) {
+ if (_state == RtcSessionState.terminated) {
return;
}
@@ -3076,21 +3332,32 @@ class RTCSession extends EventManager implements Owner {
void _toggleMuteAudio(bool mute) {
if (_localMediaStream != null) {
+ if (_localMediaStream!.getAudioTracks().isEmpty) {
+ logger.w('Went to mute video but local stream has no video tracks');
+ }
for (MediaStreamTrack track in _localMediaStream!.getAudioTracks()) {
track.enabled = !mute;
}
+ } else {
+ logger.w('Went to mute audio but local stream is null');
}
}
void _toggleMuteVideo(bool mute) {
if (_localMediaStream != null) {
+ if (_localMediaStream!.getVideoTracks().isEmpty) {
+ logger.w(
+ 'Went to toggle mute video but local stream has no video tracks');
+ }
for (MediaStreamTrack track in _localMediaStream!.getVideoTracks()) {
track.enabled = !mute;
}
+ } else {
+ logger.w('Went to mute video but local stream is null');
}
}
- void _newRTCSession(String originator, dynamic request) {
+ void _newRTCSession(Originator originator, dynamic request) {
logger.d('newRTCSession()');
_ua.newRTCSession(originator: originator, session: this, request: request);
}
@@ -3101,7 +3368,7 @@ class RTCSession extends EventManager implements Owner {
emit(EventCallConnecting(session: this, request: request));
}
- void _progress(String originator, dynamic response, [int? status_code]) {
+ void _progress(Originator originator, dynamic response, [int? status_code]) {
logger.d('session progress');
logger.d('emit "progress"');
@@ -3114,7 +3381,7 @@ class RTCSession extends EventManager implements Owner {
cause: errorCause));
}
- void _accepted(String originator, [dynamic message]) {
+ void _accepted(Originator originator, [dynamic message]) {
logger.d('session accepted');
_start_time = DateTime.now();
logger.d('emit "accepted"');
@@ -3122,14 +3389,15 @@ class RTCSession extends EventManager implements Owner {
session: this, originator: originator, response: message));
}
- void _confirmed(String originator, dynamic ack) {
+ void _confirmed(Originator originator, dynamic ack) {
logger.d('session confirmed');
_is_confirmed = true;
logger.d('emit "confirmed"');
emit(EventCallConfirmed(session: this, originator: originator, ack: ack));
}
- void _ended(String originator, IncomingRequest? request, ErrorCause cause) {
+ void _ended(
+ Originator originator, IncomingRequest? request, ErrorCause cause) {
logger.d('session ended');
_end_time = DateTime.now();
_close();
@@ -3138,7 +3406,7 @@ class RTCSession extends EventManager implements Owner {
session: this, originator: originator, request: request, cause: cause));
}
- void _failed(String originator, dynamic message, dynamic request,
+ void _failed(Originator originator, dynamic message, dynamic request,
dynamic response, int? status_code, String cause, String? reason_phrase) {
logger.d('session failed');
@@ -3163,14 +3431,14 @@ class RTCSession extends EventManager implements Owner {
response: response));
}
- void _onhold(String originator) {
+ void _onhold(Originator originator) {
logger.d('session onhold');
_setLocalMediaStatus();
logger.d('emit "hold"');
emit(EventCallHold(session: this, originator: originator));
}
- void _onunhold(String originator) {
+ void _onunhold(Originator originator) {
logger.d('session onunhold');
_setLocalMediaStatus();
logger.d('emit "unhold"');
@@ -3190,4 +3458,53 @@ class RTCSession extends EventManager implements Owner {
logger.d('emit "unmuted"');
emit(EventCallUnmuted(session: this, audio: audio, video: video));
}
+
+ Future _logCallStat() async {
+ if (!ua.configuration.log_call_statistics) return;
+
+ try {
+ List? senders = await connection?.senders;
+ List? receivers = await connection?.receivers;
+
+ RTCRtpReceiver? receiver = receivers?.firstOrNull;
+ RTCRtpSender? sender = senders?.firstOrNull;
+
+ List senderStats = [];
+ List receiverStats = [];
+
+ if (sender != null) {
+ senderStats = await sender.getStats();
+ }
+
+ if (receiver != null) {
+ receiverStats = await receiver.getStats();
+ }
+
+ String senderStat = 'Sender stats: \n';
+
+ for (StatsReport s in senderStats) {
+ senderStat += ' ${s.timestamp} ${s.id} ${s.type}:\n';
+ s.values.forEach((key, value) {
+ senderStat += ' $key: $value\n';
+ });
+ senderStat += '\r';
+ }
+
+ logger.d(senderStat);
+
+ String receiverStat = 'Receiver stats: \n';
+
+ for (StatsReport s in receiverStats) {
+ receiverStat += ' ${s.timestamp} ${s.id} ${s.type}\n';
+ s.values.forEach((key, value) {
+ receiverStat += ' $key: $value\n';
+ });
+ receiverStat += '\r';
+ }
+
+ logger.d(receiverStat);
+ } catch (e) {
+ return;
+ }
+ }
}
diff --git a/lib/src/rtc_session/dtmf.dart b/lib/src/rtc_session/dtmf.dart
index df8269e3..5609475f 100644
--- a/lib/src/rtc_session/dtmf.dart
+++ b/lib/src/rtc_session/dtmf.dart
@@ -1,13 +1,12 @@
import 'package:flutter_webrtc/flutter_webrtc.dart';
-import 'package:sip_ua/sip_ua.dart';
+import '../../sip_ua.dart';
import '../constants.dart';
import '../event_manager/event_manager.dart';
import '../event_manager/internal_events.dart';
import '../exceptions.dart' as Exceptions;
import '../logger.dart';
-import '../rtc_session.dart' as rtc;
-import '../sip_message.dart';
+import '../rtc_session.dart';
import '../utils.dart' as Utils;
class C {
@@ -23,9 +22,9 @@ class DTMF extends EventManager {
_mode = mode;
}
- final rtc.RTCSession _session;
+ final RTCSession _session;
DtmfMode? _mode;
- String? _direction;
+ Direction? _direction;
String? _tone;
int? _duration;
int? _interToneGap;
@@ -36,18 +35,18 @@ class DTMF extends EventManager {
int? get duration => _duration;
- String? get direction => _direction;
+ Direction? get direction => _direction;
void send(String tone, Map options) {
if (tone == null) {
throw Exceptions.TypeError('Not enough arguments');
}
- _direction = 'outgoing';
+ _direction = Direction.outgoing;
// Check RTCSession Status.
- if (_session.status != rtc.C.STATUS_CONFIRMED &&
- _session.status != rtc.C.STATUS_WAITING_FOR_ACK) {
+ if (_session.state != RtcSessionState.confirmed &&
+ _session.state != RtcSessionState.waitingForAck) {
throw Exceptions.InvalidStateError(_session.status);
}
@@ -69,8 +68,8 @@ class DTMF extends EventManager {
_interToneGap = options['interToneGap'];
if (_mode == DtmfMode.RFC2833) {
- RTCDTMFSender dtmfSender = _session.dtmfSender;
- dtmfSender.insertDTMF(_tone!,
+ RTCDTMFSender? dtmfSender = _session.dtmfSender;
+ dtmfSender?.insertDTMF(_tone!,
duration: _duration!, interToneGap: _interToneGap!);
} else if (_mode == DtmfMode.INFO) {
extraHeaders.add('Content-Type: application/dtmf-relay');
@@ -79,16 +78,18 @@ class DTMF extends EventManager {
body += 'Duration=$_duration';
- _session.newDTMF('local', this, _request);
+ _session.newDTMF(Originator.local, this, _request);
EventManager handlers = EventManager();
handlers.on(EventOnSuccessResponse(), (EventOnSuccessResponse event) {
- emit(EventSucceeded(originator: 'remote', response: event.response));
+ emit(EventSucceeded(
+ originator: Originator.remote, response: event.response));
});
handlers.on(EventOnErrorResponse(), (EventOnErrorResponse event) {
_eventHandlers.emit(EventOnFialed());
emit(EventOnFialed());
- emit(EventCallFailed(originator: 'remote', response: event.response));
+ emit(EventCallFailed(
+ originator: Originator.remote, response: event.response));
});
handlers.on(EventOnRequestTimeout(), (EventOnRequestTimeout event) {
_session.onRequestTimeout();
@@ -113,7 +114,7 @@ class DTMF extends EventManager {
String reg_tone = r'^(Signal\s*?=\s*?)([0-9A-D#*]{1})(\s)?.*';
String reg_duration = r'^(Duration\s?=\s?)([0-9]{1,4})(\s)?.*';
- _direction = 'incoming';
+ _direction = Direction.incoming;
_request = request;
request.reply(200);
@@ -121,7 +122,7 @@ class DTMF extends EventManager {
if (request.body != null) {
List body = request.body!.split('\n');
- if (body.length >= 1) {
+ if (body.isNotEmpty) {
if (body[0].contains(RegExp(reg_tone))) {
_tone = body[0].replaceAll(reg_tone, '\$2');
}
@@ -139,7 +140,7 @@ class DTMF extends EventManager {
if (_tone == null) {
logger.d('invalid INFO DTMF received, discarded');
} else {
- _session.newDTMF('remote', this, request);
+ _session.newDTMF(Originator.remote, this, request);
}
}
}
diff --git a/lib/src/rtc_session/info.dart b/lib/src/rtc_session/info.dart
index 6eca4c2d..5399ba23 100644
--- a/lib/src/rtc_session/info.dart
+++ b/lib/src/rtc_session/info.dart
@@ -1,16 +1,17 @@
-import 'package:sip_ua/src/sip_message.dart';
import '../constants.dart';
+import '../enums.dart';
import '../event_manager/event_manager.dart';
import '../event_manager/internal_events.dart';
import '../exceptions.dart' as Exceptions;
-import '../rtc_session.dart' as rtc;
+import '../rtc_session.dart';
+import '../sip_message.dart';
import '../utils.dart' as utils;
class Info extends EventManager {
Info(this._session);
- final rtc.RTCSession _session;
- String? _direction;
+ final RTCSession _session;
+ Direction? _direction;
String? _contentType;
String? _body;
IncomingRequest? _request;
@@ -19,18 +20,18 @@ class Info extends EventManager {
String? get body => _body;
- String? get direction => _direction;
+ Direction? get direction => _direction;
void send(String contentType, String body, Map options) {
- _direction = 'outgoing';
+ _direction = Direction.outgoing;
if (contentType == null) {
throw Exceptions.TypeError('Not enough arguments');
}
// Check RTCSession Status.
- if (_session.status != rtc.C.STATUS_CONFIRMED &&
- _session.status != rtc.C.STATUS_WAITING_FOR_ACK) {
+ if (_session.state != RtcSessionState.confirmed &&
+ _session.state != RtcSessionState.waitingForAck) {
throw Exceptions.InvalidStateError(_session.status);
}
@@ -41,14 +42,16 @@ class Info extends EventManager {
extraHeaders.add('Content-Type: $contentType');
- _session.newInfo('local', this, _request);
+ _session.newInfo(Originator.local, this, _request);
EventManager handlers = EventManager();
handlers.on(EventOnSuccessResponse(), (EventOnSuccessResponse event) {
- emit(EventSucceeded(originator: 'remote', response: event.response));
+ emit(EventSucceeded(
+ originator: Originator.remote, response: event.response));
});
handlers.on(EventOnErrorResponse(), (EventOnErrorResponse event) {
- emit(EventCallFailed(originator: 'remote', response: event.response));
+ emit(EventCallFailed(
+ originator: Originator.remote, response: event.response));
});
handlers.on(EventOnTransportError(), (EventOnTransportError event) {
_session.onTransportError();
@@ -68,7 +71,7 @@ class Info extends EventManager {
}
void init_incoming(IncomingRequest request) {
- _direction = 'incoming';
+ _direction = Direction.incoming;
_request = request;
request.reply(200);
@@ -76,6 +79,6 @@ class Info extends EventManager {
_contentType = request.getHeader('content-type');
_body = request.body;
- _session.newInfo('remote', this, request);
+ _session.newInfo(Originator.remote, this, request);
}
}
diff --git a/lib/src/rtc_session/refer_subscriber.dart b/lib/src/rtc_session/refer_subscriber.dart
index 6d410df2..716db186 100644
--- a/lib/src/rtc_session/refer_subscriber.dart
+++ b/lib/src/rtc_session/refer_subscriber.dart
@@ -1,4 +1,3 @@
-import 'package:sip_ua/src/sip_message.dart';
import '../constants.dart' as DartSIP_C;
import '../constants.dart';
import '../event_manager/event_manager.dart';
@@ -6,6 +5,7 @@ import '../event_manager/internal_events.dart';
import '../grammar.dart';
import '../logger.dart';
import '../rtc_session.dart' as rtc;
+import '../sip_message.dart';
import '../uri.dart';
import '../utils.dart' as Utils;
@@ -45,7 +45,7 @@ class ReferSubscriber extends EventManager {
// Referred-By header field.
String referredBy =
- 'Referred-By: <${_session.ua.configuration.uri.scheme}:${_session.ua.configuration.uri.user}@${_session.ua.configuration.uri.host}>';
+ 'Referred-By: <${_session.ua.configuration.uri!.scheme}:${_session.ua.configuration.uri!.user}@${_session.ua.configuration.uri!.host}>';
extraHeaders.add(referredBy);
extraHeaders.add('Contact: ${_session.contact}');
diff --git a/lib/src/sip_message.dart b/lib/src/sip_message.dart
index c593fe0f..e13763c2 100644
--- a/lib/src/sip_message.dart
+++ b/lib/src/sip_message.dart
@@ -2,7 +2,6 @@ import 'dart:convert' show utf8;
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
-import 'package:sip_ua/src/transactions/transaction_base.dart';
import 'constants.dart' as DartSIP_C;
import 'constants.dart';
import 'data.dart';
@@ -11,6 +10,7 @@ import 'grammar.dart';
import 'logger.dart';
import 'name_addr_header.dart';
import 'socket_transport.dart';
+import 'transactions/transaction_base.dart';
import 'ua.dart';
import 'uri.dart';
import 'utils.dart' as utils;
@@ -656,8 +656,6 @@ class IncomingRequest extends IncomingMessage {
// Validate code and reason values.
if (code == null || (code < 100 || code > 699)) {
throw Exceptions.TypeError('Invalid status_code: $code');
- } else if (reason != null) {
- throw Exceptions.TypeError('Invalid reason_phrase: $reason');
}
reason = reason ?? DartSIP_C.REASON_PHRASE[code] ?? '';
diff --git a/lib/src/sip_ua_helper.dart b/lib/src/sip_ua_helper.dart
index 71de9291..3ef28651 100644
--- a/lib/src/sip_ua_helper.dart
+++ b/lib/src/sip_ua_helper.dart
@@ -3,24 +3,29 @@ import 'dart:async';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:logger/logger.dart';
import 'package:sdp_transform/sdp_transform.dart' as sdp_transform;
-import 'package:sip_ua/sip_ua.dart';
-import 'package:sip_ua/src/event_manager/internal_events.dart';
-import 'package:sip_ua/src/map_helper.dart';
-import 'package:sip_ua/src/transports/socket_interface.dart';
-import 'package:sip_ua/src/transports/tcp_socket.dart';
+
+import 'package:sip_ua/src/uri.dart';
import 'config.dart';
import 'constants.dart' as DartSIP_C;
+import 'enums.dart';
import 'event_manager/event_manager.dart';
+import 'event_manager/internal_events.dart';
import 'event_manager/subscriber_events.dart';
import 'logger.dart';
+import 'map_helper.dart';
import 'message.dart';
+import 'options.dart';
import 'rtc_session.dart';
import 'rtc_session/refer_subscriber.dart';
import 'sip_message.dart';
import 'stack_trace_nj.dart';
import 'subscriber.dart';
+import 'transport_type.dart';
+import 'transports/socket_interface.dart';
+import 'transports/tcp_socket.dart';
import 'transports/web_socket.dart';
import 'ua.dart';
+import 'utils.dart' as Utils;
class SIPUAHelper extends EventManager {
SIPUAHelper({Logger? customLogger}) {
@@ -79,12 +84,13 @@ class SIPUAHelper extends EventManager {
_ua!.register();
}
- void unregister([bool all = true]) {
+ Future unregister([bool all = true]) async {
if (_ua != null) {
assert(registered, 'ERROR: you must call register first.');
- _ua!.unregister(all: all);
+ return _ua!.unregister(all: all);
} else {
logger.e('ERROR: unregister called, you must call start first.');
+ return false;
}
}
@@ -123,7 +129,7 @@ class SIPUAHelper extends EventManager {
required bool voiceOnly,
Map? options,
bool useUpdate = false,
- Function(IncomingMessage)? done,
+ Function(IncomingMessage?)? done,
}) async {
Map finalOptions = options ?? buildCallOptions(voiceOnly);
call.renegotiate(options: finalOptions, useUpdate: useUpdate, done: done);
@@ -158,7 +164,8 @@ class SIPUAHelper extends EventManager {
}
_settings.transportType = uaSettings.transportType!;
- _settings.uri = uaSettings.uri;
+ _settings.uri =
+ uaSettings.uri != null ? Utils.normalizeTarget(uaSettings.uri!) : null;
_settings.sip_message_delay = uaSettings.sip_message_delay;
_settings.realm = uaSettings.realm;
_settings.password = uaSettings.password;
@@ -175,16 +182,19 @@ class SIPUAHelper extends EventManager {
_settings.session_timers = uaSettings.sessionTimers;
_settings.ice_gathering_timeout = uaSettings.iceGatheringTimeout;
_settings.session_timers_refresh_method =
- uaSettings.sessionTimersRefreshMethod;
+ uaSettings.sessionTimersRefreshMethodEnum;
_settings.instance_id = uaSettings.instanceId;
_settings.registrar_server = uaSettings.registrarServer;
- _settings.contact_uri = uaSettings.contact_uri;
- _settings.connection_recovery_max_interval =
+ _settings.contact_uri = uaSettings.contact_uri != null
+ ? Utils.normalizeTarget(uaSettings.contact_uri!)
+ : null;
+ _settings.connection_recovery_max_interval =
uaSettings.connectionRecoveryMaxInterval;
- _settings.connection_recovery_min_interval =
+ _settings.connection_recovery_min_interval =
uaSettings.connectionRecoveryMinInterval;
_settings.terminateOnAudioMediaPortZero =
uaSettings.terminateOnMediaPortZero;
+ _settings.log_call_statistics = uaSettings.logCallStatistics;
try {
_ua = UA(_settings);
@@ -232,7 +242,7 @@ class SIPUAHelper extends EventManager {
_ua!.on(EventNewRTCSession(), (EventNewRTCSession event) {
logger.d('newRTCSession => $event');
RTCSession session = event.session!;
- if (session.direction == 'incoming') {
+ if (session.direction == Direction.incoming) {
// Set event handlers.
session.addAllEventHandlers(
buildCallOptions()['eventHandlers'] as EventManager);
@@ -250,7 +260,7 @@ class SIPUAHelper extends EventManager {
_ua!.on(EventNewMessage(), (EventNewMessage event) {
logger.d('newMessage => $event');
//Only notify incoming message to listener
- if (event.message!.direction == 'incoming') {
+ if (event.message!.direction == Direction.incoming) {
SIPMessageRequest message =
SIPMessageRequest(event.message, event.originator, event.request);
_notifyNewMessageListeners(message);
@@ -363,7 +373,11 @@ class SIPUAHelper extends EventManager {
'iceTransportPolicy':
(_uaSettings?.iceTransportPolicy ?? IceTransportPolicy.ALL)
.toParameterString(),
- 'iceServers': _uaSettings?.iceServers
+ 'iceServers': _uaSettings?.iceServers,
+ 'tcpCandidatePolicy':
+ (_uaSettings?.tcpCandidatePolicy ?? TcpCandidatePolicy.ENABLED)
+ .toParameterString(),
+ 'iceCandidatePoolSize': _uaSettings?.iceCandidatePoolSize
},
'mediaConstraints': {
'audio': true,
@@ -413,6 +427,11 @@ class SIPUAHelper extends EventManager {
return _ua!.sendMessage(target, body, options, params);
}
+ Options sendOptions(
+ String target, String body, Map? params) {
+ return _ua!.sendOptions(target, body, params);
+ }
+
void subscribe(String target, String event, String contentType) {
Subscriber s = _ua!.subscribe(target, event, contentType);
@@ -574,6 +593,8 @@ class Call {
} else {
logger.d("peerConnection is null, can't stop tracks.");
}
+
+ if (state == CallStateEnum.ENDED) return;
_session.terminate(options);
}
@@ -600,7 +621,7 @@ class Call {
void renegotiate({
required Map? options,
bool useUpdate = false,
- Function(IncomingMessage)? done,
+ Function(IncomingMessage?)? done,
}) {
assert(_session != null, 'ERROR(renegotiate): rtc session is invalid!');
_session.renegotiate(options: options, useUpdate: useUpdate, done: done);
@@ -657,12 +678,9 @@ class Call {
return '';
}
- String get direction {
+ Direction? get direction {
assert(_session != null, 'ERROR(get direction): rtc session is invalid!');
- if (_session.direction != null) {
- return _session.direction!.toUpperCase();
- }
- return '';
+ return _session.direction;
}
bool get remote_has_audio => _peerHasMediaLine('audio');
@@ -709,7 +727,7 @@ class CallState {
this.refer});
CallStateEnum state;
ErrorCause? cause;
- String? originator;
+ Originator? originator;
bool? audio;
bool? video;
MediaStream? stream;
@@ -745,7 +763,7 @@ class TransportState {
class SIPMessageRequest {
SIPMessageRequest(this.message, this.originator, this.request);
dynamic request;
- String? originator;
+ Originator? originator;
Message? message;
}
@@ -840,6 +858,19 @@ extension _IceTransportPolicyEncoding on IceTransportPolicy {
}
}
+enum TcpCandidatePolicy { ENABLED, DISABLED }
+
+extension _TcpCandidatePolicyEncoding on TcpCandidatePolicy {
+ String toParameterString() {
+ switch (this) {
+ case TcpCandidatePolicy.ENABLED:
+ return 'enabled';
+ case TcpCandidatePolicy.DISABLED:
+ return 'disabled';
+ }
+ }
+}
+
class UaSettings {
WebSocketSettings webSocketSettings = WebSocketSettings();
TcpSocketSettings tcpSocketSettings = TcpSocketSettings();
@@ -886,6 +917,9 @@ class UaSettings {
/// Min interval between recovery connection, default 2 sec
int connectionRecoveryMinInterval = 2;
+ /// Allows to write advanced call statistics in the log after the call ends
+ bool logCallStatistics = false;
+
bool terminateOnMediaPortZero = false;
/// Sip Message Delay (in millisecond) (default 0).
@@ -905,8 +939,29 @@ class UaSettings {
/// Will default to [IceTransportPolicy.ALL] if not specified.
IceTransportPolicy? iceTransportPolicy;
+ /// Allows to disable tcp candidates gathering
+ /// Will default to [TcpCandidatePolicy.ENABLED] if not specified.
+ TcpCandidatePolicy? tcpCandidatePolicy;
+
+ /// An unsigned 16-bit integer value which specifies the size of the prefetched
+ /// ICE candidate pool. The default value is 0 (meaning no candidate prefetching will occur).
+ /// You may find in some cases that connections can be established more quickly
+ /// by allowing the ICE agent to start fetching ICE candidates before you start
+ /// trying to connect, so that they're already available for inspection
+ /// when RTCPeerConnection.setLocalDescription() is called.
+ int iceCandidatePoolSize = 0;
+
/// Controls which kind of messages are to be sent to keep a SIP session
/// alive.
/// Defaults to "UPDATE"
- DartSIP_C.SipMethod sessionTimersRefreshMethod = DartSIP_C.SipMethod.UPDATE;
+ String sessionTimersRefreshMethod = 'UPDATE';
+ DartSIP_C.SipMethod get sessionTimersRefreshMethodEnum {
+ switch (sessionTimersRefreshMethod.toUpperCase()) {
+ case 'INVITE':
+ return DartSIP_C.SipMethod.INVITE;
+ case 'UPDATE':
+ default:
+ return DartSIP_C.SipMethod.UPDATE;
+ }
+ }
}
diff --git a/lib/src/socket_transport.dart b/lib/src/socket_transport.dart
index d094524a..ea82046d 100644
--- a/lib/src/socket_transport.dart
+++ b/lib/src/socket_transport.dart
@@ -1,17 +1,35 @@
import 'dart:async';
import 'dart:math';
-import 'package:sip_ua/src/event_manager/events.dart';
-import 'package:sip_ua/src/transport_constants.dart';
-import 'package:sip_ua/src/transports/socket_interface.dart';
-import 'package:sip_ua/src/transports/tcp_socket.dart';
+import './event_manager/events.dart';
+import './transports/socket_interface.dart';
import 'exceptions.dart' as Exceptions;
import 'logger.dart';
import 'stack_trace_nj.dart';
import 'timers.dart';
-import 'transports/web_socket.dart';
import 'utils.dart';
+enum TransportStatus { connected, connecting, disconnected }
+
+enum SocketStatus { ready, error }
+
+class SocketInfo {
+ SocketInfo({
+ required this.socket,
+ required this.weight,
+ required this.status,
+ });
+
+ SIPUASocketInterface socket;
+ int weight;
+ SocketStatus status;
+}
+
+const Map defaultRecoveryOptions = {
+ 'min_interval': 2, // minimum interval in seconds between recover attempts
+ 'max_interval': 30 // maximum interval in seconds between recover attempts
+};
+
/*
* Manages one or multiple DartSIP.Socket instances.
* Is reponsible for transport recovery logic among all socket instances.
@@ -19,34 +37,36 @@ import 'utils.dart';
* @socket DartSIP::Socket instance
*/
class SocketTransport {
- SocketTransport(List? sockets,
- [Map recovery_options = C.recovery_options]) {
+ SocketTransport(
+ List? sockets, [
+ Map recovery_options = defaultRecoveryOptions,
+ ]) {
logger.d('Socket Transport new()');
_recovery_options = recovery_options;
// We must recieve at least 1 socket
- if (sockets!.length == 0) {
+ if (sockets!.isEmpty) {
throw Exceptions.TypeError(
'invalid argument: Must recieve atleast 1 web socket');
}
- for (SIPUASocketInterface socket in sockets) {
- _socketsMap.add({
- 'socket': socket,
- 'weight': socket.weight ?? 0,
- 'status': C.SOCKET_STATUS_READY
- });
+ for (final SIPUASocketInterface socket in sockets) {
+ _sockets.add(SocketInfo(
+ socket: socket,
+ weight: socket.weight ?? 0,
+ status: SocketStatus.ready,
+ ));
}
// Get the socket with higher weight.
_getSocket();
}
- int status = C.STATUS_DISCONNECTED;
+ TransportStatus status = TransportStatus.disconnected;
// Current socket.
late SIPUASocketInterface socket;
// Socket collection.
- final List