first lol

This commit is contained in:
legop3
2025-11-15 04:05:01 -05:00
parent 4d3bc09e2b
commit d64cfe2d46
42 changed files with 5011 additions and 880 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+8 -87
View File
@@ -1,93 +1,14 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Multi Roomba Rover</title>
<style>
body { font-family: sans-serif; margin: 16px; }
#layout { display: flex; gap: 16px; }
#roverList { width: 240px; }
#sensorOutput { font-family: monospace; height: 320px; overflow: auto; border: 1px solid #999; padding: 8px; }
button { margin: 4px; }
.row { margin-bottom: 8px; }
#alerts { margin-top: 12px; max-width: 600px; }
.alert { background: #fff1d2; border: 1px solid #f0b651; padding: 6px 8px; margin-bottom: 6px; font-size: 0.9rem; }
#authPanel { margin: 12px 0; display: flex; gap: 8px; align-items: center; }
#authPanel input { padding: 4px; }
#activeDriver { margin-bottom: 8px; font-weight: bold; }
#videoPanel { margin-bottom: 12px; }
#videoContainer { width: 100%; background: #000; min-height: 200px; display: flex; align-items: center; justify-content: center; }
#videoContainer video { width: 100%; max-height: 320px; background: #000; }
#videoStatus { margin-top: 6px; font-size: 0.9rem; color: #444; }
</style>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-APFDmmUd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DoNH_msu.css">
</head>
<body>
<h1>Multi Roomba Rover</h1>
<div id="status">Connecting…</div>
<div id="authPanel">
<input id="username" placeholder="Username" />
<input id="password" type="password" placeholder="Password" />
<button id="loginBtn">Admin Login</button>
<span id="roleStatus">Role: user</span>
</div>
<div id="layout">
<div id="roverList">
<label for="roverSelect">Rovers</label>
<select id="roverSelect" size="10" style="width: 100%"></select>
<div class="row" id="adminControls">
<label><input type="checkbox" id="forceControl" /> Force control</label>
<button id="lockToggle">Lock/Unlock</button>
</div>
<div class="row" id="modeControls" style="display:none">
<label for="modeSelect">Server mode</label>
<select id="modeSelect">
<option value="open">Open</option>
<option value="turns">Turns</option>
<option value="admin">Admin</option>
<option value="lockdown">Lockdown</option>
</select>
</div>
<div class="row">
<button data-oi="start">Start OI</button>
<button data-oi="safe">Safe</button>
<button data-oi="full">Full</button>
<button data-oi="passive">Passive</button>
<button data-oi="dock">Dock</button>
</div>
<div class="row">
<button id="sensorToggle">Enable Sensor Stream</button>
</div>
<div class="row">
<button id="motorsStop">Stop Motors</button>
</div>
</div>
<div style="flex: 1">
<h3>Sensor Frames</h3>
<div id="currentRover">Active rover: --</div>
<div id="activeDriver">Driver: --</div>
<div id="videoPanel">
<div id="videoContainer">
<video id="driverVideo" muted playsinline></video>
</div>
<div id="videoStatus">No rover selected</div>
<button id="videoReconnect">Reconnect Video</button>
</div>
<pre id="sensorOutput"></pre>
<p>Use WASD keys to drive the selected rover. Shift increases speed.</p>
<div id="alerts"></div>
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="loader.js"></script>
<script src="src/globals/socket.js"></script>
<script src="src/helpers/formatters.js"></script>
<script src="src/services/state.js"></script>
<script src="src/services/alertFeed.js"></script>
<script src="src/services/authControls.js"></script>
<script src="src/services/videoPlayer.js"></script>
<script src="src/services/roverUI.js"></script>
<script src="src/services/driveControls.js"></script>
<script src="src/main.js"></script>
<p><a href="/spectate/">Open spectator view</a></p>
<div id="root"></div>
</body>
</html>
-22
View File
@@ -1,22 +0,0 @@
(function () {
const modules = {};
const cache = {};
window.registerModule = function (name, factory) {
modules[name] = factory;
};
window.requireModule = function (name) {
if (cache[name]) {
return cache[name];
}
const factory = modules[name];
if (!factory) {
throw new Error(`Module ${name} not found`);
}
const exports = {};
cache[name] = exports;
factory(requireModule, exports);
return exports;
};
})();
-33
View File
@@ -1,33 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Rover Spectator</title>
<style>
body { font-family: sans-serif; margin: 16px; }
#status { margin-bottom: 12px; }
#grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.rover-card { border: 1px solid #ccc; padding: 12px; border-radius: 6px; background: #fdfdfd; }
.rover-card h3 { margin: 0 0 8px 0; }
.sensor { font-family: monospace; white-space: pre-wrap; word-break: break-word; }
.meta { font-size: 0.9rem; color: #555; margin-bottom: 8px; }
.video-wrap { background: #000; min-height: 160px; margin-bottom: 6px; display: flex; align-items: center; justify-content: center; }
.video-wrap video { width: 100%; max-height: 200px; background: #000; }
.video-status { font-size: 0.85rem; color: #444; margin-bottom: 4px; }
</style>
</head>
<body>
<h1>Rover Spectator</h1>
<div id="status">Connecting…</div>
<div id="grid"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
window.__desiredRole = 'spectator';
</script>
<script src="../loader.js"></script>
<script src="../src/globals/socket.js"></script>
<script src="../src/services/videoPlayer.js"></script>
<script src="spectator.js"></script>
</body>
</html>
-131
View File
@@ -1,131 +0,0 @@
registerModule('spectator/main', (require, exports) => {
const { socket } = require('globals/socket');
const videoPlayer = require('services/videoPlayer');
const statusEl = document.getElementById('status');
const grid = document.getElementById('grid');
const cards = new Map();
socket.on('connect', () => {
statusEl.textContent = 'Connected';
});
socket.on('disconnect', () => {
statusEl.textContent = 'Disconnected';
stopAllVideos();
});
socket.on('lockdown', ({ message }) => {
statusEl.textContent = message || 'Spectator mode disabled';
stopAllVideos();
});
socket.on('rovers', (list) => {
list.forEach((rover) => {
const card = ensureCard(rover.id);
card.querySelector('.meta').textContent = `Locked: ${rover.locked ? 'yes' : 'no'} | Last seen: ${new Date(rover.lastSeen).toLocaleTimeString()}`;
if (!card.dataset.videoActive) {
connectVideo(rover.id);
}
});
Array.from(cards.keys()).forEach((id) => {
if (!list.find((r) => r.id === id)) {
removeCard(id);
}
});
});
socket.on('sensorFrame', ({ roverId, sensors }) => {
const card = ensureCard(roverId);
card.querySelector('.sensor').textContent = sensors ? JSON.stringify(sensors, null, 2) : 'No data';
});
function ensureCard(roverId) {
if (cards.has(roverId)) {
return cards.get(roverId);
}
const card = document.createElement('div');
card.className = 'rover-card';
card.dataset.videoActive = '';
card.innerHTML = `
<h3>${roverId}</h3>
<div class="meta"></div>
<div class="video-wrap"></div>
<div class="video-status">Video not connected</div>
<button class="video-reconnect">Reconnect video</button>
<pre class="sensor">Waiting for data...</pre>
`;
const reconnectBtn = card.querySelector('.video-reconnect');
reconnectBtn.addEventListener('click', () => connectVideo(roverId, true));
grid.appendChild(card);
cards.set(roverId, card);
connectVideo(roverId, true);
return card;
}
function connectVideo(roverId, force = false) {
const card = ensureCard(roverId);
const status = card.querySelector('.video-status');
const wrap = card.querySelector('.video-wrap');
const playerId = `spectator-${roverId}`;
if (!force && card.dataset.videoActive === 'yes') {
return;
}
status.textContent = 'Connecting video…';
socket.emit('video:request', { roverId }, (resp = {}) => {
if (resp.error) {
status.textContent = `Video error: ${resp.error}`;
videoPlayer.stopStream(playerId);
card.dataset.videoActive = '';
return;
}
videoPlayer
.startStream(playerId, {
url: resp.url,
token: resp.token,
mount: wrap,
onStatus: (state, detail) => {
if (state === 'playing') {
status.textContent = 'Video live';
card.dataset.videoActive = 'yes';
} else if (state === 'connecting') {
status.textContent = 'Connecting video…';
} else if (state === 'error') {
status.textContent = `Video error: ${detail}`;
card.dataset.videoActive = '';
} else if (state === 'stopped') {
status.textContent = 'Video stopped';
card.dataset.videoActive = '';
}
},
})
.catch((err) => {
status.textContent = `Video error: ${err.message}`;
card.dataset.videoActive = '';
});
});
}
function removeCard(roverId) {
const card = cards.get(roverId);
if (card) {
videoPlayer.stopStream(`spectator-${roverId}`);
card.remove();
cards.delete(roverId);
}
}
function stopAllVideos() {
Array.from(cards.keys()).forEach((id) => {
videoPlayer.stopStream(`spectator-${id}`);
const card = cards.get(id);
if (card) {
const status = card.querySelector('.video-status');
status.textContent = 'Video stopped';
card.dataset.videoActive = '';
}
});
}
});
requireModule('spectator/main');
-8
View File
@@ -1,8 +0,0 @@
registerModule('globals/socket', (require, exports) => {
const query = {};
if (window.__desiredRole) {
query.role = window.__desiredRole;
}
const socket = io(query.role ? { query } : undefined);
exports.socket = socket;
});
-11
View File
@@ -1,11 +0,0 @@
registerModule('helpers/formatters', (require, exports) => {
function formatHex(base64) {
if (!base64) return '';
const buffer = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
return Array.from(buffer)
.map((b) => b.toString(16).padStart(2, '0'))
.join(' ');
}
exports.formatHex = formatHex;
});
-7
View File
@@ -1,7 +0,0 @@
requireModule('globals/socket');
requireModule('helpers/formatters');
requireModule('services/state');
requireModule('services/alertFeed');
requireModule('services/authControls');
requireModule('services/roverUI');
requireModule('services/driveControls');
-25
View File
@@ -1,25 +0,0 @@
registerModule('services/alertFeed', (require, exports) => {
const { socket } = require('globals/socket');
const container = document.getElementById('alerts');
if (!container) return;
function pushAlert({ color = '#2196f3', title, message, ts }) {
const div = document.createElement('div');
div.className = 'alert';
div.style.borderColor = color;
div.style.background = color + '22';
div.innerHTML = `<strong>${title || 'Notice'}</strong> - ${message || ''}`;
container.prepend(div);
while (container.children.length > 5) {
container.removeChild(container.lastChild);
}
setTimeout(() => {
if (div.parentElement === container) {
container.removeChild(div);
}
}, 15000);
}
socket.on('alert', pushAlert);
});
@@ -1,35 +0,0 @@
registerModule('services/authControls', (require, exports) => {
const { socket } = require('globals/socket');
const state = require('services/state');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
const roleStatus = document.getElementById('roleStatus');
function updateRole(role) {
if (roleStatus) {
roleStatus.textContent = `Role: ${role}`;
}
state.setRole(role);
}
loginBtn?.addEventListener('click', () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
socket.emit('auth:login', { username, password }, (resp = {}) => {
if (!resp.success) {
alert(resp.error || 'Login failed');
} else {
updateRole(resp.role);
}
passwordInput.value = '';
});
});
socket.on('auth:role', ({ role }) => {
updateRole(role);
});
// initialize from default role
updateRole(state.getRole());
});
-127
View File
@@ -1,127 +0,0 @@
registerModule('services/driveControls', (require, exports) => {
const { socket } = require('globals/socket');
const state = require('services/state');
const motorsStopBtn = document.getElementById('motorsStop');
const sensorToggleBtn = document.getElementById('sensorToggle');
const modeButtons = document.querySelectorAll('[data-oi]');
let sensorEnabled = false;
let lastDrive = { left: 0, right: 0 };
const keys = new Set();
window.addEventListener('keydown', (event) => {
const key = event.key.toLowerCase();
if (keys.has(key)) return;
keys.add(key);
maybeSendDriveUpdate();
});
window.addEventListener('keyup', (event) => {
const key = event.key.toLowerCase();
if (!keys.has(key)) return;
keys.delete(key);
maybeSendDriveUpdate();
});
function maybeSendDriveUpdate() {
const roverId = state.getSelected();
if (!roverId) return;
const speeds = computeDrive();
if (speeds.left === lastDrive.left && speeds.right === lastDrive.right) return;
lastDrive = speeds;
socket.emit('command', {
roverId,
type: 'drive',
data: { driveDirect: speeds },
});
}
function computeDrive() {
const forward = keys.has('w');
const backward = keys.has('s');
const left = keys.has('a');
const right = keys.has('d');
const fast = keys.has('shift');
const base = fast ? 300 : 150;
let leftSpeed = 0;
let rightSpeed = 0;
if (forward && !backward) {
leftSpeed += base;
rightSpeed += base;
} else if (backward && !forward) {
leftSpeed -= base;
rightSpeed -= base;
}
if (left && !right) {
leftSpeed -= base;
rightSpeed += base;
} else if (right && !left) {
leftSpeed += base;
rightSpeed -= base;
}
if (!forward && !backward && (left || right)) {
leftSpeed = left ? -base : base;
rightSpeed = left ? base : -base;
}
return {
left: clamp(leftSpeed, -500, 500),
right: clamp(rightSpeed, -500, 500),
};
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
motorsStopBtn?.addEventListener('click', () => {
const roverId = state.getSelected();
if (!roverId) return;
socket.emit('command', {
roverId,
type: 'motors',
data: { motorPwm: { main: 0, side: 0, vacuum: 0 } },
});
});
sensorToggleBtn?.addEventListener('click', () => {
sensorEnabled = !sensorEnabled;
sensorToggleBtn.textContent = sensorEnabled ? 'Disable Sensor Stream' : 'Enable Sensor Stream';
const roverId = state.getSelected();
socket.emit('command', {
roverId,
type: 'sensorStream',
data: { sensorStream: { enable: sensorEnabled } },
});
});
const OI_COMMANDS = {
start: [128],
safe: [131],
full: [132],
passive: [128],
dock: [143],
};
modeButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const cmd = btn.dataset.oi;
const bytes = OI_COMMANDS[cmd];
if (!bytes) return;
const roverId = state.getSelected();
socket.emit('command', {
roverId,
type: 'raw',
data: { raw: bytesToBase64(bytes) },
});
});
});
function bytesToBase64(bytes) {
return btoa(String.fromCharCode(...bytes));
}
});
-185
View File
@@ -1,185 +0,0 @@
registerModule('services/roverUI', (require, exports) => {
const { socket } = require('globals/socket');
const { formatHex } = require('helpers/formatters');
const videoPlayer = require('services/videoPlayer');
const state = require('services/state');
const statusEl = document.getElementById('status');
const roverSelect = document.getElementById('roverSelect');
const sensorOutput = document.getElementById('sensorOutput');
const lockBtn = document.getElementById('lockToggle');
const forceCheckbox = document.getElementById('forceControl');
const adminControls = document.getElementById('adminControls');
const modeControls = document.getElementById('modeControls');
const modeSelect = document.getElementById('modeSelect');
const activeDriverEl = document.getElementById('activeDriver');
const currentRoverEl = document.getElementById('currentRover');
const videoContainer = document.getElementById('videoContainer');
const driverVideo = document.getElementById('driverVideo');
const videoStatus = document.getElementById('videoStatus');
const videoReconnect = document.getElementById('videoReconnect');
let roster = [];
let lastSelection = null;
let currentVideoRover = null;
state.onRoleChange((role) => {
const isAdmin = role === 'admin' || role === 'lockdown';
if (lockBtn) lockBtn.disabled = !isAdmin;
if (roverSelect) roverSelect.disabled = !isAdmin;
if (adminControls) {
adminControls.style.display = isAdmin ? 'flex' : 'none';
}
if (modeControls) {
modeControls.style.display = isAdmin ? 'block' : 'none';
if (modeSelect) modeSelect.disabled = !isAdmin;
}
});
socket.on('rovers', (list) => {
roster = list;
roverSelect.innerHTML = '';
list.forEach((rover) => {
const option = document.createElement('option');
option.value = rover.id;
option.textContent = `${rover.name}${rover.locked ? ' (locked)' : ''}`;
roverSelect.appendChild(option);
});
const selected = state.getSelected();
if ((!selected || !list.find((r) => r.id === selected)) && list.length) {
state.setSelected(list[0].id);
} else if (!list.length) {
state.setSelected(null);
}
renderSelection(true);
});
roverSelect.addEventListener('change', () => {
const roverId = roverSelect.value;
const role = state.getRole();
state.setSelected(roverId);
renderSelection(true);
if (role === 'admin' || role === 'lockdown') {
socket.emit('requestControl', { roverId, force: !!forceCheckbox?.checked });
}
});
socket.on('controlGranted', ({ roverId }) => {
state.setSelected(roverId);
renderSelection(true);
statusEl.textContent = `Driving ${roverId}`;
maybeConnectVideo(true);
});
socket.on('sensorFrame', ({ roverId, frame, sensors }) => {
if (roverId !== state.getSelected()) return;
const lines = [];
if (sensors) {
Object.entries(sensors).forEach(([key, value]) => {
lines.push(`${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`);
});
}
lines.push('', 'raw:', formatHex(frame?.data));
sensorOutput.textContent = lines.join('\n');
});
socket.on('activeDriver', ({ roverId, socketId }) => {
if (roverId !== state.getSelected()) return;
if (activeDriverEl) {
activeDriverEl.textContent = `Active driver: ${socketId || 'none'}`;
}
});
lockBtn?.addEventListener('click', () => {
const roverId = state.getSelected();
const rover = roster.find((r) => r.id === roverId);
if (!rover) return;
socket.emit('lockRover', { roverId, locked: !rover.locked });
});
modeSelect?.addEventListener('change', () => {
socket.emit('setMode', { mode: modeSelect.value });
});
socket.on('mode', ({ mode }) => {
if (modeSelect) {
modeSelect.value = mode;
}
});
videoReconnect?.addEventListener('click', () => maybeConnectVideo(true));
socket.on('connect', () => {
statusEl.textContent = 'Connected';
});
socket.on('disconnect', () => {
statusEl.textContent = 'Disconnected';
});
function renderSelection(clearOutput) {
const roverId = state.getSelected();
if (roverSelect && roverId) {
roverSelect.value = roverId;
} else if (roverSelect && !roverId) {
roverSelect.value = '';
}
if (currentRoverEl) {
currentRoverEl.textContent = `Active rover: ${roverId || '--'}`;
}
if (sensorOutput && (clearOutput || roverId !== lastSelection)) {
sensorOutput.textContent = roverId ? 'Waiting for sensor data...' : '';
}
lastSelection = roverId;
maybeConnectVideo(clearOutput);
}
function maybeConnectVideo(force = false) {
const roverId = state.getSelected();
if (!roverId) {
currentVideoRover = null;
videoPlayer.stopStream('driver');
setVideoStatus('No rover selected');
return;
}
if (!force && roverId === currentVideoRover) {
return;
}
currentVideoRover = roverId;
setVideoStatus('Connecting video…');
socket.emit('video:request', { roverId }, (resp = {}) => {
if (resp.error) {
setVideoStatus(`Video error: ${resp.error}`);
videoPlayer.stopStream('driver');
return;
}
const { url, token } = resp;
videoPlayer
.startStream('driver', {
url,
token,
mount: videoContainer,
videoEl: driverVideo,
onStatus: (state, detail) => {
if (state === 'playing') {
setVideoStatus('Video live');
} else if (state === 'connecting') {
setVideoStatus('Connecting video…');
} else if (state === 'error') {
setVideoStatus(`Video error: ${detail}`);
} else if (state === 'stopped') {
setVideoStatus('Video stopped');
}
},
})
.catch((err) => {
setVideoStatus(`Video error: ${err.message}`);
});
});
}
function setVideoStatus(message) {
if (videoStatus) {
videoStatus.textContent = message;
}
}
});
-34
View File
@@ -1,34 +0,0 @@
registerModule('services/state', (require, exports) => {
let selectedRover = null;
let role = 'user';
const roleListeners = new Set();
function setSelected(roverId) {
selectedRover = roverId;
}
function getSelected() {
return selectedRover;
}
function setRole(nextRole) {
role = nextRole;
roleListeners.forEach((cb) => cb(role));
}
function getRole() {
return role;
}
function onRoleChange(cb) {
roleListeners.add(cb);
cb(role);
return () => roleListeners.delete(cb);
}
exports.setSelected = setSelected;
exports.getSelected = getSelected;
exports.setRole = setRole;
exports.getRole = getRole;
exports.onRoleChange = onRoleChange;
});
-152
View File
@@ -1,152 +0,0 @@
registerModule('services/videoPlayer', (require, exports) => {
const players = new Map();
class Player {
constructor(id, options = {}) {
this.id = id;
this.options = options;
this.onStatus = options.onStatus || (() => {});
this.mount = options.mount || null;
this.externalVideo = options.videoEl || null;
this.video = this.externalVideo || this.createVideoElement();
this.video.muted = true;
this.status('idle');
}
createVideoElement() {
const el = document.createElement('video');
el.autoplay = true;
el.playsInline = true;
el.controls = false;
el.muted = true;
el.style.width = '100%';
el.style.maxHeight = '360px';
if (this.mount) {
this.mount.innerHTML = '';
this.mount.appendChild(el);
}
return el;
}
attachAutoPlayLoop() {
this.autoPlayTimer = window.setInterval(() => {
if (!this.video) return;
if (!this.video.paused && !this.video.muted) {
clearInterval(this.autoPlayTimer);
this.autoPlayTimer = null;
return;
}
const attempt = () => this.video.play().catch(() => {});
if (this.video.paused) {
attempt();
}
if (this.video.muted) {
this.video.muted = false;
attempt();
}
}, 2500);
}
status(state, detail) {
this.onStatus(state, detail);
}
async start(url, token) {
this.stopPeer();
this.status('connecting');
this.pc = new RTCPeerConnection();
this.stream = new MediaStream();
this.video.srcObject = this.stream;
this.pc.addTransceiver('video', { direction: 'recvonly' });
this.pc.addTransceiver('audio', { direction: 'recvonly' });
this.pc.ontrack = (event) => {
const [remoteStream] = event.streams;
if (remoteStream) {
this.video.srcObject = remoteStream;
} else {
this.stream.addTrack(event.track);
}
this.video.play().catch(() => {});
};
this.attachAutoPlayLoop();
if (!token) {
throw new Error('Missing video session token');
}
const offer = await this.pc.createOffer();
await this.pc.setLocalDescription(offer);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
Authorization: `Basic ${btoa(`${token}:${token}`)}`,
},
body: offer.sdp,
});
if (!response.ok) {
throw new Error(`WHEP HTTP ${response.status}`);
}
const answer = await response.text();
await this.pc.setRemoteDescription({ type: 'answer', sdp: answer });
this.status('playing');
}
stopPeer() {
if (this.autoPlayTimer) {
clearInterval(this.autoPlayTimer);
this.autoPlayTimer = null;
}
if (this.pc) {
this.pc.getSenders().forEach((sender) => sender.track && sender.track.stop());
this.pc.getReceivers().forEach((receiver) => receiver.track && receiver.track.stop());
this.pc.close();
this.pc = null;
}
if (this.video) {
const tracks = this.video.srcObject?.getTracks() || [];
tracks.forEach((t) => t.stop());
this.video.pause();
this.video.srcObject = null;
}
}
stop() {
this.stopPeer();
if (!this.externalVideo && this.video && this.video.parentElement) {
this.video.parentElement.removeChild(this.video);
}
this.status('stopped');
}
}
async function startStream(id, { url, token, mount, videoEl, onStatus } = {}) {
stopStream(id);
const player = new Player(id, { mount, videoEl, onStatus });
players.set(id, player);
try {
await player.start(url, token);
return player.video;
} catch (err) {
player.status('error', err.message);
player.stop();
players.delete(id);
throw err;
}
}
function stopStream(id) {
const existing = players.get(id);
if (existing) {
existing.stop();
players.delete(id);
}
}
function stopAll() {
Array.from(players.keys()).forEach(stopStream);
}
exports.startStream = startStream;
exports.stopStream = stopStream;
exports.stopAll = stopAll;
});
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB