cybex-dev / twilio_voice

Flutter Twilio Voice Plugin
https://twilio-voice-web.web.app/
MIT License
39 stars 79 forks source link

The unmute event is called whenever the speaker is toggled, but the microphone remains muted. #220

Open Punit30 opened 6 months ago

Punit30 commented 6 months ago

Issue Summary

I am developing a custom UI for calling, and I'm facing an issue where the unmute event is triggered when toggling the speaker, but the call remains muted. I am unsure why this is happening. Additionally, when checking with TwilioVoice.instance.call.isMuted(), it returns a false value after the speaker is toggled. Also, the function and Native Call UI of android is not in sync if trigger on native call UI it does not reflect on app UI. I am seeking assistance to understand and resolve these issues.

Steps to Reproduce

  1. Below it he code for my call ui and twiliovoice configuration:
    
    import 'dart:async';

import 'package:flutter/material.dart'; import 'package:flutter_libphonenumber/flutter_libphonenumber.dart'; import 'package:ionicons/ionicons.dart'; import 'package:softphone_mobile_app/screens/keypad/controller/keypad_controller.dart'; import 'package:softphone_mobile_app/screens/keypad/widgets/caller_info.dart'; import 'package:softphone_mobile_app/screens/keypad/widgets/circular_button.dart'; import 'package:softphone_mobile_app/screens/keypad/widgets/dial_pad_grid.dart'; import 'package:softphone_mobile_app/screens/keypad/widgets/phone_input_field.dart'; import 'package:softphone_mobile_app/themes/pallete.dart'; import 'package:twilio_voice/twilio_voice.dart';

class OutGoingView extends StatefulWidget { static route() => MaterialPageRoute(builder: (context) => const OutGoingView());

const OutGoingView({super.key});

@override State createState() => _OutGoingViewState(); }

class _OutGoingViewState extends State { TextEditingController controller = TextEditingController(); KeypadController keypadController = KeypadController();

late final StreamSubscription _subscription; final _events = [];

bool onMute = false; set stateMute(bool value) { setState(() { onMute = value; }); }

bool onHold = false; set stateHold(bool value) { setState(() { onHold = value; }); }

bool onSpeaker = false; set stateSpeaker(bool value) { setState(() { onSpeaker = value; }); }

bool onBluetooth = false; set stateBluetooth(bool value) { setState(() { onBluetooth = value; }); }

bool isKeypadActive = false; set stateKeypadActive(bool value) { setState(() { isKeypadActive = value; }); }

final _tv = TwilioVoice.instance; bool activeCall = false;

@override void initState() { super.initState(); _subscription = _tv.callEventsListener.listen((event) { _events.add(event); switch (event) { case CallEvent.unmute: print(" +++++++++++ unmute ++++++++"); stateMute = false; break; case CallEvent.mute: print("---------- mute ---------"); stateMute = true; break; case CallEvent.unhold: case CallEvent.hold: case CallEvent.speakerOn: case CallEvent.speakerOff: case CallEvent.bluetoothOn: case CallEvent.bluetoothOff: _updateStates(); break;

    case CallEvent.connected:
      activeCall = true;
      _updateStates();
      break;

    case CallEvent.callEnded:
      activeCall = false;
      break;

    case CallEvent.incoming:
    case CallEvent.ringing:
    case CallEvent.declined:
    case CallEvent.answer:
    case CallEvent.missedCall:
    case CallEvent.returningCall:
    case CallEvent.reconnecting:
    case CallEvent.reconnected:
    case CallEvent.log:
    case CallEvent.permission:
      break;
  }
});
_updateStates();

}

void _updateStates() { // get all states from call _tv.call.isMuted().then((value) => stateMute = value ?? false); _tv.call.isHolding().then((value) => stateHold = value ?? false); _tv.call.isOnSpeaker().then((value) => stateSpeaker = value ?? false); _tv.call.isBluetoothOn().then((value) => stateBluetooth = value ?? false); }

@override void dispose() { _subscription.cancel(); controller.dispose(); super.dispose(); }

@override Widget build(BuildContext context) { double screenHeight = MediaQuery.of(context).size.height;

return PopScope(
  canPop: false,
  child: Scaffold(
    body: Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment(0.07, -1.00),
          end: Alignment(-0.7, 1),
          colors: [
            Pallete.darkNavyBlue,
            Pallete.deepNavyBlue,
          ],
        ),
      ),
      child: Padding(
        padding: EdgeInsets.only(
          top: screenHeight * 0.06,
          bottom: screenHeight * 0.04,
          left: 30,
          right: 30,
        ),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            CallerInfo(
              callType: _tv.call.activeCall?.callDirection ==
                      CallDirection.incoming
                  ? "INCOMING"
                  : "OUTGOING",
              callerName: "",
              callerNumber: formatNumberSync(
                _tv.call.activeCall?.callDirection == CallDirection.incoming
                    ? _tv.call.activeCall!.from
                    : _tv.call.activeCall!.to,
              ),
              callerProfile: "",
            ),
            isKeypadActive
                ? Center(
                    child: Padding(
                      padding: const EdgeInsets.only(
                          left: 8.0, right: 8.0, top: 10),
                      child: PhoneInputField(
                        controller: controller,
                        color: Pallete.blueGrey,
                        fontWeight: FontWeight.w500,
                        textSize: 22.0,
                        showPrefixIcon: false,
                      ),
                    ),
                  )
                : Container(),
            isKeypadActive
                ? Expanded(
                    child: DialPadGrid(
                    isDialPad: false,
                    aspectFactor: 3.1,
                    itemCount: 12,
                    primaryTextColor: Pallete.white,
                    secondaryTextColor: Pallete.slateBlue,
                    buttonColor: Pallete.lightSteelBlue,
                    controller: controller,
                    onKeyPress: (value) async {
                      print(value);
                      await _tv.call
                          .sendDigits(value)
                          .then((value) => print("digit sent succefully"));
                    },
                  ))
                : Container(),
            const SizedBox(height: 24.0),
            Column(
              children: [
                !isKeypadActive
                    ? Container(
                        margin: const EdgeInsets.only(bottom: 24),
                        child: Row(
                          mainAxisAlignment: MainAxisAlignment.spaceAround,
                          crossAxisAlignment: CrossAxisAlignment.center,
                          children: [
                            CircularButton(
                                onPressed: () =>
                                    _tv.call.toggleSpeaker(!onSpeaker),
                                icon: Ionicons.volume_high,
                                color: !onSpeaker
                                    ? Pallete.white
                                    : Pallete.darkIndigo,
                                backgroundColor: !onSpeaker
                                    ? Pallete.lightSteelBlue
                                    : Pallete.white),
                            CircularButton(
                                onPressed: () {
                                  setState(() {
                                    if (!isKeypadActive) {
                                      setState(() {
                                        isKeypadActive = true;
                                        controller =
                                            TextEditingController(); // Create a new controller
                                      });
                                    } else {
                                      stateKeypadActive = false;
                                    }
                                  });
                                },
                                icon: Ionicons.keypad,
                                backgroundColor: Pallete.lightSteelBlue),
                            CircularButton(
                              onPressed: () => _tv.call.toggleMute(!onMute),
                              icon:
                                  onMute ? Ionicons.mic_off : Ionicons.mic,
                              backgroundColor: onMute
                                  ? Pallete.lightSteelBlue
                                  : Pallete.white,
                              color: onMute
                                  ? Pallete.white
                                  : Pallete.darkIndigo,
                            ),
                          ],
                        ),
                      )
                    : Container(),
                Padding(
                  padding: const EdgeInsets.only(left: 8.0, right: 8.0),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      isKeypadActive
                          ? Container(
                              width: 80,
                              padding: const EdgeInsets.all(0),
                              child: TextButton(
                                onPressed: () {
                                  setState(() {
                                    isKeypadActive = false;
                                  });
                                },
                                style: TextButton.styleFrom(
                                    padding: const EdgeInsets.all(0)),
                                child: const Text(
                                  "Hide keypad",
                                  style: TextStyle(
                                      fontFamily: 'Figtree',
                                      fontSize: 14.0,
                                      fontWeight: FontWeight.w400,
                                      letterSpacing: 0.02,
                                      color: Pallete.lightBlueGray),
                                ),
                              ),
                            )
                          : Container(),
                      CircularButton(
                        onPressed: () {
                          final activeCall =
                              TwilioVoice.instance.call.activeCall;
                          if (activeCall != null) {
                            TwilioVoice.instance.call.hangUp();
                          }
                        },
                        icon: Ionicons.close,
                        backgroundColor: Pallete.danger400,
                      ),
                      isKeypadActive
                          ? SizedBox(
                              width: 80,
                              child: IconButton(
                                onPressed: () {
                                  keypadController.onInputErase(
                                      controller, false);
                                },
                                icon: const Icon(
                                  Ionicons.backspace,
                                  color: Pallete.blueGrey500,
                                  size: 32,
                                ),
                              ),
                            )
                          : Container()
                    ],
                  ),
                ),
              ],
            )
          ],
        ),
      ),
    ),
  ),
);

} }



Any other relevant information. For example, why do you consider this a bug and what did you expect to happen instead?
I don't know if this is a bug or not but I'd like to know what changes one can make to have this thing work properly. 
Dev-Devarsh commented 6 months ago

Any updates on this issue??

Punit30 commented 6 months ago

Hi @Dev-Devarsh, I am no longer syncing mute with the Twilio Voice package. Instead, I am managing mute at the widget level. If the "call.toggleMute()" function is successful, I handle mute accordingly. Similarly, I have applied the same approach for hold. Previously, checking for the isHold function resulted in triggering the hold/unhold event on iOS devices, leading to an infinite loop that persists until the call ends.

Dev-Devarsh commented 6 months ago

I am unable to understand managing mute at the widget level. Can please explain in brief..!

Punit30 commented 5 months ago

I am no longer using "TwilioVoice.instance.call.isMuted()" function or check mute/unmute call event to get mute status. Instead, Directly checking success message from "TwilioVoice.instance.call.toggleMute()" and toggle isMute variable inside the widget itself.

await TwilioVoice.instance.call.toogleMute().then((value) {
    if(value == true) {
         setState(() {
              isMute = !isMute;
         };
    } else {
         showSnackBar("Unable to mute/unmute the call. Please try again.", "error", context);
    }
});

Hope the above code would be helpful!