diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index e7fd11f5ac..dbcd05b3c7 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -737,7 +737,11 @@ fun WebRTCView(callCommand: SnapshotStateList, onResponse: (WVAPIM } } catch (e: Exception) { Log.e(TAG, "Error initializing WebView: ${e.stackTraceToString()}") - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString())) + if (e.stackTraceToString().contains("/lib64")) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view_wrong_arch).format(e.stackTraceToString())) + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString())) + } return@AndroidView View(androidAppContext) } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 901a0565e1..429ca4af09 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -989,6 +989,7 @@ Headphones Bluetooth Error initializing WebView. Update your system to the new version. Please contact developers.\nError: %s + Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s The next generation\nof private messaging diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js index 97d157b762..665bd14a74 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/call.js @@ -76,6 +76,8 @@ const processCommand = (function () { iceCandidatePoolSize: 10, encodedInsertableStreams, iceTransportPolicy: relay ? "relay" : "all", + // needed for Android WebView >= 69 && <= 72 where default was "plan-b" which is incompatible with transceivers + sdpSemantics: "unified-plan", }, iceCandidates: { delay: 750, @@ -202,7 +204,12 @@ const processCommand = (function () { localOrPeerMediaSourcesChanged(call); await setupMediaStreams(call); let connectionTimeout = setTimeout(connectionHandler, answerTimeout); - pc.addEventListener("connectionstatechange", connectionStateChange); + if (pc.connectionState) { + pc.addEventListener("connectionstatechange", connectionStateChange); + } + else { + pc.addEventListener("iceconnectionstatechange", connectionStateChange); + } return call; async function connectionStateChange() { // "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView) @@ -211,26 +218,38 @@ const processCommand = (function () { connectionHandler(); } async function connectionHandler() { + var _a; sendMessageToNative({ resp: { type: "connection", state: { - connectionState: pc.connectionState, + connectionState: (_a = pc.connectionState) !== null && _a !== void 0 ? _a : (pc.iceConnectionState != "completed" && pc.iceConnectionState != "checking" + ? pc.iceConnectionState + : pc.iceConnectionState == "completed" + ? "connected" + : "connecting") /* webView 69-70 doesn't have connectionState yet */, iceConnectionState: pc.iceConnectionState, iceGatheringState: pc.iceGatheringState, signalingState: pc.signalingState, }, }, }); - if (pc.connectionState == "disconnected" || pc.connectionState == "failed") { + if (pc.connectionState == "disconnected" || + pc.connectionState == "failed" || + (!pc.connectionState && (pc.iceConnectionState == "disconnected" || pc.iceConnectionState == "failed"))) { clearConnectionTimeout(); - pc.removeEventListener("connectionstatechange", connectionStateChange); + if (pc.connectionState) { + pc.removeEventListener("connectionstatechange", connectionStateChange); + } + else { + pc.removeEventListener("iceconnectionstatechange", connectionStateChange); + } if (activeCall) { setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0); } endCall(); } - else if (pc.connectionState == "connected") { + else if (pc.connectionState == "connected" || (!pc.connectionState && pc.iceConnectionState == "connected")) { clearConnectionTimeout(); const stats = (await pc.getStats()); for (const stat of stats.values()) { @@ -355,7 +374,7 @@ const processCommand = (function () { activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey); const pc = activeCall.connection; // console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(offer)); + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? offer : adaptSdpToOldWebView(offer))); // setting up local stream only after setRemoteDescription in order to have transceivers set await setupLocalStream(false, activeCall); setupEncryptionForLocalStream(activeCall); @@ -397,7 +416,7 @@ const processCommand = (function () { const answer = parse(command.answer); const remoteIceCandidates = parse(command.iceCandidates); // console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(answer)); + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? answer : adaptSdpToOldWebView(answer))); adaptToOldVersion(pc.getTransceivers()[2].currentDirection == "sendonly", activeCall); addIceCandidates(pc, remoteIceCandidates); addIceCandidates(pc, afterCallInitializedCandidates); @@ -934,6 +953,7 @@ const processCommand = (function () { }); } if (inboundStatsId) { + // even though MSDN site says `packetsReceived` is available in WebView 80+, in reality it's available even in 69 const packets = (_a = stats.get(inboundStatsId)) === null || _a === void 0 ? void 0 : _a.packetsReceived; if (packets <= lastPacketsReceived) { mutedSeconds++; @@ -1156,6 +1176,22 @@ const processCommand = (function () { } } } + function webView69Or70() { + return !isDesktop && (navigator.userAgent.includes("Chrome/69.") || navigator.userAgent.includes("Chrome/70.")); + } + // Adding `a=extmap-allow-mixed` causes exception on old WebViews + // https://groups.google.com/a/chromium.org/g/blink-dev/c/7z3uvp0-ZAc/m/8Z7qpp71BgAJ + function adaptSdpToOldWebView(desc) { + var _a; + const res = []; + (_a = desc.sdp) === null || _a === void 0 ? void 0 : _a.split("\n").forEach((line) => { + // Chrome has a bug related to SDP parser in old web view versions + if (!line.includes("a=extmap-allow-mixed")) { + res.push(line); + } + }); + return { sdp: res.join("\n"), type: desc.type }; + } return processCommand; })(); function toggleRemoteVideoFitFill() { diff --git a/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js b/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js index 2d1900a0d3..f7c26ae2b4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js +++ b/apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js @@ -1 +1 @@ -var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;ne;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;ie;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString); +var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e>=1}else{for(t=1,e=0;e>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString}); \ No newline at end of file diff --git a/packages/simplex-chat-webrtc/src/call.ts b/packages/simplex-chat-webrtc/src/call.ts index cf91af07dc..1a83c159c1 100644 --- a/packages/simplex-chat-webrtc/src/call.ts +++ b/packages/simplex-chat-webrtc/src/call.ts @@ -311,8 +311,12 @@ const processCommand = (function () { encodedInsertableStreams: boolean } + type RTCConfigurationWithSdpSemantics = RTCConfiguration & { + sdpSemantics: string + } + interface CallConfig { - peerConnectionConfig: RTCConfigurationWithEncryption + peerConnectionConfig: RTCConfigurationWithEncryption & RTCConfigurationWithSdpSemantics iceCandidates: { delay: number extrasInterval: number @@ -334,6 +338,8 @@ const processCommand = (function () { iceCandidatePoolSize: 10, encodedInsertableStreams, iceTransportPolicy: relay ? "relay" : "all", + // needed for Android WebView >= 69 && <= 72 where default was "plan-b" which is incompatible with transceivers + sdpSemantics: "unified-plan", }, iceCandidates: { delay: 750, @@ -458,7 +464,11 @@ const processCommand = (function () { localOrPeerMediaSourcesChanged(call) await setupMediaStreams(call) let connectionTimeout: number | undefined = setTimeout(connectionHandler, answerTimeout) - pc.addEventListener("connectionstatechange", connectionStateChange) + if (pc.connectionState) { + pc.addEventListener("connectionstatechange", connectionStateChange) + } else { + pc.addEventListener("iceconnectionstatechange", connectionStateChange) + } return call async function connectionStateChange() { @@ -472,21 +482,35 @@ const processCommand = (function () { resp: { type: "connection", state: { - connectionState: pc.connectionState, + connectionState: + pc.connectionState ?? + (pc.iceConnectionState != "completed" && pc.iceConnectionState != "checking" + ? pc.iceConnectionState + : pc.iceConnectionState == "completed" + ? "connected" + : "connecting") /* webView 69-70 doesn't have connectionState yet */, iceConnectionState: pc.iceConnectionState, iceGatheringState: pc.iceGatheringState, signalingState: pc.signalingState, }, }, }) - if (pc.connectionState == "disconnected" || pc.connectionState == "failed") { + if ( + pc.connectionState == "disconnected" || + pc.connectionState == "failed" || + (!pc.connectionState && (pc.iceConnectionState == "disconnected" || pc.iceConnectionState == "failed")) + ) { clearConnectionTimeout() - pc.removeEventListener("connectionstatechange", connectionStateChange) + if (pc.connectionState) { + pc.removeEventListener("connectionstatechange", connectionStateChange) + } else { + pc.removeEventListener("iceconnectionstatechange", connectionStateChange) + } if (activeCall) { setTimeout(() => sendMessageToNative({resp: {type: "ended"}}), 0) } endCall() - } else if (pc.connectionState == "connected") { + } else if (pc.connectionState == "connected" || (!pc.connectionState && pc.iceConnectionState == "connected")) { clearConnectionTimeout() const stats = (await pc.getStats()) as Map for (const stat of stats.values()) { @@ -613,7 +637,7 @@ const processCommand = (function () { activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey) const pc = activeCall.connection // console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(offer)) + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? offer : adaptSdpToOldWebView(offer))) // setting up local stream only after setRemoteDescription in order to have transceivers set await setupLocalStream(false, activeCall) setupEncryptionForLocalStream(activeCall) @@ -654,7 +678,7 @@ const processCommand = (function () { const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates) // console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates)) - await pc.setRemoteDescription(new RTCSessionDescription(answer)) + await pc.setRemoteDescription(new RTCSessionDescription(!webView69Or70() ? answer : adaptSdpToOldWebView(answer))) adaptToOldVersion(pc.getTransceivers()[2].currentDirection == "sendonly", activeCall!) addIceCandidates(pc, remoteIceCandidates) addIceCandidates(pc, afterCallInitializedCandidates) @@ -1219,6 +1243,7 @@ const processCommand = (function () { }) } if (inboundStatsId) { + // even though MSDN site says `packetsReceived` is available in WebView 80+, in reality it's available even in 69 const packets = (stats as any).get(inboundStatsId)?.packetsReceived if (packets <= lastPacketsReceived) { mutedSeconds++ @@ -1447,6 +1472,23 @@ const processCommand = (function () { } } + function webView69Or70(): boolean { + return !isDesktop && (navigator.userAgent.includes("Chrome/69.") || navigator.userAgent.includes("Chrome/70.")) + } + + // Adding `a=extmap-allow-mixed` causes exception on old WebViews + // https://groups.google.com/a/chromium.org/g/blink-dev/c/7z3uvp0-ZAc/m/8Z7qpp71BgAJ + function adaptSdpToOldWebView(desc: RTCSessionDescriptionInit): RTCSessionDescriptionInit { + const res: string[] = [] + desc.sdp?.split("\n").forEach((line) => { + // Chrome has a bug related to SDP parser in old web view versions + if (!line.includes("a=extmap-allow-mixed")) { + res.push(line) + } + }) + return {sdp: res.join("\n"), type: desc.type} + } + return processCommand })()