fix SpeakerPhone switch for mobile. (#312)
* fix SpeakerPhone switch for mobile. * update.
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'] = [
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user