AgoraIO-Extensions / Agora-Flutter-SDK

Flutter plugin of Agora RTC SDK for Android/iOS/macOS/Windows

Home Page:https://pub.dev/packages/agora_rtc_engine

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Agora Flutter: Remote User Unable to Join Same Channel

21prnv opened this issue · comments

I'm currently working on integrating Agora video call functionality into my Flutter app. However, I've encountered an issue where the remote user is unable to join the same channel as the local user, despite both using the same channel name. Below are the details of the problem:

Problem:

Issue: The remote user cannot join the same channel as the local user, resulting in their video feed not being displayed.
Observations:
No error messages are displayed during the attempted joining process.
The local user's video feed is displayed correctly.
Both local and remote users are using the same channel name for joining.
Code Snippets:

Here are the relevant sections of my UI logic and UI code related to token generation, channel joining, and event handling:
UI logic:
`
import 'dart:convert';

import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:consultant/app/app.locator.dart';
import 'package:consultant/app/app.logger.dart';
import 'package:consultant/app/app.router.dart';
import 'package:consultant/services/call_service.dart';
import 'package:consultant/services/client_service.dart';
import 'package:consultant/services/login_service.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked_services/stacked_services.dart';
import 'package:http/http.dart' as http;

class CallScreenViewModel extends BaseViewModel {
final _callService = locator();
final _clientService = locator();
final _loginService = locator();
final _navigationService = locator();
final logger = getLogger('agora serivec');
int? remoteUid1;
bool localUserJoined = false;
bool isBroadcaster = true; // Client role
late Map<String, dynamic> config; // Configuration parameters
// final String channelName;
late RtcEngine engine;
String appId = 'APP_ID';
String token = "";
@OverRide
void dispose() {
super.dispose();

_dispose();

}

Future _dispose() async {
await engine.leaveChannel();
await engine.release();
}

bool isFrontCamera = false;
bool isMuted = false;
bool callEnded = false;
bool isCameraPreviewEnabled = true;

Future requestPermissions() async {
// Request camera and microphone permissions
var statusCamera = await Permission.camera.request();
var statusMicrophone = await Permission.microphone.request();

// Check if permissions are granted
if (statusCamera.isGranted && statusMicrophone.isGranted) {
  // Both camera and microphone permissions are granted
  return true;
} else {
  // Handle case where permissions are not granted
  // For example, show an error message or request permissions again
  Fluttertoast.showToast(msg: 'Permissions Are Required!');
  return false;
}

}

Future fetchTokenAndJoin(String channelName) async {
// Retrieve a token from the server
bool permissionsGranted = await requestPermissions();
if (permissionsGranted) {
try {
token = await fetchToken(channelName);
// Proceed with token usage or further operations
await initilize(channelName, token);
} catch (e) {
// Handle the exception or display an error message
// messageCallback('Error fetching token');
Fluttertoast.showToast(msg: e.toString());

    return;
  }
} else {
  Fluttertoast.showToast(msg: 'Permissions Are Required!');
}

// Join a Video SDK channel

}

getTheRole() async {
isBroadcaster = await _callService.defineBoradcasterRole();
logger.i('isBroadcaaster role $isBroadcaster');
}

Future fetchToken(String channelName) async {
// Set the token role,
// Use 1 for Host/Broadcaster, 2 for Subscriber/Audience
// String configString =
// await rootBundle.loadString('');
// config = jsonDecode(configString);
// isBroadcaster = await _callService.defineBoradcasterRole();
// logger.i('isBroadcaaster role $isBroadcaster');
int tokenRole = isBroadcaster ? 1 : 2;

// Prepare the Url
String url =
    'https://<URL>.onrender.com/rtc/$channelName/'
    '${tokenRole.toString()}/uid/${0.toString()}'
    '?expiry=${300.toString()}';

// Send the http GET request
final response = await http.get(Uri.parse(url));

// Read the response
if (response.statusCode == 200) {
  // The server returned an OK response
  // Parse the JSON.
  Map<String, dynamic> json = jsonDecode(response.body);
  logger.i(json);

  String newToken = json['rtcToken'];
  // Return the token
  Fluttertoast.showToast(msg: newToken);
  logger.i(newToken);
  return newToken;
} else {
  // Throw an exception.
  throw Exception(
      'Failed to fetch a token. Make sure that your server URL is valid');
}

}

Future initilize(String channelName, String tokens) async {
Future.delayed(Duration.zero, () async {
await _initAgoraRtcEngine();
inItagora();
await engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
VideoEncoderConfiguration configuration =
const VideoEncoderConfiguration();
await engine.setVideoEncoderConfiguration(configuration);
await engine.leaveChannel();
await engine.joinChannel(
token: tokens,
channelId: channelName,
uid: -1,
options: ChannelMediaOptions(
clientRoleType: isBroadcaster
? ClientRoleType.clientRoleBroadcaster
: ClientRoleType.clientRoleAudience,
channelProfile: ChannelProfileType.channelProfileCommunication,
),
);
rebuildUi();
});
}

Future _initAgoraRtcEngine() async {
engine = createAgoraRtcEngine();
await engine.initialize(RtcEngineContext(
appId: appId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));
await engine.enableVideo();
// await engine.startPreview();
await engine.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
}

void inItagora() {
// // Request permissions (assuming you have a permission-handling mechanism)
// logger.i('local bool value $localUserJoined');

// [Permission.microphone, Permission.camera];

// // Initialize agora engine
// try {
//   engine = createAgoraRtcEngine();
//   await engine.initialize(RtcEngineContext(
//     appId: appId,
//     channelProfile: ChannelProfileType.channelProfileCommunication1v1,
//   ));
// } catch (error) {
//   // Handle initialization errors gracefully
//   logger.i('Error initializing engine: $error');
//   return;
// }

// Add event handlers
engine.registerEventHandler(
  RtcEngineEventHandler(
    onLeaveChannel: (connection, reason) {
      // if (reason !=
      //     ConnectionChangedReasonType.connectionChangedLeaveChannel) {
      //   // Ignore intentional switching
      //   callEnded = true;
      //   rebuildUi();

      //   rejoinIfNeeded(); // Consider automatically rejoining upon disconnection
      // }
    },
    onUserOffline: (connection, uid, reason) {
      if (reason == UserOfflineReasonType.userOfflineQuit) {
        // Handle intentional leaving
        callEnded = true;
        remoteUid1 = 0;
        rebuildUi();
        // Display appropriate message or initiate rejoin logic
      }
    },
    onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
      logger.i("Local user ${connection.localUid} joined");
      localUserJoined = true;
      logger.i('local bool value $localUserJoined');
      rebuildUi(); // Ensure UI updates correctly
    },
    onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
      logger.i("Remote user $remoteUid joined");
      logger.i('local bool value $localUserJoined');
      Fluttertoast.showToast(msg: 'Remote user $remoteUid joined');
      localUserJoined = true;

      remoteUid1 = remoteUid;
      rebuildUi();
    },
    onRemoteVideoStats:
        (RtcConnection connection, RemoteVideoStats remoteVideoStats) {
      // if (remoteVideoStats.receivedBitrate == 0) {
      //   videoPaused.value = true;
      //   update();
      // } else {
      //   videoPaused.value = false;
      //   update();
      // }
    },
    onTokenPrivilegeWillExpire: (RtcConnection connection, String token) {
      logger.i(
          '[onTokenPrivilegeWillExpire] connection: ${connection.toJson()}, token: $token');
    },
  ),
);

}

Future switchCamera() async {
try {
await engine.switchCamera();
isFrontCamera = !isFrontCamera; // Toggle the camera state
notifyListeners(); // Notify UI to update camera icon
} catch (error) {
// Handle any errors that might occur during camera switching
print("Error switching camera: $error");
}
}

void hideCameraPreview() {
isCameraPreviewEnabled = false;
notifyListeners(); // Notify UI to update camera preview visibility
}

void showCameraPreview() {
isCameraPreviewEnabled = true;
notifyListeners(); // Notify UI to update camera preview visibility
}

logout() {
_loginService.logout();
_navigationService.replaceWithLoginView();
}

Future toggleMute() async {
try {
await engine.muteLocalAudioStream(!isMuted);
isMuted = !isMuted; // Toggle the muted state
notifyListeners(); // Notify UI to update mute button appearance
} catch (error) {
// Handle any errors that might occur during muting/unmuting
print("Error toggling mute: $error");
}
}
}
`

UI:
`import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:stacked/stacked.dart';
import 'package:stacked/stacked_annotations.dart';

import 'call_screen_viewmodel.dart';

@FormView(
fields: [
FormTextField(name: 'channel Name'),
],
)
class CallScreenView extends StackedView {
const CallScreenView({
Key? key,
required this.channelName,
}) : super(key: key);
final String channelName;
@OverRide
Widget builder(
BuildContext context,
CallScreenViewModel viewModel,
Widget? child,
) {
final TextEditingController channelNames = TextEditingController();

return Scaffold(
  backgroundColor: Theme.of(context).colorScheme.background,
  body: SafeArea(
    child: Column(
      children: [
        // SizedBox(
        //   height: 40,
        //   child: ElevatedButton(
        //     onPressed: () {
        //       viewModel.fetchTokenAndJoin();
        //     },
        //     // agoraManager.isJoined ? () => {leave()} : () => {join()},
        //     child: Text(viewModel.localUserJoined ? "Leave" : "Join"),
        //   ),
        // ),
        TextField(
          controller: channelNames,
        ),
        TextButton(
            onPressed: () {
              // print(channelNames.text);
              viewModel.fetchTokenAndJoin(channelNames.text);
            },
            child: const Text('JOin')),

        Expanded(
          child: Stack(
            children: [
              Column(
                children: [
                  Expanded(
                    child: Center(
                        child: viewModel.localUserJoined
                            ? viewModel.isCameraPreviewEnabled
                                ? AgoraVideoView(
                                    controller: VideoViewController(
                                      rtcEngine: viewModel.engine,
                                      canvas: const VideoCanvas(uid: 0),
                                    ),
                                  )
                                : const Center(
                                    child: Icon(Icons.person_off_outlined),
                                  )
                            : const CircularProgressIndicator()),
                  ),
                  Expanded(
                    child: Container(
                      color: Colors.black,
                      child: Center(
                        child: viewModel.remoteUid1 != null
                            ? AgoraVideoView(
                                controller: VideoViewController.remote(
                                  rtcEngine: viewModel.engine,
                                  canvas: VideoCanvas(
                                      uid: viewModel.remoteUid1),
                                  connection: RtcConnection(
                                      channelId: channelNames.text),
                                ),
                              )
                            : const Text(
                                'Please wait for remote user to join',
                                textAlign: TextAlign.center,
                                style: TextStyle(color: Colors.white),
                              ),
                      ),
                    ),
                  ),
                ],
              ),
              Align(
                alignment: Alignment.bottomRight,
                child: Row(
                  mainAxisSize: MainAxisSize.max,
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    IconButton(
                      onPressed: () async =>
                          viewModel.engine.leaveChannel(),
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.white,
                        backgroundColor: Colors.red,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30),
                        ),
                      ),
                      iconSize: 40,
                      icon: const Icon(Icons.call_end),
                    ),
                    IconButton(
                      onPressed: viewModel.toggleMute,
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.white,
                        backgroundColor:
                            viewModel.isMuted ? Colors.red : Colors.blue,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30.0),
                        ),
                      ),
                      iconSize: 40,
                      icon: Icon(
                        viewModel.isMuted ? Icons.mic_off : Icons.mic,
                        color: Colors.white,
                      ),
                    ),
                    IconButton(
                      onPressed: viewModel.isCameraPreviewEnabled
                          ? viewModel.hideCameraPreview
                          : viewModel.showCameraPreview,
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.black,
                        backgroundColor: Colors.white,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30.0),
                        ),
                      ),
                      iconSize: 40,
                      icon: Icon(
                        viewModel.isCameraPreviewEnabled
                            ? Icons.videocam_off
                            : Icons.videocam,
                        color: Colors.black,
                      ),
                    ),
                    IconButton(
                      onPressed: viewModel.switchCamera,
                      style: ElevatedButton.styleFrom(
                        foregroundColor: Colors.black,
                        backgroundColor: Colors.white,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(30.0),
                        ),
                      ),
                      iconSize: 40,
                      icon: Icon(
                        viewModel.isFrontCamera
                            ? Icons.camera_rear
                            : Icons.camera_front,
                        color: Colors.black,
                      ),
                    ),
                    // ElevatedButton(
                    //   onPressed: () {}, // Join Call onPressed method
                    //   style: ElevatedButton.styleFrom(
                    //     foregroundColor: Colors.white,
                    //     backgroundColor: Colors.green,
                    //     shape: RoundedRectangleBorder(
                    //       borderRadius: BorderRadius.circular(8.0),
                    //     ),
                    //   ),
                    //   child: const Text('Join Call'),
                    // ),
                  ],
                ),
              ),
            ],
          ),
        )
      ],
    ),
  ),
);

}

@OverRide
void onViewModelReady(CallScreenViewModel viewModel) {
// TODO: implement onViewModelReady
super.onViewModelReady(viewModel);
viewModel.getTheRole();
}

@OverRide
CallScreenViewModel viewModelBuilder(
BuildContext context,
) =>
CallScreenViewModel();
}`

Can you share the logs?

I'm testing in the released version. Btw By joining the same channel name from two different devices, both devices were able to join as broadcasters, but in different calls rather than the same one.

Please submit a ticket to Agora Support for further investigation of this issue. If you have any conclusions, you can share them here which may help other developers. Thanks!

Without additional information, we are unfortunately not sure how to resolve this issue. We are therefore reluctantly going to close this bug for now. If you find this problem please file a new issue with the same description, what happens, logs and the output. All system setups can be slightly different so it's always better to open new issues and reference the related ones. Thanks for your contribution.

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please raise a new issue.