Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/json-serializable-unpin
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="changed" "Update json_annotation to 4.12 and regenerate serialization code with json_serializable 6.14"
1 change: 1 addition & 0 deletions .changes/raise-floor-flutter-338
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minor type="changed" "Raise minimum supported versions to Flutter 3.38 / Dart 3.10, the floor for stable native assets support"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Flutter client SDK for LiveKit (`livekit_client` on pub.dev), built on top of `f

## Commands

Requires Dart >= 3.6 / Flutter >= 3.27.
Requires Dart >= 3.10 / Flutter >= 3.38 (floor set by stable native assets support).

```sh
flutter pub get
Expand Down
9 changes: 5 additions & 4 deletions lib/src/agent/agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ class Agent extends ChangeNotifier {
final RemoteAudioTrack? nextAudioTrack = _resolveAudioTrack(participant);
final RemoteVideoTrack? nextAvatarTrack = _resolveAvatarVideoTrack(participant);

final bool shouldNotify = _state != _AgentLifecycle.connected ||
final bool shouldNotify =
_state != _AgentLifecycle.connected ||
_agentState != nextAgentState ||
!identical(_audioTrack, nextAudioTrack) ||
!identical(_avatarVideoTrack, nextAvatarTrack) ||
Expand Down Expand Up @@ -220,9 +221,9 @@ enum AgentFailure {

/// A human-readable error message.
String get message => switch (this) {
AgentFailure.timeout => 'Agent did not connect',
AgentFailure.left => 'Agent left the room unexpectedly',
};
AgentFailure.timeout => 'Agent did not connect',
AgentFailure.left => 'Agent left the room unexpectedly',
};
}

enum _AgentLifecycle {
Expand Down
11 changes: 6 additions & 5 deletions lib/src/agent/chat/transcription_stream_receiver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ class TranscriptionStreamReceiver implements MessageReceiver {
this.topic = 'lk.transcription',
void Function(String topic, TextStreamHandler handler)? registerHandler,
void Function(String topic)? unregisterHandler,
}) : _room = room,
_registerHandler = registerHandler ?? room.registerTextStreamHandler,
_unregisterHandler = unregisterHandler ?? room.unregisterTextStreamHandler;
}) : _room = room,
_registerHandler = registerHandler ?? room.registerTextStreamHandler,
_unregisterHandler = unregisterHandler ?? room.unregisterTextStreamHandler;

final Room _room;
final String topic;
Expand Down Expand Up @@ -217,8 +217,9 @@ class TranscriptionStreamReceiver implements MessageReceiver {
final displayTimestamp = partial?.timestamp ?? timestamp;
final isLocalParticipant = _room.localParticipant?.identity == participantIdentity;

final ReceivedMessageContent content =
isLocalParticipant ? UserTranscript(displayContent) : AgentTranscript(displayContent);
final ReceivedMessageContent content = isLocalParticipant
? UserTranscript(displayContent)
: AgentTranscript(displayContent);

return ReceivedMessage(
id: segmentId,
Expand Down
16 changes: 8 additions & 8 deletions lib/src/agent/room_agent.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ extension AgentRoom on Room {
/// participants whose `lk.publish_on_behalf` attribute matches the agent's
/// identity.
Iterable<RemoteParticipant> get agentParticipants => remoteParticipants.values.where(
(participant) {
if (participant.kind != ParticipantKind.AGENT) {
return false;
}
final publishOnBehalf = participant.attributes[lkPublishOnBehalfAttributeKey];
return publishOnBehalf == null || publishOnBehalf.isEmpty;
},
);
(participant) {
if (participant.kind != ParticipantKind.AGENT) {
return false;
}
final publishOnBehalf = participant.attributes[lkPublishOnBehalfAttributeKey];
return publishOnBehalf == null || publishOnBehalf.isEmpty;
},
);

/// The first agent participant in the room, if one exists.
RemoteParticipant? get agentParticipant => agentParticipants.firstOrNull;
Expand Down
24 changes: 13 additions & 11 deletions lib/src/agent/session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ class Session extends DisposableChangeNotifier {
required SessionOptions options,
List<MessageSender>? senders,
List<MessageReceiver>? receivers,
}) : _tokenSourceConfiguration = tokenSourceConfiguration,
_options = options,
room = options.room {
}) : _tokenSourceConfiguration = tokenSourceConfiguration,
_options = options,
room = options.room {
_agent.addListener(notifyListeners);

final textMessageSender = TextMessageSender(room: room);
Expand Down Expand Up @@ -160,9 +160,9 @@ class Session extends DisposableChangeNotifier {
ConnectionState _connectionState = ConnectionState.disconnected;

bool get isConnected => switch (_connectionState) {
ConnectionState.connecting || ConnectionState.connected || ConnectionState.reconnecting => true,
ConnectionState.disconnected => false,
};
ConnectionState.connecting || ConnectionState.connected || ConnectionState.reconnecting => true,
ConnectionState.disconnected => false,
};

final LinkedHashMap<String, ReceivedMessage> _messages = LinkedHashMap();
UnmodifiableListView<ReceivedMessage> _messagesView = UnmodifiableListView<ReceivedMessage>(const []);
Expand Down Expand Up @@ -285,7 +285,9 @@ class Session extends DisposableChangeNotifier {
_messages
..clear()
..addEntries(
messages.sorted((a, b) => a.timestamp.compareTo(b.timestamp)).map(
messages
.sorted((a, b) => a.timestamp.compareTo(b.timestamp))
.map(
(message) => MapEntry(message.id, message),
),
);
Expand Down Expand Up @@ -407,10 +409,10 @@ class SessionError {
final Object cause;

String get message => switch (kind) {
SessionErrorKind.connection => 'Connection failed: ${cause}',
SessionErrorKind.sender => 'Message sender failed: ${cause}',
SessionErrorKind.receiver => 'Message receiver failed: ${cause}',
};
SessionErrorKind.connection => 'Connection failed: ${cause}',
SessionErrorKind.sender => 'Message sender failed: ${cause}',
SessionErrorKind.receiver => 'Message receiver failed: ${cause}',
};

static SessionError connection(Object cause) => SessionError._(SessionErrorKind.connection, cause);

Expand Down
14 changes: 8 additions & 6 deletions lib/src/audio/audio_frame_capture_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,14 @@ class AudioFrameCaptureNative implements AudioFrameCapture {
_streamSubscription = _eventChannel?.receiveBroadcastStream().listen((event) {
try {
final rawFormat = event['commonFormat'] as String?;
_controller.add(AudioFrame(
sampleRate: event['sampleRate'] as int,
channels: event['channels'] as int,
data: event['data'] as Uint8List,
format: rawFormat == AudioFormat.Float32.value ? AudioFormat.Float32 : AudioFormat.Int16,
));
_controller.add(
AudioFrame(
sampleRate: event['sampleRate'] as int,
channels: event['channels'] as int,
data: event['data'] as Uint8List,
format: rawFormat == AudioFormat.Float32.value ? AudioFormat.Float32 : AudioFormat.Int16,
),
);
} catch (e) {
logger.warning('[AudioFrameCapture] Error parsing native event: $e');
}
Expand Down
14 changes: 8 additions & 6 deletions lib/src/audio/audio_frame_capture_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,14 @@ class AudioFrameCaptureWeb implements AudioFrameCapture {
bytes = float32ToInt16Bytes(srcFloat32, channels, outChannels, frames);
}

controller.add(AudioFrame(
sampleRate: actualSampleRate,
channels: outChannels,
data: bytes,
format: _targetFormat,
));
controller.add(
AudioFrame(
sampleRate: actualSampleRate,
channels: outChannels,
data: bytes,
format: _targetFormat,
),
);
} catch (e) {
logger.warning('[AudioFrameCapture] Error processing worklet frame: $e');
}
Expand Down
10 changes: 5 additions & 5 deletions lib/src/audio/audio_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,11 @@ class AudioManager {
}

ResolvedAudioSessionPolicy _resolvedAudioSessionPolicy(AudioSessionOptions options) => ResolvedAudioSessionPolicy(
options: options,
preferSpeakerOutput: _preferSpeakerOutput,
forceSpeakerOutput: _forceSpeakerOutput && _preferSpeakerOutput,
automatic: _isAutomaticConfigurationEnabled,
);
options: options,
preferSpeakerOutput: _preferSpeakerOutput,
forceSpeakerOutput: _forceSpeakerOutput && _preferSpeakerOutput,
automatic: _isAutomaticConfigurationEnabled,
);

/// How microphone input is muted on iOS/macOS.
///
Expand Down
47 changes: 22 additions & 25 deletions lib/src/audio/audio_processing_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ enum AudioProcessingImplementation {
final String value;

static AudioProcessingImplementation fromValue(String? value) => AudioProcessingImplementation.values.firstWhere(
(e) => e.value == value,
orElse: () => AudioProcessingImplementation.unknown,
);
(e) => e.value == value,
orElse: () => AudioProcessingImplementation.unknown,
);
}

AudioProcessingMode _modeFromValue(String? value) {
Expand All @@ -56,9 +56,9 @@ class AudioProcessingComponentRequest {
});

factory AudioProcessingComponentRequest.fromMap(Map<dynamic, dynamic> map) => AudioProcessingComponentRequest(
enabled: (map['enabled'] as bool?) ?? false,
mode: _modeFromValue(map['mode'] as String?),
);
enabled: (map['enabled'] as bool?) ?? false,
mode: _modeFromValue(map['mode'] as String?),
);

final bool enabled;
final AudioProcessingMode mode;
Expand All @@ -84,16 +84,16 @@ class AudioProcessingComponentState {
});

factory AudioProcessingComponentState.fromMap(Map<dynamic, dynamic> map) => AudioProcessingComponentState(
requested: map['requested'] is Map
? AudioProcessingComponentRequest.fromMap(Map<dynamic, dynamic>.from(map['requested'] as Map))
: null,
isSoftwareResolved: (map['isSoftwareResolved'] as bool?) ?? false,
isSoftwareActive: (map['isSoftwareActive'] as bool?) ?? false,
isPlatformAvailable: (map['isPlatformAvailable'] as bool?) ?? false,
isPlatformResolved: (map['isPlatformResolved'] as bool?) ?? false,
isPlatformActive: (map['isPlatformActive'] as bool?) ?? false,
effective: AudioProcessingImplementation.fromValue(map['effective'] as String?),
);
requested: map['requested'] is Map
? AudioProcessingComponentRequest.fromMap(Map<dynamic, dynamic>.from(map['requested'] as Map))
: null,
isSoftwareResolved: (map['isSoftwareResolved'] as bool?) ?? false,
isSoftwareActive: (map['isSoftwareActive'] as bool?) ?? false,
isPlatformAvailable: (map['isPlatformAvailable'] as bool?) ?? false,
isPlatformResolved: (map['isPlatformResolved'] as bool?) ?? false,
isPlatformActive: (map['isPlatformActive'] as bool?) ?? false,
effective: AudioProcessingImplementation.fromValue(map['effective'] as String?),
);

/// What the caller most recently requested for this component. Null when no
/// audio processing options have ever been applied — "nobody asked".
Expand Down Expand Up @@ -141,15 +141,12 @@ class AudioProcessingState {
});

factory AudioProcessingState.fromMap(Map<dynamic, dynamic> map) => AudioProcessingState(
hasAudioProcessingModule: (map['hasAudioProcessingModule'] as bool?) ?? false,
echoCancellation:
AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['echoCancellation'] as Map)),
noiseSuppression:
AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['noiseSuppression'] as Map)),
autoGainControl:
AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['autoGainControl'] as Map)),
highPassFilter: AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['highPassFilter'] as Map)),
);
hasAudioProcessingModule: (map['hasAudioProcessingModule'] as bool?) ?? false,
echoCancellation: AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['echoCancellation'] as Map)),
noiseSuppression: AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['noiseSuppression'] as Map)),
autoGainControl: AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['autoGainControl'] as Map)),
highPassFilter: AudioProcessingComponentState.fromMap(Map<dynamic, dynamic>.from(map['highPassFilter'] as Map)),
);

final bool hasAudioProcessingModule;
final AudioProcessingComponentState echoCancellation;
Expand Down
42 changes: 18 additions & 24 deletions lib/src/audio/audio_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ enum AudioSessionManagementMode {
}

@immutable

/// Experimental: this API may change in a future release.
@experimental
class AudioSessionOptions {
Expand Down Expand Up @@ -99,11 +98,10 @@ class AudioSessionOptions {
AudioSessionOptions copyWith({
ValueOrAbsent<AppleAudioSessionConfiguration> apple = const ValueOrAbsent.absent(),
ValueOrAbsent<AndroidAudioSessionConfiguration> android = const ValueOrAbsent.absent(),
}) =>
AudioSessionOptions._(
apple: apple.valueOr(this.apple),
android: android.valueOr(this.android),
);
}) => AudioSessionOptions._(
apple: apple.valueOr(this.apple),
android: android.valueOr(this.android),
);
}

// https://developer.apple.com/documentation/avfaudio/avaudiosession/category
Expand Down Expand Up @@ -146,7 +144,6 @@ enum AppleAudioMode {
}

@immutable

/// Experimental: this API may change in a future release.
@experimental
class AppleAudioSessionConfiguration {
Expand Down Expand Up @@ -185,12 +182,11 @@ class AppleAudioSessionConfiguration {
ValueOrAbsent<AppleAudioCategory?> category = const ValueOrAbsent.absent(),
ValueOrAbsent<Set<AppleAudioCategoryOption>?> categoryOptions = const ValueOrAbsent.absent(),
ValueOrAbsent<AppleAudioMode?> mode = const ValueOrAbsent.absent(),
}) =>
AppleAudioSessionConfiguration(
category: category.valueOr(this.category),
categoryOptions: categoryOptions.valueOr(this.categoryOptions),
mode: mode.valueOr(this.mode),
);
}) => AppleAudioSessionConfiguration(
category: category.valueOr(this.category),
categoryOptions: categoryOptions.valueOr(this.categoryOptions),
mode: mode.valueOr(this.mode),
);
}

/// Experimental: this API may change in a future release.
Expand Down Expand Up @@ -254,7 +250,6 @@ enum AndroidAudioAttributesContentType {
}

@immutable

/// Experimental: this API may change in a future release.
@experimental
class AndroidAudioSessionConfiguration {
Expand Down Expand Up @@ -315,14 +310,13 @@ class AndroidAudioSessionConfiguration {
ValueOrAbsent<AndroidAudioAttributesUsageType?> usageType = const ValueOrAbsent.absent(),
ValueOrAbsent<AndroidAudioAttributesContentType?> contentType = const ValueOrAbsent.absent(),
ValueOrAbsent<bool?> forceAudioRouting = const ValueOrAbsent.absent(),
}) =>
AndroidAudioSessionConfiguration(
audioMode: audioMode.valueOr(this.audioMode),
manageAudioFocus: manageAudioFocus.valueOr(this.manageAudioFocus),
focusMode: focusMode.valueOr(this.focusMode),
streamType: streamType.valueOr(this.streamType),
usageType: usageType.valueOr(this.usageType),
contentType: contentType.valueOr(this.contentType),
forceAudioRouting: forceAudioRouting.valueOr(this.forceAudioRouting),
);
}) => AndroidAudioSessionConfiguration(
audioMode: audioMode.valueOr(this.audioMode),
manageAudioFocus: manageAudioFocus.valueOr(this.manageAudioFocus),
focusMode: focusMode.valueOr(this.focusMode),
streamType: streamType.valueOr(this.streamType),
usageType: usageType.valueOr(this.usageType),
contentType: contentType.valueOr(this.contentType),
forceAudioRouting: forceAudioRouting.valueOr(this.forceAudioRouting),
);
}
20 changes: 10 additions & 10 deletions lib/src/connection_check/checks/checker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ abstract class Checker extends Disposable with EventsEmittable<CheckerEvent> {
this.token, {
CheckerOptions? options,
Room? room,
}) : options = options ?? CheckerOptions(),
connectOptions = options?.connectOptions,
_ownsRoom = room == null,
room = room ?? Room(roomOptions: options?.roomOptions ?? const RoomOptions()) {
}) : options = options ?? CheckerOptions(),
connectOptions = options?.connectOptions,
_ownsRoom = room == null,
room = room ?? Room(roomOptions: options?.roomOptions ?? const RoomOptions()) {
onDispose(() async {
await events.dispose();
if (_ownsRoom) {
Expand Down Expand Up @@ -356,10 +356,10 @@ abstract class Checker extends Disposable with EventsEmittable<CheckerEvent> {

/// The current snapshot of this check.
CheckInfo getInfo() => CheckInfo(
name: name,
description: description,
status: status,
logs: List.unmodifiable(logs),
data: data,
);
name: name,
description: description,
status: status,
logs: List.unmodifiable(logs),
data: data,
);
}
3 changes: 2 additions & 1 deletion lib/src/connection_check/checks/connection_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class ProtocolStats {
int count = 0;

@override
String toString() => '$runtimeType(protocol: ${protocol.name}, packetsSent: $packetsSent, '
String toString() =>
'$runtimeType(protocol: ${protocol.name}, packetsSent: $packetsSent, '
'packetsLost: $packetsLost, count: $count)';
}

Expand Down
Loading
Loading