Home
/ Blog /
Building a Zoom clone in FlutterOctober 21, 202220 min read
Share
Today, Zoom is the most popular video and audio conferencing app. From interacting with co-workers to organizing events like workshops and webinars, Zoom is everywhere.
This post will take you through a step-by-step guide on how to build a basic Zoom-like app using Flutter and 100ms' live audio-video SDK in the following way -
Click here to learn more on how to add live interactive video to your product.
By the end of this blog, this is how your app will look like:
Before proceeding, make sure you have the following requirements:
Checkout our comprehensive guide on Flutter WebRTC
Here are some other apps you can build with 100ms flutter SDK -
- Building an Omegle clone in Flutter using 100ms SDK
- Building a Clubhouse clone using 100ms in Flutter
Download the starter app containing all the prebuilt UI from here. Open it in your editor, build and run the app:
The file structure of the starter project looks like this:
main.dart
: The entry point of the app and the screen to get user details before joining the meeting.
meeting.dart
: The video call screen to render all peer's views.
message.dart
: The chat screen sends messages to everyone in the room.
room_service.dart
: A helper service class to fetch the token to join a meeting.
peer_track_node.dart
: A data model class for user details:
class PeerTrackNode { HMSPeer peer; String name; bool isRaiseHand; @observable HMSVideoTrack? track; HMSTrack? audioTrack; PeerTrackNode( {required this.peer, this.track, this.name = "", this.audioTrack, this.isRaiseHand = false});
In the next step, you’ll start setting up your project and initialize 100ms in it.
You’ll need the Token endpoint and App id, so get these credentials from the Developer Section:
Before creating a room, you need to create a new app:
Next, choose the Video Conferencing
template:
Click on Set up App
and your app is created:
Finally, go to Rooms in the dashboard and click on the room pre-created for you:
N.B., Grab the Room Link to use it later to join the room.
Add the 100ms plugins in the pubspec.yaml dependencies as follows:
hmssdk_flutter: 0.7.0
mobx: 2.0.1
flutter_mobx: 2.0.0
mobx_codegen: 2.0.1+3
http: 0.13.3
intl: 0.17.0
Either get it using your IDE to install the plugins or use the below command for that:
flutter pub get
Update target Android version
Update the minimum Android SDK version to 21 or later and compile the SDK version to 32 by navigating to the android/app
directory and updating the build.gradle
:
android {
compileSdkVersion 32
...
defaultConfig{
minSdkVersion 21
...
}
...
}
You will require Recording Audio, Video, and Internet permission in this project as you are focused on the audio/video track in this tutorial.
A track represents either the audio or video that a peer is publishing
Add the permissions 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 to join a room.
You have to implement some new classes over the current SDK, this will help you interact with the SDK easily. So start by adding the following file in the setup subfolder in lib:
The above class provides you with a lot of methods over the HMS SDK which will be later used here.
The above contains an abstract class providing several methods to build a more advanced app. It uses the help of the meeting_store.dart to interact with the HMS SDK.
Note: Make sure to generate the class using build\_runner
and mobx\_codegen
cd zoom
flutter packages pub run build_runner build --delete-conflicting-outputs
A room is a basic object that 100ms SDK returns on completing a connection. This contains connections to peers, tracks, and everything you need to view a live audio-video app. To join a room, you require an HMSConfig
object, that’ll have the following fields:
First, you can get userName and roomLink fields, by using the TextField widget to get the userName and room information using the usernameTextEditingController
and roomLinkTextEditingController
TextEditingController :
You can then pass this info in meeting.dart file on onPressed event:
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Meeting(
name: usernameTextEditingController.text,
roomLink: roomLinkTextEditingController.text,
)),
);
},
child: const Text(
"Join",
style: TextStyle(fontSize: 20),
)),
...
)
Now move to meeting.dart file and you will find it taking 2 parameters name and roomLink which we have passed from main.dart file:
class Meeting extends StatefulWidget {
final String name, roomLink;
const Meeting({Key? key, required this.name, required this.roomLink})
: super(key: key);
@override
_MeetingState createState() => _MeetingState();
}
Next, in meeting.dart add the following code in your _meetingState:
class _MeetingState extends State<Meeting> with WidgetsBindingObserver {
//1
late MeetingStore _meetingStore
@override
void initState() {
super.initState()
WidgetsBinding.instance!.addObserver( this )
//2
_meetingStore = MeetingStore()
//3
initMeeting()
}
//4
initMeeting() async {
bool ans = await _meetingStore.join( widget.name, widget.roomLink )
if ( !ans )
{
const SnackBar( content: Text( "Unable to Join" ));
Navigator.of( context ).pop()
}
_meetingStore.addUpdateListener()
}
...
}
In the above code:
Build and run your app. Now, you have joined the meeting and moved to the meet page.
This will activate the onJoin event, and your app will bring an update from the 100ms SDK.
✅ If successful, the function onJoin(room: HMSRoom) method of HMSUpdateListener will be invoked with details about the room containing in the HMSRoom object.
❌ If failure, the fun onError(error: HMSException) method will be invoked with failure reason.
A peer is an object returned by 100ms SDKs that hold the information about a user in the meeting - name, role, track, raise hand, etc.
So, update the build
method of your meeting
by wrapping it by Observer
to rebuild the method on any changes, like below:
Flexible(
child: Observer(
builder: (_) {
//1
if (_meetingStore.isRoomEnded) {
Navigator.pop( context, true )
}
//2
if (_meetingStore.peerTracks.isEmpty) {
return const Center(child: Text('Waiting for others to join!'));
}
//3
ObservableList < PeerTrackNode > peerFilteredList = _meetingStore.peerTracks;
//4
return videoPageView(peerFilteredList);
),
),
In the above code, you did the following:
After setting up the UI for rendering we need to call HMSVideoView() and pass track which will be provided by the peerFilterList in videoTile widget.
SizedBox(
width: size,
height: size,
child: ClipRRect(
borderRadius: BorderRadius.circular( 10 ),
child: ( track.track != null && isVideoMuted )
? HMSVideoView(
track: track.track as HMSVideoTrack,
)
: Container(
width: size,
height: size,
color: Colors.black,
child: Center(
child: CircleAvatar(
radius: 50,
backgroundColor: Colors.green,
child: track.name.contains( " " )
? Text(
( track.name.toString().substring( 0, 1 ) +
track.name.toString().split( " " )[ 1 ]
.substring( 0, 1 ) ).toUpperCase(),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700 ),
)
: Text( track.name
.toString()
.substring( 0, 1 )
.toUpperCase() ),
),
))),
...
In the above code, you check if the video is on for the user or not if yes then render the video using HMSVideoView() otherwise show the Initial of the user name.
You can also pass other parameters to HmsVideoView widget like mirror view, match parent, and viewSize.
For checking if the user video is on or off we do the following:
ObservableMap<String, HMSTrackUpdate> trackUpdate = _meetingStore.trackStatus;
if ((trackUpdate[peerTracks[index].peerId]) == HMSTrackUpdate.trackMuted) {
return true;
} else {
return false;
}
For setting a username we can do:
Text(
track.name,
style: const TextStyle(fontWeight: FontWeight.w700),
),
To display the screen share tile update videoPageView function:
if (_meetingStore.curentScreenShareTrack != null) {
pageChild.add(RotatedBox(
quarterTurns: 1,
child: Container(
margin:const EdgeInsets.only(bottom: 0, left: 0, right: 100, top: 0),
child: Observer(builder: (context) {
return HMSVideoView(track: _meetingStore.curentScreenShareTrack as HMSVideoTrack);
})),
));
}
In the above code:
Now build an app and run it when you do screen-share you can see it below:
For hand raised follow the following code:
IconButton(
icon: Image.asset('assets/raise_hand.png'),
//1
color: isRaiseHand ? Colors.amber.shade300 : Colors.grey),
onPressed: () {
setState(() {
//2
isRaiseHand = !isRaiseHand;
});
//3
_meetingStore.changeMetadata();
},
),
In the above code:
isRaiseHand
boolean local variable to check and update the Image color accordingly.onPressed
event to toggle the isRaiseHand variable._meetingStore
to inform all users.To get other peers hand raise info update videoViewGrid function as follows:
peerFilteredList[index].isRaiseHand
In the above code:
peerFilteredList
array elements contain variable isRaiseHand, which will get updated on metadata change in peerOperation function inside meeting_store.dart
.Now build app and run when you raise hand you can see it on video tiles as below:
To mute or unmute your mic, update your mic button as follows:
//1
Observer( builder: ( context ) {
return CircleAvatar(
backgroundColor: Colors.black,
child: IconButton(
//2
icon: _meetingStore.isMicOn
? const Icon( Icons.mic )
: const Icon( Icons.mic_off ),
onPressed: () {
//3
_meetingStore.switchAudio()
},
color: Colors.blue,
...
Here you updated the button as follows:
isMicOn
boolean to check and update the Icon accordingly.onPressed
event to toggle the local peer mic using the _meetingStore
.// To toggle the camera, update your camera button as follow:
//1
Observer(builder: (context) {
return CircleAvatar(
backgroundColor: Colors.black,
child: IconButton(
//2
icon: _meetingStore.isVideoOn
? const Icon(Icons.videocam)
: const Icon(Icons.videocam_off),
onPressed: () {
//3
_meetingStore.switchVideo();
},
color: Colors.blue),
...
}
Here you updated the button as follows:
isVideoOn
boolean method to check and update the Icon accordingly.onPressed
event to toggle the local peer video using the _meetingStore
.To switch the camera, update your switch camera button as follow:
IconButton(
icon: const Icon( Icons.cameraswitch ),
onPressed: () {
//1
_meetingStore.switchCamera()
},
color: Colors.blue,
),
Here you updated the button as follows:
onPressed
event to switch the camera using the _meetingStore.switchCamera()
.To leave the room update the leave room button as follows:
onPressed: () {
_meetingStore.leave()
Navigator.pop( context )
}
Here, you are using the MeetingStore object to leave the room.
To add the feature to chat with everyone in a meeting you need to update your message widget.
First, accept the MeetingStore object in your message constructor from the meeting.dart to get the meeting details as below:
final MeetingStore meetingStore;
const Message({required this.meetingStore, Key? key}) : super(key: key);
Next, store this object inside your _ChatViewState as below:
late MeetingStore _meetingStore;
@override
void initState() {
super.initState();
_meetingStore = widget.meetingStore;
}
Next, update the body of the scaffold widget to render the messages as below:
Expanded(
//1
child: Observer(
builder: (_) {
//2
if (!_meetingStore.isMeetingStarted) {
return const SizedBox();
}
//3
if (_meetingStore.messages.isEmpty) {
return const Center(child: Text('No messages'));
}
//4
return ListView.separated(
itemCount: _meetingStore.messages.length,
itemBuilder: (itemBuilder, index) {
return Container(
padding: const EdgeInsets.all(5.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child:
//5
Text(_meetingStore
.messages[index].sender?.name ??
"",
style: const TextStyle(
fontSize: 10.0,
color: Colors.black,
fontWeight: FontWeight.bold),
),
),
//6
Text(formatter.format(
_meetingStore.messages[index].time),
style: const TextStyle(
fontSize: 10.0,
color: Colors.black,
fontWeight: FontWeight.w900),
)
],
),
const SizedBox(
height: 10.0,
),
Text(
//7
_meetingStore
.messages[index].message
.toString(),
style: const TextStyle(
fontSize: 14.0,
color: Colors.black,
fontWeight: FontWeight.w300),
),
],
),
);
},
separatorBuilder: (BuildContext context, int
index) {
return const Divider();
},
);
},
),
),
...
In the above code:
After this, you can see the incoming messages, however, this will not allow you to send a message yet.
So edit the onTap event of the Send button of the message as below:
// 1
String message = messageTextController.text;
if (message.isEmpty) return;
//2
_meetingStore.sendBroadcastMessage(message);
//3
messageTextController.clear();
Here you did the following:
messageTextController
TextEditingController.sendBroadcastMessage
of the _meetingStore
object to Send the message in the meeting.messageTextController
is clear after the message is sent.Build and run your app. Now, you can send and receive messages in the meeting
Finally, you have learned the essential functionality and are prepared to use these skills in your projects.
You can find the starter and final project here. In this tutorial, you discovered about 100ms and how you can efficiently use 100ms to build a zoom Application. Yet, this is only the opening, you can discover more about switching roles, changing tracks, adding peers, screen sharing from the device, different types of chats(peer-to-peer or group chat), and much more functionality.
We hope you enjoyed this tutorial. Feel free to reach out to us if you have any queries. Thank you!
Engineering
Share
Related articles
See all articles