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 invoipConfig
and forVIDEO
will be invideoConfig
but it will be the same data structure so will be using one interface for bothVoipConfig
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)")
}
}