├── Images
├── .DS_Store
├── carthage.png
├── hang-up.png
├── build-and-run.png
├── incoming-call.png
├── client-to-client.png
├── client-to-pstn.png
├── create-api-key.png
├── create-twiml-app.png
├── add-push-credential.png
├── update_push_credential.png
├── xcode-project-capabilities.png
└── create-voip-service-certificate.png
├── .gitignore
├── ringtone
└── ringtone.wav
├── Cartfile
├── ObjcVoiceQuickstart
├── TwilioLogo.png
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ ├── Icon.png
│ │ ├── Icon-40.png
│ │ ├── Icon-72.png
│ │ ├── Icon-76.png
│ │ ├── Icon@2x.png
│ │ ├── Icon-40@2x.png
│ │ ├── Icon-40@3x.png
│ │ ├── Icon-60@2x.png
│ │ ├── Icon-60@3x.png
│ │ ├── Icon-72@2x.png
│ │ ├── Icon-76@2x.png
│ │ ├── Icon-Small.png
│ │ ├── Icon-83.5@2x.png
│ │ ├── Icon-Small-50.png
│ │ ├── Icon-Small@2x.png
│ │ ├── Icon-Small@3x.png
│ │ ├── Icon-Small-50@2x.png
│ │ └── Contents.json
├── ViewController.h
├── ObjCVoiceQuickstart.entitlements
├── AppDelegate.h
├── main.m
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
├── AppDelegate.m
└── ViewController.m
├── ObjcVoiceQuickstart.xcodeproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── project.pbxproj
├── Podfile
├── Docs
├── migration-guide-3.x-4.x.md
├── new-features-4.0.md
├── new-features-3.0.md
└── migration-guide-2.x-3.x.md
├── LICENSE
├── .github
└── ISSUE_TEMPLATE.md
└── README.md
/Images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/.DS_Store
--------------------------------------------------------------------------------
/Images/carthage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/carthage.png
--------------------------------------------------------------------------------
/Images/hang-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/hang-up.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Pods/
2 | Podfile.lock
3 | ObjCVoiceQuickstart.xcworkspace
4 |
5 | xcuserdata/
6 | *.xcuserstate
--------------------------------------------------------------------------------
/ringtone/ringtone.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ringtone/ringtone.wav
--------------------------------------------------------------------------------
/Images/build-and-run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/build-and-run.png
--------------------------------------------------------------------------------
/Images/incoming-call.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/incoming-call.png
--------------------------------------------------------------------------------
/Cartfile:
--------------------------------------------------------------------------------
1 | binary "https://raw.githubusercontent.com/twilio/twilio-voice-ios/fix-carthage/twilio-voice-ios.json"
2 |
--------------------------------------------------------------------------------
/Images/client-to-client.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/client-to-client.png
--------------------------------------------------------------------------------
/Images/client-to-pstn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/client-to-pstn.png
--------------------------------------------------------------------------------
/Images/create-api-key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/create-api-key.png
--------------------------------------------------------------------------------
/Images/create-twiml-app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/create-twiml-app.png
--------------------------------------------------------------------------------
/Images/add-push-credential.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/add-push-credential.png
--------------------------------------------------------------------------------
/Images/update_push_credential.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/update_push_credential.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/TwilioLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/TwilioLogo.png
--------------------------------------------------------------------------------
/Images/xcode-project-capabilities.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/xcode-project-capabilities.png
--------------------------------------------------------------------------------
/Images/create-voip-service-certificate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/Images/create-voip-service-certificate.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-40.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-72.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio/voice-quickstart-objc/HEAD/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/ViewController.h:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.h
3 | // Twilio Voice with Quickstart - Objective-C
4 | //
5 | // Copyright © 2016 Twilio, Inc. All rights reserved.
6 | //
7 |
8 | #import
9 |
10 | @interface ViewController : UIViewController
11 |
12 |
13 | @end
14 |
15 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/ObjCVoiceQuickstart.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://github.com/CocoaPods/Specs.git'
2 |
3 | workspace 'ObjCVoiceQuickstart'
4 |
5 | abstract_target 'TwilioVoice' do
6 | pod 'TwilioVoice', '~> 5.1.1'
7 |
8 | target 'ObjCVoiceQuickstart' do
9 | platform :ios, '10.0'
10 | project 'ObjCVoiceQuickstart.xcproject'
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/AppDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.h
3 | // Twilio Voice with Quickstart - Objective-C
4 | //
5 | // Copyright © 2016 Twilio, Inc. All rights reserved.
6 | //
7 |
8 | #import
9 |
10 | @interface AppDelegate : UIResponder
11 |
12 | @property (strong, nonatomic) UIWindow *window;
13 |
14 |
15 | @end
16 |
17 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // Twilio Voice with Quickstart - Objective-C
4 | //
5 | // Copyright © 2016 Twilio, Inc. All rights reserved.
6 | //
7 |
8 | #import
9 | #import "AppDelegate.h"
10 |
11 | int main(int argc, char * argv[]) {
12 | @autoreleasepool {
13 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Docs/migration-guide-3.x-4.x.md:
--------------------------------------------------------------------------------
1 | ## SDK 4.0 New Features
2 |
3 | Voice iOS SDK 4.0 introduced a new call state: `TVOCallStateReconnecting`. You will need to update any logic you have implemented that relies on the call state. The simplest approach is to treat a `TVOCallStateReconnecting` just like a `TVOCallStateConnected` and keep the current behavior.
4 |
5 | For more advanced behaviour, you can make use of TVOCallDelegate’s new protocol methods `call:isReconnectingWithError:` and `callDidReconnect:` to update the UI for example and indicate the ongoing disruption.
6 |
7 | For example:
8 |
9 | ```
10 | - (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error {
11 | NSLog(@"Call is reconnecting");
12 |
13 | // Update UI
14 | // Check the error: It could be either
15 | // TVOErrorSignalingConnectionDisconnectedError (53001) or
16 | // TVOErrorMediaConnectionError (53405).
17 | }
18 |
19 | - (void)callDidReconnect:(TVOCall *)call {
20 | NSLog(@"Call reconnected");
21 |
22 | // Update UI
23 | }
24 | ```
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2016-2017 Twilio, Inc. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Docs/new-features-4.0.md:
--------------------------------------------------------------------------------
1 | ## 4.0 New Features
2 |
3 | #### Reconnecting State and Callbacks
4 |
5 | `TVOCallStateReconnecting` is provided as a new Call state. A callback `call:isReconnectingWithError:` corresponding to this state transition is triggered when a network change is detected when Call is already in the `TVOCallStateConnected` state. If the Call is in `TVOCallStateConnecting`or in `TVOCallStateRinging` state when network change happened, the SDK will receive the `call:didFailToConnectWithError:` callback with the `TVOErrorConnectionError` error (31005). If a Call is reconnected after reconnection attempts, the application will receive the `callDidReconnect:` callback and the Call state transitions to `TVOCallStateConnected`.
6 |
7 | Updates:
8 |
9 | ```
10 | typedef NS_ENUM(NSUInteger, TVOCallState) {
11 | TVOCallStateConnecting = 0, ///< The Call is connecting.
12 | TVOCallStateRinging, ///< The Call is ringing.
13 | TVOCallStateConnected, ///< The Call is connected.
14 | TVOCallStateReconnecting, ///< The Call is reconnecting.
15 | TVOCallStateDisconnected ///< The Call is disconnected.
16 | };
17 | ```
18 |
19 | ```
20 | @protocol TVOCallDelegate
21 |
22 | @optional
23 | - (void)callDidStartRinging:(nonnull TVOCall *)call;
24 | - (void)call:(nonnull TVOCall *)call isReconnectingWithError:(nonnull NSError *)error;
25 |
26 | @end
27 | ```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | > Before filing an issue please check that the issue is not already addressed by the following:
3 | > * [Voice SDK Guides](https://www.twilio.com/docs/api/voice-sdk)
4 | > * [GitHub Issues](https://github.com/twilio/voice-quickstart-objc/issues)
5 | > * [Changelog](https://www.twilio.com/docs/api/voice-sdk/ios/changelog)
6 |
7 | > Please ensure that you are not sharing any
8 | [Personally Identifiable Information(PII)](https://www.twilio.com/docs/glossary/what-is-personally-identifiable-information-pii)
9 | or sensitive account information (API keys, credentials, etc.) when reporting an issue.
10 |
11 | ### Description
12 |
13 | [Description of the issue]
14 |
15 | ### Steps to Reproduce
16 |
17 | 1. [Step one]
18 | 2. [Step two]
19 | 3. [Insert as many steps as needed]
20 |
21 | #### Code
22 |
23 | ```objc
24 | // Code that helps reproduce the issue
25 | ```
26 |
27 | #### Expected Behavior
28 |
29 | [What you expect to happen]
30 |
31 | #### Actual Behavior
32 |
33 | [What actually happens]
34 |
35 | #### Reproduces How Often
36 |
37 | [What percentage of the time does it reproduce?]
38 |
39 | #### Logs
40 |
41 | ```
42 | // Log output when the issue occurs
43 | ```
44 |
45 | ### Versions
46 |
47 | All relevant version information for the issue.
48 |
49 | #### Voice iOS SDK
50 |
51 | [e.g. 2.0.0-beta13 via CocoaPods]
52 |
53 | #### Xcode
54 |
55 | [e.g. 8.3.3]
56 |
57 | #### iOS Version
58 |
59 | [e.g. 10.3.3]
60 |
61 | #### iOS Device
62 |
63 | [e.g. iPhone 6s Plus]
64 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSMicrophoneUsageDescription
24 | $(EXECUTABLE_NAME) would like to access your microphone
25 | UIBackgroundModes
26 |
27 | audio
28 | voip
29 |
30 | UILaunchStoryboardName
31 | LaunchScreen
32 | UIMainStoryboardFile
33 | Main
34 | UIRequiredDeviceCapabilities
35 |
36 | armv7
37 |
38 | UISupportedInterfaceOrientations
39 |
40 | UIInterfaceOrientationPortrait
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UISupportedInterfaceOrientations~ipad
45 |
46 | UIInterfaceOrientationPortrait
47 | UIInterfaceOrientationPortraitUpsideDown
48 | UIInterfaceOrientationLandscapeLeft
49 | UIInterfaceOrientationLandscapeRight
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/AppDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.m
3 | // Twilio Voice with Quickstart - Objective-C
4 | //
5 | // Copyright © 2016 Twilio, Inc. All rights reserved.
6 | //
7 |
8 | #import "AppDelegate.h"
9 |
10 | @import TwilioVoice;
11 |
12 | @interface AppDelegate ()
13 | @end
14 |
15 | @implementation AppDelegate
16 |
17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
18 | NSLog(@"Twilio Voice Version: %@", [TwilioVoice sdkVersion]);
19 | return YES;
20 | }
21 |
22 | - (void)applicationWillResignActive:(UIApplication *)application {
23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | - (void)applicationDidEnterBackground:(UIApplication *)application {
28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | - (void)applicationWillEnterForeground:(UIApplication *)application {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | - (void)applicationDidBecomeActive:(UIApplication *)application {
37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
38 | }
39 |
40 | - (void)applicationWillTerminate:(UIApplication *)application {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 | @end
45 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "size" : "29x29",
15 | "idiom" : "iphone",
16 | "filename" : "Icon-Small.png",
17 | "scale" : "1x"
18 | },
19 | {
20 | "size" : "29x29",
21 | "idiom" : "iphone",
22 | "filename" : "Icon-Small@2x.png",
23 | "scale" : "2x"
24 | },
25 | {
26 | "size" : "29x29",
27 | "idiom" : "iphone",
28 | "filename" : "Icon-Small@3x.png",
29 | "scale" : "3x"
30 | },
31 | {
32 | "size" : "40x40",
33 | "idiom" : "iphone",
34 | "filename" : "Icon-40@2x.png",
35 | "scale" : "2x"
36 | },
37 | {
38 | "size" : "40x40",
39 | "idiom" : "iphone",
40 | "filename" : "Icon-40@3x.png",
41 | "scale" : "3x"
42 | },
43 | {
44 | "size" : "57x57",
45 | "idiom" : "iphone",
46 | "filename" : "Icon.png",
47 | "scale" : "1x"
48 | },
49 | {
50 | "size" : "57x57",
51 | "idiom" : "iphone",
52 | "filename" : "Icon@2x.png",
53 | "scale" : "2x"
54 | },
55 | {
56 | "size" : "60x60",
57 | "idiom" : "iphone",
58 | "filename" : "Icon-60@2x.png",
59 | "scale" : "2x"
60 | },
61 | {
62 | "size" : "60x60",
63 | "idiom" : "iphone",
64 | "filename" : "Icon-60@3x.png",
65 | "scale" : "3x"
66 | },
67 | {
68 | "idiom" : "ipad",
69 | "size" : "20x20",
70 | "scale" : "1x"
71 | },
72 | {
73 | "idiom" : "ipad",
74 | "size" : "20x20",
75 | "scale" : "2x"
76 | },
77 | {
78 | "size" : "29x29",
79 | "idiom" : "ipad",
80 | "filename" : "Icon-Small.png",
81 | "scale" : "1x"
82 | },
83 | {
84 | "size" : "29x29",
85 | "idiom" : "ipad",
86 | "filename" : "Icon-Small@2x.png",
87 | "scale" : "2x"
88 | },
89 | {
90 | "size" : "40x40",
91 | "idiom" : "ipad",
92 | "filename" : "Icon-40.png",
93 | "scale" : "1x"
94 | },
95 | {
96 | "size" : "40x40",
97 | "idiom" : "ipad",
98 | "filename" : "Icon-40@2x.png",
99 | "scale" : "2x"
100 | },
101 | {
102 | "size" : "50x50",
103 | "idiom" : "ipad",
104 | "filename" : "Icon-Small-50.png",
105 | "scale" : "1x"
106 | },
107 | {
108 | "size" : "50x50",
109 | "idiom" : "ipad",
110 | "filename" : "Icon-Small-50@2x.png",
111 | "scale" : "2x"
112 | },
113 | {
114 | "size" : "72x72",
115 | "idiom" : "ipad",
116 | "filename" : "Icon-72.png",
117 | "scale" : "1x"
118 | },
119 | {
120 | "size" : "72x72",
121 | "idiom" : "ipad",
122 | "filename" : "Icon-72@2x.png",
123 | "scale" : "2x"
124 | },
125 | {
126 | "size" : "76x76",
127 | "idiom" : "ipad",
128 | "filename" : "Icon-76.png",
129 | "scale" : "1x"
130 | },
131 | {
132 | "size" : "76x76",
133 | "idiom" : "ipad",
134 | "filename" : "Icon-76@2x.png",
135 | "scale" : "2x"
136 | },
137 | {
138 | "size" : "83.5x83.5",
139 | "idiom" : "ipad",
140 | "filename" : "Icon-83.5@2x.png",
141 | "scale" : "2x"
142 | },
143 | {
144 | "idiom" : "ios-marketing",
145 | "size" : "1024x1024",
146 | "scale" : "1x"
147 | }
148 | ],
149 | "info" : {
150 | "version" : 1,
151 | "author" : "xcode"
152 | }
153 | }
--------------------------------------------------------------------------------
/Docs/new-features-3.0.md:
--------------------------------------------------------------------------------
1 | ## SDK 3.0 New Features
2 | Voice iOS 3.0 has a number of new features listed below:
3 |
4 | 1. [WebRTC](#webrtc)
5 | 2. [Custom Parameters](#custom-parameters)
6 | 3. [Call Ringing APIs](#call-ringing-apis)
7 | 4. [Media Stats](#media-stats)
8 | 5. [Audio Device APIs](#audio-device-apis)
9 | * [Default Audio Device](#default-audio-device)
10 | * [Custom Audio Device](#custom-audio-device)
11 | 6. [Preferred Audio Codec](#preferred-audio-codec)
12 |
13 | #### WebRTC
14 | The SDK is built using Chromium WebRTC for iOS. This ensures that over time developers will get the best real-time media streaming capabilities available for iOS. Additionally, upgrades to new versions of Chromium WebRTC will happen without changing the public API whenever possible.
15 |
16 | #### Custom Parameters
17 | Custom Parameters is now supported in `TVOCallInvite`. `TVOCallInvite.customParamaters` returns a `NSDictionary` of custom parameters sent from the caller side to the callee.
18 |
19 | Pass custom parameters in TwiML:
20 |
21 | ```.xml
22 |
23 |
24 |
25 |
26 | bob
27 |
28 |
29 |
30 |
31 |
32 | ```
33 |
34 | `callInvite.customParameters` returns a dictionary of key-value pairs passed in the TwiML:
35 |
36 | ```.objc
37 | {
38 | "caller_first_name" = "alice";
39 | "caller_last_name" = "smith";
40 | }
41 | ```
42 |
43 | #### Call Ringing APIs
44 | Ringing is now provided as a call state. The delegate method `callDidStartRinging:` corresponding to this state transition is called once before the `callDidConnect:` method when the callee is being alerted of a Call. The behavior of this callback is determined by the `answerOnBridge` flag provided in the `Dial` verb of your TwiML application associated with this client. If the `answerOnBridge` flag is `false`, which is the default, the `callDidConnect:` callback will be called immediately after `callDidStartRinging:`. If the `answerOnBridge` flag is `true`, this will cause the `callDidConnect:` method being called only after the Call is answered. See [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) for more details on how to use it with the `Dial` TwiML verb. If the TwiML response contains a `Say` verb, then the `callDidConnect:` method will be called immediately after `callDidStartRinging:` is called, irrespective of the value of `answerOnBridge` being set to `true` or `false`.
45 |
46 | These changes are added as follows:
47 |
48 | ```.objc
49 | // TVOCall.h
50 | typedef NS_ENUM(NSUInteger, TVOCallState) {
51 | TVOCallStateConnecting = 0, ///< The Call is connecting.
52 | TVOCallStateRinging, ///< The Call is ringing.
53 | TVOCallStateConnected, ///< The Call is connected.
54 | TVOCallStateDisconnected ///< The Call is disconnected.
55 | };
56 |
57 | // TVOCallDelegate.h
58 | @protocol TVOCallDelegate
59 |
60 | @optional
61 | - (void)callDidStartRinging:(nonnull TVOCall *)call;
62 |
63 | @end
64 | ```
65 |
66 | #### Media Stats
67 | In Voice iOS 3.0 SDK you can now access media stats in a Call using the `[TVOCall getStatsWithBlock:]` method.
68 |
69 | ```.objc
70 | [call getStatsWithBlock:^(NSArray *statsReports) {
71 | for (TVOStatsReport *report in statsReports) {
72 | NSArray *localAudioTracks = report.localAudioTrackStats;
73 | TVOLocalAudioTrackStats *localAudioTrackStats = localAudioTracks[0];
74 | NSArray *remoteAudioTracks = report.remoteAudioTrackStats;
75 | TVORemoteAudioTrackStats *remoteAudioTrackStats = remoteAudioTracks[0];
76 |
77 | NSLog(@"Local Audio Track - audio level: %lu, packets sent: %lu", localAudioTrackStats.audioLevel, localAudioTrackStats.packetsSent);
78 | NSLog(@"Remote Audio Track - audio level: %lu, packets received: %lu", remoteAudioTrackStats.audioLevel, remoteAudioTrackStats.packetsReceived);
79 | }
80 | }
81 | ```
82 | ### Audio Device APIs
83 |
84 | #### Default Audio Device
85 | In Voice iOS 3.0 SDK, `TVODefaultAudioDevice` is used as the default device for rendering and capturing audio.
86 |
87 | An example of using `TVODefaultAudioDevice` to change the audio route from receiver to the speaker in a live call:
88 |
89 | ```.objc
90 | // The Voice SDK uses TVODefaultAudioDevice by default.
91 | // ... connect to a Call. The `TVODefaultAudioDevice` is configured to route audio to the receiver by default.
92 |
93 | TVODefaultAudioDevice *audioDevice = (TVODefaultAudioDevice *)TwilioVoice.audioDevice;
94 |
95 | audioDevice.block = ^ {
96 | // We will execute `kDefaultAVAudioSessionConfigurationBlock` first.
97 | kDefaultAVAudioSessionConfigurationBlock();
98 |
99 | if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]) {
100 | NSLog(@"AVAudiosession overrideOutputAudioPort %@",error);
101 | }
102 | };
103 | audioDevice.block();
104 | ```
105 |
106 | #### Custom Audio Device
107 | The `TVOAudioDevice` protocol gives you the ability to replace `TVODefaultAudioDevice`. By implementing the `TVOAudioDevice` protocol, you can write your own audio capturer to feed audio samples to the Voice SDK and an audio renderer to receive the remote audio samples. For example, you could integrate with `ReplayKit2` and capture application audio for broadcast or play music using `AVAssetReader`.
108 |
109 | Connecting to a Call using the `AVAudioSessionCategoryPlayback` category:
110 |
111 | ```.objc
112 | id audioDevice = [TVODefaultAudioDevice audioDeviceWithBlock:^ {
113 |
114 | // Execute the `kDefaultAVAudioSessionConfigurationBlock` first.
115 | kDefaultAVAudioSessionConfigurationBlock();
116 |
117 | // Overwrite the category to `playback`
118 | AVAudioSession *session = [AVAudioSession sharedInstance];
119 | NSError *error = nil;
120 | if (![session setCategory:AVAudioSessionCategoryPlayback
121 | mode:AVAudioSessionModeVoiceChat
122 | options:AVAudioSessionCategoryOptionAllowBluetooth
123 | error:&error]) {
124 | NSLog(@"AVAudioSession setCategory:options:mode:error: %@",error);
125 | }
126 | }];
127 |
128 | TwilioVoice.audioDevice = audioDevice;
129 |
130 | TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self];
131 | ```
132 |
133 | ### Preferred Audio Codec
134 | You can provide your preferred audio codecs in the `TVOConnectOptions` and the `TVOAcceptOptions`. Opus is the default codec used by the mobile infrastructure. To use PCMU as the negotiated audio codec instead of Opus you can add it as the first codec in the `preferredAudioCodecs` list.
135 |
136 | ```.objc
137 | #import "TVOAudioCodec.h"
138 |
139 | TVOConnectOptions *options = [TVOConnectOptions optionsWithAccessToken:accessToken
140 | block:^(TVOConnectOptionsBuilder *builder) {
141 | builder.preferredAudioCodecs = @[ [TVOPcmuCodec new], [TVOOpusCodec new] ];
142 | }];
143 |
144 | self.call = [TwilioVoice connectWithOptions:options delegate:self];
145 | ```
146 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/Docs/migration-guide-2.x-3.x.md:
--------------------------------------------------------------------------------
1 | ## 2.x to 3.x Migration Guide
2 | This section describes API or behavioral changes when upgrading from Voice iOS 2.X to Voice iOS 3.X. Each section provides code snippets to assist in transitioning to the new API.
3 |
4 | 1. [Making a Call](#making-a-call)
5 | 2. [TVOCallInvite Changes](#tvocallinvite-changes)
6 | 3. [Specifying a Media Region](#specifying-a-media-region)
7 | 4. [TVOConnectOptions & TVOAcceptOptions](#tvoconnectoptions-and-tvoacceptoptions)
8 | 5. [Media Establishment & Connectivity](#media-establishment-and-connectivity)
9 | 6. [CallKit](#callkit)
10 | 7. [Microphone Permission](#microphone-permission)
11 |
12 | #### Making a Call
13 | In Voice iOS 3.X, the API to make a call has changed from `[TwilioVoice call:params:delegate:]` to `[TwilioVoice connectWithAccessToken:delegate]` or `[TwilioVoice connectWithOptions:delegate:]`.
14 |
15 | ```.objc
16 | TVOCall *call = [TwilioVoice connectWithAccessToken:token delegate:self];
17 | ```
18 |
19 | #### TVOCallInvite Changes
20 | In Voice iOS 3.X, the `notificationError:` delegate method is removed from the `TVONotificationDelegate` protocol and the `[TwilioVoice handleNotification:]` method no longer raises errors via this method if an invalid notification is provided, instead a `BOOL` value is returned when `[TwilioVoice handleNotification:]` is called. The returned value is `YES` when the provided data resulted in a `TVOCallInvite` or `TVOCancelledCallInvite` received in the `TVONotificationDelegate` methods. If `NO` is returned it means the data provided was not a Twilio Voice push notification.
21 |
22 | ```.objc
23 | - (void)pushRegistry:(PKPushRegistry *)registry
24 | didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
25 | forType:(NSString *)type {
26 | if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self]) {
27 | // The push notification was not a Twilio Voice push notification.
28 | }
29 | }
30 |
31 | #pragma mark - TVONotificationDelegate
32 | - (void)callInviteReceived:(TVOCallInvite *)callInvite {
33 | // Show notification to answer or reject call
34 | }
35 |
36 | - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite {
37 | // Hide notification
38 | }
39 | ```
40 |
41 | The `TVOCallInvite` has an `accept()` and `reject()` method. `TVOCallInviteState` has been removed from the `TVOCallInvite` in favor of distinguishing between call invites and call invite cancellations with discrete stateless objects. While the `TVOCancelledCallInvite` simply provides the `to`, `from`, and `callSid` fields also available in the `TVOCallInvite`. The property `callSid` can be used to associate a `TVOCallInvite` with a `TVOCancelledCallInvite`.
42 |
43 | In Voice iOS 2.X passing a `cancel` notification into `[TwilioVoice handleNotification:delegate:]` would not raise a callback in the following two cases:
44 |
45 | - This callee accepted the call
46 | - This callee rejected the call
47 |
48 | However, in Voice iOS 3.X passing a `cancel` notification payload into `[TwilioVoice handleNotification:delegate:]` will always result in a callback. A callback is raised whenever a valid notification is provided to `[TwilioVoice handleNotification:delegate:]`.
49 |
50 | Note that Twilio will send a `cancel` notification to every registered device of the identity that accepts or rejects a call, even the device that accepted or rejected the call.
51 |
52 | #### Specifying a media region
53 | Previously, a media region could be specified via `[TwilioVoice setRegion:]`. Now this configuration can be provided as part of `TVOConnectOptions` or `TVOAcceptOptions` as shown below:
54 |
55 | ```.objc
56 | TVOConnectOptions *options = [TVOConnectOptions optionsWithAccessToken:accessToken
57 | block:^(TVOConnectOptionsBuilder *builder) {
58 | builder.region = region;
59 | }];
60 |
61 | TVOAcceptOptions *options = [TVOAcceptOptions optionsWithCallInvite:callInvite
62 | block:^(TVOAcceptOptionsBuilder *builder) {
63 | builder.region = region;
64 | }];
65 | ```
66 |
67 | #### TVOConnectOptions & TVOAcceptOptions
68 | To support configurability upon making or accepting a call, new classes have been added. Create a `TVOConnectOptions` object and make configurations via the `TVOConnectOptionsBuilder` in the `block`. Once `TVOConnectOptions` is created it can be provided when connecting a Call as shown below:
69 |
70 | ```.objc
71 | TVOConnectOptions *options = [TVOConnectOptions optionsWithAccessToken:accessToken
72 | block:^(TVOConnectOptionsBuilder *builder) {
73 | builder.params = params;
74 | }];
75 |
76 | self.call = [TwilioVoice connectWithOptions:options delegate:self];
77 | ```
78 |
79 | A `TVOCallInvite` can also be accepted using `TVOAcceptOptions` as shown below:
80 |
81 | ```.objc
82 | TVOAcceptOptions *options = [TVOAcceptOptions optionsWithCallInvite:callInvite
83 | block:^(TVOAcceptOptionsBuilder *builder) {
84 | builder.region = region;
85 | }];
86 |
87 | self.call = [callInvite acceptWithOptions:options delegate:self];
88 | ```
89 |
90 | #### Media Establishment & Connectivity
91 | The Voice iOS 3.X SDK uses WebRTC. The exchange of real-time media requires the use of Interactive Connectivity Establishment(ICE) to establish a media connection between the client and the media server. In some network environments where network access is restricted it may be necessary to provide ICE servers to establish a media connection. We reccomend using the [Network Traversal Service (NTS)](https://www.twilio.com/stun-turn) to obtain ICE servers. ICE servers can be provided when making or accepting a call by passing them into `TVOConnectOptions` or `TVOAcceptOptions` in the following way:
92 |
93 | ```.objc
94 | TVOIceOptions *iceOptions;
95 |
96 | NSMutableArray *iceServers = [NSMutableArray array];
97 | TVOIceServer *iceServer1 = [[TVOIceServer alloc] initWithURLString:@"stun:global.stun.twilio.com:3478?transport=udp"
98 | username:@""
99 | password:@""];
100 | [iceServers addObject:iceServer];
101 |
102 | TVOIceServer *iceServer2 = [[TVOIceServer alloc] initWithURLString:@"turn:global.turn.twilio.com:3478?transport=udp"
103 | username:@"TURN_USERNAME"
104 | password:@"TURN_PASSWORD"];
105 | [iceServers addObject:iceServer2];
106 |
107 | iceOptions = [TVOIceOptions optionsWithBlock:^(TVOIceOptionsBuilder *builder) {
108 | builder.servers = iceServers;
109 | }];
110 |
111 | // Specify ICE options in the builder
112 | TVOConnectOptions *options = [TVOConnectOptions optionsWithAccessToken:accessToken
113 | block:^(TVOConnectOptionsBuilder *builder) {
114 | builder.iceOptions = iceOptions;
115 | }];
116 |
117 | TVOAcceptOptions *options = [TVOAcceptOptions optionsWithCallInvite:callInvite
118 | block:^(TVOAcceptOptionsBuilder *builder) {
119 | builder.iceOptions = iceOptions;
120 | }];
121 | ```
122 |
123 | #### CallKit
124 | The Voice iOS 3.X SDK deprecates the `CallKitIntegration` category from `TwilioVoice` in favor of a new property called `TVODefaultAudioDevice.enabled`. This property provides developers with a mechanism to enable or disable the activation of the audio device prior to connecting to a Call or to stop or start the audio device while you are already connected to a Call. A Call can now be connected without activating the audio device by setting `TVODefaultAudioDevice.enabled` to `NO` and can be enabled during the lifecycle of the Call by setting `TVODefaultAudioDevice.enabled` to `YES`. The default value is `YES`. This API change was made to ensure full compatibility with CallKit as well as supporting other use cases where developers may need to disable the audio device during a call.
125 |
126 | An example of managing the `TVODefaultAudioDevice` while connecting a CallKit Call:
127 |
128 | ```.objc
129 | @property (nonatomic, strong) TVODefaultAudioDevice *audioDevice;
130 |
131 | - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action {
132 | self.audioDevice.enabled = NO;
133 | self.audioDevice.block();
134 |
135 | [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]];
136 |
137 | __weak typeof(self) weakSelf = self;
138 | [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) {
139 | __strong typeof(self) strongSelf = weakSelf;
140 | if (success) {
141 | [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]];
142 | [action fulfill];
143 | } else {
144 | [action fail];
145 | }
146 | }];
147 | }
148 |
149 | - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession {
150 | self.audioDevice.enabled = YES;
151 | }
152 |
153 | - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
154 | self.audioDevice.enabled = NO;
155 | self.audioDevice.block();
156 |
157 | [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) {
158 | if (success) {
159 | [action fulfill];
160 | } else {
161 | [action fail];
162 | }
163 | }];
164 |
165 | [action fulfill];
166 | }
167 |
168 | - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
169 | // Disconnect or reject the call
170 |
171 | self.audioDevice.enabled = YES;
172 | [action fulfill];
173 | }
174 | ```
175 |
176 | See [CallKit Example](https://github.com/twilio/voice-quickstart-objc/blob/3.x/ObjCVoiceQuickstart/ViewController.m) for the complete implementation.
177 |
178 | #### Microphone Permission
179 | Unlike Voice iOS 2.X SDKs where microphone permission is not optional in Voice 3.X SDKs, the call will connect even when the microphone permission is denied or disabled by the user, and the SDK will play the remote audio. To ensure the microphone permission is enabled prior to making or accepting a call you can add the following to request the permission beforehand:
180 |
181 | ```
182 | - (void)makeCall {
183 | // User's microphone option
184 | BOOL microphoneEnabled = YES;
185 |
186 | if (microphoneEnabled) {
187 | [self checkRecordPermission:^(BOOL permissionGranted) {
188 | if (!permissionGranted) {
189 | // The user might want to revisit the Privacy settings.
190 | } else {
191 | // Permission granted. Continue to make call.
192 | }
193 | }];
194 | } else {
195 | // Continue to make call without microphone.
196 | }
197 | }
198 |
199 | - (void)checkRecordPermission:(void(^)(BOOL permissionGranted))completion {
200 | AVAudioSessionRecordPermission permissionStatus = [[AVAudioSession sharedInstance] recordPermission];
201 | switch (permissionStatus) {
202 | case AVAudioSessionRecordPermissionGranted:
203 | // Record permission already granted.
204 | completion(YES);
205 | break;
206 | case AVAudioSessionRecordPermissionDenied:
207 | // Record permission denied.
208 | completion(NO);
209 | break;
210 | case AVAudioSessionRecordPermissionUndetermined:
211 | {
212 | // Requesting record permission.
213 | // Optional: pop up app dialog to let the users know if they want to request.
214 | [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
215 | completion(granted);
216 | }];
217 | break;
218 | }
219 | default:
220 | completion(NO);
221 | break;
222 | }
223 | }
224 | ```
225 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2459FDCB1D83664300917CA9 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2459FDCA1D83664300917CA9 /* main.m */; };
11 | 2459FDCE1D83664300917CA9 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2459FDCD1D83664300917CA9 /* AppDelegate.m */; };
12 | 2459FDD11D83664300917CA9 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2459FDD01D83664300917CA9 /* ViewController.m */; };
13 | 2459FDD41D83664300917CA9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2459FDD21D83664300917CA9 /* Main.storyboard */; };
14 | 2459FDD61D83664300917CA9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2459FDD51D83664300917CA9 /* Assets.xcassets */; };
15 | 2459FDD91D83664300917CA9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2459FDD71D83664300917CA9 /* LaunchScreen.storyboard */; };
16 | 24BA34291D87431000B0B4F7 /* TwilioLogo.png in Resources */ = {isa = PBXBuildFile; fileRef = 24BA34281D87431000B0B4F7 /* TwilioLogo.png */; };
17 | 8F6BABF623973FE80030BE4F /* ringtone.wav in Resources */ = {isa = PBXBuildFile; fileRef = 8F6BABF523973FE80030BE4F /* ringtone.wav */; };
18 | /* End PBXBuildFile section */
19 |
20 | /* Begin PBXFileReference section */
21 | 2459FDC61D83664300917CA9 /* ObjCVoiceQuickstart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ObjCVoiceQuickstart.app; sourceTree = BUILT_PRODUCTS_DIR; };
22 | 2459FDCA1D83664300917CA9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
23 | 2459FDCC1D83664300917CA9 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
24 | 2459FDCD1D83664300917CA9 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
25 | 2459FDCF1D83664300917CA9 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; };
26 | 2459FDD01D83664300917CA9 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; };
27 | 2459FDD31D83664300917CA9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
28 | 2459FDD51D83664300917CA9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
29 | 2459FDD81D83664300917CA9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
30 | 2459FDDA1D83664300917CA9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
31 | 24BA34241D87269500B0B4F7 /* ObjCVoiceQuickstart.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ObjCVoiceQuickstart.entitlements; sourceTree = ""; };
32 | 24BA34281D87431000B0B4F7 /* TwilioLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TwilioLogo.png; sourceTree = ""; };
33 | 8F6BABF523973FE80030BE4F /* ringtone.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ringtone.wav; sourceTree = ""; };
34 | /* End PBXFileReference section */
35 |
36 | /* Begin PBXFrameworksBuildPhase section */
37 | 2459FDC31D83664300917CA9 /* Frameworks */ = {
38 | isa = PBXFrameworksBuildPhase;
39 | buildActionMask = 2147483647;
40 | files = (
41 | );
42 | runOnlyForDeploymentPostprocessing = 0;
43 | };
44 | /* End PBXFrameworksBuildPhase section */
45 |
46 | /* Begin PBXGroup section */
47 | 2459FDBD1D83664300917CA9 = {
48 | isa = PBXGroup;
49 | children = (
50 | 2459FDC81D83664300917CA9 /* ObjCVoiceQuickstart */,
51 | 2459FDC71D83664300917CA9 /* Products */,
52 | 5FF12A4D246637936B670360 /* Frameworks */,
53 | 8F6BABF423973FE80030BE4F /* ringtone */,
54 | );
55 | sourceTree = "";
56 | };
57 | 2459FDC71D83664300917CA9 /* Products */ = {
58 | isa = PBXGroup;
59 | children = (
60 | 2459FDC61D83664300917CA9 /* ObjCVoiceQuickstart.app */,
61 | );
62 | name = Products;
63 | sourceTree = "";
64 | };
65 | 2459FDC81D83664300917CA9 /* ObjCVoiceQuickstart */ = {
66 | isa = PBXGroup;
67 | children = (
68 | 24BA34241D87269500B0B4F7 /* ObjCVoiceQuickstart.entitlements */,
69 | 2459FDCC1D83664300917CA9 /* AppDelegate.h */,
70 | 2459FDCD1D83664300917CA9 /* AppDelegate.m */,
71 | 2459FDCF1D83664300917CA9 /* ViewController.h */,
72 | 2459FDD01D83664300917CA9 /* ViewController.m */,
73 | 2459FDD21D83664300917CA9 /* Main.storyboard */,
74 | 2459FDD51D83664300917CA9 /* Assets.xcassets */,
75 | 24BA34281D87431000B0B4F7 /* TwilioLogo.png */,
76 | 2459FDD71D83664300917CA9 /* LaunchScreen.storyboard */,
77 | 2459FDDA1D83664300917CA9 /* Info.plist */,
78 | 2459FDC91D83664300917CA9 /* Supporting Files */,
79 | );
80 | path = ObjCVoiceQuickstart;
81 | sourceTree = "";
82 | };
83 | 2459FDC91D83664300917CA9 /* Supporting Files */ = {
84 | isa = PBXGroup;
85 | children = (
86 | 2459FDCA1D83664300917CA9 /* main.m */,
87 | );
88 | name = "Supporting Files";
89 | sourceTree = "";
90 | };
91 | 5FF12A4D246637936B670360 /* Frameworks */ = {
92 | isa = PBXGroup;
93 | children = (
94 | );
95 | name = Frameworks;
96 | sourceTree = "";
97 | };
98 | 8F6BABF423973FE80030BE4F /* ringtone */ = {
99 | isa = PBXGroup;
100 | children = (
101 | 8F6BABF523973FE80030BE4F /* ringtone.wav */,
102 | );
103 | path = ringtone;
104 | sourceTree = "";
105 | };
106 | /* End PBXGroup section */
107 |
108 | /* Begin PBXNativeTarget section */
109 | 2459FDC51D83664300917CA9 /* ObjCVoiceQuickstart */ = {
110 | isa = PBXNativeTarget;
111 | buildConfigurationList = 2459FDDD1D83664300917CA9 /* Build configuration list for PBXNativeTarget "ObjCVoiceQuickstart" */;
112 | buildPhases = (
113 | 2459FDC21D83664300917CA9 /* Sources */,
114 | 2459FDC31D83664300917CA9 /* Frameworks */,
115 | 2459FDC41D83664300917CA9 /* Resources */,
116 | );
117 | buildRules = (
118 | );
119 | dependencies = (
120 | );
121 | name = ObjCVoiceQuickstart;
122 | productName = ObjCVoiceQuickstart;
123 | productReference = 2459FDC61D83664300917CA9 /* ObjCVoiceQuickstart.app */;
124 | productType = "com.apple.product-type.application";
125 | };
126 | /* End PBXNativeTarget section */
127 |
128 | /* Begin PBXProject section */
129 | 2459FDBE1D83664300917CA9 /* Project object */ = {
130 | isa = PBXProject;
131 | attributes = {
132 | LastUpgradeCheck = 0800;
133 | ORGANIZATIONNAME = "Twilio, Inc.";
134 | TargetAttributes = {
135 | 2459FDC51D83664300917CA9 = {
136 | CreatedOnToolsVersion = 8.0;
137 | ProvisioningStyle = Automatic;
138 | SystemCapabilities = {
139 | com.apple.BackgroundModes = {
140 | enabled = 1;
141 | };
142 | com.apple.Push = {
143 | enabled = 1;
144 | };
145 | };
146 | };
147 | };
148 | };
149 | buildConfigurationList = 2459FDC11D83664300917CA9 /* Build configuration list for PBXProject "ObjCVoiceQuickstart" */;
150 | compatibilityVersion = "Xcode 3.2";
151 | developmentRegion = English;
152 | hasScannedForEncodings = 0;
153 | knownRegions = (
154 | en,
155 | Base,
156 | );
157 | mainGroup = 2459FDBD1D83664300917CA9;
158 | productRefGroup = 2459FDC71D83664300917CA9 /* Products */;
159 | projectDirPath = "";
160 | projectRoot = "";
161 | targets = (
162 | 2459FDC51D83664300917CA9 /* ObjCVoiceQuickstart */,
163 | );
164 | };
165 | /* End PBXProject section */
166 |
167 | /* Begin PBXResourcesBuildPhase section */
168 | 2459FDC41D83664300917CA9 /* Resources */ = {
169 | isa = PBXResourcesBuildPhase;
170 | buildActionMask = 2147483647;
171 | files = (
172 | 2459FDD91D83664300917CA9 /* LaunchScreen.storyboard in Resources */,
173 | 2459FDD61D83664300917CA9 /* Assets.xcassets in Resources */,
174 | 24BA34291D87431000B0B4F7 /* TwilioLogo.png in Resources */,
175 | 2459FDD41D83664300917CA9 /* Main.storyboard in Resources */,
176 | );
177 | runOnlyForDeploymentPostprocessing = 0;
178 | };
179 | /* End PBXResourcesBuildPhase section */
180 |
181 | /* Begin PBXSourcesBuildPhase section */
182 | 2459FDC21D83664300917CA9 /* Sources */ = {
183 | isa = PBXSourcesBuildPhase;
184 | buildActionMask = 2147483647;
185 | files = (
186 | 2459FDD11D83664300917CA9 /* ViewController.m in Sources */,
187 | 2459FDCE1D83664300917CA9 /* AppDelegate.m in Sources */,
188 | 2459FDCB1D83664300917CA9 /* main.m in Sources */,
189 | );
190 | runOnlyForDeploymentPostprocessing = 0;
191 | };
192 | /* End PBXSourcesBuildPhase section */
193 |
194 | /* Begin PBXVariantGroup section */
195 | 2459FDD21D83664300917CA9 /* Main.storyboard */ = {
196 | isa = PBXVariantGroup;
197 | children = (
198 | 2459FDD31D83664300917CA9 /* Base */,
199 | );
200 | name = Main.storyboard;
201 | sourceTree = "";
202 | };
203 | 2459FDD71D83664300917CA9 /* LaunchScreen.storyboard */ = {
204 | isa = PBXVariantGroup;
205 | children = (
206 | 2459FDD81D83664300917CA9 /* Base */,
207 | );
208 | name = LaunchScreen.storyboard;
209 | sourceTree = "";
210 | };
211 | /* End PBXVariantGroup section */
212 |
213 | /* Begin XCBuildConfiguration section */
214 | 2459FDDB1D83664300917CA9 /* Debug */ = {
215 | isa = XCBuildConfiguration;
216 | buildSettings = {
217 | ALWAYS_SEARCH_USER_PATHS = NO;
218 | CLANG_ANALYZER_NONNULL = YES;
219 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
220 | CLANG_CXX_LIBRARY = "libc++";
221 | CLANG_ENABLE_MODULES = YES;
222 | CLANG_ENABLE_OBJC_ARC = YES;
223 | CLANG_WARN_BOOL_CONVERSION = YES;
224 | CLANG_WARN_CONSTANT_CONVERSION = YES;
225 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
226 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
227 | CLANG_WARN_EMPTY_BODY = YES;
228 | CLANG_WARN_ENUM_CONVERSION = YES;
229 | CLANG_WARN_INFINITE_RECURSION = YES;
230 | CLANG_WARN_INT_CONVERSION = YES;
231 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
232 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
233 | CLANG_WARN_UNREACHABLE_CODE = YES;
234 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
235 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
236 | COPY_PHASE_STRIP = NO;
237 | DEBUG_INFORMATION_FORMAT = dwarf;
238 | ENABLE_STRICT_OBJC_MSGSEND = YES;
239 | ENABLE_TESTABILITY = YES;
240 | GCC_C_LANGUAGE_STANDARD = gnu99;
241 | GCC_DYNAMIC_NO_PIC = NO;
242 | GCC_NO_COMMON_BLOCKS = YES;
243 | GCC_OPTIMIZATION_LEVEL = 0;
244 | GCC_PREPROCESSOR_DEFINITIONS = (
245 | "DEBUG=1",
246 | "$(inherited)",
247 | );
248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
249 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
250 | GCC_WARN_UNDECLARED_SELECTOR = YES;
251 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
252 | GCC_WARN_UNUSED_FUNCTION = YES;
253 | GCC_WARN_UNUSED_VARIABLE = YES;
254 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
255 | MTL_ENABLE_DEBUG_INFO = YES;
256 | ONLY_ACTIVE_ARCH = YES;
257 | SDKROOT = iphoneos;
258 | TARGETED_DEVICE_FAMILY = "1,2";
259 | };
260 | name = Debug;
261 | };
262 | 2459FDDC1D83664300917CA9 /* Release */ = {
263 | isa = XCBuildConfiguration;
264 | buildSettings = {
265 | ALWAYS_SEARCH_USER_PATHS = NO;
266 | CLANG_ANALYZER_NONNULL = YES;
267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
268 | CLANG_CXX_LIBRARY = "libc++";
269 | CLANG_ENABLE_MODULES = YES;
270 | CLANG_ENABLE_OBJC_ARC = YES;
271 | CLANG_WARN_BOOL_CONVERSION = YES;
272 | CLANG_WARN_CONSTANT_CONVERSION = YES;
273 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
274 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
275 | CLANG_WARN_EMPTY_BODY = YES;
276 | CLANG_WARN_ENUM_CONVERSION = YES;
277 | CLANG_WARN_INFINITE_RECURSION = YES;
278 | CLANG_WARN_INT_CONVERSION = YES;
279 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
280 | CLANG_WARN_SUSPICIOUS_MOVES = YES;
281 | CLANG_WARN_UNREACHABLE_CODE = YES;
282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
283 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
284 | COPY_PHASE_STRIP = NO;
285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
286 | ENABLE_NS_ASSERTIONS = NO;
287 | ENABLE_STRICT_OBJC_MSGSEND = YES;
288 | GCC_C_LANGUAGE_STANDARD = gnu99;
289 | GCC_NO_COMMON_BLOCKS = YES;
290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
292 | GCC_WARN_UNDECLARED_SELECTOR = YES;
293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
294 | GCC_WARN_UNUSED_FUNCTION = YES;
295 | GCC_WARN_UNUSED_VARIABLE = YES;
296 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
297 | MTL_ENABLE_DEBUG_INFO = NO;
298 | SDKROOT = iphoneos;
299 | TARGETED_DEVICE_FAMILY = "1,2";
300 | VALIDATE_PRODUCT = YES;
301 | };
302 | name = Release;
303 | };
304 | 2459FDDE1D83664300917CA9 /* Debug */ = {
305 | isa = XCBuildConfiguration;
306 | buildSettings = {
307 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
308 | CODE_SIGN_ENTITLEMENTS = ObjCVoiceQuickstart/ObjCVoiceQuickstart.entitlements;
309 | INFOPLIST_FILE = "$(SRCROOT)/ObjCVoiceQuickstart/Info.plist";
310 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
311 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
312 | PRODUCT_BUNDLE_IDENTIFIER = com.twilio.ObjCVoiceQuickstart;
313 | PRODUCT_NAME = "$(TARGET_NAME)";
314 | };
315 | name = Debug;
316 | };
317 | 2459FDDF1D83664300917CA9 /* Release */ = {
318 | isa = XCBuildConfiguration;
319 | buildSettings = {
320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
321 | CODE_SIGN_ENTITLEMENTS = ObjCVoiceQuickstart/ObjCVoiceQuickstart.entitlements;
322 | INFOPLIST_FILE = "$(SRCROOT)/ObjCVoiceQuickstart/Info.plist";
323 | IPHONEOS_DEPLOYMENT_TARGET = 10.0;
324 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
325 | PRODUCT_BUNDLE_IDENTIFIER = com.twilio.ObjCVoiceQuickstart;
326 | PRODUCT_NAME = "$(TARGET_NAME)";
327 | };
328 | name = Release;
329 | };
330 | /* End XCBuildConfiguration section */
331 |
332 | /* Begin XCConfigurationList section */
333 | 2459FDC11D83664300917CA9 /* Build configuration list for PBXProject "ObjCVoiceQuickstart" */ = {
334 | isa = XCConfigurationList;
335 | buildConfigurations = (
336 | 2459FDDB1D83664300917CA9 /* Debug */,
337 | 2459FDDC1D83664300917CA9 /* Release */,
338 | );
339 | defaultConfigurationIsVisible = 0;
340 | defaultConfigurationName = Release;
341 | };
342 | 2459FDDD1D83664300917CA9 /* Build configuration list for PBXNativeTarget "ObjCVoiceQuickstart" */ = {
343 | isa = XCConfigurationList;
344 | buildConfigurations = (
345 | 2459FDDE1D83664300917CA9 /* Debug */,
346 | 2459FDDF1D83664300917CA9 /* Release */,
347 | );
348 | defaultConfigurationIsVisible = 0;
349 | defaultConfigurationName = Release;
350 | };
351 | /* End XCConfigurationList section */
352 | };
353 | rootObject = 2459FDBE1D83664300917CA9 /* Project object */;
354 | }
355 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Twilio Voice Objective-C Quickstart for iOS
2 |
3 | > **Deprecation Notice - voice-quickstart-objc repository**
4 | >
5 | > This repository has been deprecated and will no longer be maintained. All Objective-C examples have been merged into the [voice-quickstart-ios](https://github.com/twilio/voice-quickstart-ios) repository and will continue to be maintained there.
6 |
7 | > NOTE: These sample applications use the Twilio Voice 5.x APIs. For examples using our 2.x APIs, please see the [2.x](https://github.com/twilio/voice-quickstart-objc/tree/2.x) branch. If you are using SDK 2.x, we highly recommend planning your migration to 5.0 as soon as possible. Support for 2.x will cease 1/1/2020. Until then, SDK 2.x will only receive fixes for critical or security related issues.
8 |
9 | > Please see our [iOS 13 Migration Guide](https://github.com/twilio/twilio-voice-ios/blob/Releases/iOS-13-Migration-Guide.md) for the latest information on iOS 13.
10 |
11 | ## Get started with Voice on iOS:
12 | * [Quickstart](#quickstart) - Run the quickstart app
13 | * [Migration Guide from 4.x to 5.x](https://github.com/twilio/twilio-voice-ios/blob/Releases/iOS-13-Migration-Guide.md) - Migrating from 4.x to 5.x
14 | * [4.0 New Features](https://github.com/twilio/voice-quickstart-objc/blob/master/Docs/new-features-4.0.md) - New features in 4.0
15 | * [Migration Guide from 3.x to 4.x](https://github.com/twilio/voice-quickstart-objc/blob/master/Docs/migration-guide-3.x-4.x.md) - Migrating from 3.x to 4.x
16 | * [3.0 New Features](https://github.com/twilio/voice-quickstart-objc/blob/master/Docs/new-features-3.0.md) - New features in 3.0
17 | * [Migration Guide from 2.x to 3.x](https://github.com/twilio/voice-quickstart-objc/blob/master/Docs/migration-guide-2.x-3.x.md) - Migrating from 2.X to 3.x
18 | * [Access Tokens](#access-tokens) - Using access tokens
19 | * [Managing Audio Interruptions](#managing-audio-interruptions) - Managing audio interruptions
20 | * [Managing Push Credentials](#managing-push-credentials) - Managing push credentials
21 | * [More Documentation](#more-documentation) - More documentation related to the Voice iOS SDK
22 | * [Issues and Support](#issues-and-support) - Filing issues and general support
23 |
24 | ## Quickstart
25 | To get started with the quickstart application follow these steps. Steps 1-6 will enable the application to make a call. The remaining steps 7-10 will enable the application to receive incoming calls in the form of push notifications using Apple’s VoIP Service.
26 |
27 | 1. [Install the TwilioVoice framework](#bullet1)
28 | 2. [Create a Voice API key](#bullet2)
29 | 3. [Configure a server to generate an access token to be used in the app](#bullet3)
30 | 4. [Create a TwiML application](#bullet4)
31 | 5. [Configure your application server](#bullet5)
32 | 6. [Run the app](#bullet6)
33 | 7. [Create a VoIP Service Certificate](#bullet7)
34 | 8. [Create a Push Credential with your VoIP Service Certificate](#bullet8)
35 | 9. [Configure Xcode project settings for VoIP push notifications](#bullet9)
36 | 10. [Receive an incoming call](#bullet10)
37 | 11. [Make client to client call](#bullet11)
38 | 12. [Make client to PSTN call](#bullet12)
39 |
40 | ### 1. Install the TwilioVoice framework
41 |
42 | **Carthage**
43 |
44 | Add the following line to your Cartfile
45 |
46 | ```
47 | github "twilio/twilio-voice-ios"
48 | ```
49 |
50 | Then run `carthage bootstrap` (or `carthage update` if you are updating your SDKs)
51 |
52 | On your application targets’ “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase”. Create a Run Script in which you specify your shell (ex: `/bin/sh`), add the following contents to the script area below the shell:
53 |
54 | ```
55 | /usr/local/bin/carthage copy-frameworks
56 | ```
57 |
58 | Add the paths to the frameworks you want to use under “Input Files”, e.g.:
59 |
60 | ```
61 | $(SRCROOT)/Carthage/Build/iOS/TwilioVoice.framework
62 | ```
63 |
64 |
65 |
66 | **Cocoapods**
67 |
68 | Under the quickstart path, run `pod install` and let the Cocoapods library create the workspace for you. Also please make sure to use **Cocoapods v1.0 and later**.
69 | Once Cocoapods finishes installing, open the `ObjCVoiceQuickstart.xcworkspace` and you will find a basic Objective-C quickstart project and a CallKit quickstart project.
70 |
71 | Note: You may need to update the [CocoaPods Master Spec Repo](https://github.com/CocoaPods/Specs) by running `pod repo update master` in order to fetch the latest specs for TwilioVoice.
72 |
73 | ### 2. Create a Voice API key
74 | Go to the [Voice API Keys](https://www.twilio.com/console/voice/settings/api-keys) page and create a new API key:
75 |
76 |
77 |
78 | **Save the generated `API_KEY` and `API_KEY_SECRET` in your notepad**. You will need them in the next step.
79 |
80 | ### 3. Configure a server to generate an access token to be used in the app
81 |
82 | Download one of the starter projects for the server.
83 |
84 | * [voice-quickstart-server-java](https://github.com/twilio/voice-quickstart-server-java)
85 | * [voice-quickstart-server-node](https://github.com/twilio/voice-quickstart-server-node)
86 | * [voice-quickstart-server-php](https://github.com/twilio/voice-quickstart-server-php)
87 | * [voice-quickstart-server-python](https://github.com/twilio/voice-quickstart-server-python)
88 |
89 | Follow the instructions in the server's README to get the application server up and running locally and accessible via the public Internet. For now just replace the **Twilio Account SID** that you can obtain from the [console](https://www.twilio.com/console), and the `API_KEY` and `API_SECRET` you obtained in the previous step.
90 |
91 | ACCOUNT_SID=AC12345678901234567890123456789012
92 | API_KEY=SK12345678901234567890123456789012
93 | API_KEY_SECRET=the_secret_generated_when_creating_the_api_key
94 |
95 | ### 4. Create a TwiML application
96 | Next, we need to create a TwiML application. A TwiML application identifies a public URL for retrieving [TwiML call control instructions](https://www.twilio.com/docs/api/twiml). When your iOS app makes a call to the Twilio cloud, Twilio will make a webhook request to this URL, your application server will respond with generated TwiML, and Twilio will execute the instructions you’ve provided.
97 | To create a TwiML application, go to the [TwiML app page](https://www.twilio.com/console/voice/twiml/apps). Create a new TwiML application, and use the public URL of your application server’s `/makeCall` endpoint as the Voice Request URL (If your app server is written in PHP, then you need `.php` extension at the end).
98 |
99 |
100 |
101 | As you can see we’ve used our [ngrok](https://ngrok.com/) public address in the Request URL field above.
102 | Save your TwiML Application configuration, and grab the **TwiML Application SID** (a long identifier beginning with the characters `AP`).
103 |
104 | ### 5. Configure your application server
105 | Let's put the remaining `APP_SID` configuration info into your server code
106 |
107 | ACCOUNT_SID=AC12345678901234567890123456789012
108 | API_KEY=SK12345678901234567890123456789012
109 | API_KEY_SECRET=the_secret_generated_when_creating_the_api_key
110 | APP_SID=AP12345678901234567890123456789012
111 |
112 | Once you’ve done that, restart the server so it uses the new configuration info. Now it's time to test.
113 |
114 | Open up a browser and visit the URL for your application server's **Access Token endpoint**: `https://{YOUR_SERVER_URL}/accessToken` (If your app server is written in PHP, then you need `.php` extension at the end). If everything is configured correctly, you should see a long string of letters and numbers, which is a Twilio Access Token. Your iOS app will use a token like this to connect to Twilio.
115 |
116 | ### 6. Run the app
117 | Now let’s go back to the `ObjCVoiceQuickstart.xcworkspace`. Update the placeholder of `kYourServerBaseURLString` with your ngrok public URL
118 |
119 | ```objc
120 | @import AVFoundation;
121 | @import PushKit;
122 | @import TwilioVoice;
123 |
124 | static NSString *const kYourServerBaseURLString = @"https://3b57e324.ngrok.io";
125 | static NSString *const kAccessTokenEndpoint = @"/accessToken";
126 | static NSString *const kIdentity = @"alice";
127 | static NSString *const kTwimlParamTo = @"to";
128 |
129 | @interface ViewController ()
130 | @property (nonatomic, strong) NSString *deviceTokenString;
131 | ```
132 |
133 | Build and run the app
134 |
135 |
136 |
137 | Leave the text field empty and press the call button to start a call. You will hear the congratulatory message. Support for dialing another client or number is described in steps 11 and 12. Tap "Hang Up" to disconnect.
138 |
139 |
140 |
141 | ### 7. Create VoIP Service Certificate
142 | The Programmable Voice SDK uses Apple’s VoIP Services to let your application know when it is receiving an incoming call. If you want your users to receive incoming calls, you’ll need to enable VoIP Services in your application and generate a VoIP Services Certificate.
143 |
144 | Go to [Apple Developer portal](https://developer.apple.com/) and you’ll need to do the following:
145 | - An Apple Developer membership to be able to create the certificate.
146 | - Make sure your App ID has the “Push Notifications” service enabled.
147 | - Create a corresponding Provisioning Profile for your app ID.
148 | - Create an [Apple VoIP Services Certificate](https://developer.apple.com/library/prerelease/content/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html#//apple_ref/doc/uid/TP40015243-CH30-SW1) for this app by navigating to Certificates --> Production and clicking the `+` on the top right to add the new certificate.
149 |
150 |
151 |
152 | ### 8. Create a Push Credential with your VoIP Service Certificate
153 | Once you have generated the VoIP Services Certificate using Keychain Access, you will need to upload it to Twilio so that Twilio can send push notifications to your app on your behalf.
154 |
155 | Export your VoIP Service Certificate as a .p12 file from Keychain Access, then extract the certificate and private key from the .p12 file using the `openssl` command. If .p12 is not an option for exporting, type `voip` into the search bar of Keychain Access and make sure you select both items when exporting the certificate.
156 |
157 | $> openssl pkcs12 -in PATH_TO_YOUR_P12 -nocerts -out key.pem
158 | $> openssl rsa -in key.pem -out key.pem
159 | $> openssl pkcs12 -in PATH_TO_YOUR_P12 -clcerts -nokeys -out cert.pem
160 |
161 | Go to the [Push Credentials page](https://www.twilio.com/console/voice/sdks/credentials) and create a new Push Credential. Paste the certificate and private key extracted from your certificate. You must paste the keys in as plaintext:
162 |
163 | * For the `cert.pem` you should paste everything from `-----BEGIN CERTIFICATE-----` to `-----END CERTIFICATE-----`.
164 | * For the `key.pem` you should paste everything from `-----BEGIN RSA PRIVATE KEY-----` to `-----END RSA PRIVATE KEY-----`.
165 |
166 | **Remember to check the “Sandbox” option**. This is important. The VoIP Service Certificate you generated can be used both in production and with Apple's sandbox infrastructure. Checking this box tells Twilio to send your pushes to the Apple sandbox infrastructure which is appropriate with your development provisioning profile.
167 |
168 | Once the app is ready for store submission, update the plist with “APS Environment: production” and create another Push Credential with the same VoIP Certificate but without checking the sandbox option.
169 |
170 |
171 |
172 | Now let's go back to your server code and update the Push Credential SID. The Push Credential SID will now be embedded in your access token.
173 |
174 | PUSH_CREDENTIAL_SID=CR12345678901234567890123456789012
175 |
176 | ### 9. Configure Xcode project settings for push notifications
177 | On the project’s Capabilities tab, enable “**Push Notifications**”.
178 | In Xcode 8 or earlier, enable both “**Voice over IP**” and “**Audio, AirPlay and Picture in Picture**” capabilities in the Background Modes
179 |
180 |
181 |
182 | In Xcode 9+, make sure that the “**Audio, AirPlay and Picture in Picture**” capability is enabled and a "**UIBackgroundModes**" dictionary with "**audio**" and "**voip**" is in the app's plist.
183 | ```
184 | UIBackgroundModes
185 |
186 | audio
187 | voip
188 |
189 | ```
190 |
191 | ### 10. Receive an incoming call
192 | You are now ready to receive incoming calls. Rebuild your app and hit your application server's **/placeCall** endpoint: `https://{YOUR_SERVER_URL}/placeCall` (If your app server is written in PHP, then you need `.php` extension at the end). This will trigger a Twilio REST API request that will make an inbound call to your mobile app. Once your app accepts the call, you should hear a congratulatory message.
193 |
194 |
195 |
196 | ### 11. Make client to client call
197 | To make client to client calls, you need the application running on two devices. To run the application on an additional device, make sure you use a different identity in your access token when registering the new device. For example, change `kIdentity` to `bob` and run the application
198 |
199 | ```objc
200 | static NSString *const kAccessTokenEndpoint = @"/accessToken";
201 | static NSString *const kIdentity = @"bob";
202 | static NSString *const kTwimlParamTo = @"to";
203 | ```
204 |
205 | Use the text field to specify the identity of the call receiver, then tap the “Call” button to make a call. The TwiML parameters used in `TwilioVoice.connect()` method should match the name used in the server.
206 |
207 |
208 |
209 | ### 12. Make client to PSTN call
210 | A verified phone number is one that you can use as your Caller ID when making outbound calls with Twilio. This number has not been ported into Twilio and you do not pay Twilio for this phone number.
211 |
212 | To make client to number calls, first get a valid Twilio number to your account via https://www.twilio.com/console/phone-numbers/verified. Update your server code and replace the caller number variable (`CALLER_NUMBER` or `callerNumber` depending on which server you chose) with the verified number. Restart the server so that it uses the new value. Voice Request URL of your TwiML application should point to the public URL of your application server’s `/makeCall` endpoint.
213 |
214 |
215 |
216 | ## Access Tokens
217 |
218 | The access token generated by your server component is a [jwt](jwt.io) that contains a `grant` for Programmable Voice, an `identity` that you specify, and a `time-to-live` that sets the lifetime of the generated access token. The default `time-to-live` is 1 hour and is configurable up to 24 hours using the Twilio helper libraries.
219 |
220 | ### Uses
221 |
222 | In the iOS SDK the access token is used for the following:
223 |
224 | 1. To make an outgoing call via `[TwilioVoice connectWithOptions:delegate:] `
225 | 2. To register or unregister for incoming notifications using VoIP Push Notifications via `[TwilioVoice registerWithAccessToken:deviceToken:completion:]` and `[TwilioVoice unregisterWithAccessToken:deviceToken:completion:]`. Once registered, incoming notifications are handled via a `TVOCallInvite` where you can choose to accept or reject the invite. When accepting the call an access token is not required. Internally the `TVOCallInvite` has its own access token that ensures it can connect to our infrastructure.
226 |
227 | ### Managing Expiry
228 |
229 | As mentioned above, an access token will eventually expire. If an access token has expired, our infrastructure will return error `TVOErrorAccessTokenExpired`/`20104` via `TVOCallDelegate` or a completion error when registering.
230 |
231 | There are number of techniques you can use to ensure that access token expiry is managed accordingly:
232 |
233 | - Always fetch a new access token from your access token server before making an outbound call.
234 | - Retain the access token until getting a `TVOErrorAccessTokenExpired`/`20104` error before fetching a new access token.
235 | - Retain the access token along with the timestamp of when it was requested so you can verify ahead of time whether the token has already expired based on the `time-to-live` being used by your server.
236 | - Prefetch the access token whenever the `UIApplication`, or `UIViewController` associated with an outgoing call is created.
237 |
238 |
239 | ## Managing Audio Interruptions
240 | Different versions of iOS deal with **AVAudioSession** interruptions sightly differently. This section documents how the Programmable Voice iOS SDK manages audio interruptions and resumes call audio after the interruption ends. There are currently some cases that the SDK cannot resume call audio automatically because iOS does not provide the necessary notifications to indicate that the interruption has ended.
241 |
242 | ### How Programmable Voice iOS SDK handles audio interruption
243 | - The `TVOCall` object registers itself as an observer of `AVAudioSessionInterruptionNotification` when it's created.
244 | - When the notification is fired and the interruption type is `AVAudioSessionInterruptionTypeBegan`, the `TVOCall` object automatically disables both the local and remote audio tracks.
245 | - When the SDK receives the notification with `AVAudioSessionInterruptionTypeEnded`, it re-enables the local and remote audio tracks and resumes the audio of active Calls.
246 | - We have noticed that on iOS 8 and 9, the interruption notification with `AVAudioSessionInterruptionTypeEnded` is not always fired therefore the SDK is not able to resume call audio automatically. This is a known issue and an alternative way is to use the `UIApplicationDidBecomeActiveNotification` and resume audio when the app is active again after the interruption.
247 |
248 | ### Notifications in different iOS versions
249 | Below is a table listing the system notifications received with different steps to trigger audio interruptions and resume during an active Voice SDK call. (Assume the app is in an active Voice SDK call)
250 |
251 | |Scenario|Notification of interruption-begins|Notification of interruption-ends|Call audio resumes?|Note|
252 | |---|---|---|---|---|
253 | |PSTN Interruption|
254 | |A.
PSTN interruption
Accept the PSTN incoming call
Remote party of PSTN call hangs up|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
255 | |B.
PSTN interruption
Accept the PSTN incoming call
Local party of PSTN call hangs up|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
256 | |C.
PSTN interruption
Reject PSTN|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
257 | |D.
PSTN interruption
Ignore PSTN|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
258 | |E.
PSTN interruption
Remote party of PSTN call hangs up before local party can answer|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
259 | |Other Types of Audio Interruption
(YouTube app as example)|
260 | |F.
Switch to YouTube app and play video
Stop the video
Switch back to Voice app|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|Interruption-ended notification is not fired on iOS 9.
Interruption-ended notification is not fired until few seconds after switching back to the Voice app on iOS 10/11.
The `AVAudioSessionInterruptionOptionShouldResume` flag is `false`.|
261 | |G.
Switch to YouTube app and play video
Switch back to Voice app without stopping the video|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:x: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|Interruption-ended notification is not fired on iOS 9.
Interruption-ended notification is not fired until few seconds after switching back to the Voice app on iOS 10/11.
The `AVAudioSessionInterruptionOptionShouldResume` flag is `false`.|
262 | |H.
Switch to YouTube app and play video
Double-press Home button and terminate YouTube app
Back to Voice app|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|:white_check_mark: iOS 9
:white_check_mark: iOS 10
:white_check_mark: iOS 11|Interruption-ended notification is not fired until the Voice app is back to the active state.
The `AVAudioSessionInterruptionOptionShouldResume` flag is `false`.|
263 |
264 | ### CallKit
265 | On iOS 10 and later, CallKit (if integrated) takes care of the interruption by providing a set of delegate methods so that the application can respond with proper audio device handling and state transitioning in order to ensure call audio works after the interruption has ended.
266 |
267 | #### Notifications & Callbacks during Interruption
268 | By enabling the `supportsHolding` flag of the `CXCallUpdate` object when reporting a call to the CallKit framework, you will see the **“Hold & Accept”** option when there is another PSTN or CallKit-enabled call. By pressing the **“Hold & Accept”** option, a series of things and callbacks will happen:
269 |
270 | 1. The `provider:performSetHeldCallAction:` delegate method is called with `CXSetHeldCallAction.isOnHold = YES`. Put the Voice call on-hold here and fulfill the action.
271 | 2. The `AVAudioSessionInterruptionNotification` notification is fired to indicate the AVAudioSession interruption has started.
272 | 3. CallKit will deactivate the AVAudioSession of your app and fire the `provider:didDeactivateAudioSession:` callback.
273 | 4. When the interrupting call ends, instead of getting the `AVAudioSessionInterruptionNotification` notification, the system will notify that you can resume the call audio that was put on-hold when the interruption began by calling the `provider:performSetHeldCallAction:` method again. **Note** that this callback is not fired if the interrupting call is disconnected by the remote party.
274 | 5. The AVAudioSession of the app will be activated again and you should re-enable the audio device of the SDK `TwilioVoice.audioDevice.enabled = YES` in the `provider:didActivateAudioSession:` method.
275 |
276 | |Scenario|Audio resumes after interrupion?|Note|
277 | |---|---|---|
278 | |A.
Hold & Accept
Hang up PSTN interruption on the local end|:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
279 | |B.
Hold & Accept
Remote party hangs up PSTN interruption|:x: iOS 10
:x: iOS 11|`provider:performSetHeldCallAction:` not called after the interruption ends.|
280 | |C.
Hold & Accept
Switch back to the Voice Call on system UI|:white_check_mark: iOS 10
:white_check_mark: iOS 11| |
281 | |D.
Reject|:white_check_mark: iOS 10
:white_check_mark: iOS 11|No actual audio interruption happened since the interrupting call is not answered|
282 | |E.
Ignore|:white_check_mark: iOS 10
:white_check_mark: iOS 11|No actual audio interruption happened since the interrupting call is not answered|
283 |
284 | In case 2, CallKit does not automatically resume call audio by calling `provider:performSetHeldCallAction:` method, but the system UI will show that the Voice call is still on-hold. You can resume the call using the **"Hold"** button, or use the `CXSetHeldCallAction` to lift the on-hold state programmatically. The app is also responsible of updating UI state to indicate the "hold" state of the call to avoid user confusion.
285 |
286 | ```.objc
287 | // Resume call audio programmatically after interruption
288 | CXSetHeldCallAction *setHeldCallAction = [[CXSetHeldCallAction alloc] initWithCallUUID:self.call.uuid onHold:holdSwitch.on];
289 | CXTransaction *transaction = [[CXTransaction alloc] initWithAction:setHeldCallAction];
290 | [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
291 | if (error) {
292 | NSLog(@"Failed to submit set-call-held transaction request");
293 | } else {
294 | NSLog(@"Set-call-held transaction successfully done");
295 | }
296 | }];
297 | ```
298 |
299 | ## Managing Push Credentials
300 |
301 | A push credential is a record for a push notification channel, for iOS this push credential is a push notification channel record for APNS VoIP. Push credentials are managed in the console under [Mobile Push Credentials](https://www.twilio.com/console/voice/sdks/credentials).
302 |
303 | Whenever a registration is performed via `TwilioVoice.registerWithAccessToken:deviceToken:completion` in the iOS SDK the `identity` and the `Push Credential SID` provided in the JWT based access token, along with the `device token` are used as a unique address to send APNS VoIP push notifications to this application instance whenever a call is made to reach that `identity`. Using `TwilioVoice.unregisterWithAccessToken:deviceToken:completion` removes the association for that `identity`.
304 |
305 | ### Updating a Push Credential
306 |
307 | If you need to change or update your credentials provided by Apple you can do so by selecting the Push Credential in the [console](https://www.twilio.com/console/voice/sdks/credentials) and adding your new `certificate` and `private key` in the text box provided on the Push Credential page shown below:
308 |
309 |
310 |
311 | ### Deleting a Push Credential
312 |
313 | We **do not recommend that you delete a Push Credential** unless the application that it was created for is no longer being used.
314 |
315 | If **your APNS VoIP certificate is expiring soon or has expired you should not delete your Push Credential**, instead you should update the Push Credential by following the `Updating a Push Credential` section.
316 |
317 | When a Push Credential is deleted **any associated registrations made with this Push Credential will be deleted**. Future attempts to reach an `identity` that was registered using the Push Credential SID of this deleted push credential **will fail**.
318 |
319 | If you are certain you want to delete a Push Credential you can click on `Delete this Credential` on the [console](https://www.twilio.com/console/voice/sdks/credentials) page of the selected Push Credential.
320 |
321 | Please ensure that after deleting the Push Credential you remove or replace the Push Credential SID when generating new access tokens.
322 |
323 | ## More Documentation
324 | You can find more documentation on getting started as well as our latest AppleDoc below:
325 |
326 | * [Getting Started](https://www.twilio.com/docs/api/voice-sdk/ios/getting-started)
327 | * [AppleDoc](https://twilio.github.io/twilio-voice-ios/docs/latest/)
328 |
329 |
330 | ## Twilio Helper Libraries
331 | To learn more about how to use TwiML and the Programmable Voice Calls API, check out our TwiML quickstarts:
332 |
333 | * [TwiML Quickstart for Python](https://www.twilio.com/docs/quickstart/python/twiml)
334 | * [TwiML Quickstart for Ruby](https://www.twilio.com/docs/quickstart/ruby/twiml)
335 | * [TwiML Quickstart for PHP](https://www.twilio.com/docs/quickstart/php/twiml)
336 | * [TwiML Quickstart for Java](https://www.twilio.com/docs/quickstart/java/twiml)
337 | * [TwiML Quickstart for C#](https://www.twilio.com/docs/quickstart/csharp/twiml)
338 |
339 | ## Issues and Support
340 | Please file any issues you find here on Github: [Voice Objective-C Quickstart](https://github.com/twilio/voice-quickstart-objc).
341 | Please ensure that you are not sharing any
342 | [Personally Identifiable Information(PII)](https://www.twilio.com/docs/glossary/what-is-personally-identifiable-information-pii)
343 | or sensitive account information (API keys, credentials, etc.) when reporting an issue.
344 |
345 | For general inquiries related to the Voice SDK you can [file a support ticket](https://support.twilio.com/hc/en-us/requests/new).
346 |
347 | ## License
348 | MIT
349 |
--------------------------------------------------------------------------------
/ObjcVoiceQuickstart/ViewController.m:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.m
3 | // Twilio Voice with Quickstart - Objective-C
4 | //
5 | // Copyright © 2016-2018 Twilio, Inc. All rights reserved.
6 | //
7 |
8 | #import "ViewController.h"
9 |
10 | @import AVFoundation;
11 | @import PushKit;
12 | @import CallKit;
13 | @import TwilioVoice;
14 |
15 | static NSString *const kYourServerBaseURLString = <#URL TO YOUR ACCESS TOKEN SERVER#>;
16 | // If your token server is written in PHP, kAccessTokenEndpoint needs .php extension at the end. For example : /accessToken.php
17 | static NSString *const kAccessTokenEndpoint = @"/accessToken";
18 | static NSString *const kIdentity = @"alice";
19 | static NSString *const kTwimlParamTo = @"to";
20 |
21 | @interface ViewController ()
22 |
23 | @property (nonatomic, strong) NSString *deviceTokenString;
24 |
25 | @property (nonatomic, strong) PKPushRegistry *voipRegistry;
26 | @property (nonatomic, strong) void(^incomingPushCompletionCallback)(void);
27 | @property (nonatomic, strong) void(^callKitCompletionCallback)(BOOL);
28 | @property (nonatomic, strong) TVODefaultAudioDevice *audioDevice;
29 | @property (nonatomic, strong) NSMutableDictionary *activeCallInvites;
30 | @property (nonatomic, strong) NSMutableDictionary *activeCalls;
31 |
32 | // activeCall represents the last connected call
33 | @property (nonatomic, strong) TVOCall *activeCall;
34 |
35 | @property (nonatomic, strong) CXProvider *callKitProvider;
36 | @property (nonatomic, strong) CXCallController *callKitCallController;
37 | @property (nonatomic, assign) BOOL userInitiatedDisconnect;
38 |
39 | @property (nonatomic, weak) IBOutlet UIImageView *iconView;
40 | @property (nonatomic, assign, getter=isSpinning) BOOL spinning;
41 |
42 | @property (nonatomic, weak) IBOutlet UIButton *placeCallButton;
43 | @property (nonatomic, weak) IBOutlet UITextField *outgoingValue;
44 | @property (weak, nonatomic) IBOutlet UIView *callControlView;
45 | @property (weak, nonatomic) IBOutlet UISwitch *muteSwitch;
46 | @property (weak, nonatomic) IBOutlet UISwitch *speakerSwitch;
47 |
48 | @property (nonatomic, assign) BOOL playCustomRingback;
49 | @property (nonatomic, strong) AVAudioPlayer *ringtonePlayer;
50 |
51 | @end
52 |
53 | @implementation ViewController
54 |
55 | - (void)viewDidLoad {
56 | [super viewDidLoad];
57 |
58 | self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
59 | self.voipRegistry.delegate = self;
60 | self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
61 |
62 | [self toggleUIState:YES showCallControl:NO];
63 | self.outgoingValue.delegate = self;
64 |
65 | [self configureCallKit];
66 |
67 | /*
68 | * The important thing to remember when providing a TVOAudioDevice is that the device must be set
69 | * before performing any other actions with the SDK (such as connecting a Call, or accepting an incoming Call).
70 | * In this case we've already initialized our own `TVODefaultAudioDevice` instance which we will now set.
71 | */
72 | self.audioDevice = [TVODefaultAudioDevice audioDevice];
73 | TwilioVoice.audioDevice = self.audioDevice;
74 |
75 | self.activeCallInvites = [NSMutableDictionary dictionary];
76 | self.activeCalls = [NSMutableDictionary dictionary];
77 |
78 | /*
79 | Custom ringback will be played when this flag is enabled.
80 | When [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) is enabled in
81 | the TwiML verb, the caller will not hear the ringback while the call is ringing and awaiting
82 | to be accepted on the callee's side. Configure this flag based on the TwiML application.
83 | */
84 | self.playCustomRingback = NO;
85 | }
86 |
87 | - (void)configureCallKit {
88 | CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:@"Quickstart"];
89 | configuration.maximumCallGroups = 1;
90 | configuration.maximumCallsPerCallGroup = 1;
91 | UIImage *callkitIcon = [UIImage imageNamed:@"iconMask80"];
92 | configuration.iconTemplateImageData = UIImagePNGRepresentation(callkitIcon);
93 |
94 | _callKitProvider = [[CXProvider alloc] initWithConfiguration:configuration];
95 | [_callKitProvider setDelegate:self queue:nil];
96 |
97 | _callKitCallController = [[CXCallController alloc] init];
98 | }
99 |
100 | - (void)didReceiveMemoryWarning {
101 | [super didReceiveMemoryWarning];
102 | }
103 |
104 | - (void)dealloc {
105 | if (self.callKitProvider) {
106 | [self.callKitProvider invalidate];
107 | }
108 | }
109 |
110 | - (NSString *)fetchAccessToken {
111 | NSString *accessTokenEndpointWithIdentity = [NSString stringWithFormat:@"%@?identity=%@", kAccessTokenEndpoint, kIdentity];
112 | NSString *accessTokenURLString = [kYourServerBaseURLString stringByAppendingString:accessTokenEndpointWithIdentity];
113 |
114 | NSString *accessToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:accessTokenURLString]
115 | encoding:NSUTF8StringEncoding
116 | error:nil];
117 | return accessToken;
118 | }
119 |
120 | - (IBAction)mainButtonPressed:(id)sender {
121 | if (self.activeCall != nil) {
122 | self.userInitiatedDisconnect = YES;
123 | [self performEndCallActionWithUUID:self.activeCall.uuid];
124 | [self toggleUIState:NO showCallControl:NO];
125 | } else {
126 | NSUUID *uuid = [NSUUID UUID];
127 | NSString *handle = @"Voice Bot";
128 |
129 | [self checkRecordPermission:^(BOOL permissionGranted) {
130 | if (!permissionGranted) {
131 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Voice Quick Start"
132 | message:@"Microphone permission not granted."
133 | preferredStyle:UIAlertControllerStyleAlert];
134 |
135 | typeof(self) __weak weakSelf = self;
136 | UIAlertAction *continueWithoutMic = [UIAlertAction actionWithTitle:@"Continue without microphone"
137 | style:UIAlertActionStyleDefault
138 | handler:^(UIAlertAction *action) {
139 | typeof(self) __strong strongSelf = weakSelf;
140 | [strongSelf performStartCallActionWithUUID:uuid handle:handle];
141 | }];
142 | [alertController addAction:continueWithoutMic];
143 |
144 | NSDictionary *openURLOptions = @{UIApplicationOpenURLOptionUniversalLinksOnly: @NO};
145 | UIAlertAction *goToSettings = [UIAlertAction actionWithTitle:@"Settings"
146 | style:UIAlertActionStyleDefault
147 | handler:^(UIAlertAction *action) {
148 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]
149 | options:openURLOptions
150 | completionHandler:nil];
151 | }];
152 | [alertController addAction:goToSettings];
153 |
154 | UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"Cancel"
155 | style:UIAlertActionStyleCancel
156 | handler:^(UIAlertAction *action) {
157 | typeof(self) __strong strongSelf = weakSelf;
158 | [strongSelf toggleUIState:YES showCallControl:NO];
159 | [strongSelf stopSpin];
160 | }];
161 | [alertController addAction:cancel];
162 |
163 | [self presentViewController:alertController animated:YES completion:nil];
164 | } else {
165 | [self performStartCallActionWithUUID:uuid handle:handle];
166 | }
167 | }];
168 | }
169 | }
170 |
171 | - (void)checkRecordPermission:(void(^)(BOOL permissionGranted))completion {
172 | AVAudioSessionRecordPermission permissionStatus = [[AVAudioSession sharedInstance] recordPermission];
173 | switch (permissionStatus) {
174 | case AVAudioSessionRecordPermissionGranted:
175 | // Record permission already granted.
176 | completion(YES);
177 | break;
178 | case AVAudioSessionRecordPermissionDenied:
179 | // Record permission denied.
180 | completion(NO);
181 | break;
182 | case AVAudioSessionRecordPermissionUndetermined:
183 | {
184 | // Requesting record permission.
185 | // Optional: pop up app dialog to let the users know if they want to request.
186 | [[AVAudioSession sharedInstance] requestRecordPermission:^(BOOL granted) {
187 | completion(granted);
188 | }];
189 | break;
190 | }
191 | default:
192 | completion(NO);
193 | break;
194 | }
195 | }
196 |
197 | - (void)toggleUIState:(BOOL)isEnabled showCallControl:(BOOL)showCallControl {
198 | self.placeCallButton.enabled = isEnabled;
199 | if (showCallControl) {
200 | self.callControlView.hidden = NO;
201 | self.muteSwitch.on = NO;
202 | self.speakerSwitch.on = YES;
203 | } else {
204 | self.callControlView.hidden = YES;
205 | }
206 | }
207 |
208 | - (IBAction)muteSwitchToggled:(UISwitch *)sender {
209 | // The sample app supports toggling mute from app UI only on the last connected call.
210 | if (self.activeCall != nil) {
211 | self.activeCall.muted = sender.on;
212 | }
213 | }
214 |
215 | - (IBAction)speakerSwitchToggled:(UISwitch *)sender {
216 | [self toggleAudioRoute:sender.on];
217 | }
218 |
219 | #pragma mark - UITextFieldDelegate
220 | - (BOOL)textFieldShouldReturn:(UITextField *)textField {
221 | [self.outgoingValue resignFirstResponder];
222 | return YES;
223 | }
224 |
225 | #pragma mark - PKPushRegistryDelegate
226 | - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
227 | NSLog(@"pushRegistry:didUpdatePushCredentials:forType:");
228 |
229 | if ([type isEqualToString:PKPushTypeVoIP]) {
230 | const unsigned *tokenBytes = [credentials.token bytes];
231 | self.deviceTokenString = [NSString stringWithFormat:@"<%08x %08x %08x %08x %08x %08x %08x %08x>",
232 | ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
233 | ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
234 | ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];
235 | NSString *accessToken = [self fetchAccessToken];
236 |
237 | [TwilioVoice registerWithAccessToken:accessToken
238 | deviceToken:self.deviceTokenString
239 | completion:^(NSError *error) {
240 | if (error) {
241 | NSLog(@"An error occurred while registering: %@", [error localizedDescription]);
242 | }
243 | else {
244 | NSLog(@"Successfully registered for VoIP push notifications.");
245 | }
246 | }];
247 | }
248 | }
249 |
250 | - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type {
251 | NSLog(@"pushRegistry:didInvalidatePushTokenForType:");
252 |
253 | if ([type isEqualToString:PKPushTypeVoIP]) {
254 | NSString *accessToken = [self fetchAccessToken];
255 |
256 | [TwilioVoice unregisterWithAccessToken:accessToken
257 | deviceToken:self.deviceTokenString
258 | completion:^(NSError * _Nullable error) {
259 | if (error) {
260 | NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]);
261 | }
262 | else {
263 | NSLog(@"Successfully unregistered for VoIP push notifications.");
264 | }
265 | }];
266 |
267 | self.deviceTokenString = nil;
268 | }
269 | }
270 |
271 | /**
272 | * Try using the `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` method if
273 | * your application is targeting iOS 11. According to the docs, this delegate method is deprecated by Apple.
274 | */
275 | - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type {
276 | NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:");
277 | if ([type isEqualToString:PKPushTypeVoIP]) {
278 |
279 | // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed
280 | if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue:nil]) {
281 | NSLog(@"This is not a valid Twilio Voice notification.");
282 | }
283 | }
284 | }
285 |
286 | /**
287 | * This delegate method is available on iOS 11 and above. Call the completion handler once the
288 | * notification payload is passed to the `TwilioVoice.handleNotification()` method.
289 | */
290 | - (void)pushRegistry:(PKPushRegistry *)registry
291 | didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
292 | forType:(PKPushType)type
293 | withCompletionHandler:(void (^)(void))completion {
294 | NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:");
295 |
296 | // Save for later when the notification is properly handled.
297 | self.incomingPushCompletionCallback = completion;
298 |
299 |
300 | if ([type isEqualToString:PKPushTypeVoIP]) {
301 | // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed
302 | if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue:nil]) {
303 | NSLog(@"This is not a valid Twilio Voice notification.");
304 | }
305 | }
306 | if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
307 | // Save for later when the notification is properly handled.
308 | self.incomingPushCompletionCallback = completion;
309 | } else {
310 | /**
311 | * The Voice SDK processes the call notification and returns the call invite synchronously. Report the incoming call to
312 | * CallKit and fulfill the completion before exiting this callback method.
313 | */
314 | completion();
315 | }
316 | }
317 |
318 | - (void)incomingPushHandled {
319 | if (self.incomingPushCompletionCallback) {
320 | self.incomingPushCompletionCallback();
321 | self.incomingPushCompletionCallback = nil;
322 | }
323 | }
324 |
325 | #pragma mark - TVONotificationDelegate
326 | - (void)callInviteReceived:(TVOCallInvite *)callInvite {
327 |
328 | /**
329 | * Calling `[TwilioVoice handleNotification:delegate:]` will synchronously process your notification payload and
330 | * provide you a `TVOCallInvite` object. Report the incoming call to CallKit upon receiving this callback.
331 | */
332 |
333 | NSLog(@"callInviteReceived:");
334 |
335 | NSString *from = @"Voice Bot";
336 | if (callInvite.from) {
337 | from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""];
338 | }
339 |
340 | // Always report to CallKit
341 | [self reportIncomingCallFrom:from withUUID:callInvite.uuid];
342 | self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite;
343 | if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
344 | [self incomingPushHandled];
345 | }
346 | }
347 |
348 | - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error {
349 |
350 | /**
351 | * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue
352 | * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called
353 | * party could answer or reject the call.
354 | */
355 |
356 | NSLog(@"cancelledCallInviteReceived:");
357 |
358 | TVOCallInvite *callInvite;
359 | for (NSString *uuid in self.activeCallInvites) {
360 | TVOCallInvite *activeCallInvite = [self.activeCallInvites objectForKey:uuid];
361 | if ([cancelledCallInvite.callSid isEqualToString:activeCallInvite.callSid]) {
362 | callInvite = activeCallInvite;
363 | break;
364 | }
365 | }
366 |
367 | if (callInvite) {
368 | [self performEndCallActionWithUUID:callInvite.uuid];
369 | }
370 | }
371 |
372 | #pragma mark - TVOCallDelegate
373 | - (void)callDidStartRinging:(TVOCall *)call {
374 | NSLog(@"callDidStartRinging:");
375 |
376 | /*
377 | When [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) is enabled in the
378 | TwiML verb, the caller will not hear the ringback while the call is ringing and awaiting to be
379 | accepted on the callee's side. The application can use the `AVAudioPlayer` to play custom audio files
380 | between the `[TVOCallDelegate callDidStartRinging:]` and the `[TVOCallDelegate callDidConnect:]` callbacks.
381 | */
382 | if (self.playCustomRingback) {
383 | [self playRingback];
384 | }
385 |
386 | [self.placeCallButton setTitle:@"Ringing" forState:UIControlStateNormal];
387 | }
388 |
389 | - (void)callDidConnect:(TVOCall *)call {
390 | NSLog(@"callDidConnect:");
391 |
392 | if (self.playCustomRingback) {
393 | [self stopRingback];
394 | }
395 |
396 | self.callKitCompletionCallback(YES);
397 |
398 | [self.placeCallButton setTitle:@"Hang Up" forState:UIControlStateNormal];
399 |
400 | [self toggleUIState:YES showCallControl:YES];
401 | [self stopSpin];
402 | [self toggleAudioRoute:YES];
403 | }
404 |
405 | - (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error {
406 | NSLog(@"Call is reconnecting");
407 |
408 | [self.placeCallButton setTitle:@"Reconnecting" forState:UIControlStateNormal];
409 | [self toggleUIState:NO showCallControl:NO];
410 | }
411 |
412 | - (void)callDidReconnect:(TVOCall *)call {
413 | NSLog(@"Call reconnected");
414 |
415 | [self.placeCallButton setTitle:@"Hang Up" forState:UIControlStateNormal];
416 | [self toggleUIState:YES showCallControl:YES];
417 | }
418 |
419 | - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error {
420 | NSLog(@"Call failed to connect: %@", error);
421 |
422 | self.callKitCompletionCallback(NO);
423 | [self performEndCallActionWithUUID:call.uuid];
424 | [self callDisconnected:call];
425 | }
426 |
427 | - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error {
428 | if (error) {
429 | NSLog(@"Call failed: %@", error);
430 | } else {
431 | NSLog(@"Call disconnected");
432 | }
433 |
434 | if (!self.userInitiatedDisconnect) {
435 | CXCallEndedReason reason = CXCallEndedReasonRemoteEnded;
436 | if (error) {
437 | reason = CXCallEndedReasonFailed;
438 | }
439 |
440 | [self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason];
441 | }
442 |
443 | [self callDisconnected:call];
444 | }
445 |
446 | - (void)callDisconnected:(TVOCall *)call {
447 | if ([call isEqual:self.activeCall]) {
448 | self.activeCall = nil;
449 | }
450 | [self.activeCalls removeObjectForKey:call.uuid.UUIDString];
451 |
452 | self.userInitiatedDisconnect = NO;
453 |
454 | if (self.playCustomRingback) {
455 | [self stopRingback];
456 | }
457 |
458 | [self stopSpin];
459 | [self toggleUIState:YES showCallControl:NO];
460 | [self.placeCallButton setTitle:@"Call" forState:UIControlStateNormal];
461 | }
462 |
463 | #pragma mark - AVAudioSession
464 | - (void)toggleAudioRoute:(BOOL)toSpeaker {
465 | // The mode set by the Voice SDK is "VoiceChat" so the default audio route is the built-in receiver. Use port override to switch the route.
466 | self.audioDevice.block = ^ {
467 | // We will execute `kDefaultAVAudioSessionConfigurationBlock` first.
468 | kTVODefaultAVAudioSessionConfigurationBlock();
469 |
470 | // Overwrite the audio route
471 | AVAudioSession *session = [AVAudioSession sharedInstance];
472 | NSError *error = nil;
473 | if (toSpeaker) {
474 | if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]) {
475 | NSLog(@"Unable to reroute audio: %@", [error localizedDescription]);
476 | }
477 | } else {
478 | if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]) {
479 | NSLog(@"Unable to reroute audio: %@", [error localizedDescription]);
480 | }
481 | }
482 | };
483 | self.audioDevice.block();
484 | }
485 |
486 | #pragma mark - Icon spinning
487 | - (void)startSpin {
488 | if (!self.isSpinning) {
489 | self.spinning = YES;
490 | [self spinWithOptions:UIViewAnimationOptionCurveEaseIn];
491 | }
492 | }
493 |
494 | - (void)stopSpin {
495 | self.spinning = NO;
496 | }
497 |
498 | - (void)spinWithOptions:(UIViewAnimationOptions)options {
499 | typeof(self) __weak weakSelf = self;
500 |
501 | [UIView animateWithDuration:0.5f
502 | delay:0.0f
503 | options:options
504 | animations:^{
505 | typeof(self) __strong strongSelf = weakSelf;
506 | strongSelf.iconView.transform = CGAffineTransformRotate(strongSelf.iconView.transform, M_PI / 2);
507 | }
508 | completion:^(BOOL finished) {
509 | typeof(self) __strong strongSelf = weakSelf;
510 | if (finished) {
511 | if (strongSelf.isSpinning) {
512 | [strongSelf spinWithOptions:UIViewAnimationOptionCurveLinear];
513 | } else if (options != UIViewAnimationOptionCurveEaseOut) {
514 | [strongSelf spinWithOptions:UIViewAnimationOptionCurveEaseOut];
515 | }
516 | }
517 | }];
518 | }
519 |
520 | #pragma mark - CXProviderDelegate
521 | - (void)providerDidReset:(CXProvider *)provider {
522 | NSLog(@"providerDidReset:");
523 | self.audioDevice.enabled = YES;
524 | }
525 |
526 | - (void)providerDidBegin:(CXProvider *)provider {
527 | NSLog(@"providerDidBegin:");
528 | }
529 |
530 | - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession {
531 | NSLog(@"provider:didActivateAudioSession:");
532 | self.audioDevice.enabled = YES;
533 | }
534 |
535 | - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession {
536 | NSLog(@"provider:didDeactivateAudioSession:");
537 | }
538 |
539 | - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action {
540 | NSLog(@"provider:timedOutPerformingAction:");
541 | }
542 |
543 | - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action {
544 | NSLog(@"provider:performStartCallAction:");
545 |
546 | [self toggleUIState:NO showCallControl:NO];
547 | [self startSpin];
548 |
549 | self.audioDevice.enabled = NO;
550 | self.audioDevice.block();
551 |
552 | [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]];
553 |
554 | __weak typeof(self) weakSelf = self;
555 | [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) {
556 | __strong typeof(self) strongSelf = weakSelf;
557 | if (success) {
558 | [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]];
559 | [action fulfill];
560 | } else {
561 | [action fail];
562 | }
563 | }];
564 | }
565 |
566 | - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
567 | NSLog(@"provider:performAnswerCallAction:");
568 |
569 | self.audioDevice.enabled = NO;
570 | self.audioDevice.block();
571 |
572 | [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) {
573 | if (success) {
574 | [action fulfill];
575 | } else {
576 | [action fail];
577 | }
578 | }];
579 |
580 | [action fulfill];
581 | }
582 |
583 | - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
584 | NSLog(@"provider:performEndCallAction:");
585 |
586 | TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString];
587 | TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
588 |
589 | if (callInvite) {
590 | [callInvite reject];
591 | [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString];
592 | } else if (call) {
593 | [call disconnect];
594 | } else {
595 | NSLog(@"Unknown UUID to perform end-call action with");
596 | }
597 |
598 | [action fulfill];
599 | }
600 |
601 | - (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action {
602 | TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
603 | if (call) {
604 | [call setOnHold:action.isOnHold];
605 | [action fulfill];
606 | } else {
607 | [action fail];
608 | }
609 | }
610 |
611 | - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
612 | TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
613 | if (call) {
614 | [call setMuted:action.isMuted];
615 | [action fulfill];
616 | } else {
617 | [action fail];
618 | }
619 | }
620 |
621 | #pragma mark - CallKit Actions
622 | - (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle {
623 | if (uuid == nil || handle == nil) {
624 | return;
625 | }
626 |
627 | CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
628 | CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
629 | CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction];
630 |
631 | [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
632 | if (error) {
633 | NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]);
634 | } else {
635 | NSLog(@"StartCallAction transaction request successful");
636 |
637 | CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
638 | callUpdate.remoteHandle = callHandle;
639 | callUpdate.supportsDTMF = YES;
640 | callUpdate.supportsHolding = YES;
641 | callUpdate.supportsGrouping = NO;
642 | callUpdate.supportsUngrouping = NO;
643 | callUpdate.hasVideo = NO;
644 |
645 | [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate];
646 | }
647 | }];
648 | }
649 |
650 | - (void)reportIncomingCallFrom:(NSString *) from withUUID:(NSUUID *)uuid {
651 | CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:from];
652 |
653 | CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
654 | callUpdate.remoteHandle = callHandle;
655 | callUpdate.supportsDTMF = YES;
656 | callUpdate.supportsHolding = YES;
657 | callUpdate.supportsGrouping = NO;
658 | callUpdate.supportsUngrouping = NO;
659 | callUpdate.hasVideo = NO;
660 |
661 | [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) {
662 | if (!error) {
663 | NSLog(@"Incoming call successfully reported.");
664 | }
665 | else {
666 | NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]);
667 | }
668 | }];
669 | }
670 |
671 | - (void)performEndCallActionWithUUID:(NSUUID *)uuid {
672 | CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid];
673 | CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction];
674 |
675 | [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
676 | if (error) {
677 | NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]);
678 | }
679 | else {
680 | NSLog(@"EndCallAction transaction request successful");
681 | }
682 | }];
683 | }
684 |
685 | - (void)performVoiceCallWithUUID:(NSUUID *)uuid
686 | client:(NSString *)client
687 | completion:(void(^)(BOOL success))completionHandler {
688 | __weak typeof(self) weakSelf = self;
689 | TVOConnectOptions *connectOptions = [TVOConnectOptions optionsWithAccessToken:[self fetchAccessToken] block:^(TVOConnectOptionsBuilder *builder) {
690 | __strong typeof(self) strongSelf = weakSelf;
691 | builder.params = @{kTwimlParamTo: strongSelf.outgoingValue.text};
692 | builder.uuid = uuid;
693 | }];
694 | TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self];
695 | if (call) {
696 | self.activeCall = call;
697 | self.activeCalls[call.uuid.UUIDString] = call;
698 | }
699 | self.callKitCompletionCallback = completionHandler;
700 | }
701 |
702 | - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid
703 | completion:(void(^)(BOOL success))completionHandler {
704 | TVOCallInvite *callInvite = self.activeCallInvites[uuid.UUIDString];
705 | NSAssert(callInvite, @"No CallInvite matches the UUID");
706 |
707 | TVOAcceptOptions *acceptOptions = [TVOAcceptOptions optionsWithCallInvite:callInvite block:^(TVOAcceptOptionsBuilder *builder) {
708 | builder.uuid = callInvite.uuid;
709 | }];
710 |
711 | TVOCall *call = [callInvite acceptWithOptions:acceptOptions delegate:self];
712 |
713 | if (!call) {
714 | completionHandler(NO);
715 | } else {
716 | self.callKitCompletionCallback = completionHandler;
717 | self.activeCall = call;
718 | self.activeCalls[call.uuid.UUIDString] = call;
719 | }
720 |
721 | [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString];
722 |
723 | if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
724 | [self incomingPushHandled];
725 | }
726 | }
727 |
728 | #pragma mark - Ringtone
729 |
730 | - (void)playRingback {
731 | NSString *ringtonePath = [[NSBundle mainBundle] pathForResource:@"ringtone" ofType:@"wav"];
732 | if ([ringtonePath length] <= 0) {
733 | NSLog(@"Can't find sound file");
734 | return;
735 | }
736 |
737 | NSError *error;
738 | self.ringtonePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL URLWithString:ringtonePath] error:&error];
739 | if (error != nil) {
740 | NSLog(@"Failed to initialize audio player: %@", error);
741 | } else {
742 | self.ringtonePlayer.delegate = self;
743 | self.ringtonePlayer.numberOfLoops = -1;
744 |
745 | self.ringtonePlayer.volume = 1.0f;
746 | [self.ringtonePlayer play];
747 | }
748 | }
749 |
750 | - (void)stopRingback {
751 | if (!self.ringtonePlayer.isPlaying) {
752 | return;
753 | }
754 |
755 | [self.ringtonePlayer stop];
756 | }
757 |
758 | - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
759 | if (flag) {
760 | NSLog(@"Audio player finished playing successfully");
761 | } else {
762 | NSLog(@"Audio player finished playing with some error");
763 | }
764 | }
765 |
766 | - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {
767 | NSLog(@"Decode error occurred: %@", error);
768 | }
769 |
770 | @end
771 |
--------------------------------------------------------------------------------