Capture screen audio

This commit is contained in:
2025-10-05 12:34:43 +08:00
parent 6be7dfbc61
commit 2cd1416a13
4 changed files with 60 additions and 33 deletions

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:island/widgets/alert.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:riverpod_annotation/riverpod_annotation.dart';
import 'package:island/pods/network.dart';
@@ -41,7 +43,7 @@ sealed class CallParticipantLive with _$CallParticipantLive {
const factory CallParticipantLive({
required CallParticipant participant,
required Participant remoteParticipant,
required lk.Participant remoteParticipant,
}) = _CallParticipantLive;
bool get isSpeaking => remoteParticipant.isSpeaking;
@@ -57,21 +59,21 @@ sealed class CallParticipantLive with _$CallParticipantLive {
@Riverpod(keepAlive: true)
class CallNotifier extends _$CallNotifier {
Room? _room;
LocalParticipant? _localParticipant;
lk.Room? _room;
lk.LocalParticipant? _localParticipant;
List<CallParticipantLive> _participants = [];
final Map<String, CallParticipant> _participantInfoByIdentity = {};
EventsListener? _roomListener;
lk.EventsListener? _roomListener;
List<CallParticipantLive> get participants =>
List.unmodifiable(_participants);
LocalParticipant? get localParticipant => _localParticipant;
lk.LocalParticipant? get localParticipant => _localParticipant;
Map<String, double> participantsVolumes = {};
Timer? _durationTimer;
Room? get room => _room;
lk.Room? get room => _room;
@override
CallState build() {
@@ -91,10 +93,10 @@ class CallNotifier extends _$CallNotifier {
_roomListener = _room!.createListener();
_room!.addListener(_onRoomChange);
_roomListener!
..on<ParticipantConnectedEvent>((e) {
..on<lk.ParticipantConnectedEvent>((e) {
_refreshLiveParticipants();
})
..on<RoomDisconnectedEvent>((e) {
..on<lk.RoomDisconnectedEvent>((e) {
_participants = [];
state = state.copyWith();
});
@@ -188,7 +190,7 @@ class CallNotifier extends _$CallNotifier {
// Add remote participants
_participants.addAll(
participants.map((p) {
RemoteParticipant? remote;
lk.RemoteParticipant? remote;
for (final r in remotes) {
if (r.identity == p.identity) {
remote = r;
@@ -216,7 +218,7 @@ class CallNotifier extends _$CallNotifier {
return;
} else if (_room != null) {
if (!_room!.isDisposed &&
_room!.connectionState != ConnectionState.disconnected) {
_room!.connectionState != lk.ConnectionState.disconnected) {
throw Exception('Call already connected');
}
}
@@ -256,15 +258,15 @@ class CallNotifier extends _$CallNotifier {
});
// Connect to LiveKit
_room = Room();
_room = lk.Room();
await _room!.connect(
endpoint,
token,
connectOptions: ConnectOptions(autoSubscribe: true),
roomOptions: RoomOptions(adaptiveStream: true, dynacast: true),
fastConnectOptions: FastConnectOptions(
microphone: TrackOption(enabled: true),
connectOptions: lk.ConnectOptions(autoSubscribe: true),
roomOptions: lk.RoomOptions(adaptiveStream: true, dynacast: true),
fastConnectOptions: lk.FastConnectOptions(
microphone: lk.TrackOption(enabled: true),
),
);
_localParticipant = _room!.localParticipant;
@@ -273,14 +275,14 @@ class CallNotifier extends _$CallNotifier {
_updateLiveParticipants(participants);
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
Hardware.instance.setSpeakerphoneOn(true);
lk.Hardware.instance.setSpeakerphoneOn(true);
}
// Listen for connection updates
_room!.addListener(() {
final wasConnected = state.isConnected;
final isNowConnected =
_room!.connectionState == ConnectionState.connected;
_room!.connectionState == lk.ConnectionState.connected;
state = state.copyWith(
isConnected: isNowConnected,
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
@@ -334,18 +336,43 @@ class CallNotifier extends _$CallNotifier {
}
}
Future<void> toggleScreenShare() async {
Future<void> toggleScreenShare(BuildContext context) async {
if (_localParticipant != null) {
final target = !_localParticipant!.isScreenShareEnabled();
state = state.copyWith(isScreenSharing: target);
await _localParticipant!.setScreenShareEnabled(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);
}
state = state.copyWith();
}
}
Future<void> toggleSpeakerphone() async {
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
await lk.Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
state = state.copyWith();
}

View File

@@ -295,7 +295,7 @@ as String?,
/// @nodoc
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
CallParticipant get participant; Participant get remoteParticipant;
CallParticipant get participant; lk.Participant get remoteParticipant;
/// Create a copy of CallParticipantLive
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -332,7 +332,7 @@ abstract mixin class $CallParticipantLiveCopyWith<$Res> {
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
@useResult
$Res call({
CallParticipant participant, Participant remoteParticipant
CallParticipant participant, lk.Participant remoteParticipant
});
@@ -353,7 +353,7 @@ class _$CallParticipantLiveCopyWithImpl<$Res>
return _then(_self.copyWith(
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 Participant,
as lk.Participant,
));
}
/// 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) {
case _CallParticipantLive() when $default != null:
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) {
case _CallParticipantLive():
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) {
case _CallParticipantLive() when $default != null:
return $default(_that.participant,_that.remoteParticipant);case _:
@@ -501,7 +501,7 @@ class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMi
@override final CallParticipant participant;
@override final Participant remoteParticipant;
@override final lk.Participant remoteParticipant;
/// Create a copy of CallParticipantLive
/// 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;
@override @useResult
$Res call({
CallParticipant participant, Participant remoteParticipant
CallParticipant participant, lk.Participant remoteParticipant
});
@@ -560,7 +560,7 @@ class __$CallParticipantLiveCopyWithImpl<$Res>
return _then(_CallParticipantLive(
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 Participant,
as lk.Participant,
));
}

View File

@@ -6,7 +6,7 @@ part of 'call.dart';
// RiverpodGenerator
// **************************************************************************
String _$callNotifierHash() => r'd374402e51d331cf40724e51fd86bce3c5504776';
String _$callNotifierHash() => r'438b516c8b786f47495a3435e22b400cd0ca2772';
/// See also [CallNotifier].
@ProviderFor(CallNotifier)

View File

@@ -42,7 +42,7 @@ class CallControlsBar extends HookConsumerWidget {
callState.isScreenSharing
? Icons.stop_screen_share
: Icons.screen_share,
onPressed: () => callNotifier.toggleScreenShare(),
onPressed: () => callNotifier.toggleScreenShare(context),
backgroundColor: const Color(0xFF424242),
),
_buildCircularButtonWithDropdown(
@@ -383,7 +383,7 @@ class CallOverlayBar extends HookConsumerWidget {
: Icons.screen_share,
),
onPressed: () {
callNotifier.toggleScreenShare();
callNotifier.toggleScreenShare(context);
},
style: actionButtonStyle,
),