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
2Install 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
5Video SDK Compatibility
| Android and iOS | Web (Beta) | Desktop (Beta) | Safari |
|---|---|---|---|
| | | | |
Structure of the project
Your project structure should look like this.
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
11We 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.
Configure Project
For Android
- Update the
/android/app/src/main/AndroidManifest.xmlfor the permissions we will be using to implement the audio and video features.
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
EglBaseinterface. 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}
8If necessary, in the same
build.gradleyou will need to increaseminSdkVersionofdefaultConfigup to23(currently default Flutter generator set it to16).If necessary, in the same
build.gradleyou will need to increasecompileSdkVersionandtargetSdkVersionup to31(currently default Flutter generator set it to30).
For iOS
- Add the following entries which allow your app to access the camera and microphone to your
/ios/Runner/Info.plistfile :
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:
1# platform :ios, '12.0'
2For MacOS
- Add the following entries to your
/macos/Runner/Info.plistfile 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.entitlementsfile 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.entitlementsfile 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/>
9Before 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
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}
17Step 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
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
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}
85Update the home screen of the app in 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
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}
23Output
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
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
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}
35Step 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
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
13 State<ParticipantTile> createState() => _ParticipantTileState();
14}
15
16class _ParticipantTileState extends State<ParticipantTile> {
17 Stream? videoStream;
18
19
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
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
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}
76Step 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
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
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
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
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
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}
162What 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;
3camEnabled 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—RTCVideoViewand thestream.kind == 'video'listeners never fire, so each tile shows the participant placeholder instead. - Camera permissions —
CAMERAinAndroidManifest.xml,NSCameraUsageDescriptionon 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.
