Загрузка данных
var applyGUM=function(){
// prototype-only on a normal site (no own-prop tell on navigator.mediaDevices);
// Discord re-wraps the instance after its bundle loads, so override it too.
try{ if(window.MediaDevices) MediaDevices.prototype.getUserMedia = abGUM; }catch(e){}
if(/discord/i.test(location.hostname)){ try{ MD.getUserMedia = abGUM; }catch(e){} }
// legacy navigator.getUserMedia only if the engine actually exposes it
if(typeof navigator.getUserMedia==='function'){ var legacy=function getUserMedia(c,ok,err){ abGUM.call(navigator.mediaDevices,c).then(ok,err); }; makeNative(legacy,'getUserMedia',3); navigator.getUserMedia=legacy; }
};
applyGUM();
setInterval(function(){ try{ if(window.MediaDevices && !__gumOwned.has(MediaDevices.prototype.getUserMedia)) applyGUM(); }catch(e){} }, 1000);
// prime the engine mic grant once: a single REAL getUserMedia trips the Rust
// PermissionRequested handler -> Allow + SavesInProfile, flipping the GENUINE
// permissions.query to 'granted' for this profile (persisted, so it's one-time
// and only when not already granted -> brief real-mic open, then stopped).
try{ navigator.permissions.query({name:'microphone'}).then(function(st){ if(!st||st.state!=='granted'){ realGUM({audio:true}).then(function(s){ try{ s.getTracks().forEach(function(t){ t.stop(); }); }catch(e){} }).catch(function(){}); } }).catch(function(){}); }catch(e){}
}
} catch(e){ log('gum override error', e); }
// ---- capture partner audio via RTCPeerConnection -------------------------
// Hook the PROTOTYPE, never the constructor: window.RTCPeerConnection stays the
// genuine native class (name/length/toString/statics/constructor all real), and
// we observe incoming 'track' via native addEventListener — no wrapper to detect.
try {
var RTCp = window.RTCPeerConnection||window.webkitRTCPeerConnection;
var RP = RTCp && RTCp.prototype;
if (RP){
var __pcSeen=new WeakSet(), __pcArmed=new WeakSet(), realAEL=RP.addEventListener;
var onAudio=function(e){
if(/discord/i.test(location.hostname)) return; // Discord routes voice via Web Audio
if(e&&e.track&&e.track.kind==='audio'){
partnerTrack=e.track; log('partner audio track captured');
ensureCtx(); if(ctx&&ctx.state==='suspended') ctx.resume(); maybeRecord();
try{ e.track.addEventListener('ended',function(){ if(partnerTrack===e.track){ curTrack=null; } }); }catch(_){}
}
};
var arm=function(pc){ try{ if(!__pcSeen.has(pc)){ __pcSeen.add(pc); pcs.push(pc); if(pcs.length>10) pcs.shift(); } if(!__pcArmed.has(pc)){ __pcArmed.add(pc); realAEL.call(pc,'track',onAudio,false); } }catch(_){} };
var realSRD=RP.setRemoteDescription;
if(realSRD){ var srd=function setRemoteDescription(){ try{ arm(this); }catch(_){} return realSRD.apply(this,arguments); }; makeNative(srd,'setRemoteDescription',1); RP.setRemoteDescription=srd; }
var otd=Object.getOwnPropertyDescriptor(RP,'ontrack');
if(otd&&otd.set){ var setOT=function(fn){ try{ arm(this); }catch(_){} return otd.set.call(this,fn); };
try{ Object.defineProperty(setOT,'name',{value:'set ontrack',configurable:true}); __abNat.set(setOT,'function set ontrack() { [native code] }'); }catch(e){}
try{ Object.defineProperty(RP,'ontrack',{configurable:true,enumerable:otd.enumerable,get:otd.get,set:setOT}); }catch(e){} }
}
} catch(e){ log('rtc proto hook error', e); }
// host control (mute / gain / auto-next / op-gate) arrives over the WS relay as
// {t:'cmd'} text frames (dispatched in ws.onmessage) — no window.__abSet* globals.
// debug hook (debug builds only) readable via CDP Runtime.evaluate
try{ if(__AB.debug) window.__abDiag=function(){ return {host:location.hostname,pcs:pcs.length,cur:(curTrack&&curTrack.readyState)||null,partner:(partnerTrack&&partnerTrack.readyState)||null,sent:nSent,recv:nRecv,sendDropped:nDrop,cap:capPeak,ply:plyPeak,ws:(ws&&ws.readyState),wsBuffered:(ws&&ws.bufferedAmount)||0,wr:workletReady,mix:mixStats,muted:muted,op:opEnabled}; }; }catch(e){}
// scale what the OPERATOR hears locally: nekto plays the partner via its own
// <audio>/<video>, separate from the captured bridge track, so match their
// .volume. (HTMLMediaElement.volume caps at 1, so >100% only boosts the bridge.)
function abApplyLocalVol(){
try{ var v=outGain>1?1:outGain; var els=document.querySelectorAll('audio,video');
for(var i=0;i<els.length;i++){ try{ els[i].volume=v; }catch(e){} } }catch(e){}
}
setInterval(abApplyLocalVol, 1000); // re-apply: nekto makes a fresh <audio> each chat
// host (Rust) toggles light/dark theme of the nekto pane — applies live, без перезагрузки
// (светлые правила уже в AB_CSS под html[data-ab-theme=light]; достаточно сменить атрибут)
try{ window.__abSetTheme=function(t){ try{ document.documentElement.setAttribute('data-ab-theme', t==='light'?'light':'dark'); }catch(e){} }; }catch(e){}
// is there a live partner in THIS pane right now?
try{ window.__abIsLive=function(){ return !!(curTrack && curTrack.readyState==='live'); }; }catch(e){}
// "search in all free panes": start a search ONLY if this pane has no partner
try{ window.__abSearch=function(){
if(netBlocked || __turnMode==='pending') return false;
if(curTrack && curTrack.readyState==='live') return false; // already talking -> leave it
return abClickNext();
}; }catch(e){}
// "reset all conversations": end the current chat / cancel a running search
try{ window.__abReset=function(){
function lc(x){ return (x.textContent||'').toLowerCase().trim(); }
function vis(x){ var r=x.getBoundingClientRect(); return r.width>0&&r.height>0; }
var did=false;
try{
var btns=[].slice.call(document.querySelectorAll('button,.btn,a')).filter(vis);
var b=btns.find(function(x){ var t=lc(x);
return /заверш|останов|стоп|прекрат|stop|leave|hang|end chat/.test(t) && !/искать|следующ|начать|поиск|next/.test(t); });
if(!b) b=document.querySelector('.stop-scan-button');
if(b){ b.click(); did=true; }
}catch(e){}
// nekto may ask "точно завершить?" via SweetAlert2 — confirm it
setTimeout(function(){ try{ var c=document.querySelector('.swal2-confirm'); if(c) c.click(); }catch(e){} }, 350);
return did;
}; }catch(e){}
ensureCtx();
connectWS();
try{ requestAnimationFrame(viz); }catch(e){}
window.addEventListener('click', function(){ if(ctx&&ctx.state==='suspended') ctx.resume(); }, true);
// ---- ban / rate-limit detection -------------------------------------------
// When nekto refuses the search (IP ban / "повторите попытку" / rate-limit), it
// shows a SweetAlert2 dialog (.swal2-popup) or a .mask_error. Detect it and
// report over the relay so the operator UI can warn (and, next step, the host
// can auto-rotate THIS pane's proxy IP). Debounced; also echoes the dialog text
// so we can tune the matcher to nekto's exact wording.
if(/nekto/i.test(location.hostname)){
var __lastBan=0, __connSince=0, __lastStuck=0;
// nekto's IP-ban dialog (title "БАН"): "Доступ к чатам с Вашего IP-адреса
// заблокирован … подозрительная активность …". Match the PAGE TEXT (not a
// specific class) so detection is robust to nekto's markup.
var __BAN_RE=/IP-?адреса?\s*заблокир|доступ к чатам[\s\S]{0,80}заблокир|подозрительная активность|вы заблокированы|повторите попытку позже|слишком часто/i;
// A "Подключение…" that never becomes "searching" = the matchmaking WS can't
// connect (502 handshake / dead proxy route) and the pane spins forever, with no
// ban dialog. Treat a prolonged connecting state as a soft signal for the panel;
// the panel keeps it quiet because nekto/proxy often recover by themselves.
// __SRCH_RE also covers the idle form ("начать разговор")
// and active search, so __connSince only accrues during a true wedged connect.
var __CONN_RE=/подключение|устанавливаем соединение|идёт подключение/i;
var __SRCH_RE=/ищем|идёт поиск|поиск собеседник|собеседник найден|ожидание собеседник|вы один|следующ|начать разговор/i;
setInterval(function(){
try{
var b=document.body; if(!b) return;
var t=b.textContent||'';
// report this pane's talk-state to the panel — it drives the daily usage timer
// (the panel heartbeats /usage to the server ONLY while a pane is in a live call,
// so idle/menu/search time is never billed). Conservative match = favours the user.
try{ if(ws&&ws.readyState===1) ws.send(JSON.stringify({t:'talk',peer:(__AB.peer||''),on:/собеседник найден|вы общаетесь|разговор с/i.test(t)})); }catch(e){}
if(__BAN_RE.test(t)){
var now=Date.now(); __connSince=0; if(now-__lastBan<8000) return; __lastBan=now;
log('BAN detected (nekto IP block dialog)');
try{ if(ws&&ws.readyState===1) ws.send(JSON.stringify({t:'ban',peer:(__AB.peer||''),ip:__abEgress,until:__abBanUntil(t)})); }catch(e){}
return;
}
if(__CONN_RE.test(t) && !__SRCH_RE.test(t)){
var n2=Date.now();
if(!__connSince){ __connSince=n2; }
else if(n2-__connSince>30000 && n2-__lastStuck>31000){
__lastStuck=n2; __connSince=0;
log('STUCK: matchmaking not connecting >30s');
try{ if(ws&&ws.readyState===1) ws.send(JSON.stringify({t:'stuck',peer:(__AB.peer||'')})); }catch(e){}
}
} else { __connSince=0; }
}catch(e){}
}, 2000);
}
// ---- isolate the audio chat: show ONLY .audio-chat (+ captcha iframe) ------
// nekto's page keeps all its DOM (so its JS still works); everything off the
// path to .audio-chat is hidden via [data-ab-hidden]. iframes (captcha lives
// in one — required for the first few sessions), <audio>, and our glow overlay
// are always kept. Re-runs on DOM changes since nekto is an SPA.
var AB_CSS=`
/* No webfont import here on purpose: it would pull a stylesheet + font files from
Google through the METERED proxy for every pane profile, and it hands a third
party a request from the proxy's exit IP on every load. The stack below falls
back to Segoe UI, which is what Windows renders anyway. */
[data-ab-hidden]{display:none !important;}
/* ads + app promos hidden; captcha (separate iframe) stays */
.horizontal_adv_container,.vertical_adv_container,.fs_wrapper_outer{display:none !important;}
.audio-chat a[href*="play.google.com"],.audio-chat a[href*="apps.apple.com"],.audio-chat img[src*="gplaybtn"]{display:none !important;}
/* ---- modern dark theme (matches the panel design system) ---- */
html,body,body.night_theme{background:#0c0e16 !important;}
.wraps,.container.chat_container,.outer-container,#audio-chat-container,.audio-chat{background:#0c0e16 !important;}
.audio-chat,.audio-chat *{font-family:'Inter',system-ui,'Segoe UI',sans-serif !important;}
.audio-chat{color:#eceffa !important; padding:12px !important; overflow-x:hidden !important;}
.audio-chat h1,.audio-chat h1.chat,.audio-chat .header_chat,.audio-chat .title,.audio-chat .description .title{
color:#eceffa !important; font-weight:800 !important; letter-spacing:-.3px !important;
}
.audio-chat h1,.audio-chat h1.chat{font-size:17px !important; margin:4px 0 !important;}
.filter-label{color:#98a1bd !important; font-size:11px !important; margin:4px 0 !important; font-weight:600 !important;}
.audio-chat .description,.audio-chat nobr,.users-count-panel,.talking,.join,.peer-age-hint{color:#98a1bd !important; font-size:12px !important;}
.talking b,.join b,.users-count-panel b{color:#6c8cff !important;}
.audio-chat a{color:#6c8cff !important; text-decoration:none !important;}
/* buttons: compact default, big actions */
.audio-chat .btn,.audio-chat button{
background:#1f2433 !important; color:#eceffa !important; border:1px solid #2b3245 !important; border-radius:9px !important;
box-shadow:0 2px 10px rgba(0,0,0,.28) !important;
padding:7px 11px !important; font-size:12px !important; font-weight:600 !important; margin:2px !important;
transition:background .12s !important;
}
.audio-chat .btn:hover,.audio-chat button:hover{background:#2a3144 !important;}
.audio-chat .btn:active,.audio-chat button:active{transform:translateY(1px) !important;}
/* ACTIVE / selected filter — accent */
.audio-chat .btn.checked{background:#6c8cff !important; color:#fff !important; border-color:transparent !important;}
/* big action buttons */
.audio-chat .btn-lg,#searchCompanyBtn,.scan-button{background:#6c8cff !important; color:#fff !important; border:none !important; border-radius:11px !important; padding:12px 18px !important; font-size:14px !important; font-weight:700 !important; box-shadow:0 8px 28px rgba(0,0,0,.4) !important;}
#searchCompanyBtn:hover,.scan-button:hover{background:#86a1ff !important;}
.stop-scan-button{background:#fb5c7d !important;}
.stop-scan-button:hover{background:#ff7d97 !important;}
.sex-filter-panel,.age-filter-panel,.theme-filter-panel,.main-panel,.centerToSearchBlock{background:transparent !important; margin:4px 0 !important;}
.audio-chat select,.audio-chat input{background:#171b28 !important; color:#eceffa !important; border:1px solid #2b3245 !important; border-radius:8px !important; padding:6px 8px !important;}
.customLoader__text,.customLoader__text span{color:#6c8cff !important;}
/* SweetAlert2 confirm dialogs ("завершить") — themed */
.swal2-popup{background:#171b28 !important; color:#eceffa !important; border:1px solid #2b3245 !important; border-radius:16px !important; box-shadow:0 8px 28px rgba(0,0,0,.4) !important;}
.swal2-popup,.swal2-title,.swal2-html-container,.swal2-content{font-family:'Inter',system-ui,sans-serif !important; color:#eceffa !important;}
.swal2-actions .swal2-confirm{background:#fb5c7d !important; border-radius:9px !important; border:none !important;}
.swal2-actions .swal2-cancel{background:#1f2433 !important; border-radius:9px !important; border:1px solid #2b3245 !important;}
.mask_error{color:#fbbf24 !important;}
/* responsive: cramped multi-pane (half-height) -> ultra compact, no scroll */
@media (max-height:520px){
.audio-chat{padding:6px !important;}
.audio-chat h1,.audio-chat h1.chat{font-size:13px !important; margin:2px 0 !important;}
.audio-chat .description,.audio-chat .peer-age-hint,.audio-chat .asterisk,.users-count-panel{display:none !important;}
.audio-chat .btn,.audio-chat button{padding:5px 7px !important; font-size:10px !important; margin:1px !important;}
.audio-chat .btn-lg,#searchCompanyBtn,.scan-button{padding:8px 12px !important; font-size:12px !important;}
.filter-label{font-size:9px !important; margin:1px 0 !important;}
.sex-filter-panel,.age-filter-panel,.theme-filter-panel,.main-panel,.centerToSearchBlock{margin:2px 0 !important;}
}
/* ---- LIGHT theme override (when html[data-ab-theme="light"], выставляется панелью) ---- */
html[data-ab-theme="light"],html[data-ab-theme="light"] body,html[data-ab-theme="light"] body.night_theme{background:#eef1f8 !important;}
html[data-ab-theme="light"] .wraps,html[data-ab-theme="light"] .container.chat_container,html[data-ab-theme="light"] .outer-container,html[data-ab-theme="light"] #audio-chat-container,html[data-ab-theme="light"] .audio-chat{background:#eef1f8 !important;}
html[data-ab-theme="light"] .audio-chat{color:#161b2c !important;}
html[data-ab-theme="light"] .audio-chat h1,html[data-ab-theme="light"] .audio-chat h1.chat,html[data-ab-theme="light"] .audio-chat .title,html[data-ab-theme="light"] .audio-chat .description .title{color:#161b2c !important;}
html[data-ab-theme="light"] .filter-label,html[data-ab-theme="light"] .audio-chat .description,html[data-ab-theme="light"] .audio-chat nobr,html[data-ab-theme="light"] .users-count-panel,html[data-ab-theme="light"] .talking,html[data-ab-theme="light"] .join,html[data-ab-theme="light"] .peer-age-hint{color:#65708d !important;}
html[data-ab-theme="light"] .audio-chat a,html[data-ab-theme="light"] .talking b,html[data-ab-theme="light"] .join b,html[data-ab-theme="light"] .users-count-panel b,html[data-ab-theme="light"] .customLoader__text,html[data-ab-theme="light"] .customLoader__text span{color:#4f6cf7 !important;}
html[data-ab-theme="light"] .audio-chat .btn,html[data-ab-theme="light"] .audio-chat button{background:#ffffff !important; color:#161b2c !important; border:1px solid #dde3ef !important; box-shadow:0 2px 8px rgba(40,55,110,.10) !important;}
html[data-ab-theme="light"] .audio-chat .btn:hover,html[data-ab-theme="light"] .audio-chat button:hover{background:#f1f4fb !important;}
html[data-ab-theme="light"] .audio-chat .btn.checked{background:#4f6cf7 !important; color:#fff !important; border-color:transparent !important;}
html[data-ab-theme="light"] .audio-chat .btn-lg,html[data-ab-theme="light"] #searchCompanyBtn,html[data-ab-theme="light"] .scan-button{background:#4f6cf7 !important; color:#fff !important;}
html[data-ab-theme="light"] #searchCompanyBtn:hover,html[data-ab-theme="light"] .scan-button:hover{background:#3f5cf0 !important;}
html[data-ab-theme="light"] .audio-chat select,html[data-ab-theme="light"] .audio-chat input{background:#ffffff !important; color:#161b2c !important; border:1px solid #dde3ef !important;}
html[data-ab-theme="light"] .swal2-popup{background:#ffffff !important; color:#161b2c !important; border:1px solid #dde3ef !important;}
html[data-ab-theme="light"] .swal2-popup,html[data-ab-theme="light"] .swal2-title,html[data-ab-theme="light"] .swal2-html-container,html[data-ab-theme="light"] .swal2-content{color:#161b2c !important;}
html[data-ab-theme="light"] .swal2-actions .swal2-cancel{background:#f1f4fb !important; border:1px solid #dde3ef !important; color:#161b2c !important;}
`;
// (re)inject our stylesheet; self-heals if nekto's SPA drops it or head wasn't
// ready at document-start
function abEnsureStyle(){
if(document.getElementById('ab-style')) return;
try{ var s=document.createElement('style'); s.id='ab-style'; s.textContent=AB_CSS; (document.head||document.documentElement).appendChild(s); }catch(e){}
}
function abIsolate(){
try{
abEnsureStyle();
var stage=document.querySelector('.audio-chat');
if(!stage) return; // not the chat view yet — leave page intact (captcha/loading)
var prev=document.querySelectorAll('[data-ab-hidden]');
for(var i=0;i<prev.length;i++) prev[i].removeAttribute('data-ab-hidden');
var targets=[];
// keep: the chat, audio, our overlays, AND nekto's real dialogs (confirm
// "завершить" is SweetAlert2, connection errors are .mask_error) so they
// don't get hidden mid-flash
['.audio-chat','audio','[data-ab-glow]','[data-ab-srclabel]','[data-ab-ov]','[data-ab-avatar]','.swal2-container','.mask_error','.browser-not-supported-modal'].forEach(function(sel){
var l=document.querySelectorAll(sel); for(var k=0;k<l.length;k++) targets.push(l[k]);
});
// keep iframes (captcha) EXCEPT ad iframes — don't let ads pull their
// container back into view
var ifr=document.querySelectorAll('iframe');
for(var q=0;q<ifr.length;q++){ if(!ifr[q].closest('.horizontal_adv_container,.vertical_adv_container')) targets.push(ifr[q]); }
var path=new Set(), tset=new Set(targets);
for(var t=0;t<targets.length;t++){ for(var e=targets[t]; e && e!==document.documentElement; e=e.parentElement) path.add(e); }
path.forEach(function(el){
if(tset.has(el)) return; // keep the entire kept-subtree
var c=el.children; for(var j=0;j<c.length;j++){ if(!path.has(c[j])) c[j].setAttribute('data-ab-hidden','1'); }
});
}catch(e){ log('isolate err', e); }
}
// isolation + pixel theme only make sense on nekto; other sources (Discord)
// keep their native UI
if(/nekto/i.test(location.hostname)&&!isTextChat) (function abIsolateSetup(){
if(!document.body){ setTimeout(abIsolateSetup, 50); return; }
abIsolate();
try{ var to=null; new MutationObserver(function(){ if(to) return; to=setTimeout(function(){ to=null; abIsolate(); }, 150); }).observe(document.documentElement, {childList:true, subtree:true}); }catch(e){}
setInterval(abIsolate, 1500);
})();
// watchdog: robustly (re)attach capture to the current live partner track,
// no matter how nekto rebuilds the connection on "next" — fixes needing a reload
setInterval(function(){
if (!pcs.length) return;
if (/discord/i.test(location.hostname)) return; // Discord: captured via Web Audio tap, not RTC receivers
var live=null;
for (var i=pcs.length-1;i>=0 && !live;i--){
try{
var rs=pcs[i].getReceivers();
for (var j=0;j<rs.length;j++){
var t=rs[j].track;
if (t && t.kind==='audio' && t.readyState==='live'){ live=t; break; }
}
}catch(e){}
}
if (live && live!==curTrack){ partnerTrack=live; log('watchdog: re-arm on new partner'); maybeRecord(); }
}, 500);
// ---- tab-audio / Web-Audio capture: grab a page's outgoing audio and feed it
// into the bridge. Used for tab sources (YouTube/music) AND for Discord, whose
// incoming voice is routed through Web Audio (the RTC track is unreliable there).
// Excluded only on nekto, which uses the WebRTC path above.
// Two capture paths:
// (1) Web Audio output tap — intercept connections to the AudioContext
// destination and mirror them into a capture node. Covers Discord + YouTube.
// (2) media-element captureStream() — fallback for plain <video>/<audio>.
if(!/nekto|discord/i.test(location.hostname)){
try{
var _ANp = window.AudioNode && AudioNode.prototype;
if(_ANp && _ANp.connect && !_ANp.__abTap){
var _origConnect = _ANp.connect;
_ANp.connect = function(dst){
try{
if(dst && typeof AudioDestinationNode!=='undefined' && dst instanceof AudioDestinationNode){
var c=dst.context;
if(!c.__abCap){ c.__abCap=c.createMediaStreamDestination(); acTapped++;
var tt=c.__abCap.stream.getAudioTracks()[0]; if(tt) tabAudioTrack=tt; }
try{ _origConnect.call(this, c.__abCap); }catch(e){}
}
}catch(e){}
return _origConnect.apply(this, arguments);
};
_ANp.__abTap=1;
}
}catch(e){ log('webaudio tap err', e); }
setInterval(function(){
try{
if (curTrack && curTrack.readyState==='live') return; // already streaming something
// (1) Web Audio tap — best for YouTube
if (tabAudioTrack && tabAudioTrack.readyState==='live' && tabAudioTrack!==partnerTrack){
partnerTrack=tabAudioTrack; log('tab-audio armed (webaudio)'); maybeRecord(); return;
}
// (2) media-element captureStream fallback
var els=document.querySelectorAll('video,audio'), best=null;
for (var i=0;i<els.length;i++){ var e=els[i]; if(!e.paused && !e.ended && (e.currentTime||0)>0){ best=e; break; } }
if(!best) return;
var ms = best.captureStream ? best.captureStream() : (best.mozCaptureStream ? best.mozCaptureStream() : null);
var at = ms && ms.getAudioTracks && ms.getAudioTracks()[0];
if (at && at!==partnerTrack){ partnerTrack=at; log('tab-audio armed (element)'); maybeRecord(); }
}catch(e){}
}, 1500);
}
// ---- Discord: mix ALL live remote audio tracks into one stream. Discord's SFU
// exposes many receiver tracks (one per participant slot) and most are silent
// placeholders — picking a single track gives silence. Mixing them through a
// Web Audio destination yields whoever is actually talking, and that mix is fed
// into the bridge. (nekto stays on its single-track WebRTC path above.)
if(/discord/i.test(location.hostname)){
var discordDest=null;
var discordMix=function(){
try{
ensureCtx(); if(!ctx) return;
if(!discordDest) discordDest=ctx.createMediaStreamDestination();
for(var k=0;k<pcs.length;k++){
var rs; try{ rs=pcs[k].getReceivers(); }catch(e){ continue; }
for(var j=0;j<rs.length;j++){
var t=rs[j].track;
if(t && t.kind==='audio' && t.readyState==='live' && !t.__abMixed){
try{ ctx.createMediaStreamSource(new MediaStream([t])).connect(discordDest); t.__abMixed=true; log('discord: +track to mix'); }catch(e){}
}
}
}
var mt=discordDest.stream.getAudioTracks()[0];
if(mt && mt!==partnerTrack){ partnerTrack=mt; log('discord mix armed'); maybeRecord(); }
}catch(e){ log('discordMix err', e); }
};
setInterval(discordMix, 1000);
// TEMP DIAG — confirm the mix is now carrying audio
setInterval(function(){ try{ log('DISCORD-DIAG pcs='+pcs.length+' cur='+(curTrack?curTrack.readyState:'none')+' sent='+nSent+' cap='+Math.round(capPeak*1000)); }catch(e){} }, 3000);
}
setInterval(function(){ try{ if(ws&&ws.readyState===1){ ws.send(JSON.stringify({ab:1,id:myId,peer:(__AB.peer||''),partner:(curTrack?true:false),sr:(ctx?ctx.sampleRate:0),sent:nSent,recv:nRecv,sendDropped:nDrop,wsBuffered:ws.bufferedAmount||0,mix:mixStats,cap:Math.round(capPeak*1000),ply:Math.round(plyPeak*1000)})); capPeak=0; plyPeak=0; } }catch(e){} }, 1000);
// ---- auto-next: re-find a partner on disconnect / prolonged silence ----------
// Finds nekto's "next / search" button by its label and clicks it. Avoids the
// stop/finish buttons. Has a cooldown so it never machine-guns.
function abClickNext(){
try{
if(netBlocked || __turnMode==='pending'){ log('search blocked: network route not ready'); return false; }
var btns=[].slice.call(document.querySelectorAll('button,.btn,a'));
var b=btns.filter(function(x){ var r=x.getBoundingClientRect(); return r.width>0&&r.height>0; })
.find(function(x){ var t=(x.textContent||'').toLowerCase().trim();
return /следующ|искать|начать разговор|поиск|новый собеседник|next/.test(t) && !/останов|стоп|заверш|stop/.test(t); });
if(!b) b=document.querySelector('#searchCompanyBtn');
if(b){ b.click(); log('auto-next clicked: '+((b.id||b.textContent||'').trim().slice(0,28))); return true; }
log('auto-next: no button found'); return false;
}catch(e){ return false; }
}
// Also cover a manual click inside the foreign page. Programmatic/global search
// already checks netBlocked, but the operator can click nekto's own search button.
// Stop only search/next controls; reset/stop remains available.
document.addEventListener('click',function(e){
try{
if(!netBlocked && __turnMode!=='pending') return;
var b=e.target&&e.target.closest&&e.target.closest('button,.btn,a'); if(!b) return;
var t=(b.textContent||'').toLowerCase().trim();
if(b.id==='searchCompanyBtn'||/следующ|искать|начать разговор|поиск|новый собеседник|next/.test(t)){
e.preventDefault(); e.stopImmediatePropagation(); log('manual search blocked: network route not ready');
}
}catch(_){}
},true);
setInterval(function(){
if(!autoNext){ anPrevLive=false; return; }
var now=Date.now();
var live = !!(curTrack && curTrack.readyState==='live');
if(live && !anPrevLive){ anLastLoud=now; anLastLive=now; anHadPartner=true; } // new partner: reset silence clock
if(live){ anLastLive=now; anHadPartner=true; }
anPrevLive=live;
// 1) partner left: click almost immediately after the track ends.
if(anHadPartner && !live && (now-anLastLive>250)){
if(now-anLastNext < 1000) return;
if(abClickNext()){ anLastNext=now; anHadPartner=false; }
return;
}
// 2) silence skip (opt-in): connected but no sound for the chosen window
if(now-anLastNext < 6000) return; // cooldown after a silence-skip click
if(autoSilenceMs && live && (now-anLastLoud>autoSilenceMs)){
if(abClickNext()){ anLastNext=now; anLastLoud=now; }
}
}, 250);
})