WebRTC sinyal fix + reaktif eşler kolonu + anlık disconnect

sdk/Peer.js — :rtcpack: her zaman WebSocket üzerinden:
  - RTC bağlandıktan sonra renegotiasyon (yeni stream ekleme) sırasında
    ICE adayları DataChannel'a yönlendiriliyordu → bağlantı kurulamıyordu
  - forceWS = pack.type === ':rtcpack:' → yönlendirme mantığını atlar,
    her koşulda WebSocket kullanır
  - Signalingi writable flag da engellemez (rtcpack her zaman geçer)

sdk/index.js — peer/disconnect tam işleme:
  - pairs.delete(id) eklendi (kopan eş pairs'ten çıkar)
  - me.emit('peer/disconnect', peer) eklendi (Studio dinleyebilsin)

public/studio/Studio.js — reaktif eşler kolonu:
  - _peersCol referansı: _pushPeersColumn'da saklanır
  - _rebuildPeerItems(): mwse.pairs'i okuyup Column.setItems() çağırır
    → kolon her zaman anında güncellenir (tıklama gerekmez)
  - Olaylar: accepted/pair + end/pair + peer/disconnect → _rebuildPeerItems()
  - Kabul eden taraf: _pushPeersColumn yoksa aç, varsa rebuild
  - Disconnect status bar'da kırmızı hata mesajı

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
abdussamedulutas 2026-06-17 14:35:33 +03:00
parent f5565f5df0
commit d468c95adf
3 changed files with 65 additions and 26 deletions

View File

@ -7,16 +7,17 @@ export default class Studio {
this.mwse = mwse; this.mwse = mwse;
this._el = typeof container === 'string' this._el = typeof container === 'string'
? document.querySelector(container) : container; ? document.querySelector(container) : container;
this._view = null; // mount() içinde oluşturulur this._view = null;
this._devices = { cameras: [], microphones: [] }; this._devices = { cameras: [], microphones: [] };
this._statusEl = null; this._statusEl = null;
this._notifArea = null; this._notifArea = null;
this._myIPEl = null; this._myIPEl = null;
this._myUUIDEl = null; this._myUUIDEl = null;
this._localGrid = null; // "Gönderiyorum" tile grid'i this._localGrid = null;
this._remoteGrid = null; // "Geliyor" tile grid'i this._remoteGrid = null;
this._streamsPanel = null; this._streamsPanel = null;
this._styleOK = false; this._styleOK = false;
this._peersCol = null; // reaktif güncelleme için referans
} }
async mount() { async mount() {
@ -51,18 +52,29 @@ export default class Studio {
// Gelen eşleme isteği → bildirim banner // Gelen eşleme isteği → bildirim banner
this.mwse.me.on('request/pair', peer => this._showPairRequest(peer)); this.mwse.me.on('request/pair', peer => this._showPairRequest(peer));
// Eşleşme onaylandı // Eşleşme onaylandı (istek gönderen taraf)
this.mwse.me.on('accepted/pair', peer => { this.mwse.me.on('accepted/pair', peer => {
this._watchIncoming(peer); this._watchIncoming(peer);
if (this.mwse.virtualPressure.APIPAddress) { if (this.mwse.virtualPressure.APIPAddress) {
this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress); this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress);
} }
this._view.refresh(); this._rebuildPeerItems();
this._setStatus('online', `${this._peerLabel(peer)} eşleşmesi kuruldu`); this._setStatus('online', `${this._peerLabel(peer)} eşleşmesi kuruldu`);
}); });
this.mwse.me.on('end/pair', () => this._view.refresh()); // Eşleşme bitti (karşı taraf koptu veya endPair çağrıldı)
this.mwse.on('room', () => this._view.refresh()); this.mwse.me.on('end/pair', (from) => {
this._rebuildPeerItems();
this._setStatus('', `${typeof from === 'string' ? from.slice(-8) : ''} ayrıldı`);
});
// WebSocket bağlantısı kopunca (sayfa yenileme, ağ kesilmesi)
this.mwse.me.on('peer/disconnect', peer => {
this._rebuildPeerItems();
this._setStatus('error', `${this._peerLabel(peer)} bağlantısı kesildi`);
});
this.mwse.on('room', () => this._view.refresh());
return this; return this;
} }
@ -347,8 +359,10 @@ export default class Studio {
if (this.mwse.virtualPressure.APIPAddress) { if (this.mwse.virtualPressure.APIPAddress) {
this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress); this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress);
} }
this._view.popTo(1); // Eşler kolonunu aç ve anında doldur
this._pushPeersColumn(); if (this._view.depth < 2) this._view.popTo(1);
if (!this._peersCol) this._pushPeersColumn();
else this._rebuildPeerItems();
this._setStatus('online', `${this._peerLabel(peer)} kabul edildi`); this._setStatus('online', `${this._peerLabel(peer)} kabul edildi`);
} else { } else {
this._setStatus('error', 'Eşleşme kurulamadı'); this._setStatus('error', 'Eşleşme kurulamadı');
@ -406,7 +420,8 @@ export default class Studio {
}); });
} }
const col = this._view.pushColumn('Eşler', items); this._peersCol = this._view.pushColumn('Eşler', items);
const col = this._peersCol;
col.addAction('<span class="mi mi-sm">person_search</span> ID ile ara', '', () => { col.addAction('<span class="mi mi-sm">person_search</span> ID ile ara', '', () => {
this._showModal({ this._showModal({
title: 'Eşe bağlan', title: 'Eşe bağlan',
@ -428,6 +443,26 @@ export default class Studio {
return peer.info?.info?.ip || peer.socketId.slice(-8); return peer.info?.info?.ip || peer.socketId.slice(-8);
} }
// Eşler kolonunu mevcut mwse.pairs verisiyle canlı olarak günceller.
// Kolon henüz açılmamışsa no-op.
_rebuildPeerItems() {
if (!this._peersCol) return;
const pairs = [...this.mwse.pairs.values()];
const items = pairs.map(peer => ({
icon: peer.rtc?.active ? 'sensors' : 'person',
label: this._peerLabel(peer),
meta: () => this._peerMeta(peer),
onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); }
}));
if (!items.length) {
items.push({
icon: 'person_off', label: 'Henüz eş yok',
meta: '"ID ile ara" butonunu kullan', hasChildren: false
});
}
this._peersCol.setItems(items);
}
_peerMeta(peer) { _peerMeta(peer) {
const streams = peer.rtc?._streams?.list() ?? []; const streams = peer.rtc?._streams?.list() ?? [];
const id = peer.socketId.slice(-8); const id = peer.socketId.slice(-8);

View File

@ -164,31 +164,31 @@ export default class Peer extends MWSEEventTarget {
} }
async send(pack) { async send(pack) {
const p2pOpen = this.peerConnection && this.rtc?.active;
const serverOpen = this.mwse.server.connected; const serverOpen = this.mwse.server.connected;
let channel; // WebRTC signaling (:rtcpack:) MUST always travel via the WebSocket relay —
if (p2pOpen && serverOpen) { // never over the DataChannel. This is true even after the p2p connection is
channel = this.primaryChannel === 'websocket' ? 'websocket' : 'datachannel'; // established (renegotiation, new-stream ICE candidates, etc.). The DataChannel
} else if (serverOpen) { // does not exist yet when the first offer/answer exchange happens, and may not
channel = 'websocket'; // be the right path for out-of-band signaling later either.
} else { const forceWS = pack.type === ':rtcpack:';
channel = 'datachannel';
} const p2pOpen = !forceWS && this.peerConnection && this.rtc?.active;
const channel = (p2pOpen && serverOpen)
? (this.primaryChannel === 'websocket' ? 'websocket' : 'datachannel')
: 'websocket';
if (channel === 'websocket') { if (channel === 'websocket') {
if (!this.mwse.writable) { if (!serverOpen) return;
if (!this.mwse.writable && !forceWS) {
console.warn('MWSE: socket is not writable'); console.warn('MWSE: socket is not writable');
return; return;
} }
// WOM — no waiter registered; the engine returns nil for pack/to (#33). // WOM — no waiter registered; the engine returns nil for pack/to (#33).
this.mwse.EventPooling.only({ type: 'pack/to', pack, to: this.socketId }); this.mwse.EventPooling.only({ type: 'pack/to', pack, to: this.socketId });
} else { } else {
if (pack.type !== ':rtcpack:') { this.rtc?.sendMessage(pack);
this.rtc?.sendMessage(pack);
} else {
console.warn('MWSE: cannot send :rtcpack: over data channel');
}
} }
} }

View File

@ -234,7 +234,11 @@ export default class MWSE {
}); });
ep.signal('peer/disconnect', ({ id }) => { ep.signal('peer/disconnect', ({ id }) => {
this.peer(id, true).emit('disconnect'); const peer = this.peer(id, true);
this.pairs.delete(id);
peer.emit('disconnect');
// me üzerinden de yay, Studio dinleyebilsin.
this.peer('me').emit('peer/disconnect', peer);
}); });
ep.signal('accepted/pair', ({ from, info }) => { ep.signal('accepted/pair', ({ from, info }) => {