diff --git a/public/studio/Studio.js b/public/studio/Studio.js index ba9d66b..1914e8e 100644 --- a/public/studio/Studio.js +++ b/public/studio/Studio.js @@ -7,16 +7,17 @@ export default class Studio { this.mwse = mwse; this._el = typeof container === 'string' ? document.querySelector(container) : container; - this._view = null; // mount() içinde oluşturulur + this._view = null; this._devices = { cameras: [], microphones: [] }; this._statusEl = null; this._notifArea = null; this._myIPEl = null; this._myUUIDEl = null; - this._localGrid = null; // "Gönderiyorum" tile grid'i - this._remoteGrid = null; // "Geliyor" tile grid'i + this._localGrid = null; + this._remoteGrid = null; this._streamsPanel = null; this._styleOK = false; + this._peersCol = null; // reaktif güncelleme için referans } async mount() { @@ -51,18 +52,29 @@ export default class Studio { // Gelen eşleme isteği → bildirim banner 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._watchIncoming(peer); if (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.mwse.me.on('end/pair', () => this._view.refresh()); - this.mwse.on('room', () => this._view.refresh()); + // Eşleşme bitti (karşı taraf koptu veya endPair çağrıldı) + 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; } @@ -347,8 +359,10 @@ export default class Studio { if (this.mwse.virtualPressure.APIPAddress) { this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress); } - this._view.popTo(1); - this._pushPeersColumn(); + // Eşler kolonunu aç ve anında doldur + 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`); } else { 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('person_search ID ile ara', '', () => { this._showModal({ title: 'Eşe bağlan', @@ -428,6 +443,26 @@ export default class Studio { 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) { const streams = peer.rtc?._streams?.list() ?? []; const id = peer.socketId.slice(-8); diff --git a/sdk/Peer.js b/sdk/Peer.js index ce69470..9be9b66 100644 --- a/sdk/Peer.js +++ b/sdk/Peer.js @@ -164,31 +164,31 @@ export default class Peer extends MWSEEventTarget { } async send(pack) { - const p2pOpen = this.peerConnection && this.rtc?.active; const serverOpen = this.mwse.server.connected; - let channel; - if (p2pOpen && serverOpen) { - channel = this.primaryChannel === 'websocket' ? 'websocket' : 'datachannel'; - } else if (serverOpen) { - channel = 'websocket'; - } else { - channel = 'datachannel'; - } + // WebRTC signaling (:rtcpack:) MUST always travel via the WebSocket relay — + // never over the DataChannel. This is true even after the p2p connection is + // established (renegotiation, new-stream ICE candidates, etc.). The DataChannel + // does not exist yet when the first offer/answer exchange happens, and may not + // be the right path for out-of-band signaling later either. + const forceWS = pack.type === ':rtcpack:'; + + const p2pOpen = !forceWS && this.peerConnection && this.rtc?.active; + + const channel = (p2pOpen && serverOpen) + ? (this.primaryChannel === 'websocket' ? 'websocket' : 'datachannel') + : 'websocket'; if (channel === 'websocket') { - if (!this.mwse.writable) { + if (!serverOpen) return; + if (!this.mwse.writable && !forceWS) { console.warn('MWSE: socket is not writable'); return; } // WOM — no waiter registered; the engine returns nil for pack/to (#33). this.mwse.EventPooling.only({ type: 'pack/to', pack, to: this.socketId }); } else { - if (pack.type !== ':rtcpack:') { - this.rtc?.sendMessage(pack); - } else { - console.warn('MWSE: cannot send :rtcpack: over data channel'); - } + this.rtc?.sendMessage(pack); } } diff --git a/sdk/index.js b/sdk/index.js index a97e7ad..255501b 100644 --- a/sdk/index.js +++ b/sdk/index.js @@ -234,7 +234,11 @@ export default class MWSE { }); 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 }) => {