iflytek/astron-agent · error · DOMException
InvalidStateError
InvalidStateError
Error message
The RTCPeerConnection's signalingState is 'closed'.
What it means
adapter.js's addTrack polyfill checks this.signalingState before doing anything: if the RTCPeerConnection has been closed (signalingState === 'closed', typically after pc.close()), any addTrack call is illegal and it throws DOMException("The RTCPeerConnection's signalingState is 'closed'.", 'InvalidStateError'). This mirrors the native spec behavior that a closed connection cannot accept new tracks.
Solutions
- Check pc.signalingState !== 'closed' (or pc.connectionState) before addTrack and bail/recreate the pc if closed.
- Guard async callbacks: capture the pc in a ref and verify it is still the active connection inside the .then before adding tracks.
- Recreate the RTCPeerConnection if the session must continue after close.
- Ensure SDK teardown is complete and idempotent so a later publish gets a fresh connection.
Example fix
// before
navigator.mediaDevices.getUserMedia({audio:true}).then(s => pc.addTrack(s.getAudioTracks()[0], s));
// after
navigator.mediaDevices.getUserMedia({audio:true}).then(s => {
if (pc.signalingState === 'closed') return; // or recreate pc
pc.addTrack(s.getAudioTracks()[0], s);
}); Defensive patterns
Strategy: try-catch
Validate before calling
if (!pc || pc.signalingState === 'closed') { pc = createNewPeerConnection(); } Type guard
const isUsable = (pc) => pc instanceof RTCPeerConnection && pc.signalingState !== 'closed';
Try / catch
try { pc.addTrack(track, stream); } catch (e) {
if (e.name === 'InvalidStateError' && pc.signalingState === 'closed') {
pc = recreatePeerConnection();
pc.addTrack(track, stream);
} else throw e;
} Prevention
- Check signalingState before every addTrack/addStream call
- In async getUserMedia callbacks, verify the pc is still the active one (store in ref, compare identity)
- Cancel in-flight getUserMedia promises on component unmount/close
- Tie peer connection lifecycle to the session, not to transient retries
When it happens
Trigger: Calling pc.addTrack(track, stream) — or an SDK API that does — after pc.close() was called, or after the connection failed/was torn down; also happens when an async callback (e.g. getUserMedia .then) resolves after the component already closed the connection.
Common situations: Unmounting a player component while getUserMedia is still pending, then the promise resolves and adds the track to the closed pc; retry logic calling addTrack on a stale connection after a reconnect created a new pc.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
AI-assisted analysis of iflytek/astron-agent@5e758547a8 (2026-09-12).
Data as JSON: /api/errors/3dc0e086dd0856b7.
Report an issue: GitHub.
Appendix: source
Thrown at console/frontend/src/utils/avatar-sdk-web_3.1.2.1002/xrtc-player-BJTnVhG9.js:19402
Object.keys(e._reverseStreams || []).forEach(t => {
const r = e._reverseStreams[t];
i = i.replace(new RegExp(e._streams[r.id].id, 'g'), r.id);
}),
new RTCSessionDescription({ type: t.type, sdp: i })
);
}
((e.RTCPeerConnection.prototype.removeStream = function (e) {
((this._streams = this._streams || {}),
(this._reverseStreams = this._reverseStreams || {}),
n.apply(this, [this._streams[e.id] || e]),
delete this._reverseStreams[
this._streams[e.id] ? this._streams[e.id].id : e.id
],
delete this._streams[e.id]);
}),
(e.RTCPeerConnection.prototype.addTrack = function (t, i) {
if ('closed' === this.signalingState)
throw new DOMException(
"The RTCPeerConnection's signalingState is 'closed'.",
'InvalidStateError'
);
const r = [].slice.call(arguments, 1);
if (1 !== r.length || !r[0].getTracks().find(e => e === t))
throw new DOMException(
'The adapter.js addTrack polyfill only supports a single stream which is associated with the specified track.',
'NotSupportedError'
);
const n = this.getSenders().find(e => e.track === t);
if (n)
throw new DOMException('Track already exists.', 'InvalidAccessError');
((this._streams = this._streams || {}),
(this._reverseStreams = this._reverseStreams || {}));
const o = this._streams[i.id];
if (o)
(o.addTrack(t),
Promise.resolve().then(() => {View on GitHub (pinned to 5e758547a8)