// MWSE Studio — dahili yönetim arayüzü. // Miller kolonlar: Giriş → Eşler / Odalar / Cihazlar → Eylemler → Akışlar → Kalite import ColumnView from '/studio/ColumnView.js'; import { MediaSources } from '/sdk/webrtc/index.js'; export default class Studio { constructor(mwse, container) { this.mwse = mwse; this._el = typeof container === 'string' ? document.querySelector(container) : container; this._view = new ColumnView(this._el); this._devices = { cameras: [], microphones: [] }; this._statusEl = null; this._styleOK = false; this._remoteMedia = []; } async mount() { this._el.classList.add('mwse-studio'); this._injectStyle(); this._buildToolbar(); this._view.mount(); // Cihazları yükle await this._loadDevices(); // Kök kolon this._pushRootColumn(); // Gelen eşleme isteği bildirimi this.mwse.me.on('request/pair', peer => { this._setStatus('', `${peer.socketId.slice(-8)}… bağlanmak istiyor`); }); // Yeni eşleşme → gelen akışları dinle + kolon yenile this.mwse.me.on('accepted/pair', peer => { this._watchIncoming(peer); this._view.refresh(); }); this.mwse.me.on('end/pair', () => this._view.refresh()); this.mwse.on('room', () => this._view.refresh()); return this; } // ── Araç çubuğu ────────────────────────────────────────────────────────── _buildToolbar() { const bar = document.createElement('div'); bar.className = 'mwse-studio__toolbar'; const logo = document.createElement('span'); logo.className = 'mwse-studio__title'; logo.innerHTML = 'MWSE Studio'; bar.appendChild(logo); const idEl = document.createElement('span'); idEl.className = 'mwse-studio__status'; idEl.style.fontFamily = 'monospace'; idEl.textContent = '…'; this.mwse.me.on('scope', () => { idEl.textContent = this.mwse.me.socketId; }); bar.appendChild(idEl); this._statusEl = document.createElement('span'); this._statusEl.className = 'mwse-studio__status mwse-studio__status--online'; this._statusEl.textContent = 'Bağlı'; bar.appendChild(this._statusEl); // Kök'e dönme butonu const homeBtn = document.createElement('button'); homeBtn.className = 'mwse-btn'; homeBtn.style.cssText = 'margin-left:auto;padding:3px 10px;font-size:11px'; homeBtn.textContent = '⌂ Ana'; homeBtn.addEventListener('click', () => { this._view.popTo(0); this._pushRootColumn(); }); bar.appendChild(homeBtn); this._el.insertBefore(bar, this._el.firstChild); } // ── Kök kolon ───────────────────────────────────────────────────────────── _pushRootColumn() { const items = [ { icon: '●', label: 'Eşler', meta: () => { const n = this.mwse.pairs.size; return n ? `${n} aktif eşleşme` : 'Henüz eş yok'; }, onSelect: () => { this._view.popTo(1); this._pushPeersColumn(); } }, { icon: '#', label: 'Odalar', meta: () => { const n = this.mwse.rooms.size; return n ? `${n} oda` : 'Oda yok'; }, onSelect: () => { this._view.popTo(1); this._pushRoomsColumn(); } }, { icon: '◎', label: 'Kameralar', meta: () => `${this._devices.cameras.length} kamera`, onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'video'); } }, { icon: '♪', label: 'Mikrofonlar', meta: () => `${this._devices.microphones.length} mikrofon`, onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'audio'); } } ]; this._view.pushColumn('Studio', items, { searchable: false }); } // ── Eşler ───────────────────────────────────────────────────────────────── _pushPeersColumn() { const pairs = [...this.mwse.pairs.values()]; const items = pairs.map(peer => ({ icon: peer.peerConnection ? '◉' : '●', label: peer.socketId, meta: () => this._peerMeta(peer), onSelect: () => { this._view.popTo(2); this._pushPeerColumn(peer); } })); if (!items.length) { items.push({ icon: '—', label: 'Henüz eş yok', meta: 'Başka bir istemci bağlanınca görünür', hasChildren: false }); } const col = this._view.pushColumn('Eşler', items); col.addAction('ID ile ara', '', () => { const id = prompt('Eş socket ID:'); if (!id?.trim()) return; this.mwse.peer(id.trim()).requestPair() .then(() => this._setStatus('', `${id.slice(-8)}'e istek gönderildi`)) .catch(e => this._setStatus('error', e.message)); }); } _peerMeta(peer) { const streams = peer.rtc?._streams?.list() ?? []; if (streams.length) return `p2p · ${streams.length} akış aktif`; if (peer.rtc?.active) return `p2p · ${peer.rtc.connectionStatus}`; return 'websocket'; } // ── Eş eylem kolonu ─────────────────────────────────────────────────────── _pushPeerColumn(peer) { const streams = peer.rtc?._streams?.list() ?? []; const rtcOn = peer.rtc?.active; const items = [ { icon: '▶', label: 'Video + Ses', meta: 'Kamera ve mikrofon', onSelect: () => this._call(peer, 'cam+mic') }, { icon: '♪', label: 'Sesli Ara', meta: 'Yalnızca mikrofon', onSelect: () => this._call(peer, 'mic') }, { icon: '⬜', label: 'Ekran Paylaş', meta: 'getDisplayMedia', onSelect: () => this._call(peer, 'screen') }, { icon: '◎', label: 'Kamera Seç', meta: `${this._devices.cameras.length} kamera`, onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); } }, { icon: '♪', label: 'Mikrofon Seç', meta: `${this._devices.microphones.length} mikrofon`, onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'audio'); } }, { icon: '↗', label: 'Dosya Gönder', meta: 'P2P DataChannel', hasChildren: false, onSelect: () => this._sendFile(peer) } ]; if (streams.length) { items.push({ icon: '◉', label: `Aktif Akışlar (${streams.length})`, meta: streams.map(s => s.label).join(' · '), onSelect: () => { this._view.popTo(3); this._pushStreamsColumn(peer); } }); } if (rtcOn) { items.push({ icon: '↺', label: 'WebRTC Yeniden Başlat', meta: '', hasChildren: false, onSelect: () => { peer.rtc.destroy(); this._ensureRTC(peer); this._view.refresh(); } }); } items.push({ icon: '⊘', label: 'Eşleşmeyi Bitir', meta: '', hasChildren: false, onSelect: async () => { await peer.endPair().catch(() => {}); this._view.popTo(1); this._pushPeersColumn(); } }); this._view.pushColumn(peer.socketId.slice(-12), items, { searchable: false }); } // ── Odalar ─────────────────────────────────────────────────────────────── _pushRoomsColumn() { const items = [...this.mwse.rooms.entries()].map(([id, room]) => ({ icon: '#', label: id, meta: () => `${room.peers.size} üye`, onSelect: () => { this._view.popTo(2); this._pushRoomMembersColumn(room, id); } })); if (!items.length) { items.push({ icon: '—', label: 'Oda yok', meta: 'room.createRoom() ile oluştur', hasChildren: false }); } this._view.pushColumn('Odalar', items); } _pushRoomMembersColumn(room, roomId) { const items = [...room.peers.values()].map(peer => ({ icon: peer.selfSocket ? '★' : '●', label: peer.socketId, meta: () => this._peerMeta(peer), onSelect: () => { this._view.popTo(3); this._pushPeerColumn(peer); } })); if (!items.length) { items.push({ icon: '—', label: 'Üye yok', meta: '', hasChildren: false }); } this._view.pushColumn(roomId, items); } // ── Cihazlar ───────────────────────────────────────────────────────────── _pushDevicesColumn(peer, kind) { const list = kind === 'audio' ? this._devices.microphones : this._devices.cameras; const colTitle = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar'; const items = list.map(dev => ({ icon: kind === 'audio' ? '♪' : '◎', label: dev.label || `Cihaz ${dev.deviceId.slice(-6)}`, meta: dev.deviceId.slice(-8), onSelect: async () => { const constraints = kind === 'audio' ? { audio: { deviceId: { exact: dev.deviceId } } } : { video: { deviceId: { exact: dev.deviceId } } }; const getter = kind === 'audio' ? MediaSources.microphone(constraints) : MediaSources.camera(constraints); const stream = await getter.catch(e => { this._setStatus('error', e.message); return null; }); if (!stream) return; if (peer) { this._ensureRTC(peer); const label = dev.label || dev.deviceId.slice(-6); peer.rtc.addStream(label, stream); this._view.popTo(4); this._pushStreamsColumn(peer); } else { this._previewStream(stream, dev.label || colTitle); } } })); if (!items.length) { items.push({ icon: '—', label: 'Cihaz bulunamadı', meta: 'İzin ver ve sayfayı yenile', hasChildren: false }); } const col = this._view.pushColumn(colTitle, items); // İzin isteme butonu col.addAction('İzin İste', '', async () => { await navigator.mediaDevices.getUserMedia( kind === 'audio' ? { audio: true } : { video: true } ).catch(() => {}); await this._loadDevices(); this._view.popTo(this._view.depth - 1); this._pushDevicesColumn(peer, kind); }); } // ── Akışlar ────────────────────────────────────────────────────────────── _pushStreamsColumn(peer) { const srcs = peer.rtc?._streams?.list() ?? []; const items = srcs.map(src => ({ icon: src.tracks[0]?.kind === 'video' ? '▶' : '♪', label: src.label, meta: src.tracks.map(t => `${t.kind}${t.enabled ? '' : ' (sessiz)'}`).join(' + '), onSelect: () => { this._view.popTo(4); this._pushQualityColumn(peer, src.label, src); } })); if (!items.length) { items.push({ icon: '—', label: 'Akış yok', meta: '', hasChildren: false }); } this._view.pushColumn('Akışlar', items); } // ── Kalite / kontrol ───────────────────────────────────────────────────── _pushQualityColumn(peer, label, src) { const presets = [ { icon: '↑', label: 'Yüksek', meta: '1080p · 4 Mbps', params: { maxBitrate: 4_000_000, scaleResolutionDownBy: 1 } }, { icon: '—', label: 'Orta', meta: '720p · 1.5 Mbps', params: { maxBitrate: 1_500_000, scaleResolutionDownBy: 1.5 } }, { icon: '↓', label: 'Düşük', meta: '480p · 500 Kbps', params: { maxBitrate: 500_000, scaleResolutionDownBy: 2 } }, ]; const items = presets.map(p => ({ icon: p.icon, label: p.label, meta: p.meta, hasChildren: false, onSelect: () => peer.rtc?.setEncodings(label, 'video', p.params) })); for (const track of (src.tracks ?? [])) { items.push({ icon: track.enabled ? '⊙' : '○', label: `${track.kind === 'video' ? 'Video' : 'Ses'} ${track.enabled ? 'Sustur' : 'Aç'}`, meta: '', hasChildren: false, onSelect: () => { peer.rtc?.setEnabled(label, track.kind, !track.enabled); this._view.refresh(); } }); } items.push({ icon: '✕', label: 'Akışı Durdur', meta: '', hasChildren: false, onSelect: () => { peer.rtc?.removeStream(label); this._view.popTo(3); this._pushStreamsColumn(peer); } }); this._view.pushColumn('Kalite', items, { searchable: false }); } // ── WebRTC yardımcıları ─────────────────────────────────────────────────── async _call(peer, type) { this._ensureRTC(peer); let stream; try { if (type === 'cam+mic') stream = await MediaSources.cameraAndMic(); else if (type === 'mic') stream = await MediaSources.microphone(); else if (type === 'screen') stream = await MediaSources.screen(); } catch (e) { this._setStatus('error', e.message); return; } peer.rtc.addStream(type, stream); this._setStatus('online', `${type} → ${peer.socketId.slice(-8)}`); this._view.popTo(3); this._pushStreamsColumn(peer); } _sendFile(peer) { this._ensureRTC(peer); const input = document.createElement('input'); input.type = 'file'; input.addEventListener('change', async () => { const file = input.files?.[0]; if (!file) return; this._setStatus('', `${file.name} gönderiliyor…`); peer.rtc.files.on('progress', ({ sent, total }) => this._setStatus('', `${file.name} %${Math.round(sent / total * 100)}`) ); await peer.rtc.sendFile(file).catch(e => this._setStatus('error', e.message)); this._setStatus('online', `${file.name} gönderildi`); }); input.click(); } _ensureRTC(peer) { if (peer.rtc?._pc) return; const polite = this.mwse.me.socketId < peer.socketId; peer.rtc.connect({ polite }); } // Gelen track'leri otomatik çal _watchIncoming(peer) { peer.rtc.on('track', track => { if (track.kind === 'audio') { const audio = new Audio(); audio.autoplay = true; audio.srcObject = new MediaStream([track]); document.body.appendChild(audio); this._remoteMedia.push(audio); this._setStatus('online', `← ${peer.socketId.slice(-8)} ses gönderdi`); } else { this._setStatus('online', `← ${peer.socketId.slice(-8)} video gönderdi`); } }); } // Cihaz test önizlemesi (floating video) _previewStream(stream, label) { const old = document.getElementById('mwse-preview'); if (old) old.remove(); const wrap = document.createElement('div'); wrap.id = 'mwse-preview'; Object.assign(wrap.style, { position: 'fixed', bottom: '12px', right: '12px', width: '220px', background: '#111', border: '1px solid #333', borderRadius: '6px', overflow: 'hidden', zIndex: '9000' }); const bar = document.createElement('div'); Object.assign(bar.style, { padding: '4px 8px', fontSize: '11px', color: '#aaa', background: '#1a1a1a', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }); const closeBtn = document.createElement('span'); closeBtn.textContent = '✕'; closeBtn.style.cursor = 'pointer'; closeBtn.addEventListener('click', () => { stream.getTracks().forEach(t => t.stop()); wrap.remove(); }); bar.append(document.createTextNode(label), closeBtn); const video = document.createElement('video'); video.autoplay = true; video.muted = true; video.playsInline = true; video.srcObject = stream; video.style.cssText = 'width:100%;display:block;background:#000'; wrap.append(bar, video); document.body.appendChild(wrap); } // ── Cihaz yükleme ───────────────────────────────────────────────────────── async _loadDevices() { try { this._devices = await MediaSources.devices(); } catch (_) { this._devices = { cameras: [], microphones: [] }; } } // ── Durum çubuğu ───────────────────────────────────────────────────────── _setStatus(cls, text) { if (!this._statusEl) return; this._statusEl.className = [ 'mwse-studio__status', cls ? `mwse-studio__status--${cls}` : '' ].join(' ').trim(); this._statusEl.textContent = text; } _injectStyle() { if (this._styleOK) return; const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/studio/style.css'; document.head.appendChild(link); this._styleOK = true; } }