Загрузка данных


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);
})