fix
This commit is contained in:
1 parent
ee9cffc66d
commit
143dd26c31
3 files changed
+222
-80
No files matched your search
+175
-73
@@ -1073,19 +1073,22 @@ function stopScreenShare(stream, silent) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// position sync
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fans new joiners out from directly in front of the room's center: the
|
||||
// first joiner lands straight ahead, then each next one alternates sides,
|
||||
// widening by 10deg per pair (0, +10, -10, +20, -20, ...) instead of the
|
||||
// old distance-maximizing grid search, which tended to dump joiners in a
|
||||
// far corner rather than somewhere natural to face the group.
|
||||
function chooseOpenParticipantPosition() {
|
||||
const occupied = [...S.peers.values()].map((peer) => ({ x: peer.x, y: peer.y }));
|
||||
const candidates = [];
|
||||
for (let row = 0; row < 5; row++) {
|
||||
for (let col = 0; col < 7; col++) {
|
||||
candidates.push({ x: 15 + col * 11.7, y: 15 + row * 17.5 });
|
||||
}
|
||||
}
|
||||
if (!occupied.length) return { x: 50, y: 50 };
|
||||
return candidates.sort((a, b) => {
|
||||
const distance = (point) => Math.min(...occupied.map((other) => Math.hypot(point.x - other.x, point.y - other.y)));
|
||||
return distance(b) - distance(a);
|
||||
})[0];
|
||||
const ANCHOR = { x: 50, y: 50 };
|
||||
const RADIUS = 24;
|
||||
const n = S.peers.size;
|
||||
const step = Math.ceil(n / 2) * 10;
|
||||
const angleDeg = n === 0 ? 0 : (n % 2 === 1 ? step : -step);
|
||||
const rad = (angleDeg * Math.PI) / 180;
|
||||
return {
|
||||
x: ANCHOR.x + RADIUS * Math.sin(rad),
|
||||
y: ANCHOR.y - RADIUS * Math.cos(rad),
|
||||
};
|
||||
}
|
||||
|
||||
let lastPosSend = 0;
|
||||
@@ -1326,6 +1329,7 @@ function renderTiles() {
|
||||
|
||||
// share keys: 'me' or peer id; popped-out shares render in their own window
|
||||
const poppedWindows = new Map(); // key -> {win, name}
|
||||
let focusedShareKey = null; // clicked remote share, rendered large with the rest as a filmstrip
|
||||
|
||||
function activeShares() {
|
||||
const shares = [];
|
||||
@@ -1342,80 +1346,164 @@ function activeShares() {
|
||||
return shares;
|
||||
}
|
||||
|
||||
// Picks the column/row count (out of `count` tiles) that best fills a
|
||||
// width x height area assuming ~16:9 tiles, so multiple streams can be
|
||||
// watched at once with as little wasted space as possible.
|
||||
function bestGrid(count, width, height) {
|
||||
const aspect = 16 / 9;
|
||||
let best = { cols: count, rows: 1, area: 0 };
|
||||
for (let cols = 1; cols <= count; cols++) {
|
||||
const rows = Math.ceil(count / cols);
|
||||
let cellW = width / cols;
|
||||
let cellH = height / rows;
|
||||
if (cellW / cellH > aspect) cellW = cellH * aspect; else cellH = cellW / aspect;
|
||||
const area = cellW * cellH;
|
||||
if (area > best.area) best = { cols, rows, area };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Re-measures the live grid and updates its column/row count only — avoids
|
||||
// renderScreens()'s full teardown, which would restart <video> playback.
|
||||
function applyGridLayout() {
|
||||
const body = els.screens?.querySelector('.screens-grid');
|
||||
const count = body?.children.length;
|
||||
if (!body || !count) return;
|
||||
const rect = body.getBoundingClientRect();
|
||||
const { cols, rows } = bestGrid(count, rect.width || 800, rect.height || 450);
|
||||
body.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
|
||||
body.style.gridTemplateRows = `repeat(${rows}, 1fr)`;
|
||||
}
|
||||
|
||||
let screensResizeObserver = null;
|
||||
function ensureScreensResizeObserver() {
|
||||
if (screensResizeObserver || !els.screens || typeof ResizeObserver === 'undefined') return;
|
||||
let raf = null;
|
||||
screensResizeObserver = new ResizeObserver(() => {
|
||||
if (raf) return;
|
||||
raf = requestAnimationFrame(() => { raf = null; applyGridLayout(); });
|
||||
});
|
||||
screensResizeObserver.observe(els.screens);
|
||||
}
|
||||
|
||||
function buildPoppedPlaceholder(share) {
|
||||
const ph = document.createElement('div');
|
||||
ph.className = 'screen-card popped';
|
||||
ph.dataset.shareKey = share.key;
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'pop-btn';
|
||||
btn.appendChild(mIcon('open_in_new'));
|
||||
btn.appendChild(document.createTextNode(' re-embed'));
|
||||
btn.onclick = () => closePopout(share.key);
|
||||
const label = document.createElement('div');
|
||||
label.className = 'label';
|
||||
label.textContent = `${share.name} — popped out`;
|
||||
ph.append(btn, label);
|
||||
return ph;
|
||||
}
|
||||
|
||||
function buildShareCard(share, focused) {
|
||||
if (poppedWindows.has(share.key)) return buildPoppedPlaceholder(share);
|
||||
const card = document.createElement('div');
|
||||
card.className = 'screen-card' + (focused ? ' focused' : '');
|
||||
card.dataset.shareKey = share.key;
|
||||
card.title = focused ? 'Click to exit spotlight' : 'Click to watch this stream large';
|
||||
const video = document.createElement('video');
|
||||
video.autoplay = true;
|
||||
video.playsInline = true;
|
||||
video.muted = share.local;
|
||||
video.srcObject = share.stream;
|
||||
video.play().catch(() => {});
|
||||
const label = document.createElement('div');
|
||||
label.className = 'label';
|
||||
label.innerHTML = '<span class="live">●</span>';
|
||||
label.appendChild(document.createTextNode(share.name));
|
||||
const micBadge = document.createElement('span');
|
||||
micBadge.className = 'tile-mic-badge';
|
||||
micBadge.appendChild(mIcon('mic_off'));
|
||||
label.appendChild(micBadge);
|
||||
const pop = document.createElement('button');
|
||||
pop.className = 'pop-btn';
|
||||
pop.title = 'Pop out in new window';
|
||||
pop.appendChild(mIcon('open_in_new'));
|
||||
pop.onclick = (e) => { e.stopPropagation(); popOutShare(share); };
|
||||
card.append(video, label, pop);
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('button')) return;
|
||||
focusedShareKey = focusedShareKey === share.key ? null : share.key;
|
||||
renderScreens();
|
||||
});
|
||||
return card;
|
||||
}
|
||||
|
||||
function buildOwnPill(share) {
|
||||
const pill = document.createElement('div');
|
||||
pill.className = 'own-pill';
|
||||
pill.dataset.shareKey = share.key;
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'live';
|
||||
dot.innerHTML = '●';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'own-pill-label';
|
||||
label.textContent = 'You are sharing';
|
||||
const stop = document.createElement('button');
|
||||
stop.className = 'own-pill-close';
|
||||
stop.title = 'Stop screen share';
|
||||
stop.appendChild(mIcon('close'));
|
||||
stop.onclick = () => stopScreenShare(share.stream);
|
||||
pill.append(dot, label, stop);
|
||||
return pill;
|
||||
}
|
||||
|
||||
function renderScreens() {
|
||||
const shares = activeShares();
|
||||
// prune popouts whose share ended
|
||||
const live = new Set(activeShares().map((s) => s.key));
|
||||
const live = new Set(shares.map((s) => s.key));
|
||||
for (const [key, p] of [...poppedWindows]) {
|
||||
if (!live.has(key)) {
|
||||
try { p.win.close(); } catch {}
|
||||
poppedWindows.delete(key);
|
||||
}
|
||||
}
|
||||
if (focusedShareKey && !live.has(focusedShareKey)) focusedShareKey = null;
|
||||
|
||||
if (!els.screens) return;
|
||||
|
||||
const ownShares = shares.filter((s) => s.local);
|
||||
const remoteShares = shares.filter((s) => !s.local);
|
||||
|
||||
els.screens.innerHTML = '';
|
||||
els.screens.classList.toggle('multi', activeShares().length > 1);
|
||||
for (const share of activeShares()) {
|
||||
if (poppedWindows.has(share.key)) {
|
||||
// rendered in its own window — show a small placeholder card to re-embed
|
||||
const ph = document.createElement('div');
|
||||
ph.className = 'screen-card popped';
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'pop-btn';
|
||||
btn.appendChild(mIcon('open_in_new'));
|
||||
btn.appendChild(document.createTextNode(' re-embed'));
|
||||
btn.onclick = () => closePopout(share.key);
|
||||
const label = document.createElement('div');
|
||||
label.className = 'label';
|
||||
label.textContent = `${share.name} — popped out`;
|
||||
ph.append(btn, label);
|
||||
if (share.local) {
|
||||
const stop = document.createElement('button');
|
||||
stop.className = 'pop-btn stop-share-btn';
|
||||
stop.title = 'Stop this screen share';
|
||||
stop.appendChild(mIcon('stop_circle'));
|
||||
stop.appendChild(document.createTextNode(' stop'));
|
||||
stop.onclick = () => stopScreenShare(share.stream);
|
||||
ph.appendChild(stop);
|
||||
}
|
||||
els.screens.appendChild(ph);
|
||||
continue;
|
||||
}
|
||||
const card = document.createElement('div');
|
||||
card.className = 'screen-card';
|
||||
card.dataset.shareKey = share.key;
|
||||
const video = document.createElement('video');
|
||||
video.autoplay = true;
|
||||
video.playsInline = true;
|
||||
video.muted = share.local;
|
||||
video.srcObject = share.stream;
|
||||
video.play().catch(() => {});
|
||||
const label = document.createElement('div');
|
||||
label.className = 'label';
|
||||
label.innerHTML = '<span class="live">●</span>';
|
||||
label.appendChild(document.createTextNode(share.name));
|
||||
const micBadge = document.createElement('span');
|
||||
micBadge.className = 'tile-mic-badge';
|
||||
micBadge.appendChild(mIcon('mic_off'));
|
||||
label.appendChild(micBadge);
|
||||
const pop = document.createElement('button');
|
||||
pop.className = 'pop-btn';
|
||||
pop.title = 'Pop out in new window';
|
||||
pop.appendChild(mIcon('open_in_new'));
|
||||
pop.onclick = () => popOutShare(share);
|
||||
card.append(video, label, pop);
|
||||
if (share.local) {
|
||||
const stop = document.createElement('button');
|
||||
stop.className = 'pop-btn stop-share-btn';
|
||||
stop.title = 'Stop this screen share';
|
||||
stop.appendChild(mIcon('stop_circle'));
|
||||
stop.appendChild(document.createTextNode(' stop'));
|
||||
stop.onclick = () => stopScreenShare(share.stream);
|
||||
card.appendChild(stop);
|
||||
}
|
||||
els.screens.appendChild(card);
|
||||
|
||||
if (ownShares.length) {
|
||||
const pillRow = document.createElement('div');
|
||||
pillRow.className = 'own-pills';
|
||||
for (const share of ownShares) pillRow.appendChild(buildOwnPill(share));
|
||||
els.screens.appendChild(pillRow);
|
||||
}
|
||||
|
||||
if (remoteShares.length) {
|
||||
const body = document.createElement('div');
|
||||
if (focusedShareKey) {
|
||||
body.className = 'screens-body spotlight';
|
||||
const focused = remoteShares.find((s) => s.key === focusedShareKey);
|
||||
const rest = remoteShares.filter((s) => s.key !== focusedShareKey);
|
||||
body.appendChild(buildShareCard(focused, true));
|
||||
if (rest.length) {
|
||||
const strip = document.createElement('div');
|
||||
strip.className = 'thumbs-row';
|
||||
for (const s of rest) strip.appendChild(buildShareCard(s, false));
|
||||
body.appendChild(strip);
|
||||
}
|
||||
els.screens.appendChild(body);
|
||||
} else {
|
||||
body.className = 'screens-body screens-grid';
|
||||
for (const s of remoteShares) body.appendChild(buildShareCard(s, false));
|
||||
els.screens.appendChild(body);
|
||||
applyGridLayout();
|
||||
ensureScreensResizeObserver();
|
||||
}
|
||||
}
|
||||
|
||||
updateScreenTileIndicators();
|
||||
}
|
||||
|
||||
@@ -2146,6 +2234,20 @@ dragResize({
|
||||
});
|
||||
spatialResizer.addEventListener('mousedown', () => { spatialStartH = 0; });
|
||||
|
||||
// spatial pane collapse/expand
|
||||
const LS_SPATIAL_COLLAPSED = 'voicepoc.spatialCollapsed';
|
||||
const btnSpatialCollapse = $('#btn-spatial-collapse');
|
||||
const btnSpatialExpand = $('#btn-spatial-expand');
|
||||
function setSpatialCollapsed(collapsed) {
|
||||
document.body.classList.toggle('spatial-collapsed', collapsed);
|
||||
localStorage.setItem(LS_SPATIAL_COLLAPSED, collapsed ? '1' : '0');
|
||||
if (!collapsed && typeof cy !== 'undefined') requestAnimationFrame(() => cy.resize());
|
||||
applyGridLayout();
|
||||
}
|
||||
btnSpatialCollapse.onclick = () => setSpatialCollapsed(true);
|
||||
btnSpatialExpand.onclick = () => setSpatialCollapsed(false);
|
||||
if (localStorage.getItem(LS_SPATIAL_COLLAPSED) === '1') setSpatialCollapsed(true);
|
||||
|
||||
// dropping below the desktop breakpoint: let mobile's own CSS take over sizing
|
||||
window.addEventListener('resize', () => {
|
||||
if (isDesktop()) return;
|
||||
|
||||
Reference in new issue
Block a user