Search code examples
androidflutteragora.iovideochat

Agora in Flutter- navigating to the video chat screen more than one time keeps local video loading forever


I am using Agora for a one-to-one video chat purpose in Flutter. User1 has an app to go online and user2 has another app to go online. After both of them go online, they can do video chat with one another. Both apps have almost similar codebase.

I have a screen or activity (say screen1) where an alert dialog is shown on tapping a button (say button1). On tapping the Continue button in the alert dialog, the dialog disappears and the user is taken to the screen (say screen2) where the video chat takes place. But after going to the video chat screen successfully, if the user taps on the back button on the mobile set then s/he is taken to screen1 and after tapping on button1, if the user taps on the Continue button in the popped up alert dialog, the user is again taken to screen2 but this time the local video (i.e. video of the user using the app) keeps loading for ever. Obviously I want the local video to load as it did for the first time.

I am gonna put my code here in such a way that you can easily run that.

Following code is for user1. For user2, no alert box is there in the app. Same code from user1 is used for user2 app, except the value of remoteUid is set to be 2 for user2 while this value is set to be 1 for user1. These are just two values identifying 2 users.

For user1:

main.dart:

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'livesession1to1.dart';

void main()  {

  runApp(MessagingExampleApp());
}

class NavigationService {
  static GlobalKey<NavigatorState> navigatorKey =
  GlobalKey<NavigatorState>();
}


/// Entry point for the example application.
class MessagingExampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Messaging Example App',
      navigatorKey: NavigationService.navigatorKey, // set property
      theme: ThemeData.dark(),
      routes: {
        '/': (context) => Application(),
        '/liveSession1to1': (context) =>LiveSession1to1(),
      },
    );
  }
}

int _messageCount = 0;

/// The API endpoint here accepts a raw FCM payload for demonstration purposes.
String constructFCMPayload(String? token, String server_key) {
  _messageCount++;
  return jsonEncode({
    'token': token,
    'to':token,
    'data': {
      'via': 'FlutterFire Cloud Messaging!!!',
      'count': _messageCount.toString(),
    },
    'notification': {
      'title': 'Hello FlutterFire!',
      'body': 'This notification (#$_messageCount) was created via FCM! =============',
    },
    "delay_while_idle" : false,
    "priority" : "high",
    "content_available" : true

  });
}

/// Renders the example application.
class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  String? _token;

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



  }


  showAlertDialog() {
    BuildContext context=NavigationService.navigatorKey.currentContext!;
    // set up the buttons
    Widget cancelButton = TextButton(
      child: Text("Cancel"),
      onPressed:  () {},
    );
    Widget continueButton = TextButton(
      child: Text("Continue"),
      onPressed:  () {

        Navigator.of(context, rootNavigator: true).pop();

        Navigator.of(context).pushNamed('/liveSession1to1');
      },
    );





    Timer? timer = Timer(Duration(milliseconds: 5000), (){

      Navigator.of(context, rootNavigator: true).pop();
    });

    showDialog(
        context: context,
        builder: (BuildContext builderContext) {

          return AlertDialog(
            backgroundColor: Colors.black26,
            title: Text('One to one live session'),
            content: SingleChildScrollView(
              child: Text('Do you want to connect for a live session ?'),
            ),

            actions: [
              cancelButton,
              continueButton,
            ],
          );
        }
    ).then((value){
      // dispose the timer in case something else has triggered the dismiss.
      timer?.cancel();
      timer = null;
    });


  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My App'),

      ),
      floatingActionButton: Builder(
        builder: (context) => FloatingActionButton(
          onPressed: showAlertDialog,

          backgroundColor: Colors.white,
          child: const Icon(Icons.send),
        ),
      ),
      body: SingleChildScrollView(
        child: Text(
            'Trigger Alert'

        ),
      ),
    );
  }


}

livesession1to1.dart:

import 'dart:async';
import 'package:agora_rtc_engine/rtc_engine.dart';
import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

// const appId = "<-- Insert App Id -->";
// const token = "<-- Insert Token -->";


const appId = "......";// Put Agora App ID from Agora site here
const token = "....";// Put token ( temporary token avilable from Agora site)


void main() => runApp(MaterialApp(home: LiveSession1to1()));

class LiveSession1to1 extends StatefulWidget {
  @override
  _LiveSession1to1State createState() => _LiveSession1to1State();
}

class _LiveSession1to1State extends State<LiveSession1to1> {

  int _remoteUid=1;
  bool _localUserJoined = false;
  late RtcEngine _engine;

  @override
  void initState() {
    super.initState();
    setState(() {});
    initAgora();
  }





  Future<void> initAgora() async {
    // retrieve permissions
    await [Permission.microphone, Permission.camera].request();




    // Create RTC client instance
    RtcEngineContext context = RtcEngineContext(appId);
    _engine = await RtcEngine.createWithContext(context);


    await _engine.enableVideo();

    _engine.setEventHandler(
      RtcEngineEventHandler(
        joinChannelSuccess: (String channel, int uid, int elapsed) {
          print("local user $uid joined");
          setState(() {
            _localUserJoined = true;
          });
        },
        userJoined: (int uid, int elapsed) {
          print("remote user $uid joined");
          setState(() {
            _remoteUid = uid;
          });
        },
        userOffline: (int uid, UserOfflineReason reason) {
          print("remote user $uid left channel");
          setState(() {
            // _remoteUid = null;
            _remoteUid = 0;
          });
        },
      ),
    );

    try {
      await _engine.joinChannel(token, "InstaClass", null, 0);

    } catch (e) {
      print("error with agora = ");
      print("$e");
      print("error printeddddd");
    }
  }

  // Create UI with local view and remote view
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Agora Video Call'),
      ),
      body: Stack(
        children: [
          Center(
            child: _remoteVideo(),
          ),


          Align(
            alignment: Alignment.topLeft,
            child: Container(
              width: 100,
              height: 150,
              child: Center(
                child: _localUserJoined
                    ? RtcLocalView.SurfaceView()
                    : CircularProgressIndicator(),
              ),
            ),
          ),


        ],
      ),
    );
  }

  // Display remote user's video
  Widget _remoteVideo() {

    if (_remoteUid != 0) {
      return RtcRemoteView.SurfaceView(
        uid: _remoteUid,
        channelId: "InstaClass",
      );
    }else {
      print("'Please wait for remote user to join',");
      return Text(
        'Please wait for remote user to join',
        textAlign: TextAlign.center,
      );
    }
  }
}

For user2:

main.dart:

import 'dart:async';
import 'package:agora_rtc_engine/rtc_engine.dart';
import 'package:agora_rtc_engine/rtc_local_view.dart' as RtcLocalView;
import 'package:agora_rtc_engine/rtc_remote_view.dart' as RtcRemoteView;
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';


const appId = "....."; // Same as user1 app
const token = "....."; // same as user1 app

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // int? _remoteUid=1;

  int _remoteUid=2;
  bool _localUserJoined = false;
  late RtcEngine _engine;

  @override
  void initState() {
    super.initState();
    initAgora();
  }

  Future<void> initAgora() async {
    // retrieve permissions
    await [Permission.microphone, Permission.camera].request();

    //create the engine
    _engine = await RtcEngine.create(appId);
    await _engine.enableVideo();
    _engine.setEventHandler(
      RtcEngineEventHandler(
        joinChannelSuccess: (String channel, int uid, int elapsed) {
          print("local user $uid joined");
          setState(() {
            _localUserJoined = true;
          });
        },
        userJoined: (int uid, int elapsed) {
          print("remote user $uid joined");
          setState(() {
            _remoteUid = uid;
          });
        },
        userOffline: (int uid, UserOfflineReason reason) {
          print("remote user $uid left channel");
          setState(() {
            // _remoteUid = null;
            _remoteUid = 0;
          });
        },
      ),
    );

    // await _engine.joinChannel(token, "test", null, 0);
    await _engine.joinChannel(token, "InstaClass", null, 0);

  }

  // Create UI with local view and remote view
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Agora Video Call'),
      ),
      body: Stack(
        children: [
          Center(
            child: _remoteVideo(),
          ),
          Align(
            alignment: Alignment.topLeft,
            child: Container(
              width: 100,
              height: 150,
              child: Center(
                child: _localUserJoined
                    ? RtcLocalView.SurfaceView()
                    : CircularProgressIndicator(),
              ),
            ),
          ),
        ],
      ),
    );
  }

  // Display remote user's video
  Widget _remoteVideo() {
    /*if (_remoteUid != null) {
          return RtcRemoteView.SurfaceView(uid: _remoteUid!);
        }*/

    if (_remoteUid != 0) {
      return RtcRemoteView.SurfaceView(
        uid: _remoteUid,
        channelId: "InstaClass",
      );
    }else {
      return Text(
        'Please wait for remote user to join',
        textAlign: TextAlign.center,
      );
    }
  }
}

In order to get the app ID and token, login to Agora site. After logging in, go to the 'Project Management' section to see the projects already created there. Under the Functions column, click on the key symbol and you will be taken to a page where you can generate a temporary token. On that page, give the channel name input the value 'InstaClass' as I have used this name in my code.

How to make the video chat work smoothly after the first time it works well ?


Solution

  • I think the problem is that when pressing back button you are just being taken to the previous screen and the call session is not being end. You can try by leaving the channel when pressing back button like :

    _engine.leaveChannel();
    

    End Call button sample

                 ElevatedButton(
                        onPressed: () {
                          _rtcEngine.leaveChannel();
                          Navigator.pop(context);
                        },
                        style: ButtonStyle(
                          shape: MaterialStateProperty.all(CircleBorder()),
                          backgroundColor: MaterialStateProperty.all(Colors.red),
                          padding: MaterialStateProperty.all(
                              EdgeInsets.fromLTRB(15, 15, 15, 12)),
                        ),
                        child: Icon(
                          Icons.phone,
                          size: 30,
                        ),
                      )
    

    Back Button override using WillPopScope

    return WillPopScope(
          onWillPop: () async {
            _rtcEngine.leaveChannel();
            return true;
          },
          child: Scaffold(
            body: Container(),
          ),
        );