diff --git a/PRIMER.md b/PRIMER.md index 057f079..e5d0557 100644 --- a/PRIMER.md +++ b/PRIMER.md @@ -820,6 +820,7 @@ Automatic, from the show's location; there is no setting. `lib/units.js` reverse | 0.33.282-master.1 | **Updates page: earlier updates fold behind one button.** When up to date, the What's new box listed every earlier release under an "Earlier updates" heading, which looked busy. Now only your version's notes show, followed by a "Show earlier updates (N)" button (`details.whats-new-earlier`, label switches to "Hide earlier updates" when open). The list of updates waiting to install (not up to date) is unchanged. Main only: ShowPilot-Lite has no Updates page (it updates through FPP's Plugin Manager). Checked: `node --check` on the admin inline scripts; headless render of the Updates page with six releases of notes, closed and open. | | 0.33.282-master.2 | **Updates page on Master shows the Master updates you have.** Up to date on Master, What's new showed only GitHub Release notes, so the newest entry was the last release, not the Master update installed. `routes/updates.js` now, for a Master version that's up to date, finds the newest release older than the running version and lists the commits from that tag to this install's commit (new `updater.changesSince(tag, head)`: one compare request per pair, kept in the comparison cache, null when GitHub can't answer), keeps those whose version is `-master.N`, and puts them before the release notes. The earlier-updates fold from master.1 applies. Main only (the updater isn't in Lite). Checked: `node --check`; the status route run with the channel, version and GitHub answers stubbed from real data (release notes and the v0.33.281…main comparison): the list starts with 0.33.282-master.1, then 0.33.281 and earlier releases. | | 0.33.282 | **Release: Updates page tidy-up (master.1, master.2); "Detect my delay" turned off; iPhone mic fix.** Bundles 0.33.282-master.1 and master.2. **"Detect my delay" is hidden on every device:** `LT_DETECT_ENABLED = false` in `rf-compat.js` makes `ltDetectOk` false, so the Audio timing sheet no longer shows the button. Why: it could never work on Android (opening the mic in Chrome switches the phone to call audio and silences the player), and the maintainer's rule is that a feature that can't work on every phone stays out; the native app's own calibrator covers it. All the code stays; flip the constant to bring it back. Also adds the iPhone "Detect my delay" fix first shipped as 0.33.280-beta.4: reported in PR #71 by jddocea β€” on iPhone Safari the mic failed with "audio session category is not compatible with audio capture", because the player keeps the iOS audio session on `'playback'` (so it ignores the mute switch) and that mode doesn't allow recording. `ltGetMic()` now switches `navigator.audioSession.type` to `'play-and-record'` just before `getUserMedia`; `ltDetect()` already sets it back to `'playback'` when the mic closes (success and error paths). The PR's version changed all four `'playback'` assignments, which would have kept every iPhone listener in record mode and cancelled the reset after measuring, so only the mic path changes. The mic fix stays in the code for when the button returns. Player cache-buster 153 (beta used 152). Checked: `node --check`. Credit: jddocea (diagnosis and first fix). | +| 0.33.283 | **iPhone: Measure sync and Compare devices can use the microphone.** Why: after 0.33.282 an operator with Settings β†’ Debug β†’ Microphone sync measurement turned on got "Measurement failed: AudioSession category is not compatible with audio capture" from **🎀 Measure sync** on iPhone Safari. 0.33.282 only fixed `ltGetMic()` (Detect my delay); `micMeasure()` and `compareRun()` call `getUserMedia` themselves. New helpers `micSessionRecord()` / `micSessionDone()`: switch `navigator.audioSession.type` to `'play-and-record'` just before `getUserMedia`, and in `finally` set it back to `'playback'` and run `ltRestoreAudio()` (same engine rebuild Detect my delay uses). Only when `navigator.audioSession` exists (Safari); desktop and Android unchanged. Player cache-buster 154. Checked: `node --check`. Main only (audio player). | **Plugin version history (this session):** | Version | Change | diff --git a/lib/viewer-renderer.js b/lib/viewer-renderer.js index ea5e3a6..502f7db 100644 --- a/lib/viewer-renderer.js +++ b/lib/viewer-renderer.js @@ -1383,7 +1383,7 @@ ${visitorMessages.BOX_CSS}${showButtons.CSS} ${cfg.debug_mic_measure === 1 ? '' : ''} - + `; if (html.includes('')) { html = html.replace('', injection + ''); diff --git a/package.json b/package.json index 9a9671b..a453cb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "showpilot", - "version": "0.33.282", + "version": "0.33.283", "description": "Self-hosted light show viewer control server. Falcon Player (FPP) plugin compatible. Replaces Remote Falcon for self-hosters.", "main": "server.js", "scripts": { diff --git a/public/admin/index.html b/public/admin/index.html index ae82b11..4fe3106 100644 --- a/public/admin/index.html +++ b/public/admin/index.html @@ -2112,7 +2112,7 @@

ShowPilot Admin

ShowPilot - v0.33.282 + v0.33.283 OFF diff --git a/public/rf-compat.js b/public/rf-compat.js index 4e4d8a9..a6db80a 100644 --- a/public/rf-compat.js +++ b/public/rf-compat.js @@ -3796,6 +3796,22 @@ const refStartSec = Math.max(0, expected - 1.0); return { mic: cap.mic, ref: monoSlice(currentBuffer, refStartSec, cap.mic.length + Math.round(2.2 * sr)), refStartSec, expectedPosSec: expected }; } + // iOS Safari refuses the mic while the audio session is 'playback' + // ("AudioSession category is not compatible with audio capture"), the + // same problem #71 found in Detect my delay. Measure sync and Compare + // devices switch to play-and-record only while the mic is open, then go + // back to 'playback' and rebuild the audio engine like Detect my delay + // does. Only where navigator.audioSession exists (Safari); elsewhere + // nothing changes. + function micSessionRecord() { + if (!navigator.audioSession) return false; + try { navigator.audioSession.type = 'play-and-record'; return true; } catch (_) { return false; } + } + function micSessionDone(switched) { + if (!switched) return; + try { navigator.audioSession.type = 'playback'; } catch (_) {} + try { ltRestoreAudio(); } catch (_) {} + } async function micMeasure() { if (micBusy) return; if (!window.isSecureContext || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { @@ -3806,8 +3822,9 @@ micBusy = true; const buf = currentBuffer; const wasMuted = isMuted; - let stream = null; + let stream = null, iosSwitched = false; try { + iosSwitched = micSessionRecord(); stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } }); if (gainNode) gainNode.gain.value = 1; micPanel('Step 1 of 2 β€” listening with the phone playing (5 s)… keep the phone\'s volume up and stay still.'); @@ -3846,6 +3863,7 @@ micPanel(e && e.name === 'NotAllowedError' ? 'Microphone permission was denied.' : 'Measurement failed: ' + (e && e.message ? e.message : e)); } finally { if (stream) stream.getTracks().forEach(tr => tr.stop()); + micSessionDone(iosSwitched); micBusy = false; } } @@ -3915,8 +3933,9 @@ micBusy = true; const buf = currentBuffer; const wasMuted = isMuted; - let stream = null; + let stream = null, iosSwitched = false; try { + iosSwitched = micSessionRecord(); stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } }); if (gainNode) gainNode.gain.value = 1; micPanel('Comparing β€” step 1 of 2: listening with this device playing (4 s)…'); @@ -3952,6 +3971,7 @@ return fail(e && e.name === 'NotAllowedError' ? 'microphone permission was denied' : (e && e.message ? e.message : String(e))); } finally { if (stream) stream.getTracks().forEach(tr => tr.stop()); + micSessionDone(iosSwitched); micBusy = false; } }