Fix reconnect/media failure paths and harden signaling parse
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
## Versioning & Configuration
|
## Versioning & Configuration
|
||||||
- Bump `client/public/version.js` on every user-visible change using `YYYY.MM.DD Rn`.
|
- Bump `client/public/version.js` on every user-visible change using `YYYY.MM.DD Rn`.
|
||||||
|
- Commit each completed logical change; include the version bump in that same commit when client behavior changes.
|
||||||
- Do not duplicate version constants elsewhere in client code.
|
- Do not duplicate version constants elsewhere in client code.
|
||||||
- `server/config.toml` is deployment-local and must not be committed.
|
- `server/config.toml` is deployment-local and must not be committed.
|
||||||
- Production should use TLS (`network.allow_insecure_ws = false`).
|
- Production should use TLS (`network.allow_insecure_ws = false`).
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.20 R57";
|
window.CHGRID_WEB_VERSION = "2026.02.20 R58";
|
||||||
|
|||||||
@@ -577,9 +577,7 @@ async function checkMicPermission(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupLocalMedia(audioDeviceId = ''): Promise<void> {
|
async function setupLocalMedia(audioDeviceId = ''): Promise<void> {
|
||||||
if (localStream) {
|
stopLocalMedia();
|
||||||
localStream.getTracks().forEach((track) => track.stop());
|
|
||||||
}
|
|
||||||
|
|
||||||
await audio.ensureContext();
|
await audio.ensureContext();
|
||||||
|
|
||||||
@@ -604,6 +602,14 @@ async function setupLocalMedia(audioDeviceId = ''): Promise<void> {
|
|||||||
await peerManager.replaceOutgoingTrack(outboundStream);
|
await peerManager.replaceOutgoingTrack(outboundStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopLocalMedia(): void {
|
||||||
|
if (localStream) {
|
||||||
|
localStream.getTracks().forEach((track) => track.stop());
|
||||||
|
localStream = null;
|
||||||
|
}
|
||||||
|
outboundStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
function describeMediaError(error: unknown): string {
|
function describeMediaError(error: unknown): string {
|
||||||
if (error instanceof DOMException) {
|
if (error instanceof DOMException) {
|
||||||
if (error.name === 'NotAllowedError') return 'Microphone blocked. Allow mic access in browser site settings.';
|
if (error.name === 'NotAllowedError') return 'Microphone blocked. Allow mic access in browser site settings.';
|
||||||
@@ -638,14 +644,23 @@ async function connect(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.player.x = Math.floor(Math.random() * GRID_SIZE);
|
||||||
|
state.player.y = Math.floor(Math.random() * GRID_SIZE);
|
||||||
const storedPosition = localStorage.getItem('spatialChatPosition');
|
const storedPosition = localStorage.getItem('spatialChatPosition');
|
||||||
if (storedPosition) {
|
if (storedPosition) {
|
||||||
const parsed = JSON.parse(storedPosition) as { x: number; y: number };
|
try {
|
||||||
state.player.x = parsed.x;
|
const parsed = JSON.parse(storedPosition) as { x?: number; y?: number };
|
||||||
state.player.y = parsed.y;
|
if (Number.isFinite(parsed.x) && Number.isFinite(parsed.y)) {
|
||||||
} else {
|
const x = Math.floor(parsed.x as number);
|
||||||
state.player.x = Math.floor(Math.random() * GRID_SIZE);
|
const y = Math.floor(parsed.y as number);
|
||||||
state.player.y = Math.floor(Math.random() * GRID_SIZE);
|
if (x >= 0 && x < GRID_SIZE && y >= 0 && y < GRID_SIZE) {
|
||||||
|
state.player.x = x;
|
||||||
|
state.player.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed saved positions and keep randomized defaults.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -670,6 +685,7 @@ async function connect(): Promise<void> {
|
|||||||
await signaling.connect(onMessage);
|
await signaling.connect(onMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
stopLocalMedia();
|
||||||
updateStatus('Connect failed. Signaling server may be offline or unreachable.');
|
updateStatus('Connect failed. Signaling server may be offline or unreachable.');
|
||||||
connecting = false;
|
connecting = false;
|
||||||
updateConnectAvailability();
|
updateConnectAvailability();
|
||||||
@@ -686,11 +702,7 @@ function disconnect(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signaling.disconnect();
|
signaling.disconnect();
|
||||||
if (localStream) {
|
stopLocalMedia();
|
||||||
localStream.getTracks().forEach((track) => track.stop());
|
|
||||||
localStream = null;
|
|
||||||
}
|
|
||||||
outboundStream = null;
|
|
||||||
|
|
||||||
peerManager.cleanupAll();
|
peerManager.cleanupAll();
|
||||||
cleanupAllRadioRuntimes();
|
cleanupAllRadioRuntimes();
|
||||||
@@ -1627,8 +1639,9 @@ async function populateAudioDevices(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let temporaryStream: MediaStream | null = null;
|
||||||
try {
|
try {
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
temporaryStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
dom.audioInputSelect.innerHTML = '';
|
dom.audioInputSelect.innerHTML = '';
|
||||||
@@ -1665,6 +1678,8 @@ async function populateAudioDevices(): Promise<void> {
|
|||||||
updateDeviceSummary();
|
updateDeviceSummary();
|
||||||
} catch {
|
} catch {
|
||||||
updateStatus('Could not list devices.');
|
updateStatus('Could not list devices.');
|
||||||
|
} finally {
|
||||||
|
temporaryStream?.getTracks().forEach((track) => track.stop());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,12 @@ export class SignalingClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.ws.onmessage = async (event) => {
|
this.ws.onmessage = async (event) => {
|
||||||
const parsed = JSON.parse(String(event.data));
|
let parsed: unknown;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(String(event.data));
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const validated = incomingMessageSchema.safeParse(parsed);
|
const validated = incomingMessageSchema.safeParse(parsed);
|
||||||
if (!validated.success) return;
|
if (!validated.success) return;
|
||||||
await onMessage(validated.data);
|
await onMessage(validated.data);
|
||||||
|
|||||||
Reference in New Issue
Block a user