1. Home
  2. >
  3. Blog
  4. >
  5. Building Zoom clone in Flutter with 100ms SDK

Building Zoom clone in Flutter with 100ms SDK

January 8, 2022

Building Zoom clone in Flutter with 100ms SDK

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 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 Screen-share tile
  • hand Raised
  • Mute/Unmute
  • Camera off/on
  • Toggle Front/Back camera
  • Chatting with everyone in the room

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

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.
  • 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 initialise 100ms in it.

Setting up project

Create New App

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

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:

room-created
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.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 SDK version  to 32 by navigating to the android/app directory and updating the build.gradle:

android {
	compileSdkVersion 32
	...
	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:

setup/meeting_store.dart at master · govindmaheshwari2/zoom-example-app (github.com)

The above class provides you with a lot of methods over the HMS SDK which will be later used here.

setup/hms_sdk_interactor.dart at master · govindmaheshwari2/zoom-example-app (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.

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.addUpdateListener();
  }
...
}

In the above code:

  1. Created a late instance of MeetingStore that will get initialise in initState.
  2. Initialize the MeetingStore instance for observing the changes.
  3. Calling joinMeeting method from the initState to join the meeting
  4. 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:

  1. isRoomEnded: If the room gets ended then it will take the user to the home screen.
  2. peerTracks.isEmpty: If no one has joined the room then it shows a message to the user.
  3. peerFilteredList is an ObservableList is user get added or removed then it will notify the UI to change it.
  4. 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 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 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.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:

  1. screenShareTrack: _meetingStore.screenShareTrack contain track for screen-share if it is null then no one is sharing the screen otherwise it will return track.
  2. rotatedBox: To match the screen-share ratio with the mobile device screen.
  3. HMSVideoView will render the screen by using _meetingStore.screenShareTrack as a track parameter.

Now build app and run when you do screen-share you can see it like below:

Hand Raised

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:

  1. Used the isRaiseHand boolean local variable to check and update the Image colour accordingly.
  2. Updated the onPressed event to toggle the isRaiseHand variable.
  3. Toggle the raiseHand metadata using the _meetingStore to inform all users.

To get other peers hand raise info update videoViewGrid function as follow:

peerFilteredList[index].isRaiseHand

In the above code:

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

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:

  1. Wrapped the button with the Observer so that you can rebuild it on change of mic status.
  2. Use isMicOn boolean to check and update the Icon accordingly.
  3. 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:

  1. Wrapped the button with the Observer  so that you can rebuild it on change of camera status.
  2. Used the isVideoOn boolean method to check and update the Icon accordingly.
  3. 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:

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

  1. Observer is used to display the changes.
  2. Return the empty box if the meeting hasn’t started.
  3. Displaying "No messages" text if there are no messages.
  4. Rendering the messages as a List get updated.
  5. Displaying the sender's peer name.
  6. Displaying the DateTime of the message.
  7. 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     
_meetingStore.sendBroadcastMessage(message);
//3                          
messageTextController.clear();

Here you did the following:

  1. Saving the message using the messageTextController TextEditingController.
  2. Using the sendBroadcastMessage of the _meetingStore object to Send the message in meeting.
  3. messageTextController is clear after message is send.

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!

Create your own Zoom clone in less than 30-minutes

Share your app with other developers on 100ms Discord server


Like what you’re reading?

Get Audio/video engineering tips straight into your inbox