Home

 / Blog / 

Building a Zoom clone in Flutter

Building a Zoom clone in Flutter

October 21, 202220 min read

Share

Zoom Clone in Flutter | Cover Image

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 -

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

File structure - Starter Project

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

Setting up the 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:

New 100ms App

Next, choose the Video Conferencing template:

Click on Set up App and your app is created:

Room

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 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 the 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-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" />

iOS Permissions

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.

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

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

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 

Join Room

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:

  • 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 initialised 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 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.

Render the Peers

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:

  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 the user gets 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 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),
),

Screen share Tile

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:

  1. screenShareTrack: _meetingStore.screenShareTrack contains a track for screen-share if it is null then no one is sharing the screen otherwise it will return the 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 tracking parameter.

Now build an app and run it when you do screen-share you can see it 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 color 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 follows:

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

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

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

  1. Saving the message using the messageTextController TextEditingController.
  2. Using the sendBroadcastMessage of the _meetingStore object to Send the message in the meeting.
  3. 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.

Conclusion

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