Skip to main content

Voip Consultation

VoipConfig Struct

struct VoipConfig {
id?: number;
consultation_id?: number;
apiKey?: string;
call_id?: string;
token?: string;
}

For VOIP consultation the config data will be in voipConfig and for VIDEO will be in videoConfig but it will be the same data structure so will be using one interface for both VoipConfig

VOIP and Video consultations are rendered and handled the same way, the difference is ro enable the video on Video consultations and disable it for VOIP consultations.

Frameworks

We use OpenTok in order to handle the VOIP/Video consultation,

So you need to import OpenTok beside AltibbiTelehealth

import UIKit
import AltibbiTelehealth
import OpenTok

Static Properties

In your view controller, you need to declare some properties that will be used during the flow by many methods

// The Consultation data object that you can pass from the previous screen (The scoket screen) or use getConsultationInfo
var consultationInfo: Consultation?

// The OpenTok session where the apiKey and callId are needed and these are included in the videoConfig/voipConfig of the consultation
lazy var session: OTSession = {
if (consultationInfo?.medium == "voip") {
return OTSession(apiKey: consultationInfo?.voipConfig?.apiKey ?? "", sessionId: consultationInfo?.voipConfig?.callId ?? "", delegate: self)!
}
return OTSession(apiKey: consultationInfo?.videoConfig?.apiKey ?? "", sessionId: consultationInfo?.videoConfig?.callId ?? "", delegate: self)!
}()

// The OpenTok Publisher, and from the settings `OTPublisherSettings` the settings.videoTrack specifies the type of session (VOIP/Video)
lazy var publisher: OTPublisher = {
let settings = OTPublisherSettings()
settings.name = UIDevice.current.name
if (consultationInfo?.medium == "voip") {
settings.videoTrack = false
}
return OTPublisher(delegate: self, settings: settings)!
}()

// The OpenTok Subscriber, and this will be initialized when the session starts
var subscriber: OTSubscriber?

VOIP/Video Call Actions

There are some common actions for calls, here are some of these actions

// Handle Mute/Unmute
@IBAction func muteButtonTapped(_ sender: UIButton) {
publisher.publishAudio = !publisher.publishAudio

if publisher.publishAudio {
sender.setTitle("Mute", for: .normal)
} else {
sender.setTitle("Unmute", for: .normal)
}
}

// Handle Camera Source
@IBAction func switchCameraButtonTapped(_ sender: UIButton) {
if publisher.cameraPosition == .front {
publisher.cameraPosition = .back
sender.setTitle("Front Camera", for: .normal)
} else {
publisher.cameraPosition = .front
sender.setTitle("Rear Camera", for: .normal)
}
}

// Handle Enable/Disable Video
@IBAction func toggleVideoButtonTapped(_ sender: UIButton) {
publisher.publishVideo = !publisher.publishVideo

if publisher.publishVideo {
sender.setTitle("Disable Video", for: .normal)
} else {
sender.setTitle("Enable Video", for: .normal)
}
}

// Handle End Call
@IBAction func endCallButtonTapped(_ sender: UIButton) {
var error: OTError?
session.disconnect(&error)

if let info = consultationInfo {
ApiService.cancelConsultation(id: Int(info.consultationId!), completion: {cancelledConsultation, failure, error in
// Handle cancelledConsultation as a CancelledConsultation object
})
}
}

Display Requirements

For display the view of the call, these are recommended

let kWidgetHeight = 240
let kWidgetWidth = 320

var scrollView: UIScrollView!

func setupScrollView() {
scrollView = UIScrollView(frame: view.bounds)
scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(scrollView)
}

// On the viewDidLoad call setupScrollView()

Connection

You can call the connection method in the viewDidLoad

doConnect()

Here are the doConnect and the other related functions

/**
* 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: consultationInfo?.medium == "voip" ? (consultationInfo?.voipConfig?.token ?? "") : (consultationInfo?.videoConfig?.token ?? ""), error: &error)
}

/**
* Sets up an instance of OTPublisher to use with this session. OTPubilsher
* binds to the device camera and microphone, and will provide A/V streams
* to the OpenTok session.
*/
fileprivate func doPublish() {
var error: OTError?
defer {
processError(error)
}

session.publish(publisher, error: &error)

if let pubView = publisher.view {
pubView.frame = CGRect(x: 0, y: 0, width: kWidgetWidth, height: kWidgetHeight)
if (consultationInfo?.medium == "video"){
scrollView.addSubview(pubView)
}
//view.addSubview(pubView)
}
}

/**
* Instantiates a subscriber for the given stream and asynchronously begins the
* process to begin receiving A/V content for this stream. Unlike doPublish,
* this method does not add the subscriber to the view hierarchy. Instead, we
* add the subscriber only after it has connected and begins receiving data.
*/
fileprivate func doSubscribe(_ stream: OTStream) {
var error: OTError?
defer {
processError(error)
}
subscriber = OTSubscriber(stream: stream, delegate: self)

session.subscribe(subscriber!, error: &error)
}

fileprivate func cleanupSubscriber() {
subscriber?.view?.removeFromSuperview()
subscriber = nil
}

fileprivate func cleanupPublisher() {
publisher.view?.removeFromSuperview()
}

fileprivate func processError(_ error: OTError?) {
if let err = error {
DispatchQueue.main.async {
let controller = UIAlertController(title: "Error", message: err.localizedDescription, preferredStyle: .alert)
controller.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
self.present(controller, animated: true, completion: nil)
}
}
}

OpenTok Session Delegate

You can use an extension for the ViewController as following

extension YourViewController: OTSessionDelegate {
func sessionDidConnect(_ session: OTSession) {
print("Session connected")
doPublish()
}

func sessionDidDisconnect(_ session: OTSession) {
print("Session disconnected")
}

func session(_ session: OTSession, streamCreated stream: OTStream) {
print("Session streamCreated: \(stream.streamId)")
if subscriber == nil {
doSubscribe(stream)
}
}

func session(_ session: OTSession, streamDestroyed stream: OTStream) {
print("Session streamDestroyed: \(stream.streamId)")
if let subStream = subscriber?.stream, subStream.streamId == stream.streamId {
cleanupSubscriber()
}
}

func session(_ session: OTSession, didFailWithError error: OTError) {
print("session Failed to connect: \(error.localizedDescription)")
}
}

OpenTok Publisher Delegate

You can use an extension for the ViewController as following

extension VideoConsultationVC: OTPublisherDelegate {
func publisher(_ publisher: OTPublisherKit, streamCreated stream: OTStream) {
print("Publishing")
}

func publisher(_ publisher: OTPublisherKit, streamDestroyed stream: OTStream) {
cleanupPublisher()
if let subStream = subscriber?.stream, subStream.streamId == stream.streamId {
cleanupSubscriber()
}
}

func publisher(_ publisher: OTPublisherKit, didFailWithError error: OTError) {
print("Publisher failed: \(error.localizedDescription)")
}
}

// MARK: - OTSubscriber delegate callbacks
extension VideoConsultationVC: OTSubscriberDelegate {
func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) {
if let subsView = subscriber?.view {
subsView.frame = CGRect(x: 0, y: kWidgetHeight, width: kWidgetWidth, height: kWidgetHeight)
scrollView.addSubview(subsView)
//view.addSubview(subsView)
}
}

func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) {
print("Subscriber failed: \(error.localizedDescription)")
}
}