Studio bağlantı fix + tam WebRTC/cihaz yönetimi

sdk/index.js — bağlantı hatası düzeltildi:
  - new MWSE() options=undefined → TypeError patlaması
  - constructor(options) → opts = { endpoint:'auto', ...options }
  - Artık new MWSE() / new MWSE('ws://host') / new MWSE({…}) hepsi çalışıyor

public/studio/Studio.js — tamamen yeniden yazıldı:
  Eşler kolonu:
    - mwse.pairs'ten gerçek eşleri gösteriyor
    - Her eş için: Video+Ses / Sesli Ara / Ekran Paylaş / Kamera Seç /
      Mikrofon Seç / Dosya Gönder / Aktif Akışlar / Eşleşmeyi Bitir
    - "ID ile ara" → prompt ile direkt peer ID girerek requestPair()
  Odalar kolonu: mwse.rooms'tan oda + üye listesi
  Cihazlar kolonu:
    - MediaSources.devices() ile kamera ve mikrofon listesi
    - Her cihaza tıkla → seçili eşe akış başlat veya önizle (floating video)
    - İzin İste butonu → izin alındıktan sonra listeyi yenile
  Akışlar kolonu: peer.rtc._streams.list() canlı gösterimi
  Kalite kolonu: Yüksek/Orta/Düşük preset + mute/stop
  Araç çubuğu: socket ID, durum mesajı, Ana butonu (popTo(0))
  Gelen track'ler: audio otomatik çalınır, status bar bildirir
  Cihaz önizleme: floating video element (✕ ile kapatılır)

public/studio/app.js: studio.mount() await ile çağrılıyor

go test -race ./... — yeşil

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
abdussamedulutas 2026-06-17 13:22:03 +03:00
parent 764644176c
commit 66158b1f74
3 changed files with 423 additions and 215 deletions

View File

@ -1,204 +1,370 @@
// MWSE Studio — desktop-first Miller-column UI. // MWSE Studio — dahili yönetim arayüzü.
// // Miller kolonlar: Giriş → Eşler / Odalar / Cihazlar → Eylemler → Akışlar → Kalite
// Hierarchy: Network → Groups → Peers → Devices → Streams → Quality
//
// Usage:
// import Studio from '/studio/Studio.js';
// const studio = new Studio(mwse, '#app');
// studio.mount();
//
// The Studio is purely additive: it renders into the given container and
// never modifies the MWSE SDK state except through the public SDK API.
import ColumnView from '/studio/ColumnView.js'; import ColumnView from '/studio/ColumnView.js';
import { MediaSources } from '/sdk/webrtc/index.js'; import { MediaSources } from '/sdk/webrtc/index.js';
export default class Studio { export default class Studio {
constructor(mwse, container) { constructor(mwse, container) {
this.mwse = mwse; this.mwse = mwse;
this._el = typeof container === 'string'
this._container = typeof container === 'string' ? document.querySelector(container) : container;
? document.querySelector(container) this._view = new ColumnView(this._el);
: container; this._devices = { cameras: [], microphones: [] };
this._statusEl = null;
this._view = new ColumnView(this._container); this._styleOK = false;
this._styleInjected = false; this._remoteMedia = [];
} }
// Mount the Studio UI inside the container element. async mount() {
mount() { this._el.classList.add('mwse-studio');
this._injectStyle(); this._injectStyle();
this._container.classList.add('mwse-studio'); this._buildToolbar();
// Toolbar
const toolbar = document.createElement('div');
toolbar.className = 'mwse-studio__toolbar';
const title = document.createElement('span');
title.className = 'mwse-studio__title';
title.textContent = 'MWSE Studio';
toolbar.appendChild(title);
this._statusEl = document.createElement('span');
this._statusEl.className = 'mwse-studio__status';
toolbar.appendChild(this._statusEl);
this._container.appendChild(toolbar);
this.mwse.on('scope', () => this._setStatus('online', 'Connected'));
this.mwse.on('close', () => this._setStatus('', 'Disconnected'));
this.mwse.on('error', e => this._setStatus('error', e.message));
this._view.mount(); this._view.mount();
this._pushNetworkColumn();
// 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; return this;
} }
// ---- Column builders ------------------------------------------------ // ── Araç çubuğu ──────────────────────────────────────────────────────────
_pushNetworkColumn() { _buildToolbar() {
const items = [{ const bar = document.createElement('div');
icon: '◉', bar.className = 'mwse-studio__toolbar';
label: 'Network',
meta: () => `${this.mwse.peers.size} peers online`,
onSelect: () => {
this._view.popTo(1);
this._pushGroupsColumn();
}
}];
this._view.pushColumn('Studio', items, { searchable: false }); const logo = document.createElement('span');
} logo.className = 'mwse-studio__title';
logo.innerHTML = 'MWSE <span style="color:#0078d4">Studio</span>';
bar.appendChild(logo);
_pushGroupsColumn() { const idEl = document.createElement('span');
const items = []; 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);
for (const [id, room] of this.mwse.rooms) { this._statusEl = document.createElement('span');
items.push({ this._statusEl.className = 'mwse-studio__status mwse-studio__status--online';
icon: '#', this._statusEl.textContent = 'Bağlı';
label: id, bar.appendChild(this._statusEl);
meta: () => `${room.peers.size} peers`,
onSelect: () => { // Kök'e dönme butonu
this._view.popTo(2); const homeBtn = document.createElement('button');
this._pushPeersColumn(room); 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);
} }
if (items.length === 0) { // ── Kök kolon ─────────────────────────────────────────────────────────────
items.push({ icon: '—', label: 'No groups', meta: 'Join a room first', hasChildren: false });
}
this._view.pushColumn('Groups', items); _pushRootColumn() {
}
_pushPeersColumn(room) {
const items = [];
for (const [, peer] of room.peers) {
items.push({
icon: peer.selfSocket ? '★' : '●',
label: peer.socketId,
meta: () => peer.peerConnection ? 'p2p' : 'ws',
onSelect: () => {
this._view.popTo(3);
this._pushDevicesColumn(peer);
}
});
}
if (items.length === 0) {
items.push({ icon: '—', label: 'No peers in room', meta: '', hasChildren: false });
}
this._view.pushColumn('Peers', items);
}
_pushDevicesColumn(peer) {
const items = [ const items = [
{ {
icon: '◎', icon: '●',
label: 'Camera', label: 'Eşler',
meta: 'Capture video', meta: () => {
onSelect: () => this._openCamera(peer) 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: '●', icon: '#',
label: 'Camera + Mic', label: 'Odalar',
meta: 'Video + audio', meta: () => {
onSelect: () => this._openCameraAndMic(peer) 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: '♪', icon: '♪',
label: 'Microphone', label: 'Mikrofonlar',
meta: 'Audio only', meta: () => `${this._devices.microphones.length} mikrofon`,
onSelect: () => this._openMicrophone(peer) 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: '⬜', icon: '⬜',
label: 'Screen', label: 'Ekran Paylaş',
meta: 'Share display', meta: 'getDisplayMedia',
onSelect: () => this._openScreen(peer) 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: '↗', icon: '↗',
label: 'Send file', label: 'Dosya Gönder',
meta: 'DataChannel transfer', meta: 'P2P DataChannel',
hasChildren: false, hasChildren: false,
onSelect: () => this._openFilePicker(peer) onSelect: () => this._sendFile(peer)
} }
]; ];
const col = this._view.pushColumn('Devices', items); if (streams.length) {
items.push({
// Show already-active streams at the bottom of devices. icon: '◉',
if (peer.rtc?._streams) { label: `Aktif Akışlar (${streams.length})`,
const active = peer.rtc._streams.list(); meta: streams.map(s => s.label).join(' · '),
if (active.length) { onSelect: () => { this._view.popTo(3); this._pushStreamsColumn(peer); }
col.addAction('View streams', '', () => {
this._view.popTo(4);
this._pushActiveStreamsColumn(peer);
}); });
} }
}
if (rtcOn) {
items.push({
icon: '↺',
label: 'WebRTC Yeniden Başlat',
meta: '',
hasChildren: false,
onSelect: () => { peer.rtc.destroy(); this._ensureRTC(peer); this._view.refresh(); }
});
} }
_pushActiveStreamsColumn(peer) { 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 srcs = peer.rtc?._streams?.list() ?? [];
const items = srcs.map(src => ({ const items = srcs.map(src => ({
icon: src.tracks[0]?.kind === 'video' ? '▶' : '♪', icon: src.tracks[0]?.kind === 'video' ? '▶' : '♪',
label: src.label, label: src.label,
meta: src.tracks.map(t => t.kind).join(' + '), meta: src.tracks.map(t => `${t.kind}${t.enabled ? '' : ' (sessiz)'}`).join(' + '),
onSelect: () => { onSelect: () => { this._view.popTo(4); this._pushQualityColumn(peer, src.label, src); }
this._view.popTo(5);
this._pushQualityColumn(peer, src.label, src);
}
})); }));
this._view.pushColumn('Streams', items);
if (!items.length) {
items.push({ icon: '—', label: 'Akış yok', meta: '', hasChildren: false });
} }
this._view.pushColumn('Akışlar', items);
}
// ── Kalite / kontrol ─────────────────────────────────────────────────────
_pushQualityColumn(peer, label, src) { _pushQualityColumn(peer, label, src) {
const presets = [ const presets = [
{ icon: '↑', label: 'High', meta: '1080p · 4 Mbps', params: { maxBitrate: 4_000_000, scaleResolutionDownBy: 1 } }, { icon: '↑', label: 'Yüksek', meta: '1080p · 4 Mbps', params: { maxBitrate: 4_000_000, scaleResolutionDownBy: 1 } },
{ icon: '—', label: 'Medium', meta: '720p · 1.5 Mbps', params: { maxBitrate: 1_500_000, scaleResolutionDownBy: 1.5 } }, { icon: '—', label: 'Orta', meta: '720p · 1.5 Mbps', params: { maxBitrate: 1_500_000, scaleResolutionDownBy: 1.5 } },
{ icon: '↓', label: 'Low', meta: '480p · 500 Kbps', params: { maxBitrate: 500_000, scaleResolutionDownBy: 2 } }, { icon: '↓', label: 'Düşük', meta: '480p · 500 Kbps', params: { maxBitrate: 500_000, scaleResolutionDownBy: 2 } },
]; ];
const items = presets.map(p => ({ const items = presets.map(p => ({
icon: p.icon, icon: p.icon, label: p.label, meta: p.meta, hasChildren: false,
label: p.label,
meta: p.meta,
hasChildren: false,
onSelect: () => peer.rtc?.setEncodings(label, 'video', p.params) onSelect: () => peer.rtc?.setEncodings(label, 'video', p.params)
})); }));
// Mute / stop controls.
for (const track of (src.tracks ?? [])) { for (const track of (src.tracks ?? [])) {
items.push({ items.push({
icon: track.enabled ? '⊙' : '○', icon: track.enabled ? '⊙' : '○',
label: `${track.enabled ? 'Mute' : 'Unmute'} ${track.kind}`, label: `${track.kind === 'video' ? 'Video' : 'Ses'} ${track.enabled ? 'Sustur' : 'Aç'}`,
meta: '', meta: '', hasChildren: false,
hasChildren: false,
onSelect: () => { onSelect: () => {
peer.rtc?.setEnabled(label, track.kind, !track.enabled); peer.rtc?.setEnabled(label, track.kind, !track.enabled);
this._view.refresh(); this._view.refresh();
@ -207,102 +373,138 @@ export default class Studio {
} }
items.push({ items.push({
icon: '✕', icon: '✕', label: 'Akışı Durdur', meta: '', hasChildren: false,
label: 'Stop stream',
meta: '',
hasChildren: false,
onSelect: () => { onSelect: () => {
peer.rtc?.removeStream(label); peer.rtc?.removeStream(label);
this._view.popTo(4); this._view.popTo(3);
this._pushStreamsColumn(peer);
} }
}); });
this._view.pushColumn('Quality', items); this._view.pushColumn('Kalite', items, { searchable: false });
} }
// ---- Device helpers ------------------------------------------------- // ── WebRTC yardımcıları ───────────────────────────────────────────────────
async _openCamera(peer) { async _call(peer, type) {
const stream = await MediaSources.camera().catch(e => { this._setStatus('error', e.message); return null; });
if (!stream) return;
this._ensureRTC(peer); this._ensureRTC(peer);
peer.rtc.addStream('camera', stream); let stream;
this._view.popTo(4); try {
this._pushActiveStreamsColumn(peer); 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);
} }
async _openCameraAndMic(peer) { _sendFile(peer) {
const stream = await MediaSources.cameraAndMic().catch(e => { this._setStatus('error', e.message); return null; });
if (!stream) return;
this._ensureRTC(peer); this._ensureRTC(peer);
peer.rtc.addStream('cam+mic', stream);
this._view.popTo(4);
this._pushActiveStreamsColumn(peer);
}
async _openMicrophone(peer) {
const stream = await MediaSources.microphone().catch(e => { this._setStatus('error', e.message); return null; });
if (!stream) return;
this._ensureRTC(peer);
peer.rtc.addStream('mic', stream);
this._view.popTo(4);
this._pushActiveStreamsColumn(peer);
}
async _openScreen(peer) {
const stream = await MediaSources.screen().catch(e => { this._setStatus('error', e.message); return null; });
if (!stream) return;
this._ensureRTC(peer);
peer.rtc.addStream('screen', stream);
this._view.popTo(4);
this._pushActiveStreamsColumn(peer);
}
_openFilePicker(peer) {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.addEventListener('change', async () => { input.addEventListener('change', async () => {
const file = input.files?.[0]; const file = input.files?.[0];
if (!file) return; if (!file) return;
this._ensureRTC(peer); this._setStatus('', `${file.name} gönderiliyor…`);
this._setStatus('', `Sending ${file.name}`); peer.rtc.files.on('progress', ({ sent, total }) =>
peer.rtc.files.on('progress', ({ sent, total }) => { this._setStatus('', `${file.name} %${Math.round(sent / total * 100)}`)
const pct = Math.round(sent / total * 100); );
this._setStatus('', `${file.name}${pct}%`);
});
await peer.rtc.sendFile(file).catch(e => this._setStatus('error', e.message)); await peer.rtc.sendFile(file).catch(e => this._setStatus('error', e.message));
this._setStatus('online', `${file.name} sent`); this._setStatus('online', `${file.name} gönderildi`);
}); });
input.click(); input.click();
} }
// ---- RTCEngine management -------------------------------------------
// Ensure the peer has an RTCEngine connected (creates one if absent).
_ensureRTC(peer) { _ensureRTC(peer) {
if (peer.rtc.active) return; if (peer.rtc?._pc) return;
const polite = this.mwse.me.socketId < peer.socketId;
// Determine politeness by lexicographic socket ID comparison.
const myId = this.mwse.me.socketId;
const polite = myId < peer.socketId;
peer.rtc.connect({ polite }); peer.rtc.connect({ polite });
} }
// ---- Helpers -------------------------------------------------------- // 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) { _setStatus(cls, text) {
if (!this._statusEl) return; if (!this._statusEl) return;
this._statusEl.className = `mwse-studio__status${cls ? ` mwse-studio__status--${cls}` : ''}`; this._statusEl.className = [
'mwse-studio__status',
cls ? `mwse-studio__status--${cls}` : ''
].join(' ').trim();
this._statusEl.textContent = text; this._statusEl.textContent = text;
} }
_injectStyle() { _injectStyle() {
if (this._styleInjected) return; if (this._styleOK) return;
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.href = new URL('./style.css', import.meta.url).href; link.href = '/studio/style.css';
document.head.appendChild(link); document.head.appendChild(link);
this._styleInjected = true; this._styleOK = true;
} }
} }

View File

@ -1,15 +1,16 @@
import MWSE from '/sdk/index.js'; import MWSE from '/sdk/index.js';
import Studio from '/studio/Studio.js'; import Studio from '/studio/Studio.js';
const mwse = new MWSE();
const studio = new Studio(mwse, document.getElementById('app'));
const loadingEl = document.getElementById('loading'); const loadingEl = document.getElementById('loading');
const loadingMsg = document.getElementById('loading-msg'); const loadingMsg = document.getElementById('loading-msg');
const appEl = document.getElementById('app');
mwse.on('scope', () => { const mwse = new MWSE(); // endpoint: otomatik — aynı sunucu
const studio = new Studio(mwse, appEl);
mwse.on('scope', async () => {
loadingEl.classList.add('hidden'); loadingEl.classList.add('hidden');
studio.mount(); await studio.mount();
}); });
mwse.on('close', () => { mwse.on('close', () => {
@ -18,6 +19,7 @@ mwse.on('close', () => {
}); });
mwse.on('error', err => { mwse.on('error', err => {
// Versiyon uyuşmazlığı veya hello timeout gibi hatalar burada görünür
loadingMsg.textContent = `Hata: ${err.message}`; loadingMsg.textContent = `Hata: ${err.message}`;
loadingEl.classList.remove('hidden'); loadingEl.classList.remove('hidden');
}); });

View File

@ -35,7 +35,11 @@ export default class MWSE {
this._events = {}; this._events = {};
this.activeScope = false; this.activeScope = false;
this.server = new Connection(this, options); // Default endpoint to 'auto' (SDK reads import.meta.url → same origin).
const opts = typeof options === 'string'
? { endpoint: options }
: { endpoint: 'auto', ...options };
this.server = new Connection(this, opts);
this.WSTSProtocol = new WSTSProtocol(this); this.WSTSProtocol = new WSTSProtocol(this);
this.EventPooling = new EventPool(this); this.EventPooling = new EventPool(this);
this.virtualPressure = new IPPressure(this); this.virtualPressure = new IPPressure(this);