Home

 / Blog / 

Building an Omegle clone in Flutter

Building an Omegle clone in Flutter

October 21, 202225 min read

Share

Omegle Clone in Flutter | Cover Image

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 post will take you through a step-by-step guide on how to build an Omegle-like app in Flutter using the 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.

Check out our comprehensive guide on Flutter WebRTC. This is how your Omegle clone will look at the end of this tutorial.

Final Screenshot of Flutter Omegle Clone

Prerequisites

Ensure that you have the following requirements:

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:

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

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

Here are some other apps you can build with 100ms flutter SDK -

- Building Zoom clone in Flutter with 100ms SDK
- Building a Clubhouse clone using 100ms in Flutter

Setting up 100ms project

  • Create New App

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

Creating New App on 100ms

  • Next, choose the Video Conferencing template :

Video Conferencing Template For Omegle Clone

  • Click on Set up App and your app is created :

Setting up the App

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

Pre Created Room

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 SDK in any flutter app from scratch. In this app, we will need to set up 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 :

  • flutter pub get
  • flutter run

Hurray! This was super easy.

Now Let's build this from scratch:

  • Start a new flutter project

  • Setting up 100ms SDK:

    hmssdk_flutter: 0.6.0

  • run flutter pub get to install the dependencies

  • 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-feature android:name="android.hardware.camera.autofocus"/>

<uses-permission android:name="android.permission.CAMERA"/>

<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

iOS Permissions

Add the permissions to your Info.plist file:

<key>NSMicrophoneUsageDescription</key>
<string>{YourAppName} wants to use your microphone</string>

<key>NSCameraUsageDescription</key>
<string>{YourAppName} wants to use your camera</string>

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

Now you are ready

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

Setting up functions

  • Initialize a single instance of HMSSDK() . Multiple instances 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 into your required classes.
//Package imports
import 'package:hmssdk_flutter/hmssdk_flutter.dart';
    
class SdkInitializer {
    static HMSSDK hmssdk = HMSSDK();
}
  • We need to run the  build method of hmssdk in the next step as:
await SdkInitializer.hmssdk.build();
  • Create a join function that takes HMSSDK as a parameter:

  • For joining the 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" }

  • role: the role with which you want to join the room
  • user_id: Optional Parameter/Can be any string
  • room_id: This can be found on the room details page in your 100ms dashboard.
  • 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);
  • Now the room can be joined by passing this config object as :
await hmssdk.join(config: config);

Joining the meeting using the join method of HMSSDK

  • Once we have joined the room we need to add a listener to start listening to 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 the room and listening to room updates.

Methods for updates happening in the Room

Let’s set some jargon 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: https://docs.100ms.live/flutter/v2/features/update-listener-enums

  • onTrackUpdate: This is one of the most important methods. Here we receive the track updates for all peers. Tracks will be audio, video, or auxiliary tracks (like screen-share, and 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 the 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 peers 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 the home screen.

  • onRoleChangeRequest: This method gets called when any remote peer requests the local peer to change its 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 the user’s role to the role which has publish permissions.

  • onChangeTrackStateRequest: This is called when a 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 a Database of Rooms

We will need roomId or roomLink to join the room and since users should be able to join any random room from here we are storing some roomIds in the 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 the 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 peers available then you are shown a waiting screen.

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

Firebase Store Schema

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 room and 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)});
    }
}

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;
    }
}

2. Setting up Audio & Video for peers

We get audio, video, and all other updates from the listener which we have attached to 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 peers 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.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 listening to updates

For rendering video we will be using HMSVideoView from HMSSDK package. We just need to pass the track which we have initialized 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 the remote peers. 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 the 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 are mute so we render the screen as :

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

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.

  1. Chat Anonymously

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

Chat Screen on Flutter Zoom Clone

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

Notification when message is received

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

  1. 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:

  • The user leaves the current room
  • 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.

Conclusion

That’s it, give it a try. If you have any questions or suggestions, please let us know.

Hope you guys enjoyed it!

Engineering

Share

Related articles

See all articles