Do you marvel what video calling apps really imply? If not then no worries, you’ll get an thought quickly whereas studying this weblog. All of us have come throughout digital phrases extra typically these days. We’re connecting with our workers, colleague, and others with the assistance of on-line platforms to share content material, data and to report to at least one one other. Video SDK got here up with the thought of creating an app that helps individuals with connecting remotely. Throughout the assembly one can current their content material to others, can increase their question by dropping a textual content, one can ask questions by turning on the mic and lots of extra options are there you’ll get acquaintance of on the finish of this weblog.
4 Steps to Construct a Video Calling App in Flutter
- Develop and launch in each Android and iOS on the similar time.
Prerequisite
Venture Construction
Create one flutter challenge first by writing the next command
$ flutter create videosdk_flutter_quickstart
Your challenge construction’s lib listing must be as similar as talked about under
root-Folder Title
├── ...
├── lib
│ ├── join_screen.dart
├── foremost.dart
├── meeting_screen.dart
├── participant_grid_view.dart
├── participant_tile.dart
Step 1:Flutter SDK Integration For Android
1 : import flutter sdk from video SDK
$ flutter pub add videosdk
//run this command so as to add http library to carry out community name to generate meetingId
$ flutter pub add http
2 : Replace the AndroidManifest.xml for the permissions we will likely be utilizing to implement the audio and video options. You could find the AndroidManifest.xml file at /android/app/src/foremost/AndroidManifest.xml
<makes use of-characteristic android:title="android.{hardware}.digicam" />
<makes use of-characteristic android:title="android.{hardware}.digicam.autofocus" />
<makes use of-permission android:title="android.permission.CAMERA" />
<makes use of-permission android:title="android.permission.RECORD_AUDIO" />
<makes use of-permission android:title="android.permission.ACCESS_NETWORK_STATE" />
<makes use of-permission android:title="android.permission.CHANGE_NETWORK_STATE" />
<makes use of-permission android:title="android.permission.MODIFY_AUDIO_SETTINGS" />
3 : Additionally you have to to set your construct settings to Java 8 as a result of the official WebRTC jar now makes use of static strategies in EglBase interface. Simply add this to your app-level construct.gradle
android {
//...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
- If mandatory, in the identical construct.gradle you have to to extend minSdkVersion of defaultConfig as much as 21 (at the moment default Flutter generator set it to 16 ).
- If mandatory, in the identical construct.gradle you have to to extend compileSdkVersion and targetSdkVersion as much as 31 (at the moment default Flutter generator set it to 30 ).
Step 2:Flutter SDK Integration For IoS
Add the next entries which permit your app to entry the digicam and microphone to your Information.plist file, situated in /ios/Runner/Information.plist :
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Digital camera Utilization!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Utilization!</string>
Step 3:Begin Writing Your Code
1 : Let’s create a join-screen now.The Becoming a member of display screen will encompass
- Create Button – This button will create a brand new assembly for you.
- TextField for Assembly ID – This textual content discipline will comprise the assembly ID you wish to be part of.
- Be a part of Button – This button will be part of the assembly which you offered.
Create a brand new dart file join_screen.dart which is able to comprise our Stateful Widget named JoinScreen.
Exchange the _token with the pattern token you generated from the VideoSDK Dashboard.
import 'dart:convert';
import 'bundle:flutter/materials.dart';
import 'bundle:http/http.dart' as http;
import 'bundle:videosdk_flutter_quickstart/meeting_screen.dart';
class JoinScreen extends StatefulWidget {
const JoinScreen({Key? key}) : tremendous(key: key);
@override
_JoinScreenState createState() => _JoinScreenState();
}
class _JoinScreenState extends State<JoinScreen> {
//Repalce the token with the pattern token you generated from the VideoSDK Dashboard
String _token ="";
String _meetingID = "";
@override
Widget construct(BuildContext context) {
remaining ButtonStyle _buttonStyle = TextButton.styleFrom(
main: Colours.white,
backgroundColor: Theme.of(context).primaryColor,
textStyle: const TextStyle(
fontWeight: FontWeight.daring,
),
);
return Scaffold(
appBar: AppBar(
title: const Textual content("VideoSDK RTC"),
),
backgroundColor: Theme.of(context).backgroundColor,
physique: SafeArea(
youngster: Column(
mainAxisAlignment: MainAxisAlignment.middle,
kids: [
],
),
),
);
}
}
Replace the JoinScreen with two buttons and a textual content discipline within the kids property of column widget.
class _JoinScreenState extends State<JoinScreen> {
...
@override
Widget construct(BuildContext context) {
...
return Scaffold(
...
physique: SafeArea(
youngster: Column(
mainAxisAlignment: MainAxisAlignment.middle,
kids: [
//Button to Create Meeting
TextButton(
style: _buttonStyle,
onPressed: () async {
//When clicked
//Generate a meetingId
_meetingID = await createMeeting();
//Navigatet to MeetingScreen
navigateToMeetingScreen();
},
child: const Text("CREATE MEETING"),
),
SizedBox(height: 20),
const Text(
"OR",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 24,
),
),
SizedBox(height: 20),
//Textfield for entering meetingId
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: TextField(
onChanged: (meetingID) => _meetingID = meetingID,
decoration: InputDecoration(
border: const OutlineInputBorder(),
fillColor: Theme.of(context).primaryColor,
labelText: "Enter Meeting ID",
hintText: "Meeting ID",
prefixIcon: const Icon(
Icons.keyboard,
color: Colors.white,
),
),
),
),
SizedBox(height: 20),
//Button to join the meeting
TextButton(
onPressed: () async {
//Navigate to MeetingScreen
navigateToMeetingScreen();
},
style: _buttonStyle,
child: const Text("JOIN MEETING"),
)
],
),
),
);
}
//That is technique known as to navigate to MeetingScreen.
//It passes the token and meetingId to MeetingScreen as parameters
void navigateToMeetingScreen(){
Navigator.push(
context,
MaterialPageRoute(
//MeetingScreen is created in upcomming steps
builder: (context) => MeetingScreen(
token: _token,
meetingId: _meetingID,
displayName: "John Doe",
),
),
);
}
}
Add the createMeeting() within the JoinScreen which is able to generate a brand new assembly id.
class _JoinScreenState extends State<JoinScreen> {
//...different variables
//...construct technique
Future<String> createMeeting() async {
remaining Uri getMeetingIdUrl =
Uri.parse('https://api.videosdk.dwell/v1/conferences');
remaining http.Response meetingIdResponse =
await http.publish(getMeetingIdUrl, headers: {
"Authorization": _token,
});
remaining meetingId = json.decode(meetingIdResponse.physique)['meetingId'];
return meetingId;
}
}
Make the JoinScreen as your property within the foremost.dart as proven under.
void foremost() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : tremendous(key: key);
@override
Widget construct(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colours.blue,
),
dwelling: JoinScreen(), //change to this
);
}
}
2 : We’re achieved with the creation of be part of display screen , now let’s create a gathering display screen of video calling app.
Create a brand new meeting_screen.dart which can have a Stateful widget named MeetingScreen.
MeetingScreen accepts the next parameter:
- meetingId : This would be the assembly Id we are going to becoming a member of
- token : Auth Token to configure the Assembly
- displayName : Title with which the participant will likely be joined
- micEnabled : (Optionally available) If true, the mic will likely be on while you be part of the assembly else will probably be off.
- webcamEnabled : (Optionally available) If true, the webcam will likely be on while you be part of the assembly else will probably be off.
import 'bundle:flutter/materials.dart';
import 'bundle:videosdk/rtc.dart';
import 'bundle:videosdk_flutter_quickstart/join_screen.dart';
import 'bundle:videosdk_flutter_quickstart/participant_grid_view.dart';
class MeetingScreen extends StatefulWidget {
//add the next parameters to your MeetingScreen
remaining String meetingId, token, displayName;
remaining bool micEnabled, webcamEnabled;
const MeetingScreen({
Key? key,
required this.meetingId,
required this.token,
required this.displayName,
this.micEnabled = true,
this.webcamEnabled = true
}) : tremendous(key: key);
@override
_MeetingScreenState createState() => _MeetingScreenState();
}
class _MeetingScreenState extends State<MeetingScreen> {
@override
Widget construct(BuildContext context) {
return Container();
}
}
Now we are going to replace _MeetingScreenState to make use of the MeetingBuilder to create our assembly.
We are going to move the required parameters to the MeetingBuilder.
class _MeetingScreenState extends State<MeetingScreen> {
@override
Widget construct(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPopScope,
//MeetingBuilder is a category of @videosdk/rtc.dart
youngster: MeetingBuilder(
meetingId: widget.meetingId,
displayName: widget.displayName,
token: widget.token,
micEnabled: widget.micEnabled,
webcamEnabled: widget.webcamEnabled,
notification: const NotificationInfo(
title: "Video SDK",
message: "Video SDK is sharing display screen within the assembly",
icon: "notification_share", // drawable icon title
),
builder: (_meeting) {
},
),
);
}
}
Now we are going to add assembly , videoStream , audioStream which is able to retailer the assembly and the streams for native participant.
class _MeetingScreenState extends State<MeetingScreen> {
Assembly? assembly;
Stream? videoStream;
Stream? audioStream;
//...construct
}
Now we are going to replace the builder to generate a gathering view.
- Including the Occasion listeners for the assembly and setting the state of our native assembly
class _MeetingScreenState extends State<MeetingScreen> {
//...state declartations
@override
Widget construct(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPopScope,
youngster: MeetingBuilder(
//meetingConfig
builder: (_meeting) {
// Referred to as when joined in assembly
_meeting.on(
Occasions.meetingJoined,
() {
setState(() {
assembly = _meeting;
});
// Setting assembly occasion listeners
setMeetingListeners(_meeting);
},
);
}
),
);
}
void setMeetingListeners(Assembly assembly) {
// Referred to as when assembly is ended
assembly.on(Occasions.meetingLeft, () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const JoinScreen()),
(route) => false);
});
// Referred to as when stream is enabled
assembly.localParticipant.on(Occasions.streamEnabled, (Stream _stream) {
if (_stream.type == 'video') {
setState(() {
videoStream = _stream;
});
} else if (_stream.type == 'audio') {
setState(() {
audioStream = _stream;
});
}
});
// Referred to as when stream is disabled
assembly.localParticipant.on(Occasions.streamDisabled, (Stream _stream) {
if (_stream.type == 'video' && videoStream?.id == _stream.id) {
setState(() {
videoStream = null;
});
} else if (_stream.type == 'audio' && audioStream?.id == _stream.id) {
setState(() {
audioStream = null;
});
}
});
}
}
- We will likely be exhibiting a Ready to affix display screen till the assembly is joined after which present the assembly view.
class _MeetingScreenState extends State<MeetingScreen> {
//...state declartations
@override
Widget construct(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPopScope,
youngster: MeetingBuilder(
//meetingConfig
builder: (_meeting) {
//_meeting listener
// Exhibiting ready display screen
if (assembly == null) {
return Scaffold(
physique: Middle(
youngster: Column(
mainAxisSize: MainAxisSize.min,
kids: [
const CircularProgressIndicator(),
SizedBox(height: 20),
const Text("waiting to join meeting"),
],
),
),
);
}
//Assembly View
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor.withOpacity(0.8),
appBar: AppBar(
title: Textual content(widget.meetingId),
),
physique: Column(
kids: [
],
),
);
}
),
);
}
}
- Inside our assembly view we are going to add a ParticipantGrid and three motion buttons.
class _MeetingScreenState extends State<MeetingScreen> {
//...state declartations
@override
Widget construct(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPopScope,
youngster: MeetingBuilder(
//...meetingConfig
builder: (_meeting) {
//... _meeting listener
//...Ready Display screen UI
//Assembly View
return Scaffold(
backgroundColor: Theme.of(context).backgroundColor.withOpacity(0.8),
appBar: AppBar(
title: Textual content(widget.meetingId),
),
physique: Column(
kids: [
Expanded(
//ParticipantGridView will be created in further steps!
child: ParticipantGridView(meeting: meeting!),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => {
if (audioStream != null)
{_meeting.muteMic()}
else
{_meeting.unmuteMic()}
},
child: Text("Mic"),
),
ElevatedButton(
onPressed: () => {
if (videoStream != null)
{_meeting.disableWebcam()}
else
{_meeting.enableWebcam()}
},
child: Text("Webcam"),
),
ElevatedButton(
onPressed: () => {_meeting.leave()},
child: Text("Leave"),
),
],
)
],
),
);
}
),
);
}
}
- Add the _onWillPopScope() which is able to deal with the assembly depart on again button click on.
class _MeetingScreenState extends State<MeetingScreen> {
//...different declarations
//...construct technique
//... setMeetingListeners
Future<bool> _onWillPopScope() async {
assembly?.depart();
return true;
}
}
3 : Subsequent we will likely be creating the ParticipantGridView which will likely be used to point out the participant’s view.
ParticipantGridView maps every participant with a ParticipantTile.
It updates the participant’s listing every time somebody leaves or joins the assembly utilizing the participantJoined and participantLeft occasion listeners on the assembly.
create participant_grid_view.dart file which has a Stateful widget named as ParticipantGridView.
import 'bundle:flutter/materials.dart';
import 'bundle:videosdk/rtc.dart';
import 'participant_tile.dart';
class ParticipantGridView extends StatefulWidget {
remaining Assembly assembly;
const ParticipantGridView({
Key? key,
required this.assembly,
}) : tremendous(key: key);
@override
State<ParticipantGridView> createState() => _ParticipantGridViewState();
}
class _ParticipantGridViewState extends State<ParticipantGridView> {
Participant? localParticipant;
Map<String, Participant> contributors = {};
@override
void initState() {
// Initialize contributors
localParticipant = widget.assembly.localParticipant;
contributors = widget.assembly.contributors;
// Setting assembly occasion listeners
setMeetingListeners(widget.assembly);
tremendous.initState();
}
@override
Widget construct(BuildContext context) {
return GridView.rely(
crossAxisCount: 2,
kids: [
//This Participant Tile will hold local participants view
ParticipantTile(
participant: localParticipant!,
isLocalParticipant: true,
),
//This will map all other participants
...participants.values
.map((participant) => ParticipantTile(participant: participant))
.toList()
],
);
}
void setMeetingListeners(Assembly _meeting) {
// Referred to as when participant joined assembly
_meeting.on(
Occasions.participantJoined,
(Participant participant) {
remaining newParticipants = contributors;
newParticipants[participant.id] = participant;
setState(() {
contributors = newParticipants;
});
},
);
// Referred to as when participant left assembly
_meeting.on(
Occasions.participantLeft,
(participantId) {
remaining newParticipants = contributors;
newParticipants.take away(participantId);
setState(() {
contributors = newParticipants;
});
},
);
}
}
4 : Creating the ParticipantTile which will likely be positioned contained in the GridView and for that create participant_tile.dart file
import 'bundle:flutter/materials.dart';
import 'bundle:videosdk/rtc.dart';
class ParticipantTile extends StatefulWidget {
remaining Participant participant;
remaining bool isLocalParticipant;
const ParticipantTile(
{Key? key, required this.participant, this.isLocalParticipant = false})
: tremendous(key: key);
@override
State<ParticipantTile> createState() => _ParticipantTileState();
}
class _ParticipantTileState extends State<ParticipantTile> {
Stream? videoStream;
Stream? audioStream;
@override
Widget construct(BuildContext context) {
return Container();
}
}
- Now we are going to initialise the state with the present streams of the Participant and add listeners for stream change
class _ParticipantTileState extends State<ParticipantTile> {
Stream? videoStream;
Stream? audioStream;
@override
void initState() {
_initStreamListeners();
tremendous.initState();
widget.participant.streams.forEach((key, Stream stream) {
setState(() {
if (stream.type == 'video') {
videoStream = stream;
} else if (stream.type == 'audio') {
audioStream = stream;
}
});
});
}
//... construct
_initStreamListeners() {
widget.participant.on(Occasions.streamEnabled, (Stream _stream) {
setState(() {
if (_stream.type == 'video') {
videoStream = _stream;
} else if (_stream.type == 'audio') {
audioStream = _stream;
}
});
});
widget.participant.on(Occasions.streamDisabled, (Stream _stream) {
setState(() {
if (_stream.type == 'video' && videoStream?.id == _stream.id) {
videoStream = null;
} else if (_stream.type == 'audio' && audioStream?.id == _stream.id) {
audioStream = null;
}
});
});
widget.participant.on(Occasions.streamPaused, (Stream _stream) {
setState(() {
if (_stream.type == 'video' && videoStream?.id == _stream.id) {
videoStream = _stream;
} else if (_stream.type == 'audio' && audioStream?.id == _stream.id) {
audioStream = _stream;
}
});
});
widget.participant.on(Occasions.streamResumed, (Stream _stream) {
setState(() {
if (_stream.type == 'video' && videoStream?.id == _stream.id) {
videoStream = _stream;
} else if (_stream.type == 'audio' && audioStream?.id == _stream.id) {
audioStream = _stream;
}
});
});
}
}
- Now we are going to create RTCVideoView to point out the participant stream and in addition add different elements just like the title and mic standing indicator of the participant.
class _ParticipantTileState extends State<ParticipantTile> {
Stream? videoStream;
Stream? audioStream;
@override
Widget construct(BuildContext context) {
return Container(
margin: const EdgeInsets.all(4.0),
ornament: BoxDecoration(
colour: Theme.of(context).backgroundColor.withOpacity(1),
border: Border.all(
colour: Colours.white38,
),
),
youngster: AspectRatio(
aspectRatio: 1,
youngster: Padding(
padding: const EdgeInsets.all(4.0),
youngster: Stack(
kids: [
//To Show the participant Stream
videoStream != null
? RTCVideoView(
videoStream?.renderer as RTCVideoRenderer,
objectFit:
RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)
: const Center(
child: Icon(
Icons.person,
size: 180.0,
color: Color.fromARGB(140, 255, 255, 255),
),
),
//Display the Participant Name
Positioned(
bottom: 0,
left: 0,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Container(
padding: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor.withOpacity(0.2),
border: Border.all(
color: Colors.white24,
),
borderRadius: BorderRadius.circular(4.0),
),
child: Text(
widget.participant.displayName,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10.0,
),
),
),
),
),
//Display the Participant Mic Status
Positioned(
top: 0,
left: 0,
child: InkWell(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: audioStream != null
? Theme.of(context).backgroundColor
: Colors.red,
),
child: Icon(
audioStream != null ? Icons.mic : Icons.mic_off,
size: 16,
),
)
),
),
],
),
),
),
);
}
//... _initListener
}
Step 4:Run Your Code Now
$ flutter run
Conclusion
With this, we efficiently constructed the video chat app utilizing the video SDK in Flutter. If you happen to want to add functionalities like chat messaging, and display screen sharing, you may all the time take a look at our documentation. If you happen to face any problem with the implementation you may join with us on our discord community.