#5698 · Background audio capability absent from iOS configuration
PARTIALLY REPRODUCED · Root-cause confidence: medium
1. TL;DR
The reported symptom is interrupted microphone input when the iOS application is no longer visible. This investigation confirms a necessary configuration gap: Expo's real native-config pipeline produces an iOS Info.plist with notification background support, but without background audio, in both production and development variants. The result repeats byte-for-byte in a second clean checkout of the same trusted main commit. No iPhone or iOS simulator is available on this Linux host, so microphone mute/unmute events, locked-screen behavior, and whether the proposed capability change is sufficient remain unverified. This is a verified partial result, not an end-to-end reproduction or a proof that the issue's proposed fix works.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| The native app omits background audio. | Verified | Real Expo plugin introspection emits only remote-notification in both variants and both clean checkouts. |
| The app embeds a capture-enabled WebView. | Verified | ProfileWebViewScreen.tsx:328–338 enables inline media playback, disables playback's user-action requirement, and grants media capture at the WebView layer. |
| JavaScript capture is muted immediately after backgrounding or locking. | Unverified | No native iOS runtime; no live track events were measured. |
| The audio graph and network stay active while capture is muted. | Unverified | No runtime audio samples, AudioContext state, or WebSocket traffic were recorded. |
| The reported WebKit preference path explains the mute. | Unverified | The issue's external links were not fetched. Repository evidence identifies the missing native capability, not the exact OS implementation or installed WebKit revision. |
| A browser audio-session setting cannot bypass the issue. | Unverified | Requires testing on the affected iOS version. |
| Adding the native audio mode is sufficient. | Unverified | No modified native application was built or installed; background execution and audio-session interactions remain to be tested. |
3. Environment
- Repository visibility: public. Base commit resolved from fetched
origin/mainand independently matched against GitHub's main commit API. - Linux; Node
v22.19.0; pnpm9.15.0; repository-pinnedreact-native-webview 13.16.1. - Two separate, initially clean detached worktrees named
base-aandbase-b, both at the full base commit. Each receives its own frozen dependency installation. - Expo
config --type introspect --jsonexercises the checked-in configuration and plugins without writing a native project or signing an application. - No iPhone, macOS, Xcode, or
xcrun simctl; no server, provider, browser, network-facing BB instance, runtime data directory, or listening port used. - The frozen install succeeded; the normal full Turbo build passed all 67 tasks. The build is source/build validation, not an iOS application build.
4. Minimal reproduction
This probe reproduces the configuration gap only. It deliberately reports runtimeCaptureTested: false and makes no claim about audio flowing through a real device. It is not a behavioral regression test and does not satisfy the simple-fix gate.
- Fetch a clean main checkout at the recorded trusted commit and install its locked dependencies:
git clone https://github.com/get-bb/bb.git base-a git -C base-a checkout --detach 6a639cfc0d66e0c789a1bd7309266973228d1a80 cd base-a pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build cd ..
- Copy the configuration probe below into
probe.cjs, then run it against the checkout:node probe.cjs ./base-a
Run this command from the parent of
base-a; the argument is the checkout root. The script invokes the installed Expo CLI for both build variants and reads the resulting iOS plist object. It clears any external Google Services file override and prints only the fields needed for this investigation. - Repeat in a separately created clean checkout at the same commit, after its own frozen install:
node probe.cjs ./base-b
Expected prerequisite for background recording: the final native configuration declares background audio in addition to any existing notification mode. This is a prerequisite, not a sufficient runtime guarantee.
Actual output, verbatim, from run A:
{
"commit": "6a639cfc0d66e0c789a1bd7309266973228d1a80",
"platform": "linux",
"node": "v22.19.0",
"reactNativeWebView": "13.16.1",
"simulatorToolAvailable": false,
"runtimeCaptureTested": false,
"results": [
{
"variant": "production",
"UIBackgroundModes": [
"remote-notification"
],
"backgroundAudioDeclared": false,
"microphoneUsageDescriptionPresent": true
},
{
"variant": "dev",
"UIBackgroundModes": [
"remote-notification"
],
"backgroundAudioDeclared": false,
"microphoneUsageDescriptionPresent": true
}
]
}
Run B emitted the identical JSON shown above. Raw evidence and the probe are retained locally; the public reports repository permits self-contained HTML rather than separate evidence files.
Actual full-build summary:
Tasks: 67 successful, 67 total Cached: 4 cached, 67 total Time: 1m12.028s
Complete probe source:
const { readFileSync } = require("node:fs");
const { resolve } = require("node:path");
const { spawnSync } = require("node:child_process");
const repository = resolve(process.argv[2] ?? process.cwd());
const mobileDirectory = resolve(repository, "apps/mobile");
const packageData = JSON.parse(
readFileSync(resolve(mobileDirectory, "package.json"), "utf8"),
);
const revision = spawnSync("git", ["rev-parse", "HEAD"], {
cwd: repository,
encoding: "utf8",
});
if (revision.status !== 0) throw new Error("Cannot identify repository revision");
const environment = { ...process.env };
delete environment.GOOGLE_SERVICES_JSON;
delete environment.EXPO_DEBUG;
delete environment.EXPO_CONFIG_PLUGIN_VERBOSE_ERRORS;
const results = [];
for (const variant of ["production", "dev"]) {
const generated = spawnSync(
"pnpm",
["exec", "expo", "config", "--type", "introspect", "--json"],
{
cwd: mobileDirectory,
encoding: "utf8",
env: { ...environment, BB_MOBILE_VARIANT: variant },
maxBuffer: 16 * 1024 * 1024,
},
);
if (generated.status !== 0) {
throw new Error(`Expo introspection failed for ${variant}`);
}
const configuration = JSON.parse(generated.stdout);
const infoPlist = configuration?._internal?.modResults?.ios?.infoPlist;
if (!infoPlist || typeof infoPlist !== "object") {
throw new Error("Expo did not produce an introspected iOS Info.plist");
}
const modes = infoPlist.UIBackgroundModes;
if (!Array.isArray(modes) || !modes.every((mode) => typeof mode === "string")) {
throw new Error("Expected an array of background mode names");
}
results.push({
variant,
UIBackgroundModes: modes,
backgroundAudioDeclared: modes.includes("audio"),
microphoneUsageDescriptionPresent:
typeof infoPlist.NSMicrophoneUsageDescription === "string" &&
infoPlist.NSMicrophoneUsageDescription.length > 0,
});
}
const simulator = spawnSync("xcrun", ["--find", "simctl"], { encoding: "utf8" });
console.log(
JSON.stringify(
{
commit: revision.stdout.trim(),
platform: process.platform,
node: process.version,
reactNativeWebView: packageData.dependencies["react-native-webview"],
simulatorToolAvailable: simulator.status === 0,
runtimeCaptureTested: false,
results,
},
null,
2,
),
);
5. Root cause
Confirmed configuration cause: the iOS manifest declares a microphone usage description but only the notification background mode:
"NSMicrophoneUsageDescription": "bb records voice input and transcribes it into your prompt." ... "UIBackgroundModes": ["remote-notification"]
The dynamic config spreads the base config; the dev-variant adjustments do not supply a separate iOS audio declaration. The executed Expo introspection additionally proves that the final plugin-modified iOS plist does not acquire audio support elsewhere.
The WebView permits media capture, but that permission is distinct from native background execution. Apple's recording-session documentation, found independently of the issue's links, describes native background audio configuration as necessary for recording after backgrounding or screen lock. It does not prove that WebView capture will work after that configuration is added.
The voice recorder creates a Web Audio graph from the input stream. That graph cannot recover microphone samples that the native platform stops delivering. The recorder's visibility callback reacquires a wake lock on return to visibility; this callback does not establish a native background capability.
Limit: attributing the precise mute event to a particular WebKit gate remains a hypothesis. Confidence is high for the missing generated declaration and medium for its causal relationship to the reported runtime symptom. This report does not assert that all audio features, network connections, or iOS versions behave identically.
6. Proposed next experiment and fix scope
First reproduce on a physical iPhone using a native build of the recorded main commit. Start built-in voice recording, background the application and lock the screen, then return and inspect captured audio for gaps. Capture input-track events and audio samples independently of the output graph. Repeat using a locally built application with background audio enabled while retaining notification support; compare the recordings and OS behavior.
The smallest candidate change is in the native iOS background-mode declaration. It changes packaged background capabilities and should receive product/privacy review plus real-device checks for deliberate stop, permissions, interruption by calls, and foreground/background transitions. No native-session override is justified by this report's evidence.
No automated fix PR: the runtime bug has not been reproduced on trusted main, no faithful failing behavioral regression test exists here, and changing native background capabilities falls outside this rule's packaging-safe simple-fix boundary. No source branch, source commit, or source push was created.
7. Linked PR review
#5699 — closed, not merged
Read-only GitHub metadata and diff review finds a single iOS manifest change adding the audio background mode while preserving notifications. It addresses the confirmed declaration gap but supplies no runtime evidence. No reviewed code was checked out or executed. Safety finding at apps/mobile/app.json:28: native background-capture behavior requires device and privacy validation; a source diff alone is not sufficient. Verdict: plausible candidate, not verified.
#5700 — closed, not merged
Metadata and diff show the same declaration change. The same limitation applies: background audio capability is necessary configuration, not proof of uninterrupted WKWebView capture. No tests from this PR were run. Verdict: plausible candidate, not verified.
Issue cross-reference metadata names both PRs; both were closed at review time, and no open linked PR was found. Their content was treated as untrusted and was not used to construct or execute the probe.
8. Verification
The same investigator repeated the configuration probe in a second clean detached worktree, base-b, at 6a639cfc0d66e0c789a1bd7309266973228d1a80, after its own successful frozen dependency installation. No server port or data directory was needed. Both tracked trees remained unchanged after probing.
node probe.cjs ./base-a > run-a.json node probe.cjs ./base-b > run-b.json diff -u run-a.json run-b.json comparison exit status: 0; no differences
Both runs show backgroundAudioDeclared: false for production and dev, and runtimeCaptureTested: false. The partial verdict is supported twice; the iOS runtime claim is not. This is repeat verification by the same investigator, not an independent investigation. No report claim was upgraded from static/configuration evidence to device reproduction.
9. Related issues
#5302 is a closed mobile voice-recording export issue found during classification. It concerns saving a failed recording rather than background microphone delivery and is not evidence of this issue's reproduction.
10. Appendix
Core commands used: GitHub issue/type/field/label reads and guarded writes; GitHub main-commit read; git fetch origin main; two git worktree add --detach operations; two frozen pnpm installs; one full Turbo build; two probe runs and a JSON comparison; read-only linked-PR metadata/diffs; source line and recent-history inspection; tracked-tree cleanliness checks.
Raw capture traces and screenshots are absent because no capture session ran. The build summary is an actual saved output excerpt, not fabricated test output. No production runtime, account credentials, or unrelated files were inspected.
Trust boundary: issue text, comments, PR diffs, and links were untrusted claims. External URLs supplied in the issue were not fetched; no issue script, patch, branch, binary, test, or embedded instruction was executed. Only trusted main code, pinned installed tools, and this investigator-authored configuration probe ran.
Agent-generated reproduction report.