title: Flutter WebRTC audio but no video
body: <p>So I'm building a video calling application using flutter, flutterWeb, and the WebRTC package. </p>
  
  <p>I have a spring boot server sitting in the middle to pass the messages between the two clients. </p>
  
  <p>Each side shows the local video, but neither shows the remote. Audio does work though. I got som nasty feedback loops. Testing with headphones showed that audio does indeed work. </p>
  
  <p>My singaling code </p>
  
  <pre><code>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&lt;RTCIceCandidate&gt; _remoteCandidates = [];
    String destination;
    var hasOffer = false;
    var isNegotiating = false;
    MediaStream _localStream;
  
    final Map&lt;String, dynamic&gt; _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) =&gt; processMessage(jsonDecode(frame.body)));
    }
  
    Future&lt;void&gt; 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&lt;void&gt; processMessage(Map&lt;String, dynamic&gt; 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&lt;void&gt; processCandidate(RtcMessage candidate) async {
      Map&lt;String, dynamic&gt; map = candidate.data['candidate'];
      var rtcCandidate = RTCIceCandidate(
        map['candidate'],
        map['sdpMid'],
        map['sdpMLineIndex'],
      );
      if (_peerConnection != null) {
        _peerConnection.addCandidate(rtcCandidate);
      } else {
        _remoteCandidates.add(rtcCandidate);
      }
    }
  
    Future&lt;void&gt; processAnswer(RtcMessage answer) async {
      if (isNegotiating) {
        return;
      }
      isNegotiating = true;
      var description = answer.data['description'];
      if (_peerConnection == null) {
        return;
      }
      await _peerConnection.setRemoteDescription(
          RTCSessionDescription(description['sdp'], description['type']));
    }
  
    Future&lt;void&gt; 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) =&gt; _peerConnection.addCandidate(candidate));
        _remoteCandidates.clear();
      }
    }
  
    Future&lt;void&gt; processRemoteStream() async {
      _localStream = await createStream();
      _peerConnection = await createPeerConnection(_iceServers, _config);
      _peerConnection.addStream(_localStream);
      _peerConnection.onSignalingState = (state) {
        //isNegotiating = state != RTCSignalingState.RTCSignalingStateStable;
      };
  
      _peerConnection.onAddStream = (MediaStream stream) {
        this.onAddRemoteStream(stream);
      };
      _peerConnection.onRemoveStream =
          (MediaStream stream) =&gt; this.onRemoveRemoteStream(stream);
      _peerConnection.onIceCandidate = (RTCIceCandidate 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&lt;String, dynamic&gt; _iceServers = {
      'iceServers': [
        {'urls': '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&lt;String, dynamic&gt; _config = {
      'mandatory': {},
      'optional': [
        {'DtlsSrtpKeyAgreement': true},
      ],
    };
  
    Future&lt;MediaStream&gt; createStream() async {
      final Map&lt;String, dynamic&gt; mediaConstraints = {
        'audio': true,
        'video': {
          'mandatory': {
            'minWidth': '640',
            'minHeight': '480',
            'minFrameRate': '30',
          },
          'facingMode': 'user',
          'optional': [],
        }
      };
  
      MediaStream stream = await navigator.getUserMedia(mediaConstraints);
      if (this.onAddLocalStream != null) {
        this.onAddLocalStream(stream);
      }
      return stream;
    }
  }
  </code></pre>
  
  <p>Here are my widgets </p>
  
  <pre><code>class _CallScreenState extends State&lt;CallScreen&gt; {
    StompClient _client;
    CallingService _callingService;
    RTCVideoRenderer _localRenderer = new RTCVideoRenderer();
    RTCVideoRenderer _remoteRenderer = new RTCVideoRenderer();
    final UserService userService = GetIt.instance.get&lt;UserService&gt;();
  
    void onConnectCallback(StompClient client, StompFrame connectFrame) async {
      var currentUser = await userService.getCurrentUser();
      _callingService = CallingService(
          _client,
          widget.intent.toUserId.toString(),
          currentUser.id.toString(),
          widget.intent.authToken,
          onAddRemoteStream,
          onRemoveRemoteStream,
          onAddLocalStream);
      if (widget.intent.initialMessage != null) {
        _callingService.processMessage(jsonDecode(widget.intent.initialMessage));
      } else {
        _callingService.startCall();
      }
    }
  
    void onAddRemoteStream(MediaStream stream) {
      _remoteRenderer.srcObject = stream;
    }
  
    void onRemoveRemoteStream(MediaStream steam) {
      _remoteRenderer.srcObject = null;
    }
  
    void onAddLocalStream(MediaStream stream) {
      _localRenderer.srcObject = stream;
    }
  
    @override
    void initState() {
      super.initState();
      _localRenderer.initialize();
      _remoteRenderer.initialize();
      _client = StompClient(
        config: StompConfig(
          url: 'ws://${DomainService.getDomainBase()}/stomp',
          onConnect: onConnectCallback,
          onWebSocketError: (dynamic error) =&gt; print(error.toString()),
          stompConnectHeaders: {'Authorization': "${widget.intent.authToken}"},
          onDisconnect: (message) =&gt; print("disconnected ${message.body}"),),
      );
      _client.activate();
    }
  
    @override
    Widget build(BuildContext context) {
      return PlatformScaffold(
        pageTitle: "",
        child: Flex(
          direction: Axis.vertical,
          children: [
            Flexible(
              flex: 1,
              child: RTCVideoView(_localRenderer),
            ),
            Flexible(
              flex: 1,
              child: RTCVideoView(_remoteRenderer),
            )
          ],
        ),
      );
    }
  }
  </code></pre>
  
  <p>I put a print statment in the the widget on the addRemoteStream callback, and it's getting called. So some kind of stream is being sent. I'm just not sure why the video isnt' showing. </p>