Home
/ Blog /
Building an Omegle clone in FlutterOctober 21, 202225 min read
Share
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.
Check out our comprehensive guide on Flutter WebRTC. This is how your Omegle clone will look at the end of this tutorial.
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:
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
Before creating a room, we need to create a new app :
Finally, go to Rooms in the dashboard and click on the room pre-created for you
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 :
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
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" />
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.
//Package imports
import 'package:hmssdk_flutter/hmssdk_flutter.dart';
class SdkInitializer {
static HMSSDK hmssdk = HMSSDK();
}
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" }
HMSConfig config = HMSConfig(authToken: token from above endpoint,userName: Any username);
await hmssdk.join(config: config);
Joining the meeting using the join method of HMSSDK
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.
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.
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.
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.
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:
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:
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.
SdkInitializer.hmssdk.switchVideo(isOn: isLocalVideoOn)
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.
There is also an option to chat anonymously in the application.
Whenever we receive a message we can show a small dot over the Messages icon.
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
In the application switch room involves a series of leave room and join room function call. Whenever user clicks on switch room button:
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 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