DEV Community

Kishan Nakrani for Video SDK

Posted on • Originally published at videosdk.live on

How to Integrate Chat Feature using PubSub in iOS Video Call App

πŸ“Œ Introduction

How to Integrate Chat Feature using PubSub in iOS Video Call App?

Integrate Chat using PubSub in your iOS Video Call App to enhance user communication and experience. With PubSub, real-time messaging becomes seamless, allowing users to exchange messages effortlessly during video calls. This integration adds a layer of interactivity, enabling users to chat, share information, and collaborate seamlessly while engaging in video conversations. By incorporating PubSub technology, your app ensures reliable message delivery and synchronization across all connected devices, enhancing the overall user experience.

Benefits of Integrate Chat using PubSub in iOS Video Call App:

  1. Real-time Communication: PubSub integration enables real-time messaging within your iOS Video Call App, enhancing user interaction during video calls.
  2. Seamless Collaboration: Users can share information, exchange messages, and collaborate effectively while engaged in video conversations, fostering teamwork and productivity.
  3. Enhanced User Experience: The addition of PubSub technology enriches the app's functionality, providing a smoother and more interactive experience for users.
  4. Reliable Message Delivery: PubSub ensures reliable message delivery and synchronization across all connected devices, eliminating delays and ensuring messages are delivered promptly.
  5. Scalability: PubSub architecture allows your app to scale effortlessly, accommodating a growing user base without compromising performance.

Use Case of Integrate Chat using PubSub in iOS Video Call App:

  • Real-time Collaboration: While discussing project details via video call, team members can use the integrated chat feature powered by PubSub to share documents, links, and updates instantly.
  • Instant Feedback: Team members can provide instant feedback or ask questions via chat without interrupting the flow of the video call, enhancing communication efficiency.
  • Document Sharing: With PubSub, users can seamlessly share project documents, screenshots, or any necessary files directly within the chat, facilitating collaboration and decision-making.

In this guide, we'll walk you through the process of seamlessly adding chat features to your iOS video-calling application. From setting up the chat environment to handling chat interactions within your video call interfaces, we'll cover all the essential steps to improve your app's functionality and user experience.

πŸš€ Getting Started with VideoSDK

To integrate the Chat Feature, we must use the capabilities that VideoSDK offers. Before diving into the implementation steps, let's ensure you complete the necessary prerequisites.

Create a VideoSDK Account

Go to your VideoSDK dashboard and sign up if you don't have an account. This account gives you access to the required Video SDK token, which acts as an authentication key that allows your application to interact with VideoSDK functionality.

Generate your Auth Token

Visit your VideoSDK dashboard and navigate to the "API Key" section to generate your auth token. This token is crucial in authorizing your application to use VideoSDK features. For a more visual understanding of the account creation and token generation process, consider referring to the provided tutorial.

Prerequisites and Setup

  • iOS 11.0+
  • Xcode 12.0+
  • Swift 5.0+

This App will contain two screens:

Join Screen : This screen allows the user to either create a meeting or join the predefined meeting.

Meeting Screen : This screen basically contains local and remote participant views and some meeting controls such as Enable/Disable the microphone or Camera and Leave meeting.

πŸ› οΈ Integrate VideoSDK​

To install VideoSDK, you must initialize the pod on the project by running the following command:

pod init
Enter fullscreen mode Exit fullscreen mode

It will create the pod file in your project folder, Open that file and add the dependency for the VideoSDK, like below:

pod 'VideoSDKRTC', :git => 'https://github.com/videosdk-live/videosdk-rtc-ios-sdk.git'
Enter fullscreen mode Exit fullscreen mode

How to Integrate Chat Feature using PubSub in iOS Video Call App?

then run the below code to install the pod:

pod install
Enter fullscreen mode Exit fullscreen mode

then declare the permissions in Info.plist :

<key>NSCameraUsageDescription</key>
<string>Camera permission description</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission description</string>
Enter fullscreen mode Exit fullscreen mode

Project Structure

iOSQuickStartDemo
   β”œβ”€β”€ Models
        β”œβ”€β”€ RoomStruct.swift
        └── MeetingData.swift
   β”œβ”€β”€ ViewControllers
        β”œβ”€β”€ StartMeetingViewController.swift
        └── MeetingViewController.swift
   β”œβ”€β”€ AppDelegate.swift // Default
   β”œβ”€β”€ SceneDelegate.swift // Default
   └── APIService
           └── APIService.swift
   β”œβ”€β”€ Main.storyboard // Default
   β”œβ”€β”€ LaunchScreen.storyboard // Default
   └── Info.plist // Default
 Pods
     └── Podfile
Enter fullscreen mode Exit fullscreen mode

Create models​

Create a swift file for MeetingData and RoomStruct class model for setting data in object pattern.

import Foundation
struct MeetingData {
    let token: String
    let name: String
    let meetingId: String
    let micEnabled: Bool
    let cameraEnabled: Bool
}
Enter fullscreen mode Exit fullscreen mode

MeetingData.swift

import Foundation
struct RoomsStruct: Codable {
    let createdAt, updatedAt, roomID: String?
    let links: Links?
    let id: String?
    enum CodingKeys: String, CodingKey {
        case createdAt, updatedAt
        case roomID = "roomId"
        case links, id
    }
}

// MARK: - Links
struct Links: Codable {
    let getRoom, getSession: String?
    enum CodingKeys: String, CodingKey {
        case getRoom = "get_room"
        case getSession = "get_session"
    }
}
Enter fullscreen mode Exit fullscreen mode

RoomStruct.swift

πŸŽ₯ Essential Steps for Building the Video Calling

This guide is designed to walk you through the process of integrating Chat with VideoSDK. We'll cover everything from setting up the SDK to incorporating the visual cues into your app's interface, ensuring a smooth and efficient implementation process.

Step 1: Get started with APIClient​

Before jumping to anything else, we have to write an API to generate a unique meetingId. You will require an authentication token; you can generate it either using videosdk-server-api-example or from the VideoSDK Dashboard for developers.

import Foundation

let TOKEN_STRING: String = "<AUTH_TOKEN>"

class APIService {

  class func createMeeting(token: String, completion: @escaping (Result<String, Error>) -> Void) {

    let url = URL(string: "https://api.videosdk.live/v2/rooms")!

    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.addValue(TOKEN_STRING, forHTTPHeaderField: "authorization")

    URLSession.shared.dataTask(
      with: request,
      completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in

        DispatchQueue.main.async {

          if let data = data, let utf8Text = String(data: data, encoding: .utf8) {
            do {
              let dataArray = try JSONDecoder().decode(RoomsStruct.self, from: data)

              completion(.success(dataArray.roomID ?? ""))
            } catch {
              print("Error while creating a meeting: \(error)")
              completion(.failure(error))
            }
          }
        }
      }
    ).resume()
  }
}

Enter fullscreen mode Exit fullscreen mode

APIService.swift

Step 2: Implement Join Screen​

The Join Screen will work as a medium to either schedule a new meeting or join an existing meeting.

import Foundation
import UIKit

class StartMeetingViewController: UIViewController, UITextFieldDelegate {

  private var serverToken = ""

  /// MARK: outlet for create meeting button
  @IBOutlet weak var btnCreateMeeting: UIButton!

  /// MARK: outlet for join meeting button
  @IBOutlet weak var btnJoinMeeting: UIButton!

  /// MARK: outlet for meetingId textfield
  @IBOutlet weak var txtMeetingId: UITextField!

  /// MARK: Initialize the private variable with TOKEN_STRING &
  /// setting the meeting id in the textfield
  override func viewDidLoad() {
    txtMeetingId.delegate = self
    serverToken = TOKEN_STRING
    txtMeetingId.text = "PROVIDE-STATIC-MEETING-ID"
  }

  /// MARK: method for joining meeting through seague named as "StartMeeting"
  /// after validating the serverToken in not empty
  func joinMeeting() {

    txtMeetingId.resignFirstResponder()

    if !serverToken.isEmpty {
      DispatchQueue.main.async {
        self.dismiss(animated: true) {
          self.performSegue(withIdentifier: "StartMeeting", sender: nil)
        }
      }
    } else {
      print("Please provide auth token to start the meeting.")
    }
  }

  /// MARK: outlet for create meeting button tap event
  @IBAction func btnCreateMeetingTapped(_ sender: Any) {
    print("show loader while meeting gets connected with server")
    joinRoom()
  }

  /// MARK: outlet for join meeting button tap event
  @IBAction func btnJoinMeetingTapped(_ sender: Any) {
    if (txtMeetingId.text ?? "").isEmpty {

      print("Please provide meeting id to start the meeting.")
      txtMeetingId.resignFirstResponder()
    } else {
      joinMeeting()
    }
  }

  // MARK: - method for creating room api call and getting meetingId for joining meeting

  func joinRoom() {

    APIService.createMeeting(token: self.serverToken) { result in
      if case .success(let meetingId) = result {
        DispatchQueue.main.async {
          self.txtMeetingId.text = meetingId
          self.joinMeeting()
        }
      }
    }
  }

  /// MARK: preparing to animate to meetingViewController screen
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    guard let navigation = segue.destination as? UINavigationController,

      let meetingViewController = navigation.topViewController as? MeetingViewController
    else {
      return
    }

    meetingViewController.meetingData = MeetingData(
      token: serverToken,
      name: txtMeetingId.text ?? "Guest",
      meetingId: txtMeetingId.text ?? "",
      micEnabled: true,
      cameraEnabled: true
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

StartMeetingViewController.swift

Step 3: Initialize and Join Meeting​

Using the provided token and meetingId, we will configure and initialize the meeting in viewDidLoad().

Then, we'll add @IBOutlet for localParticipantVideoView and remoteParticipantVideoView, which can render local and remote participant media, respectively.

class MeetingViewController: UIViewController {

import UIKit
import VideoSDKRTC
import WebRTC
import AVFoundation

class MeetingViewController: UIViewController {

// MARK: - Properties
// outlet for local participant container view
   @IBOutlet weak var localParticipantViewContainer: UIView!

// outlet for label for meeting Id
   @IBOutlet weak var lblMeetingId: UILabel!

// outlet for local participant video view
   @IBOutlet weak var localParticipantVideoView: RTCMTLVideoView!

// outlet for remote participant video view
   @IBOutlet weak var remoteParticipantVideoView: RTCMTLVideoView!

// outlet for remote participant no media label
   @IBOutlet weak var lblRemoteParticipantNoMedia: UILabel!

// outlet for remote participant container view
   @IBOutlet weak var remoteParticipantViewContainer: UIView!

// outlet for local participant no media label
   @IBOutlet weak var lblLocalParticipantNoMedia: UILabel!

// Meeting data - required to start
   var meetingData: MeetingData!

// current meeting reference
   private var meeting: Meeting?

    // MARK: - video participants including self to show in UI
    private var participants: [Participant] = []

        // MARK: - Lifecycle Events

        override func viewDidLoad() {
        super.viewDidLoad()
        // configure the VideoSDK with token
        VideoSDK.config(token: meetingData.token)

        // init meeting
        initializeMeeting()

        // set meeting id in button text
        lblMeetingId.text = "Meeting Id: \(meetingData.meetingId)"
      }

      override func viewWillAppear(_ animated: Bool) {
          super.viewWillAppear(animated)
          navigationController?.navigationBar.isHidden = true
      }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        navigationController?.navigationBar.isHidden = false
        NotificationCenter.default.removeObserver(self)
    }

        // MARK: - Meeting

        private func initializeMeeting() {

            // Initialize the VideoSDK
            meeting = VideoSDK.initMeeting(
                meetingId: meetingData.meetingId,
                participantName: meetingData.name,
                micEnabled: meetingData.micEnabled,
                webcamEnabled: meetingData.cameraEnabled
            )

            // Adding the listener to meeting
            meeting?.addEventListener(self)

            // joining the meeting
            meeting?.join()
        }
}
Enter fullscreen mode Exit fullscreen mode

MeetingViewController.swift

Step 4: Implement Controls​

After initializing the meeting in the previous step, we will now add @IBOutlet for btnLeave, btnToggleVideo and btnToggleMic which can control the media in the meeting.

class MeetingViewController: UIViewController {

...

    // outlet for leave button
    @IBOutlet weak var btnLeave: UIButton!

    // outlet for toggle video button
    @IBOutlet weak var btnToggleVideo: UIButton!

    // outlet for toggle audio button
    @IBOutlet weak var btnToggleMic: UIButton!

    // bool for mic
    var micEnabled = true
    // bool for video
    var videoEnabled = true

    // outlet for leave button click event
    @IBAction func btnLeaveTapped(_ sender: Any) {
            DispatchQueue.main.async {
                self.meeting?.leave()
                self.dismiss(animated: true)
            }
        }

    // outlet for toggle mic button click event
    @IBAction func btnToggleMicTapped(_ sender: Any) {
        if micEnabled {
            micEnabled = !micEnabled // false
            self.meeting?.muteMic()
        } else {
            micEnabled = !micEnabled // true
            self.meeting?.unmuteMic()
        }
    }

    // outlet for toggle video button click event
    @IBAction func btnToggleVideoTapped(_ sender: Any) {
        if videoEnabled {
            videoEnabled = !videoEnabled // false
            self.meeting?.disableWebcam()
        } else {
            videoEnabled = !videoEnabled // true
            self.meeting?.enableWebcam()
        }
    }

...

}
Enter fullscreen mode Exit fullscreen mode

MeetingViewController.swift

Step 5: Implementing MeetingEventListener​

In this step, we'll create an extension of the MeetingViewController that implements the MeetingEventListener, which implements the onMeetingJoined, onMeetingLeft, onParticipantJoined, onParticipantLeft, onParticipantChanged etc. methods.

We'll create another extension of the MeetingViewController that implements the PubSubMessageListener that triggers when you receive a new message, it implements onMessageReceived method.

//Added pub-sub message listener.


class MeetingViewController: UIViewController {

...

extension MeetingViewController: MeetingEventListener {

        /// Meeting started
        func onMeetingJoined() {

            // handle local participant on start
            guard let localParticipant = self.meeting?.localParticipant else { return }
            // add to list
            participants.append(localParticipant)

            // add event listener
            localParticipant.addEventListener(self)

            localParticipant.setQuality(.high)

            if(localParticipant.isLocal){
                self.localParticipantViewContainer.isHidden = false
            } else {
                self.remoteParticipantViewContainer.isHidden = false
            }
        }

        /// Meeting ended
        func onMeetingLeft() {
            // remove listeners
            meeting?.localParticipant.removeEventListener(self)
            meeting?.removeEventListener(self)
        }

        /// A new participant joined
        func onParticipantJoined(_ participant: Participant) {
            participants.append(participant)

            // add listener
            participant.addEventListener(self)

            participant.setQuality(.high)

            if(participant.isLocal){
                self.localParticipantViewContainer.isHidden = false
            } else {
                self.remoteParticipantViewContainer.isHidden = false
            }
        }

        /// A participant left from the meeting
        /// - Parameter participant: participant object
        func onParticipantLeft(_ participant: Participant) {
            participant.removeEventListener(self)
            guard let index = self.participants.firstIndex(where: { $0.id == participant.id }) else {
                return
            }
            // remove participant from list
            participants.remove(at: index)
            // hide from ui
            UIView.animate(withDuration: 0.5){
                if(!participant.isLocal){
                    self.remoteParticipantViewContainer.isHidden = true
                }
            }
        }

}

extension MeetingViewController: PubSubMessageListener {

    func onMessageReceived(_ message: PubSubMessage) {
        print("Message Received:= " + message.message)

        /// Your ui code for showing the chat view
    }
}

...
Enter fullscreen mode Exit fullscreen mode

MeetingViewController.swift

Step 6: Implementing ParticipantEventListener

In this stage, we'll add an extension for the MeetingViewController that implements the ParticipantEventListener, which implements the onStreamEnabled and onStreamDisabled methods for the audio and video of MediaStreams enabled or disabled.

The function updateUI is frequently used to control or modify the user interface (enable/disable camera & mic) by the MediaStream state.

class MeetingViewController: UIViewController {

...

extension MeetingViewController: ParticipantEventListener {

/// Participant has enabled mic, video or screenshare
/// - Parameters:
/// - stream: enabled stream object
/// - participant: participant object
func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) {
    updateUI(participant: participant, forStream: stream, enabled: true)
 }

/// Participant has disabled mic, video or screenshare
/// - Parameters:
/// - stream: disabled stream object
/// - participant: participant object

func onStreamDisabled(_ stream: MediaStream, 
            forParticipant participant: Participant) {

  updateUI(participant: participant, forStream: stream, enabled: false)
 }

}

private extension MeetingViewController {

 func updateUI(participant: Participant, forStream stream: MediaStream, enabled: Bool) { // true
        switch stream.kind {
        case .state(value: .video):
            if let videotrack = stream.track as? RTCVideoTrack {
                if enabled {
                    DispatchQueue.main.async {
                        UIView.animate(withDuration: 0.5){

                            if(participant.isLocal) {

        self.localParticipantViewContainer.isHidden = false
    self.localParticipantVideoView.isHidden = false       
    self.localParticipantVideoView.videoContentMode = .scaleAspectFill self.localParticipantViewContainer.bringSubviewToFront(self.localParticipantVideoView)                                                           
    videotrack.add(self.localParticipantVideoView)
    self.lblLocalParticipantNoMedia.isHidden = true

} else {
        self.remoteParticipantViewContainer.isHidden = false
            self.remoteParticipantVideoView.isHidden = false
                                self.remoteParticipantVideoView.videoContentMode = .scaleAspectFill
                                self.remoteParticipantViewContainer.bringSubviewToFront(self.remoteParticipantVideoView)
                                                videotrack.add(self.remoteParticipantVideoView)
 self.lblRemoteParticipantNoMedia.isHidden = true
        }
     }
  }
} else {
         UIView.animate(withDuration: 0.5){
                if(participant.isLocal){

                    self.localParticipantViewContainer.isHidden = false
                    self.localParticipantVideoView.isHidden = true
                    self.lblLocalParticipantNoMedia.isHidden = false
                            videotrack.remove(self.localParticipantVideoView)
} else {
                   self.remoteParticipantViewContainer.isHidden = false
                   self.remoteParticipantVideoView.isHidden = true
                   self.lblRemoteParticipantNoMedia.isHidden = false
                            videotrack.remove(self.remoteParticipantVideoView)
      }
    }
  }
}

     case .state(value: .audio):
            if participant.isLocal {

               localParticipantViewContainer.layer.borderWidth = 4.0
               localParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
            } else {
                remoteParticipantViewContainer.layer.borderWidth = 4.0
                remoteParticipantViewContainer.layer.borderColor = enabled ? UIColor.clear.cgColor : UIColor.red.cgColor
            }
        default:
            break
        }
    }
}

...

Enter fullscreen mode Exit fullscreen mode

Known Issue​

Please add the following line to the MeetingViewController.swift file's viewDidLoad method If you get your video out of the container, view like below image.

override func viewDidLoad() {

  localParticipantVideoView.frame = CGRect(x: 10, y: 0, 
            width: localParticipantViewContainer.frame.width, 
        height: localParticipantViewContainer.frame.height)

  localParticipantVideoView.bounds = CGRect(x: 10, y: 0, 
        width: localParticipantViewContainer.frame.width, 
            height: localParticipantViewContainer.frame.height)

  localParticipantVideoView.clipsToBounds = true

  remoteParticipantVideoView.frame = CGRect(x: 10, y: 0, 
        width: remoteParticipantViewContainer.frame.width, 
            height: remoteParticipantViewContainer.frame.height)

  remoteParticipantVideoView.bounds = CGRect(x: 10, y: 0, 
        width: remoteParticipantViewContainer.frame.width, 
            height: remoteParticipantViewContainer.frame.height)

    remoteParticipantVideoView.clipsToBounds = true
}

Enter fullscreen mode Exit fullscreen mode

MeetingViewController.swift

TIP:

Stuck anywhere? Check out this [example code]

After successfully integrating the VideoSDK into your iOS app, you've unlocked the power of high-quality video calling. This not only improves user experience but can encourage longer call duration and deeper engagement in your app. By adding a chat feature, you provide users with more flexibility and control during their calls.

πŸ“Έ Integrate Chat Feature in Video App

For communication or any kind of messaging between the participants, VideoSDK provides pubSub classes that use the Publish-Subscribe mechanism and can be used to develop a wide variety of functionalities. For example, participants could use it to send chat messages to each other, share files or other media, or even trigger actions like muting or unmuting audio or video.

Now we will see, how we can use PubSub to implement Chat functionality. If you are not familiar with the PubSub mechanism and pubSub class, you can follow this guide.

Implementing Group Chat

  • The first step in creating a group chat is choosing the topic that all the participants will publish and subscribe to send and receive the messages. We will be using CHAT as the topic for this one.
  • On the send button, publish the message that the sender typed in the Text field.

NOTE:
It is assumed that you have created a user interface along with the implementation of the meeting class and its event listeners.
For reference check out our iOS example app on how can you set up your app.

class MeetingViewController {
    //Button that will send the message when tapped
    @IBAction func sendMessageTapped(_ sender: Any) {
        let options = ["persist" : true]
        // publish message
        self.meeting?.pubsub.publish(topic: "CHAT", message: "How are you?", options: options)
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The next step would be to display the messages others send. For this, we have to subscribe to that topic i.e CHAT and display all the messages. When a message is received, the onMessageReceived event of the PubSubMessageListener is triggered.
extension MeetingViewController: MeetingEventListener {
    func onMeetingJoined() {
    //subscribe to the topic 'CHAT' when onMeetingJoined is triggered
    meeting?.pubsub.subscribe(topic: "CHAT", forListener: self)
    }
}
extension MeetingViewController: PubSubMessageListener {
    // read message when it is received
    func onMessageReceived(_ message: PubSubMessage) {
        print("Message Received: " + message.message)
    }
}
Enter fullscreen mode Exit fullscreen mode

The final step in the group chat would be unsubscribe to that topic, which you had previously subscribed to but no longer needed. Here we are unsubscribe to CHAT topic on activity destroy.

extension MeetingViewController: MeetingEventListener {
    func onMeetingLeft() {
    //unsubscribe to the topic 'CHAT' when onMeetingLeft is triggered
    ////highlight-next-line
    meeting?.pubsub.unsubscribe(topic: "CHAT", forListener: self)
    }
}
Enter fullscreen mode Exit fullscreen mode
  • If you want to full guide about all features with chat, you can check our documentation:

Chat messages with PubSub - Video SDK Docs | Video SDK

Integrate Private Chat

In the above example, if you want to convert into a private chat between two participants, then all you have to do is pass the participantID of the participant in the sendOnly option as ["sendOnly" : ["ABCD"]]. Here sendOnly accepts an array of participant IDs if you wish to send a message to.

class MeetingViewController {
    //Button that will send the private message when tapped
    @IBAction func sendPrivateMessageTapped(_ sender: Any) {
        //adding sendOnly option
        let options = ["sendOnly" : ["ABCD"]]
        // publish message
        self.meeting?.pubsub.publish(topic: "CHAT", message: "How are you?", options: options)
    }
}
Enter fullscreen mode Exit fullscreen mode

Downloading Chat Messages​

All the messages from the PubSub which where published with persist : true and can be downloaded as a .csv file. This file will be available in the VideoSDK dashboard as well as through the Sessions API.

πŸ”š Conclusion

By integrating chat functionality using PubSub, you've added a valuable layer of communication to your video call application with VideoSDK.live. This guide has equipped you with the knowledge to implement this valuable addition to your iOS app. This integration improves engagement and fosters better communication among users. By leveraging PubSub, developers can create a robust and scalable chat feature that complements the video calling experience.

Unlock the full potential of VideoSDK today and craft seamless video experiences! Sign up now to receive 10,000 free minutes and take your video app to new heights.

Top comments (0)