flutteragora.ioagora-implementationrailwayagora

Flutter application is not showing remote view while joining the Agora Channel for video calls


I am developing an application in which user can contact each other via video calls. I have setup my server on railway by following the Agora documentation. If anyone has any suggestions or know what I am doing wrong please do let me know. I have tried leaving the token empty ('') but it still gives invalid token error. The token is getting generated successfully but when users join the call. Remote view is not showing up even though the onUserJoined callback is getting triggered perfectly on both caller and receiver side.

This is the call screen code which will enable users to contact with each other

// ignore_for_file: prefer_typing_uninitialized_variables, use_build_context_synchronously

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../constants/constants.dart';
import '../../global/firebase_ref.dart';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wakelock/wakelock.dart';

import '../../methods/call_methods.dart';
import '../../models/call_model.dart';
import '../../services/app_services.dart';
import '../../services/connectivity_services.dart';
import '../../services/user_services.dart';
import '../../widgets/custom_images.dart';
import '../../widgets/custom_widgets.dart';

class VideoCallScreen extends StatefulWidget {
  const VideoCallScreen(this.call, {Key? key}) : super(key: key);
  final CallModel call;

  @override
  State<VideoCallScreen> createState() => _VideoCallScreenState();
}

class _VideoCallScreenState extends State<VideoCallScreen> {
  final _users = <int>[];
  final _infoStrings = <String>[];
  bool muted = false;
  RtcEngine? _engine;
  bool isspeaker = true;
  bool isalreadyendedcall = false;
  String current = Get.find<UserServices>().adminid;
  CollectionReference? reference;
  String token = '';

  Stream<DocumentSnapshot>? stream;

  @override
  void dispose() {
    _users.clear();
    _engine!.leaveChannel();
    _engine!.release();
    super.dispose();
  }

  getToken() async {
    final url = Uri.parse(
      'https://agora-token-service-production-59a1.up.railway.app/rtc/${widget.call.channelid}/1/uid/0',
    );
    Get.log('Token URL $url');
    final response = await http.get(url);
    debugPrint('Response: $response');
    if (response.statusCode == 200) {
      setState(() {
        token = response.body;
        token = jsonDecode(token)['rtcToken'];
        Get.log('token: $token');
      });
    } else {
      Get.log('Failed to fetch the token');
    }
  }

  @override
  void initState() {
    initialize();
    super.initState();

    if (widget.call.by == current) {
      reference = userRef.doc(widget.call.receiver!.id).collection('History');
      stream = reference!.doc(widget.call.timeepoch.toString()).snapshots();
    } else {
      reference = adminRef.doc(widget.call.caller!.id).collection('History');
      stream = reference!.doc(widget.call.timeepoch.toString()).snapshots();
    }
  }

  Future<void> initialize() async {
    try {
      await [Permission.microphone, Permission.camera].request();
      await getToken();
      if (Get.find<AppServices>().appid.isEmpty) {
        setState(() {
          _infoStrings.add(
            'Agora_APP_IDD missing, please provide your Agora_APP_IDD in app_constant.dart',
          );
          _infoStrings.add('Agora Engine is not starting');
        });
        return;
      }

      await _initAgoraRtcEngine();
      _addAgoraEventHandlers();
      VideoEncoderConfiguration configuration = const VideoEncoderConfiguration(
        dimensions: VideoDimensions(height: 1920, width: 1080),
      );
      await _engine!.setVideoEncoderConfiguration(configuration);
      Get.log('Channel id: ${widget.call.channelid}');
      await _engine!.joinChannel(
        token: token,
        channelId: widget.call.channelid!,
        uid: 0,
        options: const ChannelMediaOptions(),
      );
    } catch (e) {
      Get.log('Catch: $e');
    }
  }

  Future<void> _initAgoraRtcEngine() async {
    _engine = createAgoraRtcEngine();
    await _engine!.initialize(
      RtcEngineContext(
        appId: Get.find<AppServices>().appid,
        channelProfile: ChannelProfileType.channelProfileCommunication,
      ),
    );
    // _engine = await RtcEngine.create(Get.find<AppServices>().agoraid);
    await _engine!.enableVideo();
    await _engine!.enableAudio();
    await _engine!.enableLocalVideo(true);
    await _engine!.enableLocalAudio(true);
    await _engine!.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
    Get.log('---engine----');
  }

  var remoteid;
  void _addAgoraEventHandlers() {
    _engine!.registerEventHandler(
      RtcEngineEventHandler(
        onError: (code, value) {
          setState(() {
            final info = 'onErrorCode: $code';
            _infoStrings.add(info);
            Get.log(info);
            final infp = 'onError: $value';
            _infoStrings.add(infp);
            Get.log(infp);
          });
        },
        onJoinChannelSuccess: (channel, elapsed) {
          setState(() {
            final info = 'onJoinChannel: $channel';
            _infoStrings.add(info);
            Get.log(info);
          });
          if (widget.call.caller!.id == current) {
            adminRef
                .doc(widget.call.caller!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'TYPE': 'OUTGOING',
              'ISVIDEOCALL': widget.call.video,
              'PEER': widget.call.receiver!.id,
              'TARGET': widget.call.receiver!.id,
              'TIME': widget.call.timeepoch,
              'DP': widget.call.receiver!.picture,
              'ISMUTED': false,
              'ISJOINEDEVER': false,
              'STATUS': 'calling',
              'STARTED': null,
              'ENDED': null,
              'CALLERNAME': widget.call.caller!.name,
              'CHANNEL': channel.channelId,
              'UID': channel.localUid,
            }, SetOptions(merge: true)).then(
              (value) => Get.log('added'),
            );
            userRef
                .doc(widget.call.receiver!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'TYPE': 'INCOMING',
              'ISVIDEOCALL': widget.call.video,
              'PEER': widget.call.caller!.id,
              'TARGET': widget.call.receiver!.id,
              'TIME': widget.call.timeepoch,
              'DP': widget.call.caller!.picture,
              'ISMUTED': false,
              'ISJOINEDEVER': true,
              'STATUS': 'missedcall',
              'STARTED': null,
              'ENDED': null,
              'CALLERNAME': widget.call.caller!.name,
              'CHANNEL': channel.channelId,
              'UID': channel.localUid,
            }, SetOptions(merge: true));
          }
          Wakelock.enable();
        },
        onLeaveChannel: (connection, stats) {
          // setState(() {
          _infoStrings.add('onLeaveChannel');
          _users.clear();
          // });
          if (isalreadyendedcall == false) {
            adminRef
                .doc(widget.call.caller!.id!)
                .collection("History")
                .doc(widget.call.timeepoch.toString())
                .set({
              'STATUS': 'ended',
              'ENDED': DateTime.now(),
              'ISMUTED': false,
              'UID': -1,
            }, SetOptions(merge: true));
            userRef
                .doc(widget.call.receiver!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'STATUS': 'ended',
              'ENDED': DateTime.now(),
              'ISMUTED': false,
              'UID': -1,
            }, SetOptions(merge: true));
            // //----------
            // userRef
            //     .doc(widget.call.receiver!.id)
            //     .collection('recent')
            //     .doc(widget.call.id)
            //     .set({
            //   'id': widget.call.caller!.id,
            //   'ENDED': DateTime.now().millisecondsSinceEpoch,
            //   'CALLERNAME': widget.call.receiver!.name,
            // }, SetOptions(merge: true));
          }
          Wakelock.disable();
        },
        onUserJoined: (connection, uid, elapsed) {
          setState(() {
            final info = 'userJoined: $uid';
            _infoStrings.add(info);
            _users.add(uid);
            Get.log(info);
            remoteid = uid;
            Get.log(remoteid);
          });
          startTimerNow();
          if (Get.find<UserServices>().adminid == widget.call.caller!.id) {
            adminRef
                .doc(widget.call.caller!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'STARTED': DateTime.now(),
              'STATUS': 'pickedup',
              'ISJOINEDEVER': true,
            }, SetOptions(merge: true));
            userRef
                .doc(widget.call.receiver!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'STARTED': DateTime.now(),
              'STATUS': 'pickedup',
            }, SetOptions(merge: true));
          }
          Wakelock.enable();
        },
        onUserOffline: (connection, uid, elapsed) {
          setState(() {
            final info = 'userOffline: $uid';
            _infoStrings.add(info);
            _users.remove(uid);
            Get.log(info);
            remoteid = null;
          });
          if (isalreadyendedcall == false) {
            adminRef
                .doc(widget.call.caller!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'STATUS': 'ended',
              'ENDED': DateTime.now(),
              'ISMUTED': false,
              'UID': -1,
            }, SetOptions(merge: true));
            userRef
                .doc(widget.call.receiver!.id!)
                .collection('History')
                .doc(widget.call.timeepoch.toString())
                .set({
              'STATUS': 'ended',
              'ENDED': DateTime.now(),
              'ISMUTED': false,
              'UID': -1,
            }, SetOptions(merge: true));
            //----------
            // userRef
            //     .doc(widget.call.receiver!.id)
            //     .collection('recent')
            //     .doc(widget.call.id)
            //     .set({
            //   'id': widget.call.caller!.id,
            //   'ENDED': DateTime.now().millisecondsSinceEpoch,
            //   'CALLERNAME': widget.call.receiver!.name,
            // }, SetOptions(merge: true));
          }
        },
        onFirstRemoteVideoFrame: (connection, uid, width, height, elapsed) {
          setState(() {
            final info = 'firstRemoteVideo: $uid ${width}x $height';
            _infoStrings.add(info);
            Get.log(info);
          });
        },
        onTokenPrivilegeWillExpire: (connection, string) async {
          await getToken();
          await _engine!.renewToken(token);
        },
      ),
    );
  }

  void onCallEnd(BuildContext context) async {
    await CallMethods.endCall(call: widget.call);
    DateTime now = DateTime.now();
    if (isalreadyendedcall == false) {
      await adminRef
          .doc(widget.call.caller!.id!)
          .collection('History')
          .doc(widget.call.timeepoch.toString())
          .set({
        'STATUS': 'ended',
        'ENDED': now,
        'ISMUTED': false,
        "UID": -1,
      }, SetOptions(merge: true));
      await userRef
          .doc(widget.call.receiver!.id!)
          .collection('History')
          .doc(widget.call.timeepoch.toString())
          .set({
        'STATUS': 'ended',
        'ENDED': now,
        'ISMUTED': false,
        'UID': -1,
      }, SetOptions(merge: true));
      // //----------
      // userRef
      //     .doc(widget.call.receiver!.id)
      //     .collection('recent')
      //     .doc(widget.call.id)
      //     .set({
      //   'id': widget.call.caller!.id,
      //   'ENDED': DateTime.now().millisecondsSinceEpoch,
      //   'CALLERNAME': widget.call.receiver!.name,
      // }, SetOptions(merge: true));
    }
    Wakelock.disable();
    Navigator.pop(context);
  }

  Widget callView({
    String status = 'calling',
    bool muted = false,
    int? remoteuid,
  }) {
    var w = MediaQuery.of(context).size.width;
    var h = MediaQuery.of(context).size.height;
    return Container(
      alignment: Alignment.center,
      decoration: status == 'pickedup'
          ? null
          : BoxDecoration(
              image: DecorationImage(
                fit: BoxFit.cover,
                image: providerImage(
                  widget.call.caller!.id == current
                      ? widget.call.receiver!.picture ?? ''
                      : widget.call.caller!.picture ?? '',
                ),
              ),
            ),
      child: Container(
        color: status == 'pickedup' ? null : Colors.white.withOpacity(0.3),
        child: Stack(
          alignment: Alignment.center,
          children: [
            status != 'pickedup'
                ? Column(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: [
                      Container(
                        width: w,
                        height: h / 5,
                        alignment: Alignment.center,
                        margin: EdgeInsets.only(
                            top: MediaQuery.of(context).padding.top),
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: const [
                                Icon(
                                  Icons.lock_rounded,
                                  size: 17,
                                  color: Colors.white38,
                                ),
                                SizedBox(width: 6),
                                Text(
                                  'End-to-end encrypted',
                                  style: TextStyle(
                                    color: Colors.white38,
                                    fontWeight: FontWeight.w400,
                                    fontFamily: AppStrings.opensans,
                                  ),
                                ),
                              ],
                            ).marginOnly(top: 50, bottom: 7),
                            SizedBox(
                              width: w / 1.1,
                              child: Text(
                                widget.call.caller!.id ==
                                        Get.find<UserServices>().adminid
                                    ? widget.call.receiver!.name!
                                    : widget.call.caller!.name!,
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                                textAlign: TextAlign.center,
                                style: const TextStyle(
                                  fontWeight: FontWeight.w500,
                                  fontSize: 27,
                                  fontFamily: AppStrings.opensans,
                                ),
                              ),
                            ),
                          ],
                        ),
                      ),
                      Text(
                        status == 'calling'
                            ? widget.call.receiver!.id ==
                                    Get.find<UserServices>().adminid
                                ? 'Connecting...'
                                : 'Calling...'
                            : status == 'pickedup'
                                ? '$hoursStr : $minutesStr: $secondsStr'
                                : status == 'ended'
                                    ? 'Call Ended'
                                    : status == 'rejected'
                                        ? 'Rejected'
                                        : 'Please wait...',
                        style: const TextStyle(
                          fontWeight: FontWeight.w500,
                          fontSize: 18,
                          fontFamily: AppStrings.opensans,
                        ),
                      ).marginOnly(bottom: 16, top: 10),
                      Stack(
                        children: [
                          widget.call.caller!.id ==
                                  Get.find<UserServices>().adminid
                              ? status == 'ended' || status == 'rejected'
                                  ? Container(
                                      height: w + (w / 11),
                                      width: w,
                                      color: Colors.white12,
                                      child: Icon(
                                        status == 'ended'
                                            ? Icons.person_off
                                            : status == 'rejected'
                                                ? Icons.call_end_rounded
                                                : Icons.person,
                                        size: 140,
                                      ),
                                    )
                                  : Container()
                              : status == 'ended' || status == 'rejected'
                                  ? Container(
                                      height: w + (w / 11),
                                      width: w,
                                      color: Colors.white12,
                                      child: Icon(
                                        status == 'ended'
                                            ? Icons.person_off
                                            : status == 'rejected'
                                                ? Icons.call_end_rounded
                                                : Icons.person,
                                        size: 140,
                                      ),
                                    )
                                  : Container(),
                          Positioned(
                            bottom: 20,
                            child: SizedBox(
                              width: w,
                              height: 20,
                              child: Center(
                                child: status == 'pickedup'
                                    ? muted == true
                                        ? const Text(
                                            'Muted',
                                            textAlign: TextAlign.center,
                                            style: TextStyle(
                                              fontWeight: FontWeight.w600,
                                              fontSize: 16,
                                              fontFamily: AppStrings.opensans,
                                            ),
                                          )
                                        : const SizedBox(height: 0)
                                    : const SizedBox(height: 0),
                              ),
                            ),
                          ),
                        ],
                      ),
                      SizedBox(height: h / 6),
                    ],
                  )
                : _engine == null
                    ? SizedBox()
                    : SizedBox(
                        child: AgoraVideoView(
                          controller: VideoViewController.remote(
                            rtcEngine: _engine!,
                            canvas: VideoCanvas(uid: remoteuid),
                            connection: RtcConnection(
                              channelId: widget.call.channelid!,
                            ),
                          ),
                        ),
                      ),
            if (status == 'pickedup')
              Positioned(
                top: 150,
                child: Text(
                  '$hoursStr: $minutesStr: $secondsStr',
                  style: const TextStyle(
                    fontWeight: FontWeight.w500,
                    fontSize: 18,
                    color: Colors.white,
                    fontFamily: AppStrings.opensans,
                  ),
                ),
              ),
            if (status != 'ended' || status != 'rejected')
              _engine == null
                  ? SizedBox()
                  : Align(
                      alignment: Alignment.bottomRight,
                      child: SizedBox(
                        width: 200,
                        height: 200,
                        child: AgoraVideoView(
                          controller: VideoViewController(
                            rtcEngine: _engine!,
                            canvas: const VideoCanvas(uid: 0),
                          ),
                        ),
                      ),
                    ),
          ],
        ),
      ),
    );
  }

  onToggleMute() {
    setState(() {
      muted = !muted;
    });
    _engine!.muteLocalAudioStream(muted);
    reference!
        .doc(widget.call.timeepoch.toString())
        .set({'ISMUTED': muted}, SetOptions(merge: true));
  }

  onSwitchCamera() => setState(() => _engine!.switchCamera());

  Widget toolbar({String status = 'calling'}) {
    return Container(
      alignment: Alignment.bottomCenter,
      padding: const EdgeInsets.symmetric(vertical: 35),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          status != 'ended' && status != 'rejected'
              ? SizedBox(
                  width: 65.67,
                  child: RawMaterialButton(
                    onPressed: onToggleMute,
                    shape: const CircleBorder(),
                    elevation: 2.0,
                    fillColor: muted ? Colors.blueAccent : Colors.white,
                    padding: const EdgeInsets.all(12.0),
                    child: Icon(
                      muted ? Icons.mic_off : Icons.mic,
                      color: muted ? Colors.white : Colors.blueAccent,
                      size: 22.0,
                    ),
                  ),
                )
              : const SizedBox(height: 42, width: 65.67),
          SizedBox(
            width: 65.67,
            child: RawMaterialButton(
              onPressed: () async {
                Get.log('--on call end---');
                setState(() {
                  isalreadyendedcall =
                      status == 'ended' || status == 'rejected' ? true : false;
                  onCallEnd(context);
                });
              },
              shape: const CircleBorder(),
              elevation: 2.0,
              fillColor: status == 'ended' || status == 'rejected'
                  ? Colors.black
                  : Colors.redAccent,
              padding: const EdgeInsets.all(15.0),
              child: Icon(
                status == 'ended' || status == 'rejected'
                    ? Icons.close
                    : Icons.call,
                color: Colors.white,
                size: 35.0,
              ),
            ),
          ),
          status == 'ended' || status == 'rejected'
              ? const SizedBox(width: 65.67)
              : SizedBox(
                  width: 65.67,
                  child: RawMaterialButton(
                    onPressed: onSwitchCamera,
                    shape: const CircleBorder(),
                    elevation: 2.0,
                    fillColor: Colors.white,
                    padding: const EdgeInsets.all(12.0),
                    child: const Icon(
                      Icons.switch_camera,
                      color: Colors.blueAccent,
                      size: 20.0,
                    ),
                  ),
                ),
        ],
      ),
    );
  }

  Widget panel() {
    return Container(
      padding: const EdgeInsets.symmetric(vertical: 48),
      alignment: Alignment.bottomCenter,
      child: FractionallySizedBox(
        heightFactor: 0.5,
        child: Container(
          padding: const EdgeInsets.symmetric(vertical: 48),
          child: ListView.builder(
            reverse: true,
            itemCount: _infoStrings.length,
            itemBuilder: (BuildContext context, int index) {
              if (_infoStrings.isEmpty) return const SizedBox();
              return Padding(
                padding:
                    const EdgeInsets.symmetric(vertical: 3, horizontal: 10),
                child: Text(_infoStrings[index]),
              );
            },
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Obx(
      () => Get.find<ConnectivityService>().connectionStatus.value ==
              ConnectivityResult.none
          ? const DisconnectedWidget()
          : Scaffold(
              body: Stack(
                children: [
                  _engine == null
                      ? Center(
                          child: Stack(
                            children: [callView(), panel(), toolbar()],
                          ),
                        )
                      : StreamBuilder<DocumentSnapshot>(
                          stream: stream as Stream<DocumentSnapshot>,
                          builder: (context, snapshot) {
                            if (snapshot.hasData &&
                                snapshot.data!.data() != null &&
                                snapshot.data != null) {
                              var doc = snapshot.data!;
                              Get.log(doc.toString());
                              return Center(
                                child: Stack(
                                  children: [
                                    callView(
                                      status: doc['STATUS'],
                                      muted: doc['ISMUTED'],
                                      remoteuid: doc['UID'],
                                    ),
                                    panel(),
                                    toolbar(status: doc['STATUS']),
                                  ],
                                ),
                              );
                            }
                            return Center(
                              child: Stack(
                                children: [callView(), panel(), toolbar()],
                              ),
                            );
                          },
                        ),
                ],
              ),
            ),
    );
  }
}

Solution

  • I didn't set it up with a rails server but it should work the same. On the flutter side im calling a function at the very first point:

    final token = await createToken(channelName, userId);
    

    channel name to identify the channel for the users and a userId of my user who should be able to join the channel.

      Future<dynamic> createToken(String channelName, int uid) async {
        try {
          //404
          // final response = await dio.get('${url}/api/video/create-token?agChannelName=$channelName&agRole=$role&agUid=$uid&agExpireTime=$expireTime');
    
          final response = await dio.get(
              '${url}/api/video/create-token?agChannelName=$channelName&agUid=$uid');
          print('res is ${response.data["token"]}');
          return response.data["token"];
        } on DioError catch (e) {
          // The request was made and the server responded with a status code
          // that falls out of the range of 2xx and is also not 304.
          if (e.response != null) {
            //print(HttpException(e.response!.data["message"]));
            return e.response!.data;
            //print(e.response!.headers);
            //print(e.response!.requestOptions);
          } else {
            // Something happened in setting up or sending the request that triggered an Error
            //print(e.requestOptions);
            print('get events:  ${e.message}');
          }
        }
      }
    

    On my server side where im using a javascript framework, im doing the following:

    ...
    
      const token = RtcTokenBuilder.buildTokenWithUid(
        process.env.AGORA_APP_ID,
        process.env.AGORA_APP_CERTIFICATE,
        channelName,
        uid,
        RtcRole.PUBLISHER,
        privilegeExpireTime
      );
      console.log(token)
      return res.status(201).json({ token: token });
    

    For that im using the agora-access-token library on npm https://www.npmjs.com/package/agora-access-token