Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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