feat: implement background task management for improved streaming continuity

- Integrated BackgroundTasks framework in iOS to manage background processing for audio streams.
- Added methods to register, schedule, and handle background tasks, allowing streams to continue for extended periods.
- Enhanced the BackgroundStreamingHandler to support background task notifications and keep-alive signals.
- Updated Info.plist to permit background task identifiers, ensuring compliance with iOS requirements.
- Improved the PersistentStreamingService to handle background task extensions and keep-alive signals effectively, enhancing overall streaming reliability.
This commit is contained in:
cogwheel0
2025-10-11 13:53:30 +05:30
parent 3d45154182
commit 4003941482
4 changed files with 152 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
import AVFoundation import AVFoundation
import BackgroundTasks
import Flutter import Flutter
import UIKit import UIKit
@@ -48,10 +49,13 @@ final class VoiceBackgroundAudioManager {
// Background streaming handler class // Background streaming handler class
class BackgroundStreamingHandler: NSObject { class BackgroundStreamingHandler: NSObject {
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
private var bgProcessingTask: BGTask?
private var activeStreams: Set<String> = [] private var activeStreams: Set<String> = []
private var microphoneStreams: Set<String> = [] private var microphoneStreams: Set<String> = []
private var channel: FlutterMethodChannel? private var channel: FlutterMethodChannel?
static let processingTaskIdentifier = "app.cogwheel.conduit.refresh"
override init() { override init() {
super.init() super.init()
setupNotifications() setupNotifications()
@@ -142,6 +146,7 @@ class BackgroundStreamingHandler: NSObject {
if UIApplication.shared.applicationState == .background { if UIApplication.shared.applicationState == .background {
startBackgroundTask() startBackgroundTask()
scheduleBGProcessingTask()
} }
} }
@@ -151,6 +156,7 @@ class BackgroundStreamingHandler: NSObject {
if activeStreams.isEmpty { if activeStreams.isEmpty {
endBackgroundTask() endBackgroundTask()
cancelBGProcessingTask()
} }
if microphoneStreams.isEmpty { if microphoneStreams.isEmpty {
@@ -206,7 +212,95 @@ class BackgroundStreamingHandler: NSObject {
} }
return [] return []
} }
// MARK: - BGTaskScheduler Methods
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: Self.processingTaskIdentifier,
using: nil
) { [weak self] task in
self?.handleBGProcessingTask(task: task as! BGProcessingTask)
}
}
private func scheduleBGProcessingTask() {
// Cancel any existing task
cancelBGProcessingTask()
let request = BGProcessingTaskRequest(identifier: Self.processingTaskIdentifier)
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
// Schedule for immediate execution when app backgrounds
request.earliestBeginDate = Date(timeIntervalSinceNow: 1)
do {
try BGTaskScheduler.shared.submit(request)
print("BackgroundStreamingHandler: Scheduled BGProcessingTask")
} catch {
print("BackgroundStreamingHandler: Failed to schedule BGProcessingTask: \(error)")
}
}
private func cancelBGProcessingTask() {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: Self.processingTaskIdentifier)
print("BackgroundStreamingHandler: Cancelled BGProcessingTask")
}
private func handleBGProcessingTask(task: BGProcessingTask) {
print("BackgroundStreamingHandler: BGProcessingTask started")
bgProcessingTask = task
// Schedule a new task for continuation if streams are still active
if !activeStreams.isEmpty {
scheduleBGProcessingTask()
}
// Set expiration handler
task.expirationHandler = { [weak self] in
print("BackgroundStreamingHandler: BGProcessingTask expiring")
self?.notifyTaskExpiring()
self?.bgProcessingTask = nil
}
// Notify Flutter that we have extended background time
channel?.invokeMethod("backgroundTaskExtended", arguments: [
"streamIds": Array(activeStreams),
"estimatedTime": 180 // ~3 minutes typical for BGProcessingTask
])
// Keep task alive while streams are active
let workItem = DispatchWorkItem { [weak self] in
guard let self = self else { return }
// Keep sending keepAlive signals
let keepAliveInterval: TimeInterval = 30
var elapsedTime: TimeInterval = 0
let maxTime: TimeInterval = 180 // 3 minutes
while !self.activeStreams.isEmpty && elapsedTime < maxTime {
Thread.sleep(forTimeInterval: keepAliveInterval)
elapsedTime += keepAliveInterval
// Notify Flutter to keep streams alive
DispatchQueue.main.async {
self.channel?.invokeMethod("backgroundKeepAlive", arguments: nil)
}
}
// Mark task as complete
task.setTaskCompleted(success: true)
self.bgProcessingTask = nil
}
DispatchQueue.global(qos: .background).async(execute: workItem)
}
private func notifyTaskExpiring() {
channel?.invokeMethod("backgroundTaskExpiring", arguments: nil)
}
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
endBackgroundTask() endBackgroundTask()
@@ -217,13 +311,13 @@ class BackgroundStreamingHandler: NSObject {
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
private var backgroundStreamingHandler: BackgroundStreamingHandler? private var backgroundStreamingHandler: BackgroundStreamingHandler?
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
// Setup background streaming handler using the plugin registry messenger // Setup background streaming handler using the plugin registry messenger
if let registrar = self.registrar(forPlugin: "BackgroundStreamingHandler") { if let registrar = self.registrar(forPlugin: "BackgroundStreamingHandler") {
let channel = FlutterMethodChannel( let channel = FlutterMethodChannel(
@@ -234,12 +328,15 @@ class BackgroundStreamingHandler: NSObject {
backgroundStreamingHandler = BackgroundStreamingHandler() backgroundStreamingHandler = BackgroundStreamingHandler()
backgroundStreamingHandler?.setup(with: channel) backgroundStreamingHandler?.setup(with: channel)
// Register BGTaskScheduler tasks
backgroundStreamingHandler?.registerBackgroundTasks()
// Register method call handler // Register method call handler
channel.setMethodCallHandler { [weak self] (call, result) in channel.setMethodCallHandler { [weak self] (call, result) in
self?.backgroundStreamingHandler?.handle(call, result: result) self?.backgroundStreamingHandler?.handle(call, result: result)
} }
} }
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
} }

View File

@@ -92,5 +92,10 @@
<string>audio</string> <string>audio</string>
<string>processing</string> <string>processing</string>
</array> </array>
<!-- Background Task Identifiers -->
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.cogwheel.conduit.refresh</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -5,7 +5,7 @@ import '../utils/debug_logger.dart';
/// Handles background streaming continuation for iOS and Android /// Handles background streaming continuation for iOS and Android
/// ///
/// On iOS: Uses background tasks to keep streams alive for ~30 seconds /// On iOS: Uses beginBackgroundTask (~30s) + BGTaskScheduler (~3+ minutes)
/// On Android: Uses foreground service notifications /// On Android: Uses foreground service notifications
class BackgroundStreamingHandler { class BackgroundStreamingHandler {
static const MethodChannel _channel = MethodChannel( static const MethodChannel _channel = MethodChannel(
@@ -26,6 +26,8 @@ class BackgroundStreamingHandler {
// Callbacks for platform-specific events // Callbacks for platform-specific events
void Function(List<String> streamIds)? onStreamsSuspending; void Function(List<String> streamIds)? onStreamsSuspending;
void Function()? onBackgroundTaskExpiring; void Function()? onBackgroundTaskExpiring;
void Function(List<String> streamIds, int estimatedSeconds)? onBackgroundTaskExtended;
void Function()? onBackgroundKeepAlive;
bool Function()? shouldContinueInBackground; bool Function()? shouldContinueInBackground;
void _setupMethodCallHandler() { void _setupMethodCallHandler() {
@@ -56,6 +58,26 @@ class BackgroundStreamingHandler {
DebugLogger.stream('task-expiring', scope: 'background'); DebugLogger.stream('task-expiring', scope: 'background');
onBackgroundTaskExpiring?.call(); onBackgroundTaskExpiring?.call();
break; break;
case 'backgroundTaskExtended':
final Map<String, dynamic> args =
call.arguments as Map<String, dynamic>;
final List<String> streamIds = (args['streamIds'] as List)
.cast<String>();
final int estimatedTime = args['estimatedTime'] as int;
DebugLogger.stream(
'task-extended',
scope: 'background',
data: {'count': streamIds.length, 'time': estimatedTime},
);
onBackgroundTaskExtended?.call(streamIds, estimatedTime);
break;
case 'backgroundKeepAlive':
DebugLogger.stream('keepalive-signal', scope: 'background');
onBackgroundKeepAlive?.call();
break;
} }
}); });
} }

View File

@@ -62,6 +62,28 @@ class PersistentStreamingService with WidgetsBindingObserver {
_saveStreamStatesForRecovery(); _saveStreamStatesForRecovery();
}; };
_backgroundHandler.onBackgroundTaskExtended = (streamIds, estimatedSeconds) {
DebugLogger.stream(
'PersistentStreaming: Background task extended for $estimatedSeconds seconds',
);
// BGTaskScheduler has given us more time - streams can continue
for (final streamId in streamIds) {
final metadata = _streamMetadata[streamId];
if (metadata != null) {
metadata['bgTaskExtended'] = true;
metadata['bgTaskExtendedAt'] = DateTime.now();
metadata['bgTaskEstimatedTime'] = estimatedSeconds;
}
}
};
_backgroundHandler.onBackgroundKeepAlive = () {
DebugLogger.stream('PersistentStreaming: Background keep-alive signal');
// BGTaskScheduler is keeping us alive - we can continue streaming
_heartbeatTimer?.cancel();
_startHeartbeat(); // Restart heartbeat timer
};
_backgroundHandler.shouldContinueInBackground = () { _backgroundHandler.shouldContinueInBackground = () {
return _activeStreams.isNotEmpty; return _activeStreams.isNotEmpty;
}; };