Repository: opentok/opentok-ios-sdk-samples-swift Branch: main Commit: c15f9e6dad16 Files: 197 Total size: 721.0 KB Directory structure: gitextract_wr9whvu1/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ └── metrics.yml ├── .gitignore ├── .travis.yml ├── Basic-Video-Chat/ │ ├── Basic-Video-Chat/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Basic-Video-Chat.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Basic-Video-Chat.xcscheme │ ├── Podfile │ └── README.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CallKit/ │ ├── CallKitDemo/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── IconMask.imageset/ │ │ │ │ └── Contents.json │ │ │ └── baseHeroMount.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── CallKitDemo-Bridging-Header.h │ │ ├── CallKitDemo.entitlements │ │ ├── Info.plist │ │ ├── OTDefaultAudioDevice.h │ │ ├── OTDefaultAudioDevice.m │ │ ├── ProviderDelegate.swift │ │ ├── Ringtone.caf │ │ ├── SpeakerboxCall.swift │ │ ├── SpeakerboxCallManager.swift │ │ └── ViewController.swift │ ├── CallKitDemo.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── CallKitDemo.xcscheme │ ├── LICENSE │ ├── Podfile │ └── README.md ├── CallKit-with-native-OpenTok-support/ │ ├── CallKitDemo/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── IconMask.imageset/ │ │ │ │ └── Contents.json │ │ │ └── baseHeroMount.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── CallKitDemo.entitlements │ │ ├── Info.plist │ │ ├── ProviderDelegate.swift │ │ ├── Ringtone.caf │ │ ├── SpeakerboxCall.swift │ │ ├── SpeakerboxCallManager.swift │ │ └── ViewController.swift │ ├── CallKitDemo.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── CallKitDemo.xcscheme │ ├── LICENSE │ ├── Podfile │ ├── README.md │ └── pu.sh ├── Custom-Audio-Driver/ │ ├── Custom-Audio-Driver/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── DefaultAudioDevice.swift │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Custom-Audio-Driver.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Custom-Audio-Driver.xcscheme │ ├── Podfile │ └── README.md ├── Custom-Video-Driver/ │ ├── Custom-Video-Driver.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Custom-Video-Driver.xcscheme │ ├── Lets-Build-OTPublisher/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── EAGLVideoRenderer.swift │ │ ├── ExampleVideoCapture.swift │ │ ├── ExampleVideoRender.swift │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Podfile │ └── README.md ├── E2EE-Video-Chat/ │ ├── Basic-Video-Chat/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Basic-Video-Chat.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Basic-Video-Chat.xcscheme │ ├── Podfile │ └── README.md ├── FrameMetadata/ │ ├── FrameMetadata/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── FrameMetadata.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── FrameMetadata.xcscheme │ ├── Podfile │ └── README.md ├── LICENSE ├── Live-Photo-Capture/ │ ├── Live-Photo-Capture/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── ExamplePhotoVideoCapture.swift │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Live-Photo-Capture.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Live-Photo-Capture.xcscheme │ ├── Podfile │ └── README.md ├── Media-Transformers/ │ ├── CHANGELOG.md │ ├── Media-Transformers/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ ├── Media-Transformers.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Video-Transformers.xcscheme │ ├── Podfile │ └── README.md ├── Multiparty-UICollectionView/ │ ├── Multiparty-UICollectionView/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── ChatViewController.swift │ │ ├── Info.plist │ │ └── MultipartyLayout.swift │ ├── Multiparty-UICollectionView.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Multiparty-UICollectionView.xcscheme │ ├── Podfile │ └── README.md ├── OpenTokSDKVersion.rb ├── Picture-In-Picture/ │ ├── Lets-Build-OTPublisher/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── ExampleVideoRender.swift │ │ ├── Info.plist │ │ ├── SampleBufferVideoCallView.swift │ │ └── ViewController.swift │ ├── Picture-In-Picture.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ └── Picture-In-Picture.xcscheme │ ├── Podfile │ └── README.md ├── README.md ├── Screen-Sharing/ │ ├── Podfile │ ├── README.md │ ├── Screen-Sharing/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ └── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ScreenCapturer.swift │ │ └── ViewController.swift │ └── Screen-Sharing.xcodeproj/ │ ├── project.pbxproj │ └── xcshareddata/ │ └── xcschemes/ │ └── Screen-Sharing.xcscheme ├── Signals/ │ ├── Podfile │ ├── README.md │ ├── Signals/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── FormView.swift │ │ ├── MessagesView.swift │ │ ├── Preview Content/ │ │ │ └── Preview Assets.xcassets/ │ │ │ └── Contents.json │ │ ├── SignalsApp.swift │ │ └── VonageVideoSDK.swift │ └── Signals.xcodeproj/ │ └── project.pbxproj ├── Simple-Multiparty/ │ ├── Podfile │ ├── README.md │ ├── Simple-Multiparty/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Subscriber-Speaker-35.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Subscriber-Speaker-Mute-35.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── TB Bug-30.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── camera-switch_black-33.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── camera_switch-33.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── icon_arrowLeft_disabled-28.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── icon_arrowLeft_enabled-28.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── icon_arrowRight_disabled-28.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── icon_arrowRight_enabled-28.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── mic-24.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── mic_muted-24.imageset/ │ │ │ │ └── Contents.json │ │ │ └── mic_receiving_data-35.imageset/ │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── ViewController.swift │ └── Simple-Multiparty.xcodeproj/ │ ├── project.pbxproj │ └── xcshareddata/ │ └── xcschemes/ │ └── Simple-Multiparty.xcscheme └── travis_build.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Device (please compete the following information):** - sessionId, if applicable: - iOS SDK version: - OS and version: **Additional context** Add any other context about the problem here. ================================================ FILE: .github/workflows/metrics.yml ================================================ name: Aggregit on: schedule: - cron: "0 0 * * *" jobs: recordMetrics: runs-on: ubuntu-latest steps: - uses: michaeljolley/aggregit@v1 with: githubToken: ${{ secrets.GITHUB_TOKEN }} project_id: ${{ secrets.project_id }} private_key: ${{ secrets.private_key }} client_email: ${{ secrets.client_email }} firebaseDbUrl: ${{ secrets.firebaseDbUrl }} ================================================ FILE: .gitignore ================================================ OpenTok.framework/ OpenTok.framework # Xcode .DS_Store */build/* *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata profile *.moved-aside DerivedData .idea/ *.hmap *.xccheckout #CocoaPods Pods Podfile.lock *.xcworkspacedata *.xcworkspace ================================================ FILE: .travis.yml ================================================ language: objective-c osx_image: xcode11.7 before_install: - pod repo update > /dev/null script: ./travis_build.sh ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat/AppDelegate.swift ================================================ // // AppDelegate.swift // Hello-World // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } } ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat/ViewController.swift ================================================ // // ViewController.swift // Hello-World // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" let kWidgetHeight = 240 let kWidgetWidth = 320 class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() var publisher: OTPublisher? var subscriber: OTSubscriber? override func viewDidLoad() { super.viewDidLoad() doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? defer { processError(error) } let settings = OTPublisherSettings() settings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: settings)! session.publish(publisher!, error: &error) if let pubView = publisher!.view { pubView.frame = CGRect(x: 0, y: 0, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(pubView) } } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func cleanupSubscriber() { subscriber?.view?.removeFromSuperview() subscriber = nil } fileprivate func cleanupPublisher() { publisher!.view?.removeFromSuperview() publisher = nil } fileprivate func processError(_ error: OTError?) { if let err = error { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err.localizedDescription, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") cleanupPublisher() cleanupSubscriber() } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscriber == nil { doSubscribe(stream) } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { cleanupPublisher() if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { if let subsView = subscriber?.view { subsView.frame = CGRect(x: 0, y: kWidgetHeight, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(subsView) } } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } } ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A05375D71EB1633400645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375CF1EB1633400645696 /* AppDelegate.swift */; }; A05375D81EB1633400645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05375D01EB1633400645696 /* Assets.xcassets */; }; A05375D91EB1633400645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375D11EB1633400645696 /* LaunchScreen.storyboard */; }; A05375DA1EB1633400645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375D31EB1633400645696 /* Main.storyboard */; }; A05375DC1EB1633400645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375D61EB1633400645696 /* ViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A05375CF1EB1633400645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05375D01EB1633400645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05375D21EB1633400645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05375D41EB1633400645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05375D51EB1633400645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05375D61EB1633400645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F86C649A1D5C7C630081846D /* Basic-Video-Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Basic-Video-Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F86C64971D5C7C630081846D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A05375CE1EB1633400645696 /* Basic-Video-Chat */ = { isa = PBXGroup; children = ( A05375CF1EB1633400645696 /* AppDelegate.swift */, A05375D01EB1633400645696 /* Assets.xcassets */, A05375D11EB1633400645696 /* LaunchScreen.storyboard */, A05375D31EB1633400645696 /* Main.storyboard */, A05375D51EB1633400645696 /* Info.plist */, A05375D61EB1633400645696 /* ViewController.swift */, ); path = "Basic-Video-Chat"; sourceTree = ""; }; F86C64911D5C7C630081846D = { isa = PBXGroup; children = ( A05375CE1EB1633400645696 /* Basic-Video-Chat */, F86C649B1D5C7C630081846D /* Products */, ); sourceTree = ""; }; F86C649B1D5C7C630081846D /* Products */ = { isa = PBXGroup; children = ( F86C649A1D5C7C630081846D /* Basic-Video-Chat.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F86C64991D5C7C630081846D /* Basic-Video-Chat */ = { isa = PBXNativeTarget; buildConfigurationList = F86C64AC1D5C7C630081846D /* Build configuration list for PBXNativeTarget "Basic-Video-Chat" */; buildPhases = ( F86C64961D5C7C630081846D /* Sources */, F86C64971D5C7C630081846D /* Frameworks */, F86C64981D5C7C630081846D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Basic-Video-Chat"; productName = "Hello-World"; productReference = F86C649A1D5C7C630081846D /* Basic-Video-Chat.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86C64921D5C7C630081846D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0930; ORGANIZATIONNAME = tokbox; TargetAttributes = { F86C64991D5C7C630081846D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F86C64951D5C7C630081846D /* Build configuration list for PBXProject "Basic-Video-Chat" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, ); mainGroup = F86C64911D5C7C630081846D; productRefGroup = F86C649B1D5C7C630081846D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F86C64991D5C7C630081846D /* Basic-Video-Chat */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F86C64981D5C7C630081846D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375DA1EB1633400645696 /* Main.storyboard in Resources */, A05375D81EB1633400645696 /* Assets.xcassets in Resources */, A05375D91EB1633400645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F86C64961D5C7C630081846D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375DC1EB1633400645696 /* ViewController.swift in Sources */, A05375D71EB1633400645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05375D11EB1633400645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05375D21EB1633400645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05375D31EB1633400645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05375D41EB1633400645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F86C64AA1D5C7C630081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64"; }; name = Debug; }; F86C64AB1D5C7C630081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64"; }; name = Release; }; F86C64AD1D5C7C630081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Basic-Video-Chat/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Hello-World"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Debug; }; F86C64AE1D5C7C630081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Basic-Video-Chat/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Hello-World"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F86C64951D5C7C630081846D /* Build configuration list for PBXProject "Basic-Video-Chat" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64AA1D5C7C630081846D /* Debug */, F86C64AB1D5C7C630081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F86C64AC1D5C7C630081846D /* Build configuration list for PBXNativeTarget "Basic-Video-Chat" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64AD1D5C7C630081846D /* Debug */, F86C64AE1D5C7C630081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F86C64921D5C7C630081846D /* Project object */; } ================================================ FILE: Basic-Video-Chat/Basic-Video-Chat.xcodeproj/xcshareddata/xcschemes/Basic-Video-Chat.xcscheme ================================================ ================================================ FILE: Basic-Video-Chat/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Basic-Video-Chat' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Basic-Video-Chat/README.md ================================================ Basic Video Chat Sample App =============================== The Basic-Video-Chat app is a very simple application meant to get a new developer started using the OpenTok iOS SDK. Quick Start ----------- To use this application: 1. Follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. Among other things, you need to set values for the `kApiKey`, `kSessionId`, and `kToken` constants. See [Obtaining OpenTok Credentials](../README.md#obtaining-opentok-credentials) in the main README file for the repository. 2. When you run the application, it connects to an OpenTok session and publishes an audio-video stream from your device to the session. 3. Run the app on a second client. You can do this by deploying the app to an iOS device and testing it in the simulator at the same time. Or you can use the browser_demo.html file to connect in a browser (see the following section). When the second client connects, it also publishes a stream to the session, and both clients subscribe to (view) each other’s stream. Application Notes ----------------- * Follow the code from the `ViewController.viewDidLoad(_:)` method through to the OpenTok callbacks to see how streams are created and handled in the OpenTok iOS SDK. * By default, all delegate methods from classes in the OpenTok iOS SDK are invoked on the main queue. This means that you can directly modify the view hierarchy from inside the callback, without any asynchronous callouts. * When the main view loads, the ViewController calls the `OTSession.initWithApiKey(_:, sessionId:,delegate:)` method to initialize a Session object. The app then calls the `OTSession.connectWithToken(_:, error:)` to connect to the session. The `OTSessionDelegate.sessionDidConnect(_:)` message is sent when the app connects to the OpenTok session. * The `doPublish()` method of the app initializes a publisher and passes it into the `OTSession.publish(_:,error:)` method. This publishes an audio-video stream to the session. * The `OTSessionDelegate.session(_:,streamCreated:)` message is sent when a new stream is created in the session. In response, the method calls `OTSubscriber(stream:,delegate:)`, passing in the OTStream object. This causes the app to subscribe to the stream. To add a second publisher (which will display as a subscriber in your emulator), either run the app a second time in an iOS device or use the OpenTok Playground to connect to the session in a supported web browser (Chrome, Firefox, or Internet Explorer 10-11) by following the steps below: 1. Go to [OpenTok Playground](https://tokbox.com/developer/tools/playground) (must be logged into your [Account](https://tokbox.com/account)) 2. Select the **Join existing session** tab 3. Copy the session ID you used in your project file and paste it in the **Session ID** input field 4. Click **Join Session** 5. On the next screen, click **Connect**, then click **Publish Stream** 6. You can adjust the Publisher options (not required), then click **Continue** to connect and begin publishing and subscribing Configuration Notes ------------------- * You can test in the iOS Simulator or on a supported iOS device. However, the XCode iOS Simulator does not provide access to the camera. When running in the iOS Simulator, an OTPublisher object uses a demo video instead of the camera. [1]: https://tokbox.com/account/#/ [2]: https://tokbox.com/developer/sdks/server/ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at devrel@vonage.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Guidelines For anyone looking to get involved to this project, we are glad to hear from you. Here are a few types of contributions that we would be interested in hearing about. * Bug fixes - If you find a bug, please first report it using Github Issues. - Issues that have already been identified as a bug will be labelled `bug`. - If you'd like to submit a fix for a bug, send a Pull Request from your own fork and mention the Issue number. * New Features - If you'd like to accomplish something in the library that it doesn't already do, describe the problem in a new Github Issue. - Issues that have been identified as a feature request will be labelled `enhancement`. - If you'd like to implement the new feature, please wait for feedback from the project maintainers before spending too much time writing the code. In some cases, `enhancement`s may not align well with the project objectives at the time. * Documentation and Miscellaneous - If you think the documentation could be clearer, you've got an alternative implementation of something that may have more advantages, or any other change we would still be glad hear about it. - If its a trivial change, go ahead and send a Pull Request with the changes you have in mind - If not, open a Github Issue to discuss the idea first. ## Requirements For a contribution to be accepted: * Code must follow existing styling conventions * Commit messages must be descriptive. Related issues should be mentioned by number. If the contribution doesn't meet these criteria, a maintainer will discuss it with you on the Issue. You can still continue to add more commits to the branch you have sent the Pull Request from. ## How To 1. Fork this repository on GitHub. 1. Clone/fetch your fork to your local development machine. 1. Create a new branch (e.g. `issue-12`, `feat.add_foo`, etc) and check it out. 1. Make your changes and commit them. 1. Push your new branch to your fork. (e.g. `git push myname issue-12`) 1. Open a Pull Request from your new branch to the original fork's `master` branch. ================================================ FILE: CallKit/CallKitDemo/AppDelegate.swift ================================================ // // AppDelegate.swift // CallKitDemo // // Created by Xi Huang on 6/5/17. // Copyright © 2017 Tokbox, Inc. All rights reserved. // import UIKit import PushKit import CallKit import OpenTok let apiKey = "" let sessionId = "" let token = "" @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? let pushRegistry = PKPushRegistry(queue: DispatchQueue.main) let callManager = SpeakerboxCallManager() var providerDelegate: ProviderDelegate? // Trigger VoIP registration on launch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { providerDelegate = ProviderDelegate(callManager: callManager) pushRegistry.delegate = self pushRegistry.desiredPushTypes = [.voIP] return true } } extension AppDelegate: PKPushRegistryDelegate { func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { print("\(#function) voip token: \(credentials.token)") let deviceToken = credentials.token.reduce("", {$0 + String(format: "%02X", $1) }) print("\(#function) token is: \(deviceToken)") } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { print("\(#function) incoming voip notfication: \(payload.dictionaryPayload)") if let uuidString = payload.dictionaryPayload["UUID"] as? String, let handle = payload.dictionaryPayload["handle"] as? String, let uuid = UUID(uuidString: uuidString) { OTAudioDeviceManager.setAudioDevice(OTDefaultAudioDevice.sharedInstance()) // display incoming call UI when receiving incoming voip notification let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) self.displayIncomingCall(uuid: uuid, handle: handle, hasVideo: false) { _ in UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } } } func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { print("\(#function) token invalidated") } /// Display the incoming call to the user func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion) } } ================================================ FILE: CallKit/CallKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CallKit/CallKitDemo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CallKit/CallKitDemo/Assets.xcassets/IconMask.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "IconMask-40.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "IconMask-80.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "IconMask-120.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CallKit/CallKitDemo/Assets.xcassets/baseHeroMount.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x", "filename" : "baseHeroMount.png" }, { "idiom" : "universal", "scale" : "2x", "filename" : "baseHeroMount@2x.png" }, { "idiom" : "universal", "scale" : "3x", "filename" : "baseHeroMount@3x.png" } ], "info" : { "author" : "zeplin", "version" : "1" } } ================================================ FILE: CallKit/CallKitDemo/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: CallKit/CallKitDemo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: CallKit/CallKitDemo/CallKitDemo-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import "OTDefaultAudioDevice.h" ================================================ FILE: CallKit/CallKitDemo/CallKitDemo.entitlements ================================================ aps-environment development ================================================ FILE: CallKit/CallKitDemo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName $(PRODUCT_BUNDLE_IDENTIFIER).url-scheme.dial CFBundleURLSchemes callkitdemo CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription $(PRODUCT_NAME) uses camera NSMicrophoneUsageDescription $(PRODUCT_NAME) uses microphone UIBackgroundModes audio fetch remote-notification voip UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait ================================================ FILE: CallKit/CallKitDemo/OTDefaultAudioDevice.h ================================================ // // OTAudioDeviceIOSDefault.h // // Copyright (c) 2014 TokBox, Inc. All rights reserved. // #import #import #define kMixerInputBusCount 2 #define kOutputBus 0 #define kInputBus 1 #define AUDIO_DEVICE_HEADSET @"AudioSessionManagerDevice_Headset" #define AUDIO_DEVICE_BLUETOOTH @"AudioSessionManagerDevice_Bluetooth" #define AUDIO_DEVICE_SPEAKER @"AudioSessionManagerDevice_Speaker" @interface OTDefaultAudioDevice : NSObject { AudioStreamBasicDescription stream_format; } /** Returns YES if a wired headset is available. */ @property (nonatomic, readonly) BOOL headsetDeviceAvailable; /** Returns YES if a bluetooth device is available. */ @property (nonatomic, readonly) BOOL bluetoothDeviceAvailable; - (BOOL)setAudioBus:(id)audioBus; /** * Audio device lifecycle should live for the duration of the process, and * needs to be set before OTSession is initialized. * * It is not recommended to initialize unique audio device instances. */ + (instancetype)sharedInstance; + (instancetype)sharedInstanceWithAudioSession:(AVAudioSession *)audioSession; - (OTAudioFormat*)captureFormat; - (OTAudioFormat*)renderFormat; - (BOOL)renderingIsAvailable; - (BOOL)initializeRendering; - (BOOL)renderingIsInitialized; - (BOOL)captureIsAvailable; - (BOOL)initializeCapture; - (BOOL)captureIsInitialized; - (BOOL)startRendering; - (BOOL)stopRendering; - (BOOL)isRendering; - (BOOL)startCapture; - (BOOL)stopCapture; - (BOOL)isCapturing; - (uint16_t)estimatedRenderDelay; - (uint16_t)estimatedCaptureDelay; //desired Audio Route can be bluetooth and headset. //bluetooth has higher priority of all, next headset, next speaker - (BOOL)configureAudioSessionWithDesiredAudioRoute:(NSString*)desiredAudioRoute; - (BOOL)detectCurrentRoute; - (BOOL)setPlayOutRenderCallback:(AudioUnit)unit; @end ================================================ FILE: CallKit/CallKitDemo/OTDefaultAudioDevice.m ================================================ // // OTDefaultAudioDeviceIOS.m // // Copyright (c) 2014 TokBox, Inc. All rights reserved. // #import "OTDefaultAudioDevice.h" #import #import #include #include /* * System Versioning Preprocessor Macros */ #define SYSTEM_VERSION_EQUAL_TO(v) \ ([[[UIDevice currentDevice] systemVersion] compare:v \ options:NSNumericSearch] == NSOrderedSame) #define SYSTEM_VERSION_GREATER_THAN(v) \ ([[[UIDevice currentDevice] systemVersion] compare:v \ options:NSNumericSearch] == NSOrderedDescending) #define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) \ ([[[UIDevice currentDevice] systemVersion] compare:v \ options:NSNumericSearch] != NSOrderedAscending) #define SYSTEM_VERSION_LESS_THAN(v) \ ([[[UIDevice currentDevice] systemVersion] compare:v \ options:NSNumericSearch] == NSOrderedAscending) #define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) \ ([[[UIDevice currentDevice] systemVersion] compare:v \ options:NSNumericSearch] != NSOrderedDescending) // Simulator *must* run at 44.1 kHz in order to function properly. #if (TARGET_IPHONE_SIMULATOR) #define kSampleRate 44100 #else #define kSampleRate 48000 #endif #define OT_ENABLE_AUDIO_DEBUG 0 #if OT_ENABLE_AUDIO_DEBUG #define OT_AUDIO_DEBUG(fmt, ...) NSLog(fmt, ##__VA_ARGS__) #else #define OT_AUDIO_DEBUG(fmt, ...) #endif static double kPreferredIOBufferDuration = 0.01; static mach_timebase_info_data_t info; static OSStatus recording_cb(void *ref_con, AudioUnitRenderActionFlags *action_flags, const AudioTimeStamp *time_stamp, UInt32 bus_num, UInt32 num_frames, AudioBufferList *data); static OSStatus playout_cb(void *ref_con, AudioUnitRenderActionFlags *action_flags, const AudioTimeStamp *time_stamp, UInt32 bus_num, UInt32 num_frames, AudioBufferList *data); @interface OTDefaultAudioDevice () - (BOOL) setupAudioUnit:(AudioUnit *)voice_unit playout:(BOOL)isPlayout; - (void) setupListenerBlocks; @property (assign) BOOL isAudioSessionSetup; @end @implementation OTDefaultAudioDevice { OTAudioFormat *_audioFormat; AudioUnit recording_voice_unit; AudioUnit playout_voice_unit; BOOL playing; BOOL playout_initialized; BOOL recording; BOOL recording_initialized; BOOL interrupted_playback; NSString* _previousAVAudioSessionCategory; NSString* avAudioSessionMode; double avAudioSessionPreffSampleRate; NSInteger avAudioSessionChannels; BOOL isRecorderInterrupted; BOOL isPlayerInterrupted; BOOL areListenerBlocksSetup; BOOL _isResetting; int _restartRetryCount; AVAudioSession* _avAudioSession; /* synchronize all access to the audio subsystem */ dispatch_queue_t _safetyQueue; @public id _audioBus; AudioBufferList *buffer_list; uint32_t buffer_num_frames; uint32_t buffer_size; uint32_t _recordingDelay; uint32_t _playoutDelay; uint32_t _playoutDelayMeasurementCounter; uint32_t _recordingDelayHWAndOS; uint32_t _recordingDelayMeasurementCounter; Float64 _playout_AudioUnitProperty_Latency; Float64 _recording_AudioUnitProperty_Latency; } #pragma mark - OTAudioDeviceImplementation - (instancetype)init { self = [super init]; if (self) { _audioFormat = [[OTAudioFormat alloc] init]; _audioFormat.sampleRate = kSampleRate; _audioFormat.numChannels = 1; _safetyQueue = dispatch_queue_create("ot-audio-driver", DISPATCH_QUEUE_SERIAL); _restartRetryCount = 0; } return self; } + (instancetype)sharedInstance { static OTDefaultAudioDevice* _sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedInstance = [[OTDefaultAudioDevice alloc] init]; [_sharedInstance setupAudioSession:nil]; }); return _sharedInstance; } + (instancetype)sharedInstanceWithAudioSession:(AVAudioSession *)audioSession { static OTDefaultAudioDevice* _sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedInstance = [[OTDefaultAudioDevice alloc] init]; [_sharedInstance setupAudioSession:audioSession]; }); return _sharedInstance; } - (BOOL)setAudioBus:(id)audioBus { _audioBus = audioBus; _audioFormat = [[OTAudioFormat alloc] init]; _audioFormat.sampleRate = kSampleRate; _audioFormat.numChannels = 1; return YES; } - (void)dealloc { [self removeObservers]; [self teardownAudio]; _audioFormat = nil; } - (OTAudioFormat*)captureFormat { return _audioFormat; } - (OTAudioFormat*)renderFormat { return _audioFormat; } - (BOOL)renderingIsAvailable { return YES; } // Audio Unit lifecycle is bound to start/stop cycles, so we don't have much // to do here. - (BOOL)initializeRendering { if (playing) { return NO; } if (playout_initialized) { return YES; } playout_initialized = true; return YES; } - (BOOL)renderingIsInitialized { return playout_initialized; } - (BOOL)captureIsAvailable { return YES; } // Audio Unit lifecycle is bound to start/stop cycles, so we don't have much // to do here. - (BOOL)initializeCapture { if (recording) { return NO; } if (recording_initialized) { return YES; } recording_initialized = true; return YES; } - (BOOL)captureIsInitialized { return recording_initialized; } - (BOOL)startRendering { @synchronized(self) { OT_AUDIO_DEBUG(@"startRendering %d", playing); if (playing) { return YES; } playing = YES; // Initialize only when playout voice unit is already teardown if(playout_voice_unit == NULL) { if (NO == [self setupAudioUnit:&playout_voice_unit playout:YES]) { playing = NO; return NO; } } OSStatus result = AudioOutputUnitStart(playout_voice_unit); if (CheckError(result, @"startRendering.AudioOutputUnitStart")) { playing = NO; } return playing; } } - (BOOL)stopRendering { @synchronized(self) { OT_AUDIO_DEBUG(@"stopRendering %d", playing); isPlayerInterrupted = NO; if (!playing) { return YES; } playing = NO; OSStatus result = AudioOutputUnitStop(playout_voice_unit); if (CheckError(result, @"stopRendering.AudioOutputUnitStop")) { return NO; } // publisher is already closed if (!recording && !_isResetting) { OT_AUDIO_DEBUG(@"teardownAudio from stopRendering"); [self teardownAudio]; } return YES; } } - (BOOL)isRendering { return playing; } - (BOOL)startCapture { @synchronized(self) { OT_AUDIO_DEBUG(@"startCapture %d", recording); if (recording) { return YES; } recording = YES; // Initialize only when recording voice unit is already teardown if(recording_voice_unit == NULL) { if (NO == [self setupAudioUnit:&recording_voice_unit playout:NO]) { recording = NO; return NO; } } OSStatus result = AudioOutputUnitStart(recording_voice_unit); if (CheckError(result, @"startCapture.AudioOutputUnitStart")) { recording = NO; } return recording; } } - (BOOL)stopCapture { @synchronized(self) { OT_AUDIO_DEBUG(@"stopCapture %d", recording); isRecorderInterrupted = NO; if (!recording) { return YES; } recording = NO; OSStatus result = AudioOutputUnitStop(recording_voice_unit); if (CheckError(result, @"stopCapture.AudioOutputUnitStop")) { return NO; } [self freeupAudioBuffers]; // subscriber is already closed if (!playing && !_isResetting) { OT_AUDIO_DEBUG(@"teardownAudio from stopCapture"); [self teardownAudio]; } return YES; } } - (BOOL)isCapturing { return recording; } - (uint16_t)estimatedRenderDelay { return _playoutDelay; } - (uint16_t)estimatedCaptureDelay { return _recordingDelay; } #pragma mark - AudioSession Setup static NSString* FormatError(OSStatus error) { uint32_t as_int = CFSwapInt32HostToLittle(error); uint8_t* as_char = (uint8_t*) &as_int; // see if it appears to be a 4-char-code if (isprint(as_char[0]) && isprint(as_char[1]) && isprint(as_char[2]) && isprint(as_char[3])) { return [NSString stringWithFormat:@"%c%c%c%c", as_int >> 24, as_int >> 16, as_int >> 8, as_int]; } else { // no, format it as an integer return [NSString stringWithFormat:@"%d", error]; } } /** * @return YES if in error */ static bool CheckError(OSStatus error, NSString* function) { if (!error) return NO; NSString* error_string = FormatError(error); NSLog(@"ERROR[OpenTok]:Audio device error: %@ returned error: %@", function, error_string); return YES; } - (void)checkAndPrintError:(OSStatus)error function:(NSString *)function { CheckError(error,function); } - (void)disposePlayoutUnit { if (playout_voice_unit) { AudioUnitUninitialize(playout_voice_unit); AudioComponentInstanceDispose(playout_voice_unit); playout_voice_unit = NULL; } } - (void)disposeRecordUnit { if (recording_voice_unit) { AudioUnitUninitialize(recording_voice_unit); AudioComponentInstanceDispose(recording_voice_unit); recording_voice_unit = NULL; } } - (void) teardownAudio { [self disposePlayoutUnit]; [self disposeRecordUnit]; [self freeupAudioBuffers]; AVAudioSession *mySession = [AVAudioSession sharedInstance]; [mySession setCategory:_previousAVAudioSessionCategory error:nil]; [mySession setMode:avAudioSessionMode error:nil]; [mySession setPreferredSampleRate: avAudioSessionPreffSampleRate error: nil]; [mySession setPreferredInputNumberOfChannels:avAudioSessionChannels error:nil]; self.isAudioSessionSetup = NO; } - (void)freeupAudioBuffers { if (buffer_list && buffer_list->mBuffers[0].mData) { free(buffer_list->mBuffers[0].mData); buffer_list->mBuffers[0].mData = NULL; } if (buffer_list) { free(buffer_list); buffer_list = NULL; buffer_num_frames = 0; } } - (void) setupAudioSession { AVAudioSession *mySession = [AVAudioSession sharedInstance]; _previousAVAudioSessionCategory = mySession.category; avAudioSessionMode = mySession.mode; avAudioSessionPreffSampleRate = mySession.preferredSampleRate; avAudioSessionChannels = mySession.inputNumberOfChannels; [mySession setPreferredSampleRate: kSampleRate error: nil]; [mySession setPreferredInputNumberOfChannels:1 error:nil]; [mySession setPreferredIOBufferDuration:kPreferredIOBufferDuration error:nil]; NSError *error = nil; NSUInteger audioOptions = 0; #if !(TARGET_OS_TV) audioOptions |= AVAudioSessionCategoryOptionAllowBluetooth ; audioOptions |= AVAudioSessionCategoryOptionDefaultToSpeaker; [mySession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:audioOptions error:&error]; #else [mySession setCategory:AVAudioSessionCategoryPlayback withOptions:audioOptions error:&error]; #endif if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) { [mySession setMode:AVAudioSessionModeVideoChat error:nil]; } else { [mySession setMode:AVAudioSessionModeVoiceChat error:nil]; } if (error) OT_AUDIO_DEBUG(@"Audiosession setCategory %@",error); error = nil; [self setupListenerBlocks]; [mySession setActive:YES error:&error]; if (error) OT_AUDIO_DEBUG(@"Audiosession setActive %@",error); } - (void) setupAudioSession:(AVAudioSession *)audioSession { if (self.isAudioSessionSetup) return; self.isAudioSessionSetup = YES; AVAudioSession *mySession = audioSession; if (mySession == nil) { mySession = [AVAudioSession sharedInstance]; } _previousAVAudioSessionCategory = mySession.category; avAudioSessionMode = mySession.mode; avAudioSessionPreffSampleRate = mySession.preferredSampleRate; avAudioSessionChannels = mySession.inputNumberOfChannels; if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) { [mySession setMode:AVAudioSessionModeVideoChat error:nil]; } else { [mySession setMode:AVAudioSessionModeVoiceChat error:nil]; } [mySession setPreferredSampleRate: kSampleRate error: nil]; [mySession setPreferredInputNumberOfChannels:1 error:nil]; [mySession setPreferredIOBufferDuration:kPreferredIOBufferDuration error:nil]; NSError *error = nil; NSUInteger audioOptions = AVAudioSessionCategoryOptionMixWithOthers; #if !(TARGET_OS_TV) audioOptions |= AVAudioSessionCategoryOptionAllowBluetooth ; audioOptions |= AVAudioSessionCategoryOptionDefaultToSpeaker; [mySession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:audioOptions error:&error]; #else [mySession setCategory:AVAudioSessionCategoryPlayback withOptions:audioOptions error:&error]; #endif if (error) OT_AUDIO_DEBUG(@"Audiosession setCategory %@",error); error = nil; [self setupListenerBlocks]; [mySession setActive:YES error:&error]; if (error) OT_AUDIO_DEBUG(@"Audiosession setActive %@",error); } - (void)setBluetoothAsPrefferedInputDevice { // Apple's Bug(???) : Audio Interruption Ended notification won't be called // for bluetooth devices if we dont set preffered input as bluetooth. // Should work for non bluetooth routes/ports too. This makes both input // and output to bluetooth device if available. NSArray* bluetoothRoutes = @[AVAudioSessionPortBluetoothA2DP, AVAudioSessionPortBluetoothLE, AVAudioSessionPortBluetoothHFP]; NSArray* routes = [[AVAudioSession sharedInstance] availableInputs]; for (AVAudioSessionPortDescription* route in routes) { if ([bluetoothRoutes containsObject:route.portType]) { [[AVAudioSession sharedInstance] setPreferredInput:route error:nil]; break; } } } #pragma mark - System interruptions and audio route changes - (void) onInterruptionEvent:(NSNotification *) notification { OT_AUDIO_DEBUG(@"onInterruptionEvent %@",notification); NSDictionary *interruptionDict = notification.userInfo; NSInteger interruptionType = [[interruptionDict valueForKey:AVAudioSessionInterruptionTypeKey] integerValue]; dispatch_async(_safetyQueue, ^() { [self handleInterruptionEvent:interruptionType]; }); } - (void) handleInterruptionEvent:(NSInteger) interruptionType { @synchronized(self) { OT_AUDIO_DEBUG(@"handleInterruptionEvent %ld",(long)interruptionType); switch (interruptionType) { case AVAudioSessionInterruptionTypeBegan: { OT_AUDIO_DEBUG(@"AVAudioSessionInterruptionTypeBegan"); if(recording) { // DONT change the order of the following as // stopCapture sets isRecorderInterrupted to NO [self stopCapture]; isRecorderInterrupted = YES; } if(playing) { // DONT change the order of the following as // stopRendering sets isPlayerInterrupted to NO [self stopRendering]; isPlayerInterrupted = YES; } } break; case AVAudioSessionInterruptionTypeEnded: { OT_AUDIO_DEBUG(@"AVAudioSessionInterruptionTypeEnded"); // Reconfigure audio session with highest priority device [self configureAudioSessionWithDesiredAudioRoute: AUDIO_DEVICE_BLUETOOTH]; if(isRecorderInterrupted) { if([self startCapture] == YES) { isRecorderInterrupted = NO; _restartRetryCount = 0; } else { _restartRetryCount++; if(_restartRetryCount < 3) { dispatch_after( dispatch_time( DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), _safetyQueue, ^{ [self handleInterruptionEvent: AVAudioSessionInterruptionTypeEnded]; }); } else // This shouldn't happen! { // When a call get resumed, we get first audio interruption notification from iOS // but the audio fails, since iOS holds the audio session until user presses "Unhold call", // When user press "Unhold call" we post a fake interruption notification from "didActivate" audioSession callback // So for the second time, we need to keep isRecorderInterrupted and isPlayerInterrupted to YES. // The reason for this hack is, we don't want to modify the audio driver much to avoid future audio driver // upgrades from iOS SDK internal driver. //isRecorderInterrupted = NO; //isPlayerInterrupted = NO; _restartRetryCount = 0; NSLog(@"ERROR[OpenTok]:Unable to acquire audio session"); } return; } } if(isPlayerInterrupted) { isPlayerInterrupted = NO; [self startRendering]; } } break; default: OT_AUDIO_DEBUG(@"Audio Session Interruption Notification" " case default."); break; } } } - (void) onRouteChangeEvent:(NSNotification *) notification { OT_AUDIO_DEBUG(@"onRouteChangeEvent %@",notification); dispatch_async(_safetyQueue, ^() { [self handleRouteChangeEvent:notification]; }); } - (void) handleRouteChangeEvent:(NSNotification *) notification { NSDictionary *interruptionDict = notification.userInfo; NSInteger routeChangeReason = [[interruptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; // We'll receive a routeChangedEvent when the audio unit starts; don't // process events we caused internally. if (AVAudioSessionRouteChangeReasonRouteConfigurationChange == routeChangeReason) { return; } if(routeChangeReason == AVAudioSessionRouteChangeReasonOverride || routeChangeReason == AVAudioSessionRouteChangeReasonCategoryChange) { NSString *oldOutputDeviceName = nil; NSString *currentOutputDeviceName = nil; AVAudioSessionRouteDescription* oldRouteDesc = [interruptionDict valueForKey:AVAudioSessionRouteChangePreviousRouteKey]; NSArray* outputs = [oldRouteDesc outputs]; if(outputs.count > 0) { AVAudioSessionPortDescription *portDesc = (AVAudioSessionPortDescription *)[outputs objectAtIndex:0]; oldOutputDeviceName = [portDesc portName]; } if([[[AVAudioSession sharedInstance] currentRoute] outputs].count > 0) { currentOutputDeviceName = [[[[[AVAudioSession sharedInstance] currentRoute] outputs] objectAtIndex:0] portName]; } // we need check this because some times we will receive category change // with the same device. if([oldOutputDeviceName isEqualToString:currentOutputDeviceName] || currentOutputDeviceName == nil || oldOutputDeviceName == nil) { return; } OT_AUDIO_DEBUG(@"routeChanged: old=%@ new=%@", oldOutputDeviceName, currentOutputDeviceName); } @synchronized(self) { // We've made it here, there's been a legit route change. // Restart the audio units with correct sample rate _isResetting = YES; if (recording) { [self stopCapture]; [self disposeRecordUnit]; [self startCapture]; } if (playing) { [self stopRendering]; [self disposePlayoutUnit]; [self startRendering]; } _isResetting = NO; } } /* When ringer is off, we dont get interruption ended callback as mentioned in apple doc : "There is no guarantee that a begin interruption will have an end interruption." The only caveat here is, some times we get two callbacks from interruption handler as well as from here which we handle synchronously with safteyQueue */ - (void) appDidBecomeActive:(NSNotification *) notification { OT_AUDIO_DEBUG(@"appDidBecomeActive %@",notification); dispatch_async(_safetyQueue, ^{ [self handleInterruptionEvent:AVAudioSessionInterruptionTypeEnded]; }); } - (void) setupListenerBlocks { if(!areListenerBlocksSetup) { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(onInterruptionEvent:) name:AVAudioSessionInterruptionNotification object:nil]; [center addObserver:self selector:@selector(onRouteChangeEvent:) name:AVAudioSessionRouteChangeNotification object:nil]; [center addObserver:self selector:@selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; areListenerBlocksSetup = YES; } } - (void) removeObservers { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver:self]; areListenerBlocksSetup = NO; } #pragma mark - native audio callbacks static void update_recording_delay(OTDefaultAudioDevice* device) { device->_recordingDelayMeasurementCounter++; if (device->_recordingDelayMeasurementCounter >= 100) { // Update HW and OS delay every second, unlikely to change device->_recordingDelayHWAndOS = 0; AVAudioSession *mySession = [AVAudioSession sharedInstance]; // HW input latency NSTimeInterval interval = [mySession inputLatency]; device->_recordingDelayHWAndOS += (int)(interval * 1000000); // HW buffer duration interval = [mySession IOBufferDuration]; device->_recordingDelayHWAndOS += (int)(interval * 1000000); device->_recordingDelayHWAndOS += (int)(device->_recording_AudioUnitProperty_Latency * 1000000); // To ms device->_recordingDelayHWAndOS = (device->_recordingDelayHWAndOS - 500) / 1000; // Reset counter device->_recordingDelayMeasurementCounter = 0; } device->_recordingDelay = device->_recordingDelayHWAndOS; } static OSStatus recording_cb(void *ref_con, AudioUnitRenderActionFlags *action_flags, const AudioTimeStamp *time_stamp, UInt32 bus_num, UInt32 num_frames, AudioBufferList *data) { OTDefaultAudioDevice *dev = (__bridge OTDefaultAudioDevice*) ref_con; if (!dev->buffer_list || num_frames > dev->buffer_num_frames) { if (dev->buffer_list) { free(dev->buffer_list->mBuffers[0].mData); free(dev->buffer_list); } dev->buffer_list = (AudioBufferList*)malloc(sizeof(AudioBufferList) + sizeof(AudioBuffer)); dev->buffer_list->mNumberBuffers = 1; dev->buffer_list->mBuffers[0].mNumberChannels = 1; dev->buffer_list->mBuffers[0].mDataByteSize = num_frames*sizeof(UInt16); dev->buffer_list->mBuffers[0].mData = malloc(num_frames*sizeof(UInt16)); dev->buffer_num_frames = num_frames; dev->buffer_size = dev->buffer_list->mBuffers[0].mDataByteSize; } OSStatus status; status = AudioUnitRender(dev->recording_voice_unit, action_flags, time_stamp, 1, num_frames, dev->buffer_list); if (status != noErr) { CheckError(status, @"AudioUnitRender"); } if (dev->recording) { // Some sample code to generate a sine wave instead of use the mic // static double startingFrameCount = 0; // double j = startingFrameCount; // double cycleLength = kSampleRate. / 880.0; // int frame = 0; // for (frame = 0; frame < num_frames; ++frame) // { // int16_t* data = (int16_t*)dev->buffer_list->mBuffers[0].mData; // Float32 sample = (Float32)sin (2 * M_PI * (j / cycleLength)); // (data)[frame] = (sample * 32767.0f); // j += 1.0; // if (j > cycleLength) // j -= cycleLength; // } // startingFrameCount = j; [dev->_audioBus writeCaptureData:dev->buffer_list->mBuffers[0].mData numberOfSamples:num_frames]; } // some ocassions, AudioUnitRender only renders part of the buffer and then next // call to the AudioUnitRender fails with smaller buffer. if (dev->buffer_size != dev->buffer_list->mBuffers[0].mDataByteSize) dev->buffer_list->mBuffers[0].mDataByteSize = dev->buffer_size; update_recording_delay(dev); return noErr; } static void update_playout_delay(OTDefaultAudioDevice* device) { device->_playoutDelayMeasurementCounter++; if (device->_playoutDelayMeasurementCounter >= 100) { // Update HW and OS delay every second, unlikely to change device->_playoutDelay = 0; AVAudioSession *mySession = [AVAudioSession sharedInstance]; // HW output latency NSTimeInterval interval = [mySession outputLatency]; device->_playoutDelay += (int)(interval * 1000000); // HW buffer duration interval = [mySession IOBufferDuration]; device->_playoutDelay += (int)(interval * 1000000); device->_playoutDelay += (int)(device->_playout_AudioUnitProperty_Latency * 1000000); // To ms device->_playoutDelay = (device->_playoutDelay - 500) / 1000; // Reset counter device->_playoutDelayMeasurementCounter = 0; } } static OSStatus playout_cb(void *ref_con, AudioUnitRenderActionFlags *action_flags, const AudioTimeStamp *time_stamp, UInt32 bus_num, UInt32 num_frames, AudioBufferList *buffer_list) { OTDefaultAudioDevice *dev = (__bridge OTDefaultAudioDevice*) ref_con; if (!dev->playing) { return 0; } uint32_t count = [dev->_audioBus readRenderData:buffer_list->mBuffers[0].mData numberOfSamples:num_frames]; if (count != num_frames) { //TODO: Not really an error, but conerning. Network issues? } update_playout_delay(dev); return 0; } #pragma mark - BlueTooth - (BOOL)isBluetoothDevice:(NSString*)portType { return ([portType isEqualToString:AVAudioSessionPortBluetoothA2DP] || [portType isEqualToString:AVAudioSessionPortBluetoothHFP]); } - (BOOL)detectCurrentRoute { // called on startup to initialize the devices that are available... OT_AUDIO_DEBUG(@"detect current route"); AVAudioSession *audioSession = [AVAudioSession sharedInstance]; _headsetDeviceAvailable = _bluetoothDeviceAvailable = NO; //ios 8.0 complains about Deactivating an audio session that has running // I/O. All I/O should be stopped or paused prior to deactivating the audio // session. Looks like we can get away by not using the setActive call if (SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(@"7.0")) { // close down our current session... [audioSession setActive:NO error:nil]; // start a new audio session. Without activation, the default route will // always be (inputs: null, outputs: Speaker) [audioSession setActive:YES error:nil]; } // Check for current route AVAudioSessionRouteDescription *currentRoute = [audioSession currentRoute]; for (AVAudioSessionPortDescription *output in currentRoute.outputs) { if ([[output portType] isEqualToString:AVAudioSessionPortHeadphones]) { _headsetDeviceAvailable = YES; } else if ([self isBluetoothDevice:[output portType]]) { _bluetoothDeviceAvailable = YES; } } if (_headsetDeviceAvailable) { OT_AUDIO_DEBUG(@"Current route is Headset"); } if (_bluetoothDeviceAvailable) { OT_AUDIO_DEBUG(@"Current route is Bluetooth"); } if(!_bluetoothDeviceAvailable && !_headsetDeviceAvailable) { OT_AUDIO_DEBUG(@"Current route is device speaker"); } return YES; } - (BOOL)configureAudioSessionWithDesiredAudioRoute:(NSString*)desiredAudioRoute { OT_AUDIO_DEBUG(@"configureAudioSessionWithDesiredAudioRoute %@",desiredAudioRoute); AVAudioSession *audioSession = [AVAudioSession sharedInstance]; NSError *err; //ios 8.0 complains about Deactivating an audio session that has running // I/O. All I/O should be stopped or paused prior to deactivating the audio // session. Looks like we can get away by not using the setActive call if (SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(@"7.0")) { // close down our current session... [audioSession setActive:NO error:nil]; } if ([AUDIO_DEVICE_BLUETOOTH isEqualToString:desiredAudioRoute]) { [self setBluetoothAsPrefferedInputDevice]; } if (SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(@"7.0")) { // Set our session to active... if (![audioSession setActive:YES error:&err]) { NSLog(@"unable to set audio session active: %@", err); return NO; } } if ([AUDIO_DEVICE_SPEAKER isEqualToString:desiredAudioRoute]) { // replace AudiosessionSetProperty (deprecated from iOS7) with // AVAudioSession overrideOutputAudioPort #if !(TARGET_OS_TV) [audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&err]; #endif } else { [audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&err]; } return YES; } - (BOOL)setupAudioUnit:(AudioUnit *)voice_unit playout:(BOOL)isPlayout; { OSStatus result; mach_timebase_info(&info); if (!self.isAudioSessionSetup) { [self setupAudioSession]; self.isAudioSessionSetup = YES; } UInt32 bytesPerSample = sizeof(SInt16); stream_format.mFormatID = kAudioFormatLinearPCM; stream_format.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; stream_format.mBytesPerPacket = bytesPerSample; stream_format.mFramesPerPacket = 1; stream_format.mBytesPerFrame = bytesPerSample; stream_format.mChannelsPerFrame= 1; stream_format.mBitsPerChannel = 8 * bytesPerSample; stream_format.mSampleRate = (Float64) kSampleRate; AudioComponentDescription audio_unit_description; audio_unit_description.componentType = kAudioUnitType_Output; #if !(TARGET_OS_TV) audio_unit_description.componentSubType = kAudioUnitSubType_VoiceProcessingIO; #else audio_unit_description.componentSubType = kAudioUnitSubType_RemoteIO; #endif audio_unit_description.componentManufacturer = kAudioUnitManufacturer_Apple; audio_unit_description.componentFlags = 0; audio_unit_description.componentFlagsMask = 0; AudioComponent found_vpio_unit_ref = AudioComponentFindNext(NULL, &audio_unit_description); result = AudioComponentInstanceNew(found_vpio_unit_ref, voice_unit); if (CheckError(result, @"setupAudioUnit.AudioComponentInstanceNew")) { return NO; } if (!isPlayout) { UInt32 enable_input = 1; AudioUnitSetProperty(*voice_unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &enable_input, sizeof(enable_input)); AudioUnitSetProperty(*voice_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &stream_format, sizeof (stream_format)); AURenderCallbackStruct input_callback; input_callback.inputProc = recording_cb; input_callback.inputProcRefCon = (__bridge void *)(self); AudioUnitSetProperty(*voice_unit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, kInputBus, &input_callback, sizeof(input_callback)); UInt32 flag = 0; AudioUnitSetProperty(*voice_unit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, kInputBus, &flag, sizeof(flag)); // Disable Output on record UInt32 enable_output = 0; AudioUnitSetProperty(*voice_unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &enable_output, sizeof(enable_output)); } else { UInt32 enable_output = 1; AudioUnitSetProperty(*voice_unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &enable_output, sizeof(enable_output)); AudioUnitSetProperty(*voice_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &stream_format, sizeof (stream_format)); // Disable Input on playout UInt32 enable_input = 0; AudioUnitSetProperty(*voice_unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &enable_input, sizeof(enable_input)); [self setPlayOutRenderCallback:*voice_unit]; } Float64 f64 = 0; UInt32 size = sizeof(f64); OSStatus latency_result = AudioUnitGetProperty(*voice_unit, kAudioUnitProperty_Latency, kAudioUnitScope_Global, 0, &f64, &size); if (!isPlayout) { _recording_AudioUnitProperty_Latency = (0 == latency_result) ? f64 : 0; } else { _playout_AudioUnitProperty_Latency = (0 == latency_result) ? f64 : 0; } // Initialize the Voice-Processing I/O unit instance. result = AudioUnitInitialize(*voice_unit); if (CheckError(result, @"setupAudioUnit.AudioUnitInitialize")) { return NO; } [self setBluetoothAsPrefferedInputDevice]; return YES; } - (BOOL)setPlayOutRenderCallback:(AudioUnit)unit { AURenderCallbackStruct render_callback; render_callback.inputProc = playout_cb;; render_callback.inputProcRefCon = (__bridge void *)(self); OSStatus result = AudioUnitSetProperty(unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, kOutputBus, &render_callback, sizeof(render_callback)); return (result == 0); } @end ================================================ FILE: CallKit/CallKitDemo/ProviderDelegate.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: CallKit provider delegate class, which conforms to CXProviderDelegate protocol */ import Foundation import UIKit import CallKit import AVFoundation import OpenTok final class ProviderDelegate: NSObject, CXProviderDelegate { let callManager: SpeakerboxCallManager private let provider: CXProvider init(callManager: SpeakerboxCallManager) { self.callManager = callManager provider = CXProvider(configuration: type(of: self).providerConfiguration) super.init() provider.setDelegate(self, queue: nil) } /// The app's provider configuration, representing its CallKit capabilities static var providerConfiguration: CXProviderConfiguration { let localizedName = NSLocalizedString("CallKitDemo", comment: "Name of application") let providerConfiguration = CXProviderConfiguration(localizedName: localizedName) providerConfiguration.supportsVideo = false providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber] providerConfiguration.iconTemplateImageData = #imageLiteral(resourceName: "IconMask").pngData() providerConfiguration.ringtoneSound = "Ringtone.caf" return providerConfiguration } // MARK: Incoming Calls /// Use CXProvider to report the incoming call to the system func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) update.hasVideo = hasVideo // pre-heat the AVAudioSession //OTAudioDeviceManager.setAudioDevice(OTDefaultAudioDevice.sharedInstance()) // Report the incoming call to the system provider.reportNewIncomingCall(with: uuid, update: update) { error in /* Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error) since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError. */ if error == nil { let call = SpeakerboxCall(uuid: uuid) call.handle = handle self.callManager.addCall(call) } completion?(error as NSError?) } } // MARK: CXProviderDelegate func providerDidReset(_ provider: CXProvider) { print("Provider did reset") /* End any ongoing calls if the provider resets, and remove them from the app's list of calls, since they are no longer valid. */ } var outgoingCall: SpeakerboxCall? func provider(_ provider: CXProvider, perform action: CXStartCallAction) { // Create & configure an instance of SpeakerboxCall, the app's model class representing the new outgoing call. let call = SpeakerboxCall(uuid: action.callUUID, isOutgoing: true) call.handle = action.handle.value /* Configure the audio session, but do not start call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. */ // https://forums.developer.apple.com/thread/64544 // we can't configure the audio session here for the case of launching it from locked screen // instead, we have to pre-heat the AVAudioSession by configuring as early as possible, didActivate do not get called otherwise // please look for * pre-heat the AVAudioSession * configureAudioSession() /* Set callback blocks for significant events in the call's lifecycle, so that the CXProvider may be updated to reflect the updated state. */ call.hasStartedConnectingDidChange = { [weak self] in self?.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate) } call.hasConnectedDidChange = { [weak self] in self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate) } self.outgoingCall = call // Signal to the system that the action has been successfully performed. action.fulfill() } var answerCall: SpeakerboxCall? func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } /* Configure the audio session, but do not start call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. */ // https://forums.developer.apple.com/thread/64544 // we can't configure the audio session here for the case of launching it from locked screen // instead, we have to pre-heat the AVAudioSession by configuring as early as possible, didActivate do not get called otherwise // please look for * pre-heat the AVAudioSession * configureAudioSession() self.answerCall = call // Signal to the system that the action has been successfully performed. action.fulfill() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } // Trigger the call to be ended via the underlying network service. call.endCall() // Signal to the system that the action has been successfully performed. action.fulfill() // Remove the ended call from the app's list of calls. callManager.removeCall(call) } func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } // Update the SpeakerboxCall's underlying hold state. call.isOnHold = action.isOnHold // Stop or start audio in response to holding or unholding the call. call.isMuted = call.isOnHold // Signal to the system that the action has been successfully performed. action.fulfill() } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } call.isMuted = action.isMuted // Signal to the system that the action has been successfully performed. action.fulfill() } func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { print("Timed out \(#function)") // React to the action timeout if necessary, such as showing an error UI. } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { print("Received \(#function)") // If we are returning from a hold state if answerCall?.hasConnected ?? false { //configureAudioSession() // See more details on how this works in the OTDefaultAudioDevice.m method handleInterruptionEvent sendFakeAudioInterruptionNotificationToStartAudioResources(); return } if outgoingCall?.hasConnected ?? false { //configureAudioSession() // See more details on how this works in the OTDefaultAudioDevice.m method handleInterruptionEvent sendFakeAudioInterruptionNotificationToStartAudioResources() return } // Start call audio media, now that the audio session has been activated after having its priority boosted. outgoingCall?.startCall(withAudioSession: audioSession) { [weak self] success in guard let outgoingCall = self?.outgoingCall else { return } if success { self?.callManager.addCall(outgoingCall) self?.outgoingCall?.startAudio() } else { self?.callManager.end(call: outgoingCall) } } answerCall?.answerCall(withAudioSession: audioSession) { success in if success { self.answerCall?.startAudio() } } } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { print("Received \(#function)") /* Restart any non-call related audio now that the app's audio session has been de-activated after having its priority restored to normal. */ if outgoingCall?.isOnHold ?? false || answerCall?.isOnHold ?? false { print("Call is on hold. Do not terminate any call") return } outgoingCall?.endCall() outgoingCall = nil answerCall?.endCall() answerCall = nil callManager.removeAllCalls() } func sendFakeAudioInterruptionNotificationToStartAudioResources() { var userInfo = Dictionary() let interrupttioEndedRaw = AVAudioSession.InterruptionType.ended.rawValue userInfo[AVAudioSessionInterruptionTypeKey] = interrupttioEndedRaw NotificationCenter.default.post(name: AVAudioSession.interruptionNotification, object: self, userInfo: userInfo) } func configureAudioSession() { // See https://forums.developer.apple.com/thread/64544 let session = AVAudioSession.sharedInstance() do { try session.setCategory(AVAudioSession.Category.playAndRecord, mode: .default) try session.setActive(true) try session.setMode(AVAudioSession.Mode.voiceChat) try session.setPreferredSampleRate(44100.0) try session.setPreferredIOBufferDuration(0.005) } catch { print(error) } } } ================================================ FILE: CallKit/CallKitDemo/SpeakerboxCall.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Model class representing a single call */ import Foundation import OpenTok final class SpeakerboxCall: NSObject { // MARK: Metadata Properties let uuid: UUID let isOutgoing: Bool var handle: String? // MARK: Call State Properties var connectingDate: Date? { didSet { stateDidChange?() hasStartedConnectingDidChange?() } } var connectDate: Date? { didSet { stateDidChange?() hasConnectedDidChange?() } } var endDate: Date? { didSet { stateDidChange?() hasEndedDidChange?() } } var isOnHold = false { didSet { publisher?.publishAudio = !isOnHold stateDidChange?() } } var isMuted = false { didSet { publisher?.publishAudio = !isMuted } } // MARK: State change callback blocks var stateDidChange: (() -> Void)? var hasStartedConnectingDidChange: (() -> Void)? var hasConnectedDidChange: (() -> Void)? var hasEndedDidChange: (() -> Void)? var audioChange: (() -> Void)? // MARK: Derived Properties var hasStartedConnecting: Bool { get { return connectingDate != nil } set { connectingDate = newValue ? Date() : nil } } var hasConnected: Bool { get { return connectDate != nil } set { connectDate = newValue ? Date() : nil } } var hasEnded: Bool { get { return endDate != nil } set { endDate = newValue ? Date() : nil } } var duration: TimeInterval { guard let connectDate = connectDate else { return 0 } return Date().timeIntervalSince(connectDate) } // MARK: Initialization init(uuid: UUID, isOutgoing: Bool = false) { self.uuid = uuid self.isOutgoing = isOutgoing } // MARK: Actions var session: OTSession? var publisher: OTPublisher? var subscriber: OTSubscriber? var canStartCall: ((Bool) -> Void)? func startCall(withAudioSession audioSession: AVAudioSession, completion: ((_ success: Bool) -> Void)?) { OTAudioDeviceManager.setAudioDevice(OTDefaultAudioDevice.sharedInstance(with: audioSession)) if session == nil { session = OTSession(apiKey: apiKey, sessionId: sessionId, delegate: self) } canStartCall = completion var error: OTError? hasStartedConnecting = true session?.connect(withToken: token, error: &error) if error != nil { print(error!) } } var canAnswerCall: ((Bool) -> Void)? func answerCall(withAudioSession audioSession: AVAudioSession, completion: ((_ success: Bool) -> Void)?) { OTAudioDeviceManager.setAudioDevice(OTDefaultAudioDevice.sharedInstance(with: audioSession)) if session == nil { session = OTSession(apiKey: apiKey, sessionId: sessionId, delegate: self) } canAnswerCall = completion var error: OTError? hasStartedConnecting = true session?.connect(withToken: token, error: &error) if error != nil { print(error!) } } func startAudio() { if publisher == nil { let settings = OTPublisherSettings() settings.name = UIDevice.current.name settings.audioTrack = true settings.videoTrack = false publisher = OTPublisher.init(delegate: self, settings: settings) } var error: OTError? session?.publish(publisher!, error: &error) if error != nil { print(error!) } } func endCall() { /* Simulate the end taking effect immediately, since the example app is not backed by a real network service */ if let publisher = publisher { var error: OTError? session?.unpublish(publisher, error: &error) if error != nil { print(error!) } } publisher = nil if let session = session { var error: OTError? session.disconnect(&error) if error != nil { print(error!) } } session = nil hasEnded = true } } extension SpeakerboxCall: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print(#function) hasConnected = true canStartCall?(true) canAnswerCall?(true) } func sessionDidDisconnect(_ session: OTSession) { print(#function) } func sessionDidBeginReconnecting(_ session: OTSession) { print(#function) } func sessionDidReconnect(_ session: OTSession) { print(#function) } func session(_ session: OTSession, didFailWithError error: OTError) { print(#function, error) hasConnected = false canStartCall?(false) canAnswerCall?(false) } func session(_ session: OTSession, streamCreated stream: OTStream) { print(#function) subscriber = OTSubscriber.init(stream: stream, delegate: self) subscriber?.subscribeToVideo = false if let subscriber = subscriber { var error: OTError? session.subscribe(subscriber, error: &error) if error != nil { print(error!) } } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print(#function) } } extension SpeakerboxCall: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print(#function) } } extension SpeakerboxCall: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriber: OTSubscriberKit) { print(#function) } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print(#function) } } ================================================ FILE: CallKit/CallKitDemo/SpeakerboxCallManager.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Manager of SpeakerboxCalls, which demonstrates using a CallKit CXCallController to request actions on calls */ import UIKit import CallKit import OpenTok final class SpeakerboxCallManager: NSObject { enum Call: String { case start = "startCall" case end = "endCall" case hold = "holdCall" } let callController = CXCallController() // MARK: Actions func startCall(handle: String, video: Bool = false) { let handle = CXHandle(type: .phoneNumber, value: handle) let startCallAction = CXStartCallAction(call: UUID(), handle: handle) startCallAction.isVideo = video let transaction = CXTransaction() transaction.addAction(startCallAction) requestTransaction(transaction, action: Call.start.rawValue) } func end(call: SpeakerboxCall) { let endCallAction = CXEndCallAction(call: call.uuid) let transaction = CXTransaction() transaction.addAction(endCallAction) requestTransaction(transaction, action: Call.end.rawValue) } func setHeld(call: SpeakerboxCall, onHold: Bool) { let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold) let transaction = CXTransaction() transaction.addAction(setHeldCallAction) requestTransaction(transaction, action: Call.hold.rawValue) } private func requestTransaction(_ transaction: CXTransaction, action: String = "") { callController.request(transaction) { error in if let error = error { print("Error requesting transaction: \(error)") } else { print("Requested transaction \(action) successfully") } } } // MARK: Call Management static let CallsChangedNotification = Notification.Name("CallManagerCallsChangedNotification") private(set) var calls = [SpeakerboxCall]() func callWithUUID(uuid: UUID) -> SpeakerboxCall? { guard let index = calls.index(where: { $0.uuid == uuid }) else { return nil } return calls[index] } func addCall(_ call: SpeakerboxCall) { calls.append(call) call.stateDidChange = { [weak self] in self?.postCallsChangedNotification() } postCallsChangedNotification(userInfo: ["action": Call.start.rawValue]) } func removeCall(_ call: SpeakerboxCall) { calls = calls.filter {$0 === call} postCallsChangedNotification(userInfo: ["action": Call.end.rawValue]) } func removeAllCalls() { calls.removeAll() postCallsChangedNotification(userInfo: ["action": Call.end.rawValue]) } private func postCallsChangedNotification(userInfo: [String: Any]? = nil) { NotificationCenter.default.post(name: type(of: self).CallsChangedNotification, object: self, userInfo: userInfo) } } ================================================ FILE: CallKit/CallKitDemo/ViewController.swift ================================================ // // ViewController.swift // CallKitDemo // // Created by Xi Huang on 6/5/17. // Copyright © 2017 Tokbox, Inc. All rights reserved. // import UIKit class ViewController: UIViewController { fileprivate final let displayCaller = "Lucas Huang" fileprivate final let makeACallText = "Make a call" fileprivate final let unholdCallText = "Unhold Call" fileprivate final let simulateIncomingCallText = "Simulate Call" fileprivate final let simulateIncomingCallThreeSecondsText = "Simulate Call after 3s(Background)" fileprivate final let endCallText = "End call" override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NotificationCenter.default.addObserver(self, selector: #selector(handleCallsChangedNotification(notification:)), name: SpeakerboxCallManager.CallsChangedNotification, object: nil) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) NotificationCenter.default.removeObserver(self) } @IBOutlet weak var callButton: UIButton! @IBOutlet weak var simulateCallButton: UIButton! @IBOutlet weak var simulateCallButton2: UIButton! @IBAction func receiveCallLucas(_ sender: UIButton) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if simulateCallButton.titleLabel?.text == simulateIncomingCallText { appdelegate.displayIncomingCall(uuid: UUID(), handle: displayCaller) sender.setTitle(endCallText, for: .normal) sender.setTitleColor(.red, for: .normal) callButton.isEnabled = false simulateCallButton2.isEnabled = false } else { endCall() sender.setTitle(simulateIncomingCallText, for: .normal) sender.setTitleColor(.white, for: .normal) callButton.isEnabled = true simulateCallButton2.isEnabled = true } } @IBAction func receiveCallLucasAfterThreeSeconds(_ sender: UIButton) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if sender.titleLabel?.text == simulateIncomingCallThreeSecondsText { let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 3) { appdelegate.displayIncomingCall(uuid: UUID(), handle: "Lucas Huang", hasVideo: false) { _ in UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } } sender.setTitle(endCallText, for: .normal) sender.setTitleColor(.red, for: .normal) callButton.isEnabled = false simulateCallButton.isEnabled = false } else { endCall() sender.setTitle(simulateIncomingCallThreeSecondsText, for: .normal) sender.setTitleColor(.white, for: .normal) callButton.isEnabled = true simulateCallButton.isEnabled = true } } @IBAction func callButtonPressed(_ sender: UIButton) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if sender.titleLabel?.text == makeACallText { appdelegate.callManager.startCall(handle: displayCaller) sender.setTitle(endCallText, for: .normal) sender.setTitleColor(.red, for: .normal) simulateCallButton.isEnabled = false simulateCallButton2.isEnabled = false } else if sender.titleLabel?.text == unholdCallText { // This state set when user receives another call appdelegate.callManager.setHeld(call: appdelegate.callManager.calls[0], onHold: false) } else { endCall() sender.setTitle(makeACallText, for: .normal) sender.setTitleColor(.white, for: .normal) simulateCallButton.isEnabled = true simulateCallButton2.isEnabled = true } } @objc func handleCallsChangedNotification(notification: NSNotification) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if (appdelegate.callManager.calls.count > 0) { let call = appdelegate.callManager.calls[0] if call.isOnHold { callButton.setTitle(unholdCallText, for: .normal) } else if call.session != nil { callButton.setTitle(endCallText, for: .normal) callButton.setTitleColor(.red, for: .normal) } if let action = notification.userInfo?["action"] as? String, action == SpeakerboxCallManager.Call.end.rawValue { callButton.setTitle(makeACallText, for: .normal) callButton.setTitleColor(.white, for: .normal) callButton.isEnabled = true simulateCallButton.setTitle(simulateIncomingCallText, for: .normal) simulateCallButton.setTitleColor(.white, for: .normal) simulateCallButton.isEnabled = true simulateCallButton2.setTitle(simulateIncomingCallThreeSecondsText, for: .normal) simulateCallButton2.setTitleColor(.white, for: .normal) simulateCallButton2.isEnabled = true } } } fileprivate func endCall() { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } /* End any ongoing calls if the provider resets, and remove them from the app's list of calls, since they are no longer valid. */ for call in appdelegate.callManager.calls { appdelegate.callManager.end(call: call) } } } ================================================ FILE: CallKit/CallKitDemo.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A00A84251F3A772400B2862E /* OTDefaultAudioDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = A00A84221F3A772400B2862E /* OTDefaultAudioDevice.m */; }; A04D825E1EEB1E3E00EBA4CA /* SpeakerboxCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = A04D825C1EEB1E3E00EBA4CA /* SpeakerboxCall.swift */; }; A04D825F1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A04D825D1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift */; }; A04D82611EEB1EB000EBA4CA /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A04D82601EEB1EB000EBA4CA /* ProviderDelegate.swift */; }; A062C6241EE5FAA200FD64A3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A062C6231EE5FAA200FD64A3 /* AppDelegate.swift */; }; A062C6261EE5FAA200FD64A3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A062C6251EE5FAA200FD64A3 /* ViewController.swift */; }; A062C6291EE5FAA200FD64A3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A062C6271EE5FAA200FD64A3 /* Main.storyboard */; }; A062C62B1EE5FAA200FD64A3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A062C62A1EE5FAA200FD64A3 /* Assets.xcassets */; }; A062C62E1EE5FAA200FD64A3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A062C62C1EE5FAA200FD64A3 /* LaunchScreen.storyboard */; }; A0B080C81EF9BD4D0082691D /* Ringtone.caf in Resources */ = {isa = PBXBuildFile; fileRef = A0B080C71EF9BD4A0082691D /* Ringtone.caf */; }; A0F2087A1EEF442E00104C6C /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = A0F208791EEF442E00104C6C /* Podfile */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A00A84201F3A772400B2862E /* CallKitDemo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CallKitDemo-Bridging-Header.h"; sourceTree = ""; }; A00A84211F3A772400B2862E /* OTDefaultAudioDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OTDefaultAudioDevice.h; sourceTree = ""; }; A00A84221F3A772400B2862E /* OTDefaultAudioDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OTDefaultAudioDevice.m; sourceTree = ""; }; A04D825C1EEB1E3E00EBA4CA /* SpeakerboxCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerboxCall.swift; sourceTree = ""; }; A04D825D1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerboxCallManager.swift; sourceTree = ""; }; A04D82601EEB1EB000EBA4CA /* ProviderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; A062C6201EE5FAA200FD64A3 /* CallKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; A062C6231EE5FAA200FD64A3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A062C6251EE5FAA200FD64A3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; A062C6281EE5FAA200FD64A3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A062C62A1EE5FAA200FD64A3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A062C62D1EE5FAA200FD64A3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A062C62F1EE5FAA200FD64A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A0B080C71EF9BD4A0082691D /* Ringtone.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ringtone.caf; sourceTree = ""; }; A0F208791EEF442E00104C6C /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; A0F2087B1EEF449800104C6C /* CallKitDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CallKitDemo.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ A062C61D1EE5FAA200FD64A3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A062C6171EE5FAA200FD64A3 = { isa = PBXGroup; children = ( A0F208791EEF442E00104C6C /* Podfile */, A062C6221EE5FAA200FD64A3 /* CallKitDemo */, A062C6211EE5FAA200FD64A3 /* Products */, ); sourceTree = ""; }; A062C6211EE5FAA200FD64A3 /* Products */ = { isa = PBXGroup; children = ( A062C6201EE5FAA200FD64A3 /* CallKitDemo.app */, ); name = Products; sourceTree = ""; }; A062C6221EE5FAA200FD64A3 /* CallKitDemo */ = { isa = PBXGroup; children = ( A00A84211F3A772400B2862E /* OTDefaultAudioDevice.h */, A00A84221F3A772400B2862E /* OTDefaultAudioDevice.m */, A0B080C71EF9BD4A0082691D /* Ringtone.caf */, A0F2087B1EEF449800104C6C /* CallKitDemo.entitlements */, A062C6231EE5FAA200FD64A3 /* AppDelegate.swift */, A04D82601EEB1EB000EBA4CA /* ProviderDelegate.swift */, A062C6251EE5FAA200FD64A3 /* ViewController.swift */, A04D825C1EEB1E3E00EBA4CA /* SpeakerboxCall.swift */, A04D825D1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift */, A062C6271EE5FAA200FD64A3 /* Main.storyboard */, A062C62A1EE5FAA200FD64A3 /* Assets.xcassets */, A062C62C1EE5FAA200FD64A3 /* LaunchScreen.storyboard */, A062C62F1EE5FAA200FD64A3 /* Info.plist */, A00A84201F3A772400B2862E /* CallKitDemo-Bridging-Header.h */, ); path = CallKitDemo; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ A062C61F1EE5FAA200FD64A3 /* CallKitDemo */ = { isa = PBXNativeTarget; buildConfigurationList = A062C6321EE5FAA200FD64A3 /* Build configuration list for PBXNativeTarget "CallKitDemo" */; buildPhases = ( A062C61C1EE5FAA200FD64A3 /* Sources */, A062C61D1EE5FAA200FD64A3 /* Frameworks */, A062C61E1EE5FAA200FD64A3 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = CallKitDemo; productName = CallKitDemo; productReference = A062C6201EE5FAA200FD64A3 /* CallKitDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ A062C6181EE5FAA200FD64A3 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0830; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Tokbox, Inc."; TargetAttributes = { A062C61F1EE5FAA200FD64A3 = { CreatedOnToolsVersion = 8.3.2; LastSwiftMigration = 0830; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.BackgroundModes = { enabled = 1; }; com.apple.Push = { enabled = 1; }; }; }; }; }; buildConfigurationList = A062C61B1EE5FAA200FD64A3 /* Build configuration list for PBXProject "CallKitDemo" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = A062C6171EE5FAA200FD64A3; productRefGroup = A062C6211EE5FAA200FD64A3 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( A062C61F1EE5FAA200FD64A3 /* CallKitDemo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ A062C61E1EE5FAA200FD64A3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A0B080C81EF9BD4D0082691D /* Ringtone.caf in Resources */, A062C62E1EE5FAA200FD64A3 /* LaunchScreen.storyboard in Resources */, A062C62B1EE5FAA200FD64A3 /* Assets.xcassets in Resources */, A0F2087A1EEF442E00104C6C /* Podfile in Resources */, A062C6291EE5FAA200FD64A3 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ A062C61C1EE5FAA200FD64A3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A062C6261EE5FAA200FD64A3 /* ViewController.swift in Sources */, A04D825F1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift in Sources */, A062C6241EE5FAA200FD64A3 /* AppDelegate.swift in Sources */, A00A84251F3A772400B2862E /* OTDefaultAudioDevice.m in Sources */, A04D825E1EEB1E3E00EBA4CA /* SpeakerboxCall.swift in Sources */, A04D82611EEB1EB000EBA4CA /* ProviderDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A062C6271EE5FAA200FD64A3 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A062C6281EE5FAA200FD64A3 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; A062C62C1EE5FAA200FD64A3 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A062C62D1EE5FAA200FD64A3 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ A062C6301EE5FAA200FD64A3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; A062C6311EE5FAA200FD64A3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; A062C6331EE5FAA200FD64A3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = CallKitDemo/CallKitDemo.entitlements; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = CallKitDemo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.tokbox.CallKitDemo; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "CallKitDemo/CallKitDemo-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; }; name = Debug; }; A062C6341EE5FAA200FD64A3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = CallKitDemo/CallKitDemo.entitlements; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = CallKitDemo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.tokbox.CallKitDemo; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "CallKitDemo/CallKitDemo-Bridging-Header.h"; SWIFT_VERSION = 4.2; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ A062C61B1EE5FAA200FD64A3 /* Build configuration list for PBXProject "CallKitDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( A062C6301EE5FAA200FD64A3 /* Debug */, A062C6311EE5FAA200FD64A3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; A062C6321EE5FAA200FD64A3 /* Build configuration list for PBXNativeTarget "CallKitDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( A062C6331EE5FAA200FD64A3 /* Debug */, A062C6341EE5FAA200FD64A3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = A062C6181EE5FAA200FD64A3 /* Project object */; } ================================================ FILE: CallKit/CallKitDemo.xcodeproj/xcshareddata/xcschemes/CallKitDemo.xcscheme ================================================ ================================================ FILE: CallKit/LICENSE ================================================ MIT License Copyright (c) 2017 OpenTok Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: CallKit/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'CallKitDemo' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: CallKit/README.md ================================================ ![logo](./tokbox-logo.png) ## CallKit Integration with OpenTok A sample app to demonstrate how to integrate the [CallKit](https://developer.apple.com/documentation/callkit) into OpenTok iOS SDK. This sample app is built based on the [SpeakerBox](https://developer.apple.com/library/content/samplecode/Speakerbox/Introduction/Intro.html) app from [WWDC 2016 CallKit Session](https://developer.apple.com/videos/play/wwdc2016/230/) ### Install the project files Use CocoaPods to install the project files and dependencies. 1. Install CocoaPods as described in [CocoaPods Getting Started](https://guides.cocoapods.org/using/getting-started.html#getting-started). 1. In Terminal, `cd` to your project directory and type `pod install`. (Sometimes, `pod update` is magical) 1. Reopen your project in Xcode using the new `*.xcworkspace` file. ### Configure and build the app Configure the sample app code. Then, build and run the app. 1. The application **requires** values for **API Key**, **Session ID**, and **Token**. In the sample, you can get these values at the [OpenTok Developer Dashboard](https://dashboard.tokbox.com/). For production deployment, you must generate the **Session ID** and **Token** values using one of the [OpenTok Server SDKs](https://tokbox.com/developer/sdks/server/). 1. Replace the following empty strings with the corresponding **API Key**, **Session ID**, and **Token** values in `AppDelegate.swift`: ```swift let apiKey = "" let sessionId = "" let token = "" ``` 1. Use Xcode to build and run the app on an iOS simulator or device. ### Exploring the sample app ![demo](./demo.png) 1. **Make a call**: The iOS system boosts the call priority of the app. Then, the app starts publishing to OpenTok platform. You won't notice any differences until you go to the home screen. Two ways to verify: - A badge in home screen indicating an ongoing VoIP call. - An incoming native phone call will not interrupt the current VoIP call, instead it shows the option menu. ***You will need a device to test the followings*** 2. **Simulate an incoming call** The native incoming call screen appears. Upon acceptance, the iOS system opens the app and boosts the call priority. Then, the app starts publishing to OpenTok platform. ![unlock1](./unlock1.png) ---> ![unlock2](./unlock2.png) 3. **Simulate an incoming call after 3s(Background)** (After clicking the button, please lock your cell phone to test this scenario.) The system wakes up your cell phone by making a native calling screen appear. Upon acceptance (a slider is shown instead of two buttons for the locked screen), the phone stays locked and boosts the call priority. Then, the app (which runs in the background during that time) starts publishing to OpenTok platform. ![lock1](./lock1.png) ---> ![lock2](./lock2.png) 4. **Without simulation, use a push server or [NWPusher](https://github.com/noodlewerk/NWPusher) to call** This requires a few more steps to test: - create your certificate - configure your push notification backend or NWPusher - locate your device token for testing (launch the app and get it from the console) - send a remote notification from your backend or NWPusher **Notice**: You might want to use [OpenTok.js Sample App](https://github.com/opentok/opentok-web-samples/tree/master/Basic%20Video%20Chat) to test the sample app together. ### Exploring the codes A `CXProvider` object is responsible for reporting out-of-band notifications that occur to the system. To create one, you first need to initialize a `CXProviderConfiguration` object, which encapsulates the behaviors and capabilities of calls, to pass on to the `CXProvider` initializer. In order to receive telephony events of the provider, the provider needs to specify an object conforming to the `CXProviderDelegate` protocol. ```swift // create a provider configuration let localizedName = NSLocalizedString("CallKitDemo", comment: "Name of application") let providerConfiguration = CXProviderConfiguration(localizedName: localizedName) providerConfiguration.supportsVideo = false providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber] providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(#imageLiteral(resourceName: "IconMask")) providerConfiguration.ringtoneSound = "Ringtone.caf" // set up a provider provider = CXProvider(configuration: providerConfiguration) provider.setDelegate(self, queue: nil) ``` The `CXProviderDelegate` protocol defines events of the telephony provider (`CXProvider`) such as the call starting, the call being put on hold, or the provider’s audio session is activated. ```swift // MARK: CXProviderDelegate func providerDidReset(_ provider: CXProvider) { print("Provider did reset") } func provider(_ provider: CXProvider, perform action: CXStartCallAction) { print("Provider performs the start call action") /* Configure the audio session, but do not start call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. */ } func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { print("Provider performs the answer call action") /* Configure the audio session, but do not start call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. */ } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { print("Provider performs the end call action") // Trigger the call to be ended via the underlying network service. } func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { print("Provider performs the hold call action") } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { print("Provider performs the mute call action") } ``` The following methods indicate whether your VoIP call has been successfully priority boosted or recovered. ```swift func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { print("Timed out \(#function)") // React to the action timeout if necessary, such as showing an error UI. } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { // Start call audio media, now that the audio session has been activated after having its priority boosted. } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { /* Restart any non-call related audio now that the app's audio session has been de-activated after having its priority restored to normal. */ } ``` Let's explore how to make a call and answer a call on behalf of a user. To do that, we need a `CXCallController` object to interact with the system. The `CXCallController` object takes a `CXTransaction` object to request a telephony action (which will later trigger delegate methods above if succeed). To specify a telephony action in a transaction, you need to create your desired action object and associate them with the transaction. Each telephony action has a corresponding `CXAction` class such as `CXEndCallAction` for ending a call, `CXSetHeldCallAction` for setting a call on hold. Once you have it all ready, invoke the `request(_:completion:)` by passing a ready transaction object. Here's how you start a call: ```swift // create a CXAction let startCallAction = CXStartCallAction(call: UUID(), handle: CXHandle(type: .phoneNumber, value: handle)) // create a transaction let transaction = CXTransaction() transaction.addAction(startCallAction) // create a label let action = "startCall" callController.request(transaction) { error in if let error = error { print("Error requesting transaction: \(error)") } else { print("Requested transaction \(action) successfully") } } ``` As for answering a call, the `CallKit` framework provides a convenient API to present a native calling UI like the screen-shot below. By invoking `reportNewIncomingCall(with:update:completion:)` on the provider, you will have the same experience as receiving a native phone call. Often, this piece of code works with VoIP remote notification to make calls to a device/person like WhatsApp, WeChat, and Messenger etc. ```swift // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) // Report the incoming call to the system provider.reportNewIncomingCall(with: uuid, update: update) { error in /* Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error) since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError. */ } ``` ![call](./call.jpeg) ### A glitch There is a small [issue](https://forums.developer.apple.com/thread/64544) when accepting a call from a locked screen. The underlying audio session does not get activated propertly inside the CallKit framework. Apple's engineers propose a workaround by setting up the audio session as early as possible to make the case work out temporarily: ``` then a workaround would be to configure your app's audio session (call `configureAudioSession()`) earlier in your app's lifecycle, before the `-provider:performAnswerCallAction:` method is invoked. ``` ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/AppDelegate.swift ================================================ // // AppDelegate.swift // CallKitDemo // // Created by Xi Huang on 6/5/17. // Copyright © 2017 Tokbox, Inc. All rights reserved. // import UIKit import PushKit import CallKit import OpenTok let apiKey = "" let sessionId = "" let token = "" @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? let pushRegistry = PKPushRegistry(queue: DispatchQueue.main) let callManager = SpeakerboxCallManager() var providerDelegate: ProviderDelegate? // Trigger VoIP registration on launch func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let sessionManager = OTAudioDeviceManager.currentAudioSessionManager() sessionManager?.enableCallingServicesMode() providerDelegate = ProviderDelegate(callManager: callManager, sessionManager: sessionManager) pushRegistry.delegate = self pushRegistry.desiredPushTypes = [.voIP] return true } } extension AppDelegate: PKPushRegistryDelegate { func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { print("\(#function) voip token: \(credentials.token)") let deviceToken = credentials.token.reduce("", {$0 + String(format: "%02X", $1) }) print("\(#function) token is: \(deviceToken)") } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { print("\(#function) incoming voip notfication: \(payload.dictionaryPayload)") if let uuidString = payload.dictionaryPayload["UUID"] as? String, let handle = payload.dictionaryPayload["handle"] as? String, let uuid = UUID(uuidString: uuidString) { // display incoming call UI when receiving incoming voip notification let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) self.displayIncomingCall(uuid: uuid, handle: handle, hasVideo: false) { _ in UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } } } func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { print("\(#function) token invalidated") } /// Display the incoming call to the user func displayIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { providerDelegate?.reportIncomingCall(uuid: uuid, handle: handle, hasVideo: hasVideo, completion: completion) } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Assets.xcassets/IconMask.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "IconMask-40.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "IconMask-80.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "IconMask-120.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Assets.xcassets/baseHeroMount.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x", "filename" : "baseHeroMount.png" }, { "idiom" : "universal", "scale" : "2x", "filename" : "baseHeroMount@2x.png" }, { "idiom" : "universal", "scale" : "3x", "filename" : "baseHeroMount@3x.png" } ], "info" : { "author" : "zeplin", "version" : "1" } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Base.lproj/Main.storyboard ================================================ ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/CallKitDemo.entitlements ================================================ aps-environment development ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName $(PRODUCT_BUNDLE_IDENTIFIER).url-scheme.dial CFBundleURLSchemes callkitdemo CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription $(PRODUCT_NAME) uses camera NSMicrophoneUsageDescription $(PRODUCT_NAME) uses microphone UIBackgroundModes audio fetch remote-notification voip UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/ProviderDelegate.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: CallKit provider delegate class, which conforms to CXProviderDelegate protocol */ import Foundation import UIKit import CallKit import AVFoundation import OpenTok import Combine final class ProviderDelegate: NSObject, CXProviderDelegate { let callManager: SpeakerboxCallManager private let provider: CXProvider private let sessionManager: OTAudioSessionManager? let muteSubject = PassthroughSubject() private var cancellable: AnyCancellable? init(callManager: SpeakerboxCallManager, sessionManager: OTAudioSessionManager?) { self.callManager = callManager self.sessionManager = sessionManager provider = CXProvider(configuration: type(of: self).providerConfiguration) super.init() provider.setDelegate(self, queue: nil) setupMuteThrottle() } /// The app's provider configuration, representing its CallKit capabilities static var providerConfiguration: CXProviderConfiguration { let localizedName = NSLocalizedString("CallKitDemo", comment: "Name of application") let providerConfiguration = CXProviderConfiguration(localizedName: localizedName) providerConfiguration.supportsVideo = false providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.maximumCallGroups = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber] providerConfiguration.iconTemplateImageData = #imageLiteral(resourceName: "IconMask").pngData() providerConfiguration.ringtoneSound = "Ringtone.caf" return providerConfiguration } // MARK: Incoming Calls /// Use CXProvider to report the incoming call to the system func reportIncomingCall(uuid: UUID, handle: String, hasVideo: Bool = false, completion: ((NSError?) -> Void)? = nil) { // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) update.hasVideo = hasVideo update.supportsHolding = true // pre-heat the AVAudioSession // Report the incoming call to the system provider.reportNewIncomingCall(with: uuid, update: update) { error in /* Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error) since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError. */ if error == nil { let call = SpeakerboxCall(uuid: uuid) call.handle = handle self.callManager.addCall(call) } completion?(error as NSError?) } } // MARK: CXProviderDelegate func providerDidBegin(_ provider: CXProvider) { print("Provider did begin") } func providerDidReset(_ provider: CXProvider) { print("Provider did reset") /* End any ongoing calls if the provider resets, and remove them from the app's list of calls, since they are no longer valid. */ } var outgoingCall: SpeakerboxCall? func provider(_ provider: CXProvider, perform action: CXStartCallAction) { print("Received perform CXStartCallAction \(action.callUUID)") // Create & configure an instance of SpeakerboxCall, the app's model class representing the new outgoing call. let call = SpeakerboxCall(uuid: action.callUUID, isOutgoing: true) call.handle = action.handle.value /* Configure the audio session, but do not start call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. */ // https://forums.developer.apple.com/thread/64544 // we can't configure the audio session here for the case of launching it from locked screen // instead, we have to pre-heat the AVAudioSession by configuring as early as possible, didActivate do not get called otherwise // please look for * pre-heat the AVAudioSession * sessionManager?.preconfigureAudioSessionForCall(withMode: .voiceChat) /* Set callback blocks for significant events in the call's lifecycle, so that the CXProvider may be updated to reflect the updated state. */ call.hasStartedConnectingDidChange = { [weak self] in self?.setupHold(to: call.uuid) self?.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate) } call.hasConnectedDidChange = { [weak self] in self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate) } call.callDidEnd = { [weak self] reason in self?.provider.reportCall(with: call.uuid, endedAt: nil, reason: reason) } self.outgoingCall = call // Signal to the system that the action has been successfully performed. action.fulfill() } var answerCall: SpeakerboxCall? func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { print("Received perform CXAnswerCallAction") // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } /* Configure the audio session, but do not start call audio here, since it must be done once the audio session has been activated by the system after having its priority elevated. */ // https://forums.developer.apple.com/thread/64544 // we can't configure the audio session here for the case of launching it from locked screen // instead, we have to pre-heat the AVAudioSession by configuring as early as possible, didActivate do not get called otherwise // please look for * pre-heat the AVAudioSession * sessionManager?.preconfigureAudioSessionForCall(withMode: .voiceChat) self.answerCall = call call.callDidEnd = { [weak self] reason in self?.provider.reportCall(with: call.uuid, endedAt: Date(), reason: reason) } // Signal to the system that the action has been successfully performed. action.fulfill() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { print("Received perform CXEndCallAction") // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } // Trigger the call to be ended via the underlying network service. call.endCall() // Signal to the system that the action has been successfully performed. action.fulfill() call.stateDidChange = nil call.hasStartedConnectingDidChange = nil call.hasConnectedDidChange = nil call.hasEndedDidChange = nil call.audioChange = nil call.callDidEnd = nil // Remove the ended call from the app's list of calls. callManager.removeCall(call) } func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { print("Received perform CXSetHeldCallAction \(action.isOnHold)") // Retrieve the SpeakerboxCall instance corresponding to the action's call UUID guard let call = callManager.callWithUUID(uuid: action.callUUID) else { action.fail() return } // Update the SpeakerboxCall's underlying hold state. call.isOnHold = action.isOnHold // Stop or start audio in response to holding or unholding the call. updateMuteState(call.isOnHold) // Signal to the system that the action has been successfully performed. action.fulfill() } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { print("Received perform CXSetMutedCallAction \(action.isMuted)") updateMuteState(action.isMuted) action.fulfill() } func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { print("Timed out \(#function)") // React to the action timeout if necessary, such as showing an error UI. } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { print("Received didActivate") sessionManager?.audioSessionDidActivate(audioSession) // If we are returning from a hold state if answerCall?.hasConnected ?? false { return } if outgoingCall?.hasConnected ?? false { return } // Start call audio media, now that the audio session has been activated after having its priority boosted. outgoingCall?.startCall(withAudioSession: audioSession) { [weak self] success in guard let outgoingCall = self?.outgoingCall else { return } if success { self?.callManager.addCall(outgoingCall) self?.outgoingCall?.startAudio() } else { self?.callManager.end(call: outgoingCall) } } answerCall?.answerCall(withAudioSession: audioSession) { success in if success { self.answerCall?.startAudio() } } } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { print("Received didDeactivate") sessionManager?.audioSessionDidDeactivate(audioSession) /* Restart any non-call related audio now that the app's audio session has been de-activated after having its priority restored to normal. */ if outgoingCall?.isOnHold ?? false || answerCall?.isOnHold ?? false { print("Call is on hold. Do not terminate any call") return } outgoingCall?.endCall() outgoingCall = nil answerCall?.endCall() answerCall = nil callManager.removeAllCalls() } // MARK: Helpers func setupHold(to callUUID: UUID) { let update = CXCallUpdate() update.supportsHolding = true update.hasVideo = false provider.reportCall(with: callUUID, updated: update) } func setupMuteThrottle() { cancellable = muteSubject .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] isMuted in self?.applyMuteState(isMuted) } } func updateMuteState(_ isMuted: Bool) { muteSubject.send(isMuted) } private func applyMuteState(_ isMuted: Bool) { print("applyMuteState \(isMuted)") answerCall?.isMuted = isMuted outgoingCall?.isMuted = isMuted } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/SpeakerboxCall.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Model class representing a single call */ import Foundation import OpenTok import CallKit final class SpeakerboxCall: NSObject { // MARK: Metadata Properties let uuid: UUID let isOutgoing: Bool var handle: String? // MARK: Call State Properties var connectingDate: Date? { didSet { stateDidChange?() hasStartedConnectingDidChange?() } } var connectDate: Date? { didSet { stateDidChange?() hasConnectedDidChange?() } } var endDate: Date? { didSet { stateDidChange?() hasEndedDidChange?() } } var isOnHold = false { didSet { stateDidChange?() } } var isMuted = false { didSet { publisher?.publishAudio = !isMuted } } // MARK: State change callback blocks var stateDidChange: (() -> Void)? var hasStartedConnectingDidChange: (() -> Void)? var hasConnectedDidChange: (() -> Void)? var hasEndedDidChange: (() -> Void)? var audioChange: (() -> Void)? var callDidEnd: ((CXCallEndedReason) -> Void)? // MARK: Derived Properties var hasStartedConnecting: Bool { get { return connectingDate != nil } set { connectingDate = newValue ? Date() : nil } } var hasConnected: Bool { get { return connectDate != nil } set { connectDate = newValue ? Date() : nil } } var hasEnded: Bool { get { return endDate != nil } set { endDate = newValue ? Date() : nil } } var duration: TimeInterval { guard let connectDate = connectDate else { return 0 } return Date().timeIntervalSince(connectDate) } // MARK: Initialization init(uuid: UUID, isOutgoing: Bool = false) { self.uuid = uuid self.isOutgoing = isOutgoing } // MARK: Actions var session: OTSession? var publisher: OTPublisher? var subscriber: OTSubscriber? func assertSessionParams() { assert(!apiKey.isEmpty, "Empty API key, session will not be instantiated") assert(!sessionId.isEmpty, "Empty Session ID, session will not be instantiated") assert(!token.isEmpty, "Empty token, session will not connect") } var canStartCall: ((Bool) -> Void)? func startCall(withAudioSession audioSession: AVAudioSession, completion: ((_ success: Bool) -> Void)?) { if session == nil { assertSessionParams() session = OTSession(apiKey: apiKey, sessionId: sessionId, delegate: self) } canStartCall = completion var error: OTError? hasStartedConnecting = true session?.connect(withToken: token, error: &error) if let error = error { print(error) callDidEnd?(.failed) } } var canAnswerCall: ((Bool) -> Void)? func answerCall(withAudioSession audioSession: AVAudioSession, completion: ((_ success: Bool) -> Void)?) { if session == nil { assertSessionParams() session = OTSession(apiKey: apiKey, sessionId: sessionId, delegate: self) } canAnswerCall = completion var error: OTError? hasStartedConnecting = true session?.connect(withToken: token, error: &error) if let error = error { print(error) callDidEnd?(.failed) } } func startAudio() { if publisher == nil { let settings = OTPublisherSettings() settings.name = UIDevice.current.name settings.audioTrack = true settings.videoTrack = false publisher = OTPublisher(delegate: self, settings: settings) } var error: OTError? session?.publish(publisher!, error: &error) if let error = error { print(error) if let session = session { var error: OTError? session.disconnect(&error) if let error = error { print(error) } } callDidEnd?(.failed) } } func endCall() { /* Simulate the end taking effect immediately, since the example app is not backed by a real network service */ if let publisher = publisher { var error: OTError? session?.unpublish(publisher, error: &error) if error != nil { print(error!) } } publisher = nil if let session = session { var error: OTError? session.disconnect(&error) if let error = error { print(error) } } session = nil hasEnded = true } } extension SpeakerboxCall: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print(#function) hasConnected = true canStartCall?(true) canAnswerCall?(true) } func sessionDidDisconnect(_ session: OTSession) { print(#function) } func sessionDidBeginReconnecting(_ session: OTSession) { print(#function) } func sessionDidReconnect(_ session: OTSession) { print(#function) } func session(_ session: OTSession, didFailWithError error: OTError) { print(#function, error) hasConnected = false canStartCall?(false) canAnswerCall?(false) } func session(_ session: OTSession, streamCreated stream: OTStream) { print(#function) subscriber = OTSubscriber.init(stream: stream, delegate: self) subscriber?.subscribeToVideo = false if let subscriber = subscriber { var error: OTError? session.subscribe(subscriber, error: &error) if error != nil { print(error!) } } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print(#function) } } extension SpeakerboxCall: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print(#function) callDidEnd?(.failed) } } extension SpeakerboxCall: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriber: OTSubscriberKit) { print(#function) } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print(#function) callDidEnd?(.failed) } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/SpeakerboxCallManager.swift ================================================ /* Copyright (C) 2016 Apple Inc. All Rights Reserved. See LICENSE.txt for this sample’s licensing information Abstract: Manager of SpeakerboxCalls, which demonstrates using a CallKit CXCallController to request actions on calls */ import UIKit import CallKit import OpenTok final class SpeakerboxCallManager: NSObject { enum Call: String { case start = "startCall" case end = "endCall" case hold = "holdCall" } let callController = CXCallController() // MARK: Actions func startCall(handle: String, video: Bool = false) { print("SpeakerboxCallManager: startCall") let handle = CXHandle(type: .phoneNumber, value: handle) let startCallAction = CXStartCallAction(call: UUID(), handle: handle) startCallAction.isVideo = video let transaction = CXTransaction() transaction.addAction(startCallAction) requestTransaction(transaction, action: Call.start.rawValue) } func end(call: SpeakerboxCall) { print("SpeakerboxCallManager: end") let endCallAction = CXEndCallAction(call: call.uuid) let transaction = CXTransaction() transaction.addAction(endCallAction) requestTransaction(transaction, action: Call.end.rawValue) } func setHeld(call: SpeakerboxCall, onHold: Bool) { print("SpeakerboxCallManager: setHeld \(onHold)") let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold) let transaction = CXTransaction() transaction.addAction(setHeldCallAction) requestTransaction(transaction, action: Call.hold.rawValue) } private func requestTransaction(_ transaction: CXTransaction, action: String = "") { callController.request(transaction) { error in if let error = error { print("Error requesting transaction: \(error)") } else { print("Requested transaction \(action) successfully") } } } // MARK: Call Management static let CallsChangedNotification = Notification.Name("CallManagerCallsChangedNotification") private(set) var calls = [SpeakerboxCall]() func callWithUUID(uuid: UUID) -> SpeakerboxCall? { guard let index = calls.index(where: { $0.uuid == uuid }) else { return nil } return calls[index] } func addCall(_ call: SpeakerboxCall) { calls.append(call) call.stateDidChange = { [weak self] in self?.postCallsChangedNotification() } postCallsChangedNotification(userInfo: ["action": Call.start.rawValue]) } func removeCall(_ call: SpeakerboxCall) { calls = calls.filter {$0 === call} postCallsChangedNotification(userInfo: ["action": Call.end.rawValue]) } func removeAllCalls() { calls.removeAll() postCallsChangedNotification(userInfo: ["action": Call.end.rawValue]) } private func postCallsChangedNotification(userInfo: [String: Any]? = nil) { NotificationCenter.default.post(name: type(of: self).CallsChangedNotification, object: self, userInfo: userInfo) } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo/ViewController.swift ================================================ // // ViewController.swift // CallKitDemo // // Created by Xi Huang on 6/5/17. // Copyright © 2017 Tokbox, Inc. All rights reserved. // import UIKit class ViewController: UIViewController { fileprivate final let displayCaller = "Lucas Huang" fileprivate final let makeACallText = "Make a call" fileprivate final let unholdCallText = "Unhold Call" fileprivate final let simulateIncomingCallText = "Simulate Call" fileprivate final let simulateIncomingCallThreeSecondsText = "Simulate Call after 3s(Background)" fileprivate final let endCallText = "End call" override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NotificationCenter.default.addObserver(self, selector: #selector(handleCallsChangedNotification(notification:)), name: SpeakerboxCallManager.CallsChangedNotification, object: nil) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) NotificationCenter.default.removeObserver(self) } @IBOutlet weak var callButton: UIButton! @IBOutlet weak var simulateCallButton: UIButton! @IBOutlet weak var simulateCallButton2: UIButton! @IBAction func receiveCallLucas(_ sender: UIButton) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if simulateCallButton.titleLabel?.text == simulateIncomingCallText { appdelegate.displayIncomingCall(uuid: UUID(), handle: displayCaller) sender.setTitle(endCallText, for: .normal) sender.setTitleColor(.red, for: .normal) callButton.isEnabled = false simulateCallButton2.isEnabled = false } else { endCall() sender.setTitle(simulateIncomingCallText, for: .normal) sender.setTitleColor(.white, for: .normal) callButton.isEnabled = true simulateCallButton2.isEnabled = true } } @IBAction func receiveCallLucasAfterThreeSeconds(_ sender: UIButton) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if sender.titleLabel?.text == simulateIncomingCallThreeSecondsText { let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 3) { appdelegate.displayIncomingCall(uuid: UUID(), handle: "Lucas Huang", hasVideo: false) { _ in UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier) } } sender.setTitle(endCallText, for: .normal) sender.setTitleColor(.red, for: .normal) callButton.isEnabled = false simulateCallButton.isEnabled = false } else { endCall() sender.setTitle(simulateIncomingCallThreeSecondsText, for: .normal) sender.setTitleColor(.white, for: .normal) callButton.isEnabled = true simulateCallButton.isEnabled = true } } @IBAction func callButtonPressed(_ sender: UIButton) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if sender.titleLabel?.text == makeACallText { appdelegate.callManager.startCall(handle: displayCaller) sender.setTitle(endCallText, for: .normal) sender.setTitleColor(.red, for: .normal) simulateCallButton.isEnabled = false simulateCallButton2.isEnabled = false } else if sender.titleLabel?.text == unholdCallText { // This state set when user receives another call appdelegate.callManager.setHeld(call: appdelegate.callManager.calls[0], onHold: false) } else { endCall() sender.setTitle(makeACallText, for: .normal) sender.setTitleColor(.white, for: .normal) simulateCallButton.isEnabled = true simulateCallButton2.isEnabled = true } } @objc func handleCallsChangedNotification(notification: NSNotification) { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } if (appdelegate.callManager.calls.count > 0) { let call = appdelegate.callManager.calls[0] if call.isOnHold { callButton.setTitle(unholdCallText, for: .normal) } else if call.session != nil { callButton.setTitle(endCallText, for: .normal) callButton.setTitleColor(.red, for: .normal) } if let action = notification.userInfo?["action"] as? String, action == SpeakerboxCallManager.Call.end.rawValue { callButton.setTitle(makeACallText, for: .normal) callButton.setTitleColor(.white, for: .normal) callButton.isEnabled = true simulateCallButton.setTitle(simulateIncomingCallText, for: .normal) simulateCallButton.setTitleColor(.white, for: .normal) simulateCallButton.isEnabled = true simulateCallButton2.setTitle(simulateIncomingCallThreeSecondsText, for: .normal) simulateCallButton2.setTitleColor(.white, for: .normal) simulateCallButton2.isEnabled = true } } } fileprivate func endCall() { guard let appdelegate = UIApplication.shared.delegate as? AppDelegate else { print("appdelegate is missing") return } /* End any ongoing calls if the provider resets, and remove them from the app's list of calls, since they are no longer valid. */ for call in appdelegate.callManager.calls { appdelegate.callManager.end(call: call) } } } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 373FD26FC719771DA1FE9296 /* Pods_CallKitDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBCC6CE434A3AA747DD39696 /* Pods_CallKitDemo.framework */; }; A04D825E1EEB1E3E00EBA4CA /* SpeakerboxCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = A04D825C1EEB1E3E00EBA4CA /* SpeakerboxCall.swift */; }; A04D825F1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A04D825D1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift */; }; A04D82611EEB1EB000EBA4CA /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A04D82601EEB1EB000EBA4CA /* ProviderDelegate.swift */; }; A062C6241EE5FAA200FD64A3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A062C6231EE5FAA200FD64A3 /* AppDelegate.swift */; }; A062C6261EE5FAA200FD64A3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A062C6251EE5FAA200FD64A3 /* ViewController.swift */; }; A062C6291EE5FAA200FD64A3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A062C6271EE5FAA200FD64A3 /* Main.storyboard */; }; A062C62B1EE5FAA200FD64A3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A062C62A1EE5FAA200FD64A3 /* Assets.xcassets */; }; A062C62E1EE5FAA200FD64A3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A062C62C1EE5FAA200FD64A3 /* LaunchScreen.storyboard */; }; A0B080C81EF9BD4D0082691D /* Ringtone.caf in Resources */ = {isa = PBXBuildFile; fileRef = A0B080C71EF9BD4A0082691D /* Ringtone.caf */; }; A0F2087A1EEF442E00104C6C /* Podfile in Resources */ = {isa = PBXBuildFile; fileRef = A0F208791EEF442E00104C6C /* Podfile */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 6276A9694A42BCF2E7D4265A /* Pods-CallKitDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallKitDemo.debug.xcconfig"; path = "Target Support Files/Pods-CallKitDemo/Pods-CallKitDemo.debug.xcconfig"; sourceTree = ""; }; 6FA799CB2DE9E21C00977B46 /* OpenTok.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:PR6C39UQ38:Vonage"; lastKnownFileType = wrapper.xcframework; name = OpenTok.xcframework; path = Pods/OTXCFramework/OpenTok.xcframework; sourceTree = ""; }; 6FA799E12DE9F8A200977B46 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; 6FA799E32DE9F8C400977B46 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; 6FA799E52DE9F8CE00977B46 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 6FA799E72DE9F8D600977B46 /* OpenGLES.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGLES.framework; path = System/Library/Frameworks/OpenGLES.framework; sourceTree = SDKROOT; }; 6FA799E92DE9F8DF00977B46 /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; }; 6FA799EB2DE9F8E800977B46 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; 6FA799ED2DE9F8F000977B46 /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; 6FA799EF2DE9F8F700977B46 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; 6FA799F12DE9F8FD00977B46 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 6FA799F32DE9F90400977B46 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 6FA799F52DE9F90B00977B46 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; 6FA799F72DE9F91400977B46 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; 6FA799F92DE9F92C00977B46 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = System/Library/Frameworks/Network.framework; sourceTree = SDKROOT; }; 78944A861D86C7478ADBE4CE /* Pods-CallKitDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallKitDemo.release.xcconfig"; path = "Target Support Files/Pods-CallKitDemo/Pods-CallKitDemo.release.xcconfig"; sourceTree = ""; }; A04D825C1EEB1E3E00EBA4CA /* SpeakerboxCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerboxCall.swift; sourceTree = ""; }; A04D825D1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpeakerboxCallManager.swift; sourceTree = ""; }; A04D82601EEB1EB000EBA4CA /* ProviderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; A062C6201EE5FAA200FD64A3 /* CallKitDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallKitDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; A062C6231EE5FAA200FD64A3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A062C6251EE5FAA200FD64A3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; A062C6281EE5FAA200FD64A3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A062C62A1EE5FAA200FD64A3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A062C62D1EE5FAA200FD64A3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A062C62F1EE5FAA200FD64A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A0B080C71EF9BD4A0082691D /* Ringtone.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Ringtone.caf; sourceTree = ""; }; A0F208791EEF442E00104C6C /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; A0F2087B1EEF449800104C6C /* CallKitDemo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CallKitDemo.entitlements; sourceTree = ""; }; FBCC6CE434A3AA747DD39696 /* Pods_CallKitDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CallKitDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ A062C61D1EE5FAA200FD64A3 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 373FD26FC719771DA1FE9296 /* Pods_CallKitDemo.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 053AFDF26B19C87AB193D03C /* Pods */ = { isa = PBXGroup; children = ( 6276A9694A42BCF2E7D4265A /* Pods-CallKitDemo.debug.xcconfig */, 78944A861D86C7478ADBE4CE /* Pods-CallKitDemo.release.xcconfig */, ); path = Pods; sourceTree = ""; }; 6FA799CA2DE9E21C00977B46 /* Frameworks */ = { isa = PBXGroup; children = ( 6FA799F92DE9F92C00977B46 /* Network.framework */, 6FA799F72DE9F91400977B46 /* AVFoundation.framework */, 6FA799F52DE9F90B00977B46 /* AudioToolbox.framework */, 6FA799F32DE9F90400977B46 /* CoreFoundation.framework */, 6FA799F12DE9F8FD00977B46 /* CoreGraphics.framework */, 6FA799EF2DE9F8F700977B46 /* CoreMedia.framework */, 6FA799ED2DE9F8F000977B46 /* CoreTelephony.framework */, 6FA799EB2DE9F8E800977B46 /* CoreVideo.framework */, 6FA799E92DE9F8DF00977B46 /* GLKit.framework */, 6FA799E72DE9F8D600977B46 /* OpenGLES.framework */, 6FA799E52DE9F8CE00977B46 /* QuartzCore.framework */, 6FA799E32DE9F8C400977B46 /* SystemConfiguration.framework */, 6FA799E12DE9F8A200977B46 /* VideoToolbox.framework */, 6FA799CB2DE9E21C00977B46 /* OpenTok.xcframework */, FBCC6CE434A3AA747DD39696 /* Pods_CallKitDemo.framework */, ); name = Frameworks; sourceTree = ""; }; A062C6171EE5FAA200FD64A3 = { isa = PBXGroup; children = ( A0F208791EEF442E00104C6C /* Podfile */, A062C6221EE5FAA200FD64A3 /* CallKitDemo */, 6FA799CA2DE9E21C00977B46 /* Frameworks */, A062C6211EE5FAA200FD64A3 /* Products */, 053AFDF26B19C87AB193D03C /* Pods */, ); sourceTree = ""; }; A062C6211EE5FAA200FD64A3 /* Products */ = { isa = PBXGroup; children = ( A062C6201EE5FAA200FD64A3 /* CallKitDemo.app */, ); name = Products; sourceTree = ""; }; A062C6221EE5FAA200FD64A3 /* CallKitDemo */ = { isa = PBXGroup; children = ( A0B080C71EF9BD4A0082691D /* Ringtone.caf */, A0F2087B1EEF449800104C6C /* CallKitDemo.entitlements */, A062C6231EE5FAA200FD64A3 /* AppDelegate.swift */, A04D82601EEB1EB000EBA4CA /* ProviderDelegate.swift */, A062C6251EE5FAA200FD64A3 /* ViewController.swift */, A04D825C1EEB1E3E00EBA4CA /* SpeakerboxCall.swift */, A04D825D1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift */, A062C6271EE5FAA200FD64A3 /* Main.storyboard */, A062C62A1EE5FAA200FD64A3 /* Assets.xcassets */, A062C62C1EE5FAA200FD64A3 /* LaunchScreen.storyboard */, A062C62F1EE5FAA200FD64A3 /* Info.plist */, ); path = CallKitDemo; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ A062C61F1EE5FAA200FD64A3 /* CallKitDemo */ = { isa = PBXNativeTarget; buildConfigurationList = A062C6321EE5FAA200FD64A3 /* Build configuration list for PBXNativeTarget "CallKitDemo" */; buildPhases = ( 008B415415CD40B490AEF13B /* [CP] Check Pods Manifest.lock */, A062C61C1EE5FAA200FD64A3 /* Sources */, A062C61D1EE5FAA200FD64A3 /* Frameworks */, A062C61E1EE5FAA200FD64A3 /* Resources */, 6E75212811E76BF4FAF2A9DB /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = CallKitDemo; productName = CallKitDemo; productReference = A062C6201EE5FAA200FD64A3 /* CallKitDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ A062C6181EE5FAA200FD64A3 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0830; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Tokbox, Inc."; TargetAttributes = { A062C61F1EE5FAA200FD64A3 = { CreatedOnToolsVersion = 8.3.2; LastSwiftMigration = 0830; SystemCapabilities = { com.apple.BackgroundModes = { enabled = 1; }; com.apple.Push = { enabled = 1; }; }; }; }; }; buildConfigurationList = A062C61B1EE5FAA200FD64A3 /* Build configuration list for PBXProject "CallKitDemo" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, ); mainGroup = A062C6171EE5FAA200FD64A3; productRefGroup = A062C6211EE5FAA200FD64A3 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( A062C61F1EE5FAA200FD64A3 /* CallKitDemo */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ A062C61E1EE5FAA200FD64A3 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A0B080C81EF9BD4D0082691D /* Ringtone.caf in Resources */, A062C62E1EE5FAA200FD64A3 /* LaunchScreen.storyboard in Resources */, A062C62B1EE5FAA200FD64A3 /* Assets.xcassets in Resources */, A0F2087A1EEF442E00104C6C /* Podfile in Resources */, A062C6291EE5FAA200FD64A3 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 008B415415CD40B490AEF13B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-CallKitDemo-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 6E75212811E76BF4FAF2A9DB /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-CallKitDemo/Pods-CallKitDemo-resources.sh", "${PODS_CONFIGURATION_BUILD_DIR}/OTXCFramework/OTPrivacyResources.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/OTPrivacyResources.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CallKitDemo/Pods-CallKitDemo-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ A062C61C1EE5FAA200FD64A3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A062C6261EE5FAA200FD64A3 /* ViewController.swift in Sources */, A04D825F1EEB1E3E00EBA4CA /* SpeakerboxCallManager.swift in Sources */, A062C6241EE5FAA200FD64A3 /* AppDelegate.swift in Sources */, A04D825E1EEB1E3E00EBA4CA /* SpeakerboxCall.swift in Sources */, A04D82611EEB1EB000EBA4CA /* ProviderDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A062C6271EE5FAA200FD64A3 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A062C6281EE5FAA200FD64A3 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; A062C62C1EE5FAA200FD64A3 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A062C62D1EE5FAA200FD64A3 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ A062C6301EE5FAA200FD64A3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; A062C6311EE5FAA200FD64A3 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; A062C6331EE5FAA200FD64A3 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 6276A9694A42BCF2E7D4265A /* Pods-CallKitDemo.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = CallKitDemo/CallKitDemo.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PR6C39UQ38; INFOPLIST_FILE = CallKitDemo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.tokbox.CallKitDemo2; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; }; name = Debug; }; A062C6341EE5FAA200FD64A3 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 78944A861D86C7478ADBE4CE /* Pods-CallKitDemo.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = CallKitDemo/CallKitDemo.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PR6C39UQ38; INFOPLIST_FILE = CallKitDemo/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.tokbox.CallKitDemo2; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.2; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ A062C61B1EE5FAA200FD64A3 /* Build configuration list for PBXProject "CallKitDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( A062C6301EE5FAA200FD64A3 /* Debug */, A062C6311EE5FAA200FD64A3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; A062C6321EE5FAA200FD64A3 /* Build configuration list for PBXNativeTarget "CallKitDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( A062C6331EE5FAA200FD64A3 /* Debug */, A062C6341EE5FAA200FD64A3 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = A062C6181EE5FAA200FD64A3 /* Project object */; } ================================================ FILE: CallKit-with-native-OpenTok-support/CallKitDemo.xcodeproj/xcshareddata/xcschemes/CallKitDemo.xcscheme ================================================ ================================================ FILE: CallKit-with-native-OpenTok-support/LICENSE ================================================ MIT License Copyright (c) 2017 OpenTok Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: CallKit-with-native-OpenTok-support/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'CallKitDemo' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: CallKit-with-native-OpenTok-support/README.md ================================================ ![logo](./tokbox-logo.png) ## CallKit Integration with OpenTok A sample app to demonstrate how to integrate the [CallKit](https://developer.apple.com/documentation/callkit) into OpenTok iOS SDK. This sample app is built based on the [SpeakerBox](https://developer.apple.com/library/content/samplecode/Speakerbox/Introduction/Intro.html) app from [WWDC 2016 CallKit Session](https://developer.apple.com/videos/play/wwdc2016/230/). For more information, see [this documentation](https://tokbox.com/developer/guides/mobile/ios/#user-content-call-kit). ### Install the project files Use CocoaPods to install the project files and dependencies. 1. Install CocoaPods as described in [CocoaPods Getting Started](https://guides.cocoapods.org/using/getting-started.html#getting-started). 1. In Terminal, `cd` to your project directory and type `pod install`. (Sometimes, `pod update` is magical) 1. Reopen your project in Xcode using the new `*.xcworkspace` file. ### Configure and build the app Configure the sample app code. Then, build and run the app. 1. The application **requires** values for **API Key**, **Session ID**, and **Token**. In the sample, you can get these values at the [OpenTok Developer Dashboard](https://dashboard.tokbox.com/). For production deployment, you must generate the **Session ID** and **Token** values using one of the [OpenTok Server SDKs](https://tokbox.com/developer/sdks/server/). 1. Replace the following empty strings with the corresponding **API Key**, **Session ID**, and **Token** values in `AppDelegate.swift`: ```swift let apiKey = "" let sessionId = "" let token = "" ``` 1. Use Xcode to build and run the app on an iOS simulator or device. ### Exploring the sample app ![demo](./demo.png) 1. **Make a call**: The iOS system boosts the call priority of the app. Then, the app starts publishing to OpenTok platform. You won't notice any differences until you go to the home screen. Two ways to verify: - A badge in home screen indicating an ongoing VoIP call. - An incoming native phone call will not interrupt the current VoIP call, instead it shows the option menu. ***You will need a device to test the followings*** 2. **Simulate an incoming call** The native incoming call screen appears. Upon acceptance, the iOS system opens the app and boosts the call priority. Then, the app starts publishing to OpenTok platform. ![unlock1](./unlock1.png) ---> ![unlock2](./unlock2.png) 3. **Simulate an incoming call after 3s(Background)** (After clicking the button, please lock your cell phone to test this scenario.) The system wakes up your cell phone by making a native calling screen appear. Upon acceptance (a slider is shown instead of two buttons for the locked screen), the phone stays locked and boosts the call priority. Then, the app (which runs in the background during that time) starts publishing to OpenTok platform. ![lock1](./lock1.png) ---> ![lock2](./lock2.png) 4. **Or use the provided pu.sh script to simulate an incoming call** You will need to generate an APNs enabled P8 key in the Certificates, Identifiers & Profiles in the [Apple developer site](https://developer.apple.com/account/resources/authkeys/list). Modify the parameters in the script: - TEAMID: Your developer account team ID - KEYID: Key ID that is shown in the Apple developer auth keys list - SECRET: Path to the P8 key, like "~/example/AuthKey_XXXXXXXXX.p8" - BUNDLEID: the bundle ID of the app, followed by .voip, like "com.org.app.voip" - DEVICETOKEN: VoIP push token provided by PushKit Run the script in the terminal app. **Notice**: You might want to use [OpenTok.js Sample App](https://github.com/opentok/opentok-web-samples/tree/master/Basic%20Video%20Chat) to test the sample app together. ### Exploring the code For activating the native OpenTok CallKit support you will need to enable calling services mode. The `OTAudioDeviceManager.currentAudioSessionManager` instance notifies to the SDK that should not try to activate the audio sessions, instead it will wait for the the app audio session activation and deactivation events. It’s recommended that you configure the calling services mode in the application start, for example in the `AppDelegate` `didFinishLaunchingWithOptions`method. ```swift let sessionManager = OTAudioDeviceManager.currentAudioSessionManager() sessionManager?.enableCallingServicesMode() ``` A `CXProvider` object is responsible for reporting out-of-band notifications that occur to the system. To create one, you first need to initialize a `CXProviderConfiguration` object, which encapsulates the behaviors and capabilities of calls, to pass on to the `CXProvider` initializer. In order to receive telephony events of the provider, the provider needs to specify an object conforming to the `CXProviderDelegate` protocol. ```swift // create a provider configuration let localizedName = NSLocalizedString("CallKitDemo", comment: "Name of application") let providerConfiguration = CXProviderConfiguration(localizedName: localizedName) providerConfiguration.supportsVideo = false providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber] providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(#imageLiteral(resourceName: "IconMask")) providerConfiguration.ringtoneSound = "Ringtone.caf" // set up a provider provider = CXProvider(configuration: providerConfiguration) provider.setDelegate(self, queue: nil) ``` The `CXProviderDelegate` protocol defines events of the telephony provider (`CXProvider`) such as the call starting, the call being put on hold, or the provider’s audio session is activated. CallKit requires a `preheating` stage where the developer has to set up the required `AudioSession` configuration. The configuration is done for you by calling ```swift preconfigureAudioSessionForCall(withMode: .voiceChat) or preconfigureAudioSessionForCall(withMode: .videoChat) ``` [VoiceChat](https://developer.apple.com/documentation/avfaudio/avaudiosession/mode-swift.struct/voicechat) mode is commonly used for audio only apps like the Phone app. [VideoChat](https://developer.apple.com/documentation/avfaudio/avaudiosession/mode-swift.struct/videochat) mode is commonly used for video conferencing apps like Facetime. You will need to notify to the SDK about the audio session configuration stages as well as activations and deactivations in the CXProvider delegate callbacks as seen below: ```swift let sessionManager = OTAudioDeviceManager.currentAudioSessionManager() // MARK: CXProviderDelegate func providerDidReset(_ provider: CXProvider) { print("Provider did reset") } func provider(_ provider: CXProvider, perform action: CXStartCallAction) { print("Provider performs the start call action") // Pre-heating stage for outgoing calls sessionManager?.preconfigureAudioSessionForCall(withMode: .videoChat) } func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { print("Provider performs the answer call action") // Pre-heating stage for incoming calls sessionManager?.preconfigureAudioSessionForCall(withMode: .videoChat) } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { print("Provider performs the end call action") // Trigger the call to be ended via the underlying network service. } func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { print("Provider performs the hold call action") // You may want to mute/unmute the publisher here } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { print("Provider performs the mute call action") // You may want to mute/unmute the publisher here } ``` The following methods indicate whether your VoIP call has been successfully priority boosted or recovered. ```swift func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { print("Timed out \(#function)") // React to the action timeout if necessary, such as showing an error UI. } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { // Start call audio media, now that the audio session has been activated after having its priority boosted. sessionManager?.audioSessionDidActivate(audioSession) } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { /* Restart any non-call related audio now that the app's audio session has been de-activated after having its priority restored to normal. */ sessionManager?.audioSessionDidDeactivate(audioSession) } ``` Let's explore how to make a call and answer a call on behalf of a user. To do that, we need a `CXCallController` object to interact with the system. The `CXCallController` object takes a `CXTransaction` object to request a telephony action (which will later trigger delegate methods above if succeed). To specify a telephony action in a transaction, you need to create your desired action object and associate them with the transaction. Each telephony action has a corresponding `CXAction` class such as `CXEndCallAction` for ending a call, `CXSetHeldCallAction` for setting a call on hold. Once you have it all ready, invoke the `request(_:completion:)` by passing a ready transaction object. Here's how you start a call: ```swift // create a CXAction let startCallAction = CXStartCallAction(call: UUID(), handle: CXHandle(type: .phoneNumber, value: handle)) // create a transaction let transaction = CXTransaction() transaction.addAction(startCallAction) // create a label let action = "startCall" callController.request(transaction) { error in if let error = error { print("Error requesting transaction: \(error)") } else { print("Requested transaction \(action) successfully") } } ``` As for answering a call, the `CallKit` framework provides a convenient API to present a native calling UI like the screen-shot below. By invoking `reportNewIncomingCall(with:update:completion:)` on the provider, you will have the same experience as receiving a native phone call. Often, this piece of code works with VoIP remote notification to make calls to a device/person like WhatsApp, WeChat, and Messenger etc. ```swift // Construct a CXCallUpdate describing the incoming call, including the caller. let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .phoneNumber, value: handle) // Report the incoming call to the system provider.reportNewIncomingCall(with: uuid, update: update) { error in /* Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error) since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError. */ } ``` ![call](./call.jpeg) ================================================ FILE: CallKit-with-native-OpenTok-support/pu.sh ================================================ #!/bin/bash PAYLOAD="" uuid=$(uuidgen | tr '[:upper:]' '[:lower:]') if [ -z "$1" ] then PAYLOAD="{\"aps\":{\"content-available\" : 1}, \"UUID\":\"$uuid\", \"handle\":\"Ford Prefect\"}" else PAYLOAD=$(<$1) fi TEAMID="" KEYID="p8 key name here" SECRET="p8 file path" BUNDLEID="com.vonage.CallKitDemo.voip" DEVICETOKEN="your device voip token" function base64URLSafe { openssl base64 -e -A | tr -- '+/' '-_' | tr -d = } function sign { printf "$1"| openssl dgst -binary -sha256 -sign "$SECRET" | base64URLSafe } time=$(date +%s) header=$(printf '{ "alg": "ES256", "kid": "%s" }' "$KEYID" | base64URLSafe) claims=$(printf '{ "iss": "%s", "iat": %d }' "$TEAMID" "$time" | base64URLSafe) jwt="$header.$claims.$(sign $header.$claims)" # Development server: api.sandbox.push.apple.com:443 ENDPOINT=https://api.sandbox.push.apple.com:443 # # Production server: api.push.apple.com:443 # Uncomment URL below to send pushes to production server # ENDPOINT=https://api.push.apple.com:443 # URLPATH=/3/device/ URL=$ENDPOINT$URLPATH$DEVICETOKEN curl -v \ --http2 \ --header "authorization: bearer $jwt" \ --header "apns-topic: ${BUNDLEID}" \ --header "apns-push-type: voip" \ --header "apns-priority: 10" \ --data "${PAYLOAD}" \ "${URL}" ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/AppDelegate.swift ================================================ // // AppDelegate.swift // 4.Custom-Audio-Driver // // Created by Roberto Perez Cubero on 21/09/2016. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" }, { "idiom" : "ios-marketing", "size" : "1024x1024", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/DefaultAudioDevice.swift ================================================ // // DefaultAudioDevice.swift // 4.Custom-Audio-Driver // // Created by Roberto Perez Cubero on 21/09/2016. // Copyright © 2016 tokbox. All rights reserved. // import Foundation import OpenTok class DefaultAudioDevice: NSObject { #if targetEnvironment(simulator) static let kSampleRate: UInt16 = 44100 #else static let kSampleRate: UInt16 = 48000 #endif static let kOutputBus = AudioUnitElement(0) static let kInputBus = AudioUnitElement(1) static let kAudioDeviceHeadset = "AudioSessionManagerDevice_Headset" static let kAudioDeviceBluetooth = "AudioSessionManagerDevice_Bluetooth" static let kAudioDeviceSpeaker = "AudioSessionManagerDevice_Speaker" static let kToMicroSecond: Double = 1000000 static let kMaxPlayoutDelay: UInt8 = 150 static let kMaxRecordingDelay: UInt16 = 500 var audioFormat = OTAudioFormat() let safetyQueue = DispatchQueue(label: "ot-audio-driver") var deviceAudioBus: OTAudioBus? func setAudioBus(_ audioBus: OTAudioBus?) -> Bool { deviceAudioBus = audioBus audioFormat = OTAudioFormat() audioFormat.sampleRate = DefaultAudioDevice.kSampleRate audioFormat.numChannels = 1 return true } var bufferList: UnsafeMutablePointer? var bufferSize: UInt32 = 0 var bufferNumFrames: UInt32 = 0 var playoutAudioUnitPropertyLatency: Float64 = 0 var playoutDelayMeasurementCounter: UInt32 = 0 var recordingDelayMeasurementCounter: UInt32 = 0 var recordingDelay: UInt32 = 0 var recordingAudioUnitPropertyLatency: Float64 = 0 var playoutDelay: UInt32 = 0 var playing = false var playoutInitialized = false var recording = false var recordingInitialized = false var interruptedPlayback = false var isRecorderInterrupted = false var isPlayerInterrupted = false var isResetting = false var restartRetryCount = 0 fileprivate var recordingVoiceUnit: AudioUnit? fileprivate var playoutVoiceUnit: AudioUnit? fileprivate var previousAVAudioSessionCategory: AVAudioSession.Category? fileprivate var avAudioSessionMode: AVAudioSession.Mode? fileprivate var avAudioSessionPreffSampleRate = Double(0) fileprivate var avAudioSessionChannels = 0 fileprivate var isAudioSessionSetup = false var areListenerBlocksSetup = false var streamFormat = AudioStreamBasicDescription() override init() { audioFormat.sampleRate = DefaultAudioDevice.kSampleRate audioFormat.numChannels = 1 } deinit { tearDownAudio() removeObservers() } fileprivate func restartAudio() { safetyQueue.async { self.doRestartAudio(numberOfAttempts: 3) } } fileprivate func restartAudioAfterInterruption() { if isRecorderInterrupted { if startCapture() { isRecorderInterrupted = false restartRetryCount = 0 } else { restartRetryCount += 1 if restartRetryCount < 3 { safetyQueue.asyncAfter(deadline: DispatchTime.now(), execute: { [unowned self] in self.restartAudioAfterInterruption() }) } else { isRecorderInterrupted = false isPlayerInterrupted = false restartRetryCount = 0 print("ERROR[OpenTok]:Unable to acquire audio session") } } } if isPlayerInterrupted { isPlayerInterrupted = false let _ = startRendering() } } fileprivate func doRestartAudio(numberOfAttempts: Int) { isResetting = true if recording { let _ = stopCapture() disposeAudioUnit(audioUnit: &recordingVoiceUnit) let _ = startCapture() } if playing { let _ = self.stopRendering() disposeAudioUnit(audioUnit: &playoutVoiceUnit) let _ = self.startRendering() } isResetting = false } fileprivate func setupAudioUnit(withPlayout playout: Bool) -> Bool { if !isAudioSessionSetup { setupAudioSession() isAudioSessionSetup = true } let bytesPerSample = UInt32(MemoryLayout.size) streamFormat.mFormatID = kAudioFormatLinearPCM streamFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked streamFormat.mBytesPerPacket = bytesPerSample streamFormat.mFramesPerPacket = 1 streamFormat.mBytesPerFrame = bytesPerSample streamFormat.mChannelsPerFrame = 1 streamFormat.mBitsPerChannel = 8 * bytesPerSample streamFormat.mSampleRate = Float64(DefaultAudioDevice.kSampleRate) var audioUnitDescription = AudioComponentDescription() audioUnitDescription.componentType = kAudioUnitType_Output audioUnitDescription.componentSubType = kAudioUnitSubType_VoiceProcessingIO audioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple audioUnitDescription.componentFlags = 0 audioUnitDescription.componentFlagsMask = 0 let foundVpioUnitRef = AudioComponentFindNext(nil, &audioUnitDescription) let result: OSStatus = { if playout { return AudioComponentInstanceNew(foundVpioUnitRef!, &playoutVoiceUnit) } else { return AudioComponentInstanceNew(foundVpioUnitRef!, &recordingVoiceUnit) } }() if result != noErr { print("Error seting up audio unit") return false } var value: UInt32 = 1 if playout { AudioUnitSetProperty(playoutVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, DefaultAudioDevice.kOutputBus, &value, UInt32(MemoryLayout.size)) AudioUnitSetProperty(playoutVoiceUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, DefaultAudioDevice.kOutputBus, &streamFormat, UInt32(MemoryLayout.size)) // Disable Input on playout var enableInput = 0 AudioUnitSetProperty(playoutVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, DefaultAudioDevice.kInputBus, &enableInput, UInt32(MemoryLayout.size)) } else { AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, DefaultAudioDevice.kInputBus, &value, UInt32(MemoryLayout.size)) AudioUnitSetProperty(recordingVoiceUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, DefaultAudioDevice.kInputBus, &streamFormat, UInt32(MemoryLayout.size)) // Disable Output on record var enableOutput = 0 AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, DefaultAudioDevice.kOutputBus, &enableOutput, UInt32(MemoryLayout.size)) } if playout { setupPlayoutCallback() } else { setupRecordingCallback() } setBluetoothAsPreferredInputDevice() return true } fileprivate func setupPlayoutCallback() { let selfPointer = Unmanaged.passUnretained(self).toOpaque() var renderCallback = AURenderCallbackStruct(inputProc: renderCb, inputProcRefCon: selfPointer) AudioUnitSetProperty(playoutVoiceUnit!, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, DefaultAudioDevice.kOutputBus, &renderCallback, UInt32(MemoryLayout.size)) } fileprivate func setupRecordingCallback() { let selfPointer = Unmanaged.passUnretained(self).toOpaque() var inputCallback = AURenderCallbackStruct(inputProc: recordCb, inputProcRefCon: selfPointer) AudioUnitSetProperty(recordingVoiceUnit!, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, DefaultAudioDevice.kInputBus, &inputCallback, UInt32(MemoryLayout.size)) var value = 0 AudioUnitSetProperty(recordingVoiceUnit!, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, DefaultAudioDevice.kInputBus, &value, UInt32(MemoryLayout.size)) } fileprivate func disposeAudioUnit(audioUnit: inout AudioUnit?) { if let unit = audioUnit { AudioUnitUninitialize(unit) AudioComponentInstanceDispose(unit) } audioUnit = nil } fileprivate func tearDownAudio() { print("Destoying audio units") disposeAudioUnit(audioUnit: &playoutVoiceUnit) disposeAudioUnit(audioUnit: &recordingVoiceUnit) freeupAudioBuffers() let session = AVAudioSession.sharedInstance() do { guard let previousAVAudioSessionCategory = previousAVAudioSessionCategory else { return } try session.setCategory(previousAVAudioSessionCategory, mode: .default) guard let avAudioSessionMode = avAudioSessionMode else { return } try session.setMode(avAudioSessionMode) try session.setPreferredSampleRate(avAudioSessionPreffSampleRate) try session.setPreferredInputNumberOfChannels(avAudioSessionChannels) isAudioSessionSetup = false } catch { print("Error reseting AVAudioSession") } } fileprivate func freeupAudioBuffers() { if var data = bufferList?.pointee, data.mBuffers.mData != nil { data.mBuffers.mData?.assumingMemoryBound(to: UInt16.self).deallocate() data.mBuffers.mData = nil } if let list = bufferList { list.deallocate() } bufferList = nil bufferNumFrames = 0 } } // MARK: - Audio Device Implementation extension DefaultAudioDevice: OTAudioDevice { func captureFormat() -> OTAudioFormat { return audioFormat } func renderFormat() -> OTAudioFormat { return audioFormat } func renderingIsAvailable() -> Bool { return true } func renderingIsInitialized() -> Bool { return playoutInitialized } func isRendering() -> Bool { return playing } func isCapturing() -> Bool { return recording } func estimatedRenderDelay() -> UInt16 { return UInt16(min(self.playoutDelay, UInt32(DefaultAudioDevice.kMaxPlayoutDelay))) } func estimatedCaptureDelay() -> UInt16 { return UInt16(min(self.recordingDelay, UInt32(DefaultAudioDevice.kMaxRecordingDelay))) } func captureIsAvailable() -> Bool { return true } func captureIsInitialized() -> Bool { return recordingInitialized } func initializeRendering() -> Bool { if playing { return false } playoutInitialized = true return playoutInitialized } func startRendering() -> Bool { if playing { return true } playing = true if playoutVoiceUnit == nil { playing = setupAudioUnit(withPlayout: true) if !playing { return false } } let result = AudioOutputUnitStart(playoutVoiceUnit!) if result != noErr { print("Error creaing rendering unit") playing = false } return playing } func stopRendering() -> Bool { if !playing { return true } playing = false let result = AudioOutputUnitStop(playoutVoiceUnit!) if result != noErr { print("Error creaing playout unit") return false } if !recording && !isPlayerInterrupted && !isResetting { tearDownAudio() } return true } func initializeCapture() -> Bool { if recording { return false } recordingInitialized = true return recordingInitialized } func startCapture() -> Bool { if recording { return true } recording = true if recordingVoiceUnit == nil { recording = setupAudioUnit(withPlayout: false) if !recording { return false } } let result = AudioOutputUnitStart(recordingVoiceUnit!) if result != noErr { recording = false } return recording } func stopCapture() -> Bool { if !recording { return true } recording = false let result = AudioOutputUnitStop(recordingVoiceUnit!) if result != noErr { return false } freeupAudioBuffers() if !playing && !isRecorderInterrupted && !isResetting { tearDownAudio() } return true } } // MARK: - AVAudioSession extension DefaultAudioDevice { @objc func onInterruptionEvent(notification: Notification) { let type = notification.userInfo?[AVAudioSessionInterruptionTypeKey] safetyQueue.async { self.handleInterruptionEvent(type: type as? Int) } } fileprivate func handleInterruptionEvent(type: Int?) { guard let interruptionType = type else { return } switch UInt(interruptionType) { case AVAudioSession.InterruptionType.began.rawValue: if recording { isRecorderInterrupted = true let _ = stopCapture() } if playing { isPlayerInterrupted = true let _ = stopRendering() } case AVAudioSession.InterruptionType.ended.rawValue: configureAudioSessionWithDesiredAudioRoute(desiredAudioRoute: DefaultAudioDevice.kAudioDeviceBluetooth) restartAudioAfterInterruption() default: break } } @objc func onRouteChangeEvent(notification: Notification) { safetyQueue.async { self.handleRouteChangeEvent(notification: notification) } } @objc func appDidBecomeActive(notification: Notification) { safetyQueue.async { self.handleInterruptionEvent(type: Int(AVAudioSession.InterruptionType.ended.rawValue)) } } fileprivate func handleRouteChangeEvent(notification: Notification) { guard let reason = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt else { return } if reason == AVAudioSession.RouteChangeReason.routeConfigurationChange.rawValue { return } if reason == AVAudioSession.RouteChangeReason.override.rawValue || reason == AVAudioSession.RouteChangeReason.categoryChange.rawValue { let oldRouteDesc = notification.userInfo?[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription let outputs = oldRouteDesc.outputs var oldOutputDeviceName: String? = nil var currentOutputDeviceName: String? = nil if outputs.count > 0 { let portDesc = outputs[0] oldOutputDeviceName = portDesc.portName } if AVAudioSession.sharedInstance().currentRoute.outputs.count > 0 { currentOutputDeviceName = AVAudioSession.sharedInstance().currentRoute.outputs[0].portName } if oldOutputDeviceName == currentOutputDeviceName || currentOutputDeviceName == nil || oldOutputDeviceName == nil { return } restartAudio() } } fileprivate func setupListenerBlocks() { if areListenerBlocksSetup { return } let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(DefaultAudioDevice.onInterruptionEvent), name: AVAudioSession.interruptionNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(DefaultAudioDevice.onRouteChangeEvent(notification:)), name: AVAudioSession.routeChangeNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(DefaultAudioDevice.appDidBecomeActive(notification:)), name: UIApplication.didBecomeActiveNotification, object: nil) areListenerBlocksSetup = true } fileprivate func removeObservers() { NotificationCenter.default.removeObserver(self) areListenerBlocksSetup = false } fileprivate func setupAudioSession() { let session = AVAudioSession.sharedInstance() previousAVAudioSessionCategory = session.category avAudioSessionMode = session.mode avAudioSessionPreffSampleRate = session.preferredSampleRate avAudioSessionChannels = session.inputNumberOfChannels do { try session.setPreferredSampleRate(Double(DefaultAudioDevice.kSampleRate)) try session.setPreferredIOBufferDuration(0.01) let audioOptions = AVAudioSession.CategoryOptions.mixWithOthers.rawValue | AVAudioSession.CategoryOptions.allowBluetooth.rawValue | AVAudioSession.CategoryOptions.defaultToSpeaker.rawValue try session.setCategory(AVAudioSession.Category.playAndRecord, mode: AVAudioSession.Mode.videoChat, options: AVAudioSession.CategoryOptions(rawValue: audioOptions)) setupListenerBlocks() try session.setActive(true) } catch let err as NSError { print("Error setting up audio session \(err)") } catch { print("Error setting up audio session") } } } // MARK: - Audio Route functions extension DefaultAudioDevice { fileprivate func setBluetoothAsPreferredInputDevice() { let btRoutes = [AVAudioSession.Port.bluetoothA2DP, AVAudioSession.Port.bluetoothLE, AVAudioSession.Port.bluetoothHFP] AVAudioSession.sharedInstance().availableInputs?.forEach({ el in if btRoutes.contains(el.portType) { do { try AVAudioSession.sharedInstance().setPreferredInput(el) } catch { print("Error setting BT as preferred input device") } } }) } fileprivate func configureAudioSessionWithDesiredAudioRoute(desiredAudioRoute: String) { let session = AVAudioSession.sharedInstance() if desiredAudioRoute == DefaultAudioDevice.kAudioDeviceBluetooth { setBluetoothAsPreferredInputDevice() } do { if desiredAudioRoute == DefaultAudioDevice.kAudioDeviceSpeaker { try session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker) } else { try session.overrideOutputAudioPort(AVAudioSession.PortOverride.none) } } catch let err as NSError { print("Error setting audio route: \(err)") } } } // MARK: - Render and Record C Callbacks func renderCb(inRefCon:UnsafeMutableRawPointer, ioActionFlags:UnsafeMutablePointer, inTimeStamp:UnsafePointer, inBusNumber:UInt32, inNumberFrames:UInt32, ioData:UnsafeMutablePointer?) -> OSStatus { let audioDevice: DefaultAudioDevice = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue() if !audioDevice.playing { return 0 } let _ = audioDevice.deviceAudioBus!.readRenderData((ioData?.pointee.mBuffers.mData)!, numberOfSamples: inNumberFrames) updatePlayoutDelay(withAudioDevice: audioDevice) return noErr } func recordCb(inRefCon:UnsafeMutableRawPointer, ioActionFlags:UnsafeMutablePointer, inTimeStamp:UnsafePointer, inBusNumber:UInt32, inNumberFrames:UInt32, ioData:UnsafeMutablePointer?) -> OSStatus { let audioDevice: DefaultAudioDevice = Unmanaged.fromOpaque(inRefCon).takeUnretainedValue() if audioDevice.bufferList == nil || inNumberFrames > audioDevice.bufferNumFrames { if audioDevice.bufferList != nil { audioDevice.bufferList!.pointee.mBuffers.mData? .assumingMemoryBound(to: UInt16.self).deallocate() audioDevice.bufferList?.deallocate() } audioDevice.bufferList = UnsafeMutablePointer.allocate(capacity: 1) audioDevice.bufferList?.pointee.mNumberBuffers = 1 audioDevice.bufferList?.pointee.mBuffers.mNumberChannels = 1 audioDevice.bufferList?.pointee.mBuffers.mDataByteSize = inNumberFrames * UInt32(MemoryLayout.size) audioDevice.bufferList?.pointee.mBuffers.mData = UnsafeMutableRawPointer(UnsafeMutablePointer.allocate(capacity: Int(inNumberFrames))) audioDevice.bufferNumFrames = inNumberFrames audioDevice.bufferSize = (audioDevice.bufferList?.pointee.mBuffers.mDataByteSize)! } AudioUnitRender(audioDevice.recordingVoiceUnit!, ioActionFlags, inTimeStamp, 1, inNumberFrames, audioDevice.bufferList!) if audioDevice.recording { audioDevice.deviceAudioBus!.writeCaptureData((audioDevice.bufferList?.pointee.mBuffers.mData)!, numberOfSamples: inNumberFrames) } if audioDevice.bufferSize != audioDevice.bufferList?.pointee.mBuffers.mDataByteSize { audioDevice.bufferList?.pointee.mBuffers.mDataByteSize = audioDevice.bufferSize } updateRecordingDelay(withAudioDevice: audioDevice) return noErr } func updatePlayoutDelay(withAudioDevice audioDevice: DefaultAudioDevice) { audioDevice.playoutDelayMeasurementCounter += 1 if audioDevice.playoutDelayMeasurementCounter >= 100 { // Update HW and OS delay every second, unlikely to change audioDevice.playoutDelay = 0 let session = AVAudioSession.sharedInstance() // HW output latency let interval = session.outputLatency audioDevice.playoutDelay += UInt32(interval * DefaultAudioDevice.kToMicroSecond) // HW buffer duration let ioInterval = session.ioBufferDuration audioDevice.playoutDelay += UInt32(ioInterval * DefaultAudioDevice.kToMicroSecond) audioDevice.playoutDelay += UInt32(audioDevice.playoutAudioUnitPropertyLatency * DefaultAudioDevice.kToMicroSecond) // To ms audioDevice.playoutDelay = (audioDevice.playoutDelay - 500) / 1000 audioDevice.playoutDelayMeasurementCounter = 0 } } func updateRecordingDelay(withAudioDevice audioDevice: DefaultAudioDevice) { audioDevice.recordingDelayMeasurementCounter += 1 if audioDevice.recordingDelayMeasurementCounter >= 100 { audioDevice.recordingDelay = 0 let session = AVAudioSession.sharedInstance() let interval = session.inputLatency audioDevice.recordingDelay += UInt32(interval * DefaultAudioDevice.kToMicroSecond) let ioInterval = session.ioBufferDuration audioDevice.recordingDelay += UInt32(ioInterval * DefaultAudioDevice.kToMicroSecond) audioDevice.recordingDelay += UInt32(audioDevice.recordingAudioUnitPropertyLatency * DefaultAudioDevice.kToMicroSecond) audioDevice.recordingDelay = audioDevice.recordingDelay.advanced(by: -500) / 1000 audioDevice.recordingDelayMeasurementCounter = 0 } } ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver/ViewController.swift ================================================ // // ViewController.swift // Custom-Audio-Driver // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok let kWidgetHeight = 240 let kWidgetWidth = 320 // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() var publisher: OTPublisher? var subscriber: OTSubscriber? let customAudioDevice = DefaultAudioDevice() override func viewDidLoad() { super.viewDidLoad() OTAudioDeviceManager.setAudioDevice(customAudioDevice) doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ private func doConnect() { var error: OTError? defer { process(error: error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? = nil defer { process(error: error) } let settings = OTPublisherSettings() settings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: settings) if let pub = publisher, let pubView = pub.view { session.publish(pub, error: &error) pubView.frame = CGRect(x: 0, y: 0, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(pubView) } } fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { process(error: error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func process(error err: OTError?) { if let e = err { showAlert(errorStr: e.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") doSubscribe(stream) } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { subscriber?.view?.removeFromSuperview() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { subscriber?.view?.frame = CGRect(x: 0, y: kWidgetHeight, width: kWidgetWidth, height: kWidgetHeight) if let subsView = subscriber?.view { view.addSubview(subsView) } } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A053761A1EB16D9700645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05376111EB16D9700645696 /* AppDelegate.swift */; }; A053761B1EB16D9700645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05376121EB16D9700645696 /* Assets.xcassets */; }; A053761C1EB16D9700645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05376131EB16D9700645696 /* LaunchScreen.storyboard */; }; A053761D1EB16D9700645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05376151EB16D9700645696 /* Main.storyboard */; }; A053761E1EB16D9700645696 /* DefaultAudioDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05376171EB16D9700645696 /* DefaultAudioDevice.swift */; }; A05376201EB16D9700645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05376191EB16D9700645696 /* ViewController.swift */; }; A07707051EDF7B8300E8FB97 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = A07707041EDF7B8300E8FB97 /* Default-568h@2x.png */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A05376111EB16D9700645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05376121EB16D9700645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05376141EB16D9700645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05376161EB16D9700645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05376171EB16D9700645696 /* DefaultAudioDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultAudioDevice.swift; sourceTree = ""; }; A05376181EB16D9700645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05376191EB16D9700645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; A07707041EDF7B8300E8FB97 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; F882D72A1D92B85B00FD72FD /* Custom-Audio-Driver.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Custom-Audio-Driver.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F882D7271D92B85B00FD72FD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A05376101EB16D9700645696 /* Custom-Audio-Driver */ = { isa = PBXGroup; children = ( A05376111EB16D9700645696 /* AppDelegate.swift */, A05376121EB16D9700645696 /* Assets.xcassets */, A05376131EB16D9700645696 /* LaunchScreen.storyboard */, A05376151EB16D9700645696 /* Main.storyboard */, A05376171EB16D9700645696 /* DefaultAudioDevice.swift */, A05376181EB16D9700645696 /* Info.plist */, A05376191EB16D9700645696 /* ViewController.swift */, ); path = "Custom-Audio-Driver"; sourceTree = ""; }; F882D7211D92B85B00FD72FD = { isa = PBXGroup; children = ( A07707041EDF7B8300E8FB97 /* Default-568h@2x.png */, A05376101EB16D9700645696 /* Custom-Audio-Driver */, F882D72B1D92B85B00FD72FD /* Products */, ); sourceTree = ""; }; F882D72B1D92B85B00FD72FD /* Products */ = { isa = PBXGroup; children = ( F882D72A1D92B85B00FD72FD /* Custom-Audio-Driver.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F882D7291D92B85B00FD72FD /* Custom-Audio-Driver */ = { isa = PBXNativeTarget; buildConfigurationList = F882D73C1D92B85B00FD72FD /* Build configuration list for PBXNativeTarget "Custom-Audio-Driver" */; buildPhases = ( F882D7261D92B85B00FD72FD /* Sources */, F882D7271D92B85B00FD72FD /* Frameworks */, F882D7281D92B85B00FD72FD /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Custom-Audio-Driver"; productName = "3.Custom-Audio-Driver"; productReference = F882D72A1D92B85B00FD72FD /* Custom-Audio-Driver.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F882D7221D92B85B00FD72FD /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F882D7291D92B85B00FD72FD = { CreatedOnToolsVersion = 8.0; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F882D7251D92B85B00FD72FD /* Build configuration list for PBXProject "Custom-Audio-Driver" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F882D7211D92B85B00FD72FD; productRefGroup = F882D72B1D92B85B00FD72FD /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F882D7291D92B85B00FD72FD /* Custom-Audio-Driver */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F882D7281D92B85B00FD72FD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A053761D1EB16D9700645696 /* Main.storyboard in Resources */, A053761B1EB16D9700645696 /* Assets.xcassets in Resources */, A07707051EDF7B8300E8FB97 /* Default-568h@2x.png in Resources */, A053761C1EB16D9700645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F882D7261D92B85B00FD72FD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05376201EB16D9700645696 /* ViewController.swift in Sources */, A053761E1EB16D9700645696 /* DefaultAudioDevice.swift in Sources */, A053761A1EB16D9700645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05376131EB16D9700645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05376141EB16D9700645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05376151EB16D9700645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05376161EB16D9700645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F882D73A1D92B85B00FD72FD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F882D73B1D92B85B00FD72FD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F882D73D1D92B85B00FD72FD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Custom-Audio-Driver/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Custom-Audio-Driver"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug; }; F882D73E1D92B85B00FD72FD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Custom-Audio-Driver/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Custom-Audio-Driver"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F882D7251D92B85B00FD72FD /* Build configuration list for PBXProject "Custom-Audio-Driver" */ = { isa = XCConfigurationList; buildConfigurations = ( F882D73A1D92B85B00FD72FD /* Debug */, F882D73B1D92B85B00FD72FD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F882D73C1D92B85B00FD72FD /* Build configuration list for PBXNativeTarget "Custom-Audio-Driver" */ = { isa = XCConfigurationList; buildConfigurations = ( F882D73D1D92B85B00FD72FD /* Debug */, F882D73E1D92B85B00FD72FD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F882D7221D92B85B00FD72FD /* Project object */; } ================================================ FILE: Custom-Audio-Driver/Custom-Audio-Driver.xcodeproj/xcshareddata/xcschemes/Custom-Audio-Driver.xcscheme ================================================ ================================================ FILE: Custom-Audio-Driver/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Custom-Audio-Driver' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Custom-Audio-Driver/README.md ================================================ Custom Audio Driver Sample App ================================ This project implements a controller nearly identical to the hello world sample. The entry point of new content is during the controllers `viewDidLoad()` callback, where we set up an audio device before initializing our first OpenTok object. It is important to note that audio device setup *must* occur before any instance of OTSession or OTPublisher is initialized: ```swift let customAudioDevice = DefaultAudioDevice.sharedInstance OTAudioDeviceManager.setAudioDevice(customAudioDevice) ``` `DefaultAudioDevice` is a copy of the default device driver used in the OpenTok iOS SDK. If no audio device is set prior to the first instantiation of OTSession, the default driver will be used. A common reason for a developer to look at this sample is to debug audio issues in their application. This is also a good entry point for managing more complicated audio routing and customization of handling audio events on the device during the lifecycle of your app. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. Application Notes ----------------- * `recording_cb` and `playout_cb` are the two main operator functions in the audio graph once initialization has occurred and the session is active. Setting breakpoints on these functions can be useful to verify that the audio graph is indeed running and producing/consuming audio. * The callbacks `onRouteChangeEvent:` and `onInteruptionEvent:` are hooked into the system events and listen for important audio events that developers may wish to handle properly in different contexts of their app. * There are known constraints to sample rates and formats inherited from both the WebRTC runtime and iOS. The rates chosen in the sample audio device are known working configurations, but not everything will work. The simulator needs to run at 44.1 kHz. Devices should stick between 8-32 kHz. This example sticks to using unsigned 16-bit integers as the sample format. Your mileage may vary with adjusting any of these. ================================================ FILE: Custom-Video-Driver/Custom-Video-Driver.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ F84DC3AD1D5C8B6400402BD9 /* ExampleVideoCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84DC3AC1D5C8B6400402BD9 /* ExampleVideoCapture.swift */; }; F84DC3AF1D5C8BF400402BD9 /* ExampleVideoRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84DC3AE1D5C8BF400402BD9 /* ExampleVideoRender.swift */; }; F86C64BC1D5C8A150081846D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86C64BB1D5C8A150081846D /* AppDelegate.swift */; }; F86C64BE1D5C8A150081846D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86C64BD1D5C8A150081846D /* ViewController.swift */; }; F86C64C11D5C8A150081846D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F86C64BF1D5C8A150081846D /* Main.storyboard */; }; F86C64C31D5C8A150081846D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F86C64C21D5C8A150081846D /* Assets.xcassets */; }; F86C64C61D5C8A150081846D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */; }; F8C552001D635AFE00484097 /* EAGLVideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C551FF1D635AFE00484097 /* EAGLVideoRenderer.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ F84DC3AC1D5C8B6400402BD9 /* ExampleVideoCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleVideoCapture.swift; sourceTree = ""; }; F84DC3AE1D5C8BF400402BD9 /* ExampleVideoRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleVideoRender.swift; sourceTree = ""; }; F86C64B81D5C8A150081846D /* Custom-Video-Driver.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Custom-Video-Driver.app"; sourceTree = BUILT_PRODUCTS_DIR; }; F86C64BB1D5C8A150081846D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F86C64BD1D5C8A150081846D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F86C64C01D5C8A150081846D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; F86C64C21D5C8A150081846D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F86C64C51D5C8A150081846D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F86C64C71D5C8A150081846D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F8C551FF1D635AFE00484097 /* EAGLVideoRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EAGLVideoRenderer.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F86C64B51D5C8A150081846D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F86C64AF1D5C8A150081846D = { isa = PBXGroup; children = ( F86C64BA1D5C8A150081846D /* Lets-Build-OTPublisher */, F86C64B91D5C8A150081846D /* Products */, ); sourceTree = ""; }; F86C64B91D5C8A150081846D /* Products */ = { isa = PBXGroup; children = ( F86C64B81D5C8A150081846D /* Custom-Video-Driver.app */, ); name = Products; sourceTree = ""; }; F86C64BA1D5C8A150081846D /* Lets-Build-OTPublisher */ = { isa = PBXGroup; children = ( F86C64BB1D5C8A150081846D /* AppDelegate.swift */, F86C64BD1D5C8A150081846D /* ViewController.swift */, F86C64BF1D5C8A150081846D /* Main.storyboard */, F86C64C21D5C8A150081846D /* Assets.xcassets */, F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */, F86C64C71D5C8A150081846D /* Info.plist */, F84DC3AC1D5C8B6400402BD9 /* ExampleVideoCapture.swift */, F84DC3AE1D5C8BF400402BD9 /* ExampleVideoRender.swift */, F8C551FF1D635AFE00484097 /* EAGLVideoRenderer.swift */, ); path = "Lets-Build-OTPublisher"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F86C64B71D5C8A150081846D /* Custom-Video-Driver */ = { isa = PBXNativeTarget; buildConfigurationList = F86C64CA1D5C8A150081846D /* Build configuration list for PBXNativeTarget "Custom-Video-Driver" */; buildPhases = ( F86C64B41D5C8A150081846D /* Sources */, F86C64B51D5C8A150081846D /* Frameworks */, F86C64B61D5C8A150081846D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Custom-Video-Driver"; productName = "Lets-Build-OTPublisher"; productReference = F86C64B81D5C8A150081846D /* Custom-Video-Driver.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86C64B01D5C8A150081846D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F86C64B71D5C8A150081846D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F86C64B31D5C8A150081846D /* Build configuration list for PBXProject "Custom-Video-Driver" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F86C64AF1D5C8A150081846D; productRefGroup = F86C64B91D5C8A150081846D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F86C64B71D5C8A150081846D /* Custom-Video-Driver */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F86C64B61D5C8A150081846D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F86C64C61D5C8A150081846D /* LaunchScreen.storyboard in Resources */, F86C64C31D5C8A150081846D /* Assets.xcassets in Resources */, F86C64C11D5C8A150081846D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F86C64B41D5C8A150081846D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F86C64BE1D5C8A150081846D /* ViewController.swift in Sources */, F86C64BC1D5C8A150081846D /* AppDelegate.swift in Sources */, F8C552001D635AFE00484097 /* EAGLVideoRenderer.swift in Sources */, F84DC3AD1D5C8B6400402BD9 /* ExampleVideoCapture.swift in Sources */, F84DC3AF1D5C8BF400402BD9 /* ExampleVideoRender.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ F86C64BF1D5C8A150081846D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( F86C64C01D5C8A150081846D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F86C64C51D5C8A150081846D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F86C64C81D5C8A150081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F86C64C91D5C8A150081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F86C64CB1D5C8A150081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", "GLES_SILENCE_DEPRECATION=1", ); INFOPLIST_FILE = "Lets-Build-OTPublisher/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Lets-Build-OTPublisher"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F86C64CC1D5C8A150081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "Lets-Build-OTPublisher/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Lets-Build-OTPublisher"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F86C64B31D5C8A150081846D /* Build configuration list for PBXProject "Custom-Video-Driver" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64C81D5C8A150081846D /* Debug */, F86C64C91D5C8A150081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F86C64CA1D5C8A150081846D /* Build configuration list for PBXNativeTarget "Custom-Video-Driver" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64CB1D5C8A150081846D /* Debug */, F86C64CC1D5C8A150081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F86C64B01D5C8A150081846D /* Project object */; } ================================================ FILE: Custom-Video-Driver/Custom-Video-Driver.xcodeproj/xcshareddata/xcschemes/Custom-Video-Driver.xcscheme ================================================ ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/AppDelegate.swift ================================================ // // AppDelegate.swift // Lets-Build-OTPublisher // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/EAGLVideoRenderer.swift ================================================ // // EAGLVideoRenderer.swift // Lets-Build-OTPublisher // // Created by Roberto Perez Cubero on 16/08/16. // Copyright © 2016 tokbox. All rights reserved. // import GLKit import OpenTok class EAGLVideoRenderer { static let kNumTextureSets: GLsizei = 2 static let kNumTextures: GLsizei = 3 * kNumTextureSets let vertexShader = "attribute vec2 position;" + "attribute vec2 texcoord;" + "varying vec2 v_texcoord;" + "void main() {" + " gl_Position = vec4(position.x, position.y, 0.0, 1.0);" + " v_texcoord = texcoord;" + "}" let fragmentShader = "precision highp float;" + "varying vec2 v_texcoord;" + "uniform lowp sampler2D s_textureY;" + "uniform lowp sampler2D s_textureU;" + "uniform lowp sampler2D s_textureV;" + "void main() {" + " float y, u, v, r, g, b;" + " y = texture2D(s_textureY, v_texcoord).r;" + " u = texture2D(s_textureU, v_texcoord).r;" + " v = texture2D(s_textureV, v_texcoord).r;" + " u = u - 0.5;" + " v = v - 0.5;" + " r = y + 1.403 * v;" + " g = y - 0.344 * u - 0.714 * v;" + " b = y + 1.770 * u;" + " gl_FragColor = vec4(r, g, b, 1.0);" + "}" fileprivate let context: EAGLContext fileprivate var isInitialized = false fileprivate var glProgram: GLuint = 0 fileprivate var position: GLuint = 0 fileprivate var texcoord: GLuint = 0 fileprivate var ySampler: GLint = 0 fileprivate var uSampler: GLint = 0 fileprivate var vSampler: GLint = 0 fileprivate var textures = [GLuint](repeating: GLuint(0), count: Int(kNumTextures)) fileprivate var vertexBuffer: GLuint = 0 fileprivate var vertices = [GLfloat](repeating: GLfloat(0), count: 16) fileprivate var lastImageSize = CGSize(width: -1, height: -1) fileprivate var lastViewportSize = CGSize(width: -1, height: -1) fileprivate var flushVertices = false var mirroring = false { didSet { flushVertices = true } } fileprivate var intialized = false var lastFrameTime = CMTimeValue(0) init(context: EAGLContext) { self.context = context } func setupGL() { if isInitialized { return } ensureGLContext() do { try setupProgram() setupTextures() setupVertices() glUseProgram(glProgram) glPixelStorei(GLenum(GL_UNPACK_ALIGNMENT), 1) glClearColor(0, 0, 0, 1) isInitialized = true } catch { print("Error initializing OpenGL") } } func teardownGL() { if !isInitialized { return } ensureGLContext() glDeleteProgram(glProgram) glProgram = 0 glDeleteTextures(EAGLVideoRenderer.kNumTextures, &textures) glDeleteBuffers(1, &vertexBuffer) vertexBuffer = 0 isInitialized = false } func drawFrame(frame f: OTVideoFrame, withViewport:CGRect) { guard let frameFormat = f.format, isInitialized, lastFrameTime != f.timestamp.value else { return } ensureGLContext() updateTextureSizesForFrame(frame: f) updateTextureDataForFrame(frame: f) glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) let imageSize = CGSize(width: CGFloat(frameFormat.imageWidth), height: CGFloat(frameFormat.imageHeight)) if flushVertices { flushVertices = false updateVerticesWithViewportSize() } updateVerticesWithViewportSize(withViewport.size, imageSize: imageSize) glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBuffer) glBufferData(GLenum(GL_ARRAY_BUFFER), vertices.count * MemoryLayout.size, vertices, GLenum(GL_DYNAMIC_DRAW)) glDrawArrays(GLenum(GL_TRIANGLE_FAN), 0, 4) lastFrameTime = f.timestamp.value lastDrawnWidth = frameFormat.imageWidth lastDrawnHeight = frameFormat.imageHeight } func clearFrame() { if !isInitialized { return } ensureGLContext() glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) } fileprivate func ensureGLContext() { if EAGLContext.current() != context { EAGLContext.setCurrent(context) } } fileprivate func createShader(_ type: GLenum, source: String) throws -> GLuint { let shader = glCreateShader(type) var cStringSource = (source as NSString).utf8String glShaderSource(shader, 1, &cStringSource, nil) glCompileShader(shader) var compileStatus = GL_FALSE glGetShaderiv(shader, GLenum(GL_COMPILE_STATUS), &compileStatus) if compileStatus == GL_FALSE { glDeleteShader(shader) throw NSError(domain: "opentok", code: 100, userInfo: nil) } return shader } fileprivate func createProgram(_ vertexShader: GLuint, fragmentShader: GLuint) throws -> GLuint { let program = glCreateProgram() glAttachShader(program, vertexShader) glAttachShader(program, fragmentShader) glLinkProgram(program) var status = GL_FALSE glGetProgramiv(program, GLenum(GL_LINK_STATUS), &status) if status == GL_FALSE { glDeleteProgram(program) throw NSError(domain: "opentok", code: 100, userInfo: nil) } return program } fileprivate func getAttribLocation(_ program: GLuint, attrib: String) -> GLuint { return GLuint(glGetAttribLocation(program, (attrib as NSString).utf8String)) } fileprivate func getUniformLocation(_ program: GLuint, location: String) -> GLint { return glGetUniformLocation(program, (location as NSString).utf8String) } fileprivate func setupProgram() throws { let vertexShader = try createShader(GLenum(GL_VERTEX_SHADER), source: self.vertexShader) let fragmentShader = try createShader(GLenum(GL_FRAGMENT_SHADER), source: self.fragmentShader) glProgram = try createProgram(vertexShader, fragmentShader: fragmentShader) glDeleteShader(vertexShader) glDeleteShader(fragmentShader) position = getAttribLocation(glProgram, attrib: "position") texcoord = getAttribLocation(glProgram, attrib: "texcoord") ySampler = getUniformLocation(glProgram, location: "s_textureY") uSampler = getUniformLocation(glProgram, location: "s_textureU") vSampler = getUniformLocation(glProgram, location: "s_textureV") } fileprivate func setupTextures() { glGenTextures(EAGLVideoRenderer.kNumTextures, UnsafeMutablePointer(mutating: textures)) for (index, texture) in textures.enumerated() { glActiveTexture(UInt32(GL_TEXTURE0.advanced(by: index))) glBindTexture(GLenum(GL_TEXTURE_2D), texture) glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE); glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE); } } fileprivate func setupVertices() { glGenBuffers(1, &vertexBuffer) updateVerticesWithViewportSize() } fileprivate func updateVerticesWithViewportSize(_ viewportSize: CGSize = CGSize(width: 1, height: 1), imageSize: CGSize = CGSize(width: 1, height: 1)) { if lastImageSize == imageSize && viewportSize == lastViewportSize { return } lastImageSize = imageSize lastViewportSize = viewportSize let imageRatio = GLfloat(imageSize.width / imageSize.height) let viewportRatio = GLfloat(viewportSize.width / viewportSize.height) var scaleX = GLfloat(1.0) var scaleY = GLfloat(1.0) if imageRatio > viewportRatio { scaleY = viewportRatio / imageRatio } else { scaleX = imageRatio / viewportRatio } if mirroring { scaleX = -scaleX } vertices[0] = -1 * scaleX vertices[1] = -1 * scaleY vertices[2] = 0 vertices[3] = 1 vertices[4] = 1 * scaleX vertices[5] = -1 * scaleY vertices[6] = 1 vertices[7] = 1 vertices[8] = 1 * scaleX vertices[9] = 1 * scaleY vertices[10] = 1 vertices[11] = 0 vertices[12] = -1 * scaleX vertices[13] = 1 * scaleY vertices[14] = 0 vertices[15] = 0 glBindBuffer(GLenum(GL_ARRAY_BUFFER), vertexBuffer) glBufferData(GLenum(GL_ARRAY_BUFFER), vertices.count * MemoryLayout.size, vertices, GLenum(GL_DYNAMIC_DRAW)) // Read position attribute from |_vertices| with size of 2 and stride of 4 // beginning at the start of the array. The last argument indicates offset // of data within |gVertices| as supplied to the vertex buffer. glVertexAttribPointer(position, 2, GLenum(GL_FLOAT), UInt8(GL_FALSE), 4 * Int32(MemoryLayout.size), nil) glEnableVertexAttribArray(position) // Read texcoord attribute from |_vertices| with size of 2 and stride of 4 // beginning at the first texcoord in the array. The last argument indicates // offset of data within |gVertices| as supplied to the vertex buffer. let ptr = UnsafeMutableRawPointer(bitPattern: MemoryLayout.size * 2) glVertexAttribPointer(texcoord, 2, GLenum(GL_FLOAT), UInt8(GL_FALSE), Int32(4 * MemoryLayout.size), ptr) glEnableVertexAttribArray(texcoord) } fileprivate var lastDrawnHeight = UInt32(0) fileprivate var lastDrawnWidth = UInt32(0) fileprivate func updateTextureSizesForFrame(frame: OTVideoFrame) { guard let frameFormat = frame.format, frameFormat.imageHeight != lastDrawnHeight, frameFormat.imageWidth != lastDrawnWidth else { return } let lumaWidth = GLsizei(frameFormat.imageWidth) let lumaHeight = GLsizei(frameFormat.imageHeight) let chromaWidth = GLsizei(frameFormat.imageWidth / 2) let chromaHeight = GLsizei(frameFormat.imageHeight / 2) for i in 0.. OTVideoOrientation { let orientation = statusBarOrientation if pos == .front { switch orientation { case .landscapeLeft: return .up case .landscapeRight: return .down case .portrait: return .left case .portraitUpsideDown: return .right case .unknown: return .up @unknown default: fatalError() } } else { switch orientation { case .landscapeLeft: return .down case .landscapeRight: return .up case .portrait: return .left case .portraitUpsideDown: return .right case .unknown: return .up @unknown default: fatalError() } } } } extension AVCaptureSession.Preset { func dimensionForCapturePreset() -> (width: UInt32, height: UInt32) { switch self { case AVCaptureSession.Preset.cif352x288: return (352, 288) case AVCaptureSession.Preset.vga640x480, AVCaptureSession.Preset.high: return (640, 480) case AVCaptureSession.Preset.low: return (192, 144) case AVCaptureSession.Preset.medium: return (480, 360) case AVCaptureSession.Preset.hd1280x720: return (1280, 720) default: return (352, 288) } } } protocol FrameCapturerMetadataDelegate { func finishPreparingFrame(_ videoFrame: OTVideoFrame?) } class ExampleVideoCapture: NSObject, OTVideoCapture { var videoContentHint: OTVideoContentHint var captureSession: AVCaptureSession? var videoInput: AVCaptureDeviceInput? var videoOutput: AVCaptureVideoDataOutput? var videoCaptureConsumer: OTVideoCaptureConsumer? var delegate: FrameCapturerMetadataDelegate? var cameraPosition: AVCaptureDevice.Position { get { return videoInput?.device.position ?? .unspecified } } fileprivate var capturePreset: AVCaptureSession.Preset { didSet { (captureWidth, captureHeight) = capturePreset.dimensionForCapturePreset() } } fileprivate var captureWidth: UInt32 fileprivate var captureHeight: UInt32 fileprivate var capturing = false fileprivate let videoFrame: OTVideoFrame fileprivate var videoFrameOrientation: OTVideoOrientation = .left //potrait let captureQueue: DispatchQueue fileprivate func updateFrameOrientation() { DispatchQueue.main.async { guard let inputDevice = self.videoInput else { return; } self.videoFrameOrientation = UIApplication.shared.currentDeviceOrientation(cameraPosition: inputDevice.device.position) } } override init() { self.videoContentHint = .none capturePreset = AVCaptureSession.Preset.vga640x480 captureQueue = DispatchQueue(label: "com.tokbox.VideoCapture", attributes: []) (captureWidth, captureHeight) = capturePreset.dimensionForCapturePreset() videoFrame = OTVideoFrame(format: OTVideoFormat(nv12WithWidth: captureWidth, height: captureHeight)) } // MARK: - AVFoundation functions fileprivate func setupAudioVideoSession() throws { captureSession = AVCaptureSession() captureSession?.beginConfiguration() captureSession?.sessionPreset = capturePreset captureSession?.usesApplicationAudioSession = false // Configure Camera Input guard let device = camera(withPosition: .front) else { print("Failed to acquire camera device for video") return } videoInput = try AVCaptureDeviceInput(device: device) guard let videoInput = self.videoInput else { print("There was an error creating videoInput") return } captureSession?.addInput(videoInput) // Configure Ouput videoOutput = AVCaptureVideoDataOutput() videoOutput?.alwaysDiscardsLateVideoFrames = true videoOutput?.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange) ] videoOutput?.setSampleBufferDelegate(self, queue: captureQueue) guard let videoOutput = self.videoOutput else { print("There was an error creating videoOutput") return } captureSession?.addOutput(videoOutput) setFrameRate() captureSession?.commitConfiguration() captureSession?.startRunning() } fileprivate func frameRateRange(forFrameRate fps: Int) -> AVFrameRateRange? { return videoInput?.device.activeFormat.videoSupportedFrameRateRanges.filter({ range in return range.minFrameRate <= Double(fps) && Double(fps) <= range.maxFrameRate }).first } fileprivate func setFrameRate(fps: Int = 20) { guard let _ = frameRateRange(forFrameRate: fps) else { print("Unsupported frameRate \(fps)") return } let desiredMinFps = CMTime(value: 1, timescale: CMTimeScale(fps)) let desiredMaxFps = CMTime(value: 1, timescale: CMTimeScale(fps)) do { try videoInput?.device.lockForConfiguration() videoInput?.device.activeVideoMinFrameDuration = desiredMinFps videoInput?.device.activeVideoMaxFrameDuration = desiredMaxFps } catch { print("Error setting framerate") } } fileprivate func camera(withPosition pos: AVCaptureDevice.Position) -> AVCaptureDevice? { guard #available(iOS 10, *) else { return nil } return AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: pos).devices.first } fileprivate func updateCaptureFormat(width w: UInt32, height h: UInt32) { captureWidth = w captureHeight = h videoFrame.format = OTVideoFormat.init(nv12WithWidth: w, height: h) } // MARK: - OTVideoCapture protocol func initCapture() { NotificationCenter.default.addObserver(forName: UIDevice.orientationDidChangeNotification, object: nil, queue: .main, using: { (_) in self.updateFrameOrientation() }) captureQueue.async { do { try self.setupAudioVideoSession() } catch let error as NSError { print("Error configuring AV Session: \(error)") } } } func start() -> Int32 { self.updateFrameOrientation() self.capturing = true return 0 } func stop() -> Int32 { capturing = false return 0 } func releaseCapture() { let _ = stop() videoOutput?.setSampleBufferDelegate(nil, queue: captureQueue) captureQueue.sync { self.captureSession?.stopRunning() } captureSession = nil videoOutput = nil videoInput = nil NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) } func isCaptureStarted() -> Bool { return capturing && (captureSession != nil) } func captureSettings(_ videoFormat: OTVideoFormat) -> Int32 { videoFormat.pixelFormat = .NV12 videoFormat.imageWidth = captureWidth videoFormat.imageHeight = captureHeight return 0 } fileprivate func frontFacingCamera() -> AVCaptureDevice? { return camera(withPosition: .front) } fileprivate func backFacingCamera() -> AVCaptureDevice? { return camera(withPosition: .back) } fileprivate var hasMultipleCameras: Bool { guard #available(iOS 10, *) else { return false } return AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .unspecified).devices.count > 1 } func setCameraPosition(_ position: AVCaptureDevice.Position) -> Bool { guard let preset = captureSession?.sessionPreset else { return false } let newVideoInput: AVCaptureDeviceInput? = { do { if position == AVCaptureDevice.Position.back { guard let backFacingCamera = backFacingCamera() else { return nil } return try AVCaptureDeviceInput.init(device: backFacingCamera) } else if position == AVCaptureDevice.Position.front { guard let frontFacingCamera = frontFacingCamera() else { return nil } return try AVCaptureDeviceInput.init(device: frontFacingCamera) } else { return nil } } catch { return nil } }() guard let newInput = newVideoInput else { return false } var success = true captureQueue.sync { captureSession?.beginConfiguration() guard let videoInput = self.videoInput else { return } captureSession?.removeInput(videoInput) if captureSession?.canAddInput(newInput) ?? false { captureSession?.addInput(newInput) self.videoInput = newInput } else { success = false captureSession?.addInput(videoInput) } captureSession?.commitConfiguration() } if success { capturePreset = preset } return success } func toggleCameraPosition() -> Bool { guard hasMultipleCameras else { return false } if videoInput?.device.position == .front { return setCameraPosition(.back) } else { return setCameraPosition(.front) } } } extension ExampleVideoCapture: AVCaptureVideoDataOutputSampleBufferDelegate { func captureOutput(_ captureOutput: AVCaptureOutput, didDrop sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { print("Dropping frame") } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard capturing, let videoCaptureConsumer = videoCaptureConsumer, let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { print("Error acquiring sample buffer") return } let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) videoCaptureConsumer.consumeImageBuffer(imageBuffer, orientation: videoFrameOrientation, timestamp: time, metadata: videoFrame.metadata) CVPixelBufferUnlockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) } } ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/ExampleVideoRender.swift ================================================ // // ExampleVideoRender.swift // Lets-Build-OTPublisher // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import OpenTok import GLKit protocol ExampleVideoRenderDelegate { func renderer(_ renderer: ExampleVideoRender, didReceiveFrame videoFrame: OTVideoFrame) } class ExampleVideoRender: UIView { var delegate: ExampleVideoRenderDelegate? var mirroring: Bool = true { didSet { if let renderer = renderer { renderer.mirroring = mirroring } } } fileprivate var glContext: EAGLContext? fileprivate var renderer: EAGLVideoRenderer? fileprivate var glkView: GLKView? fileprivate var frameLock: NSLock? fileprivate var renderingEnabled: Bool = true fileprivate var lastVideoFrame: OTVideoFrame? fileprivate var displayLinkProxy: DisplayLinkProxy? fileprivate var displayLink: CADisplayLink? override init(frame: CGRect) { super.init(frame: frame) glContext = EAGLContext(api:.openGLES2) renderer = EAGLVideoRenderer(context: glContext!) glkView = GLKView(frame: CGRect.zero, context: glContext!) glkView?.drawableColorFormat = .RGBA8888 glkView?.drawableDepthFormat = .formatNone glkView?.drawableStencilFormat = .formatNone glkView?.drawableMultisample = .multisampleNone glkView?.delegate = self; glkView?.layer.masksToBounds = true; addSubview(glkView!) frameLock = NSLock() NotificationCenter.default .addObserver(self, selector: #selector(ExampleVideoRender.willResignActive), name: UIApplication.willResignActiveNotification, object: nil) NotificationCenter.default .addObserver(self, selector: #selector(ExampleVideoRender.didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) displayLinkProxy = DisplayLinkProxy(glkView: glkView!, videoRender: self) displayLink = CADisplayLink(target: displayLinkProxy!, selector:#selector(DisplayLinkProxy.displayLinkDidFire(_:))) if #available(iOS 10, *) { displayLink!.preferredFramesPerSecond = 30 } displayLink!.add(to: RunLoop.main, forMode: RunLoop.Mode.common) renderer!.mirroring = mirroring renderer!.setupGL() displayLink!.isPaused = false } @objc func willResignActive() { displayLink!.isPaused = true glkView?.deleteDrawable() renderer!.teardownGL() } @objc func didBecomeActive() { renderer!.setupGL() displayLink!.isPaused = false } var needsRendererUpdate: Bool { get { guard lastVideoFrame != nil else { return false } return renderer?.lastFrameTime != lastVideoFrame?.timestamp.value } } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() glkView?.frame = bounds } fileprivate func calculatePlaneSize(forFrame frame: OTVideoFrame) -> (ySize: Int, uSize: Int, vSize: Int) { guard let frameFormat = frame.format else { return (0, 0 ,0) } let baseSize = Int(frameFormat.imageWidth * frameFormat.imageHeight) * MemoryLayout.size return (baseSize, baseSize / 4, baseSize / 4) } } extension ExampleVideoRender: GLKViewDelegate { func glkView(_ view: GLKView, drawIn rect: CGRect) { defer { frameLock?.unlock() } frameLock?.lock() //Coming from bg you will double render and deallocate will crash below. Hence check for lastVideoFrameRendered == false guard let lastVideoFrame = lastVideoFrame else { return } renderer?.drawFrame(frame: lastVideoFrame, withViewport: view.frame) deallocateFrame(lastVideoFrame) self.lastVideoFrame = nil } func deallocateFrame(_ frame: OTVideoFrame?) -> Void { guard let frame = frame else { return } let yPlane: UnsafeMutablePointer? = frame.planes?.pointer(at: 0)?.assumingMemoryBound(to: GLubyte.self) let uPlane: UnsafeMutablePointer? = frame.planes?.pointer(at: 1)?.assumingMemoryBound(to: GLubyte.self) let vPlane: UnsafeMutablePointer? = frame.planes?.pointer(at: 2)?.assumingMemoryBound(to: GLubyte.self) yPlane?.deallocate() uPlane?.deallocate() vPlane?.deallocate() } } extension ExampleVideoRender: OTVideoRender { func renderVideoFrame(_ frame: OTVideoFrame) { if let fLock = frameLock, let format = frame.format { fLock.lock() assert(format.pixelFormat == .I420) deallocateFrame(lastVideoFrame) lastVideoFrame = OTVideoFrame(format: format) lastVideoFrame?.timestamp = frame.timestamp let planeSize = calculatePlaneSize(forFrame: frame) let yPlane = UnsafeMutablePointer.allocate(capacity: planeSize.ySize) let uPlane = UnsafeMutablePointer.allocate(capacity: planeSize.uSize) let vPlane = UnsafeMutablePointer.allocate(capacity: planeSize.vSize) memcpy(yPlane, frame.planes?.pointer(at: 0), planeSize.ySize) memcpy(uPlane, frame.planes?.pointer(at: 1), planeSize.uSize) memcpy(vPlane, frame.planes?.pointer(at: 2), planeSize.vSize) lastVideoFrame?.planes?.addPointer(yPlane) lastVideoFrame?.planes?.addPointer(uPlane) lastVideoFrame?.planes?.addPointer(vPlane) fLock.unlock() if let delegate = delegate { delegate.renderer(self, didReceiveFrame: frame) } } } } class DisplayLinkProxy { var renderer: ExampleVideoRender var view: GLKView init(glkView: GLKView, videoRender: ExampleVideoRender) { renderer = videoRender view = glkView } @objc func displayLinkDidFire(_ displayLink: CADisplayLink) { if renderer.needsRendererUpdate { view.setNeedsDisplay() } } } ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Custom-Video-Driver/Lets-Build-OTPublisher/ViewController.swift ================================================ // // ViewController.swift // Lets-Build-OTPublisher // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok let kWidgetRatio: CGFloat = 1.333 // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() var publisher: OTPublisher? var subscriber: OTSubscriber? let captureSession = AVCaptureSession() let captureQueue = DispatchQueue(label: "com.tokbox.VideoCapture", attributes: []) override func viewDidLoad() { super.viewDidLoad() doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? = nil defer { processError(error) } let settings = OTPublisherSettings() settings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: settings) if let pub = publisher { let videoRender = ExampleVideoRender() pub.videoCapture = ExampleVideoCapture() pub.videoRender = videoRender session.publish(pub, error: &error) videoRender.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width / kWidgetRatio) view.addSubview(videoRender) } } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func cleanupSubscriber() { subscriber?.view?.removeFromSuperview() subscriber = nil } fileprivate func processError(_ error: OTError?) { if let err = error { showAlert(errorStr: err.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } @IBAction func toggleCamera(_ sender: Any) { if let capturer = publisher?.videoCapture as? ExampleVideoCapture, let renderer = publisher?.videoRender as? ExampleVideoRender { let _ = capturer.toggleCameraPosition() renderer.mirroring = (capturer.cameraPosition == AVCaptureDevice.Position.front) ? true : false } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscriber == nil { doSubscribe(stream) } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { subscriber?.view?.frame = CGRect(x: 0, y: view.frame.width / kWidgetRatio, width: view.frame.width, height: view.frame.width / kWidgetRatio) if let subsView = subscriber?.view { view.addSubview(subsView) } } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } ================================================ FILE: Custom-Video-Driver/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Custom-Video-Driver' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Custom-Video-Driver/README.md ================================================ Custom Video Driver Sample App ================================== This project uses the custom video driver features in the OpenTok iOS SDK. By the end of a code review, you should have a basic understanding of the internals of the video capture and render API, as well as how to start building your own extensions to the core OTPublisherKit and OTSubscriberKit classes. Note that this sample application is not supported in the XCode iOS Simulator because the custom video capturer needs to acquire video from an iOS device camera. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. ### ExampleVideoRender Both OTSubscriber and OTPublisher need an instance supporting the `OTVideoRender` protocol to display video contents. In short, the instance ID that is set to the `videoRender` property will receive YUV frames (I420) as they are captured (publisher) or as they are received (subscriber). Note that, although the publisher's `OTVideoCapture` interface can process multiple pixel formats, the images passed through the rendering callback will always be in the I420 YUV format. ExampleVideoRender is a simplified swift version of the default video renderer for the OpenTok iOS SDK. It is borrowed and modified from a series of classes in Google's [WebRTC][1] project. In this example we wire a video renderer to the publisher's rendering callback. An alternative approach for developers using video from the camera with AVFoundation is to wire [AVCaptureVideoPreviewLayer][2] directly to the capture class and leave the `OTPublisherKit.videoRender` property nil. To see ExampleVideoRender in action, put a breakpoint on `renderVideoFrame:`. You will see this method called for every video frame that is presented to the rendering endpoint by the OpenTok iOS SDK. ### ExampleVideoCapture This class interfaces with AVFoundation to provide video capture support from the device's camera hardware. By implementing the OTVideoCapture interface, it can be used as a video capture endpoint OTPublisher to provide video for publishing. To see ExampleVideoCapture in action, put a breakpoint on `captureOutput:didOutputSampleBuffer:fromConnection:`. This method is invoked by AVFoundation for every frame that is output from the camera capture session. After some processing, the video capture invokes its own `OTVideoCaptureConsumer` with the captured frame. Note the consumer is set by the OpenTok iOS SDK during instantiation of the publisher. Putting it all together ----------------------- The [ViewController](Lets-Build-OTPublisher/ViewController.m) for this application is a near-identical clone of the previous, with text substitutions for our newly-minted example publisher and subscriber classes. Notice how a majority of the calls made into the OpenTok iOS SDK classes are declared on the core classes, OTPublisherKit and OTSubscriberKit. Extending those core classes as is done in this example is as simple as defining a few simple interfaces and plugging everything in at runtime. We hope that this new class hierarchy will give you some ideas for how to extend the core functionality of the OpenTok iOS SDK to meet your application needs. [1]: https://chromium.googlesource.com/external/webrtc/+/master/talk/app/webrtc/objc/ [2]: https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVCaptureVideoPreviewLayer_Class/Reference/Reference.html ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat/AppDelegate.swift ================================================ // // AppDelegate.swift // Hello-World // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } } ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat/Base.lproj/Main.storyboard ================================================ ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat/ViewController.swift ================================================ // // ViewController.swift // Hello-World // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" // Replace with your encryption secret let kEncryptionSecret = "" let kWidgetHeight = 240 let kWidgetWidth = 320 class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() lazy var publisher: OTPublisher = { let settings = OTPublisherSettings() settings.name = UIDevice.current.name return OTPublisher(delegate: self, settings: settings)! }() var subscriber: OTSubscriber? override func viewDidLoad() { super.viewDidLoad() setEncryptionSecret() doConnect() } /** * Encryption secret must be set before publishing/subscribing. */ fileprivate func setEncryptionSecret() { var error: OTError? defer { processError(error) } session.setEncryptionSecret(kEncryptionSecret, error: &error) } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? defer { processError(error) } session.publish(publisher, error: &error) if let pubView = publisher.view { pubView.frame = CGRect(x: 0, y: 0, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(pubView) } } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func cleanupSubscriber() { subscriber?.view?.removeFromSuperview() subscriber = nil } fileprivate func cleanupPublisher() { publisher.view?.removeFromSuperview() } fileprivate func processError(_ error: OTError?) { if let err = error { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err.localizedDescription, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscriber == nil { doSubscribe(stream) } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { cleanupPublisher() if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { if let subsView = subscriber?.view { subsView.frame = CGRect(x: 0, y: kWidgetHeight, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(subsView) } } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } } ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 8CE5ECA82768CF272D93D550 /* Pods_Basic_Video_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 292117CDD5541CC8EDB8DFEF /* Pods_Basic_Video_Chat.framework */; }; A05375D71EB1633400645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375CF1EB1633400645696 /* AppDelegate.swift */; }; A05375D81EB1633400645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05375D01EB1633400645696 /* Assets.xcassets */; }; A05375D91EB1633400645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375D11EB1633400645696 /* LaunchScreen.storyboard */; }; A05375DA1EB1633400645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375D31EB1633400645696 /* Main.storyboard */; }; A05375DC1EB1633400645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375D61EB1633400645696 /* ViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 292117CDD5541CC8EDB8DFEF /* Pods_Basic_Video_Chat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Basic_Video_Chat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57CEF65B7B7E03022F938D38 /* Pods-Basic-Video-Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Basic-Video-Chat.release.xcconfig"; path = "Target Support Files/Pods-Basic-Video-Chat/Pods-Basic-Video-Chat.release.xcconfig"; sourceTree = ""; }; A05375CF1EB1633400645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05375D01EB1633400645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05375D21EB1633400645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05375D41EB1633400645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05375D51EB1633400645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05375D61EB1633400645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; C3768C616191C2590039011A /* Pods-Basic-Video-Chat.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Basic-Video-Chat.debug.xcconfig"; path = "Target Support Files/Pods-Basic-Video-Chat/Pods-Basic-Video-Chat.debug.xcconfig"; sourceTree = ""; }; F86C649A1D5C7C630081846D /* Basic-Video-Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Basic-Video-Chat.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F86C64971D5C7C630081846D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 8CE5ECA82768CF272D93D550 /* Pods_Basic_Video_Chat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 9B91B601FD7FCD4544752C8F /* Pods */ = { isa = PBXGroup; children = ( C3768C616191C2590039011A /* Pods-Basic-Video-Chat.debug.xcconfig */, 57CEF65B7B7E03022F938D38 /* Pods-Basic-Video-Chat.release.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; A05375CE1EB1633400645696 /* Basic-Video-Chat */ = { isa = PBXGroup; children = ( A05375CF1EB1633400645696 /* AppDelegate.swift */, A05375D01EB1633400645696 /* Assets.xcassets */, A05375D11EB1633400645696 /* LaunchScreen.storyboard */, A05375D31EB1633400645696 /* Main.storyboard */, A05375D51EB1633400645696 /* Info.plist */, A05375D61EB1633400645696 /* ViewController.swift */, ); path = "Basic-Video-Chat"; sourceTree = ""; }; E350D1C8D33042DC5538D354 /* Frameworks */ = { isa = PBXGroup; children = ( 292117CDD5541CC8EDB8DFEF /* Pods_Basic_Video_Chat.framework */, ); name = Frameworks; sourceTree = ""; }; F86C64911D5C7C630081846D = { isa = PBXGroup; children = ( A05375CE1EB1633400645696 /* Basic-Video-Chat */, F86C649B1D5C7C630081846D /* Products */, 9B91B601FD7FCD4544752C8F /* Pods */, E350D1C8D33042DC5538D354 /* Frameworks */, ); sourceTree = ""; }; F86C649B1D5C7C630081846D /* Products */ = { isa = PBXGroup; children = ( F86C649A1D5C7C630081846D /* Basic-Video-Chat.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F86C64991D5C7C630081846D /* Basic-Video-Chat */ = { isa = PBXNativeTarget; buildConfigurationList = F86C64AC1D5C7C630081846D /* Build configuration list for PBXNativeTarget "Basic-Video-Chat" */; buildPhases = ( C161AC7963B4CE2E0ED2AC58 /* [CP] Check Pods Manifest.lock */, F86C64961D5C7C630081846D /* Sources */, F86C64971D5C7C630081846D /* Frameworks */, F86C64981D5C7C630081846D /* Resources */, 3EF759C7D0E620322B2686EF /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = "Basic-Video-Chat"; productName = "Hello-World"; productReference = F86C649A1D5C7C630081846D /* Basic-Video-Chat.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86C64921D5C7C630081846D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0930; ORGANIZATIONNAME = tokbox; TargetAttributes = { F86C64991D5C7C630081846D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F86C64951D5C7C630081846D /* Build configuration list for PBXProject "Basic-Video-Chat" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, ); mainGroup = F86C64911D5C7C630081846D; productRefGroup = F86C649B1D5C7C630081846D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F86C64991D5C7C630081846D /* Basic-Video-Chat */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F86C64981D5C7C630081846D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375DA1EB1633400645696 /* Main.storyboard in Resources */, A05375D81EB1633400645696 /* Assets.xcassets in Resources */, A05375D91EB1633400645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3EF759C7D0E620322B2686EF /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Basic-Video-Chat/Pods-Basic-Video-Chat-resources.sh", "${PODS_ROOT}/OTXCFramework/OpenTok.xcframework/ios-arm64/OpenTok.framework/selfie_segmentation.tflite", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/selfie_segmentation.tflite", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Basic-Video-Chat/Pods-Basic-Video-Chat-resources.sh\"\n"; showEnvVarsInLog = 0; }; C161AC7963B4CE2E0ED2AC58 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Basic-Video-Chat-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F86C64961D5C7C630081846D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375DC1EB1633400645696 /* ViewController.swift in Sources */, A05375D71EB1633400645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05375D11EB1633400645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05375D21EB1633400645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05375D31EB1633400645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05375D41EB1633400645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F86C64AA1D5C7C630081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64"; }; name = Debug; }; F86C64AB1D5C7C630081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64"; }; name = Release; }; F86C64AD1D5C7C630081846D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = C3768C616191C2590039011A /* Pods-Basic-Video-Chat.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Basic-Video-Chat/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Hello-World"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Debug; }; F86C64AE1D5C7C630081846D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 57CEF65B7B7E03022F938D38 /* Pods-Basic-Video-Chat.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Basic-Video-Chat/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Hello-World"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F86C64951D5C7C630081846D /* Build configuration list for PBXProject "Basic-Video-Chat" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64AA1D5C7C630081846D /* Debug */, F86C64AB1D5C7C630081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F86C64AC1D5C7C630081846D /* Build configuration list for PBXNativeTarget "Basic-Video-Chat" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64AD1D5C7C630081846D /* Debug */, F86C64AE1D5C7C630081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F86C64921D5C7C630081846D /* Project object */; } ================================================ FILE: E2EE-Video-Chat/Basic-Video-Chat.xcodeproj/xcshareddata/xcschemes/Basic-Video-Chat.xcscheme ================================================ ================================================ FILE: E2EE-Video-Chat/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Basic-Video-Chat' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: E2EE-Video-Chat/README.md ================================================ E2EE Basic Video Chat Sample App =============================== The E2EE Basic-Video-Chat app is a very simple application meant to get a new developer started using the OpenTok iOS SDK and end to end encryption. Quick Start ----------- To use this application: 1. Follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. Among other things, you need to set values for the `kApiKey`, `kSessionId`, `kToken`, and `kEncryptionSecret` constants. See [Obtaining OpenTok Credentials](../README.md#obtaining-opentok-credentials) in the main README file for the repository. 2. To create an E2EE connection you must first enable this functionality server side. You enable end-to-end encryption when you create a session using the REST API. Set the e2ee property to true. See the [E2EE](https://tokbox.com/developer/guides/end-to-end-encryption/#server_side) guide. 3. When you run the application, it connects to an OpenTok session and publishes an audio-video stream from your device to the session. 4. Run the app on a second client. You can do this by deploying the app to an iOS device and testing it in the simulator at the same time. Or you can use the browser_demo.html file to connect in a browser (see the following section). When the second client connects, it also publishes a stream to the session, and both clients subscribe to (view) each other’s stream. Application Notes ----------------- * Follow the code from the `ViewController.viewDidLoad(_:)` method through to the OpenTok callbacks to see how streams are created and handled in the OpenTok iOS SDK. * By default, all delegate methods from classes in the OpenTok iOS SDK are invoked on the main queue. This means that you can directly modify the view hierarchy from inside the callback, without any asynchronous callouts. * When the main view loads, the ViewController calls the `OTSession.setEncryptionSecret(_:, error:)` method to set the encryption secret. Then the `OTSession.initWithApiKey(_:, sessionId:,delegate:)` method to initialize a Session object. The app then calls the `OTSession.connectWithToken(_:, error:)` to connect to the session. The `OTSessionDelegate.sessionDidConnect(_:)` message is sent when the app connects to the OpenTok session. * The `doPublish()` method of the app initializes a publisher and passes it into the `OTSession.publish(_:,error:)` method. This publishes an audio-video stream to the session. * The `OTSessionDelegate.session(_:,streamCreated:)` message is sent when a new stream is created in the session. In response, the method calls `OTSubscriber(stream:,delegate:)`, passing in the OTStream object. This causes the app to subscribe to the stream. Configuration Notes ------------------- * You can test in the iOS Simulator or on a supported iOS device. However, the XCode iOS Simulator does not provide access to the camera. When running in the iOS Simulator, an OTPublisher object uses a demo video instead of the camera. [1]: https://tokbox.com/account/#/ [2]: https://tokbox.com/developer/sdks/server/ ================================================ FILE: FrameMetadata/FrameMetadata/AppDelegate.swift ================================================ // // AppDelegate.swift // Lets-Build-OTPublisher // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: FrameMetadata/FrameMetadata/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: FrameMetadata/FrameMetadata/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: FrameMetadata/FrameMetadata/Base.lproj/Main.storyboard ================================================ ================================================ FILE: FrameMetadata/FrameMetadata/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName FrameMetadata CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription NSMicrophoneUsageDescription UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: FrameMetadata/FrameMetadata/ViewController.swift ================================================ // // ViewController.swift // Lets-Build-OTPublisher // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok let kWidgetRatio: CGFloat = 1.333 // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() lazy var dateFormatter: DateFormatter = { let theDataFormatter = DateFormatter() theDataFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" return theDataFormatter }() var publisher: OTPublisher? var subscriber: OTSubscriber? let captureSession = AVCaptureSession() @IBOutlet weak var metadataLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? = nil defer { processError(error) } let settings = OTPublisherSettings() settings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: settings) if let pub = publisher { let videoRender = ExampleVideoRender() videoRender.delegate = self let videoCapture = ExampleVideoCapture() videoCapture.delegate = self pub.videoCapture = videoCapture pub.videoRender = videoRender session.publish(pub, error: &error) videoRender.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width / kWidgetRatio) view.insertSubview(videoRender, at: 0) } } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func cleanupSubscriber() { subscriber?.view?.removeFromSuperview() subscriber = nil } fileprivate func processError(_ error: OTError?) { if let err = error { showAlert(errorStr: err.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } @IBAction func toggleCamera(_ sender: Any) { if let capturer = publisher?.videoCapture as? ExampleVideoCapture { let _ = capturer.toggleCameraPosition() } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscriber == nil { doSubscribe(stream) } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { subscriber?.view?.frame = CGRect(x: 0, y: view.frame.width / kWidgetRatio, width: view.frame.width, height: view.frame.width / kWidgetRatio) if let subsView = subscriber?.view { view.addSubview(subsView) } } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } extension ViewController: ExampleVideoRenderDelegate { func renderer(_ renderer: ExampleVideoRender, didReceiveFrame videoFrame: OTVideoFrame) { guard let metadata = videoFrame.metadata, let timestamp = String(data: metadata, encoding: .utf8) else { print("Receiving video frame without metadata attached") return } DispatchQueue.main.async { self.metadataLabel.text = timestamp print("Receiving video frame metadata", timestamp) } } } /* * This piece is optional: we demonstrate how to attach a metadata to a video frame before transmitting to the OpenTok platform. * You don't have to attach a metadata to make the transmission work */ extension ViewController: FrameCapturerMetadataDelegate { func finishPreparingFrame(_ videoFrame: OTVideoFrame?) { guard let videoFrame = videoFrame else { return } setTimestampToVideoFrame(videoFrame) } fileprivate func setTimestampToVideoFrame(_ videoFrame: OTVideoFrame?) { guard let videoFrame = videoFrame else { return } let timestamp = self.dateFormatter.string(from: Date()) let metadata = Data(timestamp.utf8) var error: OTError? videoFrame.setMetadata(metadata, error: &error) if let error = error { print(error) } } } ================================================ FILE: FrameMetadata/FrameMetadata.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A07BCDBC2089048F00A1B684 /* EAGLVideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A07BCDB92089048F00A1B684 /* EAGLVideoRenderer.swift */; }; A07BCDBD2089048F00A1B684 /* ExampleVideoCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = A07BCDBA2089048F00A1B684 /* ExampleVideoCapture.swift */; }; A07BCDBE2089048F00A1B684 /* ExampleVideoRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = A07BCDBB2089048F00A1B684 /* ExampleVideoRender.swift */; }; F86C64BC1D5C8A150081846D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86C64BB1D5C8A150081846D /* AppDelegate.swift */; }; F86C64BE1D5C8A150081846D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86C64BD1D5C8A150081846D /* ViewController.swift */; }; F86C64C11D5C8A150081846D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F86C64BF1D5C8A150081846D /* Main.storyboard */; }; F86C64C31D5C8A150081846D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F86C64C21D5C8A150081846D /* Assets.xcassets */; }; F86C64C61D5C8A150081846D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A07BCDB92089048F00A1B684 /* EAGLVideoRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EAGLVideoRenderer.swift; path = "../../Custom-Video-Driver/Lets-Build-OTPublisher/EAGLVideoRenderer.swift"; sourceTree = ""; }; A07BCDBA2089048F00A1B684 /* ExampleVideoCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleVideoCapture.swift; path = "../../Custom-Video-Driver/Lets-Build-OTPublisher/ExampleVideoCapture.swift"; sourceTree = ""; }; A07BCDBB2089048F00A1B684 /* ExampleVideoRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleVideoRender.swift; path = "../../Custom-Video-Driver/Lets-Build-OTPublisher/ExampleVideoRender.swift"; sourceTree = ""; }; F86C64B81D5C8A150081846D /* FrameMetadata.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FrameMetadata.app; sourceTree = BUILT_PRODUCTS_DIR; }; F86C64BB1D5C8A150081846D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F86C64BD1D5C8A150081846D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F86C64C01D5C8A150081846D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; F86C64C21D5C8A150081846D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F86C64C51D5C8A150081846D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F86C64C71D5C8A150081846D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F86C64B51D5C8A150081846D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F86C64AF1D5C8A150081846D = { isa = PBXGroup; children = ( F86C64BA1D5C8A150081846D /* FrameMetadata */, F86C64B91D5C8A150081846D /* Products */, ); sourceTree = ""; }; F86C64B91D5C8A150081846D /* Products */ = { isa = PBXGroup; children = ( F86C64B81D5C8A150081846D /* FrameMetadata.app */, ); name = Products; sourceTree = ""; }; F86C64BA1D5C8A150081846D /* FrameMetadata */ = { isa = PBXGroup; children = ( A07BCDB92089048F00A1B684 /* EAGLVideoRenderer.swift */, A07BCDBA2089048F00A1B684 /* ExampleVideoCapture.swift */, A07BCDBB2089048F00A1B684 /* ExampleVideoRender.swift */, F86C64BB1D5C8A150081846D /* AppDelegate.swift */, F86C64BD1D5C8A150081846D /* ViewController.swift */, F86C64BF1D5C8A150081846D /* Main.storyboard */, F86C64C21D5C8A150081846D /* Assets.xcassets */, F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */, F86C64C71D5C8A150081846D /* Info.plist */, ); path = FrameMetadata; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F86C64B71D5C8A150081846D /* FrameMetadata */ = { isa = PBXNativeTarget; buildConfigurationList = F86C64CA1D5C8A150081846D /* Build configuration list for PBXNativeTarget "FrameMetadata" */; buildPhases = ( F86C64B41D5C8A150081846D /* Sources */, F86C64B51D5C8A150081846D /* Frameworks */, F86C64B61D5C8A150081846D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = FrameMetadata; productName = "Lets-Build-OTPublisher"; productReference = F86C64B81D5C8A150081846D /* FrameMetadata.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86C64B01D5C8A150081846D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F86C64B71D5C8A150081846D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = MGD69Q275W; LastSwiftMigration = 1200; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = F86C64B31D5C8A150081846D /* Build configuration list for PBXProject "FrameMetadata" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F86C64AF1D5C8A150081846D; productRefGroup = F86C64B91D5C8A150081846D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F86C64B71D5C8A150081846D /* FrameMetadata */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F86C64B61D5C8A150081846D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F86C64C61D5C8A150081846D /* LaunchScreen.storyboard in Resources */, F86C64C31D5C8A150081846D /* Assets.xcassets in Resources */, F86C64C11D5C8A150081846D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F86C64B41D5C8A150081846D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A07BCDBC2089048F00A1B684 /* EAGLVideoRenderer.swift in Sources */, F86C64BE1D5C8A150081846D /* ViewController.swift in Sources */, F86C64BC1D5C8A150081846D /* AppDelegate.swift in Sources */, A07BCDBD2089048F00A1B684 /* ExampleVideoCapture.swift in Sources */, A07BCDBE2089048F00A1B684 /* ExampleVideoRender.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ F86C64BF1D5C8A150081846D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( F86C64C01D5C8A150081846D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F86C64C51D5C8A150081846D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F86C64C81D5C8A150081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F86C64C91D5C8A150081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F86C64CB1D5C8A150081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = MGD69Q275W; "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( "$(inherited)", "COCOAPODS=1", "GLES_SILENCE_DEPRECATION=1", ); INFOPLIST_FILE = FrameMetadata/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.tokbox.FrameMetadata; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F86C64CC1D5C8A150081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = MGD69Q275W; INFOPLIST_FILE = FrameMetadata/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.tokbox.FrameMetadata; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F86C64B31D5C8A150081846D /* Build configuration list for PBXProject "FrameMetadata" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64C81D5C8A150081846D /* Debug */, F86C64C91D5C8A150081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F86C64CA1D5C8A150081846D /* Build configuration list for PBXNativeTarget "FrameMetadata" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64CB1D5C8A150081846D /* Debug */, F86C64CC1D5C8A150081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F86C64B01D5C8A150081846D /* Project object */; } ================================================ FILE: FrameMetadata/FrameMetadata.xcodeproj/xcshareddata/xcschemes/FrameMetadata.xcscheme ================================================ ================================================ FILE: FrameMetadata/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'FrameMetadata' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: FrameMetadata/README.md ================================================ Frame Metadata ================================== This project shows how to set metadata (limited to 32 bytes) to a video frame, as well as how to read metadata from a video frame. It basically extends project 2, "Let's build OTPublisher." By the end of a code review, you should learn how to set and get your desired metedata from a video frame of publisher and subscriber. Note that this sample application is not supported in the XCode iOS Simulator because the custom video capturer needs to acquire video from an iOS device camera. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. TBExampleVideoRender and TBExampleVideoCapture ------------------------------------------ In this example we will include our implementations of OTVideoRender and OTVideoCapture that will allow us to capture and render video by ourselves. The purpose of including a custom renderer and capturer is to access the underlying video frame (`OTVideoFrame`) which is not directly visible at `OTPublisher` or `OTSubscriber` class. Once each farme is ready to transmit to the OpenTok platform, we can now attach metadata by invoking the `[setMetadata:error:]` method. In the sample app, we show how to attach an ISO standard timestamp to a video frame: ``` func finishPreparingFrame(_ videoFrame: OTVideoFrame?) { guard let videoFrame = videoFrame else { return } setTimestampToVideoFrame(videoFrame) } fileprivate func setTimestampToVideoFrame(_ videoFrame: OTVideoFrame?) { guard let videoFrame = videoFrame else { return } let timestamp = self.dateFormatter.string(from: Date()) let metdata = Data(timestamp.utf8) var error: OTError? videoFrame.setMetadata(metdata, error: &error) if let error = error { print(error) } } ``` By conforming the `FrameCapturerMetadataDelegate` protocol and implementing the `[finishPreparingFrame:]` method, you will receive a ready video frame to attach your metadata. To read the data, the approarch is similar. We simply need to access the underlying video frame and read the `metadata` property. ``` func renderer(_ renderer: ExampleVideoRender, didReceiveFrame videoFrame: OTVideoFrame) { guard let metadata = videoFrame.metadata, let timestampe = String(data: metadata, encoding: .utf8) else { print("Receiving video frame without metadata attached") return } DispatchQueue.main.async { self.metadataLabel.text = timestampe print("Receiving video frame metadata", timestampe) } } ``` In the sample app, we conform `ExampleVideoRenderDelegate` and implement `[renderer:didReceiveFrame:]` method to pass back each video frame. *Note: you can always directly access a video frame from a custom captuere or renderer. We follow the classic delegate design pattern to enforce better abstraction and reusability.* ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2013-2016 TokBox, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/AppDelegate.swift ================================================ // // AppDelegate.swift // Live-Photo-Capture // // Created by Roberto Perez Cubero on 23/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/ExamplePhotoVideoCapture.swift ================================================ // // ExamplePhotoVideoCapture.swift // Live-Photo-Capture // // Created by Roberto Perez Cubero on 23/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import AVFoundation class ExamplePhotoVideoCapture: ExampleVideoCapture { var stillImageOutput: AVCaptureStillImageOutput? var oldPreset: AVCaptureSession.Preset? private func waitForSensor() { let now = CACurrentMediaTime() let timeout = 1.0 while (timeout > (CACurrentMediaTime() - now)) && (videoInput!.device.isAdjustingExposure || videoInput!.device.isAdjustingFocus) {} return } private func pauseVideoCaptureForPhoto() { captureSession?.beginConfiguration() oldPreset = captureSession?.sessionPreset captureSession?.sessionPreset = AVCaptureSession.Preset.photo stillImageOutput = AVCaptureStillImageOutput() stillImageOutput?.outputSettings = [AVVideoCodecKey: AVVideoCodecType.jpeg] guard let stillImageOutput = self.stillImageOutput else { print("Error setting stillImageOutput") return } captureSession?.addOutput(stillImageOutput) captureSession?.commitConfiguration() waitForSensor() } private func resumeVideoCapture() { captureSession?.beginConfiguration() guard let oldPreset = oldPreset else { print("Error get oldPreset") return } captureSession?.sessionPreset = oldPreset guard let stillImageOutput = self.stillImageOutput else { print("Error setting stillImageOutput") return } captureSession?.removeOutput(stillImageOutput) captureSession?.commitConfiguration() } private func doPhotoCapture(completionHandler handler: @escaping (_ photo: UIImage?) -> ()) { guard let connection:AVCaptureConnection = stillImageOutput?.connections.filter({ conn -> Bool in (conn ).inputPorts.contains( where: { return ($0 ).mediaType == AVMediaType.video }) }).first else { handler(nil) return } stillImageOutput?.captureStillImageAsynchronously(from: connection, completionHandler: { (buffer, error) in guard let buffer = buffer else { print("Error gettinb buffer") return } let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer) let resultImage = UIImage(data: data!) handler(resultImage) }) } func takePhoto(completionHandler handler: @escaping (_ photo: UIImage?) -> ()) { captureQueue.async { self.pauseVideoCaptureForPhoto() self.doPhotoCapture(completionHandler: { img in DispatchQueue.main.async { handler(img) } }) self.resumeVideoCapture() } } } ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture/ViewController.swift ================================================ // // ViewController.swift // Live-Photo-Capture // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok let kWidgetHeight = 240 let kWidgetWidth = 320 // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() var publisher: OTPublisher? var subscriber: OTSubscriber? let captureQueue = DispatchQueue(label: "com.tokbox.VideoCapture") let photoVideoCapture = ExamplePhotoVideoCapture() var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() imageView = UIImageView(frame: CGRect(x: 0, y: kWidgetHeight, width: kWidgetWidth, height: kWidgetHeight)) imageView.backgroundColor = UIColor.green view.addSubview(imageView) let singleTap = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleSingleTap(_:))) view.addGestureRecognizer(singleTap) doConnect() } @objc func handleSingleTap(_ gestureRecognizer: UIGestureRecognizer) { photoVideoCapture.takePhoto { (photo) in self.imageView.image = photo self.imageView.setNeedsDisplay() } } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ private func doConnect() { var error: OTError? defer { process(error: error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? = nil defer { process(error: error) } let pubSettings = OTPublisherSettings() pubSettings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: pubSettings) if let pub = publisher { let videoRender = ExampleVideoRender() pub.videoRender = videoRender pub.videoCapture = photoVideoCapture session.publish(pub, error: &error) videoRender.frame = CGRect(x: 0, y: 0, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(videoRender) } } fileprivate func process(error err: OTError?) { if let e = err { showAlert(errorStr: e.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ F872F76E1D6C851400A0674B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F872F76D1D6C851400A0674B /* AppDelegate.swift */; }; F872F7701D6C851400A0674B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F872F76F1D6C851400A0674B /* ViewController.swift */; }; F872F7731D6C851400A0674B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F872F7711D6C851400A0674B /* Main.storyboard */; }; F872F7751D6C851400A0674B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F872F7741D6C851400A0674B /* Assets.xcassets */; }; F872F7781D6C851400A0674B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F872F7761D6C851400A0674B /* LaunchScreen.storyboard */; }; F872F7851D6C87DD00A0674B /* EAGLVideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F872F7811D6C87DD00A0674B /* EAGLVideoRenderer.swift */; }; F872F7871D6C87DD00A0674B /* ExampleVideoCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F872F7831D6C87DD00A0674B /* ExampleVideoCapture.swift */; }; F872F7881D6C87DD00A0674B /* ExampleVideoRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = F872F7841D6C87DD00A0674B /* ExampleVideoRender.swift */; }; F872F78B1D6C889500A0674B /* ExamplePhotoVideoCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F872F78A1D6C889500A0674B /* ExamplePhotoVideoCapture.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ F872F76A1D6C851400A0674B /* Live-Photo-Capture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Live-Photo-Capture.app"; sourceTree = BUILT_PRODUCTS_DIR; }; F872F76D1D6C851400A0674B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F872F76F1D6C851400A0674B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F872F7721D6C851400A0674B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; F872F7741D6C851400A0674B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F872F7771D6C851400A0674B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F872F7791D6C851400A0674B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F872F7811D6C87DD00A0674B /* EAGLVideoRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EAGLVideoRenderer.swift; path = "../Custom-Video-Driver/Lets-Build-OTPublisher/EAGLVideoRenderer.swift"; sourceTree = ""; }; F872F7831D6C87DD00A0674B /* ExampleVideoCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleVideoCapture.swift; path = "../Custom-Video-Driver/Lets-Build-OTPublisher/ExampleVideoCapture.swift"; sourceTree = ""; }; F872F7841D6C87DD00A0674B /* ExampleVideoRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ExampleVideoRender.swift; path = "../Custom-Video-Driver/Lets-Build-OTPublisher/ExampleVideoRender.swift"; sourceTree = ""; }; F872F78A1D6C889500A0674B /* ExamplePhotoVideoCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExamplePhotoVideoCapture.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F872F7671D6C851400A0674B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F872F7611D6C851400A0674B = { isa = PBXGroup; children = ( F872F7891D6C87E300A0674B /* Live-Photo-Capture */, F872F76C1D6C851400A0674B /* Live-Photo-Capture */, F872F76B1D6C851400A0674B /* Products */, ); sourceTree = ""; }; F872F76B1D6C851400A0674B /* Products */ = { isa = PBXGroup; children = ( F872F76A1D6C851400A0674B /* Live-Photo-Capture.app */, ); name = Products; sourceTree = ""; }; F872F76C1D6C851400A0674B /* Live-Photo-Capture */ = { isa = PBXGroup; children = ( F872F76D1D6C851400A0674B /* AppDelegate.swift */, F872F76F1D6C851400A0674B /* ViewController.swift */, F872F7711D6C851400A0674B /* Main.storyboard */, F872F7741D6C851400A0674B /* Assets.xcassets */, F872F7761D6C851400A0674B /* LaunchScreen.storyboard */, F872F7791D6C851400A0674B /* Info.plist */, F872F78A1D6C889500A0674B /* ExamplePhotoVideoCapture.swift */, ); path = "Live-Photo-Capture"; sourceTree = ""; }; F872F7891D6C87E300A0674B /* Live-Photo-Capture */ = { isa = PBXGroup; children = ( F872F7811D6C87DD00A0674B /* EAGLVideoRenderer.swift */, F872F7831D6C87DD00A0674B /* ExampleVideoCapture.swift */, F872F7841D6C87DD00A0674B /* ExampleVideoRender.swift */, ); name = "Live-Photo-Capture"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F872F7691D6C851400A0674B /* Live-Photo-Capture */ = { isa = PBXNativeTarget; buildConfigurationList = F872F77C1D6C851400A0674B /* Build configuration list for PBXNativeTarget "Live-Photo-Capture" */; buildPhases = ( F872F7661D6C851400A0674B /* Sources */, F872F7671D6C851400A0674B /* Frameworks */, F872F7681D6C851400A0674B /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Live-Photo-Capture"; productName = "Live-Photo-Capture"; productReference = F872F76A1D6C851400A0674B /* Live-Photo-Capture.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F872F7621D6C851400A0674B /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F872F7691D6C851400A0674B = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F872F7651D6C851400A0674B /* Build configuration list for PBXProject "Live-Photo-Capture" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F872F7611D6C851400A0674B; productRefGroup = F872F76B1D6C851400A0674B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F872F7691D6C851400A0674B /* Live-Photo-Capture */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F872F7681D6C851400A0674B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F872F7781D6C851400A0674B /* LaunchScreen.storyboard in Resources */, F872F7751D6C851400A0674B /* Assets.xcassets in Resources */, F872F7731D6C851400A0674B /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F872F7661D6C851400A0674B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F872F78B1D6C889500A0674B /* ExamplePhotoVideoCapture.swift in Sources */, F872F7851D6C87DD00A0674B /* EAGLVideoRenderer.swift in Sources */, F872F7701D6C851400A0674B /* ViewController.swift in Sources */, F872F7871D6C87DD00A0674B /* ExampleVideoCapture.swift in Sources */, F872F76E1D6C851400A0674B /* AppDelegate.swift in Sources */, F872F7881D6C87DD00A0674B /* ExampleVideoRender.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ F872F7711D6C851400A0674B /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( F872F7721D6C851400A0674B /* Base */, ); name = Main.storyboard; sourceTree = ""; }; F872F7761D6C851400A0674B /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F872F7771D6C851400A0674B /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F872F77A1D6C851400A0674B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F872F77B1D6C851400A0674B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F872F77D1D6C851400A0674B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", "GLES_SILENCE_DEPRECATION=1", ); INFOPLIST_FILE = "Live-Photo-Capture/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Live-Photo-Capture"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F872F77E1D6C851400A0674B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "Live-Photo-Capture/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Live-Photo-Capture"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F872F7651D6C851400A0674B /* Build configuration list for PBXProject "Live-Photo-Capture" */ = { isa = XCConfigurationList; buildConfigurations = ( F872F77A1D6C851400A0674B /* Debug */, F872F77B1D6C851400A0674B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F872F77C1D6C851400A0674B /* Build configuration list for PBXNativeTarget "Live-Photo-Capture" */ = { isa = XCConfigurationList; buildConfigurations = ( F872F77D1D6C851400A0674B /* Debug */, F872F77E1D6C851400A0674B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F872F7621D6C851400A0674B /* Project object */; } ================================================ FILE: Live-Photo-Capture/Live-Photo-Capture.xcodeproj/xcshareddata/xcschemes/Live-Photo-Capture.xcscheme ================================================ ================================================ FILE: Live-Photo-Capture/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Live-Photo-Capture' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Live-Photo-Capture/README.md ================================================ Live Photo Capture Sample App ================================ This project extends some of the material we presented in [Project 2][1], by demonstrating how a simple capture implementation can be extended to provide interesting features. By the end of a code review of this project, you should understand how to use the AVFoundation API to temporarily halt your video capture module, adjust capture settings to use photo-quality resolution, capture a picture, then resume video capture. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. Configuration Notes ------------------- Since we are importing a number of classes implemented in project 2, the header search paths in the project build settings must be extended to look in the project 2 directory. Application Notes ----------------- * The only new implementation in this project is the TBExamplePhotoVideoCapture class. By subclassing the video capture module implemented in project 2, we save some time setting up standard video capture and focus only on manipulating AVFoundation to give us a picture in the middle of a (video) capture session. * In testing, we noticed that the image sensor takes a moment to adjust white balance and exposure after switching to the capture session preset for photo quality (see `pauseVideoCaptureForPhoto` in TBExamplePhotoVideoCapture). The example implementation stalls the photo capture with a busy loop, but a more sophisticated approach might be considered to ensure the best experience for the end user. * This implementation briefly pauses the video feed during image capture. Note that a prolonged delay to the video capture consumer might result in other subscribers timing out the stream. If you wait for too long to send consecutive video frames, you might lose the publisher altogether! Consider sending a freeze frame or even a blank image buffer to the video capture endpoint if you need to pause video for a long (>2 second) period. * An alternative approach to this problem might be to continuously pipe photo- quality video into the video capture consumer. This might not work on some devices, based on processing capability and the fidelity of the image sensor. Feel free to experiment and let us know how it goes for you! * Note that this sample application is not supported in the XCode iOS Simulator because the custom video capturer needs to acquire video from an iOS device camera. [1]: ../2.Lets-Build-OTPublisher ================================================ FILE: Media-Transformers/CHANGELOG.md ================================================ # Video Transformers Changelog All notable changes to this project will be documented in this file. ## 2.27.2 ### Added - Support Vonage Media Processor library as a opt-in library. - Supports Noise Suppression ### Fixed - Decoupled Media Processor library ## 2.26.0 ### Added - Support pre-built transformers in the Vonage Media Processor library or create your own custom video transformer to apply to published video. ### Known issues - When using the simulator, video transformers are not visible on the publisher side. ### Fixed - NA ### Enhancements - NA ### Changed - NA ### Deprecated - NA ================================================ FILE: Media-Transformers/Media-Transformers/AppDelegate.swift ================================================ // // AppDelegate.swift // Hello-World // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } } ================================================ FILE: Media-Transformers/Media-Transformers/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Media-Transformers/Media-Transformers/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Media-Transformers/Media-Transformers/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Media-Transformers/Media-Transformers/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Media-Transformers/Media-Transformers/ViewController.swift ================================================ // // ViewController.swift // Hello-World // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" let kWidgetHeight = 240 let kWidgetWidth = 320 class CustomTransformer: NSObject, OTCustomVideoTransformer { func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) image.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) let resizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return resizedImage } func transform(_ videoFrame: OTVideoFrame) { if let image = UIImage(named: "Vonage_Logo.png") { let yPlaneData = videoFrame.getPlaneBinaryData(0) let videoWidth = Int(videoFrame.format?.imageWidth ?? 0) let videoHeight = Int(videoFrame.format?.imageHeight ?? 0) // Calculate the desired size of the image let desiredWidth = CGFloat(videoWidth) / 8 // Adjust this value as needed let desiredHeight = image.size.height * (desiredWidth / image.size.width) // Resize the image to the desired size if let resizedImage = resizeImage(image, to: CGSize(width: desiredWidth, height: desiredHeight)) { let yPlane = yPlaneData // Create a CGContext from the Y plane guard let context = CGContext(data: yPlane, width: videoWidth, height: videoHeight, bitsPerComponent: 8, bytesPerRow: videoWidth, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return } // Location of the image (in this case right bottom corner) let x = CGFloat(videoWidth) * 4/5 let y = CGFloat(videoHeight) * 1/5 // Draw the resized image on top of the Y plane let rect = CGRect(x: x, y: y, width: desiredWidth, height: desiredHeight) context.draw(resizedImage.cgImage!, in: rect) } } } } class ViewController: UIViewController { var buttonMediaTransformerToggle: UIButton! lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() lazy var publisher: OTPublisher = { let settings = OTPublisherSettings() settings.name = UIDevice.current.name return OTPublisher(delegate: self, settings: settings)! }() var subscriber: OTSubscriber? override func viewDidLoad() { super.viewDidLoad() doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? defer { processError(error) } session.publish(publisher, error: &error) if error != nil { fatalError("An error occurred: \(String(describing: error))") } if let pubView = publisher.view { pubView.frame = CGRect(x: 0, y: 0, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(pubView) } // Configure toogle button buttonMediaTransformerToggle = UIButton(type: .custom) buttonMediaTransformerToggle.frame = CGRect(x: kWidgetWidth - 65, y: 50, width: 50, height: 25) buttonMediaTransformerToggle.layer.cornerRadius = 5.0 self.view.addSubview(buttonMediaTransformerToggle) self.view.bringSubviewToFront(buttonMediaTransformerToggle) buttonMediaTransformerToggle.setTitle("set", for: .normal) buttonMediaTransformerToggle.titleLabel?.font = UIFont.systemFont(ofSize: 12) buttonMediaTransformerToggle.setTitleColor(.gray, for: .normal) buttonMediaTransformerToggle.backgroundColor = .white buttonMediaTransformerToggle.layer.borderWidth = 1.0 buttonMediaTransformerToggle.layer.borderColor = UIColor.gray.cgColor buttonMediaTransformerToggle.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func cleanupSubscriber() { subscriber?.view?.removeFromSuperview() subscriber = nil } fileprivate func cleanupPublisher() { publisher.view?.removeFromSuperview() } fileprivate func processError(_ error: OTError?) { if let err = error { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err.localizedDescription, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } } let logoTransformer: CustomTransformer = CustomTransformer() // Create an instance of CustomTransformer @objc func buttonTapped(_ sender: UIButton) { if publisher.videoTransformers.isEmpty { // Create background blur Vonage transformer guard let backgroundBlur = OTVideoTransformer(name: "BackgroundBlur", properties: "{\"radius\":\"High\"}") else { return } // Create custom transformer guard let myCustomTransformer = OTVideoTransformer(name: "logo", transformer: logoTransformer) else { return } var myVideoTransformers = [OTVideoTransformer]() myVideoTransformers.append(backgroundBlur) myVideoTransformers.append(myCustomTransformer) // Set video transformers to publisher video stream publisher.videoTransformers = myVideoTransformers buttonMediaTransformerToggle.setTitle("reset", for: .normal) } else { // Clear all transformers from video stream publisher.videoTransformers = [] buttonMediaTransformerToggle.setTitle("set", for: .normal) } if publisher.audioTransformers.isEmpty { // Create Noise Suppression Vonage transformer guard let ns = OTAudioTransformer(name: "NoiseSuppression", properties: "") else { return } var myAudioTransformers = [OTAudioTransformer]() myAudioTransformers.append(ns) // Set audio transformers to publisher audio stream publisher.audioTransformers = myAudioTransformers buttonMediaTransformerToggle.setTitle("reset", for: .normal) } else { // Clear all transformers from audio stream publisher.audioTransformers = [] buttonMediaTransformerToggle.setTitle("set", for: .normal) } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscriber == nil { doSubscribe(stream) } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { cleanupPublisher() if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { if let subsView = subscriber?.view { subsView.frame = CGRect(x: 0, y: kWidgetHeight, width: kWidgetWidth, height: kWidgetHeight) view.addSubview(subsView) } } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } } ================================================ FILE: Media-Transformers/Media-Transformers.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A05375D71EB1633400645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375CF1EB1633400645696 /* AppDelegate.swift */; }; A05375D81EB1633400645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05375D01EB1633400645696 /* Assets.xcassets */; }; A05375D91EB1633400645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375D11EB1633400645696 /* LaunchScreen.storyboard */; }; A05375DA1EB1633400645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375D31EB1633400645696 /* Main.storyboard */; }; A05375DC1EB1633400645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375D61EB1633400645696 /* ViewController.swift */; }; ADE6E7BE2A93B07300895674 /* Vonage_Logo.png in Resources */ = {isa = PBXBuildFile; fileRef = ADE6E7BD2A93B07300895674 /* Vonage_Logo.png */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A05375CF1EB1633400645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05375D01EB1633400645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05375D21EB1633400645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05375D41EB1633400645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05375D51EB1633400645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05375D61EB1633400645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; ADE6E7BD2A93B07300895674 /* Vonage_Logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Vonage_Logo.png; path = "Media-Transformers/Vonage_Logo.png"; sourceTree = ""; }; F86C649A1D5C7C630081846D /* Media-Transformers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Media-Transformers.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F86C64971D5C7C630081846D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 651BF1BD8407C44E924A3910 /* Pods */ = { isa = PBXGroup; children = ( ); path = Pods; sourceTree = ""; }; A05375CE1EB1633400645696 /* Media-Transformers */ = { isa = PBXGroup; children = ( A05375CF1EB1633400645696 /* AppDelegate.swift */, A05375D01EB1633400645696 /* Assets.xcassets */, A05375D11EB1633400645696 /* LaunchScreen.storyboard */, A05375D31EB1633400645696 /* Main.storyboard */, A05375D51EB1633400645696 /* Info.plist */, A05375D61EB1633400645696 /* ViewController.swift */, ); path = "Media-Transformers"; sourceTree = ""; }; F86C64911D5C7C630081846D = { isa = PBXGroup; children = ( ADE6E7BD2A93B07300895674 /* Vonage_Logo.png */, A05375CE1EB1633400645696 /* Media-Transformers */, F86C649B1D5C7C630081846D /* Products */, 651BF1BD8407C44E924A3910 /* Pods */, ); sourceTree = ""; }; F86C649B1D5C7C630081846D /* Products */ = { isa = PBXGroup; children = ( F86C649A1D5C7C630081846D /* Media-Transformers.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F86C64991D5C7C630081846D /* Media-Transformers */ = { isa = PBXNativeTarget; buildConfigurationList = F86C64AC1D5C7C630081846D /* Build configuration list for PBXNativeTarget "Media-Transformers" */; buildPhases = ( F86C64961D5C7C630081846D /* Sources */, F86C64971D5C7C630081846D /* Frameworks */, F86C64981D5C7C630081846D /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Media-Transformers"; productName = "Hello-World"; productReference = F86C649A1D5C7C630081846D /* Media-Transformers.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86C64921D5C7C630081846D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 0930; ORGANIZATIONNAME = tokbox; TargetAttributes = { F86C64991D5C7C630081846D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F86C64951D5C7C630081846D /* Build configuration list for PBXProject "Media-Transformers" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( English, en, Base, ); mainGroup = F86C64911D5C7C630081846D; productRefGroup = F86C649B1D5C7C630081846D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F86C64991D5C7C630081846D /* Media-Transformers */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F86C64981D5C7C630081846D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ADE6E7BE2A93B07300895674 /* Vonage_Logo.png in Resources */, A05375DA1EB1633400645696 /* Main.storyboard in Resources */, A05375D81EB1633400645696 /* Assets.xcassets in Resources */, A05375D91EB1633400645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F86C64961D5C7C630081846D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375DC1EB1633400645696 /* ViewController.swift in Sources */, A05375D71EB1633400645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05375D11EB1633400645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05375D21EB1633400645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05375D31EB1633400645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05375D41EB1633400645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F86C64AA1D5C7C630081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64"; }; name = Debug; }; F86C64AB1D5C7C630081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; VALID_ARCHS = "arm64 arm64e armv7 armv7s x86_64"; }; name = Release; }; F86C64AD1D5C7C630081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Media-Transformers/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Hello-World"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; name = Debug; }; F86C64AE1D5C7C630081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Media-Transformers/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.Hello-World"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F86C64951D5C7C630081846D /* Build configuration list for PBXProject "Media-Transformers" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64AA1D5C7C630081846D /* Debug */, F86C64AB1D5C7C630081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F86C64AC1D5C7C630081846D /* Build configuration list for PBXNativeTarget "Media-Transformers" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64AD1D5C7C630081846D /* Debug */, F86C64AE1D5C7C630081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F86C64921D5C7C630081846D /* Project object */; } ================================================ FILE: Media-Transformers/Media-Transformers.xcodeproj/xcshareddata/xcschemes/Video-Transformers.xcscheme ================================================ ================================================ FILE: Media-Transformers/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Media-Transformers' do pod 'OTXCFramework', OpenTokSDKVersion pod 'VonageClientSDKVideoTransformers' , OpenTokSDKVersion end ================================================ FILE: Media-Transformers/README.md ================================================ Video Transformers ====================== The Video Transformers app is a very simple application created on top of Basic Video Chat meant to get a new developer started using Media Processor APIs on OpenTok iOS SDK. For a full description, see the [Video Transformers tutorial at the OpenTok developer center](https://tokbox.com/developer/guides/vonage-media-processor/ios). You can use pre-built transformers in the Vonage Media Processor library or create your own custom video transformer to apply to published video. You can use the OTPublisherKit.videoTransformers properties to apply video transformers to a stream. For video, you can apply the background blur video transformer included in the Vonage Media Library. You can use the OTPublisherKit.audioTransformers and OTPublisherKit.videoTransformers properties to apply audio and video transformers to a stream.

Important: The audio and video transformer API is a beta feature.

For video, you can apply the background blur video transformer included in the Vonage Media Library. You can also create your own custom audio and video transformers. ## Applying a video transformer from the Vonage Media Library Use the [OTVideoTransformer initWithName:properties:] method to create a video transformer that uses a named transformer from the Vonage Media Library. Currently, only one transformer is supported: background blur. Set the `name` parameter to `"BackgroundBlur"`. Set the `properties` parameter to a JSON string defining properties for the transformer. For the background blur transformer, this JSON includes one property -- `radius` -- which can be set to `"High"`, `"Low"`, or `"None"`. ```swift guard let backgroundBlur = OTVideoTransformer(name: "BackgroundBlur", properties: "{\"radius\":\"High\"}") else { return } var myVideoTransformers = [OTVideoTransformer]() myVideoTransformers.append(backgroundBlur) // Set video transformers to publisher video stream publisher.videoTransformers = myVideoTransformers ``` ## Creating a custom video transformer Create a class that implements the OTCustomVideoTransformer protocol. Implement the `[OTCustomVideoTransformer transform:]` method, applying a transformation to the `OTVideoFrame` object passed into the method. The `[OTCustomVideoTransformer transform:]` method is triggered for each video frame: ```swift class CustomTransformer: NSObject, OTCustomVideoTransformer { func transform(_ videoFrame: OTVideoFrame) { // Your custom transformation } } ``` In this sample, to display one of the infinite transformations that can be applied to video frames, a logo is being added to the bottom right corner of the video. ```swift class CustomTransformer: NSObject, OTCustomVideoTransformer { func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) image.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) let resizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return resizedImage } func transform(_ videoFrame: OTVideoFrame) { if let image = UIImage(named: "Vonage_Logo.png") { let yPlaneData = videoFrame.getPlaneBinaryData(0) let videoWidth = Int(videoFrame.format?.imageWidth ?? 0) let videoHeight = Int(videoFrame.format?.imageHeight ?? 0) // Calculate the desired size of the image let desiredWidth = CGFloat(videoWidth) / 8 // Adjust this value as needed let desiredHeight = image.size.height * (desiredWidth / image.size.width) // Resize the image to the desired size if let resizedImage = resizeImage(image, to: CGSize(width: desiredWidth, height: desiredHeight)) { let yPlane = yPlaneData // Create a CGContext from the Y plane guard let context = CGContext(data: yPlane, width: videoWidth, height: videoHeight, bitsPerComponent: 8, bytesPerRow: videoWidth, space: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return } // Location of the image (in this case right bottom corner) let x = CGFloat(videoWidth) * 4/5 let y = CGFloat(videoHeight) * 1/5 // Draw the resized image on top of the Y plane let rect = CGRect(x: x, y: y, width: desiredWidth, height: desiredHeight) context.draw(resizedImage.cgImage!, in: rect) } } } } ``` Then set the `OTPublisherKit.videoTransformers` property to an array that includes the object that implements the OTCustomVideoTransformer interface: ```swift // Create an instance of CustomTransformer var logoTransformer: CustomTransformer = CustomTransformer() ... // Create custom transformer guard let myCustomTransformer = OTVideoTransformer(name: "logo", transformer: logoTransformer) else { return } var myVideoTransformers = [OTVideoTransformer]() myVideoTransformers.append(myCustomTransformer) // Set video transformers to publisher video stream publisher.videoTransformers = myVideoTransformers ``` You can combine the Vonage Media library transformer (see the previous section) with custom transformers or apply multiple custom transformers by adding multiple PublisherKit.VideoTransformer objects to the ArrayList used for the `OTPublisherKit.videoTransformers` property. ## Clearing video transformers for a publisher To clear video transformers for a publisher, set the `OTPublisherKit.videoTransformers` property to an empty array. ```objectivec publisher.videoTransformers = [] ``` Adding the OpenTok library ========================== In this example the OpenTok iOS SDK was not included as a dependency, you can do it through Swift Package Manager or Cocoapods. Swift Package Manager --------------------- To add a package dependency to your Xcode project, you should select *File* > *Swift Packages* > *Add Package Dependency* and enter the repository URL: `https://github.com/opentok/vonage-client-sdk-video.git`. Cocoapods --------- To use CocoaPods to add the OpenTok library and its dependencies into this sample app simply open Terminal, navigate to the root directory of the project and run: `pod install`. The Media-Transformers app is a very simple application meant to get a new developer started using the OpenTok iOS SDK. ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/AppDelegate.swift ================================================ // // AppDelegate.swift // 7.Multiparty-UICollectionView // // Created by Roberto Perez Cubero on 17/04/2017. // Copyright © 2017 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/ChatViewController.swift ================================================ // // ViewController.swift // 7.Multiparty-UICollectionView // // Created by Roberto Perez Cubero on 17/04/2017. // Copyright © 2017 tokbox. All rights reserved. // import UIKit import OpenTok // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ChatViewController: UICollectionViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() lazy var publisher: OTPublisher = { let settings = OTPublisherSettings() settings.name = UIDevice.current.name return OTPublisher(delegate: self, settings: settings)! }() var subscribers: [OTSubscriber] = [] override func viewDidLoad() { super.viewDidLoad() doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? defer { processError(error) } session.publish(publisher, error: &error) collectionView?.reloadData() } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } guard let subscriber = OTSubscriber(stream: stream, delegate: self) else { print("Error while subscribing") return } session.subscribe(subscriber, error: &error) subscribers.append(subscriber) collectionView?.reloadData() } fileprivate func cleanupSubscriber(_ stream: OTStream) { subscribers = subscribers.filter { $0.stream?.streamId != stream.streamId } collectionView?.reloadData() } fileprivate func processError(_ error: OTError?) { if let err = error { showAlert(errorStr: err.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } // MARK: - UICollectionView methods override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return subscribers.count + 1 } override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) let videoView: UIView? = { if (indexPath.row == 0) { return publisher.view } else { let sub = subscribers[indexPath.row - 1] return sub.view } }() if let viewToAdd = videoView { viewToAdd.frame = cell.bounds cell.addSubview(viewToAdd) } return cell } } // MARK: - OTSession delegate callbacks extension ChatViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") doSubscribe(stream) } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") cleanupSubscriber(stream) } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ChatViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ChatViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { print("Subscriber connected") } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription NSMicrophoneUsageDescription UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView/MultipartyLayout.swift ================================================ // // MultipartyLayout.swift // 7.Multiparty-UICollectionView // // Created by Roberto Perez Cubero on 17/04/2017. // Copyright © 2017 tokbox. All rights reserved. // import UIKit extension Int { var isEven: Bool { return self % 2 == 0 } } class MultipartyLayout: UICollectionViewLayout { fileprivate var cache = [UICollectionViewLayoutAttributes]() fileprivate var cachedNumberOfViews = 0 override func prepare() { guard let views = collectionView?.numberOfItems(inSection: 0) else { cache.removeAll() return } if views != cachedNumberOfViews { cache.removeAll() } if cache.isEmpty { cachedNumberOfViews = views let attribs: [UICollectionViewLayoutAttributes] = { switch views { case 1: return attributesForPublisherFullScreen() case 2: return attributesForPublisherAndOneSubscriber() case let x where x > 2 && x.isEven: return attributesForAllViewsTwoByTwo(withNumberOfViews: x) case let x where x > 2 && !x.isEven: return attributesForPublisherOnTopAndSubscribersTwoByTwo(withNumberOfViews: x) default: return [] } }() cache.append(contentsOf: attribs) } } fileprivate func attributesForPublisherFullScreen() -> [UICollectionViewLayoutAttributes] { var attribs = [UICollectionViewLayoutAttributes]() let ip = IndexPath(item: 0, section: 0) let attr = UICollectionViewLayoutAttributes(forCellWith: ip) attr.frame = collectionView?.superview?.bounds ?? CGRect() attribs.append(attr) return attribs } // Will layout publisher view over subscriber view fileprivate func attributesForPublisherAndOneSubscriber() -> [UICollectionViewLayoutAttributes] { var attribs = [UICollectionViewLayoutAttributes]() let height = (collectionView?.superview?.bounds.size.height ?? 0) / 2 let width = collectionView?.superview?.bounds.size.width ?? 0 let pubIp = IndexPath(item: 0, section: 0) let pubAttribs = UICollectionViewLayoutAttributes(forCellWith: pubIp) pubAttribs.frame = CGRect(x: 0, y: 0, width: width, height: height) attribs.append(pubAttribs) let subIp = IndexPath(item: 1, section: 0) let subAttribs = UICollectionViewLayoutAttributes(forCellWith: subIp) subAttribs.frame = CGRect(x: 0, y:height, width: width, height: height) attribs.append(subAttribs) return attribs } fileprivate func attributesForPublisherOnTopAndSubscribersTwoByTwo(withNumberOfViews views: Int) -> [UICollectionViewLayoutAttributes] { var attribs = [UICollectionViewLayoutAttributes]() let rows = CGFloat(((views - 1) / 2) + 1) let height = (collectionView?.superview?.bounds.size.height ?? 0) / CGFloat(rows) let width = (collectionView?.superview?.bounds.size.width ?? 0) / 2 let pubIp = IndexPath(item: 0, section: 0) let pubAttribs = UICollectionViewLayoutAttributes(forCellWith: pubIp) pubAttribs.frame = CGRect(x: 0, y: 0, width: collectionView?.superview?.bounds.size.width ?? 0, height: height) attribs.append(pubAttribs) attribs.append(contentsOf: attributesForViewsInRows(initialYOffset: height, totalNumberOfViews: views, viewSize: CGSize(width: width, height: height), viewOffset: 1)) return attribs } fileprivate func attributesForAllViewsTwoByTwo(withNumberOfViews views: Int) -> [UICollectionViewLayoutAttributes] { var attribs = [UICollectionViewLayoutAttributes]() let rows = views / 2 let height = (collectionView?.superview?.bounds.size.height ?? 0) / CGFloat(rows) let width = (collectionView?.superview?.bounds.size.width ?? 0) / 2 attribs.append(contentsOf: attributesForViewsInRows(initialYOffset: 0, totalNumberOfViews: views, viewSize: CGSize(width: width, height: height), viewOffset: 0)) return attribs } fileprivate func attributesForViewsInRows(initialYOffset: CGFloat, totalNumberOfViews views: Int, viewSize: CGSize, viewOffset: Int) -> [UICollectionViewLayoutAttributes] { var attribs = [UICollectionViewLayoutAttributes]() var yOffset = initialYOffset let newLineCondition : (Int) -> Bool = { if viewOffset == 0 { return !$0.isEven } else { return $0.isEven } } for item in viewOffset.. viewOffset && newLineCondition(item) { yOffset += viewSize.height } } return attribs } override var collectionViewContentSize: CGSize { return collectionView?.superview?.bounds.size ?? CGSize() } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return cache } } ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A05376091EB1638C00645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05376001EB1638C00645696 /* AppDelegate.swift */; }; A053760A1EB1638C00645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05376011EB1638C00645696 /* Assets.xcassets */; }; A053760B1EB1638C00645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05376021EB1638C00645696 /* LaunchScreen.storyboard */; }; A053760C1EB1638C00645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05376041EB1638C00645696 /* Main.storyboard */; }; A053760D1EB1638C00645696 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05376061EB1638C00645696 /* ChatViewController.swift */; }; A053760F1EB1638C00645696 /* MultipartyLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05376081EB1638C00645696 /* MultipartyLayout.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A05376001EB1638C00645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05376011EB1638C00645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05376031EB1638C00645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05376051EB1638C00645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05376061EB1638C00645696 /* ChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; A05376071EB1638C00645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05376081EB1638C00645696 /* MultipartyLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartyLayout.swift; sourceTree = ""; }; F852CCBF1EA4D88200ADB206 /* Multiparty-UICollectionView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Multiparty-UICollectionView.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F852CCBC1EA4D88200ADB206 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A05375FF1EB1638C00645696 /* Multiparty-UICollectionView */ = { isa = PBXGroup; children = ( A05376001EB1638C00645696 /* AppDelegate.swift */, A05376011EB1638C00645696 /* Assets.xcassets */, A05376021EB1638C00645696 /* LaunchScreen.storyboard */, A05376041EB1638C00645696 /* Main.storyboard */, A05376061EB1638C00645696 /* ChatViewController.swift */, A05376071EB1638C00645696 /* Info.plist */, A05376081EB1638C00645696 /* MultipartyLayout.swift */, ); path = "Multiparty-UICollectionView"; sourceTree = ""; }; F852CCB61EA4D88200ADB206 = { isa = PBXGroup; children = ( A05375FF1EB1638C00645696 /* Multiparty-UICollectionView */, F852CCC01EA4D88200ADB206 /* Products */, ); sourceTree = ""; }; F852CCC01EA4D88200ADB206 /* Products */ = { isa = PBXGroup; children = ( F852CCBF1EA4D88200ADB206 /* Multiparty-UICollectionView.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F852CCBE1EA4D88200ADB206 /* Multiparty-UICollectionView */ = { isa = PBXNativeTarget; buildConfigurationList = F852CCD11EA4D88200ADB206 /* Build configuration list for PBXNativeTarget "Multiparty-UICollectionView" */; buildPhases = ( F852CCBB1EA4D88200ADB206 /* Sources */, F852CCBC1EA4D88200ADB206 /* Frameworks */, F852CCBD1EA4D88200ADB206 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Multiparty-UICollectionView"; productName = "7.Multiparty-UICollectionView"; productReference = F852CCBF1EA4D88200ADB206 /* Multiparty-UICollectionView.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F852CCB71EA4D88200ADB206 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0830; LastUpgradeCheck = 0930; ORGANIZATIONNAME = tokbox; TargetAttributes = { F852CCBE1EA4D88200ADB206 = { CreatedOnToolsVersion = 8.3.1; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F852CCBA1EA4D88200ADB206 /* Build configuration list for PBXProject "Multiparty-UICollectionView" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F852CCB61EA4D88200ADB206; productRefGroup = F852CCC01EA4D88200ADB206 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F852CCBE1EA4D88200ADB206 /* Multiparty-UICollectionView */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F852CCBD1EA4D88200ADB206 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A053760C1EB1638C00645696 /* Main.storyboard in Resources */, A053760A1EB1638C00645696 /* Assets.xcassets in Resources */, A053760B1EB1638C00645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F852CCBB1EA4D88200ADB206 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A053760F1EB1638C00645696 /* MultipartyLayout.swift in Sources */, A053760D1EB1638C00645696 /* ChatViewController.swift in Sources */, A05376091EB1638C00645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05376021EB1638C00645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05376031EB1638C00645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05376041EB1638C00645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05376051EB1638C00645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F852CCCF1EA4D88200ADB206 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F852CCD01EA4D88200ADB206 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 10.3; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F852CCD21EA4D88200ADB206 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Multiparty-UICollectionView/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Multiparty-UICollectionView"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.2; }; name = Debug; }; F852CCD31EA4D88200ADB206 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = "$(SRCROOT)/Multiparty-UICollectionView/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Multiparty-UICollectionView"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.2; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F852CCBA1EA4D88200ADB206 /* Build configuration list for PBXProject "Multiparty-UICollectionView" */ = { isa = XCConfigurationList; buildConfigurations = ( F852CCCF1EA4D88200ADB206 /* Debug */, F852CCD01EA4D88200ADB206 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F852CCD11EA4D88200ADB206 /* Build configuration list for PBXNativeTarget "Multiparty-UICollectionView" */ = { isa = XCConfigurationList; buildConfigurations = ( F852CCD21EA4D88200ADB206 /* Debug */, F852CCD31EA4D88200ADB206 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F852CCB71EA4D88200ADB206 /* Project object */; } ================================================ FILE: Multiparty-UICollectionView/Multiparty-UICollectionView.xcodeproj/xcshareddata/xcschemes/Multiparty-UICollectionView.xcscheme ================================================ ================================================ FILE: Multiparty-UICollectionView/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Multiparty-UICollectionView' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Multiparty-UICollectionView/README.md ================================================ Multiparty UICollectionView Sample App ======================================== If you plan to build a multiparty app, you may want to use `UICollectionView` to dynamically adapt to the screen size and display the video views of all participants in an OpenTok session. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. Use `UICollectionView` to easily specify the way views are displayed. ## Creating a custom layout for UICollectionView When building custom layouts, you need to subclass `UICollectionViewLayout` and override two methods (`prepare()` and `layoutAttributesForElements(in:)`) and a computed property (`collectionViewContentSize`). First, you need to return the size of the entire `UICollectionView`. In our case, since we want to fill the entire screen without scrolling, we simply return the size of the `UICollectionView` container by overriding `collectionViewContentSize()`: ```swift override var collectionViewContentSize: CGSize { return collectionView?.superview?.bounds.size ?? CGSize() } ``` When the view is going to be laid out, `UIKit` calls the implementation of the `MultipartyLayout.prepare()` method. This method prepares values used when the views are drawn. It populates a cache, which is a `UICollectionViewLayoutAttributes` object that specifies the size and position of each item: ```swift override func prepare() { guard let views = collectionView?.numberOfItems(inSection: 0) else { cache.removeAll() return } if views != cachedNumberOfViews { cache.removeAll() } if cache.isEmpty { cachedNumberOfViews = views let attribs: [UICollectionViewLayoutAttributes] = { switch views { case 1: return attributesForPublisherFullScreen() case 2: return attributesForPublisherAndOneSubscriber() case let x where x > 2 && x.isEven: return attributesForAllViewsTwoByTwo(withNumberOfViews: x) case let x where x > 2 && !x.isEven: return attributesForPublisherOnTopAndSubscribersTwoByTwo(withNumberOfViews: x) default: return [] } }() cache.append(contentsOf: attribs) } } ``` The implementation of the `UICollectionViewLayout.layoutAttributesForElements(in:)` method is called when the views are laid out. It returns the cache containing the actual view sizes: ```swift override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return cache } ``` ================================================ FILE: OpenTokSDKVersion.rb ================================================ OpenTokSDKVersion = '2.32.1' MinIosSdkVersion = '15.0' ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/AppDelegate.swift ================================================ import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/ExampleVideoRender.swift ================================================ import OpenTok import GLKit import Foundation import Accelerate import UIKit class Accelerater{ var infoYpCbCrToARGB = vImage_YpCbCrToARGB() init() { _ = configureYpCbCrToARGBInfo() } func configureYpCbCrToARGBInfo() -> vImage_Error { print("Configuring") var pixelRange = vImage_YpCbCrPixelRange(Yp_bias: 0, CbCr_bias: 128, YpRangeMax: 255, CbCrRangeMax: 255, YpMax: 255, YpMin: 1, CbCrMax: 255, CbCrMin: 0) let error = vImageConvert_YpCbCrToARGB_GenerateConversion( kvImage_YpCbCrToARGBMatrix_ITU_R_601_4!, &pixelRange, &infoYpCbCrToARGB, kvImage420Yp8_Cb8_Cr8, kvImageARGB8888, vImage_Flags(kvImagePrintDiagnosticsToConsole)) print("Configration done \(error)") return error } func convertFrameVImageYUV(_ frame: OTVideoFrame, to pixelBufferRef: CVPixelBuffer?) -> vImage_Error{ if pixelBufferRef == nil { print("No PixelBuffer refrance found") return vImage_Error(kvImageInvalidParameter) } let width = frame.format?.imageWidth ?? 0 let height = frame.format?.imageHeight ?? 0 let subsampledWidth = frame.format!.imageWidth/2 let subsampledHeight = frame.format!.imageHeight/2 let planeSize = calculatePlaneSize(forFrame: frame) let yPlane = UnsafeMutablePointer.allocate(capacity: planeSize.ySize) let uPlane = UnsafeMutablePointer.allocate(capacity: planeSize.uSize) let vPlane = UnsafeMutablePointer.allocate(capacity: planeSize.vSize) memcpy(yPlane, frame.planes?.pointer(at: 0), planeSize.ySize) memcpy(uPlane, frame.planes?.pointer(at: 1), planeSize.uSize) memcpy(vPlane, frame.planes?.pointer(at: 2), planeSize.vSize) let yStride = frame.format!.bytesPerRow.object(at: 0) as! Int // multiply chroma strides by 2 as bytesPerRow represents 2x2 subsample let uStride = frame.format!.bytesPerRow.object(at: 1) as! Int let vStride = frame.format!.bytesPerRow.object(at: 2) as! Int var yPlaneBuffer = vImage_Buffer(data: yPlane, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: yStride) var uPlaneBuffer = vImage_Buffer(data: uPlane, height: vImagePixelCount(subsampledHeight), width: vImagePixelCount(subsampledWidth), rowBytes: uStride) var vPlaneBuffer = vImage_Buffer(data: vPlane, height: vImagePixelCount(subsampledHeight), width: vImagePixelCount(subsampledWidth), rowBytes: vStride) CVPixelBufferLockBaseAddress(pixelBufferRef!, .readOnly) let pixelBufferData = CVPixelBufferGetBaseAddress(pixelBufferRef!) let rowBytes = CVPixelBufferGetBytesPerRow(pixelBufferRef!) var destinationImageBuffer = vImage_Buffer() destinationImageBuffer.data = pixelBufferData destinationImageBuffer.height = vImagePixelCount(height) destinationImageBuffer.width = vImagePixelCount(width) destinationImageBuffer.rowBytes = rowBytes var permuteMap: [UInt8] = [3, 2, 1, 0] // BGRA let convertError = vImageConvert_420Yp8_Cb8_Cr8ToARGB8888(&yPlaneBuffer, &uPlaneBuffer, &vPlaneBuffer, &destinationImageBuffer, &infoYpCbCrToARGB, &permuteMap, 255, vImage_Flags(kvImagePrintDiagnosticsToConsole)) CVPixelBufferUnlockBaseAddress(pixelBufferRef!, []) yPlane.deallocate() uPlane.deallocate() vPlane.deallocate() return convertError } fileprivate func calculatePlaneSize(forFrame frame: OTVideoFrame) -> (ySize: Int, uSize: Int, vSize: Int) { guard let frameFormat = frame.format else { return (0, 0 ,0) } let baseSize = Int(frameFormat.imageWidth * frameFormat.imageHeight) * MemoryLayout.size return (baseSize, baseSize / 4, baseSize / 4) } } protocol ExampleVideoRenderDelegate { func renderer(_ renderer: ExampleVideoRender, didReceiveFrame videoFrame: OTVideoFrame) } class ExampleVideoRender: UIView { var delegate: ExampleVideoRenderDelegate? var frameLock = NSLock() var bufferDisplayLayer = AVSampleBufferDisplayLayer() var pipBufferDisplayLayer: AVSampleBufferDisplayLayer? let accel = Accelerater() override init(frame: CGRect) { super.init(frame: frame) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension ExampleVideoRender: OTVideoRender { func renderVideoFrame(_ frame: OTVideoFrame) { if let format = frame.format { frameLock.lock() assert(format.pixelFormat == .I420) if let sampleBuffer = createSampleBufferWithVideoFrame(frame, width: Int(frame.format!.imageWidth), height: Int(frame.format!.imageHeight)) { bufferDisplayLayer.enqueue(sampleBuffer) pipBufferDisplayLayer?.enqueue(sampleBuffer) } frameLock.unlock() } } func createSampleBufferWithVideoFrame(_ frame: OTVideoFrame, width: Int, height: Int) -> CMSampleBuffer? { let pixelAttributes: NSDictionary = [kCVPixelBufferIOSurfacePropertiesKey as String: [:]] var pixelBuffer: CVPixelBuffer? let result = CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, pixelAttributes as CFDictionary, &pixelBuffer) guard result == 0 else { return nil } _ = accel.convertFrameVImageYUV(frame, to: pixelBuffer) let s = createSampleBufferFrom(pixelBuffer: pixelBuffer!) return s } func createSampleBufferFrom(pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? { CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) var sampleBuffer: CMSampleBuffer? let now = CMTimeMakeWithSeconds(CACurrentMediaTime(), preferredTimescale: 1000) var timingInfo = CMSampleTimingInfo(duration: CMTimeMakeWithSeconds(1, preferredTimescale: 1000), presentationTimeStamp: now, decodeTimeStamp: now) var formatDescription: CMFormatDescription? = nil CMVideoFormatDescriptionCreateForImageBuffer(allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescriptionOut: &formatDescription) let osStatus = CMSampleBufferCreateReadyWithImageBuffer( allocator: kCFAllocatorDefault, imageBuffer: pixelBuffer, formatDescription: formatDescription!, sampleTiming: &timingInfo, sampleBufferOut: &sampleBuffer ) if osStatus != noErr { let errorMessage = osStatusToString(status: osStatus) print("osStatus error: \(errorMessage)") } guard let buffer = sampleBuffer else { print("Cannot create sample buffer") return nil } CVPixelBufferUnlockBaseAddress(pixelBuffer, []) return buffer } func osStatusToString(status: OSStatus) -> String { switch status { case kCMSampleBufferError_DataCanceled: return "kCMSampleBufferError_DataCanceled" case kCMSampleBufferError_DataFailed: return "kCMSampleBufferError_DataFailed" case kCMSampleBufferError_Invalidated: return "kCMSampleBufferError_Invalidated" case kCMSampleBufferError_InvalidMediaFormat: return "kCMSampleBufferError_InvalidMediaFormat" case kCMSampleBufferError_InvalidSampleData: return "kCMSampleBufferError_InvalidSampleData" case kCMSampleBufferError_InvalidMediaTypeForOperation: return "kCMSampleBufferError_InvalidMediaTypeForOperation" case kCMSampleBufferError_SampleTimingInfoInvalid: return "kCMSampleBufferError_SampleTimingInfoInvalid" case kCMSampleBufferError_CannotSubdivide: return "kCMSampleBufferError_CannotSubdivide" case kCMSampleBufferError_InvalidEntryCount: return "kCMSampleBufferError_InvalidEntryCount" case kCMSampleBufferError_ArrayTooSmall: return "kCMSampleBufferError_ArrayTooSmall" case kCMSampleBufferError_BufferHasNoSampleTimingInfo: return "kCMSampleBufferError_BufferHasNoSampleTimingInfo" case kCMSampleBufferError_BufferHasNoSampleSizes: return "kCMSampleBufferError_BufferHasNoSampleSizes" case kCMSampleBufferError_SampleIndexOutOfRange: return "kCMSampleBufferError_SampleIndexOutOfRange" case kCMSampleBufferError_BufferNotReady: return "kCMSampleBufferError_BufferNotReady" case kCMSampleBufferError_AlreadyHasDataBuffer: return "kCMSampleBufferError_AlreadyHasDataBuffer" case kCMSampleBufferError_RequiredParameterMissing: return "kCMSampleBufferError_RequiredParameterMissing" case kCMSampleBufferError_AllocationFailed: return "kCMSampleBufferError_AllocationFailed" default: return "Unknown error with code \(status)" } } } ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 LSRequiresIPhoneOS NSCameraUsageDescription NSMicrophoneUsageDescription UIBackgroundModes audio fetch processing UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/SampleBufferVideoCallView.swift ================================================ import UIKit import AVKit class SampleBufferVideoCallView: UIView { override class var layerClass: AnyClass { AVSampleBufferDisplayLayer.self } var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer { layer as! AVSampleBufferDisplayLayer } } ================================================ FILE: Picture-In-Picture/Lets-Build-OTPublisher/ViewController.swift ================================================ import UIKit import OpenTok import AVKit let kWidgetRatio: CGFloat = 1.333 // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() var publisher: OTPublisher? var subscriber: OTSubscriber? let sampleBufferVideoCallView = SampleBufferVideoCallView() var pipController: AVPictureInPictureController! = nil var pipObservation: NSKeyValueObservation? var frame: CGRect! @IBOutlet weak var videoContainerView: UIView! override func viewDidLoad() { super.viewDidLoad() frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.width / kWidgetRatio) doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ fileprivate func doConnect() { var error: OTError? defer { processError(error) } session.connect(withToken: kToken, error: &error) } /** * Instantiates a subscriber for the given stream and asynchronously begins the * process to begin receiving A/V content for this stream. Unlike doPublish, * this method does not add the subscriber to the view hierarchy. Instead, we * add the subscriber only after it has connected and begins receiving data. */ fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { processError(error) } subscriber = OTSubscriber(stream: stream, delegate: self) let videoRender = ExampleVideoRender() subscriber?.videoRender = videoRender session.subscribe(subscriber!, error: &error) // to allow subscriber sending videoframe even when the app is in background NotificationCenter.default.removeObserver(subscriber, name: UIApplication.willResignActiveNotification, object: nil) //SubscriberView let bufferDisplayLayer = videoRender.bufferDisplayLayer bufferDisplayLayer.frame = frame videoContainerView.layer.addSublayer(bufferDisplayLayer) pipSetup(videoRender: videoRender) } fileprivate func cleanupSubscriber() { subscriber?.view?.removeFromSuperview() subscriber = nil } fileprivate func processError(_ error: OTError?) { if let err = error { showAlert(errorStr: err.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } fileprivate func pipSetup(videoRender: ExampleVideoRender) { videoRender.pipBufferDisplayLayer = sampleBufferVideoCallView.sampleBufferDisplayLayer videoRender.pipBufferDisplayLayer?.frame = frame let pipVideoCallViewController = AVPictureInPictureVideoCallViewController() pipVideoCallViewController.preferredContentSize = CGSize(width: 640, height: 480) pipVideoCallViewController.view.addSubview(sampleBufferVideoCallView) sampleBufferVideoCallView.translatesAutoresizingMaskIntoConstraints = false let constraints = [ sampleBufferVideoCallView.leadingAnchor.constraint(equalTo: pipVideoCallViewController.view.leadingAnchor), sampleBufferVideoCallView.trailingAnchor.constraint(equalTo: pipVideoCallViewController.view.trailingAnchor), sampleBufferVideoCallView.topAnchor.constraint(equalTo: pipVideoCallViewController.view.topAnchor), sampleBufferVideoCallView.bottomAnchor.constraint(equalTo: pipVideoCallViewController.view.bottomAnchor) ] NSLayoutConstraint.activate(constraints) sampleBufferVideoCallView.bounds = pipVideoCallViewController.view.frame let contentSource = AVPictureInPictureController.ContentSource( activeVideoCallSourceView: videoContainerView, contentViewController: pipVideoCallViewController) pipController = AVPictureInPictureController(contentSource: contentSource) pipController.canStartPictureInPictureAutomaticallyFromInline = true pipController.delegate = self } @IBAction func startPiPTapped(_ sender: Any) { pipController?.startPictureInPicture() } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscriber == nil { doSubscribe(stream) } } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { print("Publishing") } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { if let subStream = subscriber?.stream, subStream.streamId == stream.streamId { cleanupSubscriber() } } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } // MARK: - AVPictureInPictureControllerDelegate extension ViewController:AVPictureInPictureControllerDelegate { func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { print("\(#function)") print("pip error: \(error)") } func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { print("\(#function)") } func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { print("\(#function)") } } ================================================ FILE: Picture-In-Picture/Picture-In-Picture.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ 6510F38796E0E44CC144B1FC /* Pods_Picture_In_Picture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0685123659B7FADE5415B782 /* Pods_Picture_In_Picture.framework */; }; EA65E79C2BEE08920060F604 /* SampleBufferVideoCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA65E79B2BEE08920060F604 /* SampleBufferVideoCallView.swift */; }; F84DC3AF1D5C8BF400402BD9 /* ExampleVideoRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84DC3AE1D5C8BF400402BD9 /* ExampleVideoRender.swift */; }; F86C64BC1D5C8A150081846D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86C64BB1D5C8A150081846D /* AppDelegate.swift */; }; F86C64BE1D5C8A150081846D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86C64BD1D5C8A150081846D /* ViewController.swift */; }; F86C64C11D5C8A150081846D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F86C64BF1D5C8A150081846D /* Main.storyboard */; }; F86C64C31D5C8A150081846D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F86C64C21D5C8A150081846D /* Assets.xcassets */; }; F86C64C61D5C8A150081846D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 0685123659B7FADE5415B782 /* Pods_Picture_In_Picture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Picture_In_Picture.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1C7B8F7E8A4C1EBDCE25C96B /* Pods-Custom-Video-Driver.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Custom-Video-Driver.release.xcconfig"; path = "Target Support Files/Pods-Custom-Video-Driver/Pods-Custom-Video-Driver.release.xcconfig"; sourceTree = ""; }; 81DA78E557B6CC9FACB46457 /* Pods-Custom-Video-Driver.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Custom-Video-Driver.debug.xcconfig"; path = "Target Support Files/Pods-Custom-Video-Driver/Pods-Custom-Video-Driver.debug.xcconfig"; sourceTree = ""; }; 859ECA77BD688D6216193F82 /* Pods-Picture-In-Picture.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Picture-In-Picture.debug.xcconfig"; path = "Target Support Files/Pods-Picture-In-Picture/Pods-Picture-In-Picture.debug.xcconfig"; sourceTree = ""; }; B049AB366F84CA3525E1BF9C /* Pods-Picture-In-Picture.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Picture-In-Picture.release.xcconfig"; path = "Target Support Files/Pods-Picture-In-Picture/Pods-Picture-In-Picture.release.xcconfig"; sourceTree = ""; }; EA017C342BF45C040010889B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; EA65E79B2BEE08920060F604 /* SampleBufferVideoCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleBufferVideoCallView.swift; sourceTree = ""; }; F84DC3AE1D5C8BF400402BD9 /* ExampleVideoRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleVideoRender.swift; sourceTree = ""; }; F86C64B81D5C8A150081846D /* Picture-In-Picture.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Picture-In-Picture.app"; sourceTree = BUILT_PRODUCTS_DIR; }; F86C64BB1D5C8A150081846D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F86C64BD1D5C8A150081846D /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F86C64C01D5C8A150081846D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; F86C64C21D5C8A150081846D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F86C64C51D5C8A150081846D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F86C64C71D5C8A150081846D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F86C64B51D5C8A150081846D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 6510F38796E0E44CC144B1FC /* Pods_Picture_In_Picture.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 026262AB90854995BA336336 /* Frameworks */ = { isa = PBXGroup; children = ( EA017C342BF45C040010889B /* Security.framework */, 0685123659B7FADE5415B782 /* Pods_Picture_In_Picture.framework */, ); name = Frameworks; sourceTree = ""; }; 5BC0EA00F0A0A1C324E1C486 /* Pods */ = { isa = PBXGroup; children = ( 81DA78E557B6CC9FACB46457 /* Pods-Custom-Video-Driver.debug.xcconfig */, 1C7B8F7E8A4C1EBDCE25C96B /* Pods-Custom-Video-Driver.release.xcconfig */, 859ECA77BD688D6216193F82 /* Pods-Picture-In-Picture.debug.xcconfig */, B049AB366F84CA3525E1BF9C /* Pods-Picture-In-Picture.release.xcconfig */, ); path = Pods; sourceTree = ""; }; F86C64AF1D5C8A150081846D = { isa = PBXGroup; children = ( F86C64BA1D5C8A150081846D /* Lets-Build-OTPublisher */, F86C64B91D5C8A150081846D /* Products */, 5BC0EA00F0A0A1C324E1C486 /* Pods */, 026262AB90854995BA336336 /* Frameworks */, ); sourceTree = ""; }; F86C64B91D5C8A150081846D /* Products */ = { isa = PBXGroup; children = ( F86C64B81D5C8A150081846D /* Picture-In-Picture.app */, ); name = Products; sourceTree = ""; }; F86C64BA1D5C8A150081846D /* Lets-Build-OTPublisher */ = { isa = PBXGroup; children = ( F86C64BB1D5C8A150081846D /* AppDelegate.swift */, F86C64BD1D5C8A150081846D /* ViewController.swift */, F86C64BF1D5C8A150081846D /* Main.storyboard */, F86C64C21D5C8A150081846D /* Assets.xcassets */, F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */, F86C64C71D5C8A150081846D /* Info.plist */, F84DC3AE1D5C8BF400402BD9 /* ExampleVideoRender.swift */, EA65E79B2BEE08920060F604 /* SampleBufferVideoCallView.swift */, ); path = "Lets-Build-OTPublisher"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F86C64B71D5C8A150081846D /* Picture-In-Picture */ = { isa = PBXNativeTarget; buildConfigurationList = F86C64CA1D5C8A150081846D /* Build configuration list for PBXNativeTarget "Picture-In-Picture" */; buildPhases = ( E666A80B93844C80118AD2EB /* [CP] Check Pods Manifest.lock */, F86C64B41D5C8A150081846D /* Sources */, F86C64B51D5C8A150081846D /* Frameworks */, F86C64B61D5C8A150081846D /* Resources */, 5269D88C572E1907C803A5F4 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = "Picture-In-Picture"; productName = "Lets-Build-OTPublisher"; productReference = F86C64B81D5C8A150081846D /* Picture-In-Picture.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F86C64B01D5C8A150081846D /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F86C64B71D5C8A150081846D = { CreatedOnToolsVersion = 7.3.1; DevelopmentTeam = PR6C39UQ38; LastSwiftMigration = 1200; ProvisioningStyle = Automatic; }; }; }; buildConfigurationList = F86C64B31D5C8A150081846D /* Build configuration list for PBXProject "Picture-In-Picture" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F86C64AF1D5C8A150081846D; productRefGroup = F86C64B91D5C8A150081846D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F86C64B71D5C8A150081846D /* Picture-In-Picture */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F86C64B61D5C8A150081846D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F86C64C61D5C8A150081846D /* LaunchScreen.storyboard in Resources */, F86C64C31D5C8A150081846D /* Assets.xcassets in Resources */, F86C64C11D5C8A150081846D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 5269D88C572E1907C803A5F4 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Picture-In-Picture/Pods-Picture-In-Picture-resources.sh", "${PODS_ROOT}/OTXCFramework/OpenTok.xcframework/ios-arm64/OpenTok.framework/selfie_segmentation.tflite", ); name = "[CP] Copy Pods Resources"; outputPaths = ( "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/selfie_segmentation.tflite", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Picture-In-Picture/Pods-Picture-In-Picture-resources.sh\"\n"; showEnvVarsInLog = 0; }; E666A80B93844C80118AD2EB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Picture-In-Picture-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F86C64B41D5C8A150081846D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( EA65E79C2BEE08920060F604 /* SampleBufferVideoCallView.swift in Sources */, F86C64BE1D5C8A150081846D /* ViewController.swift in Sources */, F86C64BC1D5C8A150081846D /* AppDelegate.swift in Sources */, F84DC3AF1D5C8BF400402BD9 /* ExampleVideoRender.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ F86C64BF1D5C8A150081846D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( F86C64C01D5C8A150081846D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; F86C64C41D5C8A150081846D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F86C64C51D5C8A150081846D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F86C64C81D5C8A150081846D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F86C64C91D5C8A150081846D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F86C64CB1D5C8A150081846D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 859ECA77BD688D6216193F82 /* Pods-Picture-In-Picture.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PR6C39UQ38; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", "GLES_SILENCE_DEPRECATION=1", ); INFOPLIST_FILE = "Lets-Build-OTPublisher/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.picture-in-picture"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F86C64CC1D5C8A150081846D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = B049AB366F84CA3525E1BF9C /* Pods-Picture-In-Picture.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = PR6C39UQ38; INFOPLIST_FILE = "Lets-Build-OTPublisher/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.picture-in-picture"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F86C64B31D5C8A150081846D /* Build configuration list for PBXProject "Picture-In-Picture" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64C81D5C8A150081846D /* Debug */, F86C64C91D5C8A150081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F86C64CA1D5C8A150081846D /* Build configuration list for PBXNativeTarget "Picture-In-Picture" */ = { isa = XCConfigurationList; buildConfigurations = ( F86C64CB1D5C8A150081846D /* Debug */, F86C64CC1D5C8A150081846D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F86C64B01D5C8A150081846D /* Project object */; } ================================================ FILE: Picture-In-Picture/Picture-In-Picture.xcodeproj/xcshareddata/xcschemes/Picture-In-Picture.xcscheme ================================================ ================================================ FILE: Picture-In-Picture/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Picture-In-Picture' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Picture-In-Picture/README.md ================================================ Picture In Picture Sample App ================================== This project uses the custom video render features in the OpenTok iOS SDK. By the end of a code review, you should have a basic understanding how to implement Picture-In-Picture for the subscribed stream. Note that this sample application is not supported in the XCode iOS Simulator because the Picture-In-Picture only works on real device. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. ### ExampleVideoRender OTSubscriber needs an instance supporting the `OTVideoRender` protocol to display video contents. In short, the instance ID that is set to the `videoRender` property will receive YUV frames (I420) as they are received (subscriber). In this example we get the YUV frames from `videoRender`, and convert the frames to CMSampleBuffer. Refer to function `createSampleBufferWithVideoFrame` for YUV frames to CMSampeBuffer conversion. Then, draw the video stream on the PIP by adding the CMSampleBuffer into PIP `sampleBufferDisplayLayer` ### ViewController Setup a PIP controller is well documented in [apple doc][1]. To see sample in action, you need to add a publisher (which will display as a subscriber), either run the app a second time in an iOS device or use the OpenTok Playground to connect to the session in a supported web browser (Chrome, Firefox, or Internet Explorer 10-11). [1]: https://developer.apple.com/documentation/avkit/adopting-picture-in-picture-for-video-calls ================================================ FILE: README.md ================================================ :warning: **This repository has been deprecated in favour of the [Vonage iOS samples](https://github.com/Vonage/vonage-video-ios-sdk-samples)** :warning: [![Build Status](https://travis-ci.org/opentok/opentok-ios-sdk-samples-swift.svg?branch=main)](https://travis-ci.org/opentok/opentok-ios-sdk-samples-swift) OpenTok iOS SDK Samples ======================= This repository is meant to provide some examples for you to better understand the features of the OpenTok iOS SDK. The sample applications are meant to be used with the latest version of the [OpenTok iOS SDK](https://tokbox.com/developer/sdks/ios/). Feel free to copy and modify the source code herein for your own projects. Please consider sharing your modifications with us, especially if they might benefit other developers using the OpenTok iOS SDK. See the [License](LICENSE) for more information. Quick Start ----------- 1. Get values for your OpenTok **API key**, **session ID**, and **token**. See [Obtaining OpenTok Credentials](#obtaining-opentok-credentials) for important information. 1. Install CocoaPods as described in [CocoaPods Getting Started](https://guides.cocoapods.org/using/getting-started.html#getting-started). 1. In Terminal, `cd` to your project directory and type `pod install`. 1. Reopen your project in Xcode using the new `.xcworkspace` file. 1. In the ViewController.swift file, replace the following empty strings with the corresponding API key, session ID, and token values: ```swift // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" ``` 1. Use Xcode to build and run the app on an iOS simulator or device. What's Inside ------------- **Basic Video Chat** -- This basic application demonstrates a short path to getting started with the OpenTok iOS SDK. **Custom Audio Driver** -- This project demonstrate how to use an external audio source with the OpenTok SDK. This project utilizes CoreAudio and the AUGraph API to create an audio session suitable for voice and video communications. **Custom Video Driver** -- This project provides classes that implement the OTVideoCapture and OTVideoRender interfaces of the core Publisher and Subscriber classes. Using these modules, we can see the basic workflow of sourcing video frames from the device camera in and out of OpenTok, via the OTPublisherKit and OTSubscriberKit interfaces. **Live Photo Capture** -- This project extends the video capture module implemented in project 2, and demonstrates how the AVFoundation media capture APIs can be used to simultaneously stream video and capture high-resolution photos from the same camera. **Screen Sharing** -- This project demonstrates how to use a custom video capturer to publish a stream that uses a UI view (instead of a camera) as the video source. **Simple Multiparty** -- This project demonstrates how to use the OpenTok iOS SDK for a multi-party call. The application publishes audio/video from an iOS device and can connect to multiple subscribers. However it shows only one subscriber video at a time due to CPU limitations on iOS devices. **Picture In Picture** -- This project demonstrates how to implement Picture In Picture on a subcribed video stream. **FrameMetadata** -- This project shows how to set metadata (limited to 32 bytes) to a video frame, as well as how to read metadata from a video frame. ## Obtaining OpenTok Credentials To use the OpenTok platform you need a session ID, token, and API key. You can get these values by creating a project on your [OpenTok Account Page](https://tokbox.com/account/) and scrolling down to the Project Tools section of your Project page. For production deployment, you must generate the session ID and token values using one of the [OpenTok Server SDKs](https://tokbox.com/developer/sdks/server/). ## Development and Contributing Interested in contributing? We :heart: pull requests! See the [Contribution](CONTRIBUTING.md) guidelines. ## Getting Help We love to hear from you so if you have questions, comments or find a bug in the project, let us know! You can either: - Open an issue on this repository - See for support options - Tweet at us! We're [@VonageDev](https://twitter.com/VonageDev) on Twitter - Or [join the Vonage Developer Community Slack](https://developer.nexmo.com/community/slack) ## Further Reading - Check out the Developer Documentation at ================================================ FILE: Screen-Sharing/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Screen-Sharing' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Screen-Sharing/README.md ================================================ Screen Sharing Sample App ========================= This project shows how to use OpenTok iOS SDK to publish a stream that uses a UIView, instead of a camera, as the video source. See the "Custom Video Driver" sample code for basic information on using a custom video capturer. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. The main storyboard includes a UITextView object that is referenced in the ViewController.swift file as the `timeDisplay` property. The `viewDidLoad` method (in ViewController.swift) sets up a timer that updates this text field periodically to display the Date timestamp. This example will use this text field's view as the video source for the published stream. videoFrame = OTVideoFrame(format:format) Upon connecting to the OpenTok session, the app instantiates an OTPublisherKit object, and calls its `setCapturer()` method to set a custom video capturer. This custom video capturer is defined by the ScreenCapture class: func doPublish() { defer { process(error: error) } let settings = OTPublisherSettings() settings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: settings) publisher?.videoType = .screen publisher?.audioFallbackEnabled = false capturer = ScreenCapturer(withView: view) publisher?.videoCapture = capturer!.videoCapture() var error: OTError? = nil session.publish(publisher!, error: &error) } Note that the call to the `OTPublisher.videoType` method sets the video type of the published stream to `OTPublisherKitVideoTypeScreen`. This optimizes the video encoding for screen sharing. It is recommended to use a low frame rate (5 frames per second or lower) with this video type. When using the screen video type in a session that uses the [OpenTok Media Server](https://tokbox.com/opentok/tutorials/create-session/#media-mode), the audio-only fallback feature is disabled, so that the video does not drop out in subscribers. (However, the publisher in this sample does not publish audio.) The code instantiates a ScreenCapture object and passes it into the `publisher.videoCapture` method. This sets the custom video capturer for the publisher The ScreenCapture class implements the OTVideoCapture protocol, defined in the OpenTok iOS SDK. The implementation of the `OTVideoCapture.initCapture()` method sets up a timer that periodically gets a UIImage based on a screenshot of the main view (`self.view`): let screen = self.screenShoot() let padded = self.resizeAndPad(image: screen) self.consume(frame: padded) The `screenshot()` method simply returns a UIImage representation of `self.view`. The `viewDidLoad` method initialized a OTVideoFormat and OTVideoFrame object to be used by the custom video capturer: let format = OTVideoFormat() format.pixelFormat = .argb The `consumeFrame()` method sets up properties of the current video frame: let timeStamp = mach_absolute_time() let time = CMTime(seconds: Double(timeStamp), preferredTimescale: 1000) let ref = pixelBuffer(fromCGImage: frame) CVPixelBufferLockBaseAddress(ref, CVPixelBufferLockFlags(rawValue: 0)) videoFrame?.timestamp = time videoFrame?.format.estimatedCaptureDelay = 100 videoFrame?.orientation = .up videoFrame?.clearPlanes() videoFrame?.planes.addPointer(CVPixelBufferGetBaseAddress(ref)) videoCaptureConsumer.consumeFrame(videoFrame) The `consumeFrame()` method then calls the `self.videoCaptureConsumer.consumeFrame(videoFrame)` method: videoCaptureConsumer.consumeFrame(videoFrame) The `videoCaptureConsumer` property of the OTVideoCapturer object is defined by the OTVideoCaptureConsumer protocol. Its `consumeFrame()` method sets a video frame to be published by the OTPublisherKit object. ================================================ FILE: Screen-Sharing/Screen-Sharing/AppDelegate.swift ================================================ // // AppDelegate.swift // 5.Screen-Sharing // // Created by Roberto Perez Cubero on 23/09/2016. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Screen-Sharing/Screen-Sharing/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Screen-Sharing/Screen-Sharing/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Screen-Sharing/Screen-Sharing/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Screen-Sharing/Screen-Sharing/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Screen-Sharing/Screen-Sharing/ScreenCapturer.swift ================================================ // // ScreenCapturer.swift // 5.Screen-Sharing // // Created by Roberto Perez Cubero on 23/09/2016. // Copyright © 2016 tokbox. All rights reserved. // import Foundation import OpenTok class ScreenCapturer: NSObject, OTVideoCapture { var videoContentHint: OTVideoContentHint var videoCaptureConsumer: OTVideoCaptureConsumer? let MAX_EDGE_SIZE_LIMIT: CGFloat = 1280.0 let EDGE_DIMENSION_COMMON_FACTOR: CGFloat = 16.0 fileprivate let captureView: UIView fileprivate let captureQueue = DispatchQueue(label: "ot-screen-capture") fileprivate var timer: DispatchSourceTimer fileprivate var capturing: Bool = false fileprivate var videoFrame = OTVideoFrame(format: OTVideoFormat(argbWithWidth: 0, height: 0)) fileprivate var pixelBuffer: CVPixelBuffer? private enum TimerState { case notStarted case started case suspended case resumed case canceled } private var timerState: TimerState = .notStarted init(withView: UIView) { self.videoContentHint = .none captureView = withView timer = DispatchSource.makeTimerSource(flags: .strict, queue: captureQueue) } fileprivate func screenShoot() -> UIImage { UIGraphicsBeginImageContextWithOptions(captureView.bounds.size, false, 0.0) captureView.drawHierarchy(in: captureView.bounds, afterScreenUpdates: false) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image! } fileprivate func resizeAndPad(image img: UIImage) -> CGImage { let source = img.cgImage! let size = CGSize(width: source.width, height: source.height) let destSizes = dimensions(forInputSize: size) UIGraphicsBeginImageContextWithOptions(destSizes.container, false, 1.0) let ctx = UIGraphicsGetCurrentContext() ctx?.scaleBy(x: 1, y: -1) ctx?.translateBy(x: 0, y: -destSizes.rect.size.height) ctx?.draw(source, in: destSizes.rect) let newImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return (newImage?.cgImage)! } fileprivate func consume(frame: CGImage) { checkSize(forImage: frame) if !capturing { return } let timeStamp = mach_absolute_time() let time = CMTime(seconds: Double(timeStamp), preferredTimescale: 1000) let ref = pixelBuffer(fromCGImage: frame) CVPixelBufferLockBaseAddress(ref, CVPixelBufferLockFlags(rawValue: 0)) videoFrame.timestamp = time //videoFrame?.format.estimatedFramesPerSecond = videoFrame.format?.estimatedCaptureDelay = 100 videoFrame.orientation = .up videoFrame.clearPlanes() videoFrame.planes?.addPointer(CVPixelBufferGetBaseAddress(ref)) videoCaptureConsumer?.consumeFrame(videoFrame) CVPixelBufferUnlockBaseAddress(ref, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) } // MARK: - OTVideoCapture protocol func initCapture() { timer.setEventHandler { DispatchQueue.main.async { let screen = self.screenShoot() let padded = self.resizeAndPad(image: screen) self.consume(frame: padded) } } timer.schedule(deadline: DispatchTime.now(), repeating: DispatchTimeInterval.milliseconds(100)) timerState = .started } func start() -> Int32 { capturing = true captureQueue.sync { timerResume() } return 0 } func stop() -> Int32 { capturing = false captureQueue.sync { timerSuspend() } return 0 } func releaseCapture() { timerCancel() } func isCaptureStarted() -> Bool { return capturing } func captureSettings(_ videoFormat: OTVideoFormat) -> Int32 { videoFormat.pixelFormat = .ARGB return 0 } func timerResume() { if timerState == .resumed { return } timerState = .resumed timer.resume() } func timerSuspend() { if timerState == .suspended { return } timerState = .suspended timer.suspend() } func timerCancel() { timer.cancel() timerState = .canceled timer.resume() } } // MARK: - Image Utils extension ScreenCapturer { fileprivate func pixelBuffer(fromCGImage img: CGImage) -> CVPixelBuffer { let frameSize = CGSize(width: img.width, height: img.height) CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) let pxdata = CVPixelBufferGetBaseAddress(pixelBuffer!) let rgbColorSpace = CGColorSpaceCreateDeviceRGB() let context = CGContext(data: pxdata, width: Int(frameSize.width), height: Int(frameSize.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) context?.draw(img, in: CGRect(x: 0, y: 0, width: img.width, height: img.height)) CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0))) return pixelBuffer!; } fileprivate func dimensions(forInputSize size: CGSize) -> (container: CGSize, rect: CGRect) { let aspect = size.width / size.height var destContainer = CGSize(width: size.width, height: size.height) var destFrame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height)) // if image is wider than tall and width breaks edge size limit if MAX_EDGE_SIZE_LIMIT < size.width && aspect >= 1.0 { destContainer.width = MAX_EDGE_SIZE_LIMIT destContainer.height = destContainer.width / aspect if 0 != fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR) { destContainer.height += (EDGE_DIMENSION_COMMON_FACTOR - fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR)) } destFrame.size.width = destContainer.width destFrame.size.height = destContainer.width / aspect } // ensure the dimensions of the resulting container are safe if (fmod(destContainer.width, EDGE_DIMENSION_COMMON_FACTOR) != 0) { let remainder = fmod(destContainer.width, EDGE_DIMENSION_COMMON_FACTOR); // increase the edge size only if doing so does not break the edge limit if (destContainer.width + (EDGE_DIMENSION_COMMON_FACTOR - remainder) > MAX_EDGE_SIZE_LIMIT) { destContainer.width -= remainder; } else { destContainer.width += EDGE_DIMENSION_COMMON_FACTOR - remainder; } } // ensure the dimensions of the resulting container are safe if (fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR) != 0) { let remainder = fmod(destContainer.height, EDGE_DIMENSION_COMMON_FACTOR); // increase the edge size only if doing so does not break the edge limit if (destContainer.height + (EDGE_DIMENSION_COMMON_FACTOR - remainder) > MAX_EDGE_SIZE_LIMIT) { destContainer.height -= remainder; } else { destContainer.height += EDGE_DIMENSION_COMMON_FACTOR - remainder; } } destFrame.size.width = destContainer.width; destFrame.size.height = destContainer.height; // scale and recenter source image to fit in destination container if (aspect > 1.0) { destFrame.origin.x = 0; destFrame.origin.y = (destContainer.height - destContainer.width) / 2; destFrame.size.width = destContainer.width; destFrame.size.height = destContainer.width / aspect; } else { destFrame.origin.x = (destContainer.width - destContainer.width) / 2; destFrame.origin.y = 0; destFrame.size.height = destContainer.height; destFrame.size.width = destContainer.height * aspect; } return (destContainer, destFrame) } fileprivate func checkSize(forImage img: CGImage) { guard let frameFormat = videoFrame.format, frameFormat.imageHeight != UInt32(img.height), frameFormat.imageWidth != UInt32(img.width) else { return } frameFormat.bytesPerRow.removeAllObjects() frameFormat.bytesPerRow.addObjects(from: [img.width * 4]) frameFormat.imageWidth = UInt32(img.width) frameFormat.imageHeight = UInt32(img.height) let frameSize = CGSize(width: img.width, height: img.height) let options: Dictionary = [ kCVPixelBufferCGImageCompatibilityKey as String: false, kCVPixelBufferCGBitmapContextCompatibilityKey as String: false ] let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(frameSize.width), Int(frameSize.height), kCVPixelFormatType_32ARGB, options as CFDictionary, &pixelBuffer) assert(status == kCVReturnSuccess && pixelBuffer != nil) } } ================================================ FILE: Screen-Sharing/Screen-Sharing/ViewController.swift ================================================ // // ViewController.swift // Screen-Sharing // // Created by Roberto Perez Cubero on 11/08/16. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok let kWidgetHeight = 240 let kWidgetWidth = 320 // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() var publisher: OTPublisher? var subscriber: OTSubscriber? var capturer: ScreenCapturer? @IBOutlet var timeText: UILabel! fileprivate let formatter: DateFormatter = { let fmt = DateFormatter() fmt.dateStyle = .short fmt.timeStyle = .long return fmt }() fileprivate func updateTimeLabel() { let text = formatter.string(from: Date()) timeText.text = text } override func viewDidLoad() { super.viewDidLoad() Timer.scheduledTimer(withTimeInterval: TimeInterval(1), repeats: true) { _ in self.updateTimeLabel() } doConnect() } /** * Asynchronously begins the session connect process. Some time later, we will * expect a delegate method to call us back with the results of this action. */ private func doConnect() { var error: OTError? defer { process(error: error) } session.connect(withToken: kToken, error: &error) } /** * Sets up an instance of OTPublisher to use with this session. OTPubilsher * binds to the device camera and microphone, and will provide A/V streams * to the OpenTok session. */ fileprivate func doPublish() { var error: OTError? = nil defer { process(error: error) } let settings = OTPublisherSettings() settings.name = UIDevice.current.name publisher = OTPublisher(delegate: self, settings: settings) publisher?.videoType = .screen publisher?.audioFallbackEnabled = false capturer = ScreenCapturer(withView: view) publisher?.videoCapture = capturer publisher?.videoCapture?.videoContentHint = .text session.publish(publisher!, error: &error) } fileprivate func doSubscribe(_ stream: OTStream) { var error: OTError? defer { process(error: error) } subscriber = OTSubscriber(stream: stream, delegate: self) session.subscribe(subscriber!, error: &error) } fileprivate func process(error err: OTError?) { if let e = err { showAlert(errorStr: e.localizedDescription) } } fileprivate func showAlert(errorStr err: String) { DispatchQueue.main.async { let controller = UIAlertController(title: "Error", message: err, preferredStyle: .alert) controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) self.present(controller, animated: true, completion: nil) } } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") doSubscribe(stream) } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { print("Subscriber connected") } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } ================================================ FILE: Screen-Sharing/Screen-Sharing.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A05375E81EB1636800645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375DE1EB1636800645696 /* AppDelegate.swift */; }; A05375E91EB1636800645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05375DF1EB1636800645696 /* Assets.xcassets */; }; A05375EA1EB1636800645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375E01EB1636800645696 /* LaunchScreen.storyboard */; }; A05375EB1EB1636800645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375E21EB1636800645696 /* Main.storyboard */; }; A05375ED1EB1636800645696 /* logo_opentok_registered.png in Resources */ = {isa = PBXBuildFile; fileRef = A05375E51EB1636800645696 /* logo_opentok_registered.png */; }; A05375EE1EB1636800645696 /* ScreenCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375E61EB1636800645696 /* ScreenCapturer.swift */; }; A05375EF1EB1636800645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375E71EB1636800645696 /* ViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A05375DE1EB1636800645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05375DF1EB1636800645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05375E11EB1636800645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05375E31EB1636800645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05375E41EB1636800645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05375E51EB1636800645696 /* logo_opentok_registered.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo_opentok_registered.png; sourceTree = ""; }; A05375E61EB1636800645696 /* ScreenCapturer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenCapturer.swift; sourceTree = ""; }; A05375E71EB1636800645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F8DE15B31D951F9200EFFA79 /* Screen-Sharing.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Screen-Sharing.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F8DE15B01D951F9200EFFA79 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A05375DD1EB1636800645696 /* Screen-Sharing */ = { isa = PBXGroup; children = ( A05375DE1EB1636800645696 /* AppDelegate.swift */, A05375DF1EB1636800645696 /* Assets.xcassets */, A05375E01EB1636800645696 /* LaunchScreen.storyboard */, A05375E21EB1636800645696 /* Main.storyboard */, A05375E41EB1636800645696 /* Info.plist */, A05375E51EB1636800645696 /* logo_opentok_registered.png */, A05375E61EB1636800645696 /* ScreenCapturer.swift */, A05375E71EB1636800645696 /* ViewController.swift */, ); path = "Screen-Sharing"; sourceTree = ""; }; F8DE15AA1D951F9200EFFA79 = { isa = PBXGroup; children = ( A05375DD1EB1636800645696 /* Screen-Sharing */, F8DE15B41D951F9200EFFA79 /* Products */, ); sourceTree = ""; }; F8DE15B41D951F9200EFFA79 /* Products */ = { isa = PBXGroup; children = ( F8DE15B31D951F9200EFFA79 /* Screen-Sharing.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F8DE15B21D951F9200EFFA79 /* Screen-Sharing */ = { isa = PBXNativeTarget; buildConfigurationList = F8DE15C51D951F9200EFFA79 /* Build configuration list for PBXNativeTarget "Screen-Sharing" */; buildPhases = ( F8DE15AF1D951F9200EFFA79 /* Sources */, F8DE15B01D951F9200EFFA79 /* Frameworks */, F8DE15B11D951F9200EFFA79 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Screen-Sharing"; productName = "4.Screen-Sharing"; productReference = F8DE15B31D951F9200EFFA79 /* Screen-Sharing.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F8DE15AB1D951F9200EFFA79 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F8DE15B21D951F9200EFFA79 = { CreatedOnToolsVersion = 8.0; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F8DE15AE1D951F9200EFFA79 /* Build configuration list for PBXProject "Screen-Sharing" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F8DE15AA1D951F9200EFFA79; productRefGroup = F8DE15B41D951F9200EFFA79 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F8DE15B21D951F9200EFFA79 /* Screen-Sharing */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F8DE15B11D951F9200EFFA79 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375ED1EB1636800645696 /* logo_opentok_registered.png in Resources */, A05375EB1EB1636800645696 /* Main.storyboard in Resources */, A05375E91EB1636800645696 /* Assets.xcassets in Resources */, A05375EA1EB1636800645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F8DE15AF1D951F9200EFFA79 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375EF1EB1636800645696 /* ViewController.swift in Sources */, A05375E81EB1636800645696 /* AppDelegate.swift in Sources */, A05375EE1EB1636800645696 /* ScreenCapturer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05375E01EB1636800645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05375E11EB1636800645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05375E21EB1636800645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05375E31EB1636800645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F8DE15C31D951F9200EFFA79 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F8DE15C41D951F9200EFFA79 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F8DE15C61D951F9200EFFA79 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Screen-Sharing/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Screen-Sharing"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "a59dc7c3-af3f-40f4-9a52-be0289761eb4"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; F8DE15C71D951F9200EFFA79 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Screen-Sharing/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Screen-Sharing"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F8DE15AE1D951F9200EFFA79 /* Build configuration list for PBXProject "Screen-Sharing" */ = { isa = XCConfigurationList; buildConfigurations = ( F8DE15C31D951F9200EFFA79 /* Debug */, F8DE15C41D951F9200EFFA79 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F8DE15C51D951F9200EFFA79 /* Build configuration list for PBXNativeTarget "Screen-Sharing" */ = { isa = XCConfigurationList; buildConfigurations = ( F8DE15C61D951F9200EFFA79 /* Debug */, F8DE15C71D951F9200EFFA79 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F8DE15AB1D951F9200EFFA79 /* Project object */; } ================================================ FILE: Screen-Sharing/Screen-Sharing.xcodeproj/xcshareddata/xcschemes/Screen-Sharing.xcscheme ================================================ ================================================ FILE: Signals/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Signals' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Signals/README.md ================================================ Signaling Sample App =============================== The Signaling app is a very simple application meant to get a new developer started using the signaling features of OpenTok iOS SDK. Quick Start ----------- To use this application: 1. Follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. Among other things, you need to set values for the `kApiKey`, `kSessionId`, and `kToken` constants. See [Obtaining OpenTok Credentials](../README.md#obtaining-opentok-credentials) in the main README file for the repository. 2. When you run the application, an OpenTok session is created . Signaling only needs OTConnection(s). 3. Run the app on a second client. You can do this by deploying the app to an iOS device and testing it in the simulator at the same time. Application Notes ----------------- * Signals are meant to transmit basic text data between participants in a session. * Signals don't have extensive chat like features (like emoji's etc). * Sending an signal using an session object as follows: ```swift session.signal(withType: type , string: data, connection:c.getOTConnection(), error: nil) ``` or ```swift session.signal(withType: type , string: data, connection:c.getOTConnection(), retryAfterReconnect: retryAfterConnect, error: nil) ``` `retryAfterReconnect` default value is `true` in the first call. The error case fails silently. * Receiving a signal is done using OTSessionDelegate callback as follows: ```swift func session(_ session: OTSession, receivedSignalType type: String?, from connection: OTConnection?, with string: String?) { .. } ``` You just need to implement the above calls in your app. * Valid Characters in a signal data is limited to `[^a-zA-Z0-9-_~\\s]`. If a non valid character is used , signal is not send. To get around this you can encode signal data with `base64` and decode it on other side. This way you can send emoji's for example. A sample code which extends `String` is provided below for reference: ```swift extension String { func fromBase64() -> String? { guard let data = Data(base64Encoded: self) else { return nil } return String(data: data, encoding: .ascii) } func toBase64() -> String { return Data(self.utf8).base64EncodedString() } func isValidSignal() -> Bool { return self.count <= 128 && self.range(of: "[^a-zA-Z0-9-_~\\s]", options: .regularExpression) == nil } ... } ``` Screen shot (SwiftUI based) ----------------- ## Starting screen: 1) Tap on "Hello !!" sends a "Hello World" message to all connections. 2) Tap on the Pen image leads to Form View (as shown below) image ## Starting screen with Scrollable Messages: image ## Signal Form view: image image ================================================ FILE: Signals/Signals/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Signals/Signals/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Signals/Signals/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Signals/Signals/ContentView.swift ================================================ // // ContentView.swift // Signals // // Created by Jaideep Shah on 2/9/23. // import SwiftUI struct ContentView { @State var oneClick = true @StateObject private var sdk = VonageVideoSDK() @State private var signalType = "Greetings" @State private var signalData = "Hello World" @State private var isRetryOnReconnect = true } extension ContentView: View { var body: some View { ZStack { VStack(alignment: .center, spacing: 25) { if (sdk.isSessionConnected == false) { Text("Connecting ...") .font(.title) } else { if oneClick == true { Button("Hello !!") { sdk.sendSignalToAll(type: "Greetings", data: "Hello World") } .font(.title) //OneClickView(oneClick: $oneClick) Button { oneClick = false // print("Edit button was tapped") } label: { Image(systemName: "square.and.pencil") } ScrollView { MessagesView() } } else { VStack { FormView(signalType: $signalType, signalData: $signalData, retryAfterConnect: $isRetryOnReconnect, oneClick: $oneClick) } } } } .padding(30) .environmentObject(sdk) .onDisappear { sdk.closeAll() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } ================================================ FILE: Signals/Signals/FormView.swift ================================================ // // FormView.swift // // Created by Jaideep Shah on 2/9/23. // import SwiftUI struct FormView { @EnvironmentObject private var sdk: VonageVideoSDK @Binding var signalType : String @Binding var signalData : String @Binding var retryAfterConnect : Bool @State private var isAllConnections = false @State private var selectedConns = Set() @Binding var oneClick : Bool @State private var signalCharError = false } extension FormView: View { var body: some View { VStack(spacing:40) { VStack { Toggle("Signal all", isOn: $isAllConnections) if (isAllConnections == false) { HStack { Text("Choose connections:") Spacer() } List(sdk.connsInfo, id: \.displayName, selection: $selectedConns) { c in Text(c.displayName) } .multilineTextAlignment(.leading) .font(.system(size: 12)) .environment(\.editMode, .constant(EditMode.active)) .listStyle(PlainListStyle()) .lineLimit(2) .overlay( RoundedRectangle(cornerRadius: 10, style: .circular).stroke(Color(uiColor: .tertiaryLabel), lineWidth: 2) ) } } HStack { Text("Type:") Spacer() TextField(signalType, text: $signalType, axis: .vertical) .multilineTextAlignment(.center) .textFieldStyle(.roundedBorder) .border(.gray, width: 1) .keyboardType(.asciiCapable) .disableAutocorrection(true) .lineLimit(1) Spacer() } HStack { Text("Content:") Spacer() TextField("Hello world", text: $signalData, axis: .vertical) .multilineTextAlignment(.center) .textFieldStyle(.roundedBorder) .border(.gray, width: 1) .keyboardType(.asciiCapable) .disableAutocorrection(true) .lineLimit(1) Spacer() } Toggle("Retry after reconnect:", isOn: $retryAfterConnect) HStack { Spacer() Button(action: { if signalData.isValidSignal() == false || signalType.isValidSignal() == false { signalCharError = true } else { self.oneClick.toggle() for connId in selectedConns { sdk.sendSignalToConnection(connection: connId, type: signalType, data: signalData, retryAfterConnect: retryAfterConnect) } } }) { Text("Send") } Spacer() Button(role: .cancel, action: { self.oneClick.toggle() }) { Text("Cancel") } Spacer() } } .padding(1) .alert("Only \"a-zA-Z0-9-_~\" and Space characters allowed for content and type.", isPresented: $signalCharError) { Button("OK", role: .cancel) { } } } } struct FormView_Previews: PreviewProvider { static var previews: some View { FormView(signalType: Binding.constant("Greeting"), signalData: Binding.constant("Hello"), retryAfterConnect: Binding.constant(true), oneClick: Binding.constant(false)) .environmentObject(VonageVideoSDK()) } } ================================================ FILE: Signals/Signals/MessagesView.swift ================================================ // // MessagesView.swift // // // Created by Jaideep Shah on 2/9/23. // import SwiftUI struct MessagesView { @EnvironmentObject private var sdk: VonageVideoSDK } extension MessagesView: View { var body: some View { LazyVStack(alignment: .leading) { ForEach(sdk.messages) { m in Spacer() VStack(alignment: .leading) { HStack { if m.outgoing { Image(systemName: "arrow.up.forward.square.fill") .foregroundColor(.yellow) } else { Image(systemName: "arrow.down.left.square.fill") .foregroundColor(.green) } Text(m.displayConnId) .padding(.horizontal) .multilineTextAlignment(.leading) .font(.system(size: 12)) Spacer() Text(m.type) .multilineTextAlignment(.trailing) .font(.system(size: 12)) } Spacer() HStack { Image(systemName: "arrow.up.forward.square.fill") .foregroundColor(.green) .hidden() Text(m.content) .padding(.horizontal) .multilineTextAlignment(.leading) .font(.system(size: 14)) .allowsTightening(true) .lineLimit(3) } } .padding(5) .overlay( RoundedRectangle(cornerRadius: 10, style: .circular).stroke(Color(uiColor: .tertiaryLabel), lineWidth: 1) .shadow(radius: 5) ) } } .padding() } } struct MessagesView_Previews: PreviewProvider { static var previews: some View { MessagesView() } } ================================================ FILE: Signals/Signals/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Signals/Signals/SignalsApp.swift ================================================ // // SignalsApp.swift // Signals // // Created by Jaideep Shah on 2/9/23. // import SwiftUI @main struct SignalsApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: Signals/Signals/VonageVideoSDK.swift ================================================ // // VonageVideoSDK.swift // Signals // // Created by Jaideep Shah on 2/15/23. // import UIKit import OpenTok let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class VonageVideoSDK: NSObject { @Published var isSessionConnected = false @Published var connsInfo: [ConnectionInfo] = [] @Published var messages: [SignalMessage] = [] //unlimited and last in , first out lazy var session: OTSession = { //make sure you have entered the credentials above , else you get an exception here return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() override init() { super.init() var error: OTError? session.connect(withToken: kToken, error: &error) if let error = error { print("Session creation error \(error.description)") } } } // MARK: ObservableObject extension VonageVideoSDK: ObservableObject { } // MARK: - OTSession delegate callbacks extension VonageVideoSDK: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { isSessionConnected = true connsInfo.append(ConnectionInfo(otMyConnection: session.connection!, otParticipantConnection: nil)) } func sessionDidDisconnect(_ session: OTSession) { } func session(_ session: OTSession, connectionCreated connection: OTConnection) { connsInfo.append(ConnectionInfo(otMyConnection: session.connection!, otParticipantConnection: connection)) } func session(_ session: OTSession, connectionDestroyed connection: OTConnection) { guard connsInfo.contains(connsInfo) else { return } let info = ConnectionInfo(otMyConnection: session.connection!, otParticipantConnection: connection) connsInfo = connsInfo.filter { $0 != info } } func session(_ session: OTSession, streamCreated stream: OTStream) { } func session(_ session: OTSession, streamDestroyed stream: OTStream) { } func session(_ session: OTSession, didFailWithError error: OTError) { print("Session Failed to connect: \(error.localizedDescription)") } func session(_ session: OTSession, receivedSignalType type: String?, from connection: OTConnection?, with string: String?) { if let string = string, let type = type, let c = connection?.connectionId { addMessage(connection: c, type: type, data: string, outgoing: false) } } } // MARK: - UI interface extension VonageVideoSDK { private func addMessage(connection: String?, type: String, data: String, outgoing: Bool) { messages.insert(SignalMessage(connId: connection, type: type, content: data, outgoing: outgoing), at: 0) } func sendSignalToAll(type: String?, data: String?) { guard let type = type, let data = data , type.isValidSignal() == true && data.isValidSignal() == true else { return } session.signal(withType: type , string: data, connection:nil, error: nil) addMessage(connection: nil, type: type, data: data, outgoing: true) } func closeAll() { session.disconnect(nil) } func sendSignalToConnection(connection: String, type: String?, data: String?, retryAfterConnect: Bool) { guard let type = type, let data = data , type.isValidSignal() == true && data.isValidSignal() == true else { return } for c in connsInfo where c.displayName == connection { if retryAfterConnect == true { //retry is true by default session.signal(withType: type , string: data, connection:c.getOTConnection(), error: nil) } else { // You can use this call for all cases. We are just distinguishing here to show various way to call signal. session.signal(withType: type , string: data, connection:c.getOTConnection(), retryAfterReconnect: retryAfterConnect, error: nil) } addMessage(connection: c.displayName, type: type, data: data, outgoing: true) } } } struct SignalMessage: Identifiable { let id = UUID() var connId : String? var type: String var content: String var outgoing: Bool var displayConnId: String { get { return connId == nil ? "All" : connId!.lastTenCharacter() } } } struct ConnectionInfo : Equatable, Hashable { let id = UUID() var otMyConnection : OTConnection var otParticipantConnection : OTConnection? let displaySelf = "Self" static func ==(lhs: ConnectionInfo, rhs: ConnectionInfo) -> Bool { return lhs.otParticipantConnection?.connectionId == rhs.otParticipantConnection?.connectionId } func hash(into hasher: inout Hasher) { hasher.combine(displayName) } var displayName: String { get { guard let otConnectionParticipant = otParticipantConnection else { return displaySelf } return otConnectionParticipant.connectionId } } func getOTConnection() -> OTConnection { guard let otConnectionParticipant = otParticipantConnection else { return otMyConnection } return otConnectionParticipant } } extension String { // only letters permitted are (A-Z and a-z), numbers (0-9), "-", "_", " " and "~". // hence we encode and decode with base64 to accomadate other characters like emojis etc. // Both sides needs to be part of this. This sample app will not use base64 encoding/decoding // and rely on the isValidSignal method below. func fromBase64() -> String? { guard let data = Data(base64Encoded: self) else { return nil } return String(data: data, encoding: .ascii) } func toBase64() -> String { return Data(self.utf8).base64EncodedString() } // The maximum length of the type string is 128 characters, and it must // contain only letters (A-Z and a-z), numbers (0-9), "-", "_", " ", and "~". // you could have used base64 encoding decoding here. But just for illustration , // we assume the other side is already deployed and we can't use base64. func isValidSignal() -> Bool { return self.count <= 128 && self.range(of: "[^a-zA-Z0-9-_~\\s]", options: .regularExpression) == nil } func lastTenCharacter() -> String { return "..." + self.suffix(10) } } ================================================ FILE: Signals/Signals.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 55; objects = { /* Begin PBXBuildFile section */ 1F8B1BA82995A93600D8E8FB /* SignalsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8B1BA72995A93600D8E8FB /* SignalsApp.swift */; }; 1F8B1BAA2995A93600D8E8FB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8B1BA92995A93600D8E8FB /* ContentView.swift */; }; 1F8B1BAC2995A93800D8E8FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F8B1BAB2995A93800D8E8FB /* Assets.xcassets */; }; 1F8B1BAF2995A93800D8E8FB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F8B1BAE2995A93800D8E8FB /* Preview Assets.xcassets */; }; 1F8B1BB82995A96400D8E8FB /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8B1BB52995A96300D8E8FB /* MessagesView.swift */; }; 1F8B1BB92995A96400D8E8FB /* FormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8B1BB62995A96300D8E8FB /* FormView.swift */; }; 1F8B1D42299D8D0800D8E8FB /* VonageVideoSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8B1D41299D8D0800D8E8FB /* VonageVideoSDK.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 1F8B1BA42995A93600D8E8FB /* Signals.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Signals.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1F8B1BA72995A93600D8E8FB /* SignalsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalsApp.swift; sourceTree = ""; }; 1F8B1BA92995A93600D8E8FB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 1F8B1BAB2995A93800D8E8FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1F8B1BAE2995A93800D8E8FB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 1F8B1BB52995A96300D8E8FB /* MessagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; 1F8B1BB62995A96300D8E8FB /* FormView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormView.swift; sourceTree = ""; }; 1F8B1D41299D8D0800D8E8FB /* VonageVideoSDK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VonageVideoSDK.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 1F8B1BA12995A93600D8E8FB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 1F8B1B9B2995A93600D8E8FB = { isa = PBXGroup; children = ( 1F8B1BA62995A93600D8E8FB /* Signals */, 1F8B1BA52995A93600D8E8FB /* Products */, FC1D2E52DCF9C6CBE4241D90 /* Pods */, ); sourceTree = ""; }; 1F8B1BA52995A93600D8E8FB /* Products */ = { isa = PBXGroup; children = ( 1F8B1BA42995A93600D8E8FB /* Signals.app */, ); name = Products; sourceTree = ""; }; 1F8B1BA62995A93600D8E8FB /* Signals */ = { isa = PBXGroup; children = ( 1F8B1D41299D8D0800D8E8FB /* VonageVideoSDK.swift */, 1F8B1BA72995A93600D8E8FB /* SignalsApp.swift */, 1F8B1BA92995A93600D8E8FB /* ContentView.swift */, 1F8B1BB52995A96300D8E8FB /* MessagesView.swift */, 1F8B1BB62995A96300D8E8FB /* FormView.swift */, 1F8B1BAB2995A93800D8E8FB /* Assets.xcassets */, 1F8B1BAD2995A93800D8E8FB /* Preview Content */, ); path = Signals; sourceTree = ""; }; 1F8B1BAD2995A93800D8E8FB /* Preview Content */ = { isa = PBXGroup; children = ( 1F8B1BAE2995A93800D8E8FB /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; FC1D2E52DCF9C6CBE4241D90 /* Pods */ = { isa = PBXGroup; children = ( ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 1F8B1BA32995A93600D8E8FB /* Signals */ = { isa = PBXNativeTarget; buildConfigurationList = 1F8B1BB22995A93800D8E8FB /* Build configuration list for PBXNativeTarget "Signals" */; buildPhases = ( 1F8B1BA02995A93600D8E8FB /* Sources */, 1F8B1BA12995A93600D8E8FB /* Frameworks */, 1F8B1BA22995A93600D8E8FB /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Signals; productName = Signals; productReference = 1F8B1BA42995A93600D8E8FB /* Signals.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 1F8B1B9C2995A93600D8E8FB /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1410; TargetAttributes = { 1F8B1BA32995A93600D8E8FB = { CreatedOnToolsVersion = 14.1; }; }; }; buildConfigurationList = 1F8B1B9F2995A93600D8E8FB /* Build configuration list for PBXProject "Signals" */; compatibilityVersion = "Xcode 13.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 1F8B1B9B2995A93600D8E8FB; productRefGroup = 1F8B1BA52995A93600D8E8FB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 1F8B1BA32995A93600D8E8FB /* Signals */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 1F8B1BA22995A93600D8E8FB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 1F8B1BAF2995A93800D8E8FB /* Preview Assets.xcassets in Resources */, 1F8B1BAC2995A93800D8E8FB /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 1F8B1BA02995A93600D8E8FB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 1F8B1D42299D8D0800D8E8FB /* VonageVideoSDK.swift in Sources */, 1F8B1BAA2995A93600D8E8FB /* ContentView.swift in Sources */, 1F8B1BB82995A96400D8E8FB /* MessagesView.swift in Sources */, 1F8B1BB92995A96400D8E8FB /* FormView.swift in Sources */, 1F8B1BA82995A93600D8E8FB /* SignalsApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ 1F8B1BB02995A93800D8E8FB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 1F8B1BB12995A93800D8E8FB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 16.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; 1F8B1BB32995A93800D8E8FB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Signals/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.c.x.Signals; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 1F8B1BB42995A93800D8E8FB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Signals/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.c.x.Signals; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1F8B1B9F2995A93600D8E8FB /* Build configuration list for PBXProject "Signals" */ = { isa = XCConfigurationList; buildConfigurations = ( 1F8B1BB02995A93800D8E8FB /* Debug */, 1F8B1BB12995A93800D8E8FB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1F8B1BB22995A93800D8E8FB /* Build configuration list for PBXNativeTarget "Signals" */ = { isa = XCConfigurationList; buildConfigurations = ( 1F8B1BB32995A93800D8E8FB /* Debug */, 1F8B1BB42995A93800D8E8FB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 1F8B1B9C2995A93600D8E8FB /* Project object */; } ================================================ FILE: Simple-Multiparty/Podfile ================================================ require_relative '../OpenTokSDKVersion' platform :ios, MinIosSdkVersion use_frameworks! target 'Simple-Multiparty' do pod 'OTXCFramework', OpenTokSDKVersion end ================================================ FILE: Simple-Multiparty/README.md ================================================ Simple Multiparty Sample App ============================== Previous samples subscribe to only one stream. In a multiparty video audio call there should be multiple parties. *Important:* To use this application, follow the instructions in the [Quick Start](../README.md#quick-start) section of the main README file for this repository. This simple multiparty app is able to handle only four subscriber parties. On a new stream received the ViewController class creates a new Subscriber object and subscribes the Session object to it. The Subscriber stream is rendered in the screen as we did it before. This sample uses a UICollectionView to show each subscriber view. We use a custom UICollectionViewCell that will hold the subscriber view and will also control some basic user interface to mute the audio of that subscriber. ```swift class SubscriberCollectionCell: UICollectionViewCell { @IBOutlet var muteButton: UIButton! var subscriber: OTSubscriber? @IBAction func muteSubscriberAction(_ sender: AnyObject) { subscriber?.subscribeToAudio = !(subscriber?.subscribeToAudio ?? true) let buttonImage: UIImage = { if !(subscriber?.subscribeToAudio ?? true) { return #imageLiteral(resourceName: "Subscriber-Speaker-Mute-35") } else { return #imageLiteral(resourceName: "Subscriber-Speaker-35") } }() muteButton.setImage(buttonImage, for: .normal) } override func layoutSubviews() { if let sub = subscriber { sub.view.frame = bounds contentView.insertSubview(sub.view, belowSubview: muteButton) muteButton.isEnabled = true muteButton.isHidden = false } } } ``` ## Adding user interface controls The ViewController class shows how you can add user interface controls for the following: * Turning a publisher's audio stream on and off * Swapping the publisher's camera When the user taps the mute button for the publisher, the following method of the ViewController class is invoked: ```swift @IBAction func muteMicAction(_ sender: AnyObject) { publisher.publishAudio = !publisher.publishAudio let buttonImage: UIImage = { if !publisher.publishAudio { return #imageLiteral(resourceName: "mic_muted-24") } else { return #imageLiteral(resourceName: "mic-24") } }() muteMicButton.setImage(buttonImage, for: .normal) } ``` ## Next steps For details on the full OpenTok Android API, see the [reference documentation](https://tokbox.com/developer/sdks/ios/reference/index.html). ================================================ FILE: Simple-Multiparty/Simple-Multiparty/AppDelegate.swift ================================================ // // AppDelegate.swift // 6.Multi-Party // // Created by Roberto Perez Cubero on 27/09/2016. // Copyright © 2016 tokbox. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "size" : "20x20", "scale" : "2x" }, { "idiom" : "iphone", "size" : "20x20", "scale" : "3x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "2x" }, { "idiom" : "iphone", "size" : "29x29", "scale" : "3x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "2x" }, { "idiom" : "iphone", "size" : "40x40", "scale" : "3x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "2x" }, { "idiom" : "iphone", "size" : "60x60", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/Subscriber-Speaker-35.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "Subscriber-Speaker-35.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "Subscriber-Speaker-35@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/Subscriber-Speaker-Mute-35.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "Subscriber-Speaker-Mute-35.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "Subscriber-Speaker-Mute-35@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/TB Bug-30.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "TB Bug-30.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "TB Bug-30@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/camera-switch_black-33.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "camera-switch_black-33.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "camera-switch_black-33@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/camera_switch-33.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "camera_switch-33.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "camera_switch-33@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/icon_arrowLeft_disabled-28.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon_arrowLeft_disabled-28.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icon_arrowLeft_disabled-28@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/icon_arrowLeft_enabled-28.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon_arrowLeft_enabled-28.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icon_arrowLeft_enabled-28@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/icon_arrowRight_disabled-28.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon_arrowRight_disabled-28.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icon_arrowRight_disabled-28@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/icon_arrowRight_enabled-28.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "icon_arrowRight_enabled-28.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "icon_arrowRight_enabled-28@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/mic-24.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "mic-24.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "mic-24@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/mic_muted-24.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "mic_muted-24.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "mic_muted-24@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Assets.xcassets/mic_receiving_data-35.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "mic_receiving_data-35.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "mic_receiving_data-35@2x.png", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Simple-Multiparty/Simple-Multiparty/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight NSCameraUsageDescription NSMicrophoneUsageDescription ================================================ FILE: Simple-Multiparty/Simple-Multiparty/ViewController.swift ================================================ // // ViewController.swift // 6.Multi-Party // // Created by Roberto Perez Cubero on 27/09/2016. // Copyright © 2016 tokbox. All rights reserved. // import UIKit import OpenTok // *** Fill the following variables using your own Project info *** // *** https://tokbox.com/account/#/ *** // Replace with your OpenTok API key let kApiKey = "" // Replace with your generated session ID let kSessionId = "" // Replace with your generated token let kToken = "" class ViewController: UIViewController { @IBOutlet var endCallButton: UIButton! @IBOutlet var swapCameraButton: UIButton! @IBOutlet var muteMicButton: UIButton! @IBOutlet var userName: UILabel! @IBOutlet var collectionView: UICollectionView! var subscribers: [IndexPath: OTSubscriber] = [:] lazy var session: OTSession = { return OTSession(apiKey: kApiKey, sessionId: kSessionId, delegate: self)! }() lazy var publisher: OTPublisher = { let settings = OTPublisherSettings() settings.name = UIDevice.current.name return OTPublisher(delegate: self, settings: settings)! }() var error: OTError? override func viewDidLoad() { super.viewDidLoad() session.connect(withToken: kToken, error: &error) userName.text = UIDevice.current.name } override func viewDidAppear(_ animated: Bool) { guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return } layout.itemSize = CGSize(width: collectionView.bounds.size.width / 2, height: collectionView.bounds.size.height / 2) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } @IBAction func swapCameraAction(_ sender: AnyObject) { if publisher.cameraPosition == .front { publisher.cameraPosition = .back } else { publisher.cameraPosition = .front } } @IBAction func tokboxButtonAction(_ sender: AnyObject) { UIApplication.shared.open(URL(string: "https://www.tokbox.com/developer/")!, options: [:], completionHandler: nil) } @IBAction func muteMicAction(_ sender: AnyObject) { publisher.publishAudio = !publisher.publishAudio let buttonImage: UIImage = { if !publisher.publishAudio { return #imageLiteral(resourceName: "mic_muted-24") } else { return #imageLiteral(resourceName: "mic-24") } }() muteMicButton.setImage(buttonImage, for: .normal) } @IBAction func endCallAction(_ sender: AnyObject) { session.disconnect(&error) } func reloadCollectionView() { collectionView.isHidden = subscribers.count == 0 collectionView.reloadData() } } extension ViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return subscribers.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "subscriberCell", for: indexPath) as! SubscriberCollectionCell cell.subscriber = subscribers[indexPath] return cell } } extension ViewController: UICollectionViewDelegate { } // MARK: - Subscriber Cell class SubscriberCollectionCell: UICollectionViewCell { @IBOutlet var muteButton: UIButton! var subscriber: OTSubscriber? @IBAction func muteSubscriberAction(_ sender: AnyObject) { subscriber?.subscribeToAudio = !(subscriber?.subscribeToAudio ?? true) let buttonImage: UIImage = { if !(subscriber?.subscribeToAudio ?? true) { return #imageLiteral(resourceName: "Subscriber-Speaker-Mute-35") } else { return #imageLiteral(resourceName: "Subscriber-Speaker-35") } }() muteButton.setImage(buttonImage, for: .normal) } override func layoutSubviews() { if let sub = subscriber, let subView = sub.view { subView.frame = bounds contentView.insertSubview(subView, belowSubview: muteButton) muteButton.isEnabled = true muteButton.isHidden = false } } } // MARK: - OpenTok Methods extension ViewController { func doPublish() { swapCameraButton.isEnabled = true muteMicButton.isEnabled = true endCallButton.isEnabled = true if let pubView = publisher.view { let publisherDimensions = CGSize(width: view.bounds.size.width / 4, height: view.bounds.size.height / 6) pubView.frame = CGRect(origin: CGPoint(x:collectionView.bounds.size.width - publisherDimensions.width, y:collectionView.bounds.size.height - publisherDimensions.height + collectionView.frame.origin.y), size: publisherDimensions) view.addSubview(pubView) } session.publish(publisher, error: &error) } func doSubscribe(to stream: OTStream) { if let subscriber = OTSubscriber(stream: stream, delegate: self) { let indexPath = IndexPath(item: subscribers.count, section: 0) subscribers[indexPath] = subscriber session.subscribe(subscriber, error: &error) reloadCollectionView() } } func findSubscriber(byStreamId id: String) -> (IndexPath, OTSubscriber)? { for (_, entry) in subscribers.enumerated() { if let stream = entry.value.stream, stream.streamId == id { return (entry.key, entry.value) } } return nil } func findSubscriberCell(byStreamId id: String) -> SubscriberCollectionCell? { for cell in collectionView.visibleCells { if let subscriberCell = cell as? SubscriberCollectionCell, let subscriberOfCell = subscriberCell.subscriber, (subscriberOfCell.stream?.streamId ?? "") == id { return subscriberCell } } return nil } } // MARK: - OTSession delegate callbacks extension ViewController: OTSessionDelegate { func sessionDidConnect(_ session: OTSession) { print("Session connected") doPublish() } func sessionDidDisconnect(_ session: OTSession) { print("Session disconnected") subscribers.removeAll() reloadCollectionView() } func session(_ session: OTSession, streamCreated stream: OTStream) { print("Session streamCreated: \(stream.streamId)") if subscribers.count == 4 { print("Sorry this sample only supports up to 4 subscribers :)") return } doSubscribe(to: stream) } func session(_ session: OTSession, streamDestroyed stream: OTStream) { print("Session streamDestroyed: \(stream.streamId)") guard let (index, subscriber) = findSubscriber(byStreamId: stream.streamId) else { return } subscriber.view?.removeFromSuperview() subscribers.removeValue(forKey: index) reloadCollectionView() } func session(_ session: OTSession, didFailWithError error: OTError) { print("session Failed to connect: \(error.localizedDescription)") } } // MARK: - OTPublisher delegate callbacks extension ViewController: OTPublisherDelegate { func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) { } func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) { print("Publisher failed: \(error.localizedDescription)") } } // MARK: - OTSubscriber delegate callbacks extension ViewController: OTSubscriberDelegate { func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) { print("Subscriber connected") reloadCollectionView() } func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) { print("Subscriber failed: \(error.localizedDescription)") } func subscriberVideoDataReceived(_ subscriber: OTSubscriber) { } } ================================================ FILE: Simple-Multiparty/Simple-Multiparty.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 46; objects = { /* Begin PBXBuildFile section */ A05375F91EB1637B00645696 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375F11EB1637B00645696 /* AppDelegate.swift */; }; A05375FA1EB1637B00645696 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A05375F21EB1637B00645696 /* Assets.xcassets */; }; A05375FB1EB1637B00645696 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375F31EB1637B00645696 /* LaunchScreen.storyboard */; }; A05375FC1EB1637B00645696 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A05375F51EB1637B00645696 /* Main.storyboard */; }; A05375FE1EB1637B00645696 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05375F81EB1637B00645696 /* ViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ A05375F11EB1637B00645696 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A05375F21EB1637B00645696 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A05375F41EB1637B00645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A05375F61EB1637B00645696 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A05375F71EB1637B00645696 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A05375F81EB1637B00645696 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; F829C8E51D9AB57700CDFBD5 /* Simple-Multiparty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Simple-Multiparty.app"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F829C8E21D9AB57700CDFBD5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ A05375F01EB1637B00645696 /* Simple-Multiparty */ = { isa = PBXGroup; children = ( A05375F11EB1637B00645696 /* AppDelegate.swift */, A05375F21EB1637B00645696 /* Assets.xcassets */, A05375F31EB1637B00645696 /* LaunchScreen.storyboard */, A05375F51EB1637B00645696 /* Main.storyboard */, A05375F71EB1637B00645696 /* Info.plist */, A05375F81EB1637B00645696 /* ViewController.swift */, ); path = "Simple-Multiparty"; sourceTree = ""; }; F829C8DC1D9AB57700CDFBD5 = { isa = PBXGroup; children = ( A05375F01EB1637B00645696 /* Simple-Multiparty */, F829C8E61D9AB57700CDFBD5 /* Products */, ); sourceTree = ""; }; F829C8E61D9AB57700CDFBD5 /* Products */ = { isa = PBXGroup; children = ( F829C8E51D9AB57700CDFBD5 /* Simple-Multiparty.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F829C8E41D9AB57700CDFBD5 /* Simple-Multiparty */ = { isa = PBXNativeTarget; buildConfigurationList = F829C8F71D9AB57700CDFBD5 /* Build configuration list for PBXNativeTarget "Simple-Multiparty" */; buildPhases = ( F829C8E11D9AB57700CDFBD5 /* Sources */, F829C8E21D9AB57700CDFBD5 /* Frameworks */, F829C8E31D9AB57700CDFBD5 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Simple-Multiparty"; productName = "6.Simple-Multiparty"; productReference = F829C8E51D9AB57700CDFBD5 /* Simple-Multiparty.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F829C8DD1D9AB57700CDFBD5 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0800; LastUpgradeCheck = 1200; ORGANIZATIONNAME = tokbox; TargetAttributes = { F829C8E41D9AB57700CDFBD5 = { CreatedOnToolsVersion = 8.0; DevelopmentTeam = ""; LastSwiftMigration = 1200; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = F829C8E01D9AB57700CDFBD5 /* Build configuration list for PBXProject "Simple-Multiparty" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F829C8DC1D9AB57700CDFBD5; productRefGroup = F829C8E61D9AB57700CDFBD5 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F829C8E41D9AB57700CDFBD5 /* Simple-Multiparty */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F829C8E31D9AB57700CDFBD5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375FC1EB1637B00645696 /* Main.storyboard in Resources */, A05375FA1EB1637B00645696 /* Assets.xcassets in Resources */, A05375FB1EB1637B00645696 /* LaunchScreen.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F829C8E11D9AB57700CDFBD5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( A05375FE1EB1637B00645696 /* ViewController.swift in Sources */, A05375F91EB1637B00645696 /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ A05375F31EB1637B00645696 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( A05375F41EB1637B00645696 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; A05375F51EB1637B00645696 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( A05375F61EB1637B00645696 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F829C8F51D9AB57700CDFBD5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F829C8F61D9AB57700CDFBD5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.4; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; F829C8F81D9AB57700CDFBD5 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Simple-Multiparty/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Multi-Party"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug; }; F829C8F91D9AB57700CDFBD5 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "$(SRCROOT)/Simple-Multiparty/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.tokbox.--Multi-Party"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F829C8E01D9AB57700CDFBD5 /* Build configuration list for PBXProject "Simple-Multiparty" */ = { isa = XCConfigurationList; buildConfigurations = ( F829C8F51D9AB57700CDFBD5 /* Debug */, F829C8F61D9AB57700CDFBD5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F829C8F71D9AB57700CDFBD5 /* Build configuration list for PBXNativeTarget "Simple-Multiparty" */ = { isa = XCConfigurationList; buildConfigurations = ( F829C8F81D9AB57700CDFBD5 /* Debug */, F829C8F91D9AB57700CDFBD5 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = F829C8DD1D9AB57700CDFBD5 /* Project object */; } ================================================ FILE: Simple-Multiparty/Simple-Multiparty.xcodeproj/xcshareddata/xcschemes/Simple-Multiparty.xcscheme ================================================ ================================================ FILE: travis_build.sh ================================================ #!/bin/sh set -e cd Basic-Video-Chat/ pod install xcodebuild -workspace Basic-Video-Chat.xcworkspace -scheme Basic-Video-Chat -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../Custom-Video-Driver/ pod install xcodebuild -workspace Custom-Video-Driver.xcworkspace -scheme Custom-Video-Driver -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../Custom-Audio-Driver/ pod install xcodebuild -workspace Custom-Audio-Driver.xcworkspace -scheme Custom-Audio-Driver -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../Screen-Sharing/ pod install xcodebuild -workspace Screen-Sharing.xcworkspace -scheme Screen-Sharing -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../Live-Photo-Capture/ pod install xcodebuild -workspace Live-Photo-Capture.xcworkspace -scheme Live-Photo-Capture -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../Simple-Multiparty/ pod install xcodebuild -workspace Simple-Multiparty.xcworkspace -scheme Simple-Multiparty -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../Multiparty-UICollectionView/ pod install xcodebuild -workspace Multiparty-UICollectionView.xcworkspace -scheme Multiparty-UICollectionView -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd ../CallKit/ pod install xcodebuild -workspace CallKitDemo.xcworkspace -scheme CallKitDemo -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO cd Video-Transformers/ pod install xcodebuild -workspace Video-Transformers.xcworkspace -scheme Video-Transformers -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -UseModernBuildSystem=NO