Fix iOS audio unlock retry and add API fetch retry.
This commit is contained in:
+2
-4
@@ -2,7 +2,7 @@ import { MotifEngine } from './core/MotifEngine';
|
||||
import { MIDIService } from './services/MIDIService';
|
||||
import { MIDIParser } from './midi/MIDIParser';
|
||||
import { SoundfontMIDIPlayer } from './synthesis/SoundfontMIDIPlayer';
|
||||
import { getAudioContext, isAudioReady, unlockAudio } from './utils/audioUnlock';
|
||||
import { getAudioContext, isAudioReady, peekAudioContext, unlockAudio } from './utils/audioUnlock';
|
||||
import type { NoteEvent } from './types';
|
||||
|
||||
class MotifApp {
|
||||
@@ -209,9 +209,7 @@ class MotifApp {
|
||||
this.iosAudioBanner.style.display = ready ? 'none' : 'block';
|
||||
|
||||
// Optional tiny state readout (helps support debugging)
|
||||
const ctx = (() => {
|
||||
try { return getAudioContext(); } catch { return null; }
|
||||
})();
|
||||
const ctx = peekAudioContext();
|
||||
if (!ready && ctx) {
|
||||
this.iosAudioState.style.display = 'block';
|
||||
this.iosAudioState.textContent = `Audio: ${ctx.state} @ ${ctx.sampleRate}Hz`;
|
||||
|
||||
+29
-13
@@ -51,25 +51,41 @@ export class MIDIService {
|
||||
}
|
||||
}
|
||||
|
||||
async search(query: string): Promise<MIDISearchResult[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/midi/search?q=${encodeURIComponent(query)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed: ${response.status}`);
|
||||
private async fetchWithRetry(url: string, init?: RequestInit, timeoutMs = 20000): Promise<Response> {
|
||||
const attempt = async (): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
const timeout = window.setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
// iOS Safari can behave oddly with cached API responses; force no-store.
|
||||
return await fetch(url, {
|
||||
...init,
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
|
||||
const data: MIDISearchResponse = await response.json();
|
||||
return data.results;
|
||||
} catch (error) {
|
||||
console.error('MIDI search error:', error);
|
||||
return [];
|
||||
try {
|
||||
return await attempt();
|
||||
} catch (e) {
|
||||
// One fast retry for transient iOS/network hiccups.
|
||||
await new Promise(r => window.setTimeout(r, 200));
|
||||
return await attempt();
|
||||
}
|
||||
}
|
||||
|
||||
async search(query: string): Promise<MIDISearchResult[]> {
|
||||
const response = await this.fetchWithRetry(`${this.baseUrl}/api/midi/search?q=${encodeURIComponent(query)}`);
|
||||
if (!response.ok) throw new Error(`Search failed: ${response.status}`);
|
||||
const data: MIDISearchResponse = await response.json();
|
||||
return data.results;
|
||||
}
|
||||
|
||||
async fetchMIDI(url: string): Promise<ArrayBuffer | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/midi/fetch?u=${encodeURIComponent(url)}`);
|
||||
const response = await this.fetchWithRetry(`${this.baseUrl}/api/midi/fetch?u=${encodeURIComponent(url)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
@@ -84,7 +100,7 @@ export class MIDIService {
|
||||
|
||||
async parseMIDI(url: string): Promise<ParsedMIDIInfo | null> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/midi/parse?u=${encodeURIComponent(url)}`);
|
||||
const response = await this.fetchWithRetry(`${this.baseUrl}/api/midi/parse?u=${encodeURIComponent(url)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Parse failed: ${response.status}`);
|
||||
|
||||
@@ -23,6 +23,14 @@ export function getAudioContext(): AudioContext {
|
||||
return sharedAudioContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the existing AudioContext without creating one.
|
||||
* Useful for UI/debug without triggering iOS restrictions.
|
||||
*/
|
||||
export function peekAudioContext(): AudioContext | null {
|
||||
return sharedAudioContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock audio for iOS/Safari. Safe to call multiple times.
|
||||
* Returns the AudioContext once it's confirmed running.
|
||||
@@ -30,12 +38,17 @@ export function getAudioContext(): AudioContext {
|
||||
* Must be called from a user gesture (click/touchend).
|
||||
*/
|
||||
export async function unlockAudio(): Promise<AudioContext> {
|
||||
// Return existing unlock in progress
|
||||
if (unlockPromise) {
|
||||
return unlockPromise;
|
||||
}
|
||||
const ctx = sharedAudioContext;
|
||||
if (ctx?.state === 'running') return ctx;
|
||||
|
||||
unlockPromise = doUnlock();
|
||||
// IMPORTANT: allow retries. If a previous unlock attempt ran outside a user gesture
|
||||
// or failed to transition the context to running, iOS can remain blocked.
|
||||
// We only de-dupe concurrent calls; once an attempt finishes, we clear the promise.
|
||||
if (unlockPromise) return unlockPromise;
|
||||
|
||||
unlockPromise = doUnlock().finally(() => {
|
||||
unlockPromise = null;
|
||||
});
|
||||
return unlockPromise;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user