diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7fbb8e12..3cba22c660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ [2.2.0] - 2025-11-24 * Added `Helper.pauseAudioPlayout()` / `Helper.resumeAudioPlayout()` to mute and restore remote playback with platform-specific handling for iOS/macOS and Android. +* [Android] Improved the audio focus handling for interruption purposes (`handleCallInterruptionCallbacks`). It now uses AudioSwitch and won't trigger unwanted interaction detections when focus is requested. + - `androidAudioAttributesUsageType` and `androidAudioAttributesContentType` parameters in `handleCallInterruptionCallbacks` are now not needed and deprecated. +* [Android] Added `regainAndroidAudioFocus` method that requests audio focus in case it was lost with no automatic regain. [2.1.0] - 2025-11-17 * [iOS] Added Swift Package Manager (SPM) support to iOS. diff --git a/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java b/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java index 7c267f1df9..00afa87e38 100644 --- a/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/MethodCallHandlerImpl.java @@ -179,6 +179,9 @@ static private void resultError(String method, String error, Result result) { } void dispose() { + if (AudioSwitchManager.instance != null) { + AudioSwitchManager.instance.setAudioFocusChangeListener(null); + } if (audioFocusManager != null) { audioFocusManager.setAudioFocusChangeListener(null); audioFocusManager = null; @@ -413,58 +416,43 @@ public void onMethodCall(MethodCall call, @NonNull Result notSafeResult) { case "handleCallInterruptionCallbacks": { String interruptionSource = call.argument("androidInterruptionSource"); AudioFocusManager.InterruptionSource source; - - switch (interruptionSource) { - case "audioFocusOnly": - source = AudioFocusManager.InterruptionSource.AUDIO_FOCUS_ONLY; - break; - case "telephonyOnly": - source = AudioFocusManager.InterruptionSource.TELEPHONY_ONLY; - break; - case "audioFocusAndTelephony": - source = AudioFocusManager.InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY; - break; - default: - source = AudioFocusManager.InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY; - break; - } - - Integer usage = null, content = null; - - // Prefer override values if provided, else fallback to persisted config - String overrideUsageStr = call.argument("androidAudioAttributesUsageType"); - String overrideContentStr = call.argument("androidAudioAttributesContentType"); - if (overrideUsageStr != null) { - usage = AudioUtils.getAudioAttributesUsageTypeForString(overrideUsageStr); - } else if (initializedAndroidAudioConfiguration != null) { - usage = AudioUtils.getAudioAttributesUsageTypeForString( - initializedAndroidAudioConfiguration.getString("androidAudioAttributesUsageType")); + switch (interruptionSource) { + case "audioFocusOnly": + source = AudioFocusManager.InterruptionSource.AUDIO_FOCUS_ONLY; + break; + case "telephonyOnly": + source = AudioFocusManager.InterruptionSource.TELEPHONY_ONLY; + break; + case "audioFocusAndTelephony": + source = AudioFocusManager.InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY; + break; + default: + source = AudioFocusManager.InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY; + break; } - if (overrideContentStr != null) { - content = AudioUtils.getAudioAttributesContentTypeFromString(overrideContentStr); - } else if (initializedAndroidAudioConfiguration != null) { - content = AudioUtils.getAudioAttributesContentTypeFromString( - initializedAndroidAudioConfiguration.getString("androidAudioAttributesContentType")); + if (audioFocusManager != null) { + audioFocusManager.setAudioFocusChangeListener(null); + audioFocusManager = null; } - audioFocusManager = new AudioFocusManager(context, source, usage, content); + audioFocusManager = new AudioFocusManager(context, source); audioFocusManager.setAudioFocusChangeListener(new AudioFocusManager.AudioFocusChangeListener() { - @Override - public void onInterruptionStart() { - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onInterruptionStart"); - FlutterWebRTCPlugin.sharedSingleton.sendEvent(params.toMap()); - } + @Override + public void onInterruptionStart() { + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onInterruptionStart"); + FlutterWebRTCPlugin.sharedSingleton.sendEvent(params.toMap()); + } - @Override - public void onInterruptionEnd() { - ConstraintsMap params = new ConstraintsMap(); - params.putString("event", "onInterruptionEnd"); - FlutterWebRTCPlugin.sharedSingleton.sendEvent(params.toMap()); - } + @Override + public void onInterruptionEnd() { + ConstraintsMap params = new ConstraintsMap(); + params.putString("event", "onInterruptionEnd"); + FlutterWebRTCPlugin.sharedSingleton.sendEvent(params.toMap()); + } }); result.success(null); break; @@ -827,6 +815,19 @@ public void onInterruptionEnd() { result.success(null); break; } + case "regainAndroidAudioFocus": { + if (AudioSwitchManager.instance == null) { + resultError("regainAndroidAudioFocus", + "AudioSwitch manager is not initialized. Ensure plugin is attached before requesting focus.", result); + break; + } + AudioSwitchManager.instance.requestAudioFocus(); + if (audioFocusManager != null) { + audioFocusManager.notifyManualAudioFocusRegain(); + } + result.success(null); + break; + } case "clearAndroidCommunicationDevice": { AudioSwitchManager.instance.clearCommunicationDevice(); result.success(null); diff --git a/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioFocusManager.java b/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioFocusManager.java index d09dacc4f1..5bc5903385 100644 --- a/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioFocusManager.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioFocusManager.java @@ -1,8 +1,8 @@ package io.getstream.webrtc.flutter.audio; +import android.Manifest; import android.content.Context; -import android.media.AudioAttributes; -import android.media.AudioFocusRequest; +import android.content.pm.PackageManager; import android.media.AudioManager; import android.os.Build; import android.telephony.PhoneStateListener; @@ -10,238 +10,161 @@ import android.telephony.TelephonyManager; import android.util.Log; -import io.getstream.webrtc.flutter.utils.ConstraintsMap; - public class AudioFocusManager { private static final String TAG = "AudioFocusManager"; - + public enum InterruptionSource { AUDIO_FOCUS_ONLY, TELEPHONY_ONLY, AUDIO_FOCUS_AND_TELEPHONY } - - private AudioManager audioManager; - private TelephonyManager telephonyManager; + private final Context context; + private final InterruptionSource interruptionSource; + private final boolean monitorAudioFocus; + private final boolean monitorTelephony; + + private TelephonyManager telephonyManager; private PhoneStateListener phoneStateListener; + private TelephonyCallback telephonyCallback; + private AudioFocusChangeListener focusChangeListener; + private volatile boolean interruptionActive = false; + + private final AudioManager.OnAudioFocusChangeListener audioSwitchFocusListener = focusChange -> { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + handleInterruptionStart("Audio focus lost: " + focusChange); + break; + case AudioManager.AUDIOFOCUS_GAIN: + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: + handleInterruptionEnd("Audio focus gained: " + focusChange); + break; + default: + break; + } + }; - private TelephonyCallback telephonyCallback; - private AudioFocusRequest audioFocusRequest; - - private InterruptionSource interruptionSource; - private Context context; - - private Integer focusUsageType; // AudioAttributes.USAGE_* - private Integer focusContentType; // AudioAttributes.CONTENT_TYPE_* - public interface AudioFocusChangeListener { void onInterruptionStart(); + void onInterruptionEnd(); } - + public AudioFocusManager(Context context) { - this(context, InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY, null, null); + this(context, InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY); } - + public AudioFocusManager(Context context, InterruptionSource interruptionSource) { - this(context, interruptionSource, null, null); - } - - public AudioFocusManager(Context context, InterruptionSource interruptionSource, Integer usageType, Integer contentType) { this.context = context; this.interruptionSource = interruptionSource; - this.focusUsageType = usageType; - this.focusContentType = contentType; - - if (interruptionSource == InterruptionSource.AUDIO_FOCUS_ONLY || - interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY) { - audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - } - - if (interruptionSource == InterruptionSource.TELEPHONY_ONLY || - interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY) { + this.monitorAudioFocus = interruptionSource == InterruptionSource.AUDIO_FOCUS_ONLY + || interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY; + this.monitorTelephony = interruptionSource == InterruptionSource.TELEPHONY_ONLY + || interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY; + + if (monitorTelephony) { telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); } } - + public void setAudioFocusChangeListener(AudioFocusChangeListener listener) { this.focusChangeListener = listener; - + if (listener != null) { startMonitoring(); } else { stopMonitoring(); } } - + public void startMonitoring() { - if (interruptionSource == InterruptionSource.AUDIO_FOCUS_ONLY || - interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY) { - requestAudioFocusInternal(); + interruptionActive = false; + if (monitorAudioFocus) { + if (AudioSwitchManager.instance != null) { + AudioSwitchManager.instance.setAudioFocusChangeListener(audioSwitchFocusListener); + AudioSwitchManager.instance.requestAudioFocus(); + } else { + Log.w(TAG, "AudioSwitchManager instance is null, cannot observe audio focus changes"); + } } - - if (interruptionSource == InterruptionSource.TELEPHONY_ONLY || - interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY) { + + if (monitorTelephony) { registerTelephonyListener(); } } - + public void stopMonitoring() { - if (interruptionSource == InterruptionSource.AUDIO_FOCUS_ONLY || - interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY) { - abandonAudioFocusInternal(); + if (monitorAudioFocus && AudioSwitchManager.instance != null) { + AudioSwitchManager.instance.setAudioFocusChangeListener(null); } - - if (interruptionSource == InterruptionSource.TELEPHONY_ONLY || - interruptionSource == InterruptionSource.AUDIO_FOCUS_AND_TELEPHONY) { + interruptionActive = false; + + if (monitorTelephony) { unregisterTelephonyListener(); } } - - private void requestAudioFocusInternal() { - if (audioManager == null) { - Log.w(TAG, "AudioManager is null, cannot request audio focus"); - return; - } - - AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = focusChange -> { - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - Log.d(TAG, "Audio focus lost"); - if (focusChangeListener != null) { - focusChangeListener.onInterruptionStart(); - } - break; - case AudioManager.AUDIOFOCUS_GAIN: - Log.d(TAG, "Audio focus gained"); - if (focusChangeListener != null) { - focusChangeListener.onInterruptionEnd(); - } - break; - } - }; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AudioAttributes audioAttributes = new AudioAttributes.Builder() - .setUsage(focusUsageType != null ? focusUsageType : AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(focusContentType != null ? focusContentType : AudioAttributes.CONTENT_TYPE_SPEECH) - .build(); - - audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) - .setAudioAttributes(audioAttributes) - .setOnAudioFocusChangeListener(onAudioFocusChangeListener) - .build(); - - audioManager.requestAudioFocus(audioFocusRequest); - } else { - int streamType = inferPreOStreamType(focusUsageType, focusContentType); - audioManager.requestAudioFocus(onAudioFocusChangeListener, - streamType, - AudioManager.AUDIOFOCUS_GAIN); - } - } - - private int inferPreOStreamType(Integer usageType, Integer contentType) { - if (usageType != null) { - if (usageType == AudioAttributes.USAGE_MEDIA - || usageType == AudioAttributes.USAGE_GAME - || usageType == AudioAttributes.USAGE_ASSISTANT) { - return AudioManager.STREAM_MUSIC; - } - if (usageType == AudioAttributes.USAGE_VOICE_COMMUNICATION - || usageType == AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) { - return AudioManager.STREAM_VOICE_CALL; - } - if (usageType == AudioAttributes.USAGE_NOTIFICATION - || usageType == AudioAttributes.USAGE_NOTIFICATION_RINGTONE - || usageType == AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST) { - return AudioManager.STREAM_NOTIFICATION; - } - if (usageType == AudioAttributes.USAGE_ALARM) { - return AudioManager.STREAM_ALARM; - } - } - - if (contentType != null) { - if (contentType == AudioAttributes.CONTENT_TYPE_MUSIC - || contentType == AudioAttributes.CONTENT_TYPE_MOVIE) { - return AudioManager.STREAM_MUSIC; - } - if (contentType == AudioAttributes.CONTENT_TYPE_SPEECH) { - return AudioManager.STREAM_VOICE_CALL; - } - } - return AudioManager.STREAM_VOICE_CALL; - } - private void registerTelephonyListener() { if (telephonyManager == null) { Log.w(TAG, "TelephonyManager is null, cannot register telephony listener"); return; } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // Use TelephonyCallback for Android 12+ (API 31+) - class CallStateCallback extends TelephonyCallback implements TelephonyCallback.CallStateListener { - @Override - public void onCallStateChanged(int state) { - handleCallStateChange(state); + + if (!hasTelephonyPermission()) { + Log.w(TAG, "Missing phone state permission, telephony interruptions disabled"); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Use TelephonyCallback for Android 12+ (API 31+) + class CallStateCallback extends TelephonyCallback implements TelephonyCallback.CallStateListener { + @Override + public void onCallStateChanged(int state) { + handleCallStateChange(state); + } } + telephonyCallback = new CallStateCallback(); + telephonyManager.registerTelephonyCallback(context.getMainExecutor(), telephonyCallback); + } else { + // Use PhoneStateListener for older Android versions + phoneStateListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String phoneNumber) { + handleCallStateChange(state); + } + }; + telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } - telephonyCallback = new CallStateCallback(); - telephonyManager.registerTelephonyCallback(context.getMainExecutor(), telephonyCallback); - } else { - // Use PhoneStateListener for older Android versions - phoneStateListener = new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String phoneNumber) { - handleCallStateChange(state); - } - }; - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + } catch (SecurityException exception) { + Log.w(TAG, "Unable to register telephony listener", exception); + telephonyCallback = null; + phoneStateListener = null; } } - + private void handleCallStateChange(int state) { - if (focusChangeListener == null) { - return; - } - switch (state) { case TelephonyManager.CALL_STATE_RINGING: case TelephonyManager.CALL_STATE_OFFHOOK: - Log.d(TAG, "Phone call interruption began"); - focusChangeListener.onInterruptionStart(); + handleInterruptionStart("Phone call interruption began"); break; case TelephonyManager.CALL_STATE_IDLE: - Log.d(TAG, "Phone call interruption ended"); - focusChangeListener.onInterruptionEnd(); + handleInterruptionEnd("Phone call interruption ended"); break; } } - - private void abandonAudioFocusInternal() { - if (audioManager == null) { - return; - } - - int result; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && audioFocusRequest != null) { - result = audioManager.abandonAudioFocusRequest(audioFocusRequest); - } else { - result = audioManager.abandonAudioFocus(null); - } - } - + private void unregisterTelephonyListener() { if (telephonyManager == null) { return; } - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && telephonyCallback != null) { telephonyManager.unregisterTelephonyCallback(telephonyCallback); telephonyCallback = null; @@ -250,4 +173,65 @@ private void unregisterTelephonyListener() { phoneStateListener = null; } } -} \ No newline at end of file + + private boolean hasTelephonyPermission() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + + if (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + return true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + && context.checkSelfPermission( + Manifest.permission.READ_PRECISE_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + return true; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && context.checkSelfPermission( + Manifest.permission.READ_BASIC_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) { + return true; + } + + return false; + } + + private void handleInterruptionStart(String logMessage) { + if (focusChangeListener == null) { + return; + } + + if (interruptionActive) { + Log.d(TAG, "Ignoring duplicate interruption start: " + logMessage); + return; + } + + Log.d(TAG, logMessage); + interruptionActive = true; + focusChangeListener.onInterruptionStart(); + } + + private void handleInterruptionEnd(String logMessage) { + if (focusChangeListener == null) { + return; + } + + if (!interruptionActive) { + Log.d(TAG, "Ignoring interruption end with no active interruption: " + logMessage); + return; + } + + Log.d(TAG, logMessage); + interruptionActive = false; + focusChangeListener.onInterruptionEnd(); + } + + public void notifyManualAudioFocusRegain() { + if (!monitorAudioFocus) { + return; + } + handleInterruptionEnd("Audio focus manually requested"); + } +} \ No newline at end of file diff --git a/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioSwitchManager.java b/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioSwitchManager.java index ad034fdce7..bdb564906f 100644 --- a/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioSwitchManager.java +++ b/android/src/main/java/io/getstream/webrtc/flutter/audio/AudioSwitchManager.java @@ -42,8 +42,7 @@ public class AudioSwitchManager { Unit> audioDeviceChangeListener = (devices, currentDevice) -> null; @NonNull - public AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = (i -> { - }); + private final ForwardingAudioFocusChangeListener audioFocusChangeListener = new ForwardingAudioFocusChangeListener(); @NonNull public List> preferredDeviceList; @@ -160,7 +159,7 @@ public void start() { handler.removeCallbacksAndMessages(null); handler.postAtFrontOfQueue(() -> { if (!isActive) { - Objects.requireNonNull(audioSwitch).activate(); + audioSwitch.activate(); isActive = true; } }); @@ -407,4 +406,33 @@ public void clearCommunicationDevice() { audioManager.clearCommunicationDevice(); } } + + public void setAudioFocusChangeListener(@Nullable AudioManager.OnAudioFocusChangeListener listener) { + audioFocusChangeListener.setDelegate(listener); + } + + public void requestAudioFocus() { + handler.post(() -> { + if (audioSwitch != null) { + Objects.requireNonNull(audioSwitch).activate(); + } + }); + } + + private static final class ForwardingAudioFocusChangeListener implements AudioManager.OnAudioFocusChangeListener { + @Nullable + private volatile AudioManager.OnAudioFocusChangeListener delegate; + + @Override + public void onAudioFocusChange(int focusChange) { + AudioManager.OnAudioFocusChangeListener current = delegate; + if (current != null) { + current.onAudioFocusChange(focusChange); + } + } + + void setDelegate(@Nullable AudioManager.OnAudioFocusChangeListener delegate) { + this.delegate = delegate; + } + } } diff --git a/lib/src/helper.dart b/lib/src/helper.dart index 37ff706c82..6b00ff8cae 100644 --- a/lib/src/helper.dart +++ b/lib/src/helper.dart @@ -178,6 +178,15 @@ class Helper { static Future clearAndroidCommunicationDevice() => WebRTC.invokeMethod('clearAndroidCommunicationDevice'); + /// Force Android to re-request audio focus after a loss. + static Future regainAndroidAudioFocus() async { + if (WebRTC.platformIsAndroid) { + return await WebRTC.invokeMethod('regainAndroidAudioFocus'); + } else { + throw Exception('regainAndroidAudioFocus is only supported on Android'); + } + } + /// Set the audio configuration for iOS static Future setAppleAudioConfiguration( AppleAudioConfiguration appleAudioConfiguration) => diff --git a/lib/src/native/factory_impl.dart b/lib/src/native/factory_impl.dart index 498d80c5e6..3a73e8189b 100644 --- a/lib/src/native/factory_impl.dart +++ b/lib/src/native/factory_impl.dart @@ -33,7 +33,11 @@ class RTCFactoryNative extends RTCFactory { void Function()? onInterruptionEnd, { AndroidInterruptionSource androidInterruptionSource = AndroidInterruptionSource.audioFocusAndTelephony, + @Deprecated( + 'Audio focus is now handled in a way that does not require this parameter. It will be removed in the next major version.') AndroidAudioAttributesUsageType? androidAudioAttributesUsageType, + @Deprecated( + 'Audio focus is now handled in a way that does not require this parameter. It will be removed in the next major version.') AndroidAudioAttributesContentType? androidAudioAttributesContentType, }) async { if (!Platform.isAndroid && !Platform.isIOS) { @@ -46,12 +50,6 @@ class RTCFactoryNative extends RTCFactory { { if (Platform.isAndroid) 'androidInterruptionSource': androidInterruptionSource.name, - if (Platform.isAndroid && androidAudioAttributesUsageType != null) - 'androidAudioAttributesUsageType': - androidAudioAttributesUsageType.name, - if (Platform.isAndroid && androidAudioAttributesContentType != null) - 'androidAudioAttributesContentType': - androidAudioAttributesContentType.name, }, ); @@ -145,7 +143,11 @@ Future handleCallInterruptionCallbacks( void Function()? onInterruptionEnd, { AndroidInterruptionSource androidInterruptionSource = AndroidInterruptionSource.audioFocusAndTelephony, + @Deprecated( + 'Audio focus is now handled in a way that does not require this parameter. It will be removed in the next major version.') AndroidAudioAttributesUsageType? androidAudioAttributesUsageType, + @Deprecated( + 'Audio focus is now handled in a way that does not require this parameter. It will be removed in the next major version.') AndroidAudioAttributesContentType? androidAudioAttributesContentType, }) { return (RTCFactoryNative.instance as RTCFactoryNative)