'Angular WebRTC error: Error: Uncaught (in promise): InvalidStateError: Failed to execute 'setRemoteDescription'

so I have been trying to make a peer-to-peer video-call service using webRTC and I have come across this error and don't really know how to solve it. I don't have that much experience with webRTC so I just adapted a project I found online to my requirements and also the documentation is not up-to-date so it is hard to find information on it.

This is the full error and I have attached below the code.

ERROR Error: Uncaught (in promise): InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable Error: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: stable

Code:

<div id="buttons">
  <button
    class="mdc-button mdc-button--raised"
    [disabled]="!isDisabled"
    id="cameraBtn"
    (click)="openUserMedia($event)"
  >
    <i class="material-icons mdc-button__icon" aria-hidden="true"
      >perm_camera_mic</i
    >
    <span class="mdc-button__label">Open camera & microphone</span>
  </button>
  <button
    class="mdc-button mdc-button--raised"
    [disabled]="isDisabled"
    id="createBtn"
    (click)="createRoom()"
  >
    <i class="material-icons mdc-button__icon" aria-hidden="true">group_add</i>
    <span class="mdc-button__label">Create room</span>
  </button>
  <button
    class="mdc-button mdc-button--raised"
    [disabled]
    id="joinBtn"
    (click)="joinRoomById(roomId.value)"
    [disabled]="isDisabled"
  >
    <i class="material-icons mdc-button__icon" aria-hidden="true">group</i>
    <span class="mdc-button__label">Join room</span>
  </button>
  <button
    class="mdc-button mdc-button--raised"
    disabled="!isDisabled"
    id="hangupBtn"
    (click)="hangUp($event)"
  >
    <i class="material-icons mdc-button__icon" aria-hidden="true">close</i>
    <span class="mdc-button__label">Hangup</span>
  </button>
</div>
<div>
  <span id="currentRoom"></span>
</div>
<div id="videos">
  <video
    id="localVideo"
    [muted]="'muted'"
    autoplay
    [srcObject]="localStream"
    playsinline
  ></video>
  <video
    id="remoteVideo"
    autoplay
    playsinline
    [srcObject]="remoteStream"
  ></video>
</div>
<div
  class="mdc-dialog"
  id="room-dialog"
  role="alertdialog"
  aria-modal="true"
  aria-labelledby="my-dialog-title"
  aria-describedby="my-dialog-content"
>
  <div class="mdc-dialog__container">
    <div class="mdc-dialog__surface">
      <h2 class="mdc-dialog__title" id="my-dialog-title">Join room</h2>
      <div class="mdc-dialog__content" id="my-dialog-content">
        Enter ID for room to join:
        <div class="mdc-text-field">
          <input
            type="text"
            id="roomId"
            #roomId
            class="mdc-text-field__input"
            name="roomId"
          />
          <label class="mdc-floating-label" for="my-text-field">Room ID</label>
          <div class="mdc-line-ripple"></div>
        </div>
      </div>
      <footer class="mdc-dialog__actions">
        <button
          type="button"
          class="mdc-button mdc-dialog__button"
          data-mdc-dialog-action="no"
        >
          <span class="mdc-button__label">Cancel</span>
        </button>
        <button
          id="confirmJoinBtn"
          type="button"
          class="mdc-button mdc-dialog__button"
          data-mdc-dialog-action="yes"
          (click)="confirm(roomId?.value)"
        >
          <span class="mdc-button__label">Join</span>
        </button>
      </footer>
    </div>
  </div>
  <div class="mdc-dialog__scrim"></div>
</div>
import { Location } from '@angular/common';
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import {
  addDoc,
  collection,
  doc,
  Firestore,
  getDoc,
  getDocs,
  onSnapshot,
  setDoc,
  updateDoc,
} from 'firebase/firestore';
import Peer from 'peerjs';
import { CallService } from '../call.service';
import { SignalingService } from '../signaling.service';
import AngularFirestore from '@angular/fire/firestore';
import { getFirestore } from 'firebase/firestore';
@Component({
  selector: 'app-video-call',
  templateUrl: './video-call.component.html',
  styleUrls: ['./video-call.component.css'],
})
export class VideoCallComponent implements OnInit {
  db = getFirestore();
  peerConnection: RTCPeerConnection = new RTCPeerConnection();
  configuration = {
    iceServers: [
      {
        urls: [
          'stun:stun1.l.google.com:19302',
          'stun:stun2.l.google.com:19302',
        ],
      },
    ],
    iceCandidatePoolSize: 10,
  };
  isDisabled = false;

  localStream: MediaStream = new MediaStream();
  remoteStream: MediaStream = new MediaStream();

  roomId: any;
  btn = document.getElementById('createBtn');

  constructor(
    private dialogReference: MatDialogRef<VideoCallComponent>,

    private location: Location,
    @Inject(MAT_DIALOG_DATA) public data: { doctor: string; user: string },
    private signalingService: SignalingService,
    private callService: CallService
  ) {}
  ngOnInit(): void {}

  async openUserMedia(e: any) {
    this.localStream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true,
    });
    this.remoteStream = new MediaStream();
  }
  async joinRoomById(roomId: any) {
    console.log(roomId);
    if (roomId) {
      const roomRef = doc(collection(this.db, 'rooms'), roomId);
      const roomSnapshot = await getDoc(roomRef);
      console.log('Got room:', roomSnapshot.exists);

      if (roomSnapshot.exists()) {
        console.log(
          'Create PeerConnection with configuration: ',
          this.configuration
        );
        this.peerConnection = new RTCPeerConnection(this.configuration);
        this.registerPeerConnectionListeners();
        this.localStream.getTracks().forEach((track) => {
          this.peerConnection.addTrack(track, this.localStream);
        });

        // Code for collecting ICE candidates below

        // Code for collecting ICE candidates above

        this.peerConnection.addEventListener('track', (event) => {
          console.log('Got remote track:', event.streams[0]);
          event.streams[0].getTracks().forEach((track) => {
            console.log('Add a track to the remoteStream:', track);
            this.remoteStream.addTrack(track);
          });
        });
      }
      console.log(roomSnapshot.data());
      const offer = roomSnapshot.data()['offer'];
      console.log('Got offer:', offer);
      await this.peerConnection.setRemoteDescription(
        new RTCSessionDescription(offer)
      );
      const answer = await this.peerConnection.createAnswer();
      console.log('Created answer:', answer);
      await this.peerConnection.setLocalDescription(answer);

      const roomWithAnswer = {
        answer: {
          type: answer.type,
          sdp: answer.sdp,
        },
      };

      await updateDoc(roomRef, roomWithAnswer);
      // Code for creating SDP answer above
      const queryCalleeSnapShot = collection(roomRef, 'calleeCandidates');
      const uns = onSnapshot(queryCalleeSnapShot, (snapshot: any) => {
        snapshot.docChanges().forEach(async (change: any) => {
          if (change.type === 'added') {
            let data = change.doc.data();
            console.log(
              `Got new remote ICE candidate: ${JSON.stringify(data)}`
            );
            await this.peerConnection.addIceCandidate(
              new RTCIceCandidate(data)
            );
          }
        });
      });
      // Listening for remote ICE candidates below
    }
  }
  async confirm(roomId: string) {
    console.log(roomId);
    await this.joinRoomById(roomId);
  }
  async createRoom() {
    this.isDisabled = true;
    const roomRef = await doc(collection(this.db, 'rooms'));
    console.log(
      'Create PeerConnection with configuration: ',
      this.configuration
    );
    this.peerConnection = new RTCPeerConnection(this.configuration);

    this.registerPeerConnectionListeners();

    this.localStream.getTracks().forEach((track) => {
      this.peerConnection.addTrack(track, this.localStream);
    });
    const callerCandidatesCollection = collection(roomRef, 'callerCandidates');
    this.peerConnection.addEventListener('icecandidate', (event) => {
      if (!event.candidate) {
        console.log('Got final candidate!');
        return;
      }

      console.log('Got candidate: ', event.candidate);
      addDoc(callerCandidatesCollection, event.candidate.toJSON);
    });
    const offer = await this.peerConnection.createOffer();
    await this.peerConnection.setLocalDescription(offer);
    console.log('Created offer:', offer);

    const roomWithOffer = {
      offer: {
        type: offer.type,
        sdp: offer.sdp,
      },
    };
    setDoc(roomRef, roomWithOffer);
    this.roomId = roomRef.id;
    console.log(`New room created with SDP offer. Room ID: ${roomRef.id}`);
    this.peerConnection.addEventListener('track', (event) => {
      console.log('Got remote track:', event.streams[0]);
      event.streams[0].getTracks().forEach((track) => {
        console.log('Add a track to the remoteStream:', track);
        this.remoteStream.addTrack(track);
      });
    });
    const querySnapshot = await getDocs(collection(this.db, 'rooms'));
    querySnapshot.forEach(async (snapshot) => {
      const data = snapshot.data();
      if (
        !this.peerConnection.currentRemoteDescription &&
        data &&
        data['answer']
      ) {
        console.log('Got remote description: ', data['answer']);
        const rtcSessionDescription = new RTCSessionDescription(data['answer']);
        console.log(rtcSessionDescription);
        await this.peerConnection.setRemoteDescription(rtcSessionDescription);
      }
    });
    const queryCalleeSnapShot = collection(roomRef, 'calleeCandidates');
    const uns = onSnapshot(queryCalleeSnapShot, (snapshot: any) => {
      snapshot.docChanges().forEach(async (change: any) => {
        if (change.type === 'added') {
          let data = change.doc.data();
          console.log(`Got new remote ICE candidate: ${JSON.stringify(data)}`);
          await this.peerConnection.addIceCandidate(new RTCIceCandidate(data));
        }
      });
    });
  }
  async hangUp(e: any) {
    this.localStream.getTracks().forEach((track) => {
      track.stop();
    });
    if (this.remoteStream) {
      this.remoteStream.getTracks().forEach((track) => track.stop());
    }

    if (this.peerConnection) {
      this.peerConnection.close();
    }
    this.localStream = new MediaStream();
    this.remoteStream = new MediaStream();
    document.location.reload();
  }

  registerPeerConnectionListeners() {
    this.peerConnection.addEventListener('icegatheringstatechange', () => {
      console.log(
        `ICE gathering state changed: ${this.peerConnection.iceGatheringState}`
      );
    });

    this.peerConnection.addEventListener('connectionstatechange', () => {
      console.log(
        `Connection state change: ${this.peerConnection.connectionState}`
      );
    });

    this.peerConnection.addEventListener('signalingstatechange', () => {
      console.log(
        `Signaling state change: ${this.peerConnection.signalingState}`
      );
    });

    this.peerConnection.addEventListener('iceconnectionstatechange ', () => {
      console.log(
        `ICE connection state change: ${this.peerConnection.iceConnectionState}`
      );
    });
  }
}
body {
  background: #eceff1;
  color: rgba(0, 0, 0, 0.87);
  font-family: Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  padding: 0;
}

#message {
  background: white;
  max-width: 360px;
  margin: 100px auto 16px;
  padding: 32px 24px;
  border-radius: 3px;
}

#message h2 {
  color: #ffa100;
  font-weight: bold;
  font-size: 16px;
  margin: 0 0 8px;
}

#message h1 {
  font-size: 22px;
  font-weight: 300;
  color: rgba(0, 0, 0, 0.6);
  margin: 0 0 16px;
}

#message p {
  line-height: 140%;
  margin: 16px 0 24px;
  font-size: 14px;
}

#message a {
  display: block;
  text-align: center;
  background: #039be5;
  text-transform: uppercase;
  text-decoration: none;
  color: white;
  padding: 16px;
  border-radius: 4px;
}

#message,
#message a {
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
}

#load {
  color: rgba(0, 0, 0, 0.4);
  text-align: center;
  font-size: 13px;
}

@media (max-width: 600px) {
  body,
  #message {
    margin-top: 0;
    background: white;
    box-shadow: none;
  }

  body {
    border-top: 16px solid #ffa100;
  }
}

body {
  margin: 1em;
}

button {
  margin: 0.2em 0.1em;
}

div#videos {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}

div#videos > video {
  background: black;
  width: 640px;
  height: 100%;
  display: block;
  margin: 1em;
}

It looks like the webRTC documentation and Firebase are not yet up-to-date so I did not really try much since it took me years to find the actual replacements for all the operations used



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source