";
};
/* 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:
[](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)
## Starting screen with Scrollable Messages:
## Signal Form view:
================================================
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
================================================