758 lines
33 KiB
JavaScript
758 lines
33 KiB
JavaScript
// MWSE Studio — dahili yönetim arayüzü.
|
||
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 = null; // mount() içinde oluşturulur
|
||
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._streamsPanel = null;
|
||
this._styleOK = false;
|
||
}
|
||
|
||
async mount() {
|
||
this._el.classList.add('mwse-studio');
|
||
this._injectStyle();
|
||
this._buildToolbar();
|
||
this._buildNotifArea();
|
||
|
||
// Ana alan: kolonlar (sol) + akış monitörü (sağ)
|
||
const mainArea = document.createElement('div');
|
||
mainArea.className = 'mwse-studio__main';
|
||
this._el.appendChild(mainArea);
|
||
|
||
this._view = new ColumnView(mainArea);
|
||
this._view.mount();
|
||
|
||
this._streamsPanel = this._buildStreamsPanel(mainArea);
|
||
|
||
await this._loadDevices();
|
||
this._pushRootColumn();
|
||
|
||
// Sanal IP al
|
||
this.mwse.scope(async () => {
|
||
try {
|
||
const ip = await this.mwse.virtualPressure.allocAPIPAddress();
|
||
this.mwse.me.info.set('ip', ip);
|
||
if (this._myIPEl) this._myIPEl.textContent = ip;
|
||
if (this._myUUIDEl) this._myUUIDEl.textContent = this.mwse.me.socketId.slice(-8);
|
||
} catch (_) {}
|
||
});
|
||
|
||
// Gelen eşleme isteği → bildirim banner
|
||
this.mwse.me.on('request/pair', peer => this._showPairRequest(peer));
|
||
|
||
// Eşleşme onaylandı
|
||
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._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());
|
||
|
||
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 <span style="color:#0078d4">Studio</span>';
|
||
bar.appendChild(logo);
|
||
|
||
// ID kartı: sadece IP + kısa UUID + kopyala
|
||
const idCard = document.createElement('div');
|
||
idCard.className = 'mwse-id-card';
|
||
idCard.title = 'Socket ID kopyala (bağlantı paylaşımı için)';
|
||
|
||
this._myIPEl = document.createElement('span');
|
||
this._myIPEl.className = 'mwse-id-card__ip';
|
||
this._myIPEl.textContent = '●'; // IP gelmeden önce nokta
|
||
|
||
this._myUUIDEl = document.createElement('span');
|
||
this._myUUIDEl.className = 'mwse-id-card__value';
|
||
this._myUUIDEl.textContent = '…';
|
||
|
||
const copyIcon = document.createElement('span');
|
||
copyIcon.className = 'mwse-id-card__copy';
|
||
copyIcon.textContent = '⎘';
|
||
|
||
idCard.append(this._myIPEl, this._myUUIDEl, copyIcon);
|
||
idCard.addEventListener('click', () => {
|
||
const id = this.mwse.me.socketId;
|
||
if (!id) return;
|
||
navigator.clipboard.writeText(id).then(() => {
|
||
copyIcon.textContent = '✓';
|
||
idCard.classList.add('mwse-id-card--copied');
|
||
setTimeout(() => {
|
||
idCard.classList.remove('mwse-id-card--copied');
|
||
copyIcon.textContent = '⎘';
|
||
}, 2000);
|
||
});
|
||
});
|
||
bar.appendChild(idCard);
|
||
|
||
this._statusEl = document.createElement('span');
|
||
this._statusEl.className = 'mwse-studio__status mwse-studio__status--online';
|
||
this._statusEl.innerHTML = '<span class="mi mi-sm">wifi</span> Bağlı';
|
||
bar.appendChild(this._statusEl);
|
||
|
||
// Canlı saat
|
||
const clock = document.createElement('span');
|
||
clock.className = 'mwse-studio__clock';
|
||
const tick = () => {
|
||
const now = new Date();
|
||
clock.textContent = now.toTimeString().slice(0, 8);
|
||
};
|
||
tick(); setInterval(tick, 1000);
|
||
bar.appendChild(clock);
|
||
|
||
this._el.insertBefore(bar, this._el.firstChild);
|
||
}
|
||
|
||
// ── Akış monitör paneli ───────────────────────────────────────────────────
|
||
|
||
_buildStreamsPanel(parent) {
|
||
const panel = document.createElement('div');
|
||
panel.className = 'mwse-streams-panel';
|
||
panel.style.display = 'none'; // akış yokken gizli
|
||
|
||
// Gönderiyorum bölümü
|
||
const outSec = document.createElement('div');
|
||
outSec.className = 'mwse-streams-section';
|
||
const outTitle = document.createElement('div');
|
||
outTitle.className = 'mwse-streams-section__title';
|
||
outTitle.textContent = '▲ Gönderiyorum';
|
||
this._localGrid = document.createElement('div');
|
||
this._localGrid.className = 'mwse-streams-grid';
|
||
outSec.append(outTitle, this._localGrid);
|
||
|
||
// Geliyor bölümü
|
||
const inSec = document.createElement('div');
|
||
inSec.className = 'mwse-streams-section';
|
||
const inTitle = document.createElement('div');
|
||
inTitle.className = 'mwse-streams-section__title';
|
||
inTitle.textContent = '▼ Geliyor';
|
||
this._remoteGrid = document.createElement('div');
|
||
this._remoteGrid.className = 'mwse-streams-grid';
|
||
inSec.append(inTitle, this._remoteGrid);
|
||
|
||
panel.append(outSec, inSec);
|
||
parent.appendChild(panel);
|
||
return panel;
|
||
}
|
||
|
||
// Yerel (gönderilen) akış tile'ı ekle
|
||
_addLocalTile(label, stream, peerLabel) {
|
||
const hasVideo = stream.getVideoTracks().length > 0;
|
||
const tile = this._makeTile(
|
||
label,
|
||
`→ ${peerLabel}`,
|
||
stream,
|
||
hasVideo,
|
||
true, // muted (geri besleme yok)
|
||
() => tile.remove() && this._updatePanelVisibility()
|
||
);
|
||
this._localGrid.appendChild(tile);
|
||
this._updatePanelVisibility();
|
||
}
|
||
|
||
// Uzak (gelen) track tile'ı → ses kontrollü
|
||
_addRemoteTile(track, streams, peerLabel) {
|
||
const isVideo = track.kind === 'video';
|
||
const stream = streams?.[0] ?? new MediaStream([track]);
|
||
|
||
const tile = document.createElement('div');
|
||
tile.className = `mwse-stream-tile${isVideo ? '' : ' mwse-stream-tile--audio'}`;
|
||
|
||
// Media element
|
||
let mediaEl;
|
||
if (isVideo) {
|
||
mediaEl = document.createElement('video');
|
||
mediaEl.autoplay = true;
|
||
mediaEl.playsInline = true;
|
||
mediaEl.muted = false;
|
||
mediaEl.srcObject = stream;
|
||
mediaEl.className = 'mwse-stream-tile__video';
|
||
tile.appendChild(mediaEl);
|
||
} else {
|
||
// Ses: görünür tile + gizli <audio> elemanı
|
||
mediaEl = document.createElement('audio');
|
||
mediaEl.autoplay = true;
|
||
mediaEl.muted = false;
|
||
mediaEl.srcObject = stream;
|
||
document.body.appendChild(mediaEl);
|
||
|
||
const icon = document.createElement('div');
|
||
icon.className = 'mwse-stream-tile__audio-icon';
|
||
icon.textContent = 'graphic_eq';
|
||
tile.appendChild(icon);
|
||
}
|
||
|
||
// Alt bilgi çubuğu: etiket + ses kıs/aç + kapat
|
||
const info = document.createElement('div');
|
||
info.className = 'mwse-stream-tile__info';
|
||
|
||
const lbl = document.createElement('span');
|
||
lbl.className = 'mwse-stream-tile__label';
|
||
lbl.textContent = isVideo ? 'Video' : 'Ses';
|
||
|
||
const peerEl = document.createElement('span');
|
||
peerEl.className = 'mwse-stream-tile__peer';
|
||
peerEl.textContent = `← ${peerLabel}`;
|
||
|
||
// Ses kısma butonu
|
||
const muteBtn = document.createElement('span');
|
||
muteBtn.className = 'mwse-stream-tile__mute';
|
||
muteBtn.textContent = 'volume_up';
|
||
muteBtn.title = 'Sesi kıs / aç';
|
||
let muted = false;
|
||
muteBtn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
muted = !muted;
|
||
mediaEl.muted = muted;
|
||
muteBtn.textContent = muted ? 'volume_off' : 'volume_up';
|
||
muteBtn.classList.toggle('mwse-stream-tile__mute--off', muted);
|
||
});
|
||
|
||
const closeBtn = document.createElement('span');
|
||
closeBtn.className = 'mwse-stream-tile__close';
|
||
closeBtn.textContent = 'close';
|
||
closeBtn.title = 'Tile kapat';
|
||
closeBtn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
if (!isVideo) { mediaEl.srcObject = null; mediaEl.remove(); }
|
||
tile.remove();
|
||
this._updatePanelVisibility();
|
||
});
|
||
|
||
info.append(lbl, peerEl, muteBtn, closeBtn);
|
||
tile.appendChild(info);
|
||
this._remoteGrid.appendChild(tile);
|
||
this._updatePanelVisibility();
|
||
|
||
// Track kapandığında tile'ı kaldır
|
||
track.addEventListener('ended', () => {
|
||
if (!isVideo) { mediaEl.srcObject = null; mediaEl.remove(); }
|
||
tile.remove();
|
||
this._updatePanelVisibility();
|
||
});
|
||
}
|
||
|
||
_makeTile(label, peerInfo, stream, isVideo, muted, onClose) {
|
||
const tile = document.createElement('div');
|
||
tile.className = `mwse-stream-tile${isVideo ? '' : ' mwse-stream-tile--audio'}`;
|
||
|
||
if (isVideo) {
|
||
const video = document.createElement('video');
|
||
video.autoplay = true;
|
||
video.playsInline = true;
|
||
video.muted = muted;
|
||
video.srcObject = stream;
|
||
video.className = 'mwse-stream-tile__video';
|
||
tile.appendChild(video);
|
||
} else {
|
||
const icon = document.createElement('div');
|
||
icon.className = 'mwse-stream-tile__audio-icon';
|
||
icon.textContent = '♪';
|
||
tile.appendChild(icon);
|
||
}
|
||
|
||
const info = document.createElement('div');
|
||
info.className = 'mwse-stream-tile__info';
|
||
|
||
const lbl = document.createElement('span');
|
||
lbl.className = 'mwse-stream-tile__label';
|
||
lbl.textContent = label;
|
||
|
||
const peer = document.createElement('span');
|
||
peer.className = 'mwse-stream-tile__peer';
|
||
peer.textContent = peerInfo;
|
||
|
||
const closeBtn = document.createElement('span');
|
||
closeBtn.className = 'mwse-stream-tile__close';
|
||
closeBtn.textContent = '✕';
|
||
closeBtn.title = 'Kapat';
|
||
closeBtn.addEventListener('click', e => { e.stopPropagation(); onClose(); });
|
||
|
||
info.append(lbl, peer, closeBtn);
|
||
tile.appendChild(info);
|
||
return tile;
|
||
}
|
||
|
||
_updatePanelVisibility() {
|
||
const hasLocal = this._localGrid.childElementCount > 0;
|
||
const hasRemote = this._remoteGrid.childElementCount > 0;
|
||
this._streamsPanel.style.display = (hasLocal || hasRemote) ? '' : 'none';
|
||
}
|
||
|
||
// ── Gelen istek bildirim alanı ────────────────────────────────────────────
|
||
|
||
_buildNotifArea() {
|
||
this._notifArea = document.createElement('div');
|
||
this._notifArea.className = 'mwse-notif-area';
|
||
const toolbar = this._el.querySelector('.mwse-studio__toolbar');
|
||
toolbar.insertAdjacentElement('afterend', this._notifArea);
|
||
}
|
||
|
||
_showPairRequest(peer) {
|
||
const bar = document.createElement('div');
|
||
bar.className = 'mwse-notif-bar';
|
||
|
||
const msg = document.createElement('div');
|
||
msg.className = 'mwse-notif-bar__msg';
|
||
msg.innerHTML = `<span class="mwse-notif-bar__dot">●</span>`
|
||
+ ` <code>${peer.socketId}</code> bağlanmak istiyor`;
|
||
|
||
const actions = document.createElement('div');
|
||
actions.className = 'mwse-notif-bar__actions';
|
||
|
||
const rejectBtn = document.createElement('button');
|
||
rejectBtn.className = 'mwse-btn mwse-btn--danger';
|
||
rejectBtn.textContent = 'Reddet';
|
||
rejectBtn.addEventListener('click', async () => {
|
||
await peer.rejectPair().catch(() => {});
|
||
bar.remove();
|
||
this._setStatus('', `${peer.socketId.slice(-8)} reddedildi`);
|
||
});
|
||
|
||
const acceptBtn = document.createElement('button');
|
||
acceptBtn.className = 'mwse-btn mwse-btn--primary';
|
||
acceptBtn.textContent = 'Kabul Et';
|
||
acceptBtn.addEventListener('click', async () => {
|
||
acceptBtn.textContent = '…';
|
||
acceptBtn.disabled = true;
|
||
const ok = await peer.acceptPair().catch(() => false);
|
||
bar.remove();
|
||
if (ok) {
|
||
this._watchIncoming(peer);
|
||
if (this.mwse.virtualPressure.APIPAddress) {
|
||
this.mwse.me.info.set('ip', this.mwse.virtualPressure.APIPAddress);
|
||
}
|
||
this._view.popTo(1);
|
||
this._pushPeersColumn();
|
||
this._setStatus('online', `${this._peerLabel(peer)} kabul edildi`);
|
||
} else {
|
||
this._setStatus('error', 'Eşleşme kurulamadı');
|
||
}
|
||
});
|
||
|
||
actions.append(rejectBtn, acceptBtn);
|
||
bar.append(msg, actions);
|
||
this._notifArea.appendChild(bar);
|
||
}
|
||
|
||
// ── Kök kolon ─────────────────────────────────────────────────────────────
|
||
|
||
_pushRootColumn() {
|
||
const items = [
|
||
{
|
||
icon: 'people', label: 'Eşler',
|
||
meta: () => { const n = this.mwse.pairs.size; return n ? `${n} eşleşme` : 'Henüz yok'; },
|
||
onSelect: () => { this._view.popTo(1); this._pushPeersColumn(); }
|
||
},
|
||
{
|
||
icon: 'meeting_room', label: 'Odalar',
|
||
meta: () => { const n = this.mwse.rooms.size; return n ? `${n} oda` : 'Oda yok'; },
|
||
onSelect: () => { this._view.popTo(1); this._pushRoomsColumn(); }
|
||
},
|
||
{
|
||
icon: 'videocam', label: 'Kameralar',
|
||
meta: () => `${this._devices.cameras.length} kamera`,
|
||
onSelect: () => { this._view.popTo(1); this._pushDevicesColumn(null, 'video'); }
|
||
},
|
||
{
|
||
icon: 'mic', 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 kolonu ──────────────────────────────────────────────────────────
|
||
|
||
_pushPeersColumn() {
|
||
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
|
||
});
|
||
}
|
||
|
||
const col = this._view.pushColumn('Eşler', items);
|
||
col.addAction('<span class="mi mi-sm">person_search</span> ID ile ara', '', () => {
|
||
this._showModal({
|
||
title: 'Eşe bağlan',
|
||
fields: [{ key: 'id', label: 'Socket ID', placeholder: 'xxxxxxxx-xxxx-…' }],
|
||
confirm: 'Bağlan',
|
||
onConfirm: ({ id }) => {
|
||
if (!id) return;
|
||
this.mwse.peer(id).requestPair()
|
||
.then(ok => ok
|
||
? this._setStatus('', `${id.slice(-8)}'e istek gönderildi`)
|
||
: this._setStatus('error', 'İstek gönderilemedi'))
|
||
.catch(e => this._setStatus('error', e.message));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
_peerLabel(peer) {
|
||
return peer.info?.info?.ip || peer.socketId.slice(-8);
|
||
}
|
||
|
||
_peerMeta(peer) {
|
||
const streams = peer.rtc?._streams?.list() ?? [];
|
||
const id = peer.socketId.slice(-8);
|
||
if (streams.length) return `${id} · p2p · ${streams.length} akış`;
|
||
if (peer.rtc?.active) return `${id} · p2p`;
|
||
return `${id} · websocket`;
|
||
}
|
||
|
||
// ── Eş eylem kolonu ───────────────────────────────────────────────────────
|
||
|
||
_pushPeerColumn(peer) {
|
||
const streams = peer.rtc?._streams?.list() ?? [];
|
||
|
||
const items = [
|
||
{ icon: 'videocam', label: 'Video + Ses', meta: 'Kamera ve mikrofon', onSelect: () => this._call(peer, 'cam+mic') },
|
||
{ icon: 'mic', label: 'Sesli Ara', meta: 'Yalnızca mikrofon', onSelect: () => this._call(peer, 'mic') },
|
||
{ icon: 'screen_share', label: 'Ekran Paylaş', meta: 'getDisplayMedia', onSelect: () => this._call(peer, 'screen') },
|
||
{ icon: 'switch_video', label: 'Kamera Seç', meta: `${this._devices.cameras.length} kamera`,
|
||
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'video'); } },
|
||
{ icon: 'settings_voice', label: 'Mikrofon Seç', meta: `${this._devices.microphones.length} mikrofon`,
|
||
onSelect: () => { this._view.popTo(3); this._pushDevicesColumn(peer, 'audio'); } },
|
||
{ icon: 'upload_file', label: 'Dosya Gönder', meta: 'P2P DataChannel', hasChildren: false,
|
||
onSelect: () => this._sendFile(peer) }
|
||
];
|
||
|
||
if (streams.length) {
|
||
items.push({
|
||
icon: 'live_tv', label: `Aktif Akışlar (${streams.length})`,
|
||
meta: streams.map(s => s.label).join(' · '),
|
||
onSelect: () => { this._view.popTo(3); this._pushStreamsColumn(peer); }
|
||
});
|
||
}
|
||
|
||
items.push({ icon: 'link_off', label: 'Eşleşmeyi Bitir', meta: '', hasChildren: false,
|
||
onSelect: async () => {
|
||
await peer.endPair().catch(() => {});
|
||
this._view.popTo(1); this._pushPeersColumn();
|
||
}
|
||
});
|
||
|
||
this._view.pushColumn(this._peerLabel(peer), items, { searchable: false });
|
||
}
|
||
|
||
// ── Odalar kolonu ─────────────────────────────────────────────────────────
|
||
|
||
_pushRoomsColumn() {
|
||
const items = [...this.mwse.rooms.entries()].map(([, room]) => ({
|
||
icon: 'meeting_room',
|
||
label: room.config?.name ?? room.roomId,
|
||
meta: () => `${room.peers.size} üye`,
|
||
onSelect: () => { this._view.popTo(2); this._pushRoomMembersColumn(room); }
|
||
}));
|
||
|
||
if (!items.length) items.push({ icon: 'block', label: 'Oda yok', meta: '"Oda Oluştur" butonunu kullan', hasChildren: false });
|
||
|
||
const col = this._view.pushColumn('Odalar', items);
|
||
col.addAction('<span class="mi mi-sm">add</span> Oda Oluştur', 'mwse-btn--primary', () => {
|
||
this._showModal({
|
||
title: 'Yeni Oda',
|
||
fields: [
|
||
{ key: 'name', label: 'Oda adı', placeholder: 'genel' },
|
||
{ key: 'desc', label: 'Açıklama (opsiyonel)', placeholder: 'Genel sohbet odası' },
|
||
{ key: 'pass', label: 'Şifre (opsiyonel)', placeholder: 'boş = şifresiz' }
|
||
],
|
||
confirm: 'Oluştur',
|
||
onConfirm: async ({ name, desc, pass }) => {
|
||
if (!name) return;
|
||
const room = this.mwse.room({
|
||
name, description: desc || name,
|
||
joinType: pass ? 'password' : 'free',
|
||
accessType: 'public', ifexistsJoin: true,
|
||
notifyActionJoined: true, notifyActionEjected: true, notifyActionInvite: false,
|
||
...(pass ? { password: pass } : {})
|
||
});
|
||
await room.createRoom();
|
||
this._setStatus('online', `"${name}" oluşturuldu`);
|
||
this._view.popTo(1); this._pushRoomsColumn();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
_pushRoomMembersColumn(room) {
|
||
const myIP = this.mwse.virtualPressure.APIPAddress;
|
||
const items = [...room.peers.values()].map(peer => ({
|
||
icon: peer.selfSocket ? 'star' : 'person',
|
||
label: peer.selfSocket ? (myIP || 'Ben') : this._peerLabel(peer),
|
||
meta: () => this._peerMeta(peer),
|
||
onSelect: () => { this._view.popTo(3); this._pushPeerColumn(peer); }
|
||
}));
|
||
|
||
if (!items.length) items.push({ icon: '—', label: 'Üye yok', meta: '', hasChildren: false });
|
||
|
||
const col = this._view.pushColumn(room.config?.name ?? room.roomId, items);
|
||
col.addAction('<span class="mi mi-sm">exit_to_app</span> Çık', 'mwse-btn--danger', async () => {
|
||
await room.eject().catch(() => {});
|
||
this._view.popTo(1); this._pushRoomsColumn();
|
||
});
|
||
}
|
||
|
||
// ── Cihazlar kolonu ───────────────────────────────────────────────────────
|
||
|
||
_pushDevicesColumn(peer, kind) {
|
||
const list = kind === 'audio' ? this._devices.microphones : this._devices.cameras;
|
||
const title = kind === 'audio' ? 'Mikrofonlar' : 'Kameralar';
|
||
|
||
const items = list.map(dev => ({
|
||
icon: kind === 'audio' ? 'mic' : 'videocam',
|
||
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 stream = await (kind === 'audio'
|
||
? MediaSources.microphone(constraints)
|
||
: MediaSources.camera(constraints)
|
||
).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);
|
||
if (peer.rtc._streams?.has(label)) peer.rtc.removeStream(label);
|
||
peer.rtc.addStream(label, stream);
|
||
this._addLocalTile(label, stream, this._peerLabel(peer));
|
||
this._view.popTo(4); this._pushStreamsColumn(peer);
|
||
} else {
|
||
this._previewStream(stream, dev.label || title);
|
||
}
|
||
}
|
||
}));
|
||
|
||
if (!items.length) items.push({ icon: 'perm_media', label: 'Cihaz bulunamadı', meta: '"İzin İste" butonunu kullan', hasChildren: false });
|
||
|
||
const col = this._view.pushColumn(title, items);
|
||
col.addAction('<span class="mi mi-sm">lock_open</span> İ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 / Kalite kolonları ────────────────────────────────────────────
|
||
|
||
_pushStreamsColumn(peer) {
|
||
const srcs = peer.rtc?._streams?.list() ?? [];
|
||
const items = srcs.map(src => ({
|
||
icon: src.tracks[0]?.kind === 'video' ? 'live_tv' : 'graphic_eq',
|
||
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);
|
||
}
|
||
|
||
_pushQualityColumn(peer, label, src) {
|
||
const presets = [
|
||
{ icon: 'hd', label: 'Yüksek', meta: '1080p · 4 Mbps', params: { maxBitrate: 4_000_000, scaleResolutionDownBy: 1 } },
|
||
{ icon: 'sd', label: 'Orta', meta: '720p · 1.5 Mbps', params: { maxBitrate: 1_500_000, scaleResolutionDownBy: 1.5 } },
|
||
{ icon: 'signal_cellular_1_bar', 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
|
||
? (track.kind === 'video' ? 'videocam_off' : 'mic_off')
|
||
: (track.kind === 'video' ? 'videocam' : 'mic'),
|
||
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: 'stop_circle', 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; }
|
||
|
||
if (peer.rtc._streams?.has(type)) peer.rtc.removeStream(type);
|
||
peer.rtc.addStream(type, stream);
|
||
this._addLocalTile(type, stream, this._peerLabel(peer));
|
||
this._setStatus('online', `${type} → ${this._peerLabel(peer)}`);
|
||
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;
|
||
peer.rtc.connect({ polite: this.mwse.me.socketId < peer.socketId });
|
||
}
|
||
|
||
// Gelen track'leri izle → panel'e ekle
|
||
_watchIncoming(peer) {
|
||
// RTC başlatılmazsa gelen :rtcpack: sinyalleri receive() içinde _neg=null
|
||
// nedeniyle sessizce düşer, track olayı hiç ateşlenmez.
|
||
this._ensureRTC(peer);
|
||
|
||
peer.rtc.on('track', (track, streams) => {
|
||
this._addRemoteTile(track, streams, this._peerLabel(peer));
|
||
this._setStatus('online', `← ${this._peerLabel(peer)} ${track.kind}`);
|
||
});
|
||
}
|
||
|
||
_previewStream(stream, label) {
|
||
document.getElementById('mwse-preview')?.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 close = document.createElement('span');
|
||
close.textContent = '✕'; close.style.cursor = 'pointer';
|
||
close.addEventListener('click', () => { stream.getTracks().forEach(t => t.stop()); wrap.remove(); });
|
||
bar.append(document.createTextNode(label), close);
|
||
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);
|
||
}
|
||
|
||
// ── Modal ─────────────────────────────────────────────────────────────────
|
||
|
||
_showModal({ title, fields = [], confirm = 'Tamam', onConfirm }) {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'mwse-modal-overlay';
|
||
const modal = document.createElement('div');
|
||
modal.className = 'mwse-modal';
|
||
const header = document.createElement('div');
|
||
header.className = 'mwse-modal__header';
|
||
const titleEl = document.createElement('span');
|
||
titleEl.textContent = title;
|
||
const closeX = document.createElement('span');
|
||
closeX.className = 'mwse-modal__close'; closeX.textContent = '✕';
|
||
closeX.addEventListener('click', () => overlay.remove());
|
||
header.append(titleEl, closeX);
|
||
const body = document.createElement('div');
|
||
body.className = 'mwse-modal__body';
|
||
const inputs = {};
|
||
for (const f of fields) {
|
||
const wrap = document.createElement('div');
|
||
wrap.className = 'mwse-modal__field';
|
||
const lbl = document.createElement('label'); lbl.textContent = f.label;
|
||
const inp = document.createElement('input');
|
||
inp.className = 'mwse-modal__input'; inp.placeholder = f.placeholder ?? ''; inp.type = f.type ?? 'text';
|
||
inputs[f.key] = inp;
|
||
inp.addEventListener('keydown', e => { if (e.key==='Enter') confirmBtn.click(); if (e.key==='Escape') overlay.remove(); });
|
||
wrap.append(lbl, inp); body.appendChild(wrap);
|
||
}
|
||
const footer = document.createElement('div');
|
||
footer.className = 'mwse-modal__footer';
|
||
const cancelBtn = document.createElement('button');
|
||
cancelBtn.className = 'mwse-btn'; cancelBtn.textContent = 'İptal';
|
||
cancelBtn.addEventListener('click', () => overlay.remove());
|
||
const confirmBtn = document.createElement('button');
|
||
confirmBtn.className = 'mwse-btn mwse-btn--primary'; confirmBtn.textContent = confirm;
|
||
confirmBtn.addEventListener('click', () => {
|
||
const values = {};
|
||
for (const [k, el] of Object.entries(inputs)) values[k] = el.value.trim();
|
||
overlay.remove(); onConfirm(values);
|
||
});
|
||
footer.append(cancelBtn, confirmBtn);
|
||
modal.append(header, body, footer);
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
|
||
setTimeout(() => Object.values(inputs)[0]?.focus(), 50);
|
||
}
|
||
|
||
// ── Yardımcılar ──────────────────────────────────────────────────────────
|
||
|
||
async _loadDevices() {
|
||
try { this._devices = await MediaSources.devices(); }
|
||
catch (_) { this._devices = { cameras: [], microphones: [] }; }
|
||
}
|
||
|
||
_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;
|
||
}
|
||
}
|