mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
video player!
This commit is contained in:
@@ -15,6 +15,10 @@
|
|||||||
#authPanel { margin: 12px 0; display: flex; gap: 8px; align-items: center; }
|
#authPanel { margin: 12px 0; display: flex; gap: 8px; align-items: center; }
|
||||||
#authPanel input { padding: 4px; }
|
#authPanel input { padding: 4px; }
|
||||||
#activeDriver { margin-bottom: 8px; font-weight: bold; }
|
#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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -61,8 +65,13 @@
|
|||||||
<h3>Sensor Frames</h3>
|
<h3>Sensor Frames</h3>
|
||||||
<div id="currentRover">Active rover: --</div>
|
<div id="currentRover">Active rover: --</div>
|
||||||
<div id="activeDriver">Driver: --</div>
|
<div id="activeDriver">Driver: --</div>
|
||||||
<button id="videoRequest">Get Video URL</button>
|
<div id="videoPanel">
|
||||||
<div id="videoLink"></div>
|
<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>
|
<pre id="sensorOutput"></pre>
|
||||||
<p>Use WASD keys to drive the selected rover. Shift increases speed.</p>
|
<p>Use WASD keys to drive the selected rover. Shift increases speed.</p>
|
||||||
<div id="alerts"></div>
|
<div id="alerts"></div>
|
||||||
@@ -75,6 +84,7 @@
|
|||||||
<script src="src/services/state.js"></script>
|
<script src="src/services/state.js"></script>
|
||||||
<script src="src/services/alertFeed.js"></script>
|
<script src="src/services/alertFeed.js"></script>
|
||||||
<script src="src/services/authControls.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/roverUI.js"></script>
|
||||||
<script src="src/services/driveControls.js"></script>
|
<script src="src/services/driveControls.js"></script>
|
||||||
<script src="src/main.js"></script>
|
<script src="src/main.js"></script>
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
.rover-card h3 { margin: 0 0 8px 0; }
|
.rover-card h3 { margin: 0 0 8px 0; }
|
||||||
.sensor { font-family: monospace; white-space: pre-wrap; word-break: break-word; }
|
.sensor { font-family: monospace; white-space: pre-wrap; word-break: break-word; }
|
||||||
.meta { font-size: 0.9rem; color: #555; margin-bottom: 8px; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="../loader.js"></script>
|
<script src="../loader.js"></script>
|
||||||
<script src="../src/globals/socket.js"></script>
|
<script src="../src/globals/socket.js"></script>
|
||||||
|
<script src="../src/services/videoPlayer.js"></script>
|
||||||
<script src="spectator.js"></script>
|
<script src="spectator.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
registerModule('spectator/main', (require, exports) => {
|
registerModule('spectator/main', (require, exports) => {
|
||||||
const { socket } = require('globals/socket');
|
const { socket } = require('globals/socket');
|
||||||
|
const videoPlayer = require('services/videoPlayer');
|
||||||
|
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
const grid = document.getElementById('grid');
|
const grid = document.getElementById('grid');
|
||||||
@@ -11,19 +12,23 @@ registerModule('spectator/main', (require, exports) => {
|
|||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
statusEl.textContent = 'Disconnected';
|
statusEl.textContent = 'Disconnected';
|
||||||
|
stopAllVideos();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('lockdown', ({ message }) => {
|
socket.on('lockdown', ({ message }) => {
|
||||||
statusEl.textContent = message || 'Spectator mode disabled';
|
statusEl.textContent = message || 'Spectator mode disabled';
|
||||||
|
stopAllVideos();
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('rovers', (list) => {
|
socket.on('rovers', (list) => {
|
||||||
const seen = new Set();
|
|
||||||
list.forEach((rover) => {
|
list.forEach((rover) => {
|
||||||
const card = ensureCard(rover.id);
|
const card = ensureCard(rover.id);
|
||||||
card.querySelector('.meta').textContent = `Locked: ${rover.locked ? 'yes' : 'no'} | Last seen: ${new Date(rover.lastSeen).toLocaleTimeString()}`;
|
card.querySelector('.meta').textContent = `Locked: ${rover.locked ? 'yes' : 'no'} | Last seen: ${new Date(rover.lastSeen).toLocaleTimeString()}`;
|
||||||
|
if (!card.dataset.videoActive) {
|
||||||
|
connectVideo(rover.id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cards.forEach((_, id) => {
|
Array.from(cards.keys()).forEach((id) => {
|
||||||
if (!list.find((r) => r.id === id)) {
|
if (!list.find((r) => r.id === id)) {
|
||||||
removeCard(id);
|
removeCard(id);
|
||||||
}
|
}
|
||||||
@@ -41,23 +46,85 @@ registerModule('spectator/main', (require, exports) => {
|
|||||||
}
|
}
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'rover-card';
|
card.className = 'rover-card';
|
||||||
|
card.dataset.videoActive = '';
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<h3>${roverId}</h3>
|
<h3>${roverId}</h3>
|
||||||
<div class="meta"></div>
|
<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>
|
<pre class="sensor">Waiting for data...</pre>
|
||||||
`;
|
`;
|
||||||
|
const reconnectBtn = card.querySelector('.video-reconnect');
|
||||||
|
reconnectBtn.addEventListener('click', () => connectVideo(roverId, true));
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
cards.set(roverId, card);
|
cards.set(roverId, card);
|
||||||
|
connectVideo(roverId, true);
|
||||||
return card;
|
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,
|
||||||
|
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) {
|
function removeCard(roverId) {
|
||||||
const card = cards.get(roverId);
|
const card = cards.get(roverId);
|
||||||
if (card) {
|
if (card) {
|
||||||
|
videoPlayer.stopStream(`spectator-${roverId}`);
|
||||||
card.remove();
|
card.remove();
|
||||||
cards.delete(roverId);
|
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');
|
requireModule('spectator/main');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
registerModule('services/roverUI', (require, exports) => {
|
registerModule('services/roverUI', (require, exports) => {
|
||||||
const { socket } = require('globals/socket');
|
const { socket } = require('globals/socket');
|
||||||
const { formatHex } = require('helpers/formatters');
|
const { formatHex } = require('helpers/formatters');
|
||||||
|
const videoPlayer = require('services/videoPlayer');
|
||||||
const state = require('services/state');
|
const state = require('services/state');
|
||||||
|
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
@@ -13,11 +14,14 @@ registerModule('services/roverUI', (require, exports) => {
|
|||||||
const modeSelect = document.getElementById('modeSelect');
|
const modeSelect = document.getElementById('modeSelect');
|
||||||
const activeDriverEl = document.getElementById('activeDriver');
|
const activeDriverEl = document.getElementById('activeDriver');
|
||||||
const currentRoverEl = document.getElementById('currentRover');
|
const currentRoverEl = document.getElementById('currentRover');
|
||||||
const videoBtn = document.getElementById('videoRequest');
|
const videoContainer = document.getElementById('videoContainer');
|
||||||
const videoLinkEl = document.getElementById('videoLink');
|
const driverVideo = document.getElementById('driverVideo');
|
||||||
|
const videoStatus = document.getElementById('videoStatus');
|
||||||
|
const videoReconnect = document.getElementById('videoReconnect');
|
||||||
|
|
||||||
let roster = [];
|
let roster = [];
|
||||||
let lastSelection = null;
|
let lastSelection = null;
|
||||||
|
let currentVideoRover = null;
|
||||||
|
|
||||||
state.onRoleChange((role) => {
|
state.onRoleChange((role) => {
|
||||||
const isAdmin = role === 'admin' || role === 'lockdown';
|
const isAdmin = role === 'admin' || role === 'lockdown';
|
||||||
@@ -47,7 +51,7 @@ registerModule('services/roverUI', (require, exports) => {
|
|||||||
} else if (!list.length) {
|
} else if (!list.length) {
|
||||||
state.setSelected(null);
|
state.setSelected(null);
|
||||||
}
|
}
|
||||||
renderSelection();
|
renderSelection(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
roverSelect.addEventListener('change', () => {
|
roverSelect.addEventListener('change', () => {
|
||||||
@@ -64,6 +68,7 @@ registerModule('services/roverUI', (require, exports) => {
|
|||||||
state.setSelected(roverId);
|
state.setSelected(roverId);
|
||||||
renderSelection(true);
|
renderSelection(true);
|
||||||
statusEl.textContent = `Driving ${roverId}`;
|
statusEl.textContent = `Driving ${roverId}`;
|
||||||
|
maybeConnectVideo(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('sensorFrame', ({ roverId, frame, sensors }) => {
|
socket.on('sensorFrame', ({ roverId, frame, sensors }) => {
|
||||||
@@ -102,20 +107,7 @@ registerModule('services/roverUI', (require, exports) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
videoBtn?.addEventListener('click', () => {
|
videoReconnect?.addEventListener('click', () => maybeConnectVideo(true));
|
||||||
const roverId = state.getSelected();
|
|
||||||
if (!roverId) {
|
|
||||||
if (videoLinkEl) videoLinkEl.textContent = 'Select a rover first';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
socket.emit('video:request', { roverId }, (resp = {}) => {
|
|
||||||
if (resp.error) {
|
|
||||||
videoLinkEl.textContent = `Video error: ${resp.error}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
videoLinkEl.innerHTML = `Video URL: <a href=\"${resp.url}\" target=\"_blank\">${resp.url}</a>`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
statusEl.textContent = 'Connected';
|
statusEl.textContent = 'Connected';
|
||||||
@@ -137,9 +129,56 @@ registerModule('services/roverUI', (require, exports) => {
|
|||||||
if (sensorOutput && (clearOutput || roverId !== lastSelection)) {
|
if (sensorOutput && (clearOutput || roverId !== lastSelection)) {
|
||||||
sensorOutput.textContent = roverId ? 'Waiting for sensor data...' : '';
|
sensorOutput.textContent = roverId ? 'Waiting for sensor data...' : '';
|
||||||
}
|
}
|
||||||
if (clearOutput && videoLinkEl) {
|
|
||||||
videoLinkEl.textContent = '';
|
|
||||||
}
|
|
||||||
lastSelection = roverId;
|
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 } = resp;
|
||||||
|
videoPlayer
|
||||||
|
.startStream('driver', {
|
||||||
|
url,
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
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();
|
||||||
|
|
||||||
|
const credential = token || extractSessionToken(url);
|
||||||
|
if (!credential) {
|
||||||
|
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(`${credential}:${credential}`)}`,
|
||||||
|
},
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSessionToken(url) {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url, window.location.origin);
|
||||||
|
return parsed.searchParams.get('session');
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user