diff --git a/example/lib/widgets/controls.dart b/example/lib/widgets/controls.dart index 0fe13c2..0b0a4c9 100644 --- a/example/lib/widgets/controls.dart +++ b/example/lib/widgets/controls.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:eva_icons_flutter/eva_icons_flutter.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_background/flutter_background.dart'; import 'package:livekit_client/livekit_client.dart'; @@ -36,6 +37,8 @@ class _ControlsWidgetState extends State { StreamSubscription? _subscription; + bool _speakerphoneOn = false; + @override void initState() { super.initState(); @@ -106,6 +109,12 @@ class _ControlsWidgetState extends State { setState(() {}); } + void _setSpeakerphoneOn() { + _speakerphoneOn = !_speakerphoneOn; + Hardware.instance.setSpeakerphoneOn(_speakerphoneOn); + setState(() {}); + } + void _toggleCamera() async { // final track = participant.videoTracks.firstOrNull?.track; @@ -124,7 +133,7 @@ class _ControlsWidgetState extends State { } void _enableScreenShare() async { - if (WebRTC.platformIsDesktop) { + if (lkPlatformIsDesktop()) { try { final source = await showDialog( context: context, @@ -147,7 +156,7 @@ class _ControlsWidgetState extends State { } return; } - if (WebRTC.platformIsAndroid) { + if (lkPlatformIs(PlatformType.android)) { // Android specific requestBackgroundPermission([bool isRetry = false]) async { // Required for android screenshare. @@ -179,7 +188,7 @@ class _ControlsWidgetState extends State { await requestBackgroundPermission(); } - if (WebRTC.platformIsIOS) { + if (lkPlatformIs(PlatformType.iOS)) { var track = await LocalVideoTrack.createScreenShareTrack( const ScreenShareCaptureOptions( useiOSBroadcastExtension: true, @@ -314,43 +323,54 @@ class _ControlsWidgetState extends State { icon: const Icon(EvaIcons.micOff), tooltip: 'un-mute audio', ), - PopupMenuButton( - icon: const Icon(Icons.volume_up), - itemBuilder: (BuildContext context) { - return [ - const PopupMenuItem( - value: null, - child: ListTile( - leading: Icon( - EvaIcons.speaker, - color: Colors.white, - ), - title: Text('Select Audio Output'), - ), - ), - if (_audioOutputs != null) - ..._audioOutputs!.map((device) { - return PopupMenuItem( - value: device, - child: ListTile( - leading: (device.deviceId == - widget.room.selectedAudioOutputDeviceId) - ? const Icon( - EvaIcons.checkmarkSquare, - color: Colors.white, - ) - : const Icon( - EvaIcons.square, - color: Colors.white, - ), - title: Text(device.label), + if (!lkPlatformIsMobile()) + PopupMenuButton( + icon: const Icon(Icons.volume_up), + itemBuilder: (BuildContext context) { + return [ + const PopupMenuItem( + value: null, + child: ListTile( + leading: Icon( + EvaIcons.speaker, + color: Colors.white, ), - onTap: () => _selectAudioOutput(device), - ); - }).toList() - ]; - }, - ), + title: Text('Select Audio Output'), + ), + ), + if (_audioOutputs != null) + ..._audioOutputs!.map((device) { + return PopupMenuItem( + value: device, + child: ListTile( + leading: (device.deviceId == + widget.room.selectedAudioOutputDeviceId) + ? const Icon( + EvaIcons.checkmarkSquare, + color: Colors.white, + ) + : const Icon( + EvaIcons.square, + color: Colors.white, + ), + title: Text(device.label), + ), + onTap: () => _selectAudioOutput(device), + ); + }).toList() + ]; + }, + ), + if (!kIsWeb && lkPlatformIsMobile()) + IconButton( + disabledColor: Colors.grey, + onPressed: Hardware.instance.canSwitchSpeakerphone + ? _setSpeakerphoneOn + : null, + icon: Icon( + _speakerphoneOn ? Icons.speaker_phone : Icons.phone_android), + tooltip: 'Switch SpeakerPhone', + ), if (participant.isCameraEnabled()) PopupMenuButton( icon: const Icon(EvaIcons.video), diff --git a/lib/src/core/transport.dart b/lib/src/core/transport.dart index 2b904db..c4dedf4 100644 --- a/lib/src/core/transport.dart +++ b/lib/src/core/transport.dart @@ -8,6 +8,7 @@ import '../internal/types.dart'; import '../logger.dart'; import '../options.dart'; import '../support/disposable.dart'; +import '../support/platform.dart'; import '../types/other.dart'; import '../utils.dart'; @@ -128,7 +129,7 @@ class Transport extends Disposable { } } - if (restartingIce && !rtc.WebRTC.platformIsWeb) { + if (restartingIce && !lkPlatformIs(PlatformType.web)) { await pc.restartIce(); } diff --git a/lib/src/hardware/hardware.dart b/lib/src/hardware/hardware.dart index 2253a68..0b4dc86 100644 --- a/lib/src/hardware/hardware.dart +++ b/lib/src/hardware/hardware.dart @@ -4,6 +4,10 @@ import 'package:collection/collection.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import '../logger.dart'; +import '../support/native.dart'; +import '../support/native_audio.dart'; +import '../support/platform.dart'; +import '../track/audio_management.dart'; class MediaDevice { const MediaDevice(this.deviceId, this.label, this.kind); @@ -59,6 +63,8 @@ class Hardware { bool? speakerOn; + bool _preferSpeakerOutput = false; + Future> enumerateDevices({String? type}) async { var infos = await rtc.navigator.mediaDevices.enumerateDevices(); var devices = @@ -82,7 +88,7 @@ class Hardware { } Future selectAudioOutput(MediaDevice device) async { - if (rtc.WebRTC.platformIsWeb) { + if (lkPlatformIs(PlatformType.web)) { logger.warning('selectAudioOutput not support on web'); return; } @@ -91,7 +97,7 @@ class Hardware { } Future selectAudioInput(MediaDevice device) async { - if (rtc.WebRTC.platformIsWeb) { + if (lkPlatformIs(PlatformType.web)) { logger.warning( 'selectAudioInput is only supported on Android/Windows/macOS'); return; @@ -100,10 +106,43 @@ class Hardware { await rtc.Helper.selectAudioInput(device.deviceId); } + Future setPreferSpeakerOutput(bool enable) async { + if (lkPlatformIsMobile()) { + if (_preferSpeakerOutput != enable) { + NativeAudioConfiguration? config; + if (lkPlatformIs(PlatformType.iOS)) { + // Only iOS for now... + config = await onConfigureNativeAudio.call(audioTrackState); + logger.fine('configuring for ${audioTrackState} using ${config}...'); + try { + await Native.configureAudio(config); + } catch (error) { + logger.warning('failed to configure ${error}'); + } + } + } + _preferSpeakerOutput = enable; + } else { + logger.warning('setPreferSpeakerOutput only support on iOS/Android'); + } + } + + bool get preferSpeakerOutput => _preferSpeakerOutput; + + bool get canSwitchSpeakerphone => + lkPlatformIsMobile() && + !_preferSpeakerOutput && + [AudioTrackState.localOnly, AudioTrackState.localAndRemote] + .contains(audioTrackState); + Future setSpeakerphoneOn(bool enable) async { - if (rtc.WebRTC.platformIsMobile) { - speakerOn = enable; - await rtc.Helper.setSpeakerphoneOn(enable); + if (lkPlatformIsMobile()) { + if (canSwitchSpeakerphone) { + speakerOn = enable; + await rtc.Helper.setSpeakerphoneOn(enable); + } else { + logger.warning('Can\'t switch speaker/earpiece'); + } } else { logger.warning('setSpeakerphoneOn only support on iOS/Android'); } @@ -115,7 +154,7 @@ class Hardware { if (facingMode != null) 'facingMode': facingMode ? 'user' : 'environment', }; if (device != null) { - if (rtc.WebRTC.platformIsWeb) { + if (lkPlatformIs(PlatformType.web)) { constraints['deviceId'] = device.deviceId; } else { constraints['optional'] = [ diff --git a/lib/src/support/platform.dart b/lib/src/support/platform.dart index 47d2576..c90f0eb 100644 --- a/lib/src/support/platform.dart +++ b/lib/src/support/platform.dart @@ -7,6 +7,15 @@ PlatformType lkPlatform() => lkPlatformImplementation(); bool lkPlatformIs(PlatformType type) => lkPlatform() == type; +bool lkPlatformIsMobile() => + [PlatformType.iOS, PlatformType.android].contains(lkPlatform()); + +bool lkPlatformIsDesktop() => [ + PlatformType.macOS, + PlatformType.windows, + PlatformType.linux + ].contains(lkPlatform()); + bool lkPlatformSupportsE2EE() => lkE2EESupportedImplementation(); bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST'); diff --git a/lib/src/track/audio_management.dart b/lib/src/track/audio_management.dart index f303e37..c2e0b0c 100644 --- a/lib/src/track/audio_management.dart +++ b/lib/src/track/audio_management.dart @@ -1,5 +1,6 @@ import 'package:synchronized/synchronized.dart' as sync; +import '../hardware/hardware.dart'; import '../logger.dart'; import '../support/native.dart'; import '../support/native_audio.dart'; @@ -23,6 +24,9 @@ ConfigureNativeAudioFunc onConfigureNativeAudio = final _trackCounterLock = sync.Lock(); AudioTrackState _audioTrackState = AudioTrackState.none; + +AudioTrackState get audioTrackState => _audioTrackState; + int _localTrackCount = 0; int _remoteTrackCount = 0; @@ -141,7 +145,9 @@ Future defaultNativeAudioConfigurationFunc( AppleAudioCategoryOption.allowBluetooth, AppleAudioCategoryOption.mixWithOthers, }, - appleAudioMode: AppleAudioMode.videoChat, + appleAudioMode: Hardware.instance.preferSpeakerOutput + ? AppleAudioMode.videoChat + : AppleAudioMode.voiceChat, ); } diff --git a/lib/src/track/options.dart b/lib/src/track/options.dart index cb5c02f..c525332 100644 --- a/lib/src/track/options.dart +++ b/lib/src/track/options.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart' show kIsWeb; -import 'package:flutter_webrtc/flutter_webrtc.dart'; - +import '../support/platform.dart'; import '../track/local/audio.dart'; import '../track/local/video.dart'; import '../types/video_parameters.dart'; @@ -132,10 +131,10 @@ class ScreenShareCaptureOptions extends VideoCaptureOptions { @override Map toMediaConstraintsMap() { var constraints = super.toMediaConstraintsMap(); - if (useiOSBroadcastExtension && WebRTC.platformIsIOS) { + if (useiOSBroadcastExtension && lkPlatformIs(PlatformType.iOS)) { constraints['deviceId'] = 'broadcast'; } - if (WebRTC.platformIsDesktop) { + if (lkPlatformIsDesktop()) { if (deviceId != null) { constraints['deviceId'] = {'exact': deviceId}; }