Home

 / Blog / 

How to build a Google Meet Clone in Flutter

How to build a Google Meet Clone in Flutter

May 11, 202237 min read

Share

Google Meet in Flutter | Cover Image

Today connecting with anyone across the world is really easy. Thanks to real-time communication we can now talk to our friends and family worldwide.

Using Flutter, you can build video-calling applications for Android, iOS, web, and Windows with ease. In this article, we will see how to build a Google Meet clone in Flutter.

I’ll use 100ms SDK to add video conferencing to our Flutter app.

What is 100ms?

100ms is a cloud platform that allows developers to add video and audio conferencing to Web, Android, and iOS applications.

100ms SDK Flutter Quickstart Guide

Setup

To get started we’ll need to set up our project on the 100ms dashboard.

Set up  — Choose a subdomain

  • For a video calling app like Google Meet, we’ll choose the Video Conferencing template already available.

Set up app — Choose a template

  • Once done, your app would be created!

App created successful

Note: 100ms offers great customization where you add/remove roles, provide permissions, and more to modify them according to your use case.

  • Now, you can proceed by clicking on ‘Go to Dashboard’. On the dashboard, you’ll see the side navigation with Rooms. Click on it.

Room Creation Successful

  • A room must have been created for you by default. Copy the Room Id of the default room or create a new room of your own. We’ll need the Room Id to use later.

Building the UI

Let us set up our Flutter project for the clone.

  • Create a new Flutter project

flutter create flutter_meet

  • Clear the default code in main.dart and replace it with:
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Meet',
      home: const HomeScreen(),
    );
  }
}

Replicating the designs

Since we are building a clone we already have designs to refer to :p

Dark Mode — Google Meet

Let’s get started by building a home screen with two buttons and an App Bar.

Note: You can also get the UI code from here.

We’ll create a new folder in lib called screens and a file in it called home_screen.dart

In home_screen.dart, let us create a Scaffold with an app bar and body as follow.

class HomeScreen extends StatelessWidget {
 const HomeScreen({Key? key}) : super(key: key);
 @override
 Widget build(BuildContext context) {
   return SafeArea(
     child: Scaffold(
       backgroundColor: Theme.of(context).primaryColor,
       appBar: AppBar(
         backgroundColor: Theme.of(context).primaryColor,
         elevation: 0,
         title: Text("Meet"),
         centerTitle: true,
         actions: [
           IconButton(
             icon: Icon(Icons.account_circle),
             onPressed: () {},
           ),
         ],
       ),
       body: Column(
         children: [
           Row(
             mainAxisAlignment: MainAxisAlignment.spaceEvenly,
             children: [
               OutlinedButton(
                 onPressed: () async {},
                 child: const Text('New meeting'),
               ),
               OutlinedButton(
                    style: Theme.of(context)
                         .outlinedButtonTheme
                         .style!
                         .copyWith(
                    side: MaterialStateProperty.all(
                          BorderSide(color: Colors.white)),
                    backgroundColor:                                                MaterialStateColor.resolveWith(
                      (states) => Colors.transparent),
                    foregroundColor: MaterialStateColor.resolveWith(
                      (states) => Colors.white)),
onPressed: () {},
                   child: const Text('Join with a code'))
             ],
           )
         ],
       ),
     ),
   );
 }
}

We’ll also need to update the ThemeData in main.dart as:

theme: ThemeData(
   primaryColor: Colors.grey[900],
   outlinedButtonTheme: OutlinedButtonThemeData(
       style: ButtonStyle(
           shape: MaterialStateProperty.all(RoundedRectangleBorder(
               borderRadius: BorderRadius.circular(20.0))),
           backgroundColor: MaterialStateColor.resolveWith(
               (states) => Colors.blueAccent),
           foregroundColor: MaterialStateColor.resolveWith(
               (states) => Colors.white)))),

This would build:

AppBar and the body

We can also add a navigation drawer to our Scaffold as follows:

drawer: Drawer(
 child: ListView(
   padding: EdgeInsets.zero,
   children: [
     DrawerHeader(
       decoration: BoxDecoration(
         color: Theme.of(context).primaryColor,
       ),
       child: const Text(
         'Google Meet',
         style: TextStyle(fontSize: 25, color: Colors.white),
       ),
     ),
     ListTile(
       title: Row(
         children: [
           Icon(Icons.settings_outlined),
           SizedBox(
             width: 10,
           ),
           const Text('Settings'),
         ],
       ),
       onTap: () {},
     ),
     ListTile(
       title: Row(
         children: [
           Icon(Icons.feedback_outlined),
           SizedBox(
             width: 10,
           ),
           const Text('Send feedback'),
         ],
       ),
       onTap: () {},
     ),
     ListTile(
       title: Row(
         children: [
           Icon(Icons.help_outline),
           SizedBox(
             width: 10,
           ),
           const Text('Help'),
         ],
       ),
       onTap: () {},
     ),
   ],
 ),
),

Drawer

Now, to start a meeting the user can click on the ‘New meeting’ button which should push a bottom sheet to ‘Start an instant meeting’ as is on Google Meet.

Add the showModalBottomSheet code to the onPressed of the OutlinedButton with the ‘New meeting’ text.

showModalBottomSheet<void>(
 context: context,
 builder: (BuildContext context) {
   return Container(
     height: 200,
     child: ListView(
       padding: EdgeInsets.zero,
       children: <Widget>[
         ListTile(
           title: Row(
             children: const [
               Icon(Icons.video_call),
               SizedBox(
                 width: 10,
               ),
               Text('Start an instant meeting'),
             ],
           ),
           onTap: () async {
            
           },
         ),
         ListTile(
           title: Row(
             children: [
               Icon(Icons.close),
               SizedBox(
                 width: 10,
               ),
               const Text('Close'),
             ],
           ),
           onTap: () {
             Navigator.pop(context);
           },
         ),
       ],
     ),
   );
 },
);

This should create:

Bottom Sheet

With that done, we’ll have most of the UI of the HomeScreen created.

You can find the complete code here.

Before we set up the meeting, we can push in an empty screen with an AppBar when clicking on the ‘Start an instant meeting’ tile which would look like this:

Empty MeetingScreen

Find the code for this here

Adding 100ms to Flutter Project

To get started, we’ll need to add the Flutter package for 100ms SDK. You can find it on pub.dev here.

Add the 100ms Flutter SDK in your pubspec.yaml as follows:

hmssdk_flutter: 1.1.0

We’ll also need to add a few other packages to pubspec.yaml file namely:

permission_handler: 8.3.0
http: 0.13.4
provider: 6.0.2
draggable_widget: 2.0.0
cupertino_icons: 1.0.2

Note: The versions used are the latest at the time of writing this article.

I’ll explain why we’ll need it as we move along with the tutorial ✌️

Setting up services

Create a new folder service with a file sdk_initializer.dart in it.

Add the following code to it:

import 'package:hmssdk_flutter/hmssdk_flutter.dart';
class SdkInitializer {
   static HMSSDK hmssdk = HMSSDK();
}

This file would have a static instance of the HMSSDK .

Now, to join a video call, we can call the join method on HMSSDK with the config settings. This would require an authentication token and a room id.

In production your own server will generate these and manage user authentication.

To get an auth token, we need to send an HTTP post request to the Token endpoint which can be obtained from the dashboard.

Go to Developer -> Copy Token endpoint (under Access Credentials)

Getting the Token endpoint

For example, my Token endpoint is: https://prod-in.100ms.live/hmsapi/adityathakur.app.100ms.live/

Create a new file called join_service.dart inside the same services folder and paste the following code replacing the roomId with your room id (copied earlier) and the endPoint with your Token endpoint.

import 'package:hmssdk_flutter/hmssdk_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class JoinService {
  static Future<bool> join(HMSSDK hmssdk) async {
    String roomId = "<Your Room ID>";
    Uri endPoint = Uri.parse(
        "https://prod-in.100ms.live/hmsapi/adityathakur.app.100ms.live/api/token");
    http.Response response = await http.post(endPoint,
        body: {'user_id': "user", 'room_id': roomId, 'role': "host"});
    var body = json.decode(response.body);
    if (body == null || body['token'] == null) {
      return false;
    }
    HMSConfig config = HMSConfig(authToken: body['token'], userName: "user");
    await hmssdk.join(config: config);
    return true;
  }
}

Note: Don’t forget to append the API/token to your Token endpoint as shown in the sample code above.

A successful HTTP post would return us a token that can be passed to the join method of hmssdk as config.

The only steps now left are to listen to these changes effectively and update our UI accordingly.

Listening to changes

We’ll create a UserDataStore that would implement the abstract class HMSUpdateListener.

HMSUpdateListener listens to all the updates happening inside the room. 100ms SDK provides callbacks to the client app about any change or update happening in the room after a user has joined by implementing HMSUpdateListener.

The UserDataStore would extend the ChangeNotifier to notify of any changes.

Create a new folder inside lib called models and add data_store.dart to it.

//Dart imports
import 'dart:developer';

//Package imports 
import 'package:flutter/material.dart';
import 'package:hmssdk_flutter/hmssdk_flutter.dart';

//File imports
import 'package:google_meet/services/sdk_initializer.dart';


class UserDataStore extends ChangeNotifier
    implements HMSUpdateListener, HMSActionResultListener {
  HMSTrack? remoteVideoTrack;
  HMSPeer? remotePeer;
  HMSTrack? remoteAudioTrack;
  HMSVideoTrack? localTrack;
  bool _disposed = false;
  late HMSPeer localPeer;
  bool isRoomEnded = false;
  
  void startListen() {
    SdkInitializer.hmssdk.addUpdateListener(listener: this);
  }

  @override
  void dispose() {
    _disposed = true;
    super.dispose();
  }

  void leaveRoom() async {
    SdkInitializer.hmssdk.leave(hmsActionResultListener: this);
  }

  @override
  void notifyListeners() {
    if (!_disposed) {
      super.notifyListeners();
    }
  }

  @override
  void onJoin({required HMSRoom room}) {
    for (HMSPeer each in room.peers!) {
      if (each.isLocal) {
        localPeer = each;
        break;
      }
    }
  }

  @override
  void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) {
    switch (update) {
      case HMSPeerUpdate.peerJoined:
        remotePeer = peer;
        remoteAudioTrack = peer.audioTrack;
        remoteVideoTrack = peer.videoTrack;
        break;
      case HMSPeerUpdate.peerLeft:
        remotePeer = null;
        break;
      case HMSPeerUpdate.roleUpdated:
        break;
      case HMSPeerUpdate.metadataChanged:
        break;
      case HMSPeerUpdate.nameChanged:
        break;
      case HMSPeerUpdate.defaultUpdate:
        break;
      case HMSPeerUpdate.networkQualityUpdated:
        break;
    }
    notifyListeners();
  }

  @override
  void onTrackUpdate(
      {required HMSTrack track,
      required HMSTrackUpdate trackUpdate,
      required HMSPeer peer}) {
    switch (trackUpdate) {
      case HMSTrackUpdate.trackAdded:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!peer.isLocal) remoteAudioTrack = track;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!peer.isLocal) {
            remoteVideoTrack = track;
          } else {
            localTrack = track as HMSVideoTrack;
          }
        }
        break;
      case HMSTrackUpdate.trackRemoved:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!peer.isLocal) remoteAudioTrack = null;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!peer.isLocal) {
            remoteVideoTrack = null;
          } else {
            localTrack = null;
          }
        }
        break;
      case HMSTrackUpdate.trackMuted:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!peer.isLocal) remoteAudioTrack = track;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!peer.isLocal) {
            remoteVideoTrack = track;
          } else {
            localTrack = null;
          }
        }
        break;
      case HMSTrackUpdate.trackUnMuted:
        if (track.kind == HMSTrackKind.kHMSTrackKindAudio) {
          if (!peer.isLocal) remoteAudioTrack = track;
        } else if (track.kind == HMSTrackKind.kHMSTrackKindVideo) {
          if (!peer.isLocal) {
            remoteVideoTrack = track;
          } else {
            localTrack = track as HMSVideoTrack;
          }
        }
        break;
      case HMSTrackUpdate.trackDescriptionChanged:
        break;
      case HMSTrackUpdate.trackDegraded:
        break;
      case HMSTrackUpdate.trackRestored:
        break;
      case HMSTrackUpdate.defaultUpdate:
        break;
    }
    notifyListeners();
  }
  
  @override
  void onHMSError({required HMSException error}) {
    log(error.message??"");
  }
  
  @override
  void onMessage({required HMSMessage message}) {}
  
  @override
  void onRoomUpdate({required HMSRoom room, required HMSRoomUpdate update}) {}
  
  @override
  void onUpdateSpeakers({required List<HMSSpeaker> updateSpeakers}) {}

  @override
  void onReconnected() {}

  @override
  void onReconnecting() {}

  @override
  void onRemovedFromRoom(
      {required HMSPeerRemovedFromPeer hmsPeerRemovedFromPeer}) {}

  @override
  void onRoleChangeRequest({required HMSRoleChangeRequest roleChangeRequest}) {}

  @override
  void onChangeTrackStateRequest(
      {required HMSTrackChangeRequest hmsTrackChangeRequest}) {}
  
  @override
  void onAudioDeviceChanged(
      {HMSAudioDevice? currentAudioDevice,
      List<HMSAudioDevice>? availableAudioDevice}) {}

  @override
  void onException(
      {required HMSActionResultListenerMethod methodType,
      Map<String, dynamic>? arguments,
      required HMSException hmsException}) {
    // TODO: implement onException
    switch (methodType) {
      case HMSActionResultListenerMethod.leave:
        log("Leave room error ${hmsException.message}");
    }
  }

  @override
  void onSuccess(
      {required HMSActionResultListenerMethod methodType,
      Map<String, dynamic>? arguments}) {
    switch (methodType) {
      case HMSActionResultListenerMethod.leave:
        isRoomEnded = true;
        notifyListeners();
    }
  }
}

In the above code, we have implemented a few callbacks:

  • onJoin - called when the join was successful and you have entered the room.

Audio is automatically connected, video needs to be configured.

  • onPeerUpdate - called when a person joins or leaves the call and when their audio/video mutes/unmutes.
  • onTrackUpdate - usually, when a person joins the call, the listener will first call onPeerUpdate to notify them about the join. Subsequently, onTrackUpdate will be called with their actual video track.

Updating the UI

Let us return to our screens folder to now update the UI with changes.

Getting the permissions

To use the camera and microphone in our video-calling app, we need to get the required permissions from the user!

We can use the previously added permission_handler package to do that easily.

  • Start by converting the HomeScreen widget to a stateful widget.
  • Add a new getPermissions function that would request permissions using the added package.
void getPermissions() async {
await Permission.camera.request();
await Permission.microphone.request();
while ((await Permission.camera.isDenied)) {
   await Permission.camera.request();
 }
while ((await Permission.microphone.isDenied)) {
   await Permission.microphone.request();
 }
}
  • Inside the initState() of your now Stateful HomeScreen widget, call this getPermissions function.
@override
void initState() {
 SdkInitializer.hmssdk.build();
 getPermissions();
 super.initState();
}

We’ll also call the build() on the hmssdk instance in the same initState().

Joining a room and listening to changes

Create a new function joinRoom() which would handle the room joining functionality for us.

Future<bool> joinRoom() async {
    setState(() {
     _isLoading = true;
    });
    bool isJoinSuccessful = await JoinService.join(SdkInitializer.hmssdk);
    if (!isJoinSuccessful) {
     return false;
    }
    _dataStore = UserDataStore();
    //Here we are attaching a listener to our DataStoreClass
    _dataStore.startListen();
    setState(() {
     _isLoading = false;
    });
    return true;
}

In this method, we call on join of our previously created class JoinService .

We are also attaching a listener to our DataStoreClass and using a local _isLoading while these tasks are completed. The _isLoading is a boolean which is initially set to false.

We can use this _isLoading to show a CircularProgressIndicator while the tasks are completed.

We need to now configure the onTap of our ‘Start an instant meeting’ as follows:

bool isJoined = await joinRoom();
if (isJoined) {
 Navigator.of(context).push(
   MaterialPageRoute( builder: (_) =>
      ListenableProvider.value(
        value: _dataStore,
        child: MeetingScreen())));
} else {
 const SnackBar(content: Text("Error"));
}

We wait for the joinRoom function, if the return value is true, we push the MeetingScreen wrapped with a ListenableProvider and value set to _dataStore else we show an error SnackBar.

To summarize, the sequence to be followed while using hmssdk is:

  • Initialize the SDK
  • Call the build method
  • Call the join method after building HMSConfig
  • Call the addUpdateListener method

You can find the complete HomeScreen code here

Setting up the Meeting Screen

The MeetingScreen would be a StatefulWidget. We’ll need that to setState() whenever there are any changes in the local or remote users.

We’ll start by creating a few local variables to be used later:

bool isLocalAudioOn = true;
bool isLocalVideoOn = true;
final bool _isLoading = false;

And, an onLeave method (which would help leave the room and pop the screen):

Future<bool> leaveRoom() async {
 SdkInitializer.hmssdk.leave();
 Navigator.pop(context);
 return false;
}

Now, when the local user starts and joins the meeting we’d show them a waiting screen with the text “You’re the only one here” as is on the Google Meet app.

Waiting screen - Google meet on flutter

When a remote user joins in, the video of them should be shown.

The MeetingScreen should also have buttons to mute/unmute the mic, switch on/off the video and disconnect the call.

Onscreen buttons

This all can be implemented as follows:

//Package imports
import 'package:draggable_widget/draggable_widget.dart';
import 'package:flutter/material.dart';
import 'package:hmssdk_flutter/hmssdk_flutter.dart';
import 'package:provider/provider.dart';

//File imports
import 'package:google_meet/models/data_store.dart';
import 'package:google_meet/services/sdk_initializer.dart';

class MeetingScreen extends StatefulWidget {
  const MeetingScreen({Key? key}) : super(key: key);

  @override
  _MeetingScreenState createState() => _MeetingScreenState();
}

class _MeetingScreenState extends State<MeetingScreen> {
  bool isLocalAudioOn = true;
  bool isLocalVideoOn = true;
  final bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    final _isVideoOff = context.select<UserDataStore, bool>(
        (user) => user.remoteVideoTrack?.isMute ?? true);
    final _peer =
        context.select<UserDataStore, HMSPeer?>((user) => user.remotePeer);
    final remoteTrack = context
        .select<UserDataStore, HMSTrack?>((user) => user.remoteVideoTrack);
    final localTrack = context
        .select<UserDataStore, HMSVideoTrack?>((user) => user.localTrack);

    return WillPopScope(
      onWillPop: () async {
        context.read<UserDataStore>().leaveRoom();
        Navigator.pop(context);
        return true;
      },
      child: SafeArea(
        child: Scaffold(
          body: (_isLoading)
              ? const CircularProgressIndicator()
              : (_peer == null)
                  ? Container(
                      color: Colors.black.withOpacity(0.9),
                      width: MediaQuery.of(context).size.width,
                      height: MediaQuery.of(context).size.height,
                      child: Stack(
                        children: [
                          Positioned(
                              child: IconButton(
                                  onPressed: () {
                                    context.read<UserDataStore>().leaveRoom();
                                    Navigator.pop(context);
                                  },
                                  icon: const Icon(
                                    Icons.arrow_back_ios,
                                    color: Colors.white,
                                  ))),
                          Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: const [
                              Padding(
                                padding:
                                    EdgeInsets.only(left: 20.0, bottom: 20),
                                child: Text(
                                  "You're the only one here",
                                  style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 20,
                                      fontWeight: FontWeight.bold),
                                ),
                              ),
                              Padding(
                                padding: EdgeInsets.only(left: 20.0),
                                child: Text(
                                  "Share meeting link with others",
                                  style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 12,
                                      fontWeight: FontWeight.bold),
                                ),
                              ),
                              Padding(
                                padding: EdgeInsets.only(left: 20.0),
                                child: Text(
                                  "that you want in the meeting",
                                  style: TextStyle(
                                      color: Colors.white,
                                      fontSize: 12,
                                      fontWeight: FontWeight.bold),
                                ),
                              ),
                              Padding(
                                padding: EdgeInsets.only(left: 20.0, top: 10),
                                child: CircularProgressIndicator(
                                  strokeWidth: 2,
                                ),
                              ),
                            ],
                          ),
                          DraggableWidget(
                            topMargin: 10,
                            bottomMargin: 130,
                            horizontalSpace: 10,
                            child: localPeerTile(localTrack),
                          ),
                        ],
                      ),
                    )
                  : SizedBox(
                      height: MediaQuery.of(context).size.height,
                      width: MediaQuery.of(context).size.width,
                      child: Stack(
                        children: [
                          Container(
                              color: Colors.black.withOpacity(0.9),
                              child: _isVideoOff
                                  ? Center(
                                      child: Container(
                                        decoration: BoxDecoration(
                                            shape: BoxShape.circle,
                                            boxShadow: [
                                              BoxShadow(
                                                color:
                                                    Colors.blue.withAlpha(60),
                                                blurRadius: 10.0,
                                                spreadRadius: 2.0,
                                              ),
                                            ]),
                                        child: const Icon(
                                          Icons.videocam_off,
                                          color: Colors.white,
                                          size: 30,
                                        ),
                                      ),
                                    )
                                  : (remoteTrack != null)
                                      ? Container(
                                          child: HMSVideoView(
                                            scaleType: ScaleType.SCALE_ASPECT_FILL,
                                            track: remoteTrack as HMSVideoTrack,
                                          ),
                                        )
                                      : const Center(child: Text("No Video"))),
                          Align(
                            alignment: Alignment.bottomCenter,
                            child: Padding(
                              padding: const EdgeInsets.only(bottom: 15),
                              child: Row(
                                mainAxisAlignment:
                                    MainAxisAlignment.spaceEvenly,
                                children: [
                                  GestureDetector(
                                    onTap: () async {
                                      context.read<UserDataStore>().leaveRoom();
                                      Navigator.pop(context);
                                    },
                                    child: Container(
                                      decoration: BoxDecoration(
                                          shape: BoxShape.circle,
                                          boxShadow: [
                                            BoxShadow(
                                              color: Colors.red.withAlpha(60),
                                              blurRadius: 3.0,
                                              spreadRadius: 5.0,
                                            ),
                                          ]),
                                      child: const CircleAvatar(
                                        radius: 25,
                                        backgroundColor: Colors.red,
                                        child: Icon(Icons.call_end,
                                            color: Colors.white),
                                      ),
                                    ),
                                  ),
                                  GestureDetector(
                                    onTap: () => {
                                      SdkInitializer.hmssdk
                                          .toggleCameraMuteState(),
                                      setState(() {
                                        isLocalVideoOn = !isLocalVideoOn;
                                      })
                                    },
                                    child: CircleAvatar(
                                      radius: 25,
                                      backgroundColor:
                                          Colors.transparent.withOpacity(0.2),
                                      child: Icon(
                                        isLocalVideoOn
                                            ? Icons.videocam
                                            : Icons.videocam_off_rounded,
                                        color: Colors.white,
                                      ),
                                    ),
                                  ),
                                  GestureDetector(
                                    onTap: () => {
                                      SdkInitializer.hmssdk
                                          .toggleMicMuteState(),
                                      setState(() {
                                        isLocalAudioOn = !isLocalAudioOn;
                                      })
                                    },
                                    child: CircleAvatar(
                                      radius: 25,
                                      backgroundColor:
                                          Colors.transparent.withOpacity(0.2),
                                      child: Icon(
                                        isLocalAudioOn
                                            ? Icons.mic
                                            : Icons.mic_off,
                                        color: Colors.white,
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ),
                          Positioned(
                            top: 10,
                            left: 10,
                            child: GestureDetector(
                              onTap: () {
                                context.read<UserDataStore>().leaveRoom();
                                Navigator.pop(context);
                              },
                              child: const Icon(
                                Icons.arrow_back_ios,
                                color: Colors.white,
                              ),
                            ),
                          ),
                          Positioned(
                            top: 10,
                            right: 10,
                            child: GestureDetector(
                              onTap: () {
                                if (isLocalVideoOn) {
                                  SdkInitializer.hmssdk.switchCamera();
                                }
                              },
                              child: CircleAvatar(
                                radius: 25,
                                backgroundColor:
                                    Colors.transparent.withOpacity(0.2),
                                child: const Icon(
                                  Icons.switch_camera_outlined,
                                  color: Colors.white,
                                ),
                              ),
                            ),
                          ),
                          DraggableWidget(
                            topMargin: 10,
                            bottomMargin: 130,
                            horizontalSpace: 10,
                            child: localPeerTile(localTrack),
                          ),
                        ],
                      ),
                    ),
        ),
      ),
    );
  }

  Widget localPeerTile(HMSVideoTrack? localTrack) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(10),
      child: Container(
        height: 150,
        width: 100,
        color: Colors.black,
        child: (isLocalVideoOn && localTrack != null)
            ? HMSVideoView(
                track: localTrack,
              )
            : const Icon(
                Icons.videocam_off_rounded,
                color: Colors.white,
              ),
      ),
    );
  }
}

The code above renders a waiting screen while the local user waits for the remote user to join in and show their video once available.

You can join the room using your mobile device as a local user.

For remote user link:

  • Go to 100ms dashboard.
  • Navigate to rooms and click on the room id that you have used for the project.

Copying the room ID

  • Click on ‘Join room’.

Getting the role URL

  • This would reveal Role URLs. You can copy-paste the Guest URL into a new tab to join as a remote user.

Demo of the app

Google Clone Flutter Demo

Voila 🎉 We have successfully created a Google Meet clone in Flutter using 100ms SDK for video functionality.

You can find the complete code here.

Thank you! ✌️

Engineering

Share

Related articles

See all articles