Code With Bisky

Building a FREE Video Call App with Flutter and Appwrite | Step-by-Step Tutorial [26]

Key Topics covered:

  • Flutter WebRTC
  • Initiate Video Call
  • Listening to An Incoming Video Call

Description:

In this tutorial, we'll earn how to build a fully functional video call app using Flutter, the powerful flutter_webrtc dependency, and the versatile Appwrite backend. Say goodbye to expensive video conferencing solutions and hello to a seamless and cost-effective video communication experience!

We will start by Video Call State model

Models:

  • lib/model
  • VideoCallState.dart

Code Snippet(VideoCallState.dart):


import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/pages/dashboard/chat/video_calls/one_to_one/VideoCallViewModel.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'VideoCallState.freezed.dart';
@freezed
class VideoCallState with _$VideoCallState {
  factory VideoCallState({
    @Default('') String myUserId,
    @Default(null) String? roomId,
    @Default(false) bool isOnCall,
    @Default(true) bool micOn,
    @Default(true) bool speakerOn,
    @Default(true) bool isCaller,
    @Default(null) String? sessionType,
    @Default(null) String? sessionDescription,
    @Default(null) UserAppwrite? friend,
    @Default(true) bool addCandidatesIntoList,
    @Default(false) bool calleeAnswer,
    @Default('video') String callerType,
    @Default(CallState.newCall) CallState callState,
  }) = _VideoCallState;
}

 

Code Snippet(pubspecyaml.dart):


//add the following dependencies to keep the screen active while you are on video call
dependencies:
    ........
  wakelock: ^0.6.2
  wakelock_windows: ^0.2.1
  circular_image: ^0.0.6

dependency_overrides:
  win32: ^5.0.0
 

Resize User Image:

  • lib/widget
  • UserImage.dart

Code Snippet(UserImage.dart):


  double? radius; // add new variable to change image size
  UserImage(this.userId, {this.radius = 20,super.key});


 // remove width of the SizedBox in build()
 // Do these modification in memberProfilePicture() method
  Row(
      mainAxisAlignment: MainAxisAlignment.center,   // add  mainAxisAlignment: MainAxisAlignment.center, to the Row
  children: [
   CircleAvatar(
            radius: radius,  // replace with radius variable on CircleAvatar
            ..............
 

Run the following command flutter packages pub run build_runner build

We need to a package video_calls/one_to_one. Create below classes to handle video call. We can have a view model and screen page that's all. Remeber that we are using riverpod

  • lib/pages/dashboard/video_calls/one_to_one
  • VideoCallViewModel.dart
  • VideoCallVMScreen.dart

Code Snippet(VideoCallViewModel.dart):


import 'dart:async';
import 'dart:convert';

import 'package:chat_with_bisky/constant/strings.dart';
import 'package:chat_with_bisky/core/providers/RealtimeNotifierProvider.dart';
import 'package:chat_with_bisky/core/providers/RoomRepositoryProvider.dart';
import 'package:chat_with_bisky/model/CandidateModel.dart';
import 'package:chat_with_bisky/model/RTCSessionDescriptionModel.dart';
import 'package:chat_with_bisky/model/RealtimeNotifier.dart';
import 'package:chat_with_bisky/model/RoomAppwrite.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/model/VideoCallState.dart';
import 'package:flutter/material.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:realm/realm.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'VideoCallViewModel.g.dart';

enum CallState {
  newCall,
  connected,
  endCall,
}
@riverpod
class VideoCallViewModel extends _$VideoCallViewModel {

  RoomRepositoryProvider get _roomRepository =>
// ignore: avoid_manual_providers_as_generated_provider_dependency
      ref.read(roomRepositoryProvider);
// ignore: avoid_public_notifier_properties
  ValueChanged<CallState>? onStateChange;
// ignore: avoid_public_notifier_properties
  ValueChanged<MediaStream>? onLocalStream;
// ignore: avoid_public_notifier_properties
  ValueChanged<bool>? onHangUp;
// ignore: avoid_public_notifier_properties
  ValueChanged<MediaStream>? onAddRemoteStream;
// ignore: avoid_public_notifier_properties
  ValueChanged<MediaStream>? onRemoveRemoteStream;
// ignore: avoid_public_notifier_properties
  Timer? countdownTimer;
  final _peerConnections = <String, RTCPeerConnection>{};
  final _remoteCandidates = [];
  final List<RTCIceCandidate> _localCandidates = [];
// ignore: avoid_public_notifier_properties
  StreamSubscription? hangupSub;
  MediaStream? _localStream;
  List<MediaStream>? _remoteStreams;
// ignore: avoid_public_notifier_properties
  RTCPeerConnection? peerConnection;

  @override
  VideoCallState build() {
    return VideoCallState();
  }
  void setRoomId(String? input) {
    state = state.copyWith(roomId: input);
  }
  void setMyUserId(String input) {
    state = state.copyWith(myUserId: input);
  }
  void setMicStatus(bool input) {
    state = state.copyWith(micOn: input);
  }
  void setSpeaker(bool input) {
    state = state.copyWith(speakerOn: input);
  }
  void setCallActiveStatus(bool input) {
    state = state.copyWith(isOnCall: input);
  }
  void setCaller(bool input) {
    state = state.copyWith(isCaller: input);
  }
  void setSessionType(String input) {
    state = state.copyWith(sessionType: input);
  }
  void setSessionDescription(String input) {
    state = state.copyWith(sessionDescription: input);
  }
  void setAddCandidatesIntoList(bool input) {
    state = state.copyWith(addCandidatesIntoList: input);
  }
  void setCalleeAnswerStatus(bool input) {
    state = state.copyWith(calleeAnswer: input);
  }
  void setCallerType(String input) {
    state = state.copyWith(callerType: input);
  }
  void setCallState(CallState input) {
    state = state.copyWith(callState: input);
  }
  void setFriend(UserAppwrite friend) {
    state = state.copyWith(friend: friend);
  }

  final Map<String, dynamic> _iceServers = {
    'iceServers': [
      {'url': 'stun:stun.l.google.com:19302'},
      { 'url': 'stun:stun1.l.google.com:19302' },
      { 'url': 'stun:stun2.l.google.com:19302' },
      { 'url': 'stun:stun3.l.google.com:19302' },
      {
        'url': 'turn:144.91.113.238:3478?transport=udp',
        'username': 'codewithbisky',
        'credential': 'webrtc'
      },
    ]
  };

  final Map<String, dynamic> _config = {
    'mandatory': {},
    'optional': [
      {'DtlsSrtpKeyAgreement': true},
    ],
  };

  final Map<String, dynamic> _constraints = {
    'mandatory': {
      'OfferToReceiveVideo': true,
      'OfferToReceiveAudio': true,
    },
    'optional': [],
  };


  void createRoomAndInitiateCall(String friendUserId) async {
    if (onStateChange != null) {
      onStateChange!(CallState.newCall);
    }
    _createPeerConnection(friendUserId).then((peerConection) async {
      _peerConnections[friendUserId] = peerConection;
      peerConnection = peerConection;
      await _createOffer(friendUserId, peerConection);
      initiateCountDownTimer();
    }, onError: (e) {
      print(e);
    });
  }

  Future<RTCPeerConnection> _createPeerConnection(id) async {
    _localStream = await createStream();
    RTCPeerConnection peerConnection = await createPeerConnection(_iceServers, _config);

    peerConnection.onIceCandidate = (RTCIceCandidate candidate) {
      _localCandidates.add(candidate);
    };
    peerConnection.onAddStream = (stream) {
      if (onAddRemoteStream != null) onAddRemoteStream!(stream);
    };
    peerConnection.onRemoveStream = (stream) {
      if (onRemoveRemoteStream != null) onRemoveRemoteStream!(stream);
      _remoteStreams!.removeWhere((MediaStream it) {
        return (it.id == stream.id);
      });
    };

    _localStream?.getTracks().forEach((track) {
      peerConnection.addTrack(track, _localStream!);
    });
    return peerConnection;
  }

  _createOffer(String id, RTCPeerConnection peerConnection) async {
    try {
      RTCSessionDescription s = await peerConnection.createOffer(_constraints);
      peerConnection.setLocalDescription(s);
      final id = ObjectId().hexString;
      RoomAppwrite room = RoomAppwrite(
          roomId: id,
          callType: state.callerType,
          groupCall: false,
          callerUserId: state.myUserId,
          calleeUserId: state.friend?.userId ?? "");

      setRoomId(id);
      await _roomRepository.createNewRoom(room);
      await _roomRepository.addRtcSessionDescription(
          room, s);
      _syncChangesCaller(state.roomId!, room);
    } catch (e) {
      print(e.toString());
    }
  }




  Future<MediaStream> createStream() async {
    final Map<String, dynamic> mediaConstraints = {
      'audio': true,
      'video': {
        'minWidth': '640',
        'minHeight': '480',
        'minFrameRate': '24',
        'facingMode': 'user',
        'optional': [],
      }
    };
    MediaStream stream =
    await navigator.mediaDevices.getUserMedia(mediaConstraints);
    if (onLocalStream != null) {
      onLocalStream!(stream);
    }
    return stream;
  }


  listenToCallerEvents() {
    _syncChangesCallee(state.roomId!);
  }

  void initiateCountDownTimer() {

    countdownTimer = Timer(const Duration(minutes: 1), () {
      deleteRoom();
      if (!state.isCaller) {
        FlutterRingtonePlayer.stop();
      }
      onHangUp!(true);
    });
  }


  void deleteRoom() async {
    if (state.roomId != null) {
      await _roomRepository.deleteRoom(state.roomId ?? "");
    }
  }

  joinRoom(String sessionDescription, String sessionType) async {
    try {
      if (onStateChange != null) {
        onStateChange!(CallState.newCall);
      }
      String friendUserId = state.friend?.userId ?? "";
      RTCPeerConnection peerConnection = await _createPeerConnection(friendUserId);
      peerConnection = peerConnection;
      _peerConnections[friendUserId] = peerConnection;
      await peerConnection.setRemoteDescription(
          RTCSessionDescription(sessionDescription, sessionType));
      RTCSessionDescription s = await peerConnection.createAnswer(_constraints);
      peerConnection.setLocalDescription(s);
      _sendAnswerAndCalleeCandidates(s);
      if (_remoteCandidates.isNotEmpty) {
        _remoteCandidates.forEach((candidate) async {
          await peerConnection.addCandidate(candidate);
        });
        _remoteCandidates.clear();
      }
    } catch (e) {
      print('JoinVideoException $e');
    }
  }

  void _sendAnswerAndCalleeCandidates(RTCSessionDescription answer) async {
    RoomAppwrite? room = await _roomRepository.getRoom(state.roomId!);
    if (room != null) {
      await _roomRepository.addRtcSessionDescription(room, answer);
      if (_localCandidates.isNotEmpty) {
        for (RTCIceCandidate candidate in _localCandidates) {
          await _roomRepository.addCalleeCandidates(room, candidate);
        }
      }
    }
  }



  void _syncChangesCaller(String roomId, RoomAppwrite room) async {
    ref.listen<RealtimeNotifier?>(
// ignore: avoid_manual_providers_as_generated_provider_dependency
        realtimeNotifierProvider.select((value) => value.asData?.value),
            (_, next) async {
          if (next?.document.$collectionId == Strings.collectionRoomId) {


            final roomState = RoomAppwrite.fromJson(next!.document.data);
            if (roomState.roomId == roomId) {

              if (next.type == RealtimeNotifier.delete) {
                onHangUp!(true);
                return;
              }

              final rtcSessionDescription = roomState.rtcSessionDescription;

              if (rtcSessionDescription != null) {
                List<dynamic> jsonData = json.decode(rtcSessionDescription);
                List<RTCSessionDescriptionModel> listRTCDescriptions = jsonData
                    .map((item) => RTCSessionDescriptionModel.fromJson(item))
                    .toList();

                if (roomState.status == 'busy') {
                  return;
                }

                if (state.calleeAnswer == false) {
                  RTCSessionDescriptionModel? rtcSessionDescriptionAnswer =
                      listRTCDescriptions
                          .where((element) => element.type == 'answer')
                          .firstOrNull;
                  if (rtcSessionDescriptionAnswer != null) {
                    setCalleeAnswerStatus(true);
                    var answer = RTCSessionDescription(
                      rtcSessionDescriptionAnswer.description,
                      rtcSessionDescriptionAnswer.type,
                    );
                    await peerConnection?.setRemoteDescription(answer);
                    if (_localCandidates.isNotEmpty) {
                      for (RTCIceCandidate candidate in _localCandidates) {
                        await _roomRepository.addCallerCandidates(room, candidate);
                      }
                    }
                    setAddCandidatesIntoList(false);
                  }
                }
              }
            }
          } else if (next?.document.$collectionId == Strings.collectionCalleeId) {
            // Listen for remote Ice candidates below
            final calleeCandidate = CandidateModel.fromJson(next!.document.data);
            if (calleeCandidate.roomId == roomId &&
                next.type == RealtimeNotifier.create) {
              var id = state.friend?.userId ?? "";
              var pc = _peerConnections[id];
              RTCIceCandidate candidate = RTCIceCandidate(
                calleeCandidate.candidate,
                calleeCandidate.sdpMid,
                calleeCandidate.sdpMLineIndex,
              );
              if (pc != null) {
                await pc.addCandidate(candidate);
              } else {
                _remoteCandidates.add(candidate);
              }

              if (onStateChange != null) {
                onStateChange!(CallState.connected);
              }

            }
          }
        });
  }

  void _syncChangesCallee(String roomId) async {
    ref.listen<RealtimeNotifier?>(
// ignore: avoid_manual_providers_as_generated_provider_dependency
        realtimeNotifierProvider.select((value) => value.asData?.value),
            (_, next) async {

          if (next?.document.$collectionId == Strings.collectionCallerId) {
            final callerCandidate = CandidateModel.fromJson(next!.document.data);

            if (callerCandidate.roomId == roomId &&
                next.type == RealtimeNotifier.create) {
              var id = state.friend?.userId ?? "";
              var pc = _peerConnections[id];
              RTCIceCandidate candidate = RTCIceCandidate(
                callerCandidate.candidate,
                callerCandidate.sdpMid,
                callerCandidate.sdpMLineIndex,
              );
              if (pc != null) {
                await pc.addCandidate(candidate);
              } else {
                _remoteCandidates.add(candidate);
              }

              if (onStateChange != null) {
                onStateChange!(CallState.connected);
              }
            }
          } else if (next?.document.$collectionId == Strings.collectionRoomId &&
              next?.type != RealtimeNotifier.delete) {
            final roomState = RoomAppwrite.fromJson(next!.document.data);

            if (roomState.roomId == roomId &&
                next.type == RealtimeNotifier.delete) {
              onHangUp!(true);
            }
          }
        });
  }



  closeStreams() {
    if (_localStream != null) {
      _localStream?.dispose();
      _localStream = null;
    }
    hangupSub?.cancel();
    _peerConnections.forEach((key, pc) {
      pc.close();
    });
  }


}



 

Code Snippet(VideoCallVMScreen.dart):


import 'dart:ui';

import 'package:chat_with_bisky/core/util/Util.dart';
import 'package:chat_with_bisky/model/UserAppwrite.dart';
import 'package:chat_with_bisky/model/VideoCallState.dart';
import 'package:chat_with_bisky/pages/dashboard/chat/video_calls/one_to_one/VideoCallViewModel.dart';
import 'package:chat_with_bisky/service/LocalStorageService.dart';
import 'package:chat_with_bisky/widget/UserImage.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:wakelock/wakelock.dart';

class VideoCallVMScreen extends ConsumerStatefulWidget {
  final UserAppwrite friend;
  final bool isCaller;
  final String? sessionDescription;
  final String? sessionType;
  final String selId;
  final String? roomId;

  const VideoCallVMScreen(
      {super.key,
      required this.friend,
      required this.isCaller,
      required this.sessionDescription,
      required this.selId,
      required this.sessionType,
      this.roomId});

  @override
  _VideoCallScreenState createState() => _VideoCallScreenState();
}

class _VideoCallScreenState extends ConsumerState<VideoCallVMScreen> {
  VideoCallViewModel? _notifier;
  VideoCallState? _state;
  final RTCVideoRenderer _localRenderer = RTCVideoRenderer();
  final RTCVideoRenderer _remoteRenderer = RTCVideoRenderer();
  MediaStream? _localStream;

  @override
  initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => initialization());
  }

  initialization() {
    LocalStorageService.putString(LocalStorageService.callInProgress, 'YES');
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
    if (!widget.isCaller) {
      FlutterRingtonePlayer.playRingtone();
    }
    initRenderers();
    _connect();
    if (!widget.isCaller) {
      _notifier?.listenToCallerEvents();
      _notifier?.initiateCountDownTimer();
    }
    Wakelock.enable();
  }

  initRenderers() async {
    await _localRenderer.initialize();
    await _remoteRenderer.initialize();
  }

  @override
  deactivate() {
    super.deactivate();
    if (_notifier != null) _notifier?.closeStreams();
    _localRenderer.dispose();
    _remoteRenderer.dispose();
  }

  @override
  void dispose() {
    _notifier?.hangupSub?.cancel();
    _notifier?.countdownTimer?.cancel();
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
        overlays: SystemUiOverlay.values);
    if (!widget.isCaller) {
      FlutterRingtonePlayer.stop();
    }
    LocalStorageService.deleteKey(LocalStorageService.callInProgress);
    super.dispose();
    Wakelock.disable();
  }

  void _connect() async {

    initializeNotifier();

    initializeStateChangeListener();
    callsEventListeners();
    if (widget.isCaller) {
      _notifier?.createRoomAndInitiateCall(widget.friend.userId ?? "");
    }
  }

  @override
  Widget build(BuildContext context) {
    FocusScope.of(context).unfocus();
    _notifier = ref.read(videoCallViewModelProvider.notifier);
    _state = ref.watch(videoCallViewModelProvider);
    return Material(child: OrientationBuilder(builder: (context, orientation) {
      return Stack(children: [
        remoteVideoWidget(),
        callingBlur(),
        userProfilePicture(orientation),
        videoFullScreen(orientation),
        callButtons(),
      ]);
    }));
  }

  _endCall() {
    if (_notifier != null) {
      _notifier?.countdownTimer?.cancel();
      _notifier?.deleteRoom();
    }
    if (!widget.isCaller) {
      FlutterRingtonePlayer.stop();
    }
    Navigator.pop(context);
  }

  remoteVideoWidget() {
    return _state!.isOnCall
        ? Positioned(
            top: 0,
            left: 0,
            right: 0,
            bottom: 0,
            child: Container(
              margin: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
              width: MediaQuery.of(context).size.width,
              height: MediaQuery.of(context).size.height,
              decoration: const BoxDecoration(color: Colors.blue),
              child: RTCVideoView(
                _remoteRenderer,
                mirror: true,
                objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
              ),
            ),
          )
        : const SizedBox();
  }

  userProfilePicture(Orientation orientation) {
    return _state!.isOnCall
        ? const SizedBox()
        : Column(
            mainAxisSize: MainAxisSize.max,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: <Widget>[
              Padding(
                padding: EdgeInsets.symmetric(
                    vertical: orientation == Orientation.portrait ? 80 : 15),
                child: SizedBox(width: MediaQuery.of(context).size.width),
              ),
              UserImage(
                widget.friend.userId ?? "",
                radius: 60,
              ),
              const SizedBox(height: 10),
              Text(
                '${widget.friend.name}',
                style: const TextStyle(
                    fontWeight: FontWeight.bold, color: Colors.white),
              )
            ],
          );
  }

  videoFullScreen(Orientation orientation) {
    return _state!.isOnCall
        ? Positioned.directional(
            textDirection: Directionality.of(context),
            start: 20.0,
            top: 20.0,
            child: Card(
              shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(25),
              ),
              color: Colors.black,
              child: SizedBox(
                  width: orientation == Orientation.portrait ? 90.0 : 120.0,
                  height: orientation == Orientation.portrait ? 120.0 : 90.0,
                  child: ClipRRect(
                      borderRadius: BorderRadius.circular(25),
                      child: RTCVideoView(
                        _localRenderer,
                        mirror: true,
                        objectFit:
                            RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
                      ))),
            ),
          )
        : const SizedBox();
  }

  callButtons() {
    return Positioned(
      bottom: 40,
      left: 16,
      right: 16,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          widget.isCaller || _state!.isOnCall
              ? const SizedBox()
              : FloatingActionButton(
                  backgroundColor: Colors.green,
                  child: const Icon(Icons.call),
                  onPressed: () {
                    FlutterRingtonePlayer.stop();
                    _notifier?.countdownTimer?.cancel();
                    _notifier?.joinRoom(
                        widget.sessionDescription!, widget.sessionType!);
                    _notifier?.setCallActiveStatus(true);
                  }),
          _state!.isOnCall
              ? FloatingActionButton(
                  backgroundColor: Colors.orange,
                  onPressed: () {
                    _notifier?.setSpeaker(_state!.speakerOn ? false : true);
                    _localStream
                        ?.getAudioTracks()[0]
                        .enableSpeakerphone(_state!.speakerOn);
                  },
                  child: Icon(
                      _state!.speakerOn ? Icons.volume_up : Icons.volume_off),
                )
              : const SizedBox(),
          FloatingActionButton(
            onPressed: () => _endCall(),
            backgroundColor: Colors.red,
            child: const Icon(Icons.call_end),
          ),
          _state!.isOnCall
              ? FloatingActionButton(
                  backgroundColor: Colors.orange,
                  onPressed: () {
                    _notifier?.setMicStatus(_state!.micOn ? false : true);
                  },
                  child: Icon(_state!.micOn ? Icons.mic : Icons.mic_off),
                )
              : const SizedBox()
        ],
      ),
    );
  }

  callingBlur() {
    return _state!.isOnCall
        ? const SizedBox()
        : SizedBox(
            width: MediaQuery.of(context).size.width,
            height: MediaQuery.of(context).size.height,
            child: BackdropFilter(
              filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
              child: Container(
                decoration: BoxDecoration(color: Colors.black.withOpacity(0.3)),
              ),
            ),
          );
  }

  void callsEventListeners() {

    _notifier?.onLocalStream = ((stream) {
      if (mounted) {
        _localStream = stream;
        setState(() {
          _localRenderer.srcObject = _localStream;
        });
      }
    });


    _notifier?.onHangUp = (value) {
      if (value == true) {
        Navigator.pop(context);
      }
    };

    _notifier?.onAddRemoteStream = ((stream) {
      if (mounted) {
        setState(() {
          _notifier?.setCallActiveStatus(true);
          _remoteRenderer.srcObject = stream;
        });
      }
    });

    _notifier?.onRemoveRemoteStream = ((stream) {
      if (mounted) {
        setState(() {
          _notifier?.setCallActiveStatus(false);
          _remoteRenderer.srcObject = null;
        });
      }
    });
  }

  void initializeNotifier() {
    _notifier?.setCaller(widget.isCaller);
    _notifier?.setMyUserId(widget.selId);
    _notifier?.setFriend(widget.friend);
    _notifier?.setRoomId(widget.roomId);
  }

  void initializeStateChangeListener() {
    _notifier?.onStateChange = (CallState state) {
      switch (state) {
        case CallState.newCall:
          break;
        case CallState.connected:
          {
            if (mounted) {
              _notifier?.setCallActiveStatus(true);
              setState(() {
                _notifier?.countdownTimer?.cancel();
              });
            }
            break;
          }
        case CallState.endCall:
          setState(() {
            _localRenderer.srcObject = null;
            _remoteRenderer.srcObject = null;
            _notifier?.setCallActiveStatus(false);
          });
          break;
        default:
          break;
      }
    };
  }
}
 

Run the following command flutter packages pub run build_runner build

Call Another User:

Add a tap event to call another user in MessageScreen.dart

  • lib/widget
  • modify MessageScreen.dart

Code Snippet(MessageScreen.dart):


// import below class
import 'package:chat_with_bisky/pages/dashboard/chat/video_calls/one_to_one/VideoCallVMScreen.dart';
// add below code on video icon onTap() method
                        Navigator.of(context).push(MaterialPageRoute(
                            builder: (context) => VideoCallVMScreen(
                              isCaller: true,
                              friend: widget.friendUser,
                              sessionDescription: null,
                              sessionType: null,
                              selId: widget.myUserId,
                            )));
 

Listen To An Incoming Video Call:

Let's listen to an incoming video call on Dashboard

  • lib/pages/dashboard
  • DashboardPage.dart

Code Snippet(DashboardPage.dart):


// import these files
import 'dart:convert';
import 'package:chat_with_bisky/pages/dashboard/chat/video_calls/one_to_one/VideoCallVMScreen.dart';

  // modify _listenIncomingCalls to differintaite caller type , either a Video Call or Voice Call

if(roomState.callType == 'voice'){
                  Navigator.of(context)
                      .push(MaterialPageRoute(builder: (context) =>
                      VoiceCallingPage(
                        user: caller,
                        callStatus: CallStatus.ringing,
                        roomId: roomState.roomId ?? "",
                      )));
                }else if(roomState.callType == 'video'){

                  final rtcSessionDescription = roomState.rtcSessionDescription;

                  if (rtcSessionDescription != null) {
                    List<dynamic> jsonData = json.decode(rtcSessionDescription);
                    List<
                        RTCSessionDescriptionModel> listRTCDescriptions = jsonData
                        .map((item) =>
                        RTCSessionDescriptionModel.fromJson(item))
                        .toList();

                    RTCSessionDescriptionModel? rtcSessionDescriptionOffer = listRTCDescriptions.where((element) => element.type == 'offer').firstOrNull;
                    if(rtcSessionDescriptionOffer != null){

                      Navigator.of(context)
                          .push(MaterialPageRoute(builder: (context) =>
                          VideoCallVMScreen(
                            friend:caller,
                            isCaller: false,
                            sessionDescription: rtcSessionDescriptionOffer.description,
                            sessionType: rtcSessionDescriptionOffer.type,
                            selId: username,
                            roomId: roomState.roomId,)));
                    }

                  }
                }
 

Conclusion:

You've now mastered the art of building a seamless and cost-effective video call app from scratch using Flutter, flutter_webrtc, and Appwrite. Say hello to hassle-free video communication without breaking the bank. Start connecting with friends, family, or colleagues in real time, all within your own custom app.

Thank you for joining us on this exciting coding adventure! If you found this blog valuable, don't forget to share, and subscribe to our YouTube channel for more engaging Flutter content. Keep coding, keep innovating, and keep pushing the boundaries of what you can create.