Building Zoom clone in Flutter with 100ms SDK

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

  1. Add 100ms to a Flutter app
  2. Join a room
  3. Leave a room
  4. Show video tiles with the user’s name
  5. Show Screenshare tile
  6. hand Raised
  7. Mute/Unmute
  8. Camera off/on
  9. Toggle Front/Back camera
  10. Chatting with everyone in the room

By the end of this blog, this is how your app will look like:

Image description

Before proceeding, make sure you have the following requirements:

  1. Flutter v2.0.0 or later (stable)
  2. 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:

Image description

Image description

Image description

The file structure of the starter project looks like this:

Image description

  1. main.dart: The entry point of the app and the screen to get user details before joining the meeting.
  2. meeting.dart: The video call screen to render all peers view.
  3. message.dart: The chat screen to send messages to everyone in the room.
  4. room_service.dart: A helper service class to fetch the token to join a meeting.
  5. 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:

Image description

Create New App

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

Image description

Next, choose the Video Conferencing template:

Image description

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

Image description

Room

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

Image description

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 :

Image description

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:

  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.

Image description

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!,
                  )
                : 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),
),

Image description

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:

  1. screenShareTrack: _meetingStore.screenShareTrack contain track for screenshare if it is null then no one is sharing the screen otherwise it will return track.
  2. rotatedBox: To match the screenshare 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 screenshare you can see it like below:

Image description

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:

  1. Used the isRaiseHandboolean 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:

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

  1. _meetingStore.peers will return all the information about the peers in form of list.
  2. We will find HMSPeer object from peers list by comparing peerId.
  3. 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:

Image description

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.

Image description

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:

  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.

Image description

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:

  1. Saving the message using the messageTextController TextEditingController.
  2. Saving the current DateTime and formatting it to string.
  3. Using the sendBroadcastMessage of the _meetingStore object to Send the message in meeting.
  4. 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

Image description

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!