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-title" content="Multi Roomba Rover" />
<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">
</head>
<body>
+7 -3
View File
@@ -9,9 +9,13 @@ export default function DriverVideoPanel({layoutFormat = 'desktop'}) {
const rosterEntry =
roverId && session?.roster ? session.roster.find((item) => String(item.id) === String(roverId)) : null;
const hasAudio = Boolean(rosterEntry?.media?.audioPublishUrl);
const sources = useVideoRequests(
roverId ? [{ type: 'rover', id: roverId, audioId: hasAudio ? `${roverId}-audio` : null }] : [],
);
const entries = roverId
? [
{ 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 audioInfo = roverId && hasAudio ? sources[`${roverId}-audio`] : null;
const frame = useTelemetryFrame(roverId);
+3 -3
View File
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { WhepPlayer } from '../lib/whepPlayer.js';
import { AudioWhepPlayer } from '../lib/audioWhepPlayer.js';
const RESTART_DELAY_MS = 2000;
const UNMUTE_RETRY_MS = 3000;
@@ -177,10 +176,11 @@ export default function VideoTile({
}
};
player = new AudioWhepPlayer({
player = new WhepPlayer({
url: audioSessionInfo.url,
token: audioSessionInfo.token,
audio: audioRef.current,
video: audioRef.current,
audioOnly: true,
onStatus: handleStatus,
});
+1 -16
View File
@@ -13,7 +13,6 @@ function normalizeEntry(entry) {
if (typeof entry === 'object') {
if (entry.type && entry.id) {
const id = String(entry.id);
const audioId = entry.audioId ? String(entry.audioId) : null;
let key = entry.key;
if (!key) {
key = entry.type === 'room' ? `room:${id}` : id;
@@ -22,7 +21,6 @@ function normalizeEntry(entry) {
type: entry.type,
id,
key,
audioId,
};
}
if (entry.roverId) {
@@ -111,17 +109,7 @@ export function useVideoRequests(sourceList = []) {
return;
}
clearRetry(entry.key);
setSources((prev) => {
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;
});
setSources((prev) => ({ ...prev, [entry.key]: resp }));
// eslint-disable-next-line no-console
console.info('video:request ok', { entry, url: resp.url });
});
@@ -146,9 +134,6 @@ export function useVideoRequests(sourceList = []) {
if (sources[entry.key]) {
next[entry.key] = sources[entry.key];
}
if (entry.audioId && sources[entry.audioId]) {
next[entry.audioId] = sources[entry.audioId];
}
});
return next;
}, [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 = {
iceServers: [
@@ -9,11 +9,29 @@ const RTC_CONFIG = {
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 {
constructor({ url, token, video, onStatus }) {
constructor({ url, token, video, onStatus, audioOnly = false }) {
this.url = url;
this.token = token;
this.video = video;
this.audioOnly = audioOnly;
this.pc = null;
this.abortController = null;
this.onStatus = onStatus;
@@ -38,7 +56,7 @@ export class WhepPlayer {
async start() {
if (!this.url || !this.video) {
throw new Error('Video target missing');
throw new Error('Media target missing');
}
this.stop();
this.notify('connecting');
@@ -54,8 +72,12 @@ export class WhepPlayer {
event.receiver.playoutDelayHint = 0;
}
};
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
if (this.audioOnly) {
pc.addTransceiver('audio', { direction: 'recvonly' });
} else {
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
}
pc.onconnectionstatechange = () => {
const state = pc.connectionState;
@@ -71,7 +93,7 @@ export class WhepPlayer {
try {
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
offerToReceiveVideo: !this.audioOnly,
});
await pc.setLocalDescription(offer);
+7 -5
View File
@@ -118,11 +118,13 @@ export default function SpectatorApp() {
useSpectatorMode();
const frames = useTelemetryFrames();
const roster = session?.roster ?? [];
const entries = roster.map((rover) => ({
type: 'rover',
id: rover.id,
audioId: rover.media?.audioPublishUrl ? `${rover.id}-audio` : null,
}));
const entries = roster.flatMap((rover) => {
const base = { type: 'rover', id: rover.id, key: rover.id };
if (rover.media?.audioPublishUrl) {
return [base, { type: 'rover', id: `${rover.id}-audio`, key: `${rover.id}-audio` }];
}
return [base];
});
const videoSources = useVideoRequests(entries);
return (