@sphoro/bugcatch-web-sdk 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -4
- package/dist/bugcatch-call.es.js +586 -0
- package/dist/bugcatch-call.es.js.map +1 -0
- package/dist/bugcatch-call.umd.js +2 -0
- package/dist/bugcatch-call.umd.js.map +1 -0
- package/dist/bugcatch-core.es.js +3214 -0
- package/dist/bugcatch-core.es.js.map +1 -0
- package/dist/bugcatch-panel.es.js +2402 -0
- package/dist/bugcatch-panel.es.js.map +1 -0
- package/dist/bugcatch-panel.umd.js +1240 -0
- package/dist/bugcatch-panel.umd.js.map +1 -0
- package/dist/bugcatch.es.js +2 -5625
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.standalone.umd.js +1536 -0
- package/dist/bugcatch.standalone.umd.js.map +1 -0
- package/dist/bugcatch.umd.js +89 -1224
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/announcements.js +147 -0
- package/src/busy.js +18 -0
- package/src/call.js +7 -0
- package/src/capture.js +9 -0
- package/src/chunks.js +56 -0
- package/src/gesture.js +33 -0
- package/src/icons.js +18 -0
- package/src/identity.js +13 -8
- package/src/incoming-call.js +8 -30
- package/src/index.d.ts +50 -0
- package/src/index.js +138 -35
- package/src/lazy.js +23 -0
- package/src/notice.js +68 -0
- package/src/panel/call.js +242 -0
- package/src/panel/compose.js +416 -0
- package/src/panel/composer.js +235 -0
- package/src/panel/icons.js +34 -0
- package/src/panel/index.js +251 -0
- package/src/panel/known.js +130 -0
- package/src/panel/messages.js +176 -0
- package/src/panel/notices.js +34 -0
- package/src/panel/picks.js +61 -0
- package/src/panel/refs.js +321 -0
- package/src/panel/render.js +124 -0
- package/src/panel/signin.js +228 -0
- package/src/panel/styles.js +960 -0
- package/src/panel/thread.js +384 -0
- package/src/session.js +8 -3
- package/src/umd/call.js +6 -0
- package/src/umd/lazy.js +98 -0
- package/src/umd/panel.js +6 -0
- package/src/updates.js +20 -7
- package/src/widget.js +250 -3260
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bugcatch-call.es.js","sources":["../src/audio.js","../src/live-call.js","../src/incoming-call.js"],"sourcesContent":["// Microphone to µ-law, and back.\n//\n// sphoro.voice speaks G.711 µ-law at 8 kHz in both directions — the format a\n// telephone carrier hands it, which is what makes a browser call and a phone\n// call indistinguishable to everything downstream. A browser has neither: it\n// captures 32-bit floats at whatever rate the sound card runs at. The gap\n// between those two is a codec and a resampler, and neither is available from\n// the platform, so both are here.\n//\n// This file exists rather than the two callers each having a copy. The agent\n// call sends µ-law because that is the whole conversation; a live call sends it\n// because sphoro.voice cannot record or transcribe a conversation it is not in\n// the middle of, and asks each browser for a copy of its own microphone. Two\n// implementations of G.711 in one package would be one implementation and one\n// that is subtly wrong on the day somebody fixes only the first.\n//\n// Nothing is imported to do it. The SDK runs on someone else's page and has no\n// dependencies, and that rule does not bend for audio.\n\n/** The wire's sample rate. Not negotiable — see the note above. */\nexport const WIRE_RATE = 8000;\n\n/**\n * An audio context, at the wire's rate where the browser will give us one.\n *\n * Asking for 8 kHz outright makes the resampler below dead code on most\n * machines. Some browsers refuse a rate their hardware does not like, so the\n * fallback is an ordinary context and the manual path.\n */\nexport function createContext(rate = WIRE_RATE) {\n const Ctor = window.AudioContext || window.webkitAudioContext;\n try {\n return new Ctor({ sampleRate: rate });\n } catch {\n return new Ctor();\n }\n}\n\n/**\n * Start a context that the browser has parked, if it will let us.\n *\n * Resolves either way. There is nothing useful to do with the rejection: it\n * means this browser wants a user gesture we are not inside, and the call is\n * still worth attempting — a context that stays suspended is a silent call,\n * which is what the caller finds out about from the states it is already being\n * sent.\n */\nexport async function resumeContext(context) {\n if (context.state !== 'suspended') return;\n try {\n await context.resume();\n } catch { /* no activation to spend */ }\n}\n\n/**\n * Drop a signal to the wire rate by averaging each source window.\n *\n * Exported for its own test: the browsers that refuse an 8 kHz context are the\n * ones that need this, and they are exactly the browsers a developer is least\n * likely to be looking at when they change it.\n *\n * Averaging rather than picking every nth sample: plain decimation aliases\n * everything above 4 kHz down into the speech band, and consonants are exactly\n * what lives up there. This is a crude low-pass, but it is the difference\n * between \"sixty\" and \"sixteen\" surviving the trip.\n */\nexport function downsample(input, fromRate, toRate) {\n const ratio = fromRate / toRate;\n const out = new Float32Array(Math.floor(input.length / ratio));\n for (let i = 0; i < out.length; i++) {\n const start = Math.floor(i * ratio);\n const end = Math.min(Math.floor((i + 1) * ratio), input.length);\n let sum = 0;\n for (let j = start; j < end; j++) sum += input[j];\n out[i] = end > start ? sum / (end - start) : 0;\n }\n return out;\n}\n\n/** G.711 µ-law: the bias and clip the standard specifies. */\nconst MU_BIAS = 0x84;\nconst MU_CLIP = 32635;\n\n/**\n * Float samples to µ-law bytes, ready to put on the wire.\n *\n * Exported for the codec test rather than for callers: this file is not the\n * package's entry point, and a hand-rolled G.711 that nothing checks is a\n * silent call nobody can debug from the outside.\n */\nexport function encodeMuLaw(samples) {\n const out = new Uint8Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n // Clamped before scaling: a float outside [-1, 1] is legal in Web Audio and\n // would wrap round to the opposite sign as an integer.\n const clamped = Math.max(-1, Math.min(1, samples[i]));\n let sample = Math.round(clamped * 32767);\n\n const sign = sample < 0 ? 0x80 : 0;\n if (sign) sample = -sample;\n if (sample > MU_CLIP) sample = MU_CLIP;\n sample += MU_BIAS;\n\n let exponent = 7;\n for (let mask = 0x4000; (sample & mask) === 0 && exponent > 0; exponent--, mask >>= 1);\n const mantissa = (sample >> (exponent + 3)) & 0x0F;\n out[i] = ~(sign | (exponent << 4) | mantissa) & 0xFF;\n }\n return out;\n}\n\n/** µ-law bytes back to float samples, ready for an AudioBuffer. */\nexport function decodeMuLaw(bytes) {\n const out = new Float32Array(bytes.length);\n for (let i = 0; i < bytes.length; i++) {\n const byte = ~bytes[i] & 0xFF;\n const sign = byte & 0x80;\n const exponent = (byte >> 4) & 0x07;\n const mantissa = byte & 0x0F;\n const magnitude = (((mantissa << 3) + MU_BIAS) << exponent) - MU_BIAS;\n out[i] = (sign ? -magnitude : magnitude) / 32768;\n }\n return out;\n}\n\n/**\n * How much audio rides in one frame.\n *\n * Measured in samples, and the context above runs at 8 kHz wherever the browser\n * allows it — so this is 64 ms of speech, not the 20-odd it would be at\n * hardware rate. That distinction is the whole reason for the number: at 2048\n * the same code sends a quarter of a second per frame, which is a quarter of a\n * second added to the start of every turn and to every barge-in, and it sounds\n * exactly like an agent that is slow to notice you talking.\n *\n * Not smaller than this. ScriptProcessorNode runs on the main thread, and a\n * block it cannot fill in time is a block it drops — which is a gap in the\n * middle of a word rather than a delay before one. Powers of two only.\n */\nexport const CAPTURE_BLOCK = 512;\n\n/**\n * Tap a microphone and hand each block over as µ-law bytes.\n *\n * ScriptProcessorNode is deprecated in favour of AudioWorklet, and is used\n * anyway. A worklet's processor has to be loaded from a URL, which on this SDK\n * means a blob: URL — and a blob: worklet is refused outright by the\n * Content-Security-Policy of a great many of the sites this code runs on. A\n * deprecated node that works on a customer's page beats a modern one that is\n * blocked by their headers.\n *\n * Returns a `stop` that lets go of the whole chain. The microphone track is\n * deliberately *not* stopped by it: on a live call the same track is carrying\n * the conversation, and stopping it to end a recording would end the call.\n *\n * @param {AudioContext} context\n * @param {MediaStream} stream\n * @param {(bytes: Uint8Array) => void} onFrame\n * @param {number} [wireRate]\n * @returns {{stop: () => void}}\n */\nexport function tapMicrophone(context, stream, onFrame, wireRate = WIRE_RATE) {\n const source = context.createMediaStreamSource(stream);\n const capture = context.createScriptProcessor(CAPTURE_BLOCK, 1, 1);\n\n capture.onaudioprocess = (event) => {\n const input = event.inputBuffer.getChannelData(0);\n const frames = context.sampleRate === wireRate\n ? input\n : downsample(input, context.sampleRate, wireRate);\n onFrame(encodeMuLaw(frames));\n };\n\n source.connect(capture);\n // A ScriptProcessorNode does not run unless it is connected to a destination,\n // even when nothing is meant to come out of it. Zero gain is what keeps the\n // speaker from hearing their own voice played back at them.\n const mute = context.createGain();\n mute.gain.value = 0;\n capture.connect(mute);\n mute.connect(context.destination);\n\n return {\n stop() {\n capture.onaudioprocess = null;\n try { source.disconnect(); } catch { /* already gone */ }\n try { capture.disconnect(); } catch { /* already gone */ }\n try { mute.disconnect(); } catch { /* already gone */ }\n },\n };\n}\n","// Two people, talking directly.\n//\n// The agent call next door is a microphone, a socket and a codec: the browser\n// sends µ-law up to sphoro.voice, a model answers, and every sample passes\n// through a server. This is the opposite arrangement. The audio here goes\n// straight from one browser to the other over WebRTC, and sphoro.voice's part is\n// only to introduce them — it passes an offer, an answer and a handful of ICE\n// candidates between the two sockets and then has nothing left to do.\n//\n// Which means there is no codec in this file, no resampler, and no audio path\n// at all. The browser's own WebRTC stack owns those, and it is better at them\n// than anything hand-written: Opus at whatever rate the two ends agree on,\n// echo cancellation, jitter buffering and packet loss concealment, none of\n// which the µ-law path can have because a telephone carrier cannot receive\n// them. A live call sounds better than the agent call, and costs nothing per\n// minute, because nobody is in the middle of it.\n//\n// # Shared with the dashboard\n//\n// Both ends of a live call run this file: the reporter's widget and the member\n// who answers in the portal, which imports it from here rather than keeping a\n// second copy. The two sides differ by one flag — who makes the offer — and it\n// arrives from the server rather than being decided here, so the two ends\n// cannot both sit waiting for the other to go first.\n//\n// # The one exception: the copy that gets kept\n//\n// An agent that is set to record or transcribe cannot do either from here —\n// the audio it would need never reaches it. So sphoro.voice asks instead: it\n// names, in `session.created`, what this call is keeping and in what format,\n// and each browser uplinks a copy of *its own* microphone over the signaling\n// socket it already has open. That copy is the only way a recording or a\n// transcript of a peer-to-peer call can exist at all, and it is why the codec\n// this file said it did not have is imported below.\n//\n// It is a copy and not the call. The conversation still runs browser to\n// browser; an uplink that stalls or is refused costs a recording, never a call.\n//\n// Nothing outside the package is imported to do any of it. The SDK runs on\n// someone else's page and has no dependencies, and that rule does not bend for\n// a peer connection.\n\nimport { createContext, resumeContext, tapMicrophone } from './audio.js';\n\n/**\n * sphoro.voice's realtime protocol, sent as the WebSocket subprotocol. The same\n * one the agent call speaks — a bridged call is a different conversation over\n * the same envelope, not a different protocol.\n */\nconst SUBPROTOCOL = 'voiceai.realtime.v1';\n\n/**\n * Where a browser looks to find its way to the other one, when the server names\n * nowhere.\n *\n * The server almost always does name somewhere — it is configured per\n * deployment, because a network behind symmetric NAT needs a TURN relay and\n * that costs money. This is the floor: a public STUN server, which is enough\n * for most home and office networks and free.\n */\nconst FALLBACK_ICE = [{ urls: 'stun:stun.l.google.com:19302' }];\n\n/**\n * Ask for the microphone.\n *\n * Separate from joining, and deliberately. The permission prompt takes as long\n * as the person takes to read it, and both ends have something they should not\n * do until it is answered: the reporter should not make a room full of\n * dashboards ring before knowing they can speak, and whoever answers should not\n * claim a call they then cannot take. So both ask first and act second.\n *\n * @returns {Promise<MediaStream>}\n */\nexport function openMicrophone() {\n return navigator.mediaDevices.getUserMedia({\n audio: {\n // Both ends are on speakers as often as not. Without cancellation each\n // hears themselves back through the other, half a second late, which is\n // the single most effective way to make a conversation impossible.\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n channelCount: 1,\n },\n });\n}\n\n/** Whether this browser can be on a live call at all. */\nexport function liveCallSupported() {\n return typeof window !== 'undefined'\n && typeof window.RTCPeerConnection === 'function'\n && Boolean(navigator.mediaDevices?.getUserMedia);\n}\n\n/**\n * Join a live call and stay on it until somebody hangs up.\n *\n * @param {object} opts\n * @param {string} opts.realtimeUrl From the API, carrying its own credential\n * and its own seat. Never log it and never build one by hand.\n * @param {MediaStream} [opts.stream] A microphone already opened by\n * `openMicrophone`. One is opened here if not, which is the wrong order for\n * anything that had a decision to make first.\n * @param {string} [opts.ringsUntil] ISO-8601, on the caller's side only: when\n * the ringing stops. The browser is the thing that decides it has, because it\n * is the one client guaranteed to still be listening at that moment.\n * @param {(state: string, detail?: object) => void} [opts.onState]\n * 'ringing' | 'connecting' | 'connected' | 'reconnecting' | 'ended' | 'missed'\n * | 'failed'. 'reconnecting' is not a failure and never ends a call — see\n * `routeChanged`.\n * @param {(blocked: boolean) => void} [opts.onAudioBlocked] The browser refused\n * to play the other person, or has since started. Its own callback rather\n * than a state, because it is orthogonal to every one of them: the call is\n * connected and working, and one end cannot hear it. See `playRemote`.\n * @param {(live: boolean) => void} [opts.onMicrophone] Whether this person's\n * microphone is still reaching the call. Also orthogonal, and for the same\n * reason: the call is up, the other end is audible, and nothing this person\n * says is going anywhere. See `watchMicrophone`.\n * @returns {Promise<{hangUp: () => void, setMuted: (muted: boolean) => boolean,\n * isMuted: () => boolean}>}\n */\nexport async function joinLiveCall({\n realtimeUrl, stream, ringsUntil, onState, onAudioBlocked, onMicrophone,\n}) {\n const emit = (state, detail) => { try { onState?.(state, detail); } catch { /* theirs */ } };\n const sayBlocked = (blocked) => {\n try { onAudioBlocked?.(blocked); } catch { /* theirs */ }\n };\n const sayMicrophone = (live) => {\n try { onMicrophone?.(live); } catch { /* theirs */ }\n };\n\n if (!liveCallSupported()) {\n const err = new Error('[BugCatch] this browser cannot make live calls.');\n emit('failed', { reason: 'unsupported' });\n throw err;\n }\n\n let microphone = stream;\n if (!microphone) {\n try {\n microphone = await openMicrophone();\n } catch (err) {\n emit('failed', { reason: 'microphone', error: String(err?.name || err) });\n throw err;\n }\n }\n\n let socket;\n try {\n socket = await openSocket(realtimeUrl);\n } catch (err) {\n stopStream(microphone);\n emit('failed', { reason: 'connect' });\n throw err;\n }\n\n let peer = null;\n let remoteAudio = null;\n let finished = false;\n let ringTimer = null;\n // What sphoro.voice said this call is keeping, and the tap feeding it. Both\n // stay null on the ordinary call, which keeps nothing and uplinks nothing.\n let capture = null;\n let uplink = null;\n // Whether the browser is currently refusing to play the other person, and the\n // teardown for the gesture listeners armed to try again. See playRemote.\n let blocked = false;\n let disarmGesture = null;\n /** Teardown for the microphone watch, armed once the peer connection is built. */\n let unwatchMicrophone = null;\n // Claimed synchronously, before the first await below, because the seat that\n // answers is told the call has started twice — once in its own\n // `session.created`, which says somebody is already waiting, and again in the\n // `peer.joined` that goes to both ends a moment later. Two starts that both\n // check `uplink` before either has set it are two taps on one microphone, and\n // two copies of every frame interleaved into one recording.\n let starting = false;\n // Candidates that arrived before there was anywhere to put them.\n //\n // The offer and the first candidates are sent within a millisecond of each\n // other and arrive in that order, but `setRemoteDescription` is a promise —\n // so a candidate can be in hand while the description it belongs to is still\n // being applied. Adding one then throws, and the call comes up with a worse\n // route than it could have had, or none.\n const pending = [];\n\n // --- surviving a change of network ------------------------------------------------\n //\n // A phone call moves. Somebody walks out of the office and the handset hands the\n // connection from Wi-Fi to cellular; a lift takes the signal for four seconds; a laptop\n // wakes on a different access point. Every one of those changes the local address the\n // call was negotiated against, and WebRTC's answer to all of them is the same: the ICE\n // agent reports `disconnected`, and unless somebody restarts it, `failed` a moment later.\n //\n // Before this, `failed` ended the call and `disconnected` was not handled at all — so a\n // call that would have recovered on its own in two seconds was hung up on, and one that\n // needed a restart was hung up on with no attempt made. On a desk that is rare. On the\n // phone this app also ships as, it is what happens when you stand up.\n //\n // What replaces it is a grace period, then an ICE restart, then a deadline. Each is\n // there for a distinct reason:\n //\n // the grace `disconnected` is frequently transient — a couple of lost packets on a\n // route that is still fine. Restarting immediately would tear down a\n // working connection to rebuild the same one, and take audio with it.\n //\n // the restart Renegotiation from scratch over the signaling socket, which is still\n // open: it never depended on the media route. This is the part that\n // actually crosses a network change.\n //\n // the deadline Reconnecting forever is its own failure. Somebody staring at a card\n // that says \"reconnecting\" for five minutes has been abandoned more\n // thoroughly than somebody told the call dropped.\n //\n // Only one side offers, and it is the side the server nominated at the start. Both ends\n // see the drop, so if the two sides both restarted there would be two offers crossing —\n // a glare condition WebRTC resolves badly. The other side asks instead, over the\n // signaling socket, and its request is best-effort: the offerer's own detection is what\n // this actually rests on.\n\n /** Which side was told to make the offer. Set from `peer.joined`, for both ends. */\n let shouldOffer = false;\n /** Whether the two ends have ever been connected. A drop before that is a failed setup. */\n let connectedOnce = false;\n let reconnecting = false;\n let recoverTimer = null;\n let giveUpTimer = null;\n let restarts = 0;\n\n /** How long `disconnected` is allowed to resolve itself before a restart is attempted. */\n const RECOVER_GRACE_MS = 2000;\n /** How long between one restart and the next, when the first did not take. */\n const RECOVER_RETRY_MS = 5000;\n /** How long a call may spend off the air in total before it is called dropped. */\n const RECOVER_DEADLINE_MS = 30000;\n const MAX_RESTARTS = 4;\n\n /** Everything ends here, whatever ended it, exactly once. */\n const finish = (state, detail) => {\n if (finished) return;\n finished = true;\n if (ringTimer) { clearTimeout(ringTimer); ringTimer = null; }\n // Before the peer is closed, and before the stream is stopped. Closing a connection\n // moves it to `closed` and stopping a track fires `ended`, and both would otherwise\n // reach handlers that would report a dropped call and a dead microphone to somebody\n // who has just hung up.\n clearRecovery();\n unwatchMicrophone?.();\n unwatchMicrophone = null;\n stopUplink();\n // Before anything else is torn down: these are listeners on `window`, and a\n // call that left them behind would keep retrying playback on an element\n // that is about to be removed — once per click, for the rest of the session.\n disarmGesture?.();\n disarmGesture = null;\n try { peer?.close(); } catch { /* already gone */ }\n stopStream(microphone);\n if (remoteAudio) {\n remoteAudio.srcObject = null;\n remoteAudio.remove();\n remoteAudio = null;\n }\n try {\n if (socket.readyState === WebSocket.OPEN) socket.close(1000, 'call over');\n } catch { /* already closing */ }\n emit(state, detail);\n };\n\n const send = (message) => {\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));\n };\n\n /**\n * Start sending sphoro.voice a copy of this person's microphone.\n *\n * Only when it asked, and only once both people are on the call. The server\n * throws away anything that arrives while the phone is still ringing — a\n * recording is timed from the moment the second person joined, and a minute\n * of somebody waiting alone at the head of the file is a timeline that\n * disagrees with the call's own duration. Sending it anyway would just be\n * bandwidth spent on audio that gets dropped, on the same network carrying\n * the call.\n *\n * Every failure here is survivable and none of them ends the call. A browser\n * with no Web Audio, a context the autoplay policy will not start, a format\n * this version does not know how to produce — each costs the recording and\n * leaves two people talking, which is the right way round. It is said out\n * loud, though: an agent set to record whose calls silently arrive with no\n * audio is the failure that is impossible to diagnose from the outside.\n */\n const startUplink = async () => {\n if (starting || uplink || finished || !capture) return;\n if (!capture.audio && !capture.transcript) return;\n starting = true;\n\n // The server names the format rather than assuming one, so a client that\n // does not recognise it sends nothing rather than sending µ-law into a\n // recording that expected something else — which would store a file of\n // noise and read as a working recording to everything downstream.\n const encoding = capture.encoding || 'mulaw';\n if (encoding !== 'mulaw') {\n warn(`this call is kept as ${encoding}, which this SDK cannot produce.`);\n return;\n }\n\n\n const rate = Number(capture.sample_rate) || 8000;\n try {\n const context = createContext(rate);\n // A context built without a user gesture starts suspended, and a\n // suspended context's ScriptProcessorNode never fires — so nothing would\n // be uplinked and nothing would say why. Both ends granted microphone\n // access a moment ago, which is the activation this spends.\n await resumeContext(context);\n if (finished) {\n try { context.close(); } catch { /* already closed */ }\n return;\n }\n\n const tap = tapMicrophone(context, microphone, (bytes) => {\n // Binary, not the base64 envelope the protocol also accepts: this is a\n // second copy of audio going up a network that is already carrying the\n // call, and base64 would make it a third larger for nothing.\n if (socket.readyState === WebSocket.OPEN) socket.send(bytes);\n }, rate);\n\n uplink = { context, tap };\n } catch (err) {\n warn(`could not send the copy this call is keeping: ${String(err?.message || err)}`);\n }\n };\n\n /**\n * Stop sending it.\n *\n * The microphone track is deliberately left alone — `finish` stops the whole\n * stream a moment later, and stopping it here would cut the conversation off\n * to end a recording.\n */\n function stopUplink() {\n starting = false;\n if (!uplink) return;\n const { context, tap } = uplink;\n uplink = null;\n try { tap.stop(); } catch { /* already gone */ }\n try { context.close(); } catch { /* already closed */ }\n }\n\n /**\n * Play the other person, and do something about it when the browser says no.\n *\n * The refusal used to be swallowed: `play().catch(() => {})`, on the reasoning\n * that both ends had granted microphone access a moment earlier and that\n * satisfies every autoplay policy. It does not always. A call answered from a\n * notification arrives with no user gesture on the page at all — the tap\n * happened on a system notification, which is not this document — and inside\n * an Android WebView that is exactly the case that fails.\n *\n * What that produced is the worst shape a bug can have: the call connects,\n * every state says `connected`, both ends are billed for it, and one person\n * hears nothing. Nothing is logged, because the rejection was caught and\n * dropped, so there is no evidence anywhere that it happened.\n *\n * So now it is reported and it is retried. The retry is armed on the next\n * gesture anywhere on the page, which is the one thing that reliably lifts an\n * autoplay block, and it is armed in capture so a handler calling\n * `stopPropagation` cannot hide the gesture from it. Whoever is on the call is\n * about to touch something — the mute button, the hang-up button, the report\n * they are talking about — and the sound comes up when they do, with the\n * caller free to say so on screen in the meantime.\n */\n function playRemote() {\n if (!remoteAudio || finished) return;\n let attempt;\n try {\n attempt = remoteAudio.play?.();\n } catch {\n // A synchronous throw, which some engines do instead of rejecting.\n markBlocked();\n return;\n }\n if (!attempt || typeof attempt.then !== 'function') {\n // No promise: an older engine, or a stub. Nothing to wait on and nothing\n // to report — it either made a sound or it did not.\n markPlaying();\n return;\n }\n attempt.then(markPlaying, markBlocked);\n }\n\n function markPlaying() {\n disarmGesture?.();\n disarmGesture = null;\n if (!blocked) return;\n blocked = false;\n sayBlocked(false);\n }\n\n function markBlocked() {\n if (finished) return;\n if (!blocked) {\n blocked = true;\n sayBlocked(true);\n }\n armGesture();\n }\n\n /** Try again on the next thing the person touches. Armed once. */\n function armGesture() {\n if (disarmGesture) return;\n // Guarded rather than assumed: this file is also loaded by a host page's\n // bundler into environments that define a partial `window`, and a retry\n // that cannot be armed is a quieter failure than one that throws inside a\n // promise nobody is awaiting.\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;\n const retry = () => playRemote();\n const events = ['pointerdown', 'keydown', 'touchstart'];\n for (const name of events) {\n window.addEventListener(name, retry, { capture: true, passive: true });\n }\n disarmGesture = () => {\n for (const name of events) {\n try { window.removeEventListener(name, retry, { capture: true }); } catch { /* gone */ }\n }\n };\n }\n\n /**\n * Build the peer connection.\n *\n * Made on `session.created` rather than lazily on the first signal, because\n * the ICE gathering it starts is the slowest part of connecting and the whole\n * of it can happen while the phone is still ringing. Waiting until somebody\n * answers would add that delay to the exact moment a person says hello, which\n * is the one moment on this call where it is audible.\n */\n const build = (iceServers) => {\n peer = new RTCPeerConnection({ iceServers: iceServers?.length ? iceServers : FALLBACK_ICE });\n\n for (const track of microphone.getAudioTracks()) peer.addTrack(track, microphone);\n\n // Armed here rather than at the top, so a call that never got a peer connection has\n // nothing to tear down — and so the watch covers exactly the span in which being\n // unheard is a problem worth telling somebody about.\n unwatchMicrophone = watchMicrophone();\n\n peer.onicecandidate = (event) => {\n if (!event.candidate) return;\n send({ type: 'signal', signal: { kind: 'candidate', payload: event.candidate.toJSON() } });\n };\n\n peer.ontrack = (event) => {\n const [incoming] = event.streams;\n if (!incoming) return;\n // An element rather than the Web Audio API. Audio routed through a\n // MediaStreamAudioSourceNode loses the browser's own echo cancellation on\n // several engines, and this call has a live microphone next to it.\n if (!remoteAudio) {\n remoteAudio = document.createElement('audio');\n remoteAudio.autoplay = true;\n // Inline, for a WebView that would otherwise hand a media element to a\n // full-screen player — which on a call is a black rectangle over the\n // dashboard.\n remoteAudio.playsInline = true;\n remoteAudio.setAttribute?.('playsinline', '');\n // Off-screen rather than display:none — a hidden media element is\n // treated as invisible by some autoplay heuristics and refused.\n remoteAudio.style.cssText = 'position:fixed;width:0;height:0;opacity:0;pointer-events:none';\n document.body.appendChild(remoteAudio);\n }\n remoteAudio.srcObject = incoming;\n playRemote();\n };\n\n peer.onconnectionstatechange = routeChanged;\n // The same signal, read the other way, for the engines that do not give a reliable\n // `connectionState`. WebKit — which is every browser on iOS and the WebView this app\n // ships inside there — has historically fired `iceconnectionstatechange` and not the\n // aggregate one. Both are wired to the same handler, which reads whichever of the two\n // the engine actually maintains, so a duplicate event costs one comparison.\n peer.oniceconnectionstatechange = routeChanged;\n };\n\n /**\n * What the media route is doing, and what to do about it.\n *\n * Reads whichever state the engine keeps up to date, and treats `completed` — which only\n * the ICE-level state has — as connected, because it is: it means a route was found and\n * checking has stopped.\n */\n function routeChanged() {\n if (!peer || finished) return;\n const state = peer.connectionState || peer.iceConnectionState;\n\n if (state === 'connected' || state === 'completed') {\n connectedOnce = true;\n restarts = 0;\n clearRecovery();\n // Emitted whether or not this is a reconnection. The caller renders one state, and a\n // card that came back from \"reconnecting\" has to be told the call is up again.\n reconnecting = false;\n emit('connected');\n return;\n }\n\n if (state === 'disconnected') {\n // Before the two ends ever met, a `disconnected` is ICE still working — it goes\n // there between checking one candidate pair and the next. Only `failed` is final at\n // that stage, and it is handled below.\n if (!connectedOnce) return;\n enterRecovery(RECOVER_GRACE_MS);\n return;\n }\n\n if (state === 'failed') {\n if (!connectedOnce) {\n // The two browsers never found a route to each other at all. Almost always a\n // network that needs a TURN relay the deployment has not configured; reported as\n // its own reason so that is diagnosable from a support ticket rather than from a\n // packet capture. A restart would try the same candidates again.\n finish('failed', { reason: 'ice' });\n return;\n }\n // A route that worked and then stopped. That is a network change, and a restart is\n // exactly the remedy — no grace, because `failed` is not going to resolve itself.\n enterRecovery(0);\n }\n }\n\n /** Say the call is off the air, and line up an attempt to bring it back. */\n function enterRecovery(delay) {\n if (finished) return;\n if (!reconnecting) {\n reconnecting = true;\n emit('reconnecting');\n giveUpTimer = setTimeout(() => finish('failed', { reason: 'network' }), RECOVER_DEADLINE_MS);\n }\n // One attempt in flight at a time. `disconnected` and `failed` both arrive on the way\n // down, and two timers would mean two offers crossing on one connection.\n if (recoverTimer) return;\n recoverTimer = setTimeout(recover, delay);\n }\n\n function clearRecovery() {\n if (recoverTimer) { clearTimeout(recoverTimer); recoverTimer = null; }\n if (giveUpTimer) { clearTimeout(giveUpTimer); giveUpTimer = null; }\n }\n\n /**\n * Renegotiate the media route over the signaling socket, which never left.\n *\n * `createOffer({ iceRestart: true })` rather than `restartIce()`: the latter is the\n * modern spelling and does the same thing by firing `negotiationneeded`, but it is\n * missing from engines this SDK is expected to run in and the explicit form works\n * everywhere. It also keeps the offer on the same path as the original one, so there is a\n * single place where an offer is made.\n */\n async function recover() {\n recoverTimer = null;\n if (finished || !peer) return;\n // It came back on its own while the grace period was running, which is the common case\n // and the reason the grace period exists.\n const state = peer.connectionState || peer.iceConnectionState;\n if (state === 'connected' || state === 'completed') return;\n\n if (!shouldOffer) {\n // Not this side's to restart. Ask, and keep asking — the offerer sees the same drop\n // and is very likely already doing it, so this is the belt to that braces.\n send({ type: 'signal', signal: { kind: 'restart', payload: {} } });\n recoverTimer = setTimeout(recover, RECOVER_RETRY_MS);\n return;\n }\n\n if (restarts >= MAX_RESTARTS) return; // the deadline is what ends it\n restarts += 1;\n await restartIce();\n recoverTimer = setTimeout(recover, RECOVER_RETRY_MS);\n }\n\n /** The restart itself, also reachable from the other side's request. */\n async function restartIce() {\n if (finished || !peer) return;\n try {\n const description = await peer.createOffer({ iceRestart: true });\n await peer.setLocalDescription(description);\n send({ type: 'signal', signal: { kind: 'offer', payload: peer.localDescription.toJSON() } });\n } catch {\n // Nothing to do here but let the next attempt or the deadline decide. Ending the\n // call on one refused renegotiation would throw away the retries that follow.\n }\n }\n\n /**\n * Whether this person is still being heard.\n *\n * A track's `muted` is not the mute button — that is `enabled`, which this file sets and\n * therefore already knows about. `muted` is the *source* stopping: the OS handed the\n * microphone to an incoming phone call, a headset was unplugged, or — the case this app\n * meets daily — Android revoked microphone access because the app went to the background.\n *\n * Which produces the same shape as every other bug on this path: the call is up, the\n * other person is audible, both ends are billed, and nothing this person says goes\n * anywhere. They find out when the other person says \"hello? are you there?\".\n *\n * The loss is reported on a short delay and the recovery immediately. Several engines\n * flick `muted` on for a few frames while a track settles, and a card that said \"your\n * microphone was cut off\" for 200ms at the start of every call would be noise — while a\n * microphone that came back and was still reported lost is the error saying the wrong\n * thing.\n */\n function watchMicrophone() {\n const SETTLE_MS = 600;\n let settle = null;\n let lost = false;\n\n const say = (live) => {\n if (settle) { clearTimeout(settle); settle = null; }\n if (live) {\n if (!lost) return;\n lost = false;\n sayMicrophone(true);\n return;\n }\n if (lost) return;\n settle = setTimeout(() => {\n settle = null;\n if (finished) return;\n lost = true;\n sayMicrophone(false);\n }, SETTLE_MS);\n };\n\n for (const track of microphone.getAudioTracks()) {\n track.onmute = () => say(false);\n track.onunmute = () => say(true);\n // An ended track never comes back — the device is gone. Said at once rather than\n // after the settle, because there is nothing to wait for.\n track.onended = () => {\n if (settle) { clearTimeout(settle); settle = null; }\n if (lost) return;\n lost = true;\n sayMicrophone(false);\n };\n }\n return () => {\n if (settle) { clearTimeout(settle); settle = null; }\n for (const track of microphone.getAudioTracks()) {\n track.onmute = null;\n track.onunmute = null;\n track.onended = null;\n }\n };\n }\n\n const offer = async () => {\n try {\n const description = await peer.createOffer();\n await peer.setLocalDescription(description);\n send({ type: 'signal', signal: { kind: 'offer', payload: peer.localDescription.toJSON() } });\n } catch {\n finish('failed', { reason: 'negotiation' });\n }\n };\n\n const accept = async (payload) => {\n try {\n await peer.setRemoteDescription(new RTCSessionDescription(payload));\n const answer = await peer.createAnswer();\n await peer.setLocalDescription(answer);\n send({ type: 'signal', signal: { kind: 'answer', payload: peer.localDescription.toJSON() } });\n await drainCandidates();\n } catch {\n finish('failed', { reason: 'negotiation' });\n }\n };\n\n const drainCandidates = async () => {\n while (pending.length) {\n const candidate = pending.shift();\n try {\n await peer.addIceCandidate(new RTCIceCandidate(candidate));\n } catch {\n // One unusable candidate is not a failed call — there are usually\n // several, and the connection needs only one of them to work.\n }\n }\n };\n\n /**\n * One message at a time, in the order they arrived.\n *\n * Handling is asynchronous — `setRemoteDescription` and `createAnswer` are\n * promises — and the socket does not wait. Without this chain a candidate\n * arriving mid-answer is examined while the description it belongs to is\n * still being applied, so it goes on the pending list a moment after the\n * pending list was drained, and stays there for the rest of the call.\n */\n let handling = Promise.resolve();\n socket.onmessage = (event) => {\n handling = handling.then(() => handle(event)).catch(() => { /* one bad frame */ });\n };\n\n const handle = async (event) => {\n if (typeof event.data !== 'string') return;\n let message;\n try { message = JSON.parse(event.data); } catch { return; }\n\n switch (message.type) {\n case 'session.created': {\n build(message.data?.ice_servers);\n // What this call keeps, and in what format. Held rather than acted on:\n // nothing is uplinked until somebody is actually on the other end.\n capture = message.data?.capture || null;\n // Ringing, until somebody is on the other side. Whoever answers finds\n // the caller already waiting and skips straight past it — the\n // `peer.joined` that follows a moment later says the same thing, and\n // this is what keeps the answerer's first frame from being silence.\n if (message.data?.peer_present) {\n emit('connecting');\n void startUplink();\n break;\n }\n emit('ringing');\n armRingTimer();\n break;\n }\n\n case 'peer.joined': {\n if (ringTimer) { clearTimeout(ringTimer); ringTimer = null; }\n emit('connecting');\n // Told, not inferred. Both ends learn somebody arrived in the same\n // frame, and only one of them is asked to go first.\n //\n // Remembered as well as acted on: it is also the answer to \"whose job is it to\n // renegotiate when the network changes\", and that question is asked later, when\n // the frame that answered it is long gone. See `recover`.\n shouldOffer = Boolean(message.data?.should_offer);\n if (shouldOffer) await offer();\n // After the offer, and not awaited: opening an audio context is\n // milliseconds, but they would be milliseconds added to the moment\n // somebody says hello, which is the one place on this call where they\n // are audible.\n void startUplink();\n break;\n }\n\n case 'signal': {\n if (!peer) break;\n const { kind, payload } = message.data || {};\n if (kind === 'offer') {\n await accept(payload);\n } else if (kind === 'answer') {\n try {\n await peer.setRemoteDescription(new RTCSessionDescription(payload));\n await drainCandidates();\n } catch {\n finish('failed', { reason: 'negotiation' });\n }\n } else if (kind === 'candidate') {\n if (peer.remoteDescription) {\n try { await peer.addIceCandidate(new RTCIceCandidate(payload)); } catch { /* see above */ }\n } else {\n pending.push(payload);\n }\n } else if (kind === 'restart') {\n // The other side lost the route and cannot renegotiate itself. Honoured only by\n // the side that was nominated to offer, so a request arriving at both ends of a\n // mis-signalled call cannot produce two offers crossing.\n if (shouldOffer) await restartIce();\n }\n break;\n }\n\n case 'peer.left':\n finish('ended', { reason: 'peer_left' });\n break;\n\n case 'error':\n finish('failed', { reason: 'server', detail: message.error?.detail });\n break;\n\n case 'session.ended':\n finish('ended', { reason: 'server' });\n break;\n\n default:\n // Every message the agent conversation has — audio deltas, transcripts,\n // state changes. None of them happen on a bridged call, and one\n // arriving is not worth ending a working call over.\n break;\n }\n };\n\n socket.onclose = () => finish('ended', { reason: 'disconnected' });\n socket.onerror = () => finish('failed', { reason: 'socket' });\n\n /**\n * Stop waiting.\n *\n * The deadline comes from the server, which wrote it down when the call was\n * created — so a browser with a slow clock waits a little longer rather than\n * hanging up on somebody mid-answer, and the server refuses a give-up that\n * arrives early in any case.\n */\n function armRingTimer() {\n if (!ringsUntil) return;\n const remaining = new Date(ringsUntil).getTime() - Date.now();\n if (!Number.isFinite(remaining)) return;\n ringTimer = setTimeout(() => finish('missed'), Math.max(0, remaining));\n }\n\n return {\n hangUp() {\n // Said out loud before the socket closes. A close on its own would end\n // the call too — sphoro.voice ends a bridge the moment one side leaves —\n // but saying so first is what distinguishes hanging up from a tunnel.\n send({ type: 'session.end' });\n finish('ended', { reason: 'hung_up' });\n },\n setMuted(muted) {\n for (const track of microphone.getAudioTracks()) track.enabled = !muted;\n return muted;\n },\n isMuted() {\n return microphone.getAudioTracks().every((track) => !track.enabled);\n },\n };\n}\n\n/** Open the socket, resolving only once it is actually open. */\nfunction openSocket(url) {\n return new Promise((resolve, reject) => {\n let socket;\n try {\n socket = new WebSocket(url, SUBPROTOCOL);\n } catch (err) {\n reject(err);\n return;\n }\n socket.onopen = () => {\n socket.onopen = null;\n socket.onerror = null;\n resolve(socket);\n };\n socket.onerror = () => {\n socket.onopen = null;\n socket.onerror = null;\n reject(new Error('[BugCatch] could not connect the call.'));\n };\n });\n}\n\n/**\n * Say something went wrong with the copy, without ending anything.\n *\n * `console.warn` and not a state: every caller of this file renders the states\n * to somebody who is mid-conversation, and \"the recording did not start\" is not\n * news to the two people talking. It is news to whoever turned recording on,\n * and the console is where they will look.\n */\nfunction warn(message) {\n try { console.warn(`[BugCatch] ${message}`); } catch { /* no console */ }\n}\n\nfunction stopStream(stream) {\n for (const track of stream?.getTracks?.() ?? []) {\n try { track.stop(); } catch { /* already stopped */ }\n }\n}\n","// The reporter's telephone.\n//\n// Everything else this SDK draws is a consequence of something the person on\n// the page did: they pressed the widget, they filed a report, they asked to\n// talk it through. This is the one thing that arrives unasked — a member of the\n// team has a question about a report and is ringing to ask it — and that makes\n// it the piece with the most rules around it.\n//\n// # What it is allowed to do\n//\n// Appear, and make a noise. That is the whole of it. No microphone is opened,\n// no audio moves and nothing is connected until the person presses Answer, and\n// if they press nothing at all it takes itself away when the ringing stops. The\n// card is an offer; the SDK does not put a customer on a call because somebody\n// at the vendor clicked a button.\n//\n// # And what it says\n//\n// A name, a product and what the call is about, because a card that says\n// \"someone is calling about a report\" is indistinguishable from the thing every\n// bank tells people to hang up on. And whether the call would be recorded,\n// stated before Answer rather than after — on a peer-to-peer call the copy that\n// gets kept is uplinked by this browser, so telling somebody afterwards is\n// telling them about something that already happened.\n//\n// # The noise\n//\n// Synthesised, for the reason the dashboard's is: a ringtone as an audio file\n// is a binary in a bundle that runs on somebody else's site, an extra request\n// on their page, and a thing that can go missing behind a cache header. Four\n// oscillators and a table of numbers cost none of that.\n//\n// It is also softer and shorter than the dashboard's. A phone on a support\n// desk is meant to interrupt; this one is ringing in the middle of a stranger's\n// shopping cart, and a page that suddenly plays a loud telephone at somebody is\n// a page they close. If the browser refuses to play it at all — no interaction\n// yet, which is its rule and not ours — the card rings silently rather than not\n// appearing.\n\n/**\n * A quiet double-pulse, once every three seconds.\n *\n * Two notes rather than one so it reads as a telephone rather than as an alert,\n * and a fifth apart because that interval is the one people hear as \"ringing\"\n * across every phone system anybody has used.\n */\nconst RING = {\n cycleMs: 3000,\n peak: 0.045,\n bursts: [\n { at: 0, seconds: 0.4, hz: [440, 660] },\n { at: 0.55, seconds: 0.4, hz: [440, 660] },\n ],\n};\n\n/**\n * The same question asked of the browser rather than of ourselves.\n *\n * Chrome and Edge implement this and are the browsers that enforce the policy\n * in the first place, so where it matters it answers. It is the recovery for a\n * host page that swallowed every gesture before we saw it — better to make a\n * quiet noise the person expects than to stay silent because of somebody else's\n * event handler.\n */\nfunction activatedByBrowser() {\n try {\n return Boolean(navigator.userActivation?.hasBeenActive);\n } catch {\n return false;\n }\n}\n\n/**\n * Start ringing, and give back the way to stop.\n *\n * Returns a no-op teardown on every browser that will not do it, which is a\n * real and ordinary case: a page nobody has clicked on yet has no audio\n * permission, and the correct behaviour there is a card that appears in silence\n * rather than no card.\n */\nfunction startRinging(touched) {\n if (!touched() && !activatedByBrowser()) return () => {};\n\n const Ctx = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext);\n if (!Ctx) return () => {};\n\n let context;\n try {\n context = new Ctx();\n } catch {\n return () => {};\n }\n\n let stopped = false;\n let timer = null;\n const voices = [];\n\n const burst = ({ at, seconds, hz }) => {\n const start = context.currentTime + at;\n const gain = context.createGain();\n gain.connect(context.destination);\n // Shaped rather than switched. A square edge on a sine is a click, and a\n // click every three seconds is more irritating than the tone it introduces.\n gain.gain.setValueAtTime(0, start);\n gain.gain.linearRampToValueAtTime(RING.peak, start + 0.02);\n gain.gain.setValueAtTime(RING.peak, start + seconds - 0.06);\n gain.gain.linearRampToValueAtTime(0, start + seconds);\n\n for (const frequency of hz) {\n const osc = context.createOscillator();\n osc.type = 'sine';\n osc.frequency.value = frequency;\n osc.connect(gain);\n osc.start(start);\n osc.stop(start + seconds + 0.02);\n voices.push(osc);\n }\n };\n\n const cycle = () => {\n if (stopped) return;\n try {\n RING.bursts.forEach(burst);\n } catch { /* the card is still on screen, which is the part that matters */ }\n };\n\n const begin = () => {\n if (stopped) return;\n cycle();\n timer = setInterval(cycle, RING.cycleMs);\n };\n\n if (context.state === 'running') begin();\n else context.resume().then(begin).catch(() => {});\n\n return () => {\n stopped = true;\n if (timer) clearInterval(timer);\n // Stopped rather than left to finish. Answered, this is cut off by the\n // microphone opening a fraction of a second later, and a ring still sounding\n // over the first word of a conversation is the thing people apologise for.\n for (const osc of voices) {\n try { osc.stop(); } catch { /* already stopped on its own schedule */ }\n }\n try { context.close(); } catch { /* nothing left to close */ }\n };\n}\n\n/** Seconds left until a deadline, floored at zero. */\nfunction secondsLeft(ringsUntil) {\n const until = Date.parse(ringsUntil || '');\n if (Number.isNaN(until)) return 0;\n return Math.max(0, Math.round((until - Date.now()) / 1000));\n}\n\nconst BUTTON = [\n 'flex:1',\n 'padding:9px 12px',\n 'border:0',\n 'border-radius:8px',\n 'font:600 13px/1.4 inherit',\n 'cursor:pointer',\n].join(';');\n\n/**\n * Put the card on screen and ring until something happens to it.\n *\n * @param {object} opts\n * @param {object} opts.call The `incomingCall` object off the update stream —\n * `callId`, `from`, `project`, `reference`, `ringsUntil`, `recorded`.\n * @param {string} opts.accent The integration's own colour, for Answer.\n * @param {string} [opts.position] Same two words the toast takes.\n * @param {() => void} opts.onAnswer Pressed Answer.\n * @param {() => void} opts.onDecline Pressed Decline.\n * @param {() => void} [opts.onExpire] Nobody pressed anything in time.\n * @param {() => boolean} [opts.touched] Whether anybody has touched the page\n * yet, which is when a sound may be made (gesture.js). Asked of the caller\n * because this file is fetched when a call rings, and could not have been\n * listening for the clicks that came before.\n * @returns {(reason?: string) => void} Take it away. Called with no reason on\n * an ordinary dismissal; the caller passes one only for its own logging.\n */\nexport function showIncomingCall({\n call, accent = '#5b6ef5', position = 'bottom-right',\n onAnswer, onDecline, onExpire, touched = () => false,\n}) {\n if (typeof document === 'undefined' || !document.body) return () => {};\n\n const [vertical, horizontal] = String(position).split('-');\n const el = document.createElement('div');\n el.setAttribute('role', 'alertdialog');\n el.setAttribute('aria-live', 'assertive');\n el.setAttribute('aria-label', `${call.from || 'Support'} is calling about ${call.reference}`);\n el.style.cssText = [\n 'position:fixed',\n `${vertical === 'top' ? 'top' : 'bottom'}:20px`,\n `${horizontal === 'left' ? 'left' : 'right'}:20px`,\n // Above the update toast, which uses the maximum and may be on screen at\n // the same moment — a status line must not sit on top of a ringing phone.\n 'z-index:2147483647',\n 'width:300px',\n 'max-width:calc(100vw - 40px)',\n 'padding:14px',\n 'border-radius:12px',\n 'background:#1b1e26',\n 'color:#eceef2',\n 'font:13px/1.5 -apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif',\n 'box-shadow:0 18px 44px rgba(0,0,0,0.5)',\n `border:1px solid ${accent}`,\n 'opacity:0',\n 'transform:translateY(8px)',\n 'transition:opacity .2s ease,transform .2s ease',\n ].join(';');\n\n const heading = document.createElement('div');\n heading.style.cssText = 'font-weight:600;font-size:14px;margin-bottom:2px';\n // textContent throughout: every string on this card came from a person typing\n // it, and it is being written into somebody else's page.\n heading.textContent = `${call.from || 'Support'} is calling`;\n\n const about = document.createElement('div');\n about.style.cssText = 'color:#a4abba;margin-bottom:10px';\n about.textContent = call.project\n ? `About your report ${call.reference} · ${call.project}`\n : `About your report ${call.reference}`;\n\n const countdown = document.createElement('div');\n countdown.style.cssText = 'color:#7d8598;font-size:12px;margin-bottom:10px';\n\n const answer = document.createElement('button');\n answer.type = 'button';\n answer.textContent = 'Answer';\n answer.style.cssText = `${BUTTON};background:${accent};color:#fff`;\n\n const decline = document.createElement('button');\n decline.type = 'button';\n decline.textContent = 'Not now';\n decline.style.cssText = `${BUTTON};background:#2b2f3a;color:#c8cedb`;\n\n const actions = document.createElement('div');\n actions.style.cssText = 'display:flex;gap:8px';\n actions.append(answer, decline);\n\n el.append(heading, about, countdown);\n if (call.recorded) {\n const kept = document.createElement('div');\n kept.style.cssText = 'color:#7d8598;font-size:12px;margin-bottom:10px';\n kept.textContent = 'This call will be recorded.';\n el.append(kept);\n }\n el.append(actions);\n document.body.appendChild(el);\n\n const stopRinging = startRinging(touched);\n let closed = false;\n let ticker = null;\n\n const close = () => {\n if (closed) return;\n closed = true;\n if (ticker) clearInterval(ticker);\n stopRinging();\n el.style.opacity = '0';\n el.style.transform = 'translateY(8px)';\n setTimeout(() => el.remove(), 250);\n };\n\n const tick = () => {\n const left = secondsLeft(call.ringsUntil);\n countdown.textContent = left\n ? `Ringing — ${left}s`\n : 'The call has stopped ringing.';\n if (left) return;\n // The deadline is the server's, not this browser's arithmetic, for the\n // reason the API writes it down: two clocks disagreeing about when a phone\n // stopped ringing is how somebody gets cut off mid-answer.\n close();\n try { onExpire?.(); } catch { /* theirs */ }\n };\n tick();\n ticker = setInterval(tick, 1000);\n\n answer.addEventListener('click', () => {\n // Stopped before the handler runs, not after: opening a microphone takes a\n // moment, and a phone still ringing during it sounds like the button\n // did nothing.\n close();\n try { onAnswer?.(); } catch { /* theirs */ }\n });\n\n decline.addEventListener('click', () => {\n close();\n try { onDecline?.(); } catch { /* theirs */ }\n });\n\n requestAnimationFrame(() => {\n el.style.opacity = '1';\n el.style.transform = 'translateY(0)';\n });\n\n return close;\n}\n"],"names":["createContext","rate","Ctor","resumeContext","context","downsample","input","fromRate","toRate","ratio","out","i","start","end","sum","j","MU_BIAS","MU_CLIP","encodeMuLaw","samples","clamped","sample","sign","exponent","mask","mantissa","CAPTURE_BLOCK","tapMicrophone","stream","onFrame","wireRate","source","capture","event","frames","mute","SUBPROTOCOL","FALLBACK_ICE","openMicrophone","liveCallSupported","_a","joinLiveCall","realtimeUrl","ringsUntil","onState","onAudioBlocked","onMicrophone","emit","state","detail","sayBlocked","blocked","sayMicrophone","live","err","microphone","socket","openSocket","stopStream","peer","remoteAudio","finished","ringTimer","uplink","disarmGesture","unwatchMicrophone","starting","pending","shouldOffer","connectedOnce","reconnecting","recoverTimer","giveUpTimer","restarts","RECOVER_GRACE_MS","RECOVER_RETRY_MS","RECOVER_DEADLINE_MS","MAX_RESTARTS","finish","clearRecovery","stopUplink","send","message","startUplink","encoding","warn","tap","bytes","playRemote","attempt","markBlocked","markPlaying","armGesture","retry","events","name","build","iceServers","track","watchMicrophone","incoming","routeChanged","enterRecovery","delay","recover","restartIce","description","settle","lost","say","offer","accept","payload","answer","drainCandidates","candidate","handling","handle","_b","_c","_d","_e","armRingTimer","kind","remaining","muted","url","resolve","reject","RING","activatedByBrowser","startRinging","touched","Ctx","stopped","timer","voices","burst","at","seconds","hz","gain","frequency","osc","cycle","begin","secondsLeft","until","BUTTON","showIncomingCall","call","accent","position","onAnswer","onDecline","onExpire","vertical","horizontal","el","heading","about","countdown","decline","actions","kept","stopRinging","closed","ticker","close","tick","left"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(){"use strict";function at(e=8e3){const l=window.AudioContext||window.webkitAudioContext;try{return new l({sampleRate:e})}catch{return new l}}async function ct(e){if(e.state==="suspended")try{await e.resume()}catch{}}function it(e,l,a){const c=l/a,i=new Float32Array(Math.floor(e.length/c));for(let f=0;f<i.length;f++){const u=Math.floor(f*c),g=Math.min(Math.floor((f+1)*c),e.length);let m=0;for(let r=u;r<g;r++)m+=e[r];i[f]=g>u?m/(g-u):0}return i}const st=132,U=32635;function lt(e){const l=new Uint8Array(e.length);for(let a=0;a<e.length;a++){const c=Math.max(-1,Math.min(1,e[a]));let i=Math.round(c*32767);const f=i<0?128:0;f&&(i=-i),i>U&&(i=U),i+=st;let u=7;for(let m=16384;!(i&m)&&u>0;u--,m>>=1);const g=i>>u+3&15;l[a]=~(f|u<<4|g)&255}return l}const ut=512;function dt(e,l,a,c=8e3){const i=e.createMediaStreamSource(l),f=e.createScriptProcessor(ut,1,1);f.onaudioprocess=g=>{const m=g.inputBuffer.getChannelData(0),r=e.sampleRate===c?m:it(m,e.sampleRate,c);a(lt(r))},i.connect(f);const u=e.createGain();return u.gain.value=0,f.connect(u),u.connect(e.destination),{stop(){f.onaudioprocess=null;try{i.disconnect()}catch{}try{f.disconnect()}catch{}try{u.disconnect()}catch{}}}}const ft="voiceai.realtime.v1",pt=[{urls:"stun:stun.l.google.com:19302"}];function j(){return navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,channelCount:1}})}function z(){var e;return typeof window<"u"&&typeof window.RTCPeerConnection=="function"&&!!((e=navigator.mediaDevices)!=null&&e.getUserMedia)}async function yt({realtimeUrl:e,stream:l,ringsUntil:a,onState:c,onAudioBlocked:i,onMicrophone:f}){const u=(t,n)=>{try{c==null||c(t,n)}catch{}},g=t=>{try{i==null||i(t)}catch{}},m=t=>{try{f==null||f(t)}catch{}};if(!z()){const t=new Error("[BugCatch] this browser cannot make live calls.");throw u("failed",{reason:"unsupported"}),t}let r=l;if(!r)try{r=await j()}catch(t){throw u("failed",{reason:"microphone",error:String((t==null?void 0:t.name)||t)}),t}let y;try{y=await mt(e)}catch(t){throw V(r),u("failed",{reason:"connect"}),t}let o=null,s=null,p=!1,w=null,h=null,E=null,S=!1,b=null,C=null,_=!1;const x=[];let A=!1,$=!1,D=!1,R=null,I=null,B=0;const vt=2e3,Y=5e3,Rt=3e4,Et=4,k=(t,n)=>{if(!p){p=!0,w&&(clearTimeout(w),w=null),Z(),C==null||C(),C=null,St(),b==null||b(),b=null;try{o==null||o.close()}catch{}V(r),s&&(s.srcObject=null,s.remove(),s=null);try{y.readyState===WebSocket.OPEN&&y.close(1e3,"call over")}catch{}u(t,n)}},M=t=>{y.readyState===WebSocket.OPEN&&y.send(JSON.stringify(t))},G=async()=>{if(_||E||p||!h||!h.audio&&!h.transcript)return;_=!0;const t=h.encoding||"mulaw";if(t!=="mulaw"){F(`this call is kept as ${t}, which this SDK cannot produce.`);return}const n=Number(h.sample_rate)||8e3;try{const d=at(n);if(await ct(d),p){try{d.close()}catch{}return}const v=dt(d,r,T=>{y.readyState===WebSocket.OPEN&&y.send(T)},n);E={context:d,tap:v}}catch(d){F(`could not send the copy this call is keeping: ${String((d==null?void 0:d.message)||d)}`)}};function St(){if(_=!1,!E)return;const{context:t,tap:n}=E;E=null;try{n.stop()}catch{}try{t.close()}catch{}}function q(){var n;if(!s||p)return;let t;try{t=(n=s.play)==null?void 0:n.call(s)}catch{X();return}if(!t||typeof t.then!="function"){K();return}t.then(K,X)}function K(){b==null||b(),b=null,S&&(S=!1,g(!1))}function X(){p||(S||(S=!0,g(!0)),_t())}function _t(){if(b||typeof window>"u"||typeof window.addEventListener!="function")return;const t=()=>q(),n=["pointerdown","keydown","touchstart"];for(const d of n)window.addEventListener(d,t,{capture:!0,passive:!0});b=()=>{for(const d of n)try{window.removeEventListener(d,t,{capture:!0})}catch{}}}const Mt=t=>{o=new RTCPeerConnection({iceServers:t!=null&&t.length?t:pt});for(const n of r.getAudioTracks())o.addTrack(n,r);C=Ot(),o.onicecandidate=n=>{n.candidate&&M({type:"signal",signal:{kind:"candidate",payload:n.candidate.toJSON()}})},o.ontrack=n=>{var v;const[d]=n.streams;d&&(s||(s=document.createElement("audio"),s.autoplay=!0,s.playsInline=!0,(v=s.setAttribute)==null||v.call(s,"playsinline",""),s.style.cssText="position:fixed;width:0;height:0;opacity:0;pointer-events:none",document.body.appendChild(s)),s.srcObject=d,q())},o.onconnectionstatechange=H,o.oniceconnectionstatechange=H};function H(){if(!o||p)return;const t=o.connectionState||o.iceConnectionState;if(t==="connected"||t==="completed"){$=!0,B=0,Z(),D=!1,u("connected");return}if(t==="disconnected"){if(!$)return;Q(vt);return}if(t==="failed"){if(!$){k("failed",{reason:"ice"});return}Q(0)}}function Q(t){p||(D||(D=!0,u("reconnecting"),I=setTimeout(()=>k("failed",{reason:"network"}),Rt)),!R&&(R=setTimeout(P,t)))}function Z(){R&&(clearTimeout(R),R=null),I&&(clearTimeout(I),I=null)}async function P(){if(R=null,p||!o)return;const t=o.connectionState||o.iceConnectionState;if(!(t==="connected"||t==="completed")){if(!A){M({type:"signal",signal:{kind:"restart",payload:{}}}),R=setTimeout(P,Y);return}B>=Et||(B+=1,await tt(),R=setTimeout(P,Y))}}async function tt(){if(!(p||!o))try{const t=await o.createOffer({iceRestart:!0});await o.setLocalDescription(t),M({type:"signal",signal:{kind:"offer",payload:o.localDescription.toJSON()}})}catch{}}function Ot(){let n=null,d=!1;const v=T=>{if(n&&(clearTimeout(n),n=null),T){if(!d)return;d=!1,m(!0);return}d||(n=setTimeout(()=>{n=null,!p&&(d=!0,m(!1))},600))};for(const T of r.getAudioTracks())T.onmute=()=>v(!1),T.onunmute=()=>v(!0),T.onended=()=>{n&&(clearTimeout(n),n=null),!d&&(d=!0,m(!1))};return()=>{n&&(clearTimeout(n),n=null);for(const T of r.getAudioTracks())T.onmute=null,T.onunmute=null,T.onended=null}}const At=async()=>{try{const t=await o.createOffer();await o.setLocalDescription(t),M({type:"signal",signal:{kind:"offer",payload:o.localDescription.toJSON()}})}catch{k("failed",{reason:"negotiation"})}},It=async t=>{try{await o.setRemoteDescription(new RTCSessionDescription(t));const n=await o.createAnswer();await o.setLocalDescription(n),M({type:"signal",signal:{kind:"answer",payload:o.localDescription.toJSON()}}),await et()}catch{k("failed",{reason:"negotiation"})}},et=async()=>{for(;x.length;){const t=x.shift();try{await o.addIceCandidate(new RTCIceCandidate(t))}catch{}}};let nt=Promise.resolve();y.onmessage=t=>{nt=nt.then(()=>Lt(t)).catch(()=>{})};const Lt=async t=>{var d,v,T,ot,rt;if(typeof t.data!="string")return;let n;try{n=JSON.parse(t.data)}catch{return}switch(n.type){case"session.created":{if(Mt((d=n.data)==null?void 0:d.ice_servers),h=((v=n.data)==null?void 0:v.capture)||null,(T=n.data)!=null&&T.peer_present){u("connecting"),G();break}u("ringing"),Nt();break}case"peer.joined":{w&&(clearTimeout(w),w=null),u("connecting"),A=!!((ot=n.data)!=null&&ot.should_offer),A&&await At(),G();break}case"signal":{if(!o)break;const{kind:L,payload:N}=n.data||{};if(L==="offer")await It(N);else if(L==="answer")try{await o.setRemoteDescription(new RTCSessionDescription(N)),await et()}catch{k("failed",{reason:"negotiation"})}else if(L==="candidate")if(o.remoteDescription)try{await o.addIceCandidate(new RTCIceCandidate(N))}catch{}else x.push(N);else L==="restart"&&A&&await tt();break}case"peer.left":k("ended",{reason:"peer_left"});break;case"error":k("failed",{reason:"server",detail:(rt=n.error)==null?void 0:rt.detail});break;case"session.ended":k("ended",{reason:"server"});break}};y.onclose=()=>k("ended",{reason:"disconnected"}),y.onerror=()=>k("failed",{reason:"socket"});function Nt(){if(!a)return;const t=new Date(a).getTime()-Date.now();Number.isFinite(t)&&(w=setTimeout(()=>k("missed"),Math.max(0,t)))}return{hangUp(){M({type:"session.end"}),k("ended",{reason:"hung_up"})},setMuted(t){for(const n of r.getAudioTracks())n.enabled=!t;return t},isMuted(){return r.getAudioTracks().every(t=>!t.enabled)}}}function mt(e){return new Promise((l,a)=>{let c;try{c=new WebSocket(e,ft)}catch(i){a(i);return}c.onopen=()=>{c.onopen=null,c.onerror=null,l(c)},c.onerror=()=>{c.onopen=null,c.onerror=null,a(new Error("[BugCatch] could not connect the call."))}})}function F(e){try{console.warn(`[BugCatch] ${e}`)}catch{}}function V(e){var l;for(const a of((l=e==null?void 0:e.getTracks)==null?void 0:l.call(e))??[])try{a.stop()}catch{}}const O={cycleMs:3e3,peak:.045,bursts:[{at:0,seconds:.4,hz:[440,660]},{at:.55,seconds:.4,hz:[440,660]}]};function ht(){var e;try{return!!((e=navigator.userActivation)!=null&&e.hasBeenActive)}catch{return!1}}function gt(e){if(!e()&&!ht())return()=>{};const l=typeof window<"u"&&(window.AudioContext||window.webkitAudioContext);if(!l)return()=>{};let a;try{a=new l}catch{return()=>{}}let c=!1,i=null;const f=[],u=({at:r,seconds:y,hz:o})=>{const s=a.currentTime+r,p=a.createGain();p.connect(a.destination),p.gain.setValueAtTime(0,s),p.gain.linearRampToValueAtTime(O.peak,s+.02),p.gain.setValueAtTime(O.peak,s+y-.06),p.gain.linearRampToValueAtTime(0,s+y);for(const w of o){const h=a.createOscillator();h.type="sine",h.frequency.value=w,h.connect(p),h.start(s),h.stop(s+y+.02),f.push(h)}},g=()=>{if(!c)try{O.bursts.forEach(u)}catch{}},m=()=>{c||(g(),i=setInterval(g,O.cycleMs))};return a.state==="running"?m():a.resume().then(m).catch(()=>{}),()=>{c=!0,i&&clearInterval(i);for(const r of f)try{r.stop()}catch{}try{a.close()}catch{}}}function wt(e){const l=Date.parse(e||"");return Number.isNaN(l)?0:Math.max(0,Math.round((l-Date.now())/1e3))}const W=["flex:1","padding:9px 12px","border:0","border-radius:8px","font:600 13px/1.4 inherit","cursor:pointer"].join(";");function bt({call:e,accent:l="#5b6ef5",position:a="bottom-right",onAnswer:c,onDecline:i,onExpire:f,touched:u=()=>!1}){if(typeof document>"u"||!document.body)return()=>{};const[g,m]=String(a).split("-"),r=document.createElement("div");r.setAttribute("role","alertdialog"),r.setAttribute("aria-live","assertive"),r.setAttribute("aria-label",`${e.from||"Support"} is calling about ${e.reference}`),r.style.cssText=["position:fixed",`${g==="top"?"top":"bottom"}:20px`,`${m==="left"?"left":"right"}:20px`,"z-index:2147483647","width:300px","max-width:calc(100vw - 40px)","padding:14px","border-radius:12px","background:#1b1e26","color:#eceef2",'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',"box-shadow:0 18px 44px rgba(0,0,0,0.5)",`border:1px solid ${l}`,"opacity:0","transform:translateY(8px)","transition:opacity .2s ease,transform .2s ease"].join(";");const y=document.createElement("div");y.style.cssText="font-weight:600;font-size:14px;margin-bottom:2px",y.textContent=`${e.from||"Support"} is calling`;const o=document.createElement("div");o.style.cssText="color:#a4abba;margin-bottom:10px",o.textContent=e.project?`About your report ${e.reference} · ${e.project}`:`About your report ${e.reference}`;const s=document.createElement("div");s.style.cssText="color:#7d8598;font-size:12px;margin-bottom:10px";const p=document.createElement("button");p.type="button",p.textContent="Answer",p.style.cssText=`${W};background:${l};color:#fff`;const w=document.createElement("button");w.type="button",w.textContent="Not now",w.style.cssText=`${W};background:#2b2f3a;color:#c8cedb`;const h=document.createElement("div");if(h.style.cssText="display:flex;gap:8px",h.append(p,w),r.append(y,o,s),e.recorded){const x=document.createElement("div");x.style.cssText="color:#7d8598;font-size:12px;margin-bottom:10px",x.textContent="This call will be recorded.",r.append(x)}r.append(h),document.body.appendChild(r);const E=gt(u);let S=!1,b=null;const C=()=>{S||(S=!0,b&&clearInterval(b),E(),r.style.opacity="0",r.style.transform="translateY(8px)",setTimeout(()=>r.remove(),250))},_=()=>{const x=wt(e.ringsUntil);if(s.textContent=x?`Ringing — ${x}s`:"The call has stopped ringing.",!x){C();try{f==null||f()}catch{}}};return _(),b=setInterval(_,1e3),p.addEventListener("click",()=>{C();try{c==null||c()}catch{}}),w.addEventListener("click",()=>{C();try{i==null||i()}catch{}}),requestAnimationFrame(()=>{r.style.opacity="1",r.style.transform="translateY(0)"}),C}const Tt=Object.freeze(Object.defineProperty({__proto__:null,joinLiveCall:yt,liveCallSupported:z,openMicrophone:j,showIncomingCall:bt},Symbol.toStringTag,{value:"Module"})),kt="0.18.0",J="__bugcatchChunks",xt=e=>`${e}@${kt}`;function Ct(e,l){const a=window[J]||(window[J]={});a[xt(e)]=l}Ct("call",Tt)})();
|
|
2
|
+
//# sourceMappingURL=bugcatch-call.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bugcatch-call.umd.js","sources":["../src/audio.js","../src/live-call.js","../src/incoming-call.js","../src/chunks.js","../src/umd/call.js"],"sourcesContent":["// Microphone to µ-law, and back.\n//\n// sphoro.voice speaks G.711 µ-law at 8 kHz in both directions — the format a\n// telephone carrier hands it, which is what makes a browser call and a phone\n// call indistinguishable to everything downstream. A browser has neither: it\n// captures 32-bit floats at whatever rate the sound card runs at. The gap\n// between those two is a codec and a resampler, and neither is available from\n// the platform, so both are here.\n//\n// This file exists rather than the two callers each having a copy. The agent\n// call sends µ-law because that is the whole conversation; a live call sends it\n// because sphoro.voice cannot record or transcribe a conversation it is not in\n// the middle of, and asks each browser for a copy of its own microphone. Two\n// implementations of G.711 in one package would be one implementation and one\n// that is subtly wrong on the day somebody fixes only the first.\n//\n// Nothing is imported to do it. The SDK runs on someone else's page and has no\n// dependencies, and that rule does not bend for audio.\n\n/** The wire's sample rate. Not negotiable — see the note above. */\nexport const WIRE_RATE = 8000;\n\n/**\n * An audio context, at the wire's rate where the browser will give us one.\n *\n * Asking for 8 kHz outright makes the resampler below dead code on most\n * machines. Some browsers refuse a rate their hardware does not like, so the\n * fallback is an ordinary context and the manual path.\n */\nexport function createContext(rate = WIRE_RATE) {\n const Ctor = window.AudioContext || window.webkitAudioContext;\n try {\n return new Ctor({ sampleRate: rate });\n } catch {\n return new Ctor();\n }\n}\n\n/**\n * Start a context that the browser has parked, if it will let us.\n *\n * Resolves either way. There is nothing useful to do with the rejection: it\n * means this browser wants a user gesture we are not inside, and the call is\n * still worth attempting — a context that stays suspended is a silent call,\n * which is what the caller finds out about from the states it is already being\n * sent.\n */\nexport async function resumeContext(context) {\n if (context.state !== 'suspended') return;\n try {\n await context.resume();\n } catch { /* no activation to spend */ }\n}\n\n/**\n * Drop a signal to the wire rate by averaging each source window.\n *\n * Exported for its own test: the browsers that refuse an 8 kHz context are the\n * ones that need this, and they are exactly the browsers a developer is least\n * likely to be looking at when they change it.\n *\n * Averaging rather than picking every nth sample: plain decimation aliases\n * everything above 4 kHz down into the speech band, and consonants are exactly\n * what lives up there. This is a crude low-pass, but it is the difference\n * between \"sixty\" and \"sixteen\" surviving the trip.\n */\nexport function downsample(input, fromRate, toRate) {\n const ratio = fromRate / toRate;\n const out = new Float32Array(Math.floor(input.length / ratio));\n for (let i = 0; i < out.length; i++) {\n const start = Math.floor(i * ratio);\n const end = Math.min(Math.floor((i + 1) * ratio), input.length);\n let sum = 0;\n for (let j = start; j < end; j++) sum += input[j];\n out[i] = end > start ? sum / (end - start) : 0;\n }\n return out;\n}\n\n/** G.711 µ-law: the bias and clip the standard specifies. */\nconst MU_BIAS = 0x84;\nconst MU_CLIP = 32635;\n\n/**\n * Float samples to µ-law bytes, ready to put on the wire.\n *\n * Exported for the codec test rather than for callers: this file is not the\n * package's entry point, and a hand-rolled G.711 that nothing checks is a\n * silent call nobody can debug from the outside.\n */\nexport function encodeMuLaw(samples) {\n const out = new Uint8Array(samples.length);\n for (let i = 0; i < samples.length; i++) {\n // Clamped before scaling: a float outside [-1, 1] is legal in Web Audio and\n // would wrap round to the opposite sign as an integer.\n const clamped = Math.max(-1, Math.min(1, samples[i]));\n let sample = Math.round(clamped * 32767);\n\n const sign = sample < 0 ? 0x80 : 0;\n if (sign) sample = -sample;\n if (sample > MU_CLIP) sample = MU_CLIP;\n sample += MU_BIAS;\n\n let exponent = 7;\n for (let mask = 0x4000; (sample & mask) === 0 && exponent > 0; exponent--, mask >>= 1);\n const mantissa = (sample >> (exponent + 3)) & 0x0F;\n out[i] = ~(sign | (exponent << 4) | mantissa) & 0xFF;\n }\n return out;\n}\n\n/** µ-law bytes back to float samples, ready for an AudioBuffer. */\nexport function decodeMuLaw(bytes) {\n const out = new Float32Array(bytes.length);\n for (let i = 0; i < bytes.length; i++) {\n const byte = ~bytes[i] & 0xFF;\n const sign = byte & 0x80;\n const exponent = (byte >> 4) & 0x07;\n const mantissa = byte & 0x0F;\n const magnitude = (((mantissa << 3) + MU_BIAS) << exponent) - MU_BIAS;\n out[i] = (sign ? -magnitude : magnitude) / 32768;\n }\n return out;\n}\n\n/**\n * How much audio rides in one frame.\n *\n * Measured in samples, and the context above runs at 8 kHz wherever the browser\n * allows it — so this is 64 ms of speech, not the 20-odd it would be at\n * hardware rate. That distinction is the whole reason for the number: at 2048\n * the same code sends a quarter of a second per frame, which is a quarter of a\n * second added to the start of every turn and to every barge-in, and it sounds\n * exactly like an agent that is slow to notice you talking.\n *\n * Not smaller than this. ScriptProcessorNode runs on the main thread, and a\n * block it cannot fill in time is a block it drops — which is a gap in the\n * middle of a word rather than a delay before one. Powers of two only.\n */\nexport const CAPTURE_BLOCK = 512;\n\n/**\n * Tap a microphone and hand each block over as µ-law bytes.\n *\n * ScriptProcessorNode is deprecated in favour of AudioWorklet, and is used\n * anyway. A worklet's processor has to be loaded from a URL, which on this SDK\n * means a blob: URL — and a blob: worklet is refused outright by the\n * Content-Security-Policy of a great many of the sites this code runs on. A\n * deprecated node that works on a customer's page beats a modern one that is\n * blocked by their headers.\n *\n * Returns a `stop` that lets go of the whole chain. The microphone track is\n * deliberately *not* stopped by it: on a live call the same track is carrying\n * the conversation, and stopping it to end a recording would end the call.\n *\n * @param {AudioContext} context\n * @param {MediaStream} stream\n * @param {(bytes: Uint8Array) => void} onFrame\n * @param {number} [wireRate]\n * @returns {{stop: () => void}}\n */\nexport function tapMicrophone(context, stream, onFrame, wireRate = WIRE_RATE) {\n const source = context.createMediaStreamSource(stream);\n const capture = context.createScriptProcessor(CAPTURE_BLOCK, 1, 1);\n\n capture.onaudioprocess = (event) => {\n const input = event.inputBuffer.getChannelData(0);\n const frames = context.sampleRate === wireRate\n ? input\n : downsample(input, context.sampleRate, wireRate);\n onFrame(encodeMuLaw(frames));\n };\n\n source.connect(capture);\n // A ScriptProcessorNode does not run unless it is connected to a destination,\n // even when nothing is meant to come out of it. Zero gain is what keeps the\n // speaker from hearing their own voice played back at them.\n const mute = context.createGain();\n mute.gain.value = 0;\n capture.connect(mute);\n mute.connect(context.destination);\n\n return {\n stop() {\n capture.onaudioprocess = null;\n try { source.disconnect(); } catch { /* already gone */ }\n try { capture.disconnect(); } catch { /* already gone */ }\n try { mute.disconnect(); } catch { /* already gone */ }\n },\n };\n}\n","// Two people, talking directly.\n//\n// The agent call next door is a microphone, a socket and a codec: the browser\n// sends µ-law up to sphoro.voice, a model answers, and every sample passes\n// through a server. This is the opposite arrangement. The audio here goes\n// straight from one browser to the other over WebRTC, and sphoro.voice's part is\n// only to introduce them — it passes an offer, an answer and a handful of ICE\n// candidates between the two sockets and then has nothing left to do.\n//\n// Which means there is no codec in this file, no resampler, and no audio path\n// at all. The browser's own WebRTC stack owns those, and it is better at them\n// than anything hand-written: Opus at whatever rate the two ends agree on,\n// echo cancellation, jitter buffering and packet loss concealment, none of\n// which the µ-law path can have because a telephone carrier cannot receive\n// them. A live call sounds better than the agent call, and costs nothing per\n// minute, because nobody is in the middle of it.\n//\n// # Shared with the dashboard\n//\n// Both ends of a live call run this file: the reporter's widget and the member\n// who answers in the portal, which imports it from here rather than keeping a\n// second copy. The two sides differ by one flag — who makes the offer — and it\n// arrives from the server rather than being decided here, so the two ends\n// cannot both sit waiting for the other to go first.\n//\n// # The one exception: the copy that gets kept\n//\n// An agent that is set to record or transcribe cannot do either from here —\n// the audio it would need never reaches it. So sphoro.voice asks instead: it\n// names, in `session.created`, what this call is keeping and in what format,\n// and each browser uplinks a copy of *its own* microphone over the signaling\n// socket it already has open. That copy is the only way a recording or a\n// transcript of a peer-to-peer call can exist at all, and it is why the codec\n// this file said it did not have is imported below.\n//\n// It is a copy and not the call. The conversation still runs browser to\n// browser; an uplink that stalls or is refused costs a recording, never a call.\n//\n// Nothing outside the package is imported to do any of it. The SDK runs on\n// someone else's page and has no dependencies, and that rule does not bend for\n// a peer connection.\n\nimport { createContext, resumeContext, tapMicrophone } from './audio.js';\n\n/**\n * sphoro.voice's realtime protocol, sent as the WebSocket subprotocol. The same\n * one the agent call speaks — a bridged call is a different conversation over\n * the same envelope, not a different protocol.\n */\nconst SUBPROTOCOL = 'voiceai.realtime.v1';\n\n/**\n * Where a browser looks to find its way to the other one, when the server names\n * nowhere.\n *\n * The server almost always does name somewhere — it is configured per\n * deployment, because a network behind symmetric NAT needs a TURN relay and\n * that costs money. This is the floor: a public STUN server, which is enough\n * for most home and office networks and free.\n */\nconst FALLBACK_ICE = [{ urls: 'stun:stun.l.google.com:19302' }];\n\n/**\n * Ask for the microphone.\n *\n * Separate from joining, and deliberately. The permission prompt takes as long\n * as the person takes to read it, and both ends have something they should not\n * do until it is answered: the reporter should not make a room full of\n * dashboards ring before knowing they can speak, and whoever answers should not\n * claim a call they then cannot take. So both ask first and act second.\n *\n * @returns {Promise<MediaStream>}\n */\nexport function openMicrophone() {\n return navigator.mediaDevices.getUserMedia({\n audio: {\n // Both ends are on speakers as often as not. Without cancellation each\n // hears themselves back through the other, half a second late, which is\n // the single most effective way to make a conversation impossible.\n echoCancellation: true,\n noiseSuppression: true,\n autoGainControl: true,\n channelCount: 1,\n },\n });\n}\n\n/** Whether this browser can be on a live call at all. */\nexport function liveCallSupported() {\n return typeof window !== 'undefined'\n && typeof window.RTCPeerConnection === 'function'\n && Boolean(navigator.mediaDevices?.getUserMedia);\n}\n\n/**\n * Join a live call and stay on it until somebody hangs up.\n *\n * @param {object} opts\n * @param {string} opts.realtimeUrl From the API, carrying its own credential\n * and its own seat. Never log it and never build one by hand.\n * @param {MediaStream} [opts.stream] A microphone already opened by\n * `openMicrophone`. One is opened here if not, which is the wrong order for\n * anything that had a decision to make first.\n * @param {string} [opts.ringsUntil] ISO-8601, on the caller's side only: when\n * the ringing stops. The browser is the thing that decides it has, because it\n * is the one client guaranteed to still be listening at that moment.\n * @param {(state: string, detail?: object) => void} [opts.onState]\n * 'ringing' | 'connecting' | 'connected' | 'reconnecting' | 'ended' | 'missed'\n * | 'failed'. 'reconnecting' is not a failure and never ends a call — see\n * `routeChanged`.\n * @param {(blocked: boolean) => void} [opts.onAudioBlocked] The browser refused\n * to play the other person, or has since started. Its own callback rather\n * than a state, because it is orthogonal to every one of them: the call is\n * connected and working, and one end cannot hear it. See `playRemote`.\n * @param {(live: boolean) => void} [opts.onMicrophone] Whether this person's\n * microphone is still reaching the call. Also orthogonal, and for the same\n * reason: the call is up, the other end is audible, and nothing this person\n * says is going anywhere. See `watchMicrophone`.\n * @returns {Promise<{hangUp: () => void, setMuted: (muted: boolean) => boolean,\n * isMuted: () => boolean}>}\n */\nexport async function joinLiveCall({\n realtimeUrl, stream, ringsUntil, onState, onAudioBlocked, onMicrophone,\n}) {\n const emit = (state, detail) => { try { onState?.(state, detail); } catch { /* theirs */ } };\n const sayBlocked = (blocked) => {\n try { onAudioBlocked?.(blocked); } catch { /* theirs */ }\n };\n const sayMicrophone = (live) => {\n try { onMicrophone?.(live); } catch { /* theirs */ }\n };\n\n if (!liveCallSupported()) {\n const err = new Error('[BugCatch] this browser cannot make live calls.');\n emit('failed', { reason: 'unsupported' });\n throw err;\n }\n\n let microphone = stream;\n if (!microphone) {\n try {\n microphone = await openMicrophone();\n } catch (err) {\n emit('failed', { reason: 'microphone', error: String(err?.name || err) });\n throw err;\n }\n }\n\n let socket;\n try {\n socket = await openSocket(realtimeUrl);\n } catch (err) {\n stopStream(microphone);\n emit('failed', { reason: 'connect' });\n throw err;\n }\n\n let peer = null;\n let remoteAudio = null;\n let finished = false;\n let ringTimer = null;\n // What sphoro.voice said this call is keeping, and the tap feeding it. Both\n // stay null on the ordinary call, which keeps nothing and uplinks nothing.\n let capture = null;\n let uplink = null;\n // Whether the browser is currently refusing to play the other person, and the\n // teardown for the gesture listeners armed to try again. See playRemote.\n let blocked = false;\n let disarmGesture = null;\n /** Teardown for the microphone watch, armed once the peer connection is built. */\n let unwatchMicrophone = null;\n // Claimed synchronously, before the first await below, because the seat that\n // answers is told the call has started twice — once in its own\n // `session.created`, which says somebody is already waiting, and again in the\n // `peer.joined` that goes to both ends a moment later. Two starts that both\n // check `uplink` before either has set it are two taps on one microphone, and\n // two copies of every frame interleaved into one recording.\n let starting = false;\n // Candidates that arrived before there was anywhere to put them.\n //\n // The offer and the first candidates are sent within a millisecond of each\n // other and arrive in that order, but `setRemoteDescription` is a promise —\n // so a candidate can be in hand while the description it belongs to is still\n // being applied. Adding one then throws, and the call comes up with a worse\n // route than it could have had, or none.\n const pending = [];\n\n // --- surviving a change of network ------------------------------------------------\n //\n // A phone call moves. Somebody walks out of the office and the handset hands the\n // connection from Wi-Fi to cellular; a lift takes the signal for four seconds; a laptop\n // wakes on a different access point. Every one of those changes the local address the\n // call was negotiated against, and WebRTC's answer to all of them is the same: the ICE\n // agent reports `disconnected`, and unless somebody restarts it, `failed` a moment later.\n //\n // Before this, `failed` ended the call and `disconnected` was not handled at all — so a\n // call that would have recovered on its own in two seconds was hung up on, and one that\n // needed a restart was hung up on with no attempt made. On a desk that is rare. On the\n // phone this app also ships as, it is what happens when you stand up.\n //\n // What replaces it is a grace period, then an ICE restart, then a deadline. Each is\n // there for a distinct reason:\n //\n // the grace `disconnected` is frequently transient — a couple of lost packets on a\n // route that is still fine. Restarting immediately would tear down a\n // working connection to rebuild the same one, and take audio with it.\n //\n // the restart Renegotiation from scratch over the signaling socket, which is still\n // open: it never depended on the media route. This is the part that\n // actually crosses a network change.\n //\n // the deadline Reconnecting forever is its own failure. Somebody staring at a card\n // that says \"reconnecting\" for five minutes has been abandoned more\n // thoroughly than somebody told the call dropped.\n //\n // Only one side offers, and it is the side the server nominated at the start. Both ends\n // see the drop, so if the two sides both restarted there would be two offers crossing —\n // a glare condition WebRTC resolves badly. The other side asks instead, over the\n // signaling socket, and its request is best-effort: the offerer's own detection is what\n // this actually rests on.\n\n /** Which side was told to make the offer. Set from `peer.joined`, for both ends. */\n let shouldOffer = false;\n /** Whether the two ends have ever been connected. A drop before that is a failed setup. */\n let connectedOnce = false;\n let reconnecting = false;\n let recoverTimer = null;\n let giveUpTimer = null;\n let restarts = 0;\n\n /** How long `disconnected` is allowed to resolve itself before a restart is attempted. */\n const RECOVER_GRACE_MS = 2000;\n /** How long between one restart and the next, when the first did not take. */\n const RECOVER_RETRY_MS = 5000;\n /** How long a call may spend off the air in total before it is called dropped. */\n const RECOVER_DEADLINE_MS = 30000;\n const MAX_RESTARTS = 4;\n\n /** Everything ends here, whatever ended it, exactly once. */\n const finish = (state, detail) => {\n if (finished) return;\n finished = true;\n if (ringTimer) { clearTimeout(ringTimer); ringTimer = null; }\n // Before the peer is closed, and before the stream is stopped. Closing a connection\n // moves it to `closed` and stopping a track fires `ended`, and both would otherwise\n // reach handlers that would report a dropped call and a dead microphone to somebody\n // who has just hung up.\n clearRecovery();\n unwatchMicrophone?.();\n unwatchMicrophone = null;\n stopUplink();\n // Before anything else is torn down: these are listeners on `window`, and a\n // call that left them behind would keep retrying playback on an element\n // that is about to be removed — once per click, for the rest of the session.\n disarmGesture?.();\n disarmGesture = null;\n try { peer?.close(); } catch { /* already gone */ }\n stopStream(microphone);\n if (remoteAudio) {\n remoteAudio.srcObject = null;\n remoteAudio.remove();\n remoteAudio = null;\n }\n try {\n if (socket.readyState === WebSocket.OPEN) socket.close(1000, 'call over');\n } catch { /* already closing */ }\n emit(state, detail);\n };\n\n const send = (message) => {\n if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));\n };\n\n /**\n * Start sending sphoro.voice a copy of this person's microphone.\n *\n * Only when it asked, and only once both people are on the call. The server\n * throws away anything that arrives while the phone is still ringing — a\n * recording is timed from the moment the second person joined, and a minute\n * of somebody waiting alone at the head of the file is a timeline that\n * disagrees with the call's own duration. Sending it anyway would just be\n * bandwidth spent on audio that gets dropped, on the same network carrying\n * the call.\n *\n * Every failure here is survivable and none of them ends the call. A browser\n * with no Web Audio, a context the autoplay policy will not start, a format\n * this version does not know how to produce — each costs the recording and\n * leaves two people talking, which is the right way round. It is said out\n * loud, though: an agent set to record whose calls silently arrive with no\n * audio is the failure that is impossible to diagnose from the outside.\n */\n const startUplink = async () => {\n if (starting || uplink || finished || !capture) return;\n if (!capture.audio && !capture.transcript) return;\n starting = true;\n\n // The server names the format rather than assuming one, so a client that\n // does not recognise it sends nothing rather than sending µ-law into a\n // recording that expected something else — which would store a file of\n // noise and read as a working recording to everything downstream.\n const encoding = capture.encoding || 'mulaw';\n if (encoding !== 'mulaw') {\n warn(`this call is kept as ${encoding}, which this SDK cannot produce.`);\n return;\n }\n\n\n const rate = Number(capture.sample_rate) || 8000;\n try {\n const context = createContext(rate);\n // A context built without a user gesture starts suspended, and a\n // suspended context's ScriptProcessorNode never fires — so nothing would\n // be uplinked and nothing would say why. Both ends granted microphone\n // access a moment ago, which is the activation this spends.\n await resumeContext(context);\n if (finished) {\n try { context.close(); } catch { /* already closed */ }\n return;\n }\n\n const tap = tapMicrophone(context, microphone, (bytes) => {\n // Binary, not the base64 envelope the protocol also accepts: this is a\n // second copy of audio going up a network that is already carrying the\n // call, and base64 would make it a third larger for nothing.\n if (socket.readyState === WebSocket.OPEN) socket.send(bytes);\n }, rate);\n\n uplink = { context, tap };\n } catch (err) {\n warn(`could not send the copy this call is keeping: ${String(err?.message || err)}`);\n }\n };\n\n /**\n * Stop sending it.\n *\n * The microphone track is deliberately left alone — `finish` stops the whole\n * stream a moment later, and stopping it here would cut the conversation off\n * to end a recording.\n */\n function stopUplink() {\n starting = false;\n if (!uplink) return;\n const { context, tap } = uplink;\n uplink = null;\n try { tap.stop(); } catch { /* already gone */ }\n try { context.close(); } catch { /* already closed */ }\n }\n\n /**\n * Play the other person, and do something about it when the browser says no.\n *\n * The refusal used to be swallowed: `play().catch(() => {})`, on the reasoning\n * that both ends had granted microphone access a moment earlier and that\n * satisfies every autoplay policy. It does not always. A call answered from a\n * notification arrives with no user gesture on the page at all — the tap\n * happened on a system notification, which is not this document — and inside\n * an Android WebView that is exactly the case that fails.\n *\n * What that produced is the worst shape a bug can have: the call connects,\n * every state says `connected`, both ends are billed for it, and one person\n * hears nothing. Nothing is logged, because the rejection was caught and\n * dropped, so there is no evidence anywhere that it happened.\n *\n * So now it is reported and it is retried. The retry is armed on the next\n * gesture anywhere on the page, which is the one thing that reliably lifts an\n * autoplay block, and it is armed in capture so a handler calling\n * `stopPropagation` cannot hide the gesture from it. Whoever is on the call is\n * about to touch something — the mute button, the hang-up button, the report\n * they are talking about — and the sound comes up when they do, with the\n * caller free to say so on screen in the meantime.\n */\n function playRemote() {\n if (!remoteAudio || finished) return;\n let attempt;\n try {\n attempt = remoteAudio.play?.();\n } catch {\n // A synchronous throw, which some engines do instead of rejecting.\n markBlocked();\n return;\n }\n if (!attempt || typeof attempt.then !== 'function') {\n // No promise: an older engine, or a stub. Nothing to wait on and nothing\n // to report — it either made a sound or it did not.\n markPlaying();\n return;\n }\n attempt.then(markPlaying, markBlocked);\n }\n\n function markPlaying() {\n disarmGesture?.();\n disarmGesture = null;\n if (!blocked) return;\n blocked = false;\n sayBlocked(false);\n }\n\n function markBlocked() {\n if (finished) return;\n if (!blocked) {\n blocked = true;\n sayBlocked(true);\n }\n armGesture();\n }\n\n /** Try again on the next thing the person touches. Armed once. */\n function armGesture() {\n if (disarmGesture) return;\n // Guarded rather than assumed: this file is also loaded by a host page's\n // bundler into environments that define a partial `window`, and a retry\n // that cannot be armed is a quieter failure than one that throws inside a\n // promise nobody is awaiting.\n if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return;\n const retry = () => playRemote();\n const events = ['pointerdown', 'keydown', 'touchstart'];\n for (const name of events) {\n window.addEventListener(name, retry, { capture: true, passive: true });\n }\n disarmGesture = () => {\n for (const name of events) {\n try { window.removeEventListener(name, retry, { capture: true }); } catch { /* gone */ }\n }\n };\n }\n\n /**\n * Build the peer connection.\n *\n * Made on `session.created` rather than lazily on the first signal, because\n * the ICE gathering it starts is the slowest part of connecting and the whole\n * of it can happen while the phone is still ringing. Waiting until somebody\n * answers would add that delay to the exact moment a person says hello, which\n * is the one moment on this call where it is audible.\n */\n const build = (iceServers) => {\n peer = new RTCPeerConnection({ iceServers: iceServers?.length ? iceServers : FALLBACK_ICE });\n\n for (const track of microphone.getAudioTracks()) peer.addTrack(track, microphone);\n\n // Armed here rather than at the top, so a call that never got a peer connection has\n // nothing to tear down — and so the watch covers exactly the span in which being\n // unheard is a problem worth telling somebody about.\n unwatchMicrophone = watchMicrophone();\n\n peer.onicecandidate = (event) => {\n if (!event.candidate) return;\n send({ type: 'signal', signal: { kind: 'candidate', payload: event.candidate.toJSON() } });\n };\n\n peer.ontrack = (event) => {\n const [incoming] = event.streams;\n if (!incoming) return;\n // An element rather than the Web Audio API. Audio routed through a\n // MediaStreamAudioSourceNode loses the browser's own echo cancellation on\n // several engines, and this call has a live microphone next to it.\n if (!remoteAudio) {\n remoteAudio = document.createElement('audio');\n remoteAudio.autoplay = true;\n // Inline, for a WebView that would otherwise hand a media element to a\n // full-screen player — which on a call is a black rectangle over the\n // dashboard.\n remoteAudio.playsInline = true;\n remoteAudio.setAttribute?.('playsinline', '');\n // Off-screen rather than display:none — a hidden media element is\n // treated as invisible by some autoplay heuristics and refused.\n remoteAudio.style.cssText = 'position:fixed;width:0;height:0;opacity:0;pointer-events:none';\n document.body.appendChild(remoteAudio);\n }\n remoteAudio.srcObject = incoming;\n playRemote();\n };\n\n peer.onconnectionstatechange = routeChanged;\n // The same signal, read the other way, for the engines that do not give a reliable\n // `connectionState`. WebKit — which is every browser on iOS and the WebView this app\n // ships inside there — has historically fired `iceconnectionstatechange` and not the\n // aggregate one. Both are wired to the same handler, which reads whichever of the two\n // the engine actually maintains, so a duplicate event costs one comparison.\n peer.oniceconnectionstatechange = routeChanged;\n };\n\n /**\n * What the media route is doing, and what to do about it.\n *\n * Reads whichever state the engine keeps up to date, and treats `completed` — which only\n * the ICE-level state has — as connected, because it is: it means a route was found and\n * checking has stopped.\n */\n function routeChanged() {\n if (!peer || finished) return;\n const state = peer.connectionState || peer.iceConnectionState;\n\n if (state === 'connected' || state === 'completed') {\n connectedOnce = true;\n restarts = 0;\n clearRecovery();\n // Emitted whether or not this is a reconnection. The caller renders one state, and a\n // card that came back from \"reconnecting\" has to be told the call is up again.\n reconnecting = false;\n emit('connected');\n return;\n }\n\n if (state === 'disconnected') {\n // Before the two ends ever met, a `disconnected` is ICE still working — it goes\n // there between checking one candidate pair and the next. Only `failed` is final at\n // that stage, and it is handled below.\n if (!connectedOnce) return;\n enterRecovery(RECOVER_GRACE_MS);\n return;\n }\n\n if (state === 'failed') {\n if (!connectedOnce) {\n // The two browsers never found a route to each other at all. Almost always a\n // network that needs a TURN relay the deployment has not configured; reported as\n // its own reason so that is diagnosable from a support ticket rather than from a\n // packet capture. A restart would try the same candidates again.\n finish('failed', { reason: 'ice' });\n return;\n }\n // A route that worked and then stopped. That is a network change, and a restart is\n // exactly the remedy — no grace, because `failed` is not going to resolve itself.\n enterRecovery(0);\n }\n }\n\n /** Say the call is off the air, and line up an attempt to bring it back. */\n function enterRecovery(delay) {\n if (finished) return;\n if (!reconnecting) {\n reconnecting = true;\n emit('reconnecting');\n giveUpTimer = setTimeout(() => finish('failed', { reason: 'network' }), RECOVER_DEADLINE_MS);\n }\n // One attempt in flight at a time. `disconnected` and `failed` both arrive on the way\n // down, and two timers would mean two offers crossing on one connection.\n if (recoverTimer) return;\n recoverTimer = setTimeout(recover, delay);\n }\n\n function clearRecovery() {\n if (recoverTimer) { clearTimeout(recoverTimer); recoverTimer = null; }\n if (giveUpTimer) { clearTimeout(giveUpTimer); giveUpTimer = null; }\n }\n\n /**\n * Renegotiate the media route over the signaling socket, which never left.\n *\n * `createOffer({ iceRestart: true })` rather than `restartIce()`: the latter is the\n * modern spelling and does the same thing by firing `negotiationneeded`, but it is\n * missing from engines this SDK is expected to run in and the explicit form works\n * everywhere. It also keeps the offer on the same path as the original one, so there is a\n * single place where an offer is made.\n */\n async function recover() {\n recoverTimer = null;\n if (finished || !peer) return;\n // It came back on its own while the grace period was running, which is the common case\n // and the reason the grace period exists.\n const state = peer.connectionState || peer.iceConnectionState;\n if (state === 'connected' || state === 'completed') return;\n\n if (!shouldOffer) {\n // Not this side's to restart. Ask, and keep asking — the offerer sees the same drop\n // and is very likely already doing it, so this is the belt to that braces.\n send({ type: 'signal', signal: { kind: 'restart', payload: {} } });\n recoverTimer = setTimeout(recover, RECOVER_RETRY_MS);\n return;\n }\n\n if (restarts >= MAX_RESTARTS) return; // the deadline is what ends it\n restarts += 1;\n await restartIce();\n recoverTimer = setTimeout(recover, RECOVER_RETRY_MS);\n }\n\n /** The restart itself, also reachable from the other side's request. */\n async function restartIce() {\n if (finished || !peer) return;\n try {\n const description = await peer.createOffer({ iceRestart: true });\n await peer.setLocalDescription(description);\n send({ type: 'signal', signal: { kind: 'offer', payload: peer.localDescription.toJSON() } });\n } catch {\n // Nothing to do here but let the next attempt or the deadline decide. Ending the\n // call on one refused renegotiation would throw away the retries that follow.\n }\n }\n\n /**\n * Whether this person is still being heard.\n *\n * A track's `muted` is not the mute button — that is `enabled`, which this file sets and\n * therefore already knows about. `muted` is the *source* stopping: the OS handed the\n * microphone to an incoming phone call, a headset was unplugged, or — the case this app\n * meets daily — Android revoked microphone access because the app went to the background.\n *\n * Which produces the same shape as every other bug on this path: the call is up, the\n * other person is audible, both ends are billed, and nothing this person says goes\n * anywhere. They find out when the other person says \"hello? are you there?\".\n *\n * The loss is reported on a short delay and the recovery immediately. Several engines\n * flick `muted` on for a few frames while a track settles, and a card that said \"your\n * microphone was cut off\" for 200ms at the start of every call would be noise — while a\n * microphone that came back and was still reported lost is the error saying the wrong\n * thing.\n */\n function watchMicrophone() {\n const SETTLE_MS = 600;\n let settle = null;\n let lost = false;\n\n const say = (live) => {\n if (settle) { clearTimeout(settle); settle = null; }\n if (live) {\n if (!lost) return;\n lost = false;\n sayMicrophone(true);\n return;\n }\n if (lost) return;\n settle = setTimeout(() => {\n settle = null;\n if (finished) return;\n lost = true;\n sayMicrophone(false);\n }, SETTLE_MS);\n };\n\n for (const track of microphone.getAudioTracks()) {\n track.onmute = () => say(false);\n track.onunmute = () => say(true);\n // An ended track never comes back — the device is gone. Said at once rather than\n // after the settle, because there is nothing to wait for.\n track.onended = () => {\n if (settle) { clearTimeout(settle); settle = null; }\n if (lost) return;\n lost = true;\n sayMicrophone(false);\n };\n }\n return () => {\n if (settle) { clearTimeout(settle); settle = null; }\n for (const track of microphone.getAudioTracks()) {\n track.onmute = null;\n track.onunmute = null;\n track.onended = null;\n }\n };\n }\n\n const offer = async () => {\n try {\n const description = await peer.createOffer();\n await peer.setLocalDescription(description);\n send({ type: 'signal', signal: { kind: 'offer', payload: peer.localDescription.toJSON() } });\n } catch {\n finish('failed', { reason: 'negotiation' });\n }\n };\n\n const accept = async (payload) => {\n try {\n await peer.setRemoteDescription(new RTCSessionDescription(payload));\n const answer = await peer.createAnswer();\n await peer.setLocalDescription(answer);\n send({ type: 'signal', signal: { kind: 'answer', payload: peer.localDescription.toJSON() } });\n await drainCandidates();\n } catch {\n finish('failed', { reason: 'negotiation' });\n }\n };\n\n const drainCandidates = async () => {\n while (pending.length) {\n const candidate = pending.shift();\n try {\n await peer.addIceCandidate(new RTCIceCandidate(candidate));\n } catch {\n // One unusable candidate is not a failed call — there are usually\n // several, and the connection needs only one of them to work.\n }\n }\n };\n\n /**\n * One message at a time, in the order they arrived.\n *\n * Handling is asynchronous — `setRemoteDescription` and `createAnswer` are\n * promises — and the socket does not wait. Without this chain a candidate\n * arriving mid-answer is examined while the description it belongs to is\n * still being applied, so it goes on the pending list a moment after the\n * pending list was drained, and stays there for the rest of the call.\n */\n let handling = Promise.resolve();\n socket.onmessage = (event) => {\n handling = handling.then(() => handle(event)).catch(() => { /* one bad frame */ });\n };\n\n const handle = async (event) => {\n if (typeof event.data !== 'string') return;\n let message;\n try { message = JSON.parse(event.data); } catch { return; }\n\n switch (message.type) {\n case 'session.created': {\n build(message.data?.ice_servers);\n // What this call keeps, and in what format. Held rather than acted on:\n // nothing is uplinked until somebody is actually on the other end.\n capture = message.data?.capture || null;\n // Ringing, until somebody is on the other side. Whoever answers finds\n // the caller already waiting and skips straight past it — the\n // `peer.joined` that follows a moment later says the same thing, and\n // this is what keeps the answerer's first frame from being silence.\n if (message.data?.peer_present) {\n emit('connecting');\n void startUplink();\n break;\n }\n emit('ringing');\n armRingTimer();\n break;\n }\n\n case 'peer.joined': {\n if (ringTimer) { clearTimeout(ringTimer); ringTimer = null; }\n emit('connecting');\n // Told, not inferred. Both ends learn somebody arrived in the same\n // frame, and only one of them is asked to go first.\n //\n // Remembered as well as acted on: it is also the answer to \"whose job is it to\n // renegotiate when the network changes\", and that question is asked later, when\n // the frame that answered it is long gone. See `recover`.\n shouldOffer = Boolean(message.data?.should_offer);\n if (shouldOffer) await offer();\n // After the offer, and not awaited: opening an audio context is\n // milliseconds, but they would be milliseconds added to the moment\n // somebody says hello, which is the one place on this call where they\n // are audible.\n void startUplink();\n break;\n }\n\n case 'signal': {\n if (!peer) break;\n const { kind, payload } = message.data || {};\n if (kind === 'offer') {\n await accept(payload);\n } else if (kind === 'answer') {\n try {\n await peer.setRemoteDescription(new RTCSessionDescription(payload));\n await drainCandidates();\n } catch {\n finish('failed', { reason: 'negotiation' });\n }\n } else if (kind === 'candidate') {\n if (peer.remoteDescription) {\n try { await peer.addIceCandidate(new RTCIceCandidate(payload)); } catch { /* see above */ }\n } else {\n pending.push(payload);\n }\n } else if (kind === 'restart') {\n // The other side lost the route and cannot renegotiate itself. Honoured only by\n // the side that was nominated to offer, so a request arriving at both ends of a\n // mis-signalled call cannot produce two offers crossing.\n if (shouldOffer) await restartIce();\n }\n break;\n }\n\n case 'peer.left':\n finish('ended', { reason: 'peer_left' });\n break;\n\n case 'error':\n finish('failed', { reason: 'server', detail: message.error?.detail });\n break;\n\n case 'session.ended':\n finish('ended', { reason: 'server' });\n break;\n\n default:\n // Every message the agent conversation has — audio deltas, transcripts,\n // state changes. None of them happen on a bridged call, and one\n // arriving is not worth ending a working call over.\n break;\n }\n };\n\n socket.onclose = () => finish('ended', { reason: 'disconnected' });\n socket.onerror = () => finish('failed', { reason: 'socket' });\n\n /**\n * Stop waiting.\n *\n * The deadline comes from the server, which wrote it down when the call was\n * created — so a browser with a slow clock waits a little longer rather than\n * hanging up on somebody mid-answer, and the server refuses a give-up that\n * arrives early in any case.\n */\n function armRingTimer() {\n if (!ringsUntil) return;\n const remaining = new Date(ringsUntil).getTime() - Date.now();\n if (!Number.isFinite(remaining)) return;\n ringTimer = setTimeout(() => finish('missed'), Math.max(0, remaining));\n }\n\n return {\n hangUp() {\n // Said out loud before the socket closes. A close on its own would end\n // the call too — sphoro.voice ends a bridge the moment one side leaves —\n // but saying so first is what distinguishes hanging up from a tunnel.\n send({ type: 'session.end' });\n finish('ended', { reason: 'hung_up' });\n },\n setMuted(muted) {\n for (const track of microphone.getAudioTracks()) track.enabled = !muted;\n return muted;\n },\n isMuted() {\n return microphone.getAudioTracks().every((track) => !track.enabled);\n },\n };\n}\n\n/** Open the socket, resolving only once it is actually open. */\nfunction openSocket(url) {\n return new Promise((resolve, reject) => {\n let socket;\n try {\n socket = new WebSocket(url, SUBPROTOCOL);\n } catch (err) {\n reject(err);\n return;\n }\n socket.onopen = () => {\n socket.onopen = null;\n socket.onerror = null;\n resolve(socket);\n };\n socket.onerror = () => {\n socket.onopen = null;\n socket.onerror = null;\n reject(new Error('[BugCatch] could not connect the call.'));\n };\n });\n}\n\n/**\n * Say something went wrong with the copy, without ending anything.\n *\n * `console.warn` and not a state: every caller of this file renders the states\n * to somebody who is mid-conversation, and \"the recording did not start\" is not\n * news to the two people talking. It is news to whoever turned recording on,\n * and the console is where they will look.\n */\nfunction warn(message) {\n try { console.warn(`[BugCatch] ${message}`); } catch { /* no console */ }\n}\n\nfunction stopStream(stream) {\n for (const track of stream?.getTracks?.() ?? []) {\n try { track.stop(); } catch { /* already stopped */ }\n }\n}\n","// The reporter's telephone.\n//\n// Everything else this SDK draws is a consequence of something the person on\n// the page did: they pressed the widget, they filed a report, they asked to\n// talk it through. This is the one thing that arrives unasked — a member of the\n// team has a question about a report and is ringing to ask it — and that makes\n// it the piece with the most rules around it.\n//\n// # What it is allowed to do\n//\n// Appear, and make a noise. That is the whole of it. No microphone is opened,\n// no audio moves and nothing is connected until the person presses Answer, and\n// if they press nothing at all it takes itself away when the ringing stops. The\n// card is an offer; the SDK does not put a customer on a call because somebody\n// at the vendor clicked a button.\n//\n// # And what it says\n//\n// A name, a product and what the call is about, because a card that says\n// \"someone is calling about a report\" is indistinguishable from the thing every\n// bank tells people to hang up on. And whether the call would be recorded,\n// stated before Answer rather than after — on a peer-to-peer call the copy that\n// gets kept is uplinked by this browser, so telling somebody afterwards is\n// telling them about something that already happened.\n//\n// # The noise\n//\n// Synthesised, for the reason the dashboard's is: a ringtone as an audio file\n// is a binary in a bundle that runs on somebody else's site, an extra request\n// on their page, and a thing that can go missing behind a cache header. Four\n// oscillators and a table of numbers cost none of that.\n//\n// It is also softer and shorter than the dashboard's. A phone on a support\n// desk is meant to interrupt; this one is ringing in the middle of a stranger's\n// shopping cart, and a page that suddenly plays a loud telephone at somebody is\n// a page they close. If the browser refuses to play it at all — no interaction\n// yet, which is its rule and not ours — the card rings silently rather than not\n// appearing.\n\n/**\n * A quiet double-pulse, once every three seconds.\n *\n * Two notes rather than one so it reads as a telephone rather than as an alert,\n * and a fifth apart because that interval is the one people hear as \"ringing\"\n * across every phone system anybody has used.\n */\nconst RING = {\n cycleMs: 3000,\n peak: 0.045,\n bursts: [\n { at: 0, seconds: 0.4, hz: [440, 660] },\n { at: 0.55, seconds: 0.4, hz: [440, 660] },\n ],\n};\n\n/**\n * The same question asked of the browser rather than of ourselves.\n *\n * Chrome and Edge implement this and are the browsers that enforce the policy\n * in the first place, so where it matters it answers. It is the recovery for a\n * host page that swallowed every gesture before we saw it — better to make a\n * quiet noise the person expects than to stay silent because of somebody else's\n * event handler.\n */\nfunction activatedByBrowser() {\n try {\n return Boolean(navigator.userActivation?.hasBeenActive);\n } catch {\n return false;\n }\n}\n\n/**\n * Start ringing, and give back the way to stop.\n *\n * Returns a no-op teardown on every browser that will not do it, which is a\n * real and ordinary case: a page nobody has clicked on yet has no audio\n * permission, and the correct behaviour there is a card that appears in silence\n * rather than no card.\n */\nfunction startRinging(touched) {\n if (!touched() && !activatedByBrowser()) return () => {};\n\n const Ctx = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext);\n if (!Ctx) return () => {};\n\n let context;\n try {\n context = new Ctx();\n } catch {\n return () => {};\n }\n\n let stopped = false;\n let timer = null;\n const voices = [];\n\n const burst = ({ at, seconds, hz }) => {\n const start = context.currentTime + at;\n const gain = context.createGain();\n gain.connect(context.destination);\n // Shaped rather than switched. A square edge on a sine is a click, and a\n // click every three seconds is more irritating than the tone it introduces.\n gain.gain.setValueAtTime(0, start);\n gain.gain.linearRampToValueAtTime(RING.peak, start + 0.02);\n gain.gain.setValueAtTime(RING.peak, start + seconds - 0.06);\n gain.gain.linearRampToValueAtTime(0, start + seconds);\n\n for (const frequency of hz) {\n const osc = context.createOscillator();\n osc.type = 'sine';\n osc.frequency.value = frequency;\n osc.connect(gain);\n osc.start(start);\n osc.stop(start + seconds + 0.02);\n voices.push(osc);\n }\n };\n\n const cycle = () => {\n if (stopped) return;\n try {\n RING.bursts.forEach(burst);\n } catch { /* the card is still on screen, which is the part that matters */ }\n };\n\n const begin = () => {\n if (stopped) return;\n cycle();\n timer = setInterval(cycle, RING.cycleMs);\n };\n\n if (context.state === 'running') begin();\n else context.resume().then(begin).catch(() => {});\n\n return () => {\n stopped = true;\n if (timer) clearInterval(timer);\n // Stopped rather than left to finish. Answered, this is cut off by the\n // microphone opening a fraction of a second later, and a ring still sounding\n // over the first word of a conversation is the thing people apologise for.\n for (const osc of voices) {\n try { osc.stop(); } catch { /* already stopped on its own schedule */ }\n }\n try { context.close(); } catch { /* nothing left to close */ }\n };\n}\n\n/** Seconds left until a deadline, floored at zero. */\nfunction secondsLeft(ringsUntil) {\n const until = Date.parse(ringsUntil || '');\n if (Number.isNaN(until)) return 0;\n return Math.max(0, Math.round((until - Date.now()) / 1000));\n}\n\nconst BUTTON = [\n 'flex:1',\n 'padding:9px 12px',\n 'border:0',\n 'border-radius:8px',\n 'font:600 13px/1.4 inherit',\n 'cursor:pointer',\n].join(';');\n\n/**\n * Put the card on screen and ring until something happens to it.\n *\n * @param {object} opts\n * @param {object} opts.call The `incomingCall` object off the update stream —\n * `callId`, `from`, `project`, `reference`, `ringsUntil`, `recorded`.\n * @param {string} opts.accent The integration's own colour, for Answer.\n * @param {string} [opts.position] Same two words the toast takes.\n * @param {() => void} opts.onAnswer Pressed Answer.\n * @param {() => void} opts.onDecline Pressed Decline.\n * @param {() => void} [opts.onExpire] Nobody pressed anything in time.\n * @param {() => boolean} [opts.touched] Whether anybody has touched the page\n * yet, which is when a sound may be made (gesture.js). Asked of the caller\n * because this file is fetched when a call rings, and could not have been\n * listening for the clicks that came before.\n * @returns {(reason?: string) => void} Take it away. Called with no reason on\n * an ordinary dismissal; the caller passes one only for its own logging.\n */\nexport function showIncomingCall({\n call, accent = '#5b6ef5', position = 'bottom-right',\n onAnswer, onDecline, onExpire, touched = () => false,\n}) {\n if (typeof document === 'undefined' || !document.body) return () => {};\n\n const [vertical, horizontal] = String(position).split('-');\n const el = document.createElement('div');\n el.setAttribute('role', 'alertdialog');\n el.setAttribute('aria-live', 'assertive');\n el.setAttribute('aria-label', `${call.from || 'Support'} is calling about ${call.reference}`);\n el.style.cssText = [\n 'position:fixed',\n `${vertical === 'top' ? 'top' : 'bottom'}:20px`,\n `${horizontal === 'left' ? 'left' : 'right'}:20px`,\n // Above the update toast, which uses the maximum and may be on screen at\n // the same moment — a status line must not sit on top of a ringing phone.\n 'z-index:2147483647',\n 'width:300px',\n 'max-width:calc(100vw - 40px)',\n 'padding:14px',\n 'border-radius:12px',\n 'background:#1b1e26',\n 'color:#eceef2',\n 'font:13px/1.5 -apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif',\n 'box-shadow:0 18px 44px rgba(0,0,0,0.5)',\n `border:1px solid ${accent}`,\n 'opacity:0',\n 'transform:translateY(8px)',\n 'transition:opacity .2s ease,transform .2s ease',\n ].join(';');\n\n const heading = document.createElement('div');\n heading.style.cssText = 'font-weight:600;font-size:14px;margin-bottom:2px';\n // textContent throughout: every string on this card came from a person typing\n // it, and it is being written into somebody else's page.\n heading.textContent = `${call.from || 'Support'} is calling`;\n\n const about = document.createElement('div');\n about.style.cssText = 'color:#a4abba;margin-bottom:10px';\n about.textContent = call.project\n ? `About your report ${call.reference} · ${call.project}`\n : `About your report ${call.reference}`;\n\n const countdown = document.createElement('div');\n countdown.style.cssText = 'color:#7d8598;font-size:12px;margin-bottom:10px';\n\n const answer = document.createElement('button');\n answer.type = 'button';\n answer.textContent = 'Answer';\n answer.style.cssText = `${BUTTON};background:${accent};color:#fff`;\n\n const decline = document.createElement('button');\n decline.type = 'button';\n decline.textContent = 'Not now';\n decline.style.cssText = `${BUTTON};background:#2b2f3a;color:#c8cedb`;\n\n const actions = document.createElement('div');\n actions.style.cssText = 'display:flex;gap:8px';\n actions.append(answer, decline);\n\n el.append(heading, about, countdown);\n if (call.recorded) {\n const kept = document.createElement('div');\n kept.style.cssText = 'color:#7d8598;font-size:12px;margin-bottom:10px';\n kept.textContent = 'This call will be recorded.';\n el.append(kept);\n }\n el.append(actions);\n document.body.appendChild(el);\n\n const stopRinging = startRinging(touched);\n let closed = false;\n let ticker = null;\n\n const close = () => {\n if (closed) return;\n closed = true;\n if (ticker) clearInterval(ticker);\n stopRinging();\n el.style.opacity = '0';\n el.style.transform = 'translateY(8px)';\n setTimeout(() => el.remove(), 250);\n };\n\n const tick = () => {\n const left = secondsLeft(call.ringsUntil);\n countdown.textContent = left\n ? `Ringing — ${left}s`\n : 'The call has stopped ringing.';\n if (left) return;\n // The deadline is the server's, not this browser's arithmetic, for the\n // reason the API writes it down: two clocks disagreeing about when a phone\n // stopped ringing is how somebody gets cut off mid-answer.\n close();\n try { onExpire?.(); } catch { /* theirs */ }\n };\n tick();\n ticker = setInterval(tick, 1000);\n\n answer.addEventListener('click', () => {\n // Stopped before the handler runs, not after: opening a microphone takes a\n // moment, and a phone still ringing during it sounds like the button\n // did nothing.\n close();\n try { onAnswer?.(); } catch { /* theirs */ }\n });\n\n decline.addEventListener('click', () => {\n close();\n try { onDecline?.(); } catch { /* theirs */ }\n });\n\n requestAnimationFrame(() => {\n el.style.opacity = '1';\n el.style.transform = 'translateY(0)';\n });\n\n return close;\n}\n","// What the two loaders (lazy.js, umd/lazy.js) share: how a part of the\n// script-tag build that arrived on its own is found, and what a part that\n// never arrived is called.\n//\n// The ES build does not use the registry: a bundler splits it at its import()\n// calls and wires the pieces back together itself. A <script> tag has no such\n// machinery, so each part of that build is a plain script that, when it runs,\n// leaves its exports under a name on `window` — and umd/lazy.js, having added\n// the script, reads them from there once it has loaded.\n//\n// The name carries the version, so a part left in a cache by an older release\n// is never mistaken for this one's: it registers under its own version, this\n// release finds nothing under its, and says so rather than running code that\n// expects a different client.\n\n// Replaced at build time from package.json; see vite.config.js.\nconst VERSION = typeof __SDK_VERSION__ === 'string' ? __SDK_VERSION__ : '0.0.0-dev';\n\nconst REGISTRY = '__bugcatchChunks';\n\nconst key = (name) => `${name}@${VERSION}`;\n\n/** Called by a part as its script runs: here is what I export. */\nexport function registerChunk(name, exports) {\n const registry = window[REGISTRY] || (window[REGISTRY] = {});\n registry[key(name)] = exports;\n}\n\n/** What a part registered, once its script has run; undefined until then. */\nexport function registeredChunk(name) {\n return typeof window !== 'undefined' ? window[REGISTRY]?.[key(name)] : undefined;\n}\n\n/**\n * A part that could not be loaded, said twice: the details for whoever is\n * debugging the page, in the console, and a sentence for the person using it,\n * as the error — which is what the panel shows when a call cannot start.\n */\nexport function chunkUnavailable(detail, cause) {\n // eslint-disable-next-line no-console\n console.warn(`[BugCatch] ${detail}`, ...(cause ? [cause] : []));\n const err = new Error('[BugCatch] part of the widget could not be loaded — check the connection and try again.');\n if (cause) err.cause = cause;\n return err;\n}\n\n/** One fetch per part at a time; a failure is forgotten, so the next ask retries. */\nexport function once(load) {\n let pending = null;\n return (...args) => {\n pending ||= load(...args).catch((err) => { pending = null; throw err; });\n return pending;\n };\n}\n\nexport { VERSION as CHUNK_VERSION };\n","// The call code as a script of its own, for the script-tag build: run, it\n// hands its exports to the loader that added it (lazy.js beside this file).\nimport * as call from '../call.js';\nimport { registerChunk } from '../chunks.js';\n\nregisterChunk('call', call);\n"],"names":["createContext","rate","Ctor","resumeContext","context","downsample","input","fromRate","toRate","ratio","out","i","start","end","sum","j","MU_BIAS","MU_CLIP","encodeMuLaw","samples","clamped","sample","sign","exponent","mask","mantissa","CAPTURE_BLOCK","tapMicrophone","stream","onFrame","wireRate","source","capture","event","frames","mute","SUBPROTOCOL","FALLBACK_ICE","openMicrophone","liveCallSupported","_a","joinLiveCall","realtimeUrl","ringsUntil","onState","onAudioBlocked","onMicrophone","emit","state","detail","sayBlocked","blocked","sayMicrophone","live","err","microphone","socket","openSocket","stopStream","peer","remoteAudio","finished","ringTimer","uplink","disarmGesture","unwatchMicrophone","starting","pending","shouldOffer","connectedOnce","reconnecting","recoverTimer","giveUpTimer","restarts","RECOVER_GRACE_MS","RECOVER_RETRY_MS","RECOVER_DEADLINE_MS","MAX_RESTARTS","finish","clearRecovery","stopUplink","send","message","startUplink","encoding","warn","tap","bytes","playRemote","attempt","markBlocked","markPlaying","armGesture","retry","events","name","build","iceServers","track","watchMicrophone","incoming","routeChanged","enterRecovery","delay","recover","restartIce","description","settle","lost","say","offer","accept","payload","answer","drainCandidates","candidate","handling","handle","_b","_c","armRingTimer","_d","kind","_e","remaining","muted","url","resolve","reject","RING","activatedByBrowser","startRinging","touched","Ctx","stopped","timer","voices","burst","at","seconds","hz","gain","frequency","osc","cycle","begin","secondsLeft","until","BUTTON","showIncomingCall","call","accent","position","onAnswer","onDecline","onExpire","vertical","horizontal","el","heading","about","countdown","decline","actions","kept","stopRinging","closed","ticker","close","tick","left","VERSION","REGISTRY","key","registerChunk","exports","registry"],"mappings":"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"}
|