WebRTC Calling

See the Bit6CallDemo and Bit6CallDemo-Swift sample projects included with the sdk.

Active Calls

When there are ongoing active calls, your app will need to present some form of UI to control the calls (end, mute etc) and render video streams if applicable. The following steps happen:

  1. Bit6 SDK determines that a new active call has been initiated, either incoming or outgoing. The SDK sends a Bit6CallAddedNotification. SDK will send a Bit6CallPermissionsMissingNotification if there are not permissions to make a call (restricted microphone or camera).

  2. The application code needs to handle this notification to create or update the in-call UI.

  3. The in-call UI is notified about changes to the active calls and can control them as well.

Steps 2 and 3 can be implemented by either (a) using Bit6 UI Library components or (b) developing custom code. Sample code for both options is available in Bit6CallDemo and Bit6CallDemo-Swift samples and is also briefly described below.

- (void)callAddedNotification:(NSNotification*)notification {
    Bit6CallController *callController = notification.object;
    Bit6CallViewController *callViewController = [Bit6 createViewControllerForCall:callController];
    [callViewController show];
}

- (void)callPermissionsMissingNotification:(NSNotification*)notification
{
    Bit6CallController *callController = notification.object;
    NSError *error = callController.error;
}
func callAddedNotification(notification:NSNotification) {
    let callController = notification.object as! Bit6CallController
    let callViewController = Bit6.createViewControllerForCall(callController)!
    callViewController.show()
}

func callPermissionsMissingNotification(notification:NSNotification)
{
    let callController = notification.object as! Bit6CallController
    let error = callController.error
}

Active Calls UI

(a) Use Bit6 UI Component - BXUCallViewController

See BXUCallViewController

(b) Customize the InCall UI

Step 1. Create an UIViewController class that extends from Bit6CallViewController.

@interface MyCallViewController : Bit6CallViewController

@end

@implementation MyCallViewController

//this method will be called on [Bit6CallViewController createViewControllerForCall:]; to create a new object if necessary.
+ (nonnull Bit6CallViewController*)createForCall:(nonnull Bit6CallController*)callController
{
    return [[MyCallViewController alloc] initWithNibName:@"MyCallViewController" bundle:nil];
}

//called in the Main Thread when the UI controls should be updated. For example when the speaker is activated or the audio is muted.
- (void)refreshControlsView { }

//called in the Main Thread when the status of the call changes.
- (void)callController:(Bit6CallController *)callController callDidChangeToState:(Bit6CallState)state 
{
    [super callController:callController callDidChangeToState:state];
}

//called in the Main Thread each second to allow the refresh of a timer UILabel.
- (void)secondsDidChangeForCallController:(Bit6CallController *)callController
{
    [super secondsDidChangeForCallController:callController];

    NSLog(@"Seconds: %@",[Bit6Utils clockFormatForSeconds:self.callController.seconds]); break;
}

//called in the Main Thread to customize the frames for the video feeds. You can call [self setNeedsUpdateVideoViewLayout] at any time to force a refresh of the frames.
- (void)updateLayoutForVideoFeedViews:(NSArray<Bit6VideoFeedView*>*)videoFeedViews
{
    //Here you can do your own calculations to set the frames for the video feeds, or just call super. You can easily keep the aspect ratio by using AVMakeRectWithAspectRatioInsideRect() (import AVFoundation/AVFoundation.h) to do an scaleToFit or Bit6MakeRectWithAspectRatioToFillRect to do an scaleToFill.
    [super updateLayoutForVideoFeedViews:videoFeedViews];
}

@end
class MyCallViewController: Bit6CallViewController

//this method will be called on Bit6CallViewController.createViewControllerForCall() to create a new object if necessary.
override class func createForCall(callController:Bit6CallController) -> Bit6CallViewController {
    return MyCallViewController(nibName:"MyCallViewController", bundle:nil)
}

//called in the Main Thread when the UI controls should be updated. For example when the speaker is activated or the audio is muted.
override func refreshControlsView() { }

//called in the Main Thread when the status of the call changes.
override func callController(callController: Bit6CallController, callDidChangeToState state: Bit6CallState) 
{ 
    super.callController(callController, callDidChangeToState:state)
}

//called in the Main Thread each second to allow the refresh of a timer UILabel.
override func secondsDidChangeForCallController(callController: Bit6CallController)
{
    super.secondsDidChangeForCallController(callController)

    NSLog(@"Seconds: %@",Bit6Utils.clockFormatForSeconds(Double(self.callController.seconds)));
}

//called to customize the frames for the video feeds. You can call [self setNeedsUpdateVideoViewLayout] at any time to force a refresh of the frames.
override func updateLayoutForVideoFeedViews(videoFeedViews: [Bit6VideoFeedView])
{
    //Here you can do your own calculations to set the frames for the video feeds, or just call super. You can easily keep the aspect ratio by using AVMakeRectWithAspectRatioInsideRect() (import AVFoundation/AVFoundation.h) to do an scaleToFit or Bit6MakeRectWithAspectRatioToFillRect to do an scaleToFill.
    super.updateLayoutForVideoFeedViews(videoFeedViews)
}

@end

Step 2. Implement the actions and do the connections in your nib/storyboard file.

- (IBAction)muteAudioCall:(id)sender {
    [Bit6CallController setLocalAudioEnabled:!Bit6CallController.isLocalAudioEnabled];
}

- (IBAction)bluetooth:(id)sender {
    [Bit6CallController setBluetoothEnabled:!Bit6CallController.isBluetoothEnabled];
}

- (IBAction)speaker:(id)sender {
    [Bit6CallController setSpeakerEnabled:!Bit6CallController.isSpeakerEnabled];
}

- (IBAction)muteVideoCall:(id)sender {
    [Bit6CallController setLocalVideoEnabled:!Bit6CallController.isLocalVideoEnabled];
}

- (IBAction)switchCamera:(id)sender {
    [Bit6CallController setLocalVideoSource:[Bit6CallController localVideoSource]==Bit6VideoSource_CameraBack ? Bit6VideoSource_CameraFront: Bit6VideoSource_CameraBack];
}

- (IBAction)switchRecording:(id)sender {
    self.callController.recording = !self.callController.recording;
}

- (IBAction)hangup:(id)sender {
    [Bit6CallController hangupAll];
}
@IBAction func muteAudioCall(sender : UIButton) {
    Bit6CallController.setLocalAudioEnabled(!Bit6CallController.isLocalAudioEnabled())
}

@IBAction func bluetooth(sender : UIButton) {
    Bit6CallController.setBluetoothEnabled(!Bit6CallController.isBluetoothEnabled())
}

@IBAction func speaker(sender : UIButton) {
    Bit6CallController.setSpeakerEnabled(!Bit6CallController.isSpeakerEnabled())
}

@IBAction func muteVideoCall(sender : UIButton) {
    Bit6CallController.setLocalVideoEnabled(!Bit6CallController.isLocalVideoEnabled())
}

@IBAction func switchCamera(sender : UIButton) {
    Bit6CallController.setLocalVideoSource(Bit6CallController.localVideoSource() == .CameraBack ? .CameraFront : .CameraBack)
}

@IBAction func switchRecording(sender : UIButton) {
    self.callController!.recording = !self.callController!.recording
}

@IBAction func hangup(sender : UIButton) {
    Bit6CallController.hangupAll()
}

Step 3. Set the name of your Bit6CallViewController subclass at the beginning of your AppDelegate.

[Bit6 setInCallClass:[MyCallViewController class]];
Bit6.setInCallClass(MyCallViewController)

Voice & Video

Bit6Address *address = [Bit6Address addressWithUsername:@"john"];
[Bit6 startCallTo:address streams:Bit6CallStreams_Audio|Bit6CallStreams_Video mediaMode:Bit6CallMediaModeP2P offnet:NO];
let address = Bit6Address(username:"john")
Bit6.startCallTo(address, streams:[.Audio], mediaMode:Bit6CallMediaModeP2P, offnet:false)

After the call has been initiated with startCallTo() method, the SDK will post a Bit6CallAddedNotification to notify the application UI about a new ongoing call.

Phone/PSTN

Bit6 interconnects with the phone networks (PSTN) and allows making outgoing phone calls.

Phone numbers must be in E164 format, prefixed with +. So a US (country code 1) number (555) 123-1234 must be presented as +15551231234.

For the test purposes we allow short 1 minute calls to destinations in US and Canada. In later releases we will also add billing, which will in turn allow to connect calls of any length to any destination number.

NSString *phoneNumber = @"+14445556666";
[Bit6 startPhoneCallTo:phoneNumber];
let phoneNumber = "+14445556666";
Bit6.startPhoneCallTo(phoneNumber)

After the call has been initiated, the application will be notified about a new call in progress via the same NSNotification described in the previous section.

P2P Data Transfers

See the DataChannelDemo sample project included with the sdk.

The process works similar to a regular audio/video call.

Step1. Start the call.

Bit6Address *address = [Bit6Address addressWithUsername:@"john"];
[Bit6 startCallTo:address streams:Bit6CallStreams_Data mediaMode:Bit6CallMediaModeP2P offnet:NO];
let address = Bit6Address(username:"john")
Bit6.startCallTo(address, streams:[. Data], mediaMode:Bit6CallMediaModeP2P, offnet:false)

Step2. Start a transfer.

[self.callController addDelegate:self];

UIImage *image = ...
NSData *imageData = UIImagePNGRepresentation(image);
Bit6OutgoingTransfer *transfer = [[Bit6OutgoingTransfer alloc] initWithData:imageData name:@"my_image.png" mimeType:@"image/png"];
[self.callController addTransfer:transfer];
self.callController.addDelegate(self)

let image = ...
let data = UIImagePNGRepresentation(image)
let transfer = Bit6OutgoingTransfer(data:imageData, name:"my_image.png", mimeType:"image/png")
self.callController.addTransfer(transfer)

Step3. Listen for incoming transfers and updates in outgoing transfers.

You need to implement the Bit6CallControllerDelegate protocol.

- (void)callController:(nonnull Bit6CallController*)callController transfer:(nonnull Bit6Transfer*)transfer change:(nonnull NSString*)change
{
    Bit6TransferType transferType = transfer.type;

    if (callController == self.callController) {
        //the transfer started
        if ([change isEqualToString:Bit6TransferStartedKey]) {

        }
        //transfer updated
        else if ([change isEqualToString:Bit6TransferProgressKey]) {
            CGFloat progressAtTheMoment = [notification.userInfo[Bit6ProgressKey] floatValue];
        }
        //the transfer ended
        else if ([change isEqualToString:Bit6TransferEndedKey]) {
            //transfer received
            if (transferType == Bit6TransferType_INCOMING) {
                if ([transfer.mimeType hasPrefix:@"image/"]) {
                    UIImage *image = [UIImage imageWithData:transfer.data];
                }
            }
            else {
                //transfer sent
            }
        }
        //the transfer failed
        else if ([change isEqualToString:Bit6TransferEndedWithErrorKey]) {
            NSLog(@"%@",transfer.error.localizedDescription);
        }
    }
}
func callController(callController: Bit6CallController, transfer: Bit6Transfer, change: String) {
{
    let transferType = transfer.type

    if callController == self.callController {
        switch change {
            //the transfer started
            case Bit6TransferStartedKey : break

            //transfer updated
            case Bit6TransferProgressKey :
                let progressNow = (notification.userInfo![Bit6ProgressKey] as! NSNumber).floatValue

            //the transfer ended
            case Bit6TransferEndedKey :
                //transfer received
                if transferType == .INCOMING {
                    if transfer.mimeType.hasPrefix("image/") {
                        let image = UIImage(data:transfer.data)!
                    }
                }
                else {
                    //transfer sent
                }

            //the transfer failed
            case Bit6TransferEndedWithErrorKey :
                NSLog("\(transfer.error.localizedDescription)")

            default: break
        }
    }
}

Incoming Calls

Bit6 SDK has a seamless integration with Apple push notification systems for receiving incoming calls. The process of handling an incoming call consists of the following steps:

  1. Incoming call push notification arrives to the device and is handled by Bit6 SDK.

  2. SDK sends a Bit6IncomingCallNotification.

  3. The application code can handle this notification to present the incoming call information to the user allowing to answer or reject the call.

  4. If the call is answered, SDK will notify the app to create or update its InCall UI via Bit6CallAddedNotification. This is done in exactly the same way as for the outgoing calls.

Steps 2 and 3 can be implemented by either (a) using Bit6 UI Library components or (b) developing custom code. Sample code for both options is available in Bit6CallDemo and Bit6CallDemo-Swift samples.

Note SDK will send a Bit6CallMissedNotification for each missed call.

(a) Use Bit6 UI Component - BXUIncomingCallPrompt

See BXUIncomingCallPrompt

- (void)callMissedNotification:(NSNotification*)notification {
    if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
        Bit6CallController *callController = notification.object;
        NSString *msg= [NSString stringWithFormat:@"MissedCall from %@",callController.other.uri];
    }
}
func callMissedNotification(notification:NSNotification)
{
    if UIApplication.sharedApplication().applicationState == UIApplicationStateActive {
        let callController = notification.object as! Bit6CallController
        let message = "Missed Call from \(callController.other.uri)"
    }
}

If the call is answered, SDK will notify the app to create or update its InCall UI via Bit6CallAddedNotification. This is done in exactly the same way as for the outgoing calls.

(b) Custom Incoming Call UI

- (void)incomingCallNotification:(NSNotification*)notification
{
    Bit6CallController *callController = notification.object;

    //there's a call prompt showing at the time
    if (self.callController) {
        //if this is not the same call as the one being shown for the prompt then we reject it
        if ( ![self.callController isEqual:callController] ) {
            [callController hangup];
        }
    }
    else {
        callController.remoteStreams = callController.availableStreamsForIncomingCall;

        //the call was answered by taping the push notification
        if (callController.incomingCallAnswered) {
            callController.localStreams = callController.remoteStreams;
            [callController start];
        }
        else {
            self.callController = callController;

            UIAlertController *alertView = [UIAlertController alertControllerWithTitle:callController.incomingCallAlert message:nil preferredStyle:UIAlertControllerStyleAlert];

            if (callController.hasRemoteAudio) {
                [alertView addAction:[UIAlertAction actionWithTitle:@"Audio" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                    callController.localStreams = Bit6CallStreams_Audio;
                    [callController start];
                    self.callController = nil;
                }]];
            }
            if (callController.hasRemoteVideo) {
                [alertView addAction:[UIAlertAction actionWithTitle:@"Video" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                    callController.localStreams = Bit6CallStreams_Audio|Bit6CallStreams_Video;
                    [callController start];
                    self.callController = nil;
                }]];
            }
            [alertView addAction:[UIAlertAction actionWithTitle:@"Reject" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
                [self.callController hangup];
                self.callController = nil;
            }]];

            [self.window.rootViewController presentViewController:alertView animated:YES completion:nil];
        }
    }
}

- (void)callMissedNotification:(NSNotification*)notification
{
    if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
        Bit6CallController *callController = notification.object;
        if (self.callController == callController) {
            self.callController = nil;
            [self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
        }

        NSString *msg= [NSString stringWithFormat:@"MissedCall from %@",callController.other.uri];
    }
}

func incomingCallNotification(notification:NSNotification)
{
    let callController = notification.object as! Bit6CallController

    //there's a call prompt showing at the time
    if let callController = self.callController {
        //if this is not the same call as the one being shown for the prompt then we reject it
        if self.callController != callController {
            callController.hangup()
        }
    }
    else {
        callController.remoteStreams = callController.availableStreamsForIncomingCall

        //the call was answered by taping the push notification
        if callController.incomingCallAnswered {
            callController.localStreams = callController.remoteStreams
            callController.start()
        }
        else {
            self.callController = callController

            let alertView = UIAlertController(title:callController.incomingCallAlert, message:nil, preferredStyle:.Alert)

            if callController.hasRemoteAudio {
                alertView.addAction(UIAlertAction(title:"Audio", style:.Default) { (action) in
                    callController.localStreams = .Audio
                    callController.start()
                    self.callController = nil
                })
            }
            if callController.hasRemoteVideo {
                alertView.addAction(UIAlertAction(title:"Video", style:.Default) { (action) in
                    callController.localStreams = [.Audio,.Video]
                    callController.start()
                    self.callController = nil
                })
            }
            alertView.addAction(UIAlertAction(title:"Reject", style:.Cancel) { (action) in
                callController.hangup()
                self.callController = nil
            })

            self.window?.rootViewController?.presentViewController(alertView, animated:true, completion:nil)

        }
    }
}

func callMissedNotification(notification:NSNotification)
{
    if UIApplication.sharedApplication().applicationState == UIApplicationStateActive {
        let callController = notification.object as! Bit6CallController
        if self.callController == callController {
            self.callController = nil
            self.window?.rootViewController?.dismissViewControllerAnimated(true, completion:nil)
        }

        let message = "Missed Call from \(callController.other.uri)"
    }
}

If the call is answered, SDK will notify the app to create or update its InCall UI via Bit6CallAddedNotification. This is done in exactly the same way as for the outgoing calls.