1

So I'm using flutter (And flutter for web) to build a WebRTC client. I have a spring-boot server acting as the go-between for two clients. They both subscribe to a WebSocket to get messages from the other. It does nothing more than that.

I'm getting Error: InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: kStable

I don't know why this error is happening.

Here's the code for the signalling

 typedef void StreamStateCallback(MediaStream stream);

class CallingService {
  String sendToUserId;
  String currentUserId;
  final String authToken;
  final StompClient _client;
  final StreamStateCallback onAddRemoteStream;
  final StreamStateCallback onRemoveRemoteStream;
  final StreamStateCallback onAddLocalStream;
  RTCPeerConnection _peerConnection;
  List<RTCIceCandidate> _remoteCandidates = [];
  String destination;
  var hasOffer = false;
  var isNegotiating = false;

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

  CallingService(
      this._client,
      this.sendToUserId,
      this.currentUserId,
      this.authToken,
      this.onAddRemoteStream,
      this.onRemoveRemoteStream,
      this.onAddLocalStream) {
    destination = '/app/start-call/$sendToUserId';
    print("destination $destination");
    _client.subscribe(
        destination: destination,
        headers: {'Authorization': "$authToken"},
        callback: (StompFrame frame) => processMessage(jsonDecode(frame.body)));
  }

  Future<void> startCall() async {
    await processRemoteStream();
    RTCSessionDescription description =
        await _peerConnection.createOffer(_constraints);
    await _peerConnection.setLocalDescription(description);
    var message = RtcMessage(RtcMessageType.OFFER, currentUserId, {
      'description': {'sdp': description.sdp, 'type': description.type},
    });
    sendMessage(message);
  }

  Future<void> processMessage(Map<String, dynamic> messageJson) async {
    var message = RtcMessage.fromJson(messageJson);
    if (message.from == currentUserId) {
      return;
    }
    print("processing ${message.messageType.toString()}");
    switch (message.messageType) {
      case RtcMessageType.BYE:
        // TODO: Handle this case.
        break;
      case RtcMessageType.LEAVE:
        // TODO: Handle this case.
        break;
      case RtcMessageType.CANDIDATE:
        await processCandidate(message);
        break;
      case RtcMessageType.ANSWER:
        await processAnswer(message);
        break;
      case RtcMessageType.OFFER:
        await processOffer(message);
        break;
    }
  }

  Future<void> processCandidate(RtcMessage candidate) async {
    Map<String, dynamic> map = candidate.data['candidate'];
    var rtcCandidate = RTCIceCandidate(
      map['candidate'],
      map['sdpMid'],
      map['sdpMLineIndex'],
    );
    if (_peerConnection != null) {
      _peerConnection.addCandidate(rtcCandidate);
    } else {
      _remoteCandidates.add(rtcCandidate);
    }
  }

  Future<void> processAnswer(RtcMessage answer) async {
    if (isNegotiating){
      return;
    }
    var description = answer.data['description'];
    await _peerConnection.setRemoteDescription(
        RTCSessionDescription(description['sdp'], description['type']));
  }

  Future<void> processOffer(RtcMessage offer) async {
    await processRemoteStream();
    var description = offer.data['description'];
    await _peerConnection.setRemoteDescription(
        new RTCSessionDescription(description['sdp'], description['type']));
    var answerDescription = await _peerConnection.createAnswer(_constraints);
    await _peerConnection.setLocalDescription(answerDescription);
    var answerMessage = RtcMessage(RtcMessageType.ANSWER, currentUserId, {
      'description': {
        'sdp': answerDescription.sdp,
        'type': answerDescription.type
      },
    });
    sendMessage(answerMessage);
    if (_remoteCandidates.isNotEmpty) {
      _remoteCandidates
          .forEach((candidate) => _peerConnection.addCandidate(candidate));
      _remoteCandidates.clear();
    }
  }

  Future<void> processRemoteStream() async {
    await createStream();
    _peerConnection = await createPeerConnection(_iceServers, _config);
    _peerConnection.onSignalingState = (state) {
      isNegotiating = state != RTCSignalingState.RTCSignalingStateStable;
    };
    _peerConnection.onAddTrack = (MediaStream stream, _) {
      this.onAddRemoteStream(stream);
      print("sending stream from track");
    };

    _peerConnection.onAddStream = (MediaStream stream) {
      this.onAddRemoteStream(stream);
      print("sending stream");
    };
    _peerConnection.onRemoveStream =
        (MediaStream stream) => this.onRemoveRemoteStream(stream);
    _peerConnection.onIceCandidate = (RTCIceCandidate candidate) {
      print("sending candidate");
      var data = {
        'candidate': {
          'sdpMLineIndex': candidate.sdpMlineIndex,
          'sdpMid': candidate.sdpMid,
          'candidate': candidate.candidate,
        },
      };
      var message = RtcMessage(RtcMessageType.CANDIDATE, currentUserId, data);
      sendMessage(message);
    };
  }

  void sendMessage(RtcMessage message) {
    _client.send(
        destination: destination,
        headers: {'Authorization': "$authToken"},
        body: jsonEncode(message.toJson()));
  }

  Map<String, dynamic> _iceServers = {
    'iceServers': [
      {"url" : "stun:stun2.1.google.com:19302"},
      {'url' : 'stun:stun.l.google.com:19302'},
      /*
       * turn server configuration example.
      {
        'url': 'turn:123.45.67.89:3478',
        'username': 'change_to_real_user',
        'credential': 'change_to_real_secret'
      },
       */
    ]
  };

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

  Future<MediaStream> createStream() async {
    final Map<String, dynamic> mediaConstraints = {
      'audio': true,
      'video': {
        'mandatory': {
          'minWidth':
              '640', // Provide your own width, height and frame rate here
          'minHeight': '480',
          'minFrameRate': '30',
        },
        'facingMode': 'user',
        'optional': [],
      }
    };

    MediaStream stream = await navigator.getUserMedia(mediaConstraints);
    if (this.onAddLocalStream != null) {
      this.onAddLocalStream(stream);
    }
    return stream;
  }
}

My first problem was I was not setting the local description for the offer/answer stage.

However, when I add a new stun server, I get the same exception. Either way, I don't get a remote stream showing.

Cate Daniel
  • 724
  • 2
  • 14
  • 30
  • Does this answer your question? [Failed to set local answer sdp: Called in wrong state: kStable](https://stackoverflow.com/questions/48963787/failed-to-set-local-answer-sdp-called-in-wrong-state-kstable) – Abhilash Chandran May 31 '20 at 08:14
  • No sadly. It happens in any browser or device, and I’m not manually calling addtrack – Cate Daniel May 31 '20 at 14:42

1 Answers1

0

So when I was creating the offer and answer I wasn't setting local description. So there's that.

It's still not showing remote connections though.

Cate Daniel
  • 724
  • 2
  • 14
  • 30