From cead73ce6514143883708c72633e190d4e722dc9 Mon Sep 17 00:00:00 2001 From: Brazol Date: Mon, 26 May 2025 19:09:32 +0200 Subject: [PATCH 1/2] Expanded the AudioDevice type check on Android + added native audio route picker on iOS --- .../webrtc/flutter/audio/AudioUtils.java | 8 +++- common/darwin/Classes/FlutterRTCMediaStream.h | 2 + common/darwin/Classes/FlutterRTCMediaStream.m | 48 +++++++++++++++++++ common/darwin/Classes/FlutterWebRTCPlugin.m | 3 ++ lib/src/helper.dart | 11 +++++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioUtils.java b/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioUtils.java index 68aed7a446..28c207847a 100644 --- a/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioUtils.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioUtils.java @@ -231,10 +231,14 @@ static public String getAudioGroupId(AudioDeviceInfo device) { if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_MIC) { groupId = "microphone"; } - if (device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) { + if (device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET + || device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET + || device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES) { groupId = "wired-headset"; } - if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO) { + if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_SCO + || device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP + || device.getType() == AudioDeviceInfo.TYPE_BLE_SPEAKER) { groupId = "bluetooth"; } return groupId; diff --git a/common/darwin/Classes/FlutterRTCMediaStream.h b/common/darwin/Classes/FlutterRTCMediaStream.h index cf92e494ce..91f19be617 100644 --- a/common/darwin/Classes/FlutterRTCMediaStream.h +++ b/common/darwin/Classes/FlutterRTCMediaStream.h @@ -22,4 +22,6 @@ - (void)selectAudioInput:(nonnull NSString*)deviceId result:(nullable FlutterResult)result; - (void)selectAudioOutput:(nonnull NSString*)deviceId result:(nullable FlutterResult)result; + +- (void)triggeriOSAudioRouteSelectionUI:(nonnull FlutterResult)result; @end diff --git a/common/darwin/Classes/FlutterRTCMediaStream.m b/common/darwin/Classes/FlutterRTCMediaStream.m index 077f6bbc5f..5dca6a48f5 100644 --- a/common/darwin/Classes/FlutterRTCMediaStream.m +++ b/common/darwin/Classes/FlutterRTCMediaStream.m @@ -7,6 +7,7 @@ #import "VideoProcessingAdapter.h" #import "LocalVideoTrack.h" #import "LocalAudioTrack.h" +#import "AVKit/AVKit.h" @implementation RTCMediaStreamTrack (Flutter) @@ -874,6 +875,53 @@ - (void)selectAudioOutput:(NSString*)deviceId result:(FlutterResult)result { details:nil]); } +- (void)triggeriOSAudioRouteSelectionUI:(FlutterResult)result { + if (@available(iOS 11.0, *)) { + AVRoutePickerView *routePicker = [[AVRoutePickerView alloc] init]; + routePicker.frame = CGRectMake(0, 0, 44, 44); + + // Add the route picker to a temporary window to ensure it's in the view hierarchy + UIWindow *window = [[UIApplication sharedApplication] keyWindow]; + if (!window) { + // Fallback for iOS 13+ where keyWindow is deprecated + for (UIWindowScene *windowScene in [UIApplication sharedApplication].connectedScenes) { + if (windowScene.activationState == UISceneActivationStateForegroundActive) { + window = windowScene.windows.firstObject; + break; + } + } + } + + if (window) { + [window addSubview:routePicker]; + + // Trigger the route picker programmatically + for (UIView *view in routePicker.subviews) { + if ([view isKindOfClass:[UIButton class]]) { + UIButton *button = (UIButton *)view; + [button sendActionsForControlEvents:UIControlEventTouchUpInside]; + break; // Only trigger the first button found + } + } + + // Remove the route picker after a short delay + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [routePicker removeFromSuperview]; + }); + + result(nil); + } else { + result([FlutterError errorWithCode:@"NoWindowError" + message:@"Could not find a window to present the route picker" + details:nil]); + } + } else { + result([FlutterError errorWithCode:@"UnsupportedVersionError" + message:@"AVRoutePickerView is only available on iOS 11.0 or later" + details:nil]); + } +} + - (void)mediaStreamTrackRelease:(RTCMediaStream*)mediaStream track:(RTCMediaStreamTrack*)track { // what's different to mediaStreamTrackStop? only call mediaStream explicitly? if (mediaStream && track) { diff --git a/common/darwin/Classes/FlutterWebRTCPlugin.m b/common/darwin/Classes/FlutterWebRTCPlugin.m index ce00c282fa..8e99f8c3e8 100644 --- a/common/darwin/Classes/FlutterWebRTCPlugin.m +++ b/common/darwin/Classes/FlutterWebRTCPlugin.m @@ -20,6 +20,7 @@ #import #import #import +#import #import "LocalTrack.h" #import "LocalAudioTrack.h" @@ -357,6 +358,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSDictionary* argsMap = call.arguments; NSString* deviceId = argsMap[@"deviceId"]; [self selectAudioOutput:deviceId result:result]; + } else if ([@"triggeriOSAudioRouteSelectionUI" isEqualToString:call.method]) { + [self triggeriOSAudioRouteSelectionUI:result]; } else if ([@"mediaStreamGetTracks" isEqualToString:call.method]) { NSDictionary* argsMap = call.arguments; NSString* streamId = argsMap[@"streamId"]; diff --git a/lib/src/helper.dart b/lib/src/helper.dart index e4fd66f9b6..6c39c1aa95 100644 --- a/lib/src/helper.dart +++ b/lib/src/helper.dart @@ -191,4 +191,15 @@ class Helper { 'enableIOSMultitaskingCameraAccess is only supported for iOS'); } } + + static Future triggeriOSAudioRouteSelectionUI() async { + if (WebRTC.platformIsIOS) { + return await WebRTC.invokeMethod( + 'triggeriOSAudioRouteSelectionUI', + ); + } else { + throw Exception( + 'triggeriOSAudioRouteSelectionUI is only supported for iOS'); + } + } } From 9697a0c96cadf90f7b526b33f7966549efa0e631 Mon Sep 17 00:00:00 2001 From: Brazol Date: Mon, 26 May 2025 19:24:37 +0200 Subject: [PATCH 2/2] tweak --- common/darwin/Classes/FlutterRTCMediaStream.m | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/common/darwin/Classes/FlutterRTCMediaStream.m b/common/darwin/Classes/FlutterRTCMediaStream.m index 5dca6a48f5..868cbb0e9a 100644 --- a/common/darwin/Classes/FlutterRTCMediaStream.m +++ b/common/darwin/Classes/FlutterRTCMediaStream.m @@ -876,6 +876,7 @@ - (void)selectAudioOutput:(NSString*)deviceId result:(FlutterResult)result { } - (void)triggeriOSAudioRouteSelectionUI:(FlutterResult)result { +#if TARGET_OS_IPHONE if (@available(iOS 11.0, *)) { AVRoutePickerView *routePicker = [[AVRoutePickerView alloc] init]; routePicker.frame = CGRectMake(0, 0, 44, 44); @@ -920,6 +921,12 @@ - (void)triggeriOSAudioRouteSelectionUI:(FlutterResult)result { message:@"AVRoutePickerView is only available on iOS 11.0 or later" details:nil]); } +#else + // macOS doesn't support iOS audio route selection UI + result([FlutterError errorWithCode:@"UnsupportedPlatformError" + message:@"triggeriOSAudioRouteSelectionUI is only supported on iOS" + details:nil]); +#endif } - (void)mediaStreamTrackRelease:(RTCMediaStream*)mediaStream track:(RTCMediaStreamTrack*)track {