mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 23:46:41 +00:00
ui: don't reconnect calls with peers before v22 (#7785)
This commit is contained in:
16 files changed
+64
-40
No files matched your search
@@ -876,7 +876,7 @@ extension WebRTCClient: RTCPeerConnectionDelegate {
|
||||
|
||||
guard let connectionStateString = newState.toString() else { return }
|
||||
Task {
|
||||
if activeCall?.wasConnected == true {
|
||||
if activeCall?.wasConnected == true && ChatModel.shared.activeCall?.contact.callReconnectSupported == true {
|
||||
switch newState {
|
||||
case .disconnected, .failed:
|
||||
if activeCall?.reconnection == nil {
|
||||
|
||||
@@ -19,6 +19,8 @@ public let REPORTS_VERSION = 12
|
||||
// support group knocking (MsgScope)
|
||||
public let GROUP_KNOCKING_VERSION = 15
|
||||
|
||||
public let CALL_RECONNECT_VERSION = 22
|
||||
|
||||
public let contentModerationPostLink = URL(string: "https://simplex.chat/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.html#preventing-server-abuse-without-compromising-e2e-encryption")!
|
||||
|
||||
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
|
||||
@@ -2505,6 +2507,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } }
|
||||
public var active: Bool { get { contactStatus == .active } }
|
||||
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
||||
public var callReconnectSupported: Bool { (activeConn?.peerChatVRange.maxVersion ?? 0) >= CALL_RECONNECT_VERSION }
|
||||
public var nextConnectPrepared: Bool { active && preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) }
|
||||
public var profileChangeProhibited: Bool { activeConn != nil }
|
||||
public var nextAcceptContactRequest: Bool {
|
||||
|
||||
@@ -109,7 +109,7 @@ All signaling messages are exchanged via E2E encrypted SMP messages (no central
|
||||
2. The call shows "reconnecting…" and plays the connecting sound. Call duration continues, as this state is not reported to the core.
|
||||
3. The callee (the party that sent the initial offer) restarts ICE every 10 seconds, sending the new offer via `apiSendCallExtraInfo` with `rtcSession`; the caller answers the same way.
|
||||
4. When the connection is restored, the call returns to connected state.
|
||||
5. If the connection is not restored in 30 seconds, the call ends. Clients without ICE restart support ignore the restart offer, so the call ends in this case.
|
||||
5. If the connection is not restored in 30 seconds, the call ends. If the contact's chat version is below 22 (`CALL_RECONNECT_VERSION`), the call ends as soon as the connection is lost, without reconnection.
|
||||
|
||||
### 7. Audio Routing
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ All call signaling travels through the same encrypted SMP message channels used
|
||||
|
||||
### Reconnection
|
||||
|
||||
When a connected call loses ICE connection (`.disconnected` or `.failed`), `WebRTCClient` reports `"reconnecting"` connection state instead of ending the call. `ActiveCallView` sets `.reconnecting` call state and plays the connecting sound; this state is not sent to the core via `apiCallStatus`, and `connectedAt` is kept. The party that sent the initial offer (callee) restarts ICE every 10 seconds: the restart offer and answer are sent via `apiSendCallExtraInfo` with `rtcSession` (`x.call.extra`). When ICE connects again, the call returns to `.connected`. If it does not reconnect in 30 seconds, `WebRTCClient` reports `"disconnected"` and ends the call.
|
||||
When a connected call loses ICE connection (`.disconnected` or `.failed`), `WebRTCClient` reports `"reconnecting"` connection state instead of ending the call. `ActiveCallView` sets `.reconnecting` call state and plays the connecting sound; this state is not sent to the core via `apiCallStatus`, and `connectedAt` is kept. The party that sent the initial offer (callee) restarts ICE every 10 seconds: the restart offer and answer are sent via `apiSendCallExtraInfo` with `rtcSession` (`x.call.extra`). When ICE connects again, the call returns to `.connected`. If it does not reconnect in 30 seconds, `WebRTCClient` reports `"disconnected"` and ends the call. Reconnection happens only when the contact's peer chat version is at least `CALL_RECONNECT_VERSION` (22); otherwise the call ends as soon as the connection is lost.
|
||||
|
||||
### API Commands
|
||||
|
||||
|
||||
@@ -1917,6 +1917,7 @@ data class Contact(
|
||||
val active get() = contactStatus == ContactStatus.Active
|
||||
override val nextConnect get() = sendMsgToConnect
|
||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||
val callReconnectSupported get() = (activeConn?.peerChatVRange?.maxVersion ?: 0) >= CALL_RECONNECT_VERSION
|
||||
override val nextConnectPrepared get() = active && preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
|
||||
override val profileChangeProhibited get() = activeConn != null
|
||||
val nextAcceptContactRequest get() =
|
||||
|
||||
+4
-1
@@ -74,6 +74,8 @@ val CREATE_MEMBER_CONTACT_VERSION = 2
|
||||
// support group knocking (MsgScope)
|
||||
val GROUP_KNOCKING_VERSION = 15
|
||||
|
||||
val CALL_RECONNECT_VERSION = 22
|
||||
|
||||
enum class CallOnLockScreen {
|
||||
DISABLE,
|
||||
SHOW,
|
||||
@@ -3694,7 +3696,8 @@ object ChatController {
|
||||
media = r.callType.media,
|
||||
aesKey = r.sharedKey,
|
||||
iceServers = iceServers,
|
||||
relay = useRelay
|
||||
relay = useRelay,
|
||||
canReconnect = r.contact.callReconnectSupported
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -62,7 +62,8 @@ class CallManager(val chatModel: ChatModel) {
|
||||
media = invitation.callType.media,
|
||||
aesKey = invitation.sharedKey,
|
||||
iceServers = iceServers,
|
||||
relay = useRelay
|
||||
relay = useRelay,
|
||||
canReconnect = invitation.contact.callReconnectSupported
|
||||
))
|
||||
callInvitations.remove(invitation.contact.id)
|
||||
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
|
||||
|
||||
+2
-2
@@ -90,8 +90,8 @@ enum class CallState {
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
|
||||
@Serializable @SerialName("permission") data class Permission(val title: String, val chrome: String, val safari: String): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null, val canReconnect: Boolean = false): WCallCommand()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null, val canReconnect: Boolean = false): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String, val session: String? = null): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val source: CallMediaSource, val enable: Boolean): WCallCommand()
|
||||
|
||||
@@ -145,7 +145,7 @@ const processCommand = (function () {
|
||||
}
|
||||
});
|
||||
}
|
||||
async function initializeCall(config, mediaType, aesKey) {
|
||||
async function initializeCall(config, mediaType, aesKey, canReconnect) {
|
||||
var _a, _b, _c;
|
||||
let pc;
|
||||
try {
|
||||
@@ -233,7 +233,7 @@ const processCommand = (function () {
|
||||
: pc.iceConnectionState == "completed"
|
||||
? "connected"
|
||||
: "connecting"); /* webView 69-70 doesn't have connectionState yet */
|
||||
if (wasConnected) {
|
||||
if (wasConnected && canReconnect) {
|
||||
if (reconnectTimedOut) {
|
||||
reconnectingTimeout = undefined;
|
||||
if (activeCall !== call || state == "connected")
|
||||
@@ -415,10 +415,10 @@ const processCommand = (function () {
|
||||
inactiveCallMediaSources.camera = command.media == CallMediaType.Video;
|
||||
inactiveCallMediaSourcesChanged(inactiveCallMediaSources);
|
||||
setupLocalVideoRatio(getVideoElements().local);
|
||||
const { media, iceServers, relay } = command;
|
||||
const { media, iceServers, relay, canReconnect } = command;
|
||||
const encryption = supportsInsertableStreams(useWorker);
|
||||
const aesKey = encryption ? command.aesKey : undefined;
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey);
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey, canReconnect);
|
||||
await setupLocalStream(true, activeCall);
|
||||
setupCodecPreferences(activeCall);
|
||||
const pc = activeCall.connection;
|
||||
@@ -458,8 +458,8 @@ const processCommand = (function () {
|
||||
else {
|
||||
const offer = parse(command.offer);
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
const { media, aesKey, iceServers, relay } = command;
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey);
|
||||
const { media, aesKey, iceServers, relay, canReconnect } = command;
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey, canReconnect);
|
||||
const pc = activeCall.connection;
|
||||
// console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? offer : adaptSdpToOldWebView(offer)));
|
||||
|
||||
@@ -121,7 +121,7 @@ Reconnection: Connected -> Reconnecting -> Connected | Ended
|
||||
2. The WebRTC engine reports `"reconnecting"` connection state; the app shows "reconnecting…" and plays the connecting sound. This state is not reported to the core, and `connectedAt` is kept, so call duration continues.
|
||||
3. The callee's WebRTC engine (the party that sent the initial offer) restarts ICE every 10 seconds, sending the new offer in `WCallResponse.Ice` with `session` via `apiSendCallExtraInfo`. The caller answers the same way.
|
||||
4. When the connection is restored, the call returns to `Connected` and the sound stops.
|
||||
5. If the connection is not restored in 30 seconds, the call ends as disconnected. Clients without ICE restart support ignore the restart offer, so the call ends in this case.
|
||||
5. If the connection is not restored in 30 seconds, the call ends as disconnected. If the contact's chat version is below 22 (`CALL_RECONNECT_VERSION`), the call ends as soon as the connection is lost, without reconnection.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ enum class CallState {
|
||||
|
||||
State transitions are driven by `WCallResponse` messages from the WebRTC layer. Each transition typically triggers a corresponding API command (e.g., `apiSendCallInvitation`, `apiSendCallOffer`).
|
||||
|
||||
**Reconnection**: `Connected` -> `Reconnecting` -> `Connected` or `Ended`. When a connected call loses its WebRTC connection, `call.js` reports `"reconnecting"` connection state instead of ending the call. The app sets `CallState.Reconnecting` and plays the connecting sound; this state is not sent to the core with `apiCallStatus`, so the call item duration is not affected, and `connectedAt` is kept. The party that sent the initial offer (callee) restarts ICE every 10 seconds until reconnected: the restart offer and the answer are sent as `WCallResponse.Ice` / `WCallCommand.Ice` with `session`, via `apiSendCallExtraInfo` (`x.call.extra` with `rtcSession`). If the connection is not restored in 30 seconds, `call.js` reports `"disconnected"` and ends the call.
|
||||
**Reconnection**: `Connected` -> `Reconnecting` -> `Connected` or `Ended`. When a connected call loses its WebRTC connection, `call.js` reports `"reconnecting"` connection state instead of ending the call. The app sets `CallState.Reconnecting` and plays the connecting sound; this state is not sent to the core with `apiCallStatus`, so the call item duration is not affected, and `connectedAt` is kept. The party that sent the initial offer (callee) restarts ICE every 10 seconds until reconnected: the restart offer and the answer are sent as `WCallResponse.Ice` / `WCallCommand.Ice` with `session`, via `apiSendCallExtraInfo` (`x.call.extra` with `rtcSession`). If the connection is not restored in 30 seconds, `call.js` reports `"disconnected"` and ends the call. Reconnection is enabled by `canReconnect` in `WCallCommand.Start` / `WCallCommand.Offer`, set when the contact's peer chat version is at least `CALL_RECONNECT_VERSION` (22); otherwise the call ends as soon as the connection is lost.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -286,7 +286,7 @@ These message are used for WebRTC calls:
|
||||
|
||||
3. `x.call.answer`: to continue with call connection the initiating clients must reply with `x.call.answer` message. This message contains WebRTC answer and collected ICE candidates. Additional ICE candidates can be sent in `x.call.extra` message.
|
||||
|
||||
4. `x.call.extra` with `rtcSession` property: when an established connection is lost, the client that sent `x.call.offer` restarts ICE and sends the new offer in `rtcSession` of `x.call.extra`. The other client replies with the answer in `rtcSession` of `x.call.extra`. Clients that do not support it ignore this property, and the call ends when reconnection times out.
|
||||
4. `x.call.extra` with `rtcSession` property: when an established connection is lost, the client that sent `x.call.offer` restarts ICE and sends the new offer in `rtcSession` of `x.call.extra`. The other client replies with the answer in `rtcSession` of `x.call.extra`. Clients restart ICE only when the peer supports chat protocol version 22 or later, otherwise the call ends when the connection is lost.
|
||||
|
||||
5. `x.call.end` message is sent to notify the other party that the call is terminated.
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ interface WCStartCall extends IWCallCommand {
|
||||
aesKey?: string
|
||||
iceServers?: RTCIceServer[]
|
||||
relay?: boolean
|
||||
canReconnect?: boolean
|
||||
}
|
||||
|
||||
interface WCEndCall extends IWCallCommand {
|
||||
@@ -124,6 +125,7 @@ interface WCAcceptOffer extends IWCallCommand {
|
||||
aesKey?: string
|
||||
iceServers?: RTCIceServer[]
|
||||
relay?: boolean
|
||||
canReconnect?: boolean
|
||||
}
|
||||
|
||||
interface WCallOffer extends IWCallResponse {
|
||||
@@ -429,7 +431,7 @@ const processCommand = (function () {
|
||||
})
|
||||
}
|
||||
|
||||
async function initializeCall(config: CallConfig, mediaType: CallMediaType, aesKey?: string): Promise<Call> {
|
||||
async function initializeCall(config: CallConfig, mediaType: CallMediaType, aesKey?: string, canReconnect?: boolean): Promise<Call> {
|
||||
let pc: RTCPeerConnection
|
||||
try {
|
||||
pc = new RTCPeerConnection(config.peerConnectionConfig)
|
||||
@@ -520,7 +522,7 @@ const processCommand = (function () {
|
||||
: pc.iceConnectionState == "completed"
|
||||
? "connected"
|
||||
: "connecting") /* webView 69-70 doesn't have connectionState yet */
|
||||
if (wasConnected) {
|
||||
if (wasConnected && canReconnect) {
|
||||
if (reconnectTimedOut) {
|
||||
reconnectingTimeout = undefined
|
||||
if (activeCall !== call || state == "connected") return
|
||||
@@ -700,10 +702,10 @@ const processCommand = (function () {
|
||||
inactiveCallMediaSourcesChanged(inactiveCallMediaSources)
|
||||
setupLocalVideoRatio(getVideoElements()!.local)
|
||||
|
||||
const {media, iceServers, relay} = command
|
||||
const {media, iceServers, relay, canReconnect} = command
|
||||
const encryption = supportsInsertableStreams(useWorker)
|
||||
const aesKey = encryption ? command.aesKey : undefined
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey)
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey, canReconnect)
|
||||
await setupLocalStream(true, activeCall)
|
||||
setupCodecPreferences(activeCall)
|
||||
|
||||
@@ -742,8 +744,8 @@ const processCommand = (function () {
|
||||
} else {
|
||||
const offer: RTCSessionDescriptionInit = parse(command.offer)
|
||||
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
|
||||
const {media, aesKey, iceServers, relay} = command
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey)
|
||||
const {media, aesKey, iceServers, relay, canReconnect} = command
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey, canReconnect)
|
||||
const pc = activeCall.connection
|
||||
// console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? offer : adaptSdpToOldWebView(offer)))
|
||||
|
||||
@@ -14,6 +14,7 @@ const DISCONNECT_DETECTION = 10000
|
||||
const RECONNECT = 20000
|
||||
const SIGNALING = 15000
|
||||
const CORE_CALL_STATUSES = ["connecting", "connected", "disconnected", "failed"]
|
||||
const CALL_RECONNECT_VERSION = 22
|
||||
|
||||
async function withChatCall({useWorker = false}, run) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "simplex-call-test-"))
|
||||
@@ -78,7 +79,13 @@ async function startCall({a, b, network, alice, bob, aliceContact, bobContact})
|
||||
await alice.cmd(`/_call invite @${aliceContact} ${JSON.stringify(callType)}`)
|
||||
|
||||
const {callInvitation} = await bob.waitEvent((e) => e.type === "callInvitation", SIGNALING, "callInvitation")
|
||||
const offer = await b.command({type: "start", media: MEDIA, aesKey: callInvitation.sharedKey, iceServers: []})
|
||||
const offer = await b.command({
|
||||
type: "start",
|
||||
media: MEDIA,
|
||||
aesKey: callInvitation.sharedKey,
|
||||
iceServers: [],
|
||||
canReconnect: canReconnect(callInvitation.contact),
|
||||
})
|
||||
assert.equal(offer.resp.type, "offer", JSON.stringify(offer.resp))
|
||||
const offerSignal = await network.routeSignal("B", offer.resp.offer, offer.resp.iceCandidates)
|
||||
const callOffer = {
|
||||
@@ -95,6 +102,7 @@ async function startCall({a, b, network, alice, bob, aliceContact, bobContact})
|
||||
media: MEDIA,
|
||||
aesKey: offered.sharedKey,
|
||||
iceServers: [],
|
||||
canReconnect: canReconnect(offered.contact),
|
||||
})
|
||||
assert.equal(answer.resp.type, "answer", JSON.stringify(answer.resp))
|
||||
const answerSignal = await network.routeSignal("A", answer.resp.answer, answer.resp.iceCandidates)
|
||||
@@ -107,6 +115,10 @@ async function startCall({a, b, network, alice, bob, aliceContact, bobContact})
|
||||
await Promise.all([a.waitForState("connected", RECONNECT), b.waitForState("connected", RECONNECT)])
|
||||
}
|
||||
|
||||
function canReconnect(contact) {
|
||||
return contact.activeConn.peerChatVRange.maxVersion >= CALL_RECONNECT_VERSION
|
||||
}
|
||||
|
||||
async function assertAudioDecoded(a, b) {
|
||||
const before = [await a.receivedAudioEnergy(), await b.receivedAudioEnergy()]
|
||||
await sleep(2000)
|
||||
|
||||
@@ -129,10 +129,9 @@ class Network {
|
||||
}
|
||||
|
||||
class Peer {
|
||||
constructor(name, page, {ignoresIceRestart = false} = {}) {
|
||||
constructor(name, page) {
|
||||
this.name = name
|
||||
this.page = page
|
||||
this.ignoresIceRestart = ignoresIceRestart
|
||||
this.messages = []
|
||||
this.waiters = []
|
||||
this.onResponse = async () => {}
|
||||
@@ -245,7 +244,7 @@ class Signaling {
|
||||
}
|
||||
|
||||
// useWorker encrypts media in a worker with RTCRtpScriptTransform, as the desktop app does
|
||||
async function openPeers({answerer = {}, useWorker = false} = {}) {
|
||||
async function openPeers({useWorker = false} = {}) {
|
||||
const server = await servePage()
|
||||
const url = `http://localhost:${server.address().port}/android/call.html`
|
||||
const browser = await chromium.launch({
|
||||
@@ -257,15 +256,15 @@ async function openPeers({answerer = {}, useWorker = false} = {}) {
|
||||
"--autoplay-policy=no-user-gesture-required",
|
||||
],
|
||||
})
|
||||
const newPeer = async (name, opts) => {
|
||||
const newPeer = async (name) => {
|
||||
const context = await browser.newContext({permissions: ["camera", "microphone"]})
|
||||
const peer = new Peer(name, await context.newPage(), opts)
|
||||
const peer = new Peer(name, await context.newPage())
|
||||
await peer.open(url)
|
||||
if (useWorker) await peer.page.evaluate(() => (useWorker = true))
|
||||
return peer
|
||||
}
|
||||
// A sends the call invitation and B accepts it, so B sends the WebRTC offer, as "start" command in the apps
|
||||
const a = await newPeer("A", answerer)
|
||||
const a = await newPeer("A")
|
||||
const b = await newPeer("B")
|
||||
const network = new Network()
|
||||
const signaling = new Signaling(network)
|
||||
@@ -279,20 +278,20 @@ async function openPeers({answerer = {}, useWorker = false} = {}) {
|
||||
}
|
||||
|
||||
// The test relays signaling between the peers directly, as the apps and the chat core would do.
|
||||
async function startCall({media = "audio", answerer = {}} = {}) {
|
||||
const peers = await openPeers({answerer})
|
||||
async function startCall({media = "audio", canReconnect = true} = {}) {
|
||||
const peers = await openPeers()
|
||||
const {a, b, network, signaling, close} = peers
|
||||
const relayIce = (from, to) => async (resp) => {
|
||||
if (resp.type !== "ice") return
|
||||
const {session, iceCandidates} = await network.routeSignal(from.name, resp.session, resp.iceCandidates)
|
||||
signaling.send(() => to.command({type: "ice", iceCandidates, session: to.ignoresIceRestart ? undefined : session}))
|
||||
signaling.send(() => to.command({type: "ice", iceCandidates, session}))
|
||||
}
|
||||
a.onResponse = relayIce(a, b)
|
||||
b.onResponse = relayIce(b, a)
|
||||
|
||||
try {
|
||||
await a.command({type: "capabilities", media})
|
||||
const offer = await b.command({type: "start", media, iceServers: []})
|
||||
const offer = await b.command({type: "start", media, iceServers: [], canReconnect})
|
||||
if (offer.resp.type !== "offer") throw new Error(`B start: ${JSON.stringify(offer.resp)}`)
|
||||
const offerSignal = await network.routeSignal("B", offer.resp.offer, offer.resp.iceCandidates)
|
||||
const answer = await a.command({
|
||||
@@ -301,6 +300,7 @@ async function startCall({media = "audio", answerer = {}} = {}) {
|
||||
iceCandidates: offerSignal.iceCandidates,
|
||||
media,
|
||||
iceServers: [],
|
||||
canReconnect,
|
||||
})
|
||||
if (answer.resp.type !== "answer") throw new Error(`A offer: ${JSON.stringify(answer.resp)}`)
|
||||
const answerSignal = await network.routeSignal("A", answer.resp.answer, answer.resp.iceCandidates)
|
||||
|
||||
@@ -83,13 +83,15 @@ test("call reconnection", {concurrency: true}, async (t) => {
|
||||
})
|
||||
)
|
||||
|
||||
await t.test("ends the call when the peer does not support ICE restart", () =>
|
||||
withCall({answerer: {ignoresIceRestart: true}}, async ({a, b, network}) => {
|
||||
await Promise.all([a.setTimeouts({reconnectTimeout: 10000}), b.setTimeouts({reconnectTimeout: 10000})])
|
||||
network.changePath()
|
||||
await Promise.all([a.waitForEnded(DISCONNECT_DETECTION + 15000), b.waitForEnded(DISCONNECT_DETECTION + 15000)])
|
||||
assert.ok(b.sessionsSent() > 0, "offerer did not restart ICE")
|
||||
assert.equal(a.sessionsSent(), 0)
|
||||
await t.test("ends the call without reconnecting when the peer does not support ICE restart", () =>
|
||||
withCall({canReconnect: false}, async ({a, b, network}) => {
|
||||
network.down = true
|
||||
await Promise.all([a.waitForEnded(DISCONNECT_DETECTION), b.waitForEnded(DISCONNECT_DETECTION)])
|
||||
assert.ok(!a.connectionStates().includes("reconnecting"), `A states: ${a.connectionStates()}`)
|
||||
assert.ok(!b.connectionStates().includes("reconnecting"), `B states: ${b.connectionStates()}`)
|
||||
assert.equal(a.connectionStates().at(-1), "disconnected")
|
||||
assert.equal(b.connectionStates().at(-1), "disconnected")
|
||||
assert.equal(a.sessionsSent() + b.sessionsSent(), 0, "ICE was restarted")
|
||||
})
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user