Building Zoom clone in Flutter with 100ms SDK
Nilay Jayswal
Posted on January 19, 2022
Today, Zoom is the most popular video and audio conferencing app. From interacting with co-workers to organising events like workshops and webinars, Zoom is everywhere.
This content was originally published - HERE
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 -
- Add 100ms to a Flutter app
- Join a room
- Leave a room
- Show video tiles with the user’s name
- Show Screenshare tile
- hand Raised
- Mute/Unmute
- Camera off/on
- Toggle Front/Back camera
- Chatting with everyone in the room
By the end of this blog, this is how your app will look like:
Before proceeding, make sure you have the following requirements:
- Flutter v2.0.0 or later (stable)
- 100ms Account (Create 100ms Account)
Getting started
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 peers view. -
message.dart
: The chat screen to send messages to everyone in the room. -
room_service.dart
: A helper service class to fetch the token to join a meeting. -
peerTrackNode.dart
: A data model class for user details:
class PeerTracKNode {
String peerId;
String name;
@observable
HMSTrack? track;
HMSTrack? audioTrack;
PeerTracKNode({
required this.peerId,
this.track,
this.name = "",
this.audioTrack,
});
In the next step, you’ll start setting up your project and initialise 100ms in it.
Setting up project
Get the Access Credentials
You’ll need the Token endpoint and App id, so get these credentials from the Developer Section:
Create New App
Before creating a room, you need to create a new app:
Next, choose the Video Conferencing
template:
Click on Set up App
and and your app is created:
Room
Finally, go to Rooms in the dashboard and click on room pre-created for you:
N.B., Grab the Room Link to use it later to join the room.
Add 100ms to your Flutter app
Add the 100ms plugins in the pubspec dependencies as follows:
hmssdk_flutter: ^0.5.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 Android SDK version to 21 or later by navigating to the android/app
directory and updating the build.gradle
:
defaultConfig{
minSdkVersion 21
...
}
Add Permissions
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
Android Permissions
Add the permissions in your AndroidManifest file (android/app/src/main/AndroidManifest.xml
):
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera"/>
<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>NSMicrophoneUsageDescription</key>
<string>{YourAppName} wants to use your microphone</string>
<key>NSLocalNetworkUsageDescription</key>
<string>{YourAppName} App wants to use your local network</string>
Now you are ready to join a room.
Implement Listener
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:
100ms-flutter/meeting_store.dart at main · 100mslive/100ms-flutter (github.com)
The above class provides you with a lot of methods over the HMS SDK which will be later used here.
100ms-flutter/hms_sdk_interactor.dart at main · 100mslive/100ms-flutter (github.com)
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.
100ms-flutter/HmsSdkManager.dart at main · 100mslive/100ms-flutter (github.com)
The meeting_store
file is to interact with HMSSDKInteractor.
N.B., Make sure to generate the class using build_runner and mobx_codegen:
cd zoom
flutter packages pub run build_runner build --delete-conflicting-outputs
Join Room
A room is a basic object that 100ms SDK returns on a 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:
userName: A name shown to other peers in a room.
roomLink: A room link, that was generated earlier while creating the room.
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.startListen();
}
...
}
In the above code:
- Created a late instance of MeetingStore that will get initialise in initState.
- Initialize the MeetingStore instance for observing the changes.
- Calling joinMeeting method from the initState to join the meeting
- initMeeting: Here you are using the _meetingStore object to join the meeting. If joined successfully, then you are starting to listen to the changes in the meeting.
Build and run your app. Now, you have joined the meeting and move to meeting page.
This will activate the onJoin event, and your app will be 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.
Render the Peers
A peer is an object returned by 100ms SDKs that hold the information about a user in 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:
- isRoomEnded: If the room gets ended then it will take the user to the home screen.
- peerTracks.isEmpty: If no one has joined the room then it shows a message to the user.
- peerFilteredList is an ObservableList is user get added or removed then it will notify the UI to change it.
- videoPageView: It is a function to render multiple peers videos on screen. (UI implementation).
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!,
)
: 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 of the user or not if yes then render the video using HMSVideoView() otherwise show Initial of user name.
You can also pass other parameters to HmsVideoView widget like mirror view, match parent and viewSize.
For checking if user video is on or off we do following:
ObservableMap<String, HMSTrackUpdate> trackUpdate = _meetingStore.trackStatus;
if((trackUpdate[peerTracks[index].peerId]) == HMSTrackUpdate.trackMuted){
return true;
}else{
return false;
}
For getting a username we have call:
Text(
track.name,
style: const TextStyle(fontWeight: FontWeight.w700),
),
Screen share Tile
To display the screenshare tile update videoPageView function:
if (_meetingStore.screenShareTrack != 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.screenShareTrack!);
})),
));
}
In the above code:
- screenShareTrack: _meetingStore.screenShareTrack contain track for screenshare if it is null then no one is sharing the screen otherwise it will return track.
- rotatedBox: To match the screenshare ratio with the mobile device screen.
- HMSVideoView will render the screen by using _meetingStore.screenShareTrack as a track parameter.
Now build app and run when you do screenshare you can see it like below:
Hand Raised
For hand raised follow the follwing 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:
- Used the
isRaiseHand
boolean local variable to check and update the Image colour accordingly. - Updated the
onPressed
event to toggle the isRaiseHand variable. - Toggle the raiseHand metadata using the
_meetingStore
to inform all users.
To get other peers hand raise info update videoViewGrid function as follow:
//1
ObservableList<HMSPeer> peers = _meetingStore.peers;
//2
HMSPeer peer = peers[peers.indexWhere(
(element) => element.peerId == peerTracks[index].peerId)];
//3
if(peer.metadata.toString() == "{\"isHandRaised\":true}"){
return true;
}
else{
return false;
}
In the above code:
-
_meetingStore.peers
will return all the information about the peers in form of list. - We will find HMSPeer object from peers list by comparing peerId.
- Check metadata of peer if it is equal to {"isHandRaised":true} then return true else false.
Now build app and run when you raise hand you can see it on video tiles as below:
Mute/ Unmute
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:
- Wrapped the button with the Observer so that you can rebuild it on change of mic status.
- Use
isMicOn
boolean to check and update the Icon accordingly. - Updated the
onPressed
event to toggle the local peer mic using the_meetingStore
.
Camera Toggle
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:
- Wrapped the button with the Observer so that you can rebuild it on change of camera status.
- Used the
isVideoOn
boolean method to check and update the Icon accordingly. - Updated the
onPressed
event to toggle the local peer video using the_meetingStore
.
Switch between Front/Back Camera
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:
- Updated the
onPressed
event to switch the camera using the_meetingStore.switchCamera()
.
Leave Room
To leave the room update the leave room button as follow:
onPressed: () {
_meetingStore.leave();
Navigator.pop(context);
}
Here, you are using the MeetingStore object to leave the room.
Chat
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 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: Text(
//5
_meetingStore
.messages[index].sender?.name ??
"",
style: const TextStyle(
fontSize: 10.0,
color: Colors.black,
fontWeight: FontWeight.bold),
),
),
Text(
//6
_meetingStore
.messages[index].time
.toString(),
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:
- Observer is used to display the changes.
- Return the empty box if the meeting hasn’t started.
- Displaying "No messages" text if there are no messages.
- Rendering the messages as a List get updated.
- Displaying the sender's peer name.
- Displaying the DateTime of the message.
- Displaying the message.
After this, you can see the incoming messages, however, this willl not allow 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
DateTime currentTime = DateTime.now();
final DateFormat formatter =
DateFormat('d MMM y hh:mm:ss a');
//3 _meetingStore.sendBroadcastMessage(message);
// 4
_meetingStore.addMessage(HMSMessage(
sender: _meetingStore.localPeer!,
message: message,
type: "chat",
time: formatter.format(currentTime),
hmsMessageRecipient: HMSMessageRecipient(
recipientPeer: null,
recipientRoles: null,
hmsMessageRecipientType:
HMSMessageRecipientType.BROADCAST),
));
messageTextController.clear();
Here you did the following:
- Saving the message using the
messageTextController
TextEditingController. - Saving the current DateTime and formatting it to string.
- Using the
sendBroadcastMessage
of the_meetingStore
object to Send the message in meeting. - Add the message as an
HMSMessage
object to render message in above list.
Build and run your app. Now, you can send and receive message in the meeting
Finally, you have learned the essential functionality and are prepared to use these skills in your projects.
Conclusion
You can find the starter and final project from 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 share from device, different type of chats(peer to peer or group chat), and many more functionality.
We hope you enjoyed this tutorial. Feel free to reach out to us if you have any queries. Thank you!
Posted on January 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024