mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
mobile: prevent WebRTC call failure/hanging when webview "failed" state happens before 30 sec timeout (#1866)
* mobile: do not end calls * better way of continue connection and end with timeout * making data classes instead of classes for making logs informative * refactor * update webrtc package version * refactor * fix * clear conneciton timeout on disconnection * refactor * v0.2.1 --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
4a58ca60ac
commit
f22ee1a6cf
4 changed files with 58 additions and 25 deletions
|
@ -22,6 +22,7 @@ var TransformOperation;
|
||||||
TransformOperation["Decrypt"] = "decrypt";
|
TransformOperation["Decrypt"] = "decrypt";
|
||||||
})(TransformOperation || (TransformOperation = {}));
|
})(TransformOperation || (TransformOperation = {}));
|
||||||
let activeCall;
|
let activeCall;
|
||||||
|
let answerTimeout = 30000;
|
||||||
const processCommand = (function () {
|
const processCommand = (function () {
|
||||||
const defaultIceServers = [
|
const defaultIceServers = [
|
||||||
{ urls: ["stun:stun.simplex.im:443"] },
|
{ urls: ["stun:stun.simplex.im:443"] },
|
||||||
|
@ -100,9 +101,16 @@ const processCommand = (function () {
|
||||||
const iceCandidates = getIceCandidates(pc, config);
|
const iceCandidates = getIceCandidates(pc, config);
|
||||||
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
||||||
await setupMediaStreams(call);
|
await setupMediaStreams(call);
|
||||||
|
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
|
||||||
pc.addEventListener("connectionstatechange", connectionStateChange);
|
pc.addEventListener("connectionstatechange", connectionStateChange);
|
||||||
return call;
|
return call;
|
||||||
async function connectionStateChange() {
|
async function connectionStateChange() {
|
||||||
|
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
|
||||||
|
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
|
||||||
|
if (pc.connectionState !== "failed")
|
||||||
|
connectionHandler();
|
||||||
|
}
|
||||||
|
async function connectionHandler() {
|
||||||
sendMessageToNative({
|
sendMessageToNative({
|
||||||
resp: {
|
resp: {
|
||||||
type: "connection",
|
type: "connection",
|
||||||
|
@ -115,6 +123,7 @@ const processCommand = (function () {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
||||||
|
clearConnectionTimeout();
|
||||||
pc.removeEventListener("connectionstatechange", connectionStateChange);
|
pc.removeEventListener("connectionstatechange", connectionStateChange);
|
||||||
if (activeCall) {
|
if (activeCall) {
|
||||||
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
|
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
|
||||||
|
@ -122,6 +131,7 @@ const processCommand = (function () {
|
||||||
endCall();
|
endCall();
|
||||||
}
|
}
|
||||||
else if (pc.connectionState == "connected") {
|
else if (pc.connectionState == "connected") {
|
||||||
|
clearConnectionTimeout();
|
||||||
const stats = (await pc.getStats());
|
const stats = (await pc.getStats());
|
||||||
for (const stat of stats.values()) {
|
for (const stat of stats.values()) {
|
||||||
const { type, state } = stat;
|
const { type, state } = stat;
|
||||||
|
@ -141,6 +151,12 @@ const processCommand = (function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function clearConnectionTimeout() {
|
||||||
|
if (connectionTimeout) {
|
||||||
|
clearTimeout(connectionTimeout);
|
||||||
|
connectionTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function serialize(x) {
|
function serialize(x) {
|
||||||
return LZString.compressToBase64(JSON.stringify(x));
|
return LZString.compressToBase64(JSON.stringify(x));
|
||||||
|
|
|
@ -62,39 +62,39 @@ enum class CallState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||||
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class WCallCommand {
|
sealed class WCallCommand {
|
||||||
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
|
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
|
||||||
@Serializable @SerialName("start") class Start(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): WCallCommand()
|
||||||
@Serializable @SerialName("offer") 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("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("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand()
|
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
|
||||||
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||||
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand()
|
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||||
@Serializable @SerialName("end") object End: WCallCommand()
|
@Serializable @SerialName("end") object End: WCallCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class WCallResponse {
|
sealed class WCallResponse {
|
||||||
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
@Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||||
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
@Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse()
|
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
|
||||||
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse()
|
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
|
||||||
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||||
@Serializable @SerialName("error") class Error(val message: String): WCallResponse()
|
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
@Serializable data class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
||||||
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||||
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
|
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||||
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||||
@Serializable class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||||
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
|
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
|
||||||
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
|
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
|
||||||
|
@ -104,8 +104,8 @@ sealed class WCallResponse {
|
||||||
CallMediaType.Audio -> R.string.incoming_audio_call
|
CallMediaType.Audio -> R.string.incoming_audio_call
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@Serializable class CallCapabilities(val encryption: Boolean)
|
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||||
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||||
val text: String @Composable get() = when {
|
val text: String @Composable get() = when {
|
||||||
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
|
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
|
||||||
stringResource(R.string.call_connection_peer_to_peer)
|
stringResource(R.string.call_connection_peer_to_peer)
|
||||||
|
@ -116,7 +116,7 @@ sealed class WCallResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||||
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ enum class VideoCamera {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ConnectionState(
|
data class ConnectionState(
|
||||||
val connectionState: String,
|
val connectionState: String,
|
||||||
val iceConnectionState: String,
|
val iceConnectionState: String,
|
||||||
val iceGatheringState: String,
|
val iceGatheringState: String,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@simplex-chat/webrtc",
|
"name": "@simplex-chat/webrtc",
|
||||||
"version": "0.1.1",
|
"version": "0.2.1",
|
||||||
"description": "WebRTC call in browser and webview for SimpleX Chat clients",
|
"description": "WebRTC call in browser and webview for SimpleX Chat clients",
|
||||||
"main": "dist/call.js",
|
"main": "dist/call.js",
|
||||||
"types": "dist/call.d.ts",
|
"types": "dist/call.d.ts",
|
||||||
|
|
|
@ -191,6 +191,7 @@ interface Call {
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeCall: Call | undefined
|
let activeCall: Call | undefined
|
||||||
|
let answerTimeout = 30_000
|
||||||
|
|
||||||
const processCommand = (function () {
|
const processCommand = (function () {
|
||||||
type RTCRtpSenderWithEncryption = RTCRtpSender & {
|
type RTCRtpSenderWithEncryption = RTCRtpSender & {
|
||||||
|
@ -294,10 +295,17 @@ const processCommand = (function () {
|
||||||
const iceCandidates = getIceCandidates(pc, config)
|
const iceCandidates = getIceCandidates(pc, config)
|
||||||
const call = {connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker}
|
const call = {connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker}
|
||||||
await setupMediaStreams(call)
|
await setupMediaStreams(call)
|
||||||
|
let connectionTimeout: number | undefined = setTimeout(connectionHandler, answerTimeout)
|
||||||
pc.addEventListener("connectionstatechange", connectionStateChange)
|
pc.addEventListener("connectionstatechange", connectionStateChange)
|
||||||
return call
|
return call
|
||||||
|
|
||||||
async function connectionStateChange() {
|
async function connectionStateChange() {
|
||||||
|
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
|
||||||
|
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
|
||||||
|
if (pc.connectionState !== "failed") connectionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectionHandler() {
|
||||||
sendMessageToNative({
|
sendMessageToNative({
|
||||||
resp: {
|
resp: {
|
||||||
type: "connection",
|
type: "connection",
|
||||||
|
@ -310,12 +318,14 @@ const processCommand = (function () {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
||||||
|
clearConnectionTimeout()
|
||||||
pc.removeEventListener("connectionstatechange", connectionStateChange)
|
pc.removeEventListener("connectionstatechange", connectionStateChange)
|
||||||
if (activeCall) {
|
if (activeCall) {
|
||||||
setTimeout(() => sendMessageToNative({resp: {type: "ended"}}), 0)
|
setTimeout(() => sendMessageToNative({resp: {type: "ended"}}), 0)
|
||||||
}
|
}
|
||||||
endCall()
|
endCall()
|
||||||
} else if (pc.connectionState == "connected") {
|
} else if (pc.connectionState == "connected") {
|
||||||
|
clearConnectionTimeout()
|
||||||
const stats = (await pc.getStats()) as Map<string, any>
|
const stats = (await pc.getStats()) as Map<string, any>
|
||||||
for (const stat of stats.values()) {
|
for (const stat of stats.values()) {
|
||||||
const {type, state} = stat
|
const {type, state} = stat
|
||||||
|
@ -335,6 +345,13 @@ const processCommand = (function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearConnectionTimeout() {
|
||||||
|
if (connectionTimeout) {
|
||||||
|
clearTimeout(connectionTimeout)
|
||||||
|
connectionTimeout = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serialize<T>(x: T): string {
|
function serialize<T>(x: T): string {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue