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:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user