How to implement one to one or group live video chat in iOS app using OpenTok

in #utopian-io6 years ago (edited)

Repository

e.g. https://github.com/opentok/opentok-ios-sdk-samples

one-to-one.gif

group.gif

What Will I Learn?

Today I am going to introduce to the topic that has been implemented widely in every platforms and Application either it is WebApp or Mobile App. In this tutorial, we will learn how we can start a one-to-one video chat and Group video chat by using TokBox Api's. You can Join the Video conferencing room and can publish the Video of yourself.

  • You will learn to publish your Video Stream.
  • You will learn to publish your Audio Stream.
  • You will learn How you can subscribe to another user's Video Stream.

Requirements

  • Xcode
  • The server that will be generating the Session token and User authentication tokens everytime user joins the session.
  • Coding in Swift 4.

Difficulty

  • Basic

Tutorial Contents

Ok, Let's begin So, first of all, we will be creating the account on OpenTok and create a project on it. Go to https://tokbox.com/ and create your account.

After creating the account you will be asked about you, your role and the industry you work in. After finishing the signup process let's go and create your first project. On the right-hand side of the page there is an option to create the project, Click on it and it will ask you to choose between two options, Choose "create Standard Project". Give it a project title like "OpenTokChatDemo" in this tute. After clicking next you will be given the API Key and Secret, copy them and store in safe place. Now go to view Project.

As we are dealing with the only iOS part so we will be in need of the session id which is used for differentiating the chat rooms and for every user we will be generating the auth tokens. At the end of the page create a session id and create an auth token for the Publisher.

Now time for the coding part.

  • Create a demo project in Xcode and make a class named StartViewController.swift which contains two buttons for one-to-one and another for Group Video call. Make another swift file named BroadcastVC.swift which will contain all the logic behind and where almost all the call process occurs.

  • Now in storyboard file, drag a UIViewController and give it class name StartViewController put two buttons on the view and connect the outlets. drag another UIViewController and give it a class name BroadcastVC.

  • In BroadcastVC file make outlets like

    @IBOutlet weak var instructorNotHereContainer : UIView!
    @IBOutlet weak var instructorNotHereActivityIndicator : UIActivityIndicatorView!
    @IBOutlet weak var participantsCollectionView : UICollectionView!
    @IBOutlet weak var dismissBtn : UIButton!
    @IBOutlet weak var muteUnMuteBtn : UIButton!
    @IBOutlet weak var screenShareBtn : UIButton!
    @IBOutlet weak var enableDisableVideoBtn : UIButton!
    @IBOutlet weak var publisherView : UIView!
    @IBOutlet var publisherViewContainer: UIView!
    @IBOutlet weak var instructorScreenShareViewOrMainView : UIView! // show instructor main view when he so not share 
        screen
    @IBOutlet weak var instructorViewWhileScreenShareContainer : UIView!
    @IBOutlet weak var instructorViewWhileScreenShare : UIView! //(round)show instructor main view in this view when 
           instructor share screen and show his screen share view in instructorScreenShareViewOrHisMainView
    @IBOutlet weak var instructorNoImageViewWhileScreenShare : UIView! //(round)show instructor main view in this view 
          when instructor share screen and show his screen share view in instructorScreenShareViewOrHisMainView
    @IBOutlet weak var instructorVideoDisabledImageView : UIImageView!
    @IBOutlet weak var subscriberViewHeightConstraint : NSLayoutConstraint!
    @IBOutlet weak var enlargeBtn : UIButton!
    @IBOutlet weak var activityIndicatorView : UIActivityIndicatorView!
    @IBOutlet weak var teacherLeftStackView: UIView!
    @IBOutlet var instructorFloatingHeadXConstraint: NSLayoutConstraint!
    @IBOutlet var instructorFloatingHeadYConstraint: NSLayoutConstraint!
    @IBOutlet var publisherFloatingHeadYConstraint: NSLayoutConstraint!
    @IBOutlet var publisherFloatingHeadXConstraint: NSLayoutConstraint!
    @IBOutlet var noPublisherVideoImageView: UIImageView!  
  • Now import OpenTok framework in the file
     import OpenTok
  • Make a variable of OTSession as
  lazy var otSession: OTSession = {
        return OTSession(apiKey: TOKBOXApiKey, sessionId: kSessionId, delegate: self)!
    }()
     and to publish video make a variable of OTPublisher
    lazy var publisher: OTPublisher = {
        let settings = OTPublisherSettings()
        settings.name = UIDevice.current.name
         return OTPublisher(delegate: self, settings: settings)!
      }()
  • make a method to connect to the session and call this method in ViewDidLoad method
    fileprivate func doConnect() {
        var error: OTError?
        defer {
            processError(error)
        }
        
        otSession.connect(withToken: kToken, error: &error)
     }

    override func viewDidLoad() {
        super.viewDidLoad()
         self.doConnect()
    }
  • Now we will implement the OTSEssionDelegates to handle the callbacks when the session is connected and when the
    stream is ready
   func sessionDidConnect(_ session: OTSession) {
        print("Session connected")
             doPublish()
     }
  • if the session is connected we publish the own video here
      func sessionDidDisconnect(_ session: OTSession) {
        print("Session disconnected")
        self.showAlertWithOkCompletion(title: "Oops!", message: "There is some issue with the class. Please rejoin") { 
      (compelted) in
            if !IsPad {
                AppUtility.lockOrientation(UIInterfaceOrientationMask.portrait, andRotateTo: UIInterfaceOrientation.portrait)
            }
            self.dismiss(animated: true, completion: nil)
        }
    }
  • sessionDidDisconnect method tell if the user is disconnected from the session, we basically free up all the resources and
    try to dismiss the chat of the user in this case.
    func session(_ session: OTSession, streamCreated stream: OTStream) {
        print("Session streamCreated: \(stream.streamId)")
         doSubscribe(stream)
     }
  • Now when a stream is created we subscribe to the stream and show it in the main view if it is for one-to-one and in UICollectionView if it is Group call.
    func session(_ session: OTSession, streamDestroyed stream: OTStream) {
        print("Session streamDestroyed: \(stream.streamId)")
        self.activityIndicatorView.isHidden = false
        self.activityIndicatorView.startAnimating()
        if let subStream = self.mainSubscriber?.stream, subStream.streamId == stream.streamId {
            //cleanupSubscriber()
            self.mainSubscriber?.view?.removeFromSuperview()
            self.mainSubscriber = nil
            if let _ = self.screenShareSubscriber {
                self.mainSubscriber = self.screenShareSubscriber
                self.screenShareSubscriber = nil
                self.instructorNoImageViewWhileScreenShare.isHidden = true
                self.instructorViewWhileScreenShare.isHidden = true
                self.activityIndicatorView.stopAnimating()
                self.activityIndicatorView.isHidden = true
                self.instructorVideoDisabledImageView.isHidden = true
                if let subView = self.mainSubscriber?.view {
                    subView.frame = self.instructorScreenShareViewOrMainView.bounds
                    self.instructorScreenShareViewOrMainView.addSubview(subView)
                    self.instructorScreenShareViewOrMainView.clipsToBounds = true
                    subView.prepareForNewConstraints(block: { (v) in
                        v?.setLeadingSpaceFromSuperView(leadingSpace: 0)
                        v?.setTrailingSpaceFromSuperView(trailingSpace: 0)
                        v?.setTopSpaceFromSuperView(topSpace: 0)
                        v?.setBottomSpaceFromSuperView(bottomSpace: 0)
                    })
                }
            } else {
                self.mainSubscriber = nil
                self.mainSubscriber?.view?.removeFromSuperview()
                self.teacherLeftStackView.isHidden = false
                instructorVideoDisabledImageView.isHidden = true
            }
        }
        if let screenSharesubscriberStream = self.screenShareSubscriber?.stream, screenSharesubscriberStream.streamId == stream.streamId {
            if screenSharesubscriberStream.videoType == .screen {
                self.screenShareSubscriber?.view?.removeFromSuperview()
                self.screenShareSubscriber = nil
            }//instructor view while screen share (round one)
            self.screenShareSubscriber?.view?.removeFromSuperview()
            self.mainSubscriber = self.screenShareSubscriber
            self.screenShareSubscriber = nil
            self.instructorNoImageViewWhileScreenShare.isHidden = true
            self.instructorViewWhileScreenShare.isHidden = true
            self.activityIndicatorView.stopAnimating()
            self.activityIndicatorView.isHidden = true
            self.instructorVideoDisabledImageView.isHidden = true
            
            if screenSharesubscriberStream.videoType == .camera {
                self.mainSubscriber?.view?.removeFromSuperview()
                self.mainSubscriber = nil
                for v in self.instructorScreenShareViewOrMainView.subviews {
                    if !v.isKind(of: UIActivityIndicatorView.self) && !v.isKind(of: UIStackView.self) && !v.isKind(of: UILabel.self){
                        v.removeFromSuperview()
                    }
                }
                self.teacherLeftStackView.isHidden = false
                self.view.bringSubview(toFront: self.teacherLeftStackView)
            }  else {
                if let subView = self.mainSubscriber?.view {
                    subView.frame = self.instructorScreenShareViewOrMainView.bounds
                    self.instructorScreenShareViewOrMainView.addSubview(subView)
                    self.instructorScreenShareViewOrMainView.clipsToBounds = true
                    subView.prepareForNewConstraints(block: { (v) in
                        v?.setLeadingSpaceFromSuperView(leadingSpace: 0)
                        v?.setTrailingSpaceFromSuperView(trailingSpace: 0)
                        v?.setTopSpaceFromSuperView(topSpace: 0)
                        v?.setBottomSpaceFromSuperView(bottomSpace: 0)
                    })
                }//instructor view while screen share (round one)
            }
            
        }
        subscribers = subscribers.filter { $0.stream?.streamId != stream.streamId }
        self.subscribersInfo = self.subscribersInfo.filter({ (info) -> Bool in
            return info.key != stream.streamId
        })
        participantsCollectionView?.reloadData()
    }
  • When the stream is destroyed we remove that stream view from the super view and free up some resources that can be created again when the stream is created again.
    func session(_ session: OTSession, didFailWithError error: OTError) {
        print("session Failed to connect: \(error.localizedDescription)")
    }
  • This method tells that the session fails to connect.
    func session(_ session: OTSession, receivedSignalType type: String?, from connection: OTConnection?, with string: 
    String?) {
        if let string = string {
            print("Received signal", string)
            if(connection?.connectionId == session.connection?.connectionId) {
                print("signaling own message")
            } else {
                let data = string.utf8Data()
                do {
                    let json = try JSONSerialization.jsonObject(with: data, options: [])
                    if let jsonInfo = (json as? [String : Any]){
                        
                        if let commandSendByInstructor = jsonInfo["command"] as? Int {
                            if commandSendByInstructor == 1 { //instructor commands to mute the voice
                                self.muteUnMuteBtn.isSelected = false
                                self.muteUnMuteBtnAction(self.muteUnMuteBtn)
                            } else if commandSendByInstructor == 2 { //instructor commands to unmute the voice
                                self.muteUnMuteBtn.isSelected = true
                                self.muteUnMuteBtnAction(self.muteUnMuteBtn)
                            }else if commandSendByInstructor == 4 { //instructor stops the whiteboard
                                //self.dismissBtn.isSelected = false
                                // self.dismissBtnAction(self.dismissBtn)
                            }
                        }
                    }
                } catch {
                    
                }
            }
        }
    }
  • This method is used to signal custom parameters from the user side such as sending the message. We will discuss this in the next tutorial when we will work on chat process.

  • Now we will discuss OTSubscriberDelegate methods

    // MARK: - OTSubscriber delegate callbacks

extension BroadcastVC: OTSubscriberDelegate {
    
    func subscriberDidConnect(toStream subscriberKit: OTSubscriberKit) {
        
    }
  • When a subscriber connects from the other end, we get action in this method.
    func subscriber(_ subscriber: OTSubscriberKit, didFailWithError error: OTError) {
        print("Subscriber failed: \(error.localizedDescription)")
    }
  • This is called when Subscriber failed to connect.
  func subscriberVideoDisabled(_ subscriber: OTSubscriberKit, reason: OTSubscriberVideoEventReason) {
        print("subscriberVideoDisabled")
        if subscriber == self.mainSubscriber {
            self.instructorVideoDisabledImageView.isHidden = false
        } else if subscriber == self.screenShareSubscriber {
            self.instructorViewWhileScreenShareContainer.bringSubview(toFront: instructorNoImageViewWhileScreenShare)
            self.instructorNoImageViewWhileScreenShare.isHidden = false
        } else {
            if let streamId = subscriber.stream?.streamId{
                self.subscribersInfo[streamId] = ["hasVideo" : false]
            }
            self.participantsCollectionView.reloadData()
        }
    }
  • When the subscriber disabled the video we handle that process in this method, i have shown a video disabled image on the view if the subscriber disables the video.
func subscriberVideoEnabled(_ subscriber: OTSubscriberKit, reason: OTSubscriberVideoEventReason) {
        print("subscriberVideoEnabled")
        self.instructorNoImageViewWhileScreenShare.isHidden = true
        if subscriber == self.mainSubscriber {
            self.instructorVideoDisabledImageView.isHidden = true
        } else if subscriber == self.screenShareSubscriber {
            self.instructorNoImageViewWhileScreenShare.isHidden = true
        } else {
            if let streamId = subscriber.stream?.streamId{
                self.subscribersInfo[streamId] = ["hasVideo" : true]
            }
            self.participantsCollectionView.reloadData()
        }
    }
  • This method is used when the subscriber enables the video we just show the actual video on the particular subscriber video view if the video is present.
  func subscriberVideoDisableWarning(_ subscriber: OTSubscriberKit) {
        print("subscriberVideoDisableWarning")
        if subscriber == self.mainSubscriber {
            self.instructorVideoDisabledImageView.isHidden = false
        } else {
            if let streamId = subscriber.stream?.streamId{
                self.subscribersInfo[streamId] = ["hasVideo" : false]
            }
            self.participantsCollectionView.reloadData()
        }
    }
  • Now there comes a situation when the connection is poor and the subscriber fails to publish the video stream, We just keep the stream id of that stream and handle the views accordingly to show the video disabled image view if the video is not present.
   func subscriberVideoDisableWarningLifted(_ subscriber: OTSubscriberKit) {
        print("subscriberVideoDisableWarningLifted")
        if subscriber == self.mainSubscriber {
            self.instructorVideoDisabledImageView.isHidden = true
        } else {
            if let streamId = subscriber.stream?.streamId{
                self.subscribersInfo[streamId] = ["hasVideo" : true]
            }
            self.participantsCollectionView.reloadData()
        }
    }
  • When the video can be published or the connection is not poor then we handle that particular situation here and show the actual video for the particular stream.

  • In the Constants.swift file replace TOKBOXApiKey and TOKBOXAPISecretKey with your generated keys on TokBox and you have to create the session id and auth token for the user you want the user to join.

Proof of Work Done

https://github.com/hitenkmr/OpenTokChatDemo

Sort:  

well done @hitenkmr, i am really looking for something like this. Your tutorial is well detailed and i hope i use this for my next project. Thanks for your contribution.

nice work !

@therealwolf 's created platform smartsteem scammed my post this morning (mothersday) that was supposed to be for an Abused Childrens Charity. Dude literally stole from abused children that don't have mothers ... on mothersday.

https://steemit.com/steemit/@prometheusrisen/beware-of-smartsteem-scam

Your contribution can't be approved because there are lots of similar tutorial on net and yours isn't unique.


Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

This whole tutorial has been made by me with such an effort I can bet upon. Such things when you hardly prepare for something but the effort is worthless makes me feel bad.

Please provide me a link which can prove the same post has been implemented before.

Sorry I didn´t put a link here. In this link you have tutorials on OpenTok well documented.

  • An example of a good tutorial: Link.

Need help? Write a ticket on https://support.utopian.io/.
Chat with us on Discord.
[utopian-moderator]

The link you gave just explains how to add the particular feature but does not provide full support to make a Video chat the one i have implemented. But keeping it in mind, i will try to make some unique things that you people would like.

Coin Marketplace

STEEM 0.30
TRX 0.12
JST 0.034
BTC 64231.88
ETH 3128.59
USDT 1.00
SBD 3.95