simplification

This commit is contained in:
legop3
2025-11-29 22:12:26 -05:00
parent 44d1920e2b
commit a343c5d1e2
10 changed files with 58 additions and 177 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-DIyMMVJE.js"></script> <script type="module" crossorigin src="/assets/index-DPNmbtZ-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BxUpjkzh.css"> <link rel="stylesheet" crossorigin href="/assets/index-BxUpjkzh.css">
</head> </head>
<body> <body>
+7 -3
View File
@@ -9,9 +9,13 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const rosterEntry = const rosterEntry =
roverId && session?.roster ? session.roster.find((item) => String(item.id) === String(roverId)) : null; roverId && session?.roster ? session.roster.find((item) => String(item.id) === String(roverId)) : null;
const hasAudio = Boolean(rosterEntry?.media?.audioPublishUrl); const hasAudio = Boolean(rosterEntry?.media?.audioPublishUrl);
const sources = useVideoRequests( const entries = roverId
roverId ? [{ type: 'rover', id: roverId, audioId: hasAudio ? `${roverId}-audio` : null }] : [], ? [
); { type: 'rover', id: roverId, key: roverId },
...(hasAudio ? [{ type: 'rover', id: `${roverId}-audio`, key: `${roverId}-audio` }] : []),
]
: [];
const sources = useVideoRequests(entries);
const info = roverId ? sources[roverId] : null; const info = roverId ? sources[roverId] : null;
const audioInfo = roverId && hasAudio ? sources[`${roverId}-audio`] : null; const audioInfo = roverId && hasAudio ? sources[`${roverId}-audio`] : null;
const frame = useTelemetryFrame(roverId); const frame = useTelemetryFrame(roverId);
+3 -3
View File
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { WhepPlayer } from '../lib/whepPlayer.js'; import { WhepPlayer } from '../lib/whepPlayer.js';
import { AudioWhepPlayer } from '../lib/audioWhepPlayer.js';
const RESTART_DELAY_MS = 2000; const RESTART_DELAY_MS = 2000;
const UNMUTE_RETRY_MS = 3000; const UNMUTE_RETRY_MS = 3000;
@@ -177,10 +176,11 @@ export default function VideoTile({
} }
}; };
player = new AudioWhepPlayer({ player = new WhepPlayer({
url: audioSessionInfo.url, url: audioSessionInfo.url,
token: audioSessionInfo.token, token: audioSessionInfo.token,
audio: audioRef.current, video: audioRef.current,
audioOnly: true,
onStatus: handleStatus, onStatus: handleStatus,
}); });
+1 -16
View File
@@ -13,7 +13,6 @@ function normalizeEntry(entry) {
if (typeof entry === 'object') { if (typeof entry === 'object') {
if (entry.type && entry.id) { if (entry.type && entry.id) {
const id = String(entry.id); const id = String(entry.id);
const audioId = entry.audioId ? String(entry.audioId) : null;
let key = entry.key; let key = entry.key;
if (!key) { if (!key) {
key = entry.type === 'room' ? `room:${id}` : id; key = entry.type === 'room' ? `room:${id}` : id;
@@ -22,7 +21,6 @@ function normalizeEntry(entry) {
type: entry.type, type: entry.type,
id, id,
key, key,
audioId,
}; };
} }
if (entry.roverId) { if (entry.roverId) {
@@ -111,17 +109,7 @@ export function useVideoRequests(sourceList = []) {
return; return;
} }
clearRetry(entry.key); clearRetry(entry.key);
setSources((prev) => { setSources((prev) => ({ ...prev, [entry.key]: resp }));
const next = { ...prev, [entry.key]: resp };
if (entry.audioId && resp.url && resp.token) {
const audioUrl = resp.url.replace(
`/${encodeURIComponent(entry.id)}/whep`,
`/${encodeURIComponent(entry.audioId)}/whep`,
);
next[entry.audioId] = { url: audioUrl, token: resp.token };
}
return next;
});
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.info('video:request ok', { entry, url: resp.url }); console.info('video:request ok', { entry, url: resp.url });
}); });
@@ -146,9 +134,6 @@ export function useVideoRequests(sourceList = []) {
if (sources[entry.key]) { if (sources[entry.key]) {
next[entry.key] = sources[entry.key]; next[entry.key] = sources[entry.key];
} }
if (entry.audioId && sources[entry.audioId]) {
next[entry.audioId] = sources[entry.audioId];
}
}); });
return next; return next;
}, [normalizedKey, sources, normalizedEntries]); }, [normalizedKey, sources, normalizedEntries]);
-114
View File
@@ -1,114 +0,0 @@
/* global Buffer */
import { buildAuthHeader } from './whepAuth.js';
const RTC_CONFIG = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:your.turn.server:3478', username: 'user', credential: 'pass' },
],
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require',
};
export class AudioWhepPlayer {
constructor({ url, token, audio, onStatus }) {
this.url = url;
this.token = token;
this.audio = audio;
this.pc = null;
this.abortController = null;
this.onStatus = onStatus;
}
notify(status, detail) {
if (typeof this.onStatus === 'function') {
this.onStatus(status, detail);
}
}
configureElement() {
if (!this.audio) return;
this.audio.autoplay = true;
this.audio.muted = false;
this.audio.playsInline = true;
}
async start() {
if (!this.url || !this.audio) {
throw new Error('Audio target missing');
}
this.stop();
this.configureElement();
this.notify('connecting');
this.abortController = new AbortController();
const pc = new RTCPeerConnection(RTC_CONFIG);
this.pc = pc;
const stream = new MediaStream();
pc.ontrack = (event) => {
event.streams[0]?.getTracks().forEach((track) => stream.addTrack(track));
this.audio.srcObject = stream;
if (event.receiver && 'playoutDelayHint' in event.receiver) {
event.receiver.playoutDelayHint = 0;
}
};
pc.addTransceiver('audio', { direction: 'recvonly' });
pc.onconnectionstatechange = () => {
this.notify(pc.connectionState);
};
pc.oniceconnectionstatechange = () => {
const state = pc.iceConnectionState;
if (state === 'failed' || state === 'disconnected') {
this.notify(state);
}
};
try {
const offer = await pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: false });
await pc.setLocalDescription(offer);
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
...buildAuthHeader(this.token),
},
body: offer.sdp,
signal: this.abortController.signal,
});
if (!response.ok) {
throw new Error(`WHEP audio request failed: ${response.status}`);
}
const answerSdp = await response.text();
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
pc.getReceivers().forEach((receiver) => {
if ('playoutDelayHint' in receiver) {
receiver.playoutDelayHint = 0;
}
});
await this.audio.play().catch(() => {});
this.notify('playing');
} catch (err) {
this.notify('error', err.message);
this.stop();
throw err;
}
}
stop() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
if (this.pc) {
this.pc.getSenders().forEach((s) => s.track?.stop());
this.pc.getReceivers().forEach((r) => r.track?.stop());
this.pc.close();
this.pc = null;
}
if (this.audio) {
this.audio.srcObject = null;
}
this.notify('stopped');
}
}
-18
View File
@@ -1,18 +0,0 @@
/* global Buffer */
function encodeBase64(value) {
if (typeof btoa === 'function') {
return btoa(value);
}
if (typeof Buffer !== 'undefined') {
return Buffer.from(value).toString('base64');
}
throw new Error('No base64 encoder available');
}
export function buildAuthHeader(token) {
if (!token) return {};
const credential = `${token}:${token}`;
const encoded = encodeBase64(credential);
return { Authorization: `Basic ${encoded}` };
}
+28 -6
View File
@@ -1,4 +1,4 @@
import { buildAuthHeader } from './whepAuth.js'; /* global Buffer */
const RTC_CONFIG = { const RTC_CONFIG = {
iceServers: [ iceServers: [
@@ -9,11 +9,29 @@ const RTC_CONFIG = {
rtcpMuxPolicy: 'require', rtcpMuxPolicy: 'require',
}; };
function encodeBase64(value) {
if (typeof btoa === 'function') {
return btoa(value);
}
if (typeof Buffer !== 'undefined') {
return Buffer.from(value).toString('base64');
}
throw new Error('No base64 encoder available');
}
function buildAuthHeader(token) {
if (!token) return {};
const credential = `${token}:${token}`;
const encoded = encodeBase64(credential);
return { Authorization: `Basic ${encoded}` };
}
export class WhepPlayer { export class WhepPlayer {
constructor({ url, token, video, onStatus }) { constructor({ url, token, video, onStatus, audioOnly = false }) {
this.url = url; this.url = url;
this.token = token; this.token = token;
this.video = video; this.video = video;
this.audioOnly = audioOnly;
this.pc = null; this.pc = null;
this.abortController = null; this.abortController = null;
this.onStatus = onStatus; this.onStatus = onStatus;
@@ -38,7 +56,7 @@ export class WhepPlayer {
async start() { async start() {
if (!this.url || !this.video) { if (!this.url || !this.video) {
throw new Error('Video target missing'); throw new Error('Media target missing');
} }
this.stop(); this.stop();
this.notify('connecting'); this.notify('connecting');
@@ -54,8 +72,12 @@ export class WhepPlayer {
event.receiver.playoutDelayHint = 0; event.receiver.playoutDelayHint = 0;
} }
}; };
pc.addTransceiver('video', { direction: 'recvonly' }); if (this.audioOnly) {
pc.addTransceiver('audio', { direction: 'recvonly' }); pc.addTransceiver('audio', { direction: 'recvonly' });
} else {
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
}
pc.onconnectionstatechange = () => { pc.onconnectionstatechange = () => {
const state = pc.connectionState; const state = pc.connectionState;
@@ -71,7 +93,7 @@ export class WhepPlayer {
try { try {
const offer = await pc.createOffer({ const offer = await pc.createOffer({
offerToReceiveAudio: true, offerToReceiveAudio: true,
offerToReceiveVideo: true, offerToReceiveVideo: !this.audioOnly,
}); });
await pc.setLocalDescription(offer); await pc.setLocalDescription(offer);
+7 -5
View File
@@ -118,11 +118,13 @@ export default function SpectatorApp() {
useSpectatorMode(); useSpectatorMode();
const frames = useTelemetryFrames(); const frames = useTelemetryFrames();
const roster = session?.roster ?? []; const roster = session?.roster ?? [];
const entries = roster.map((rover) => ({ const entries = roster.flatMap((rover) => {
type: 'rover', const base = { type: 'rover', id: rover.id, key: rover.id };
id: rover.id, if (rover.media?.audioPublishUrl) {
audioId: rover.media?.audioPublishUrl ? `${rover.id}-audio` : null, return [base, { type: 'rover', id: `${rover.id}-audio`, key: `${rover.id}-audio` }];
})); }
return [base];
});
const videoSources = useVideoRequests(entries); const videoSources = useVideoRequests(entries);
return ( return (