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:
parent
764644176c
commit
66158b1f74
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue