✨ Capture screen audio
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||||
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/chat/call_button.dart';
|
import 'package:island/widgets/chat/call_button.dart';
|
||||||
import 'package:livekit_client/livekit_client.dart';
|
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
@@ -41,7 +43,7 @@ sealed class CallParticipantLive with _$CallParticipantLive {
|
|||||||
|
|
||||||
const factory CallParticipantLive({
|
const factory CallParticipantLive({
|
||||||
required CallParticipant participant,
|
required CallParticipant participant,
|
||||||
required Participant remoteParticipant,
|
required lk.Participant remoteParticipant,
|
||||||
}) = _CallParticipantLive;
|
}) = _CallParticipantLive;
|
||||||
|
|
||||||
bool get isSpeaking => remoteParticipant.isSpeaking;
|
bool get isSpeaking => remoteParticipant.isSpeaking;
|
||||||
@@ -57,21 +59,21 @@ sealed class CallParticipantLive with _$CallParticipantLive {
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class CallNotifier extends _$CallNotifier {
|
class CallNotifier extends _$CallNotifier {
|
||||||
Room? _room;
|
lk.Room? _room;
|
||||||
LocalParticipant? _localParticipant;
|
lk.LocalParticipant? _localParticipant;
|
||||||
List<CallParticipantLive> _participants = [];
|
List<CallParticipantLive> _participants = [];
|
||||||
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
||||||
EventsListener? _roomListener;
|
lk.EventsListener? _roomListener;
|
||||||
|
|
||||||
List<CallParticipantLive> get participants =>
|
List<CallParticipantLive> get participants =>
|
||||||
List.unmodifiable(_participants);
|
List.unmodifiable(_participants);
|
||||||
LocalParticipant? get localParticipant => _localParticipant;
|
lk.LocalParticipant? get localParticipant => _localParticipant;
|
||||||
|
|
||||||
Map<String, double> participantsVolumes = {};
|
Map<String, double> participantsVolumes = {};
|
||||||
|
|
||||||
Timer? _durationTimer;
|
Timer? _durationTimer;
|
||||||
|
|
||||||
Room? get room => _room;
|
lk.Room? get room => _room;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
CallState build() {
|
CallState build() {
|
||||||
@@ -91,10 +93,10 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
_roomListener = _room!.createListener();
|
_roomListener = _room!.createListener();
|
||||||
_room!.addListener(_onRoomChange);
|
_room!.addListener(_onRoomChange);
|
||||||
_roomListener!
|
_roomListener!
|
||||||
..on<ParticipantConnectedEvent>((e) {
|
..on<lk.ParticipantConnectedEvent>((e) {
|
||||||
_refreshLiveParticipants();
|
_refreshLiveParticipants();
|
||||||
})
|
})
|
||||||
..on<RoomDisconnectedEvent>((e) {
|
..on<lk.RoomDisconnectedEvent>((e) {
|
||||||
_participants = [];
|
_participants = [];
|
||||||
state = state.copyWith();
|
state = state.copyWith();
|
||||||
});
|
});
|
||||||
@@ -188,7 +190,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
// Add remote participants
|
// Add remote participants
|
||||||
_participants.addAll(
|
_participants.addAll(
|
||||||
participants.map((p) {
|
participants.map((p) {
|
||||||
RemoteParticipant? remote;
|
lk.RemoteParticipant? remote;
|
||||||
for (final r in remotes) {
|
for (final r in remotes) {
|
||||||
if (r.identity == p.identity) {
|
if (r.identity == p.identity) {
|
||||||
remote = r;
|
remote = r;
|
||||||
@@ -216,7 +218,7 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
return;
|
return;
|
||||||
} else if (_room != null) {
|
} else if (_room != null) {
|
||||||
if (!_room!.isDisposed &&
|
if (!_room!.isDisposed &&
|
||||||
_room!.connectionState != ConnectionState.disconnected) {
|
_room!.connectionState != lk.ConnectionState.disconnected) {
|
||||||
throw Exception('Call already connected');
|
throw Exception('Call already connected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,15 +258,15 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Connect to LiveKit
|
// Connect to LiveKit
|
||||||
_room = Room();
|
_room = lk.Room();
|
||||||
|
|
||||||
await _room!.connect(
|
await _room!.connect(
|
||||||
endpoint,
|
endpoint,
|
||||||
token,
|
token,
|
||||||
connectOptions: ConnectOptions(autoSubscribe: true),
|
connectOptions: lk.ConnectOptions(autoSubscribe: true),
|
||||||
roomOptions: RoomOptions(adaptiveStream: true, dynacast: true),
|
roomOptions: lk.RoomOptions(adaptiveStream: true, dynacast: true),
|
||||||
fastConnectOptions: FastConnectOptions(
|
fastConnectOptions: lk.FastConnectOptions(
|
||||||
microphone: TrackOption(enabled: true),
|
microphone: lk.TrackOption(enabled: true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_localParticipant = _room!.localParticipant;
|
_localParticipant = _room!.localParticipant;
|
||||||
@@ -273,14 +275,14 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
_updateLiveParticipants(participants);
|
_updateLiveParticipants(participants);
|
||||||
|
|
||||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||||
Hardware.instance.setSpeakerphoneOn(true);
|
lk.Hardware.instance.setSpeakerphoneOn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for connection updates
|
// Listen for connection updates
|
||||||
_room!.addListener(() {
|
_room!.addListener(() {
|
||||||
final wasConnected = state.isConnected;
|
final wasConnected = state.isConnected;
|
||||||
final isNowConnected =
|
final isNowConnected =
|
||||||
_room!.connectionState == ConnectionState.connected;
|
_room!.connectionState == lk.ConnectionState.connected;
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isConnected: isNowConnected,
|
isConnected: isNowConnected,
|
||||||
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
|
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
|
||||||
@@ -334,18 +336,43 @@ class CallNotifier extends _$CallNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleScreenShare() async {
|
Future<void> toggleScreenShare(BuildContext context) async {
|
||||||
if (_localParticipant != null) {
|
if (_localParticipant != null) {
|
||||||
final target = !_localParticipant!.isScreenShareEnabled();
|
final target = !_localParticipant!.isScreenShareEnabled();
|
||||||
state = state.copyWith(isScreenSharing: target);
|
state = state.copyWith(isScreenSharing: target);
|
||||||
|
|
||||||
|
if (target && lk.lkPlatformIsDesktop()) {
|
||||||
|
try {
|
||||||
|
final source = await showDialog<DesktopCapturerSource>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => lk.ScreenSelectDialog(),
|
||||||
|
);
|
||||||
|
if (source == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var track = await lk.LocalVideoTrack.createScreenShareTrack(
|
||||||
|
lk.ScreenShareCaptureOptions(
|
||||||
|
sourceId: source.id,
|
||||||
|
maxFrameRate: 30.0,
|
||||||
|
captureScreenAudio: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await _localParticipant!.publishVideoTrack(track);
|
||||||
|
} catch (err) {
|
||||||
|
showErrorAlert(err);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
await _localParticipant!.setScreenShareEnabled(target);
|
await _localParticipant!.setScreenShareEnabled(target);
|
||||||
|
}
|
||||||
|
|
||||||
state = state.copyWith();
|
state = state.copyWith();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleSpeakerphone() async {
|
Future<void> toggleSpeakerphone() async {
|
||||||
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
||||||
await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
await lk.Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
||||||
state = state.copyWith();
|
state = state.copyWith();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -295,7 +295,7 @@ as String?,
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
|
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
|
||||||
|
|
||||||
CallParticipant get participant; Participant get remoteParticipant;
|
CallParticipant get participant; lk.Participant get remoteParticipant;
|
||||||
/// Create a copy of CallParticipantLive
|
/// Create a copy of CallParticipantLive
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -332,7 +332,7 @@ abstract mixin class $CallParticipantLiveCopyWith<$Res> {
|
|||||||
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
|
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
CallParticipant participant, Participant remoteParticipant
|
CallParticipant participant, lk.Participant remoteParticipant
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ class _$CallParticipantLiveCopyWithImpl<$Res>
|
|||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
||||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
||||||
as Participant,
|
as lk.Participant,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
/// Create a copy of CallParticipantLive
|
/// Create a copy of CallParticipantLive
|
||||||
@@ -444,7 +444,7 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant, Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipantLive() when $default != null:
|
case _CallParticipantLive() when $default != null:
|
||||||
return $default(_that.participant,_that.remoteParticipant);case _:
|
return $default(_that.participant,_that.remoteParticipant);case _:
|
||||||
@@ -465,7 +465,7 @@ return $default(_that.participant,_that.remoteParticipant);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant, Participant remoteParticipant) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant, lk.Participant remoteParticipant) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipantLive():
|
case _CallParticipantLive():
|
||||||
return $default(_that.participant,_that.remoteParticipant);}
|
return $default(_that.participant,_that.remoteParticipant);}
|
||||||
@@ -482,7 +482,7 @@ return $default(_that.participant,_that.remoteParticipant);}
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant, Participant remoteParticipant)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _CallParticipantLive() when $default != null:
|
case _CallParticipantLive() when $default != null:
|
||||||
return $default(_that.participant,_that.remoteParticipant);case _:
|
return $default(_that.participant,_that.remoteParticipant);case _:
|
||||||
@@ -501,7 +501,7 @@ class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMi
|
|||||||
|
|
||||||
|
|
||||||
@override final CallParticipant participant;
|
@override final CallParticipant participant;
|
||||||
@override final Participant remoteParticipant;
|
@override final lk.Participant remoteParticipant;
|
||||||
|
|
||||||
/// Create a copy of CallParticipantLive
|
/// Create a copy of CallParticipantLive
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -539,7 +539,7 @@ abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallPartici
|
|||||||
factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
|
factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
CallParticipant participant, Participant remoteParticipant
|
CallParticipant participant, lk.Participant remoteParticipant
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -560,7 +560,7 @@ class __$CallParticipantLiveCopyWithImpl<$Res>
|
|||||||
return _then(_CallParticipantLive(
|
return _then(_CallParticipantLive(
|
||||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
||||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
||||||
as Participant,
|
as lk.Participant,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ part of 'call.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$callNotifierHash() => r'd374402e51d331cf40724e51fd86bce3c5504776';
|
String _$callNotifierHash() => r'438b516c8b786f47495a3435e22b400cd0ca2772';
|
||||||
|
|
||||||
/// See also [CallNotifier].
|
/// See also [CallNotifier].
|
||||||
@ProviderFor(CallNotifier)
|
@ProviderFor(CallNotifier)
|
||||||
|
@@ -42,7 +42,7 @@ class CallControlsBar extends HookConsumerWidget {
|
|||||||
callState.isScreenSharing
|
callState.isScreenSharing
|
||||||
? Icons.stop_screen_share
|
? Icons.stop_screen_share
|
||||||
: Icons.screen_share,
|
: Icons.screen_share,
|
||||||
onPressed: () => callNotifier.toggleScreenShare(),
|
onPressed: () => callNotifier.toggleScreenShare(context),
|
||||||
backgroundColor: const Color(0xFF424242),
|
backgroundColor: const Color(0xFF424242),
|
||||||
),
|
),
|
||||||
_buildCircularButtonWithDropdown(
|
_buildCircularButtonWithDropdown(
|
||||||
@@ -383,7 +383,7 @@ class CallOverlayBar extends HookConsumerWidget {
|
|||||||
: Icons.screen_share,
|
: Icons.screen_share,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
callNotifier.toggleScreenShare();
|
callNotifier.toggleScreenShare(context);
|
||||||
},
|
},
|
||||||
style: actionButtonStyle,
|
style: actionButtonStyle,
|
||||||
),
|
),
|
||||||
|
Reference in New Issue
Block a user