+ Confirm this code on your computer to finish pairing
+
+
+ )
+}
+
+export default PairingOverlay
diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx
index aa6bef9..ea669d7 100644
--- a/src/overlay/index.tsx
+++ b/src/overlay/index.tsx
@@ -8,6 +8,7 @@ import { useActionStore } from '@src/stores/actionStore'
import { useEffect, useMemo, useState } from 'react'
import YouAreHere from './YouAreHere'
import { ServerStatus } from './ConnectionStatus'
+import { PairingOverlay } from './PairingOverlay'
import ScreenSaverWrapper from './ScreenSaver/ScreenSaverWrapper'
interface OverlayProps {
@@ -49,6 +50,7 @@ const Overlays: React.FC = ({ children }) => {
{!preferences.onboarding || }
+
{!isConnected && }
diff --git a/src/stores/musicStore.ts b/src/stores/musicStore.ts
index 3bebd33..a9a6d21 100644
--- a/src/stores/musicStore.ts
+++ b/src/stores/musicStore.ts
@@ -34,6 +34,20 @@ export interface MusicState {
setShuffle: () => void
}
+/**
+ * Spotify encodes the artwork size in the CDN path prefix: 0000b273 is 640x640
+ * (~110KB) while 00001e02 is 300x300 (~36KB). The Car Thing panel is 800x480, so
+ * the 640px asset is mostly wasted bytes — and on a Bluetooth-tunneled connection
+ * (~155KB/s) it costs about 0.7s of a saturated link on every track change.
+ * Non-Spotify URLs are left untouched.
+ */
+const preferSmallerArtwork = (url: string): string =>
+ // Match the bare id prefix rather than "/image/": by the time we see the
+ // URL the server has usually wrapped it in /proxy/v1?url=..., where the
+ // slashes are percent-encoded but the id is not. Keying off the id alone
+ // works for both the raw and the wrapped form.
+ url.replace(/ab67616d0000b273/g, 'ab67616d00001e02')
+
export const useMusicStore = create((set, get) => ({
song: null,
setSong: (newData) => {
@@ -49,11 +63,16 @@ export const useMusicStore = create((set, get) => ({
if (context.id == ClientPlatformIDs.CarThing || context.ip == 'localhost') {
if (newData.thumbnail.includes(`${context.ip}:${context.port}`)) return // already parsed as a corrected IP
- newData.thumbnail = `http://${context.ip}:${context.port}/proxy/v1?url=${encodeURIComponent(newData.thumbnail)}`
+ newData.thumbnail = `http://${context.ip}:${context.port}/proxy/v1?url=${encodeURIComponent(preferSmallerArtwork(newData.thumbnail))}`
}
} else if (newData.thumbnail.startsWith('/')) {
+ // The server normally hands us a relative /proxy/v1?url=... — this is
+ // the path Spotify artwork actually takes, so the size preference has
+ // to be applied here too or it never runs at all.
const context = useSettingsStore.getState().manifest?.context
- newData.thumbnail = `http://${context.ip}:${context.port}${newData.thumbnail}`
+ newData.thumbnail = preferSmallerArtwork(
+ `http://${context.ip}:${context.port}${newData.thumbnail}`
+ )
}
}
diff --git a/src/utils/websocketManager.ts b/src/utils/websocketManager.ts
index 546d120..8ee7e47 100644
--- a/src/utils/websocketManager.ts
+++ b/src/utils/websocketManager.ts
@@ -8,6 +8,10 @@ import {
type SocketEventListener = (msg: DeskThingToDeviceCore & { app?: string }) => void
type ConnectionStatus = 'connected' | 'disconnected' | 'reconnecting'
+
+/** Reconnect backoff: quick first attempt, then ease off to this ceiling. */
+const RECONNECT_BASE_MS = 500
+const RECONNECT_MAX_MS = 10000
type StatusListener = (status: ConnectionStatus) => void
/**
@@ -19,6 +23,7 @@ export class WebSocketManager {
private listeners: SocketEventListener[] = []
private statusListeners: StatusListener[] = []
private reconnecting = false
+ private reconnectAttempts = 0
private url: string
private heartbeatInterval: NodeJS.Timeout | null = null
private pongTimeout: NodeJS.Timeout | null = null
@@ -52,8 +57,16 @@ export class WebSocketManager {
this.socket.onerror = null
this.socket.onmessage = null
- // Close connection if open
- if (this.socket.readyState === WebSocket.OPEN) {
+ // Close the connection if it is open OR still connecting. A socket left
+ // in CONNECTING (e.g. the server was unreachable when we dialed — the
+ // case on a Bluetooth link that comes up ~80s after the device boots)
+ // must be closed too, or it lingers half-open, holds a tunnel stream,
+ // and blocks the client from ever cleanly reconnecting once the link is
+ // up. Only closing OPEN sockets leaked these on every failed attempt.
+ if (
+ this.socket.readyState === WebSocket.OPEN ||
+ this.socket.readyState === WebSocket.CONNECTING
+ ) {
await this.socket.close(4000, 'Closing existing connection')
}
@@ -97,6 +110,7 @@ export class WebSocketManager {
clearTimeout(timeout)
console.info(`[${id}] Connected to ${this.url}`)
this.reconnecting = false
+ this.reconnectAttempts = 0 // connected: start the backoff over
this.startHeartbeat()
this.notifyStatusChange('connected')
resolve()
@@ -112,6 +126,7 @@ export class WebSocketManager {
this.socket.onopen = () => {
console.info(`[${id}] Connected to ${this.url}`)
this.reconnecting = false
+ this.reconnectAttempts = 0 // connected: start the backoff over
this.startHeartbeat()
this.notifyStatusChange('connected')
}
@@ -125,9 +140,12 @@ export class WebSocketManager {
clearTimeout(this.closeTimeoutId)
}
+ // Retry promptly. This used to wait 5s here and then a further 10s in
+ // reconnect(), so the first attempt was 15s after a drop no matter how
+ // briefly the link had gone away.
this.closeTimeoutId = setTimeout(() => {
this.reconnect()
- }, 5000)
+ }, 250)
}
this.socket.onerror = (error) => {
@@ -167,7 +185,7 @@ export class WebSocketManager {
this.closeTimeoutId = setTimeout(() => {
this.reconnect()
- }, 5000)
+ }, 250)
return false
}
}
@@ -177,7 +195,6 @@ export class WebSocketManager {
}
reconnect() {
- Logger.info('Reconnecting in 10s...')
if (this.reconnecting) return
this.reconnecting = true
this.notifyStatusChange('reconnecting')
@@ -188,11 +205,21 @@ export class WebSocketManager {
clearTimeout(this.reconnectId)
}
+ // Back off instead of waiting a flat 10s. A dropped link usually comes
+ // back immediately, so the first retry should be quick; only a genuinely
+ // absent server should push the delay out.
+ const delay = Math.min(
+ RECONNECT_BASE_MS * 2 ** this.reconnectAttempts,
+ RECONNECT_MAX_MS
+ )
+ this.reconnectAttempts += 1
+ Logger.info(`Reconnecting in ${delay}ms...`)
+
this.reconnectId = setTimeout(() => {
Logger.info('Conecting...')
this.connect()
this.reconnecting = false
- }, 10000) // Reconnect after 10 seconds
+ }, delay)
}
sendMessage(message: DeviceToDeskthingData) {