1// PPOAGroup STAGE-1 AI conference harness â browser interop (additive, test-only). 2// 3// Deliberately SEPARATE from ppoaDialer (js/dialer.js): it owns its OWN Twilio.Device so the 4// manual dialer's device/handle is never touched. The operator joins the conference as an 5// OUTGOING Device.connect (same TwiML app â /outbound, which branches on the conferenceName 6// param), so NO change to the voice grant (IncomingAllow stays false) is required. 7// 8// Listen vs. take-over is local-mic mute: 9// - join muted â operator hears the AIâdestination conversation but is silent (live-listen) 10// - un-mute â operator speaks live on the conference (barge); pair with dropping the AI 11window.ppoaConference = (function () { 12 let device = null; 13 let activeCall = null; 14 let dotNetRef = null; 15 16 function reportEvent(name, payload) { 17 if (!dotNetRef) return; 18 try { 19 dotNetRef.invokeMethodAsync('OnConferenceEvent', name, payload || {}); 20 } catch (e) { 21 // swallow â Blazor disposed before event reached 22 } 23 } 24 25 function isSdkUnavailable() { 26 if (window.__ppoaTwilioSdkLoadFailed === true) return true; 27 if (typeof window.Twilio === 'undefined') return true; 28 if (!window.Twilio.Device) return true; 29 return false; 30 } 31 32 function attachCallEvents(call) { 33 call.on('accept', () => reportEvent('accept', {})); 34 call.on('disconnect', () => { reportEvent('disconnect', {}); activeCall = null; }); 35 call.on('cancel', () => reportEvent('cancel', {})); 36 call.on('error', (err) => reportEvent('error', { message: (err && err.message) || 'Unknown call error' })); 37 } 38 39 return { 40 isReady: function () { return !isSdkUnavailable(); }, 41 42 register: function (token, dotNet) { 43 dotNetRef = dotNet; 44 if (isSdkUnavailable()) { 45 reportEvent('sdkUnavailable', { message: 'Voice library failed to load. Hard refresh (Ctrl+Shift+R).' }); 46 return false; 47 } 48 try { 49 if (device) { 50 device.updateToken(token); 51 reportEvent('ready', {}); 52 return true; 53 } 54 device = new window.Twilio.Device(token, { 55 logLevel: 'warn', 56 closeProtection: true, 57 edge: 'ashburn' 58 }); 59 device.on('registered', () => reportEvent('ready', {})); 60 device.on('error', (err) => reportEvent('error', { message: (err && err.message) || 'Device error' })); 61 device.on('tokenWillExpire', () => reportEvent('tokenWillExpire', {})); 62 device.on('incoming', () => { /* harness is outbound-join only */ }); 63 device.register(); 64 return true; 65 } catch (e) { 66 reportEvent('error', { message: e.message || 'register failed' }); 67 return false; 68 } 69 }, 70 71 // Join the named conference, muted, for silent live-listen. 72 joinMuted: async function (conferenceName) { 73 if (isSdkUnavailable()) { 74 reportEvent('sdkUnavailable', { message: 'Voice library failed to load. Hard refresh (Ctrl+Shift+R).' }); 75 return false; 76 } 77 if (!device) { reportEvent('error', { message: 'Device not registered' }); return false; } 78 try { 79 activeCall = await device.connect({ params: { conferenceName: conferenceName } }); 80 attachCallEvents(activeCall); 81 // Join silent â operator listens; conference does not hear them until barge. 82 try { activeCall.mute(true); } catch { /* ignore */ } 83 reportEvent('joined', { muted: true }); 84 return true; 85 } catch (e) { 86 reportEvent('error', { message: e.message || 'join failed' }); 87 return false; 88 } 89 }, 90 91 setMuted: function (shouldMute) { 92 if (activeCall) { 93 try { activeCall.mute(!!shouldMute); reportEvent('muteChanged', { muted: !!shouldMute }); } catch { /* ignore */ } 94 } 95 }, 96 97 leave: function () { 98 if (activeCall) { try { activeCall.disconnect(); } catch { /* ignore */ } } 99 activeCall = null; 100 }, 101 102 unregister: function () { 103 try { if (activeCall) activeCall.disconnect(); } catch { /* ignore */ } 104 try { if (device) device.unregister(); } catch { /* ignore */ } 105 activeCall = null; 106 device = null; 107 } 108 }; 109})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.