mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
simplification
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}` };
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
Reference in New Issue
Block a user