Building an Omegle clone in Flutter using 100ms SDK

Building an Omegle clone in Flutter using 100ms SDK

The Internet is full of cool people; Omegle lets you meet them. When you use Omegle, it picks someone else at random so you can have a one-on-one live audio/video/text chat with them.

This content was originally published - HERE

This post will take you through a step by step guide on how to build an Omegle like app in Flutter using 100ms Live Audio Video package.

Features in Omegle

  1. Join the room anonymously.
  2. Can Share audio and video.
  3. Can chat anonymously
  4. Can switch the room.

This is how your Omegle clone will look like at the end of this tutorial.

Image description

Prerequisites

Ensure that you have the following requirements:

  1. Flutter v2.0.0 or later (stable)
  2. 100ms Account (Create 100ms Account)

This tutorial assumes you have some prior knowledge of Flutter. If you are new to Flutter, please go through the official documentation(https://flutter.dev/).

100ms is a real-time audio-video conferencing platform that enables you to quickly build a fully customizable audio-video engagement experience. It is quick to integrate with native/cross mobile and web SDKs.

It provides you with the following features:

  1. Production-ready pre-built templates to use.
  2. Ability to build large rooms with a capacity of 100 participants with audio & video on.
  3. Support for dynamic roles, disconnection handling, and bandwidth management.

100ms SDK itself handles cases like headphone switching , phone call interruptions etc. on it’s own so no need to write extra code for that.

Setting up 100ms project

Create New App Before creating a room, we need to create a new app :

Image description

Next, choose the Video Conferencing template :

Image description

Click on Set up App and your app is created :

Image description

Finally, go to Rooms in the dashboard and click on room pre-created for you

Image description

QuickStart

Start by cloning the code from here : https://github.com/pushpam5/Omegle-Clone-100ms.git

We will see the step by step implementation of the app and also how to use 100ms flutter SDKin any flutter app from scratch. In this app we will need to setup the firebase for firestore which we are using as our database. The setup steps can be found here:

https://firebase.google.com/docs/flutter/setup

We need to put the google-services.json file in the android/app folder.

For running the project :

  1. flutter pub get
  2. flutter run

Hurray! This was super easy.

Now Let's build this from scratch:

  1. Start a new flutter project
  2. Setting up 100ms SDK:

hmssdk_flutter: ^0.6.0

In the pubspec.yaml file under dependencies add:

  1. run flutter pub get to install the dependencies
  2. Add Permissions

We will require Recording Audio, Video and Internet permission in this project as we are focused on the audio and video track in this tutorial.

A track represents either the audio or video that a peer is publishing

Android Permission

Add the permissions outside your application tag in your AndroidManifest file (android/app/src/main/AndroidManifest.xml):

<uses-feature android:name="android.hardware.camera"/>                         <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>             <uses-feature android:name="android.hardware.camera.autofocus"/>
<uses-permission android:name="android.permission.CAMERA"/>

iOS Permissions

Add the permissions to your Info.plist file:

<key>NSCameraUsageDescription</key>                                           <string>{YourAppName} wants to use your camera</string><key>NSLocalNetworkUsageDescription</key>                                     <string>{YourAppName} App wants to use your local network</string><key>NSMicrophoneUsageDescription</key>                                     <string>{YourAppName} wants to use your microphone</string>

Now you are ready ✨

Let’s dive deeper for setting up the functions. These steps are common to any application using 100ms SDK.

Setting up functions

  1. Initialize a single instance of HMSSDK() .Multiple instance can also be created but as our app only needs a single instance so we will be focusing on that. You can also inject HMSSDK dependency to your required classes.
//Package imports
import 'package:hmssdk_flutter/hmssdk_flutter.dart';

class SdkInitializer {
  static HMSSDK hmssdk = HMSSDK();
}
  1. We need to run the build method of hmssdk in the next step as:
SdkInitializer.hmssdk.build();
  1. Create a join function which takes HMSSDK as parameter:
  2. For joining room we need auth token. For this we need to make http post request to : https://prod-in.100ms.live/hmsapi/decoder.app.100ms.live/api/token
Body parameters :
body: {
      'user_id': "user",
      'room_id':room_Id,
      'role':"host"
    }
  1. role : the role with which you want to join the room
  2. user_id : Optional Parameter/Can be any string
  3. room_id : This can be found from the room details page in your 100ms dashboard.
  1. After getting the auth token from the above endpoint we need to create an HMSConfig Object to be passed in the join method of HMSSDK.
HMSConfig config = HMSConfig(authToken: token from above endpoint, userName: Any username);
  1. Now the room can be joined by passing this config object as :
await hmssdk.join(config: config);
  1. Once we have joined the room we need to add listener to start Listening the SDK updates.
hmssdk.addUpdateListener(listener:HMSUpdateListener);

We need to pass the HMSUpdateListener instance in the listener parameter from wherever we want to listen to the updates from SDK. General implementation involves implementing HMSUpdateListener in the class where we are maintaining the state of the application .

We have completed the 100ms SDK setup for joining room and listening to room updates.

Methods for updates happening in the Room

Let’s set some jargons here

Local peer : You Remote peers : All peers in the room excluding you

– onJoin : This is the method where we get the join update for the local peer. We get the local peer object in this method. – onPeerUpdate : In this method we receive the updates for all the remote Peers like when a new peer joins the room or leaves the room. In this we receive HMSPeerUpdate object which is an enum. You can find more info about enum here: docs.100ms.live/flutter/v2/features/update-.. – onTrackUpdate: This is one of the most important method. Here we receive the track updates for all peers. Tracks will be audio, video or auxiliary tracks (like screen-share, custom audio/video played from a file). – onMessage: In this method we receive the chat messages update from remote peers. – onError: This is called when errors occur from the SDK side. – onUpdateSpeakers: In this method we receive updates about the current speaker. This can be used to highlight who is speaking currently. – onReconnecting: This method gets called when the app loses connection to internet & is trying to reconnect now. – onReconnected : This method gets called when the user gets connected after reconnection. One important thing to note here is that the user will receive the update in OnJoin for local peer and in onPeerUpdate for remotePeers. – onRemovedFromRoom: This method gets called when any remote peer removes the local peer from the room. Generally used to pop the user back to home-screen. – onRoleChangeRequest: This method gets called when any remote peer requests the local peer to change it’s role. Generally used in the scenario when you want to invite the user to speak and the user's current role does not have that permission. So, this can be achieved by changing user’s role to the role which has publish permissions. – onChangeTrackStateRequest : This is called when remote peer asks you, the local peer to mute/unmute your audio/video track. – onRoomUpdate: This method gets called when the user gets room updates like Recording Started/Stopped , RTMP/HLS Streaming Start/Stop updates, etc.

Setting up Database of Rooms

We will need roomId or roomLink to join the room and since user should be able to join any random room from here we are storing some roomId’s in database along with the number of users in the room. This is not the best way as all the rooms may get occupied at a time. So the best way is to generate room dynamically.

More details regarding creating a room from api can be found here : https://docs.100ms.live/server-side/v2/features/room

For the sake of simplicity, we have created rooms & stored roomIds in Firebase.

  1. First, we make a request from the app looking for rooms with a single user and assign it to new user.
  2. If there are no rooms with a single user then we assign a new room to that user.
  3. If all the rooms are occupied then we should be able to dynamically create rooms.
  4. Omegle assumes that every peer should be mapped with another peer and if there are no peer’s available then you are shown a waiting screen.

Here is a basic schema of the Firebase Store of roomIds . Feel free to suggest your database strategies we will be more than happy to hear from you all.

Image description

Implementing features

  1. Joining a Room

There are two ways to join a room by using room-link or roomId. Since here we need roomId or roomUrl. We are using Firebase to store all the rooms so we'll fetch the roomId use it to join the room.

The Firebase services can be found in services.dart:

class FireBaseServices {
  static late QuerySnapshot _querySnapshot;
  static final _db = FirebaseFirestore.instance;

  //Function to get Rooms
  static getRooms() async {
    //Looking for rooms with single user
    _querySnapshot = await _db
        .collection('rooms')
        .where('users', isEqualTo: 1)
        .limit(1)
        .get();
    //Looking for empty rooms
    if (_querySnapshot.docs.isEmpty) {
      _querySnapshot = await _db
          .collection('rooms')
          .where('users', isEqualTo: 0)
          .limit(1)
          .get();
    }
    await _db
    .collection('rooms')
    .doc(_querySnapshot.docs[0].id)
    .update({'users': FieldValue.increment(1)});
    return _querySnapshot;
  }

  //Function to leave room basically reducing user count in the room
  static leaveRoom() async {
    await _db
        .collection('rooms')
        .doc(_querySnapshot.docs[0].id)
        .update({'users': FieldValue.increment(-1)});
  }
}

Image description

When the user clicks JoinRoom, the joinRoom function is invoked and it initializes the 100ms SDK, attaches update listeners & joins the room.

//Handles room joining functionality
  Future<bool> joinRoom() async {
    setState(() {
      _isLoading = true;
    });

    //The join method initialize sdk,gets auth token,creates HMSConfig and helps in joining the room
    bool isJoinSuccessful = await JoinService.join(SdkInitializer.hmssdk);

    if (!isJoinSuccessful) {
      return false;
    }
    _dataStore = UserDataStore();

    //Here we are attaching a listener to our DataStoreClass
    _dataStore.startListen();
    setState(() {
      _isLoading = false;
    });

    return true;
  }

Let’s look into the join function from JoinService.dart

class JoinService {

  //Function to get roomId stored in Firebase
  static Future<String> getRoom() async {
    QuerySnapshot? _result;
    await FireBaseServices.getRooms().then((data) {
      _result = data;
    });
    return _result?.docs[0].get('roomId');
  }

  //Function to join the room
  static Future<bool> join(HMSSDK hmssdk) async {
    String roomUrl = await getRoom();
    Uri endPoint = Uri.parse("https://prod-in.100ms.live/hmsapi/decoder.app.100ms.live/api/token");
    http.Response response = await http.post(endPoint, body: {
      'user_id': "user",
      'room_id':roomUrl,
      'role':"host"
    });

    var body = json.decode(response.body);
    if (body == null || body['token'] == null) {
      return false;
    }

    //We use the token from above response to create the HMSConfig Object which
    //we need to pass in the join method of hmssdk
    HMSConfig config = HMSConfig(authToken: body['token'], userName: "user");

    await hmssdk.join(config: config);
    return true;
  }
}
  1. Setting up Audio & Video for peers

We get audio, video and all other updates from the listener which we have attached on our HMSSDK instance. Among the various update methods available, we will be using only the following 2 methods for our app:

OnPeerUpdate - to handle when peer join/ leave a room OnTrackUpdate - to get audio/video updates

class UserDataStore extends ChangeNotifier implements HMSUpdateListener {

  //To store remote peer tracks and peer objects
  HMSTrack? remoteVideoTrack;
  HMSPeer? remotePeer;
  HMSTrack? remoteAudioTrack;
  HMSVideoTrack? localTrack;
  bool _disposed = false;
  List<Message> messages = [];
  late HMSPeer localPeer;
  bool isNewMessage = false;

  //To dispose the objects when user leaves the room
  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }

  //Method provided by Provider to notify the listeners whenever there is a change in the model
  @override
  void notifyListeners() {
    if (!_disposed) {
      super.notifyListeners();
    }
  }

  //Method to attach listener to sdk
  void startListen() {
    SdkInitializer.hmssdk.addUpdateListener(listener: this);
  }

  //Method to listen to local Peer join update
  @override
  void onJoin({required HMSRoom room}) {
    for (HMSPeer each in room.peers!) {
      if (each.isLocal) {
        localPeer = each;
        break;
      }
    }
  }

  // Method to listen to peer Updates we are only using peerJoined and peerLeft updates here
  @override
  void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) {
    switch (update) {
      //To handle when peer joins
      //We are setting up remote peers audio and video track here.
      case HMSPeerUpdate.peerJoined:
        messages = [];
        remotePeer = peer;
        isNewMessage = false;
        remoteAudioTrack = peer.audioTrack;
        remoteVideoTrack = peer.videoTrack;
        break;
      // Setting up the remote peer to null so that we can render UI accordingly
      case HMSPeerUpdate.peerLeft:
        messages = [];
        isNewMessage = false;
        remotePeer = null;
        break;
      case HMSPeerUpdate.audioToggled:
        break;
      case HMSPeerUpdate.videoToggled:
        break;
      case HMSPeerUpdate.roleUpdated:
        break;
      case HMSPeerUpdate.metadataChanged:
        break;
      case HMSPeerUpdate.nameChanged:
        break;
      case HMSPeerUpdate.defaultUpdate:
        break;
    }
    notifyListeners();
  }

  //Method to get Track Updates of all the peers
  @override
  void onTrackUpdate(
      {required HMSTrack track,
      required HMSTrackUpdate trackUpdate,
      required HMSPeer peer}) {
    switch (trackUpdate) {
      //Setting up tracks for remote peers
      //When a track is added for the first time
      case HMSTrackUpdate.trackAdded:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!track.peer!.isLocal) remoteAudioTrack = track;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!track.peer!.isLocal)
            remoteVideoTrack = track;
          else
            localTrack = track as HMSVideoTrack;
        }
        break;
      //When a track is removed i.e when remote peer lefts we get 
    //trackRemoved update
      case HMSTrackUpdate.trackRemoved:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!track.peer!.isLocal) remoteAudioTrack = null;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!track.peer!.isLocal)
            remoteVideoTrack = null;
          else
            localTrack = null;
        }
        break;
      //Case when a remote peer mutes audio/video
      case HMSTrackUpdate.trackMuted:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!track.peer!.isLocal) remoteAudioTrack = track;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!track.peer!.isLocal) {
            remoteVideoTrack = track;
          } else {
            localTrack = null;
          }
        }
        break;
      //Case when a remote peer unmutes audio/video
      case HMSTrackUpdate.trackUnMuted:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!track.peer!.isLocal) remoteAudioTrack = track;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!track.peer!.isLocal) {
            remoteVideoTrack = track;
          } else {
            localTrack = track as HMSVideoTrack;
          }
        }
        break;
      case HMSTrackUpdate.trackDescriptionChanged:
        break;
      case HMSTrackUpdate.trackDegraded:
        break;
      case HMSTrackUpdate.trackRestored:
        break;
      case HMSTrackUpdate.defaultUpdate:
        break;
    }
    notifyListeners();
  }

  //Method to listen to remote peer messages
  @override
  void onMessage({required HMSMessage message}) {
    Message _newMessage =
        Message(message: message.message, peerId: message.sender!.peerId);
    messages.add(_newMessage);
    isNewMessage = true;
    notifyListeners();
  }

  //Method to listen to Error Updates
  @override
  void onError({required HMSException error}) {}

  //Method to get the list of current speakers
  @override
  void onUpdateSpeakers({required List<HMSSpeaker> updateSpeakers}) {}

  //Method to listen when the reconnection is successful
  @override
  void onReconnected() {}

  //Method to listen while reconnection
  @override
  void onReconnecting() {}

  //Method to be listened when remote peer remove local peer from room
  @override
  void onRemovedFromRoom(
      {required HMSPeerRemovedFromPeer hmsPeerRemovedFromPeer}) {}

  //Method to listen to role change request
  @override
  void onRoleChangeRequest({required HMSRoleChangeRequest roleChangeRequest}) {}

  //Method to listen to room updates
  @override
  void onRoomUpdate({required HMSRoom room, required HMSRoomUpdate update}) {}

  //Method to listen to change track request
  @override
  void onChangeTrackStateRequest(
      {required HMSTrackChangeRequest hmsTrackChangeRequest}) {}


}

We have successfully set up the methods for audio and video now let’s see how to use them to render video on UI. The code for this can be found in user_screen.dart. We are using providers for listening to the audio, video toggle changes. We are using context.select to listen to specific changes corresponding to audio, video, peer and track updates :

final _isVideoOff = context.select<UserDataStore, bool>(
        (user) => user.remoteVideoTrack?.isMute ?? true);
final _isAudioOff = context.select<UserDataStore, bool>(
        (user) => user.remoteAudioTrack?.isMute ?? true);
final _peer = context.select<UserDataStore,HMSPeer?>(
        (user) => user.remotePeer);
final track = context
        .select<UserDataStore, HMSTrack?>(
        (user) => user.remoteVideoTrack);

For rendering video we will be using HMSVideoView from HMSSDK package. We just need to pass the track which we have initialised above in this as :

HMSVideoView(track: track as HMSVideoTrack, matchParent: false) HMSVideoView is just like any other widget in flutter and super easy to use. More details regarding HMSVideoView can be found here: https://docs.100ms.live/flutter/v2/features/render-video

In this way we can render the video for remote peer. There are some more functions used in the application let’s discuss them one by one:

– switchAudio: This function is used to switch local peer audio i.e if you want to mute yourself then just call this method with isOn parameter as true.

SdkInitializer.hmssdk.switchAudio(isOn: isLocalAudioOn)

isOn seems to be confusing so just keep in mind that we need to pass the current audioStatus in isOn parameter.

– switchVideo: This is similar to switchAudio function.This is used to switch local peer video i.e if you want to turnOff the video then just pass isOn parameter as true.

SdkInitializer.hmssdk.switchVideo(isOn: isLocalVideoOn)

– switchCamera: This is used to switch between front or rear camera.

SdkInitializer.hmssdk.switchCamera();

If the user’s audio and video is mute so we render screen as :

Image description

If the user’s video is mute so we render screen as :

Image description

These are the loading screens based on if you are joining a room/switching a room and if the room does not have any other peers respectively.

Image description

Image description

  1. Chat Anonymously

There is also an option to chat anonymously in the application.

Image description

Whenever we receive a message we can show a small dot over the Messages icon.

Image description

Whenever we receive message from remote peer the onMessage method is called where we receive an Object of HMSMessage. Here we have created a custom class Message for the application as we only need direct messaging. The Message class looks like:

class Message {
  String peerId;
  String message;

  Message({required this.message, required this.peerId});
}

So in the onMessage we are converting HMSMessage to Message object and adding in our messages list.

@override
  void onMessage({required HMSMessage message}) {
    Message _newMessage =
        Message(message: message.message, peerId: message.sender!.peerId);
    messages.add(_newMessage);
    isNewMessage = true;
    notifyListeners();
  }

For sending message we are using sendBroadcastMessage method as :

SdkInitializer.hmssdk.sendBroadcastMessage(message: messageController.text);

Now let’s move to the last feature of our application, the ability to switch rooms

4. Switch Room

In the application switch room involves a series of leave room and join room function call. Whenever user clicks on switch room button:

  1. The user leaves the current room
  2. The user joins another room

For leaving the room the leave function of HMSSDK is used as :

SdkInitializer.hmssdk.leave();

We also update the Firebase to maintain correct roomState. The join method is same as discussed above.

bool roomJoinSuccessful = await JoinService.join(SdkInitializer.hmssdk);

The complete switchRoom function can be found below :

Future<void> switchRoom() async {
    setState(() {
      _isLoading = true;
    });
    SdkInitializer.hmssdk.leave();
    FireBaseServices.leaveRoom();
    bool roomJoinSuccessful = await JoinService.join(SdkInitializer.hmssdk);
    if (!roomJoinSuccessful) {
      Navigator.pop(context);
    }
    setState(() {
      _isLoading = false;
    });
  }

So the user leaves the room and then joins another room whenever the switchRoom button(the center red button) is pressed.

That’s it, give yourself a try.Any sort of question, suggestion please let us know.

Hope you guys enjoyed...