fix SpeakerPhone switch for mobile. (#312)

* fix SpeakerPhone switch for mobile.

* update.
This commit is contained in:
CloudWebRTC
2023-06-30 14:47:49 +08:00
committed by GitHub
parent 3c964a6553
commit 7b72721560
6 changed files with 125 additions and 51 deletions
+59 -39
View File
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:eva_icons_flutter/eva_icons_flutter.dart'; import 'package:eva_icons_flutter/eva_icons_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_background/flutter_background.dart'; import 'package:flutter_background/flutter_background.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
@@ -36,6 +37,8 @@ class _ControlsWidgetState extends State<ControlsWidget> {
StreamSubscription? _subscription; StreamSubscription? _subscription;
bool _speakerphoneOn = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -106,6 +109,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {
setState(() {}); setState(() {});
} }
void _setSpeakerphoneOn() {
_speakerphoneOn = !_speakerphoneOn;
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
setState(() {});
}
void _toggleCamera() async { void _toggleCamera() async {
// //
final track = participant.videoTracks.firstOrNull?.track; final track = participant.videoTracks.firstOrNull?.track;
@@ -124,7 +133,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
void _enableScreenShare() async { void _enableScreenShare() async {
if (WebRTC.platformIsDesktop) { if (lkPlatformIsDesktop()) {
try { try {
final source = await showDialog<DesktopCapturerSource>( final source = await showDialog<DesktopCapturerSource>(
context: context, context: context,
@@ -147,7 +156,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
return; return;
} }
if (WebRTC.platformIsAndroid) { if (lkPlatformIs(PlatformType.android)) {
// Android specific // Android specific
requestBackgroundPermission([bool isRetry = false]) async { requestBackgroundPermission([bool isRetry = false]) async {
// Required for android screenshare. // Required for android screenshare.
@@ -179,7 +188,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
await requestBackgroundPermission(); await requestBackgroundPermission();
} }
if (WebRTC.platformIsIOS) { if (lkPlatformIs(PlatformType.iOS)) {
var track = await LocalVideoTrack.createScreenShareTrack( var track = await LocalVideoTrack.createScreenShareTrack(
const ScreenShareCaptureOptions( const ScreenShareCaptureOptions(
useiOSBroadcastExtension: true, useiOSBroadcastExtension: true,
@@ -314,43 +323,54 @@ class _ControlsWidgetState extends State<ControlsWidget> {
icon: const Icon(EvaIcons.micOff), icon: const Icon(EvaIcons.micOff),
tooltip: 'un-mute audio', tooltip: 'un-mute audio',
), ),
PopupMenuButton<MediaDevice>( if (!lkPlatformIsMobile())
icon: const Icon(Icons.volume_up), PopupMenuButton<MediaDevice>(
itemBuilder: (BuildContext context) { icon: const Icon(Icons.volume_up),
return [ itemBuilder: (BuildContext context) {
const PopupMenuItem<MediaDevice>( return [
value: null, const PopupMenuItem<MediaDevice>(
child: ListTile( value: null,
leading: Icon( child: ListTile(
EvaIcons.speaker, leading: Icon(
color: Colors.white, EvaIcons.speaker,
), color: Colors.white,
title: Text('Select Audio Output'),
),
),
if (_audioOutputs != null)
..._audioOutputs!.map((device) {
return PopupMenuItem<MediaDevice>(
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), title: Text('Select Audio Output'),
); ),
}).toList() ),
]; if (_audioOutputs != null)
}, ..._audioOutputs!.map((device) {
), return PopupMenuItem<MediaDevice>(
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()) if (participant.isCameraEnabled())
PopupMenuButton<MediaDevice>( PopupMenuButton<MediaDevice>(
icon: const Icon(EvaIcons.video), icon: const Icon(EvaIcons.video),
+2 -1
View File
@@ -8,6 +8,7 @@ import '../internal/types.dart';
import '../logger.dart'; import '../logger.dart';
import '../options.dart'; import '../options.dart';
import '../support/disposable.dart'; import '../support/disposable.dart';
import '../support/platform.dart';
import '../types/other.dart'; import '../types/other.dart';
import '../utils.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(); await pc.restartIce();
} }
+45 -6
View File
@@ -4,6 +4,10 @@ import 'package:collection/collection.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import '../logger.dart'; import '../logger.dart';
import '../support/native.dart';
import '../support/native_audio.dart';
import '../support/platform.dart';
import '../track/audio_management.dart';
class MediaDevice { class MediaDevice {
const MediaDevice(this.deviceId, this.label, this.kind); const MediaDevice(this.deviceId, this.label, this.kind);
@@ -59,6 +63,8 @@ class Hardware {
bool? speakerOn; bool? speakerOn;
bool _preferSpeakerOutput = false;
Future<List<MediaDevice>> enumerateDevices({String? type}) async { Future<List<MediaDevice>> enumerateDevices({String? type}) async {
var infos = await rtc.navigator.mediaDevices.enumerateDevices(); var infos = await rtc.navigator.mediaDevices.enumerateDevices();
var devices = var devices =
@@ -82,7 +88,7 @@ class Hardware {
} }
Future<void> selectAudioOutput(MediaDevice device) async { Future<void> selectAudioOutput(MediaDevice device) async {
if (rtc.WebRTC.platformIsWeb) { if (lkPlatformIs(PlatformType.web)) {
logger.warning('selectAudioOutput not support on web'); logger.warning('selectAudioOutput not support on web');
return; return;
} }
@@ -91,7 +97,7 @@ class Hardware {
} }
Future<void> selectAudioInput(MediaDevice device) async { Future<void> selectAudioInput(MediaDevice device) async {
if (rtc.WebRTC.platformIsWeb) { if (lkPlatformIs(PlatformType.web)) {
logger.warning( logger.warning(
'selectAudioInput is only supported on Android/Windows/macOS'); 'selectAudioInput is only supported on Android/Windows/macOS');
return; return;
@@ -100,10 +106,43 @@ class Hardware {
await rtc.Helper.selectAudioInput(device.deviceId); await rtc.Helper.selectAudioInput(device.deviceId);
} }
Future<void> 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<void> setSpeakerphoneOn(bool enable) async { Future<void> setSpeakerphoneOn(bool enable) async {
if (rtc.WebRTC.platformIsMobile) { if (lkPlatformIsMobile()) {
speakerOn = enable; if (canSwitchSpeakerphone) {
await rtc.Helper.setSpeakerphoneOn(enable); speakerOn = enable;
await rtc.Helper.setSpeakerphoneOn(enable);
} else {
logger.warning('Can\'t switch speaker/earpiece');
}
} else { } else {
logger.warning('setSpeakerphoneOn only support on iOS/Android'); logger.warning('setSpeakerphoneOn only support on iOS/Android');
} }
@@ -115,7 +154,7 @@ class Hardware {
if (facingMode != null) 'facingMode': facingMode ? 'user' : 'environment', if (facingMode != null) 'facingMode': facingMode ? 'user' : 'environment',
}; };
if (device != null) { if (device != null) {
if (rtc.WebRTC.platformIsWeb) { if (lkPlatformIs(PlatformType.web)) {
constraints['deviceId'] = device.deviceId; constraints['deviceId'] = device.deviceId;
} else { } else {
constraints['optional'] = [ constraints['optional'] = [
+9
View File
@@ -7,6 +7,15 @@ PlatformType lkPlatform() => lkPlatformImplementation();
bool lkPlatformIs(PlatformType type) => lkPlatform() == type; 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 lkPlatformSupportsE2EE() => lkE2EESupportedImplementation();
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST'); bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
+7 -1
View File
@@ -1,5 +1,6 @@
import 'package:synchronized/synchronized.dart' as sync; import 'package:synchronized/synchronized.dart' as sync;
import '../hardware/hardware.dart';
import '../logger.dart'; import '../logger.dart';
import '../support/native.dart'; import '../support/native.dart';
import '../support/native_audio.dart'; import '../support/native_audio.dart';
@@ -23,6 +24,9 @@ ConfigureNativeAudioFunc onConfigureNativeAudio =
final _trackCounterLock = sync.Lock(); final _trackCounterLock = sync.Lock();
AudioTrackState _audioTrackState = AudioTrackState.none; AudioTrackState _audioTrackState = AudioTrackState.none;
AudioTrackState get audioTrackState => _audioTrackState;
int _localTrackCount = 0; int _localTrackCount = 0;
int _remoteTrackCount = 0; int _remoteTrackCount = 0;
@@ -141,7 +145,9 @@ Future<NativeAudioConfiguration> defaultNativeAudioConfigurationFunc(
AppleAudioCategoryOption.allowBluetooth, AppleAudioCategoryOption.allowBluetooth,
AppleAudioCategoryOption.mixWithOthers, AppleAudioCategoryOption.mixWithOthers,
}, },
appleAudioMode: AppleAudioMode.videoChat, appleAudioMode: Hardware.instance.preferSpeakerOutput
? AppleAudioMode.videoChat
: AppleAudioMode.voiceChat,
); );
} }
+3 -4
View File
@@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart' show kIsWeb; 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/audio.dart';
import '../track/local/video.dart'; import '../track/local/video.dart';
import '../types/video_parameters.dart'; import '../types/video_parameters.dart';
@@ -132,10 +131,10 @@ class ScreenShareCaptureOptions extends VideoCaptureOptions {
@override @override
Map<String, dynamic> toMediaConstraintsMap() { Map<String, dynamic> toMediaConstraintsMap() {
var constraints = super.toMediaConstraintsMap(); var constraints = super.toMediaConstraintsMap();
if (useiOSBroadcastExtension && WebRTC.platformIsIOS) { if (useiOSBroadcastExtension && lkPlatformIs(PlatformType.iOS)) {
constraints['deviceId'] = 'broadcast'; constraints['deviceId'] = 'broadcast';
} }
if (WebRTC.platformIsDesktop) { if (lkPlatformIsDesktop()) {
if (deviceId != null) { if (deviceId != null) {
constraints['deviceId'] = {'exact': deviceId}; constraints['deviceId'] = {'exact': deviceId};
} }