steep-balloon-41261
05/19/2026, 3:07 PMtall-airline-16756
05/19/2026, 3:08 PMtall-airline-16756
05/19/2026, 3:08 PMcaptureScreenAudio: false and RPSystemBroadcastPickerView.showsMicrophoneButton = false do not reliably prevent the system from activating replayd audio capture on iOS 26.
• Because the extension cannot control system replayd internals, you cannot guarantee suppression from the extension side. Workarounds to try:
◦ Ensure the picker is presented with microphone button hidden (you already do this).
◦ Try toggling mic availability in the main app before starting the broadcast (see question 3).
◦ Consider delaying or temporarily stopping the main app’s active AVAudioSession or local mic publish before invoking the broadcast picker so replayd may not see an in-use mic conflict. (I’ll give concrete steps after your answer on whether the mic is published when tapping Share.)
2) Interaction between main-app WebRTC mic and ReplayKit on iOS 26
• iOS 26 can treat the extension’s replayd audio capture as a competing audio client and will terminate the broadcast when the system decides the audio clients conflict; your logs showing RPMicAudioCaptureManager/RPAppAudioCaptureManager stop immediately before broadcastFinished() match that behavior.
• This means an active .playAndRecord session in the main app can conflict with ReplayKit’s capture path on iOS 26 and cause the system to interrupt the broadcast after seconds.
3) Recommended AVAudioSession configuration for simultaneous room + broadcast
• The reliable approach is to avoid two active, competing audio sessions at the same time. Options:
◦ Best-first: Temporarily suspend the main app’s mic capture / local audio publishing before starting the broadcast. Sequence: 1) unpublish/stop local audio track and deactivate or lower the main app AVAudioSession, 2) start the broadcast picker, 3) after broadcast is confirmed started, optionally re-evaluate if local mic is needed and coordinate re-enable with care (expect conflicts).
◦ If you must keep main-app mic active, try using AVAudioSessionCategoryPlayAndRecord with `AVAudioSessionModeVoiceChat`/`voiceProcessing` and the mixWithOthers option (you already tried mixWithOthers). mixWithOthers alone may not avoid the system-level conflict on iOS 26.
◦ Another pattern: mute the published audio track (so the local track exists but does not actively drive hardware) then deactivate the session if possible. The core goal is to avoid two simultaneous active capture clients from the system perspective.
4) writeback pool capacity exceeded + mitigations
• The writeback pool errors indicate replayd’s virtual display writeback pool is being exhausted. Reducing frame size, rate, and JPEG quality (you already did) are correct mitigations. Additional mitigations to attempt:
◦ Lower maxFrameRate further (e.g., 10 or 8 fps) and reduce resolution more aggressively in SampleUploader.prepare.
◦ Reduce CPU/GPU load elsewhere in the app during broadcast (stop nonessential renderers or view layers).
◦ Ensure the extension remains tiny (you already removed LiveKitClient).
◦ Consider using hardware-accelerated pixel formats if available in your SampleUploader pipeline, to avoid expensive software conversions.
• There is no application-level LiveKit setting that directly controls replayd writeback pool limits — this is an OS-level resource. If those mitigations don’t help, coordinating lower resource usage plus the audio-session changes above is the likely path.
Next steps I can provide (pick which you want)
• A tight sequence you can try to avoid conflict (code-level steps for unpublishing/deactivating audio, then starting broadcast, then re-enabling) — I’ll provide sample pseudo-code for Flutter+native if you confirm the main app is publishing mic when tapping Share.
• Debug logging suggestions to confirm whether the main app’s AVAudioSession is active at the time replayd starts (specific AVAudioSession state checks, timestamps to correlate with replayd logs).
• Additional tuning suggestions for SampleUploader (frame dropping, alternate pixel formats).
If you confirm whether the mic is actively published when you tap Share, I’ll give the concrete sequence and sample code to try next.
, with interactive elementsaloof-lion-11633
05/19/2026, 3:10 PMtall-airline-16756
05/19/2026, 3:11 PMiOS_BroadcastStarted notification?
If you confirm both, I’ll provide the Flutter + native pseudo-code sequence (unpublish/quiesce, start broadcast picker, wait for Darwin notification, re-acquire mic) plus notes on timing, AVAudioSession calls, and how to avoid re-triggering the audio-client conflict.
, with interactive elementsaloof-lion-11633
05/19/2026, 3:12 PMtall-airline-16756
05/19/2026, 3:12 PMaloof-lion-11633
05/19/2026, 3:13 PMtall-airline-16756
05/19/2026, 3:13 PMFuture<void> toggleScreenShareStart() async {
// 1) Step: unpublish/stop local audio
final localAudioPublication = room.localParticipant?.getTrackPublication('audio');
if (localAudioPublication != null) {
// Acceptable to unpublish temporarily
await room.localParticipant!.unpublishTrack(localAudioPublication.track!);
_wasAudioPublished = true;
} else {
_wasAudioPublished = false;
}
// 2) Tell native to deactivate AVAudioSession (stop hardware capture)
await pipChannel.invokeMethod('deactivateAudioSession');
// 3) Present the broadcast picker (native) — you can also present it from Dart if implemented
await pipChannel.invokeMethod('showBroadcastPicker');
// 4) Wait for iOS_BroadcastStarted Darwin notification (forwarded from native)
await _waitForBroadcastStarted(timeout: Duration(seconds: 10));
// 5) Re-activate audio session and re-publish
await pipChannel.invokeMethod('reactivateAudioSession');
if (_wasAudioPublished) {
// Re-create local audio track if needed and publish
final audioTrack = await createAndPublishLocalAudioTrack(); // implement per your app (LiveKit APIs)
}
}
2) Listening for broadcast-started event forwarded from native
// register once at app startup
pipChannel.setMethodCallHandler((call) async {
if (call.method == 'onBroadcastStarted') {
_broadcastStartedCompleter?.complete();
} else if (call.method == 'onBroadcastStopped') {
// optional: handle stop
}
});
Future<void> _waitForBroadcastStarted({Duration timeout = const Duration(seconds:10)}) {
_broadcastStartedCompleter = Completer<void>();
return _broadcastStartedCompleter!.future.timeout(timeout);
}
3) createAndPublishLocalAudioTrack()
• Use your existing flow to create a local audio track and publish it to the room. This may be:
◦ create LocalAudioTrack (or use the SDK helper), then room.localParticipant?.publishTrack(localAudioTrack).
• If your app used setMicrophoneEnabled(true) to toggle, re-invoke that after session activation.
Swift (App side) — minimal helpers invoked via pip_channel 1) deactivateAudioSession
• Purpose: fully stop capture and deactivate AVAudioSession so ReplayKit's replayd can acquire audio without conflict.
• Calls:
import AVFoundation
@objc func deactivateAudioSession(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let session = AVAudioSession.sharedInstance()
do {
try session.setActive(false, options: .notifyOthersOnDeactivation)
result(nil)
} catch {
result(FlutterError(code: "audio_error", message: "deactivate failed", details: error.localizedDescription))
}
}
2) reactivateAudioSession
• Purpose: set category/mode/options and reactivate for WebRTC capture.
• Recommendation:
◦ Category: .playAndRecord
◦ Mode: .voiceChat (or .voiceProcessing if needed)
◦ Options: [.allowBluetooth, .allowBluetoothA2DP, .allowAirPlay] (avoid .mixWithOthers for reactivation if system conflict occurred, but you can include it if you need mixing)
• Calls (order: setCategory → setMode (optional) → setActive(true)):
@objc func reactivateAudioSession(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord,
options: [.allowBluetooth, .allowBluetoothA2DP, .allowAirPlay])
try session.setMode(.voiceChat)
try session.setActive(true)
result(nil)
} catch {
result(FlutterError(code: "audio_error", message: "reactivate failed", details: error.localizedDescription))
}
}
3) showBroadcastPicker (if you present natively)
• Present RPSystemBroadcastPickerView or show the broadcast UI appropriate to your flow. Your sample already does this.
4) Darwin observer in AppDelegate -> forward to Dart
• Observe Darwin CFNotificationCenterGetDarwinNotifyCenter() notifications your extension posts.
• On receiving iOS_BroadcastStarted, invoke Dart via the pip_channel:
// In AppDelegate after Flutter engine/channel are ready
func registerDarwinNotifications() {
let center = CFNotificationCenterGetDarwinNotifyCenter()
let callback: CFNotificationCallback = { (center, observer, name, object, userInfo) in
guard let name = name?.rawValue as String? else { return }
if name == "iOS_BroadcastStarted" {
DispatchQueue.main.async {
// pipChannel is your FlutterMethodChannel
pipChannel.invokeMethod("onBroadcastStarted", arguments: nil)
}
} else if name == "iOS_BroadcastStopped" {
DispatchQueue.main.async {
pipChannel.invokeMethod("onBroadcastStopped", arguments: nil)
}
}
}
CFNotificationCenterAddObserver(center,
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
callback,
"iOS_BroadcastStarted" as CFString,
nil,
.deliverImmediately)
CFNotificationCenterAddObserver(center,
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
callback,
"iOS_BroadcastStopped" as CFString,
nil,
.deliverImmediately)
}
Broadcast extension (SampleHandler)
• On broadcastStarted, post Darwin `iOS_BroadcastStarted`:
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(),
CFNotificationName("iOS_BroadcastStarted" as CFString),
nil,
nil,
true)
• You already have this; ensure it fires immediately in broadcastStarted().
Timing and safety notes
• Deactivate app AVAudioSession before presenting the broadcast picker. This avoids the system seeing two active capture clients.
• Wait for the iOS_BroadcastStarted notification before reactivating the app audio and publishing. The Darwin notification tells you replayd has started the broadcast pipeline; only then re-acquire mic.
• Allow a small safety margin: after receiving iOS_BroadcastStarted, wait ~200–500 ms before reactivating session to let replayd settle (tune as needed).
• If you still see immediate termination, lengthen the wait to 1s and reduce main-app audio config aggressiveness (use fewer options).
• Use unpublish/publish (not mute) for simplicity — you accepted brief Published→Unpublished→Published transitions.
Edge cases and additional tuning
• Re-publishing immediately may retrigger system arbitration in some iOS builds. If you see termination, add a slight delay after reactivation (200–1000 ms) before publishing.
• If your app wants to avoid audible glitches for remote users, you can publish a silent audio track instead of leaving them with no audio; but that adds complexity.
• Keep SampleHandler minimal and reduce frame rate/resolution as you plan (8 fps, smaller scale). That helps the writeback pool issues.
Summary checklist
• Implement Dart-first orchestration:
◦ unpublish local audio → invoke Swift deactivateAudioSession → show broadcast picker → wait for onBroadcastStarted from Swift → wait 200–500 ms → invoke reactivateAudioSession → re-create & publish audio track.
• Swift: implement deactivateAudioSession, reactivateAudioSession, Darwin observer forwarding onBroadcastStarted.
• Extension: ensure it posts iOS_BroadcastStarted in broadcastStarted().
• Tune waits/delays if termination continues; reduce frame rate/resolution further.
If you want, I can:
• Provide exact LiveKit Flutter API call examples for unpublish/publish based on the exact methods you use (share your current publish code snippet), or
• Draft the precise Swift method-channel wiring code to paste, with interactive elementsaloof-lion-11633
05/19/2026, 3:16 PMtall-airline-16756
05/19/2026, 3:16 PMsetMicrophoneEnabled(true) flow. If you prefer preserving the same SID, I'll show how to keep and re-publish the same LocalAudioTrack instance.)
2) You already indicated you call setMicrophoneEnabled(true) to publish. Confirm you want the simple flow that uses removePublishedTrack to unpublish and setMicrophoneEnabled(true) to re-create and publish the mic, otherwise I’ll show the alternate pattern that retains a LocalAudioTrack instance.
Tell me "new SID OK" or "preserve SID", and "use setMicrophoneEnabled" or "reuse LocalAudioTrack" and I’ll return the exact livekit_client 2.6.x Dart code for the sequence.
, with interactive elementsaloof-lion-11633
05/19/2026, 3:20 PMtall-airline-16756
05/19/2026, 3:20 PMaloof-lion-11633
05/19/2026, 3:21 PMtall-airline-16756
05/19/2026, 3:22 PM