DEV Community

Chetan Sandanshiv for Video SDK

Posted on • Originally published at videosdk.live on

How to Implement Active Speaker Indication in Flutter Video Call App?

πŸ“Œ Introduction

How to Implement Active Speaker Indication in Flutter Video Call App?

Active speaker indication is an important feature in any video calling application, particularly during group calls. It plays a crucial role in keeping users engaged and informed about who is speaking at any given moment. This not only gives smoother communication but also reduces confusion, leading to an overall improved user experience.

In this comprehensive guide, we will understand the process of implementing active speaker indication in your Flutter video-calling app using VideoSDK. Leveraging the advanced capabilities offered by VideoSDK, we will demonstrate how to accurately identify the loudest speaker in a call and seamlessly integrate visual cues within your app's interface.

πŸš€ Getting Started with VideoSDK

To take advantage of Active Speaker, we must use the capabilities that the VideoSDK offers. Before diving into the implementation steps, let's ensure you complete the necessary prerequisites.

Create a VideoSDK Account

Go to your VideoSDK dashboard and sign up if you don't have an account. This account gives you access to the required Video SDK token, which acts as an authentication key that allows your application to interact with VideoSDK functionality.

Generate your Auth Token

Visit your VideoSDK dashboard and navigate to the "API Key" section to generate your auth token. This token is crucial in authorizing your application to use VideoSDK features.

For a more visual understanding of the account creation and token generation process, consider referring to the provided tutorial.

Prerequisites​

Before proceeding, ensure that your development environment meets the following requirements:

  • VideoSDK Developer Account (if you do not have one, follow VideoSDK Dashboard
  • The basic understanding of Flutter.
  • Flutter VideoSDK
  • Have Flutter installed on your device.

πŸ› οΈ Install VideoSDK​

Install the VideoSDK using the below-mentioned flutter command. Make sure you are in your Flutter app directory before you run this command.

$ flutter pub add videosdk

//run this command to add http library to perform network call to generate roomId
$ flutter pub add http
Enter fullscreen mode Exit fullscreen mode

VideoSDK Compatibility

Android and iOS app Web Desktop app Safari browser
βœ…
βœ…
βœ…
❌

Structure of the project​

Your project structure should look like this.

    root
    β”œβ”€β”€ android
    β”œβ”€β”€ ios
    β”œβ”€β”€ lib
         β”œβ”€β”€ api_call.dart
         β”œβ”€β”€ join_screen.dart
         β”œβ”€β”€ main.dart
         β”œβ”€β”€ meeting_controls.dart
         β”œβ”€β”€ meeting_screen.dart
         β”œβ”€β”€ participant_tile.dart
Enter fullscreen mode Exit fullscreen mode

We are going to create flutter widgets (JoinScreen, MeetingScreen, MeetingControls, and ParticipantTile).

App Structure​

The app widget will contain JoinScreen and MeetingScreen widget. MeetingScreen will have MeetingControls and ParticipantTile widget.

How to Implement Active Speaker Indication in Flutter Video Call App?

Configure Project

For Android​

  • Update /android/app/src/main/AndroidManifest.xml for the permissions we will be using to implement the audio and video features.
  • Also, you will need to set your build settings to Java 8 because the official WebRTC jar now uses static methods in EglBase the interface. Just add this to your app-level /android/app/build.gradle.
  • If necessary, in the same build.gradle you will need to increase minSdkVersion of defaultConfig up to 23 (currently default Flutter generator set to 16).
  • If necessary, in the same build.gradle you will need to increase compileSdkVersion and targetSdkVersion up to 33 (currently, the default Flutter generator sets it to 30).
<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.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
Enter fullscreen mode Exit fullscreen mode

AndroidManifest.xml

android {
    //...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
Enter fullscreen mode Exit fullscreen mode

For iOS​

  • Add the following entries that allow your app to access the camera and microphone in your /ios/Runner/Info.plist file:
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
Enter fullscreen mode Exit fullscreen mode
  • Uncomment the following line to define a global platform for your project in /ios/Podfile :
# platform :ios, '12.0'
Enter fullscreen mode Exit fullscreen mode

For MacOS​

  • Add the following entries to your /macos/Runner/Info.plist file that allow your app to access the camera and microphone:
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
Enter fullscreen mode Exit fullscreen mode
  • Add the following entries to your /macos/Runner/DebugProfile.entitlements file that allows your app to access the camera, microphone, and open outgoing network connections:
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
Enter fullscreen mode Exit fullscreen mode
  • Add the following entries to your /macos/Runner/Release.entitlements file that allows your app to access the camera, microphone, and open outgoing network connections:
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
Enter fullscreen mode Exit fullscreen mode

πŸŽ₯ Essential Steps to Implement Video Call Functionality

Before diving into the specifics of screen sharing implementation, it's crucial to ensure you have videoSDK properly installed and configured within your Flutter project. Refer to VideoSDK's documentation for detailed installation instructions. Once you have a functional video calling setup, you can proceed with adding the screen-sharing feature.

Step 1: Get started with api_call.dart​

Before jumping to anything else, you will write a function to generate a unique meetingId. You will require an authentication token, you can generate it either by using videosdk-rtc-api-server-examples or by generating it from the VideoSDK Dashboard for development.

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

//Auth token we will use to generate a meeting and connect to it
String token = "<Generated-from-dashboard>";

// API call to create meeting
Future<String> createMeeting() async {
  final http.Response httpResponse = await http.post(
    Uri.parse("https://api.videosdk.live/v2/rooms"),
    headers: {'Authorization': token},
  );

//Destructuring the roomId from the response
  return json.decode(httpResponse.body)['roomId'];
}
Enter fullscreen mode Exit fullscreen mode

api_call.dart

Step 2: Creating the JoinScreen​

Let's create join_screen.dart file in lib directory and create JoinScreen StatelessWidget.

The JoinScreen will consist of:

  • Create Meeting Button : This button will create a new meeting for you.
  • Meeting ID TextField : This text field will contain the meeting ID, you want to join.
  • Join Meeting Button : This button will join the meeting, which you have provided.
import 'package:flutter/material.dart';
import 'api_call.dart';
import 'meeting_screen.dart';

class JoinScreen extends StatelessWidget {
  final _meetingIdController = TextEditingController();

  JoinScreen({super.key});

  void onCreateButtonPressed(BuildContext context) async {
    // call api to create meeting and then navigate to MeetingScreen with meetingId,token
    await createMeeting().then((meetingId) {
      if (!context.mounted) return;
      Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) => MeetingScreen(
            meetingId: meetingId,
            token: token,
          ),
        ),
      );
    });
  }

  void onJoinButtonPressed(BuildContext context) {
    String meetingId = _meetingIdController.text;
    var re = RegExp("\\w{4}\\-\\w{4}\\-\\w{4}");
    // check meeting id is not null or invaild
    // if meeting id is vaild then navigate to MeetingScreen with meetingId,token
    if (meetingId.isNotEmpty && re.hasMatch(meetingId)) {
      _meetingIdController.clear();
      Navigator.of(context).push(
        MaterialPageRoute(
          builder: (context) => MeetingScreen(
            meetingId: meetingId,
            token: token,
          ),
        ),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
        content: Text("Please enter valid meeting id"),
      ));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('VideoSDK QuickStart'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(12.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () => onCreateButtonPressed(context),
              child: const Text('Create Meeting'),
            ),
            Container(
              margin: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
              child: TextField(
                decoration: const InputDecoration(
                  hintText: 'Meeting Id',
                  border: OutlineInputBorder(),
                ),
                controller: _meetingIdController,
              ),
            ),
            ElevatedButton(
              onPressed: () => onJoinButtonPressed(context),
              child: const Text('Join Meeting'),
            ),
          ],
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

join_screen.dart

  • Update the home screen of the app in the main.dart
import 'package:flutter/material.dart';
import 'join_screen.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'VideoSDK QuickStart',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: JoinScreen(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

main.dart

Step 3: Creating the MeetingControls​

Let's create meeting_controls.dart file and create MeetingControls StatelessWidget.

The MeetingControls will consist of:

  • Leave Button : This button will leave the meeting.
  • Toggle Mic Button : This button will unmute or mute the mic.
  • Toggle Camera Button : This button will enable or disable the camera.

MeetingControls will accept 3 functions in the constructor.

  • onLeaveButtonPressed : invoked when the Leave button pressed
  • onToggleMicButtonPressed : invoked when the toggle mic button pressed
  • onToggleCameraButtonPressed : invoked when the toggle Camera button pressed
import 'package:flutter/material.dart';

class MeetingControls extends StatelessWidget {
  final void Function() onToggleMicButtonPressed;
  final void Function() onToggleCameraButtonPressed;
  final void Function() onLeaveButtonPressed;

  const MeetingControls(
      {super.key,
      required this.onToggleMicButtonPressed,
      required this.onToggleCameraButtonPressed,
      required this.onLeaveButtonPressed});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        ElevatedButton(
            onPressed: onLeaveButtonPressed, child: const Text('Leave')),
        ElevatedButton(
            onPressed: onToggleMicButtonPressed, child: const Text('Toggle Mic')),
        ElevatedButton(
            onPressed: onToggleCameraButtonPressed,
            child: const Text('Toggle WebCam')),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

meeting_controls.dart

Step 4: Creating ParticipantTile​

Let's create participant_tile.dart file and create ParticipantTile StatefulWidget.

The ParticipantTile will consist of:

  • RTCVideoView : This will show the participant's video stream.

ParticipantTile will accept Participant in constructor

  • participant: participant of the meeting.
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';

class ParticipantTile extends StatefulWidget {
  final Participant participant;
  const ParticipantTile({super.key, required this.participant});

  @override
  State<ParticipantTile> createState() => _ParticipantTileState();
}

class _ParticipantTileState extends State<ParticipantTile> {
  Stream? videoStream;

  @override
  void initState() {
    // initial video stream for the participant
    widget.participant.streams.forEach((key, Stream stream) {
      setState(() {
        if (stream.kind == 'video') {
          videoStream = stream;
        }
      });
    });
    _initStreamListeners();
    super.initState();
  }

  _initStreamListeners() {
    widget.participant.on(Events.streamEnabled, (Stream stream) {
      if (stream.kind == 'video') {
        setState(() => videoStream = stream);
      }
    });

    widget.participant.on(Events.streamDisabled, (Stream stream) {
      if (stream.kind == 'video') {
        setState(() => videoStream = null);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: videoStream != null
          ? RTCVideoView(
              videoStream?.renderer as RTCVideoRenderer,
              objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
            )
          : Container(
              color: Colors.grey.shade800,
              child: const Center(
                child: Icon(
                  Icons.person,
                  size: 100,
                ),
              ),
            ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

participant_tile.dart

Step 5: Creating the MeetingScreen​

Let's create meeting_screen.dart file and create MeetingScreen StatefulWidget.

MeetingScreen will accept meetingId and token in the constructor.

  • meetingID: meetingId, you want to join
  • token : VideoSDK Auth token.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import './participant_tile.dart';

class MeetingScreen extends StatefulWidget {
  final String meetingId;
  final String token;

  const MeetingScreen(
      {super.key, required this.meetingId, required this.token});

  @override
  State<MeetingScreen> createState() => _MeetingScreenState();
}

class _MeetingScreenState extends State<MeetingScreen> {
  late Room _room;
  var micEnabled = true;
  var camEnabled = true;

  Map<String, Participant> participants = {};

  @override
  void initState() {
    // create room
    _room = VideoSDK.createRoom(
      roomId: widget.meetingId,
      token: widget.token,
      displayName: "John Doe",
      micEnabled: micEnabled,
      camEnabled: camEnabled
    );

    setMeetingEventListener();

    // Join room
    _room.join();

    super.initState();
  }

  // listening to meeting events
  void setMeetingEventListener() {
    _room.on(Events.roomJoined, () {
      setState(() {
        participants.putIfAbsent(
            _room.localParticipant.id, () => _room.localParticipant);
      });
    });

    _room.on(
      Events.participantJoined,
      (Participant participant) {
        setState(
          () => participants.putIfAbsent(participant.id, () => participant),
        );
      },
    );

    _room.on(Events.participantLeft, (String participantId) {
      if (participants.containsKey(participantId)) {
        setState(
          () => participants.remove(participantId),
        );
      }
    });

    _room.on(Events.roomLeft, () {
      participants.clear();
      Navigator.popUntil(context, ModalRoute.withName('/'));
    });
  }

  // onbackButton pressed leave the room
  Future<bool> _onWillPop() async {
    _room.leave();
    return true;
  }

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () => _onWillPop(),
      child: Scaffold(
        appBar: AppBar(
          title: const Text('VideoSDK QuickStart'),
        ),
        body: Padding(
          padding: const EdgeInsets.all(8.0),
          child: Column(
            children: [
              Text(widget.meetingId),
              //render all participant
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: GridView.builder(
                    gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 2,
                      crossAxisSpacing: 10,
                      mainAxisSpacing: 10,
                      mainAxisExtent: 300,
                    ),
                    itemBuilder: (context, index) {
                      return ParticipantTile(
                        key: Key(participants.values.elementAt(index).id),
                          participant: participants.values.elementAt(index));
                    },
                    itemCount: participants.length,
                  ),
                ),
              ),
              MeetingControls(
                onToggleMicButtonPressed: () {
                  micEnabled ? _room.muteMic() : _room.unmuteMic();
                  micEnabled = !micEnabled;
                },
                onToggleCameraButtonPressed: () {
                  camEnabled ? _room.disableCam() : _room.enableCam();
                  camEnabled = !camEnabled;
                },
                onLeaveButtonPressed: () {
                  _room.leave()
                },
              ),
            ],
          ),
        ),
      ),
      home: JoinScreen(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

meeting_screen.dart

CAUTION

If you get webrtc/webrtc.h file not found error at a runtime in iOS, then check the solution here.

TIP

You can checkout the complete quick start example here.

Integrate Active Speaker Indication Feature

Once you've seamlessly integrated VideoSDK into your Flutter project, you'll unlock the powerful capability of Active Speaker Indication. This innovative feature operates by gauging the audio volume of participants, dynamically discerning the speaker in real time. By implementing VideoSDK's provided guides, your application gains the ability to stay synced with levels, ensuring a seamless experience for your users.

In the following sections, we'll go through these guides, offering comprehensive guidance on their integration. By following our step-by-step instructions, you'll adeptly implement these callbacks, effectively illuminating the active speaker within your video call interface.

The active Speaker indication feature in VideoSDK lets you know, which participant in a meeting is an active speaker. This feature can be particularly useful in larger meetings or webinars, where there may be many participants and it can be difficult to tell who is speaking.

Whenever any participant speaks in a meeting, speakerChanged the event will trigger with the participant id of the active speaker.

For example, the meeting is running with Alice and Bob. Whenever any of them speaks, speakerChanged the event will trigger and return the speaker participantId.

import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';

class MeetingScreen extends StatefulWidget {
  ...
}

class _MeetingScreenState extends State<MeetingScreen> {
  late Room room;

  @override
  void initState() {
    ...

    setupRoomEventListener();
  }

  @override
  Widget build(BuildContext context) {
    return YourMeetingWidget();
  }

  void setupRoomEventListener() {

    room.on(Events.speakerChanged, (String? activeSpeakerId) {
      //Room active speaker has changed
      //Participant ID of current active speaker is activeSpeakerId
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”š Conclusion

By following these steps, you'll successfully integrate active speaker indication into your Flutter video-calling app, elevating its group call functionalities to new heights. Empower your users with real-time insights into the active speaker, fostering more engaging and efficient communication experiences.

Remember to refer to VideoSDK's documentation for ongoing support and updates, ensuring your app remains at the forefront of video conferencing innovation.

Top comments (0)