video sdk logo

Login

Flutter Audio Rooms Tutorial

Build drop-in audio rooms across iOS, Android, and web from one Dart codebase. Speaker and listener roles, raise hand, and moderation are built in, with echo cancellation and noise suppression on by default.
Flutter audio room built with VideoSDK showing speakers on stage and listeners on mobile and web

Trusted by 300+ global business & tech leaders

Build an Audio-Only Room in Flutter, Step by Step

Learn how to build an audio-only room in Flutter using the VideoSDK Flutter SDK. This tutorial covers room setup, speaker and listener modes, raise-hand requests, and mute, moderation, and leave controls.


Prerequisites

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

  • Video SDK Developer Account (Not having one, follow Video SDK Dashboard)
  • The basic understanding of Flutter.
  • Flutter Video SDK
  • Have Flutter installed on your device.

One should have a VideoSDK account to generate token. Visit VideoSDK dashboard to generate tokenVisit VideoSDK dashboard to generate token

Getting Started with the Code!

Follow the steps to create the environment necessary to add video calls into your app. Also you can find the code sample for quickstart here.

Create a new Flutter project.

Create a new Flutter App using the below command.

1 flutter create videosdk_flutter_quickstart
2

Install Video SDK

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

1 flutter pub add videosdk
2
3//run this command to add http library to perform network call to generate roomId
4 flutter pub add http
5

Video SDK Compatibility

Android and iOS Web (Beta) Desktop (Beta) Safari
:white_check_mark:
:white_check_mark:
:white_check_mark:
:x:

Structure of the project

Your project structure should look like this.

Project Structure
1    root
2    ├── android
3    ├── ios
4    ├── lib
5         ├── api_call.dart
6         ├── join_screen.dart
7         ├── main.dart
8         ├── meeting_controls.dart
9         ├── meeting_screen.dart
10         ├── participant_tile.dart
11

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

App Structure

App widget will contain JoinScreen and MeetingScreen widget. MeetingScreen will have MeetingControls and ParticipantTile widget.

VideoSDK Flutter Quick Start Architecture

Configure Project

For Android

  • Update the /android/app/src/main/AndroidManifest.xml for the permissions we will be using to implement the audio and video features.
AndroidManifest.xml
1<uses-feature android:name="android.hardware.camera" />
2<uses-feature android:name="android.hardware.camera.autofocus" />
3<uses-permission android:name="android.permission.CAMERA" />
4<uses-permission android:name="android.permission.RECORD_AUDIO" />
5<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
7<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
8<uses-permission android:name="android.permission.INTERNET"/>
9<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
10<uses-permission android:name="android.permission.WAKE_LOCK" />
11
  • Also you will need to set your build settings to Java 8 because the official WebRTC jar now uses static methods in EglBase interface. Just add this to your app-level /android/app/build.gradle.
1android {
2    //...
3    compileOptions {
4        sourceCompatibility JavaVersion.VERSION_1_8
5        targetCompatibility JavaVersion.VERSION_1_8
6    }
7}
8
  • If necessary, in the same build.gradle you will need to increase minSdkVersion of defaultConfig up to 23 (currently default Flutter generator set it to 16).

  • If necessary, in the same build.gradle you will need to increase compileSdkVersion and targetSdkVersion up to 31 (currently default Flutter generator set it to 30).

For iOS

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

For MacOS

  • Add the following entries to your /macos/Runner/Info.plist file which allow your app to access the camera and microphone.
1<key>NSCameraUsageDescription</key>
2<string>$(PRODUCT_NAME) Camera Usage!</string>
3<key>NSMicrophoneUsageDescription</key>
4<string>$(PRODUCT_NAME) Microphone Usage!</string>
5
  • Add the following entries to your /macos/Runner/DebugProfile.entitlements file which allow your app to access the camera, microphone and open outgoing network connections.
1<key>com.apple.security.network.client</key>
2<true/>
3<key>com.apple.security.device.camera</key>
4<true/>
5<key>com.apple.security.device.microphone</key>
6<true/>
7
  • Add the following entries to your /macos/Runner/Release.entitlements file which allow your app to access the camera, microphone and open outgoing network connections.
1<key>com.apple.security.network.server</key>
2<true/>
3<key>com.apple.security.network.client</key>
4<true/>
5<key>com.apple.security.device.camera</key>
6<true/>
7<key>com.apple.security.device.microphone</key>
8<true/>
9

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

api_call.dart

api_call.dart
1import 'dart:convert';
2import 'package:http/http.dart' as http;
3
4//Auth token we will use to generate a meeting and connect to it
5String token = "<Generated-from-dashboard>";
6
7// API call to create meeting
8Future<String> createMeeting() async {
9  final http.Response httpResponse = await http.post(
10    Uri.parse("https://api.videosdk.live/v2/rooms"),
11    headers: {'Authorization': token},
12  );
13
14  //Destructuring the roomId from the response
15  return json.decode(httpResponse.body)['roomId'];
16}
17

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.

join_screen.dart

join_screen.dart
1import 'package:flutter/material.dart';
2import 'api_call.dart';
3import 'meeting_screen.dart';
4
5class JoinScreen extends StatelessWidget {
6  final _meetingIdController = TextEditingController();
7
8  JoinScreen({super.key});
9
10  void onCreateButtonPressed(BuildContext context) async {
11    // call api to create meeting and then navigate to MeetingScreen with meetingId,token
12    await createMeeting().then((meetingId) {
13      if (!context.mounted) return;
14      Navigator.of(context).push(
15        MaterialPageRoute(
16          builder: (context) => MeetingScreen(
17            meetingId: meetingId,
18            token: token,
19          ),
20        ),
21      );
22    });
23  }
24
25  void onJoinButtonPressed(BuildContext context) {
26    String meetingId = _meetingIdController.text;
27    var re = RegExp("\\w{4}\\-\\w{4}\\-\\w{4}");
28
29    // check meeting id is not null or invaild
30    // if meeting id is vaild then navigate to MeetingScreen with meetingId,token
31    if (meetingId.isNotEmpty && re.hasMatch(meetingId)) {
32      _meetingIdController.clear();
33      Navigator.of(context).push(
34        MaterialPageRoute(
35          builder: (context) => MeetingScreen(
36            meetingId: meetingId,
37            token: token,
38          ),
39        ),
40      );
41    } else {
42      ScaffoldMessenger.of(context).showSnackBar(
43        const SnackBar(
44          content: Text("Please enter valid meeting id"),
45        ),
46      );
47    }
48  }
49
50  @override
51  Widget build(BuildContext context) {
52    return Scaffold(
53      appBar: AppBar(
54        title: const Text('VideoSDK QuickStart'),
55      ),
56      body: Padding(
57        padding: const EdgeInsets.all(12.0),
58        child: Column(
59          mainAxisAlignment: MainAxisAlignment.center,
60          children: [
61            ElevatedButton(
62              onPressed: () => onCreateButtonPressed(context),
63              child: const Text('Create Meeting'),
64            ),
65            Container(
66              margin: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
67              child: TextField(
68                decoration: const InputDecoration(
69                  hintText: 'Meeting Id',
70                  border: OutlineInputBorder(),
71                ),
72                controller: _meetingIdController,
73              ),
74            ),
75            ElevatedButton(
76              onPressed: () => onJoinButtonPressed(context),
77              child: const Text('Join Meeting'),
78            ),
79          ],
80        ),
81      ),
82    );
83  }
84}
85

Update the home screen of the app in main.dart.

main.dart

main.dart
1import 'package:flutter/material.dart';
2import 'join_screen.dart';
3
4void main() {
5  runApp(const MyApp());
6}
7
8class MyApp extends StatelessWidget {
9  const MyApp({super.key});
10
11  // This widget is the root of your application.
12  @override
13  Widget build(BuildContext context) {
14    return MaterialApp(
15      title: 'VideoSDK QuickStart',
16      theme: ThemeData(
17        primarySwatch: Colors.blue,
18      ),
19      home: JoinScreen(),
20    );
21  }
22}
23

Output

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 mic.
  • Toggle Camera Button - This button will enable or disable camera.

MeetingControls will accept 3 functions in constructor:

  • onLeaveButtonPressed - invoked when Leave button pressed.
  • onToggleMicButtonPressed - invoked when Toggle Mic button pressed.
  • onToggleCameraButtonPressed - invoked when Toggle Camera button pressed.

meeting_controls.dart

meeting_controls.dart
1import 'package:flutter/material.dart';
2
3class MeetingControls extends StatelessWidget {
4  final void Function() onToggleMicButtonPressed;
5  final void Function() onToggleCameraButtonPressed;
6  final void Function() onLeaveButtonPressed;
7
8  const MeetingControls(
9      {super.key,
10      required this.onToggleMicButtonPressed,
11      required this.onToggleCameraButtonPressed,
12      required this.onLeaveButtonPressed});
13
14  @override
15  Widget build(BuildContext context) {
16    return Row(
17      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
18      children: [
19        ElevatedButton(
20          onPressed: onLeaveButtonPressed,
21          child: const Text('Leave'),
22        ),
23        ElevatedButton(
24          onPressed: onToggleMicButtonPressed,
25          child: const Text('Toggle Mic'),
26        ),
27        ElevatedButton(
28          onPressed: onToggleCameraButtonPressed,
29          child: const Text('Toggle WebCam'),
30        ),
31      ],
32    );
33  }
34}
35

Step 4: Creating ParticipantTile

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

The ParticipantTile will consist of:

  • RTCVideoView - This will show participant's video stream.

ParticipantTile will accept Participant in constructor:

  • participant - participant of the meeting.

participant_tile.dart

participant_tile.dart
1import 'package:flutter/material.dart';
2import 'package:videosdk/videosdk.dart';
3
4class ParticipantTile extends StatefulWidget {
5  final Participant participant;
6
7  const ParticipantTile({
8    super.key,
9    required this.participant,
10  });
11
12  @override
13  State<ParticipantTile> createState() => _ParticipantTileState();
14}
15
16class _ParticipantTileState extends State<ParticipantTile> {
17  Stream? videoStream;
18
19  @override
20  void initState() {
21    // initial video stream for the participant
22    widget.participant.streams.forEach((key, Stream stream) {
23      setState(() {
24        if (stream.kind == 'video') {
25          videoStream = stream;
26        }
27      });
28    });
29
30    _initStreamListeners();
31    super.initState();
32  }
33
34  @override
35  void setState(fn) {
36    if (mounted) {
37      super.setState(fn);
38    }
39  }
40
41  _initStreamListeners() {
42    widget.participant.on(Events.streamEnabled, (Stream stream) {
43      if (stream.kind == 'video') {
44        setState(() => videoStream = stream);
45      }
46    });
47
48    widget.participant.on(Events.streamDisabled, (Stream stream) {
49      if (stream.kind == 'video') {
50        setState(() => videoStream = null);
51      }
52    });
53  }
54
55  @override
56  Widget build(BuildContext context) {
57    return Padding(
58      padding: const EdgeInsets.all(8.0),
59      child: videoStream != null
60          ? RTCVideoView(
61              videoStream?.renderer as RTCVideoRenderer,
62              objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
63            )
64          : Container(
65              color: Colors.grey.shade800,
66              child: const Center(
67                child: Icon(
68                  Icons.person,
69                  size: 100,
70                ),
71              ),
72            ),
73    );
74  }
75}
76

Step 5: Creating the MeetingScreen

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

MeetingScreen will accept meetingId and token in constructor:

  • meetingId - meetingId, you want to join.
  • token - VideoSdk Auth token.

meeting_screen.dart

meeting_screen.dart
1import 'package:flutter/foundation.dart';
2import 'package:flutter/material.dart';
3import 'package:videosdk/videosdk.dart';
4import './participant_tile.dart';
5
6class MeetingScreen extends StatefulWidget {
7  final String meetingId;
8  final String token;
9
10  const MeetingScreen({
11    super.key,
12    required this.meetingId,
13    required this.token,
14  });
15
16  @override
17  State<MeetingScreen> createState() => _MeetingScreenState();
18}
19
20class _MeetingScreenState extends State<MeetingScreen> {
21  late Room _room;
22  var micEnabled = true;
23  var camEnabled = false;
24
25  Map<String, Participant> participants = {};
26
27  @override
28  void initState() {
29    // create room
30    _room = VideoSDK.createRoom(
31      roomId: widget.meetingId,
32      token: widget.token,
33      displayName: "John Doe",
34      micEnabled: micEnabled,
35      camEnabled: camEnabled,
36      defaultCameraIndex: kIsWeb
37          ? 0
38          : 1, // Index of MediaDevices will be used to set default camera
39    );
40
41    setMeetingEventListener();
42
43    // Join room
44    _room.join();
45
46    super.initState();
47  }
48
49  @override
50  void setState(fn) {
51    if (mounted) {
52      super.setState(fn);
53    }
54  }
55
56  // listening to meeting events
57  void setMeetingEventListener() {
58    _room.on(Events.roomJoined, () {
59      setState(() {
60        participants.putIfAbsent(
61          _room.localParticipant.id,
62          () => _room.localParticipant,
63        );
64      });
65    });
66
67    _room.on(
68      Events.participantJoined,
69      (Participant participant) {
70        setState(
71          () => participants.putIfAbsent(
72            participant.id,
73            () => participant,
74          ),
75        );
76      },
77    );
78
79    _room.on(
80      Events.participantLeft,
81      (String participantId, Map<String, dynamic> reason) {
82        if (participants.containsKey(participantId)) {
83          setState(
84            () => participants.remove(participantId),
85          );
86        }
87      },
88    );
89
90    _room.on(Events.roomLeft, () {
91      participants.clear();
92      Navigator.popUntil(context, ModalRoute.withName('/'));
93    });
94  }
95
96  // onbackButton pressed leave the room
97  Future<bool> _onWillPop() async {
98    _room.leave();
99    return true;
100  }
101
102  // This widget is the root of your application.
103  @override
104  Widget build(BuildContext context) {
105    return WillPopScope(
106      onWillPop: () => _onWillPop(),
107      child: Scaffold(
108        appBar: AppBar(
109          title: const Text('VideoSDK QuickStart'),
110        ),
111        body: Padding(
112          padding: const EdgeInsets.all(8.0),
113          child: Column(
114            children: [
115              Text(widget.meetingId),
116
117              //render all participant
118              Expanded(
119                child: Padding(
120                  padding: const EdgeInsets.all(8.0),
121                  child: GridView.builder(
122                    gridDelegate:
123                        const SliverGridDelegateWithFixedCrossAxisCount(
124                      crossAxisCount: 2,
125                      crossAxisSpacing: 10,
126                      mainAxisSpacing: 10,
127                      mainAxisExtent: 300,
128                    ),
129                    itemBuilder: (context, index) {
130                      return ParticipantTile(
131                        key: Key(
132                          participants.values.elementAt(index).id,
133                        ),
134                        participant: participants.values.elementAt(index),
135                      );
136                    },
137                    itemCount: participants.length,
138                  ),
139                ),
140              ),
141
142              MeetingControls(
143                onToggleMicButtonPressed: () {
144                  micEnabled ? _room.muteMic() : _room.unmuteMic();
145                  micEnabled = !micEnabled;
146                },
147                onToggleCameraButtonPressed: () {
148                  camEnabled ? _room.disableCam() : _room.enableCam();
149                  camEnabled = !camEnabled;
150                },
151                onLeaveButtonPressed: () {
152                  _room.leave();
153                },
154              ),
155            ],
156          ),
157        ),
158      ),
159    );
160  }
161}
162

What Makes This an Audio-Only Room

This tutorial is the audio-only version of the Flutter video calling quickstart. The room, the join flow, and the participant handling are identical — the difference is one flag.

In meeting_screen.dart, the camera is never turned on:

1var micEnabled = true;
2var camEnabled = false;
3

camEnabled is passed to VideoSDK.createRoom(), so the local participant joins publishing microphone audio and nothing else. No camera track is created, and the device's camera indicator never lights up.

Because no video is published or received, three things from the video quickstart are no longer needed:

  • The Toggle WebCam control in meeting_controls.dart — with no camera track, there is nothing to toggle. Leaving it in would let a user switch the camera on and break the audio-only guarantee.
  • The video renderer in participant_tile.dart — RTCVideoView and the stream.kind == 'video' listeners never fire, so each tile shows the participant placeholder instead.
  • Camera permissions — CAMERA in AndroidManifest.xml, NSCameraUsageDescription on iOS and macOS, and the camera entitlements on macOS. Dropping them means your users are never asked for camera access at all.

Remote audio is played by the SDK automatically once you join, so there is no player to wire up.

Want video back? Set camEnabled = true, restore the camera permissions for your target platforms, and follow the video calling quickstart for the renderer and camera controls.

Output

Run and Test

The app is all set to test. Make sure to update the token in api_call.dart.

Summarize with

ChatGPTClaudePerplexityGoogleGrok
Prefer VideoSDK on Google

Table of Contents

Start Building Now With $20 Free Balance

No credit card required to start.

Documentation →Github Repo →

Share This Article

TwitterLinkedInFacebookHacker News

Get started for free today

Grab your API keys and start building with free $20 included in your account. Need enterprise-grade security or custom workloads? Talk to an expert!

Book a demo

Video SDK Logo

United States

28 Geary St, Suite 650,
San Francisco, CA 94108, United States

India flag

India

18th Floor, 1812, The Junomoneta Tower,
Adajan-Hazira Rd, Surat, Gujarat 395009, India

Video SDK Logo
Video SDK Logo
Video SDK Logo
Video SDK Logo
SOLUTIONS

Video KYC

Video Banking

Virtual Claim

Video MER

Telehealth

Astrology

Gaming

Dating

Live Commerce

Auto Proctoring

Interview-as-a-service

Virtual Events

Live Audio Streaming

Ed-Tech

PRODUCTS

AI-Agents

Real-time Audio & Video SDK

Interactive Live Streaming SDK

Real-time Transcription SDK

Character SDK

Open Source Examples

SUCCESS STORIES

Examedi

Coderschool

TYHO

ForagerOne

Immigo

DEVELOPERS

Documentation

Code Samples

Developer Updates

Developer Hub

TOP ARTICLES

What is WebRTC?

Build a React Native Video Calling App

Build a Flutter Video Calling App

RESOURCES

The Protocol by Video SDK

AI Apps

Creator Program

LEGAL

Terms Of Service

Privacy Policy

Cookie Notice

CCPA Notice

Subprocessors

DPA

RSS

COMPANY

Contact Us

Pricing

Support

Blog

Press Kit