How to add a video call feature to your iOS app in 15 minutes
Tags: ios, webrtc
Published: 2023-06-27 06:29:34

In this article, we’ll show you how to integrate video calls into your iOS app in 15 minutes. You don’t have to implement the entire WebRTC stack from scratch; you can just use a ready-made SDK.

Here is what the result will look like:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/psy2dwvyxaeotk7l6bo4.gif)


You’ll implement the business logic and interface. The video call function will be integrated using GCoreVideoCallsSDK, a GCore framework that takes care of creating and interacting with sockets and WebRTC, connecting/creating a video call room, and interacting with the server.

> **Note**: This article is part of a series about working with iOS. In other articles, we show you how to [create a mobile streaming app on iOS](https://gcore.com/blog/how-to-create-a-mobile-streaming-app-on-ios/), and how to [add VOD uploading](https://gcore.com/blog/add-vod-uploading-feature-to-ios-app/) and [smooth scrolling VOD](https://gcore.com/blog/add-smooth-scrolling-vod-feature-to-ios-app/) features to an existing app.


## What functions you can add with the help of this guide

The solution includes the following:

1. Video calling with the camera and microphone.
2. Showing the conversation participants using your designs.
3. Sending a video stream from the built-in camera to the server.
4. Receiving a video stream from the server.


## The solution architecture

Here is what the solution architecture for video calls looks like:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/741a2do2crbtp03g1d0d.png)


## How to integrate the video calling feature into your app

### Step 1: Prep

The application must connect to the room and show the user and speakers on the screen. To accomplish this, we’ll use UICollectionView and UICollectionViewCel to display the participants, and UIView to display the user. WebRTC provides `RTCEAGLVideoView` to display video streams in the application. We’ll also create a small model to store data and link it all. The SDK will be UIViewController.

The final application will look something like this:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qgpvvxh0h5if74tw7xwu.png)


First, you’ll need to install all the dependencies and request the rights to video/microphone recording from the user.

#### Dependencies

Use CocoaPods to instal [Mediasoup iOS Client](https://github.com/ethand91/mediasoup-ios-client) and [GcoreVideoCallsSDK](https://github.com/G-Core/ios-video-calls-SDK) by specifying the following in podfile:

```
source 'https://github.com/G-Core/ios-video-calls-SDK.git'
 
... 
 
pod "mediasoup_ios_client", '1.5.3' 
pod "GCoreVideoCallsSDK", '2.6.0'
```


#### Permissions

To allow the application to access the camera and microphone, specify the permissions in the project’s Info.plist:

- `NSMicrophoneUsageDescription` (Privacy – Microphone Usage Description)
- `NSCameraUsageDescription` (Privacy – Camera Usage Description)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ph4y2y851nhcx8b3o18y.png)


### Step 2: Create the UI

#### Model

A model is required to store any data, in this case, user data.

Create a model file and import GCoreVideoCallsSDK into it. To store user data, create the VideoCallUnit structure, which will contain the user’s ID and name, and RTCEAGLVideoView, which is assigned to it. Here is what the whole file will look like:

```
import GCoreVideoCallsSDK 
 
final class Model { 
    var localUser: VideoCallUnit? 
    var remoteUsers: [VideoCallUnit] = [] 
} 
 
struct VideoCallUnit { 
    let peerID: String 
    let name: String 
    let view = RTCEAGLVideoView() 
} 
```


#### CollectionCell

CollectionCell will be used to display the speaker.

Cells are reused and can show a large number of different speakers in their lifetime. For this to work, we need a mechanism that will remove the previous thread from the screen, pull a new thread onto the screen, and set its position on the screen. To implement the mechanism, create a CollectionCell class and set the inheritance from UICollectionViewCell. This class will contain only one property: _rtcView_.

```
import GCoreVideoCallsSDK 
 
final class CollectionCell: UICollectionViewCell { 
   weak var rtcView: RTCEAGLVideoView? { 
       didSet { 
           oldValue?.removeFromSuperview() 
           guard let rtcView = rtcView else { return } 
           rtcView.frame = self.bounds 
           addSubview(rtcView) 
       } 
   } 
}
```


#### ViewController

Set up a controller to manage the entire process:

Import GCoreVideoCallsSDK.

```
   import GCoreVideoCallsSDK
```

Create a model property.

```
   let model = Model()
```

Create the property cellID to hold the cell ID, and the lazy property collectionView to manage the cells (lazy to use cellID, view, and self). Set up the collection layout, assign a controller as _dataSource_, and register the cell.

```
lazy var collectionView: UICollectionView = { 
    let layout = UICollectionViewFlowLayout() 
    layout.itemSize.width = UIScreen.main.bounds.width - 100 
    layout.itemSize.height = layout.itemSize.width 
    layout.minimumInteritemSpacing = 10 
         
    let collection = UICollectionView(frame: view.bounds, collectionViewLayout: layout) 
    collection.backgroundColor = .white 
    collection.dataSource = self 
    collection.register(CollectionCell.self, forCellWithReuseIdentifier: cellID) 
 
    return collection 
}()
```

Create the localView property. The layout for this view needs to be made from the code; to do this, create the method initConstraints.

```
let localView: UIView = { 
    let view = UIView(frame: .zero) 
    view.translatesAutoresizingMaskIntoConstraints = false 
    view.layer.cornerRadius = 10  
    view.backgroundColor = .black 
    view.clipsToBounds = true  
         
    return view 
}() 
 
func initConstraints() { 
     NSLayoutConstraint.activate([ 
      localView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 3), 
      localView.heightAnchor.constraint(equalTo: localView.widthAnchor, multiplier: 4/3), 
      localView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 5), 
      localView.bottomAnchor.constraint(equalTo:  view.bottomAnchor, constant: -5) 
    ]) 
}
```

Create the gcMeet property.

```
var gcMeet = gcMeet.shared
```

In the _viewDidLoad_ method, add collectionView and localView to the main view and initialize the constraints.

```
override func viewDidLoad() { 
    super.viewDidLoad() 
 
    view.backgroundColor = .white 
    view.addSubview(collectonView) 
    view.addSubview(localView) 
     
    initConstraints() 
})
```

Create an extension to subscribe the controller to UICollectionViewDataSource. This is necessary in order to link the UI to the model and set up cells when they appear.

```
extension ViewController: UICollectionViewDataSource { 
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 
        // Get remote peers count
        return model.remoteUsers.count
    } 
     
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! CollectionCell 
        cell.rtcView = model.remoteUsers[indexPath.row].view 
        cell.layer.cornerRadius = 10 
        cell.backgroundColor = .black 
        cell.clipsToBounds = true 
 
        return cell 
    } 
}
```

The UI is now ready to display the call. Now we need to set up the relationship with the SDK.


#### Initializing GCoreMeet

The connection to the server is made through _GCoreMeet.shared_. The parameters for the local user, room, and camera will be passed to it. You also need to call the method to activate the audio session, which will allow you to capture headphones connected. The SDK passes data from the server to the application through listeners: _RoomListener_ and _ModeratorListener_.

Add all this to the viewDidLoad method:

```
override func viewDidLoad() { 
    super.viewDidLoad() 
 
    view.backgroundColor = .white 
    view.addSubview(collectonView) 
    view.addSubview(localView) 
     
    let userParams = GCoreLocalUserParams(name: "EvgenMob", role: .moderator) 
    let roomParams = GCoreRoomParams(id: "serv1z3snbnoq") 
    gcMeet.connectionParams = (userParams, roomParams) 
 
    gcMeet.audioSessionActivate() 
 
    gcMeet.moderatorListener = self 
    gcMeet.roomListener = self 
     
    try? gcMeet.startConnection() 
         
    initConstraints() 
}
```

The _roomID_ can be taken from https://meet.gcore.com by clicking on the _Create a room for free_ button and selecting a conference.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fx7dnzulxuxzboinyb86.png)


In the RoomListener methods, you will also receive video streams (both your own and remote users’), audio streams, and information about users and moderator actions, which will allow you to render the necessary UI. This will be discussed further later in the article.

Through the ModeratorListener, you will receive requests from other users to enable streams and be notified when new users join the waiting room. The `MeetRoomParameters` can also use the following parameters:

- `clientHostName`: You can leave this nil, in which case the default will be meet.gcorelabs.com
- `peerId`: If you leave this nil, the ID will be auto-generated by the SDK.


### Step 3: Interacting with the Gcore server

Interaction with the server is carried out through an object that subscribes to the RoomListener protocol. It has a number of methods. You can find more details about these, as well as about the SDK, on [SDK readme](https://github.com/G-Core/ios-video-calls-SDK/blob/main/README.md). Below is an image of what the process of this interaction looks like:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fics3bqgwly3xdfyfl8f.png)


For the simplest implementation, you need only a few methods.
First, subscribe the controller to the RoomListener protocol:

```
extension ViewController: RoomListener { 
 
}
```

Add the required methods to extension.

The first method is called when joining a room and sends initial data related to room permissions, a list of participants, and information about the local user.

```
func roomClientHandle(_ client: GCoreRoomClient, forAllRoles joinData: GCoreJoinData)
```

We’re interested in the user list:

```
// To get data at the moment of entering the room
func roomClientHandle(_ client: GCoreRoomClient, forAllRoles joinData: GCoreJoinData) { 
    switch joinData { 
    case othersInRoom(remoteUsers: [GCoreRemoteUser]): 
        remoteUsers.forEach { 
            model.remoteUsers += [ .init(peerID: $0.id, name: $0.displayName ?? "") ] 
        } 
        collectonView.reloadData() 
    default: 
        break 
    } 
 }
```

The next method is called when the connection status of the room changes.

```
func roomClientHandle(_ client: GCoreRoomClient, connectionEvent: GCoreRoomConnectionEvent)
```

The device’s camera and microphone will turn on once successfully connected.

```
// To update the status of the connection to the room
func roomClientHandle(_ client: GCoreRoomClient, forAllRoles joinData: GCoreJoinData) { 
    switch joinData { 
    case othersInRoom(remoteUsers: [GCoreRemoteUser]): 
        remoteUsers.forEach { 
            model.remoteUsers += [ .init(peerID: $0.id, name: $0.displayName ?? "") ] 
        } 
        collectonView.reloadData() 
    default: 
        break 
    } 
 }
```

The next method is called for events related to remote user data but not related to media.

``` 
func roomClientHandle(_ client: GCoreRoomClient, remoteUsersEvent: GCoreRemoteUsersEvent) 
```

You will use it to add and remove users during the call.

```
// To respond to actions related to remote users, not related to video/audio streams
func roomClientHandle(_ client: GCoreRoomClient, remoteUsersEvent: GCoreRemoteUsersEvent) { 
    switch remoteUsersEvent { 
    case handleRemote(user: GCoreRemoteUser): 
        model.remoteUsers += [.init(peerID: handlePeer.id, name: handlePeer.displayName ?? "")] 
        collectonView.reloadData() 
 
    case closedRemote(userId: String): 
        model.remoteUsers.removeAll(where: { $0.peerID == peerClosed }) 
        collectonView.reloadData() 
 
    default: 
        break 
    } 
}
```

The last method is called when the SDK is ready to provide the user’s video stream and when videos from the peers arrive.

```
func roomClientHandle(_ client: GCoreRoomClient, mediaEvent: GCoreMediaEvent)
```

You’ll use it to render the UI.

```
// To respond to receiving and disabling video/audio streams
func roomClientHandle(_ client: GCoreRoomClient, mediaEvent: GCoreMediaEvent) { 
    switch mediaEvent { 
    case produceLocalVideo(track: RTCVideoTrack): 
        guard let localUser = model.localUser else { return } 
        videoTrack.add(localUser.view) 
        localUser.view.frame = self.localView.bounds 
        localView.addSubview(localUser.view) 
 
    case handledRemoteVideo(videoObject: GCoreVideoStream):  
        guard let user = model.remoteUsers.first(where: { $0.peerID == videoObject.peerId }) else { return } 
        videoObject.rtcVideoTrack.add(user.view) 
 
    default: 
        break 
    } 
}
```

Let _xcode_ fill in the rest of the methods without giving them functionality.

Setup is complete! The project is ready to run on a real device (running on an emulator is not recommended).


## Results


Your app now has a video calling feature. Here is what the implementation of this feature looks like in our [demo application](https://github.com/G-Core/ios-demo-video-calls).


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/prrz4bor4w3xpxlxvfiu.png)


## Developer notes


You can get pixelBuffer from the SDK, which contains an image frame link (received from the camera) so you can do whatever you like with it. To do this, you need to subscribe the controller under `MediaCapturerBufferDelegate` and implement the `mediaCapturerDidBuffer` method. The example below adds a blur before sending the frame to the server:

```
extension ViewController: MediaCapturerBufferDelegate { 
    func mediaCapturerDidBuffer(_ pixelBuffer: CVPixelBuffer) { 
        let image = CIImage(cvPixelBuffer: pixelBuffer).applyingGaussianBlur(sigma: 10) 
        CIContext().render(image, to: pixelBuffer) 
    } 
}
```


## Conclusion


Using the SDK and Gcore services, you can easily and quickly integrate video calling functionality into your application. Users will be pleased; they’re used to video calls in popular services like Instagram, WhatsApp, and Facebook, and now they will see a familiar feature in your application.

You can check out the source code for our project here: [ios-demo-video-calls](https://github.com/G-Core/ios-demo-video-calls). There you can also take a peek at the implementation of other methods, moderator mode, screen preview, etc.

[Mediasoup iOS Client](https://github.com/ethand91/mediasoup-ios-client) was used to implement WebRTC on iOS.

[GcoreVideoCallsSDK](https://github.com/G-Core/ios-video-calls-SDK) is used to connect and interact with the room, as well as to create sockets.

The article is based on the [GcoreVideoCalls](https://github.com/G-Core/ios-demo-video-calls) application.