video calling app video calling app

Video Calling App: WebRTC Video Calling Implementation in Flutter

At Sreyas IT Solutions, Our Flutter development team specializes in developing high-quality video calling app and video chat app for both iOS and Android. With advanced technologies such as flutter_webrtc, we provide instant communication with smooth transitions between foreground, background, and terminated application states. Our goal is to deliver reliable, low-latency, and high-quality video experiences that keep your users connected anytime, anywhere.

WebRTC for Video Calling App

WebRTC (Web Real-Time Communication) is an open-source application that supports real-time audio, video callings app, and data sharing directly between devices—without going through external servers. It is the underlying technology behind video calling and video chatting applications since it permits low-latency communication between peers, which means a fast, secure, and high-quality experience on both web and mobile applications.

We have developed two Flutter apps. One app is for initiating video calls, and the other is for receiving them. This allows for seamless video chat across different platforms.

Packages Used in Caller & Receiver Apps:

  • flutter_webrtc:  For real-time video and audio streaming.  
  • firebase_core: Core Firebase SDK for initializing Firebase.  
  • cloud_firestore: To store and sync call data and user info.  
  • uuid: To generate unique IDs for calls and users.  
  • permission_handler: To manage camera, microphone, and other permissions.  
  • font_awesome_flutter: For icons in the UI.  
  • firebase_auth:  For user authentication.  
  • firebase_messaging:For push notifications and call alerts.  
  • flutter_callkit_incoming: To handle incoming call UI and native call notifications.  
  • http:  For network requests and signaling operations.  

Caller App: Complete Code Guide

  • Add this function to initiate call and update roomId :
Future<void> initiateCall(String selectedUserId,String selectedUserName) async {
 String roomId = const Uuid().v4();
 final CollectionReference usersCollection =
 FirebaseFirestore.instance.collection('Users');
 print("Sending.... RoomId to Callee ( $selectedUserId ) : $roomId ");
 await usersCollection.doc(selectedUserId).update({'roomId': roomId});
 print("RoomId sendeddddddddd");
 if (mounted) {
   Navigator.push(
     context,
     MaterialPageRoute(
       builder: (_) => CallerScreen(roomId: roomId,userId: selectedUserId,userName: selectedUserName,),
     ),
   );
 }
}
  • WebRTC Functions and Variables in Caller Screen:
class CallerScreen extends StatefulWidget {
 final String? roomId;
 final String? userId;
 const CallerScreen({super.key, this.roomId, this.userId});


 @override
 State<CallerScreen> createState() => _CallerScreenState();
}


class _CallerScreenState extends State<CallerScreen> {
 final _localRenderer = RTCVideoRenderer();
 final _remoteRenderer = RTCVideoRenderer();


 RTCPeerConnection? _peerConnection;
 MediaStream? _localStream;
 String? roomId;
 String? userId;
 CollectionReference rooms = FirebaseFirestore.instance.collection("rooms");
 bool micEnabled = true;
 bool _isRemoteVideoReady = false;
 bool _isFrontCamera = true;


 @override
 void initState() {
   super.initState();
   userId= widget.userId;
   roomId = widget.roomId; // ✅ initialize here
   print("Initstate RoomId : $roomId userId : $userId");
   initRenderers();
   requestPermissions();
 }


 Future<void> requestPermissions() async {
   await [Permission.camera, Permission.microphone].request();
 }


 @override
 void dispose() {
   _localRenderer.dispose();
   _remoteRenderer.dispose();
   _peerConnection?.close();
   if (roomId != null) {
     rooms.doc(roomId).delete();
   }
   print("Disposed");
   super.dispose();
 }


 Future<void> initRenderers() async {
   print("Initializing renderers");
   await _localRenderer.initialize();
   await _remoteRenderer.initialize();
   print("Renderers initialized");
 }


 Future<void> openUserMedia() async {
   print("openUsermedia IN");
   final stream = await navigator.mediaDevices.getUserMedia({
     'video': {
       'mandatory': {
         'minWidth': '640',
         'minHeight': '480',
         'minFrameRate': '30',
       },
       'facingMode': _isFrontCamera ? 'user' : 'environment'
     },
     'audio': true,
   });
   _localRenderer.srcObject = stream;
   _localStream = stream;
   print("openUserMedia OUT");
 }


 Future<RTCPeerConnection> createPeerConnectionAndSetup() async {
   print("creating ...Peerconnection");
   final pc = await createPeerConnection({
     'iceServers': [
       {
         'urls': 'stun:stun.l.google.com:19302'
       }
     ]
   });
   print("PeerConnection created");


   pc.onIceCandidate = (candidate) async {
     print("onIceCandidate fired");
     if (candidate != null && roomId != null) {
       final candidateData = candidate.toMap();
       await rooms.doc(roomId).collection('candidates').add(candidateData);
       print("Local ICE candidate added to Firestore");
     }
   };


   pc.onTrack = (event) {
     print("onTrack event received with ${event.streams.length} streams");
     if (event.streams.isNotEmpty) {
       setState(() {
         _remoteRenderer.srcObject = event.streams.first;
         _isRemoteVideoReady = true;
       });
       print("Remote video stream set");
     }
   };


   _localStream?.getTracks().forEach((track) {
     pc.addTrack(track, _localStream!);
   });
   print("Local tracks added to PeerConnection");


   return pc;
 }


 /// Function to send notification
 Future<void> sendCallNotification(String selectedUserId, String roomId) async {
   print("Sending call notification to user: $selectedUserId with room: $roomId");


   try {
     // Get current user info
     final currentUser = FirebaseAuth.instance.currentUser;
     if (currentUser == null) return;


     // Get current user's name
     final currentUserDoc = await FirebaseFirestore.instance
         .collection('Users')
         .doc(currentUser.uid)
         .get();
     final callerName = currentUserDoc.data()?['name'] ?? 'Unknown';
     print("HTTP req sendingggg........");


     // Send notification via Cloud Function
     final response = await http.post(
       Uri.parse(
           'https://us-central1-webrtc-calling-2559f.cloudfunctions.net/notificationStartAppointment'),
       headers: {'Content-Type': 'application/json'},
       body: json.encode({
         'data': {
           'callerName': callerName,
           'roomName': roomId,
           'userId': selectedUserId,
           'callerId': currentUser.uid,
         }
       }),
     );


     if (response.statusCode == 200) {
       print("Call notification sent successfully");
     } else {
       print("Failed to send notification: ${response.body}");
     }
   } catch (e) {
     print("Error sending call notification: $e");
   }
 }


 Future<void> createRoom() async {
   print("Room creatingg...");
   await openUserMedia();
   _peerConnection = await createPeerConnectionAndSetup();


   final offer = await _peerConnection!.createOffer();
   await _peerConnection!.setLocalDescription(offer);


   //roomId = Uuid().v4();
   await rooms.doc(roomId).set({'offer': offer.toMap()});
   print("Room created with ID: $roomId userId : $userId (share this with callee)");


   // ✅ Send call notification to the selected user
   await sendCallNotification(userId!,roomId!);
   print("FCM notification SENDdddddddd");


   //Listen for answer
   rooms.doc(roomId).snapshots().listen((snapshot) async {
     final data = snapshot.data() as Map<String, dynamic>?;
     if (data != null && data['answer'] != null) {
       final answer = data['answer'] as Map<String, dynamic>;
       await _peerConnection!.setRemoteDescription(
         RTCSessionDescription(answer['sdp'], answer['type']),
       );
       print("Remote description set (Answer received)");
     }
   });
   print("Listening for Answer...");


   // Listen for remote ICE candidates
   rooms.doc(roomId).collection('candidates').snapshots().listen((snap) {
     for (var doc in snap.docChanges) {
       if (doc.type == DocumentChangeType.added) {
         final data = doc.doc.data()!;
         _peerConnection!.addCandidate(RTCIceCandidate(
             data['candidate'], data['sdpMid'], data['sdpMLineIndex']));
         print("Remote ICE candidate added");
       }
     }
   });
   print("Listening for remote ICE candidates...");
 }


 void toggleMic() {
   print("toggleMic");
   _localStream?.getAudioTracks().forEach((track) {
     track.enabled = !track.enabled;
   });
   setState(() {
     micEnabled = !micEnabled;
   });
   print("Mic toggled: $micEnabled");
 }


 Future<void> switchCamera() async {
   print("Switch cam");


   _localStream?.getVideoTracks().forEach((track) {
     track.stop();
   });


   setState(() {
     _isFrontCamera = !_isFrontCamera;
   });


   final newStream = await navigator.mediaDevices.getUserMedia({
     'video': {
       'mandatory': {
         'minWidth': '640',
         'minHeight': '480',
         'minFrameRate': '30',
       },
       'facingMode': _isFrontCamera ? 'user' : 'environment'
     },
     'audio': true,
   });


   _localRenderer.srcObject = newStream;


   if (_peerConnection != null) {
     final senders = await _peerConnection!.getSenders();
     for (var sender in senders) {
       if (sender.track?.kind == 'video') {
         await _peerConnection!.removeTrack(sender);
       }
     }


     newStream.getVideoTracks().forEach((track) {
       _peerConnection!.addTrack(track, newStream);
     });
   }


   _localStream = newStream;


   print("cam switched to ${_isFrontCamera ? 'front' : 'back'}");
 }


 void endCall() {
   print("end call");
   _peerConnection?.close();
   _localStream?.getTracks().forEach((track) {
     track.stop();
   });
   _localRenderer.srcObject = null;
   _remoteRenderer.srcObject = null;
   if (roomId != null) rooms.doc(roomId).delete();
   setState(() {
     roomId = null;
     _isRemoteVideoReady = false;
   });
   print("ended Call");
   if (mounted) {
     Navigator.pushReplacement(
       context,
       MaterialPageRoute(builder: (_) => const CallerListScreen()),
     );
   }
 }




 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title:Text(
       "TalkOn CalleR",
       style: TextStyle(
       fontSize: 20,
       fontWeight: FontWeight.bold,
       color: Colors.blue.shade700,
   ),)
     ),
     body: Column(
       children: [
         Expanded(
           child: Stack(
             children: [
               RTCVideoView(_remoteRenderer),


               Positioned(
                 right: 20,
                 bottom: 20,
                 width: 120,
                 height: 180,
                 child: Container(
                   decoration: BoxDecoration(
                     border: Border.all(color: Colors.white, width: 2),
                     borderRadius: BorderRadius.circular(8),
                   ),
                   child: RTCVideoView(_localRenderer, mirror: _isFrontCamera),
                 ),
               ),


               if (!_isRemoteVideoReady)
                 Positioned.fill(
                   child: Container(
                     color: Colors.black54,
                     child: Center(
                       child: Column(
                         mainAxisAlignment: MainAxisAlignment.center,
                         children: [
                           CircularProgressIndicator(),
                           SizedBox(height: 16),
                           Text(
                             "Waiting for remote video...",
                             style:
                             TextStyle(color: Colors.white, fontSize: 16),
                           ),
                           if (roomId != null)
                             Padding(
                               padding: EdgeInsets.only(top: 8),
                               child: Text(
                                 "Room ID: $roomId",
                                 style: TextStyle(
                                     color: Colors.white70, fontSize: 14),
                               ),
                             ),
                         ],
                       ),
                     ),
                   ),
                 ),
             ],
           ),
         ),
         Padding(
           padding: const EdgeInsets.all(16.0),
           child: Row(
             mainAxisAlignment: MainAxisAlignment.spaceEvenly,
             children: [
               ElevatedButton.icon(
                 onPressed: roomId != null ? createRoom : null,
                 icon: const FaIcon(FontAwesomeIcons.video, color: Colors.green),
                 label: const Text("Call"),
               ),
               IconButton(
                 icon: Icon(micEnabled ? Icons.mic : Icons.mic_off),
                 onPressed: toggleMic,
               ),
               IconButton(
                 icon: Icon(Icons.switch_camera),
                 onPressed: switchCamera,
               ),
               IconButton(
                 icon: Icon(Icons.call_end, color: Colors.red),
                 onPressed: endCall,
               ),
             ],
           ),
         )
       ],
     ),
   );
 }
}

Building robust, real-time video calling applications requires not only a deep understanding of WebRTC and signaling workflows but also strong expertise in designing intuitive, high-performance mobile interfaces. At Sreyas IT Solutions, our team brings extensive experience in Flutter, advanced UI/UX engineering, and real-time communication systems to deliver seamless caller–receiver flows, responsive layouts, and optimized in-call interactions. By combining Flutter’s cross-platform capability with the reliability of flutter_webrtc and Firebase, we ensure a smooth, low-latency calling experience across both Android and iOS.

This Caller App implementation forms the foundation of a scalable communication platform. To complete the system, refer to the companion article, “Building the Receiver App & Push Notification System for WebRTC Video Calls in Flutter,” where we delve into handling incoming calls, push notifications, and end-to-end call lifecycle management.

Recent Blogs


Posted

in

by

Tags:

To Know Us Better

Browse through our work.

Explore The Technology Used

Learn about the cutting-edge technology and techniques we use to create innovative software solutions.