From 779b37530a5b5e588697479fd568a86c1100767f Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:31:07 +0700 Subject: [PATCH 1/3] v2.5.3 --- .version | 2 +- CHANGELOG.md | 6 ++++++ ios/livekit_client.podspec | 2 +- lib/src/livekit.dart | 2 +- macos/livekit_client.podspec | 2 +- pubspec.yaml | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.version b/.version index f225a78a..aedc15bb 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.5.2 +2.5.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2304a82f..9edb9747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 2.5.3 + +feat: Data Packet Cryptor (#873) +feat: Add E2EE support for H265 (#864) +fix: fix async call for update participant info (#897) + ## 2.5.2 * Fix missing properties for TextStreamInfo (#881) diff --git a/ios/livekit_client.podspec b/ios/livekit_client.podspec index 4c7ed198..395a88a4 100644 --- a/ios/livekit_client.podspec +++ b/ios/livekit_client.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'livekit_client' - s.version = '2.5.2' + s.version = '2.5.3' s.summary = 'Open source platform for real-time audio and video.' s.description = 'Open source platform for real-time audio and video.' s.homepage = 'https://livekit.io/' diff --git a/lib/src/livekit.dart b/lib/src/livekit.dart index 31fb0fde..9a7dba07 100644 --- a/lib/src/livekit.dart +++ b/lib/src/livekit.dart @@ -20,7 +20,7 @@ import 'support/platform.dart' show lkPlatformIsMobile; /// Main entry point to connect to a room. /// {@category Room} class LiveKitClient { - static const version = '2.5.2'; + static const version = '2.5.3'; /// Initialize the WebRTC plugin. If this is not manually called, will be /// initialized with default settings. diff --git a/macos/livekit_client.podspec b/macos/livekit_client.podspec index b472e04f..e9465c93 100644 --- a/macos/livekit_client.podspec +++ b/macos/livekit_client.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'livekit_client' - s.version = '2.5.2' + s.version = '2.5.3' s.summary = 'Open source platform for real-time audio and video.' s.description = 'Open source platform for real-time audio and video.' s.homepage = 'https://livekit.io/' diff --git a/pubspec.yaml b/pubspec.yaml index 6090391e..17f5b97c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ name: livekit_client description: Flutter Client SDK for LiveKit. Build real-time video and audio into your apps. Supports iOS, Android, and Web. -version: 2.5.2 +version: 2.5.3 homepage: https://github.com/livekit/client-sdk-flutter environment: From 44915defa8c8ecbc3ba715fb8841faab6da4e1e5 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:31:55 +0700 Subject: [PATCH 2/3] format --- lib/src/participant/local.dart | 3 +- lib/src/publication/track_publication.dart | 3 +- lib/src/track/local/audio.dart | 3 +- lib/src/track/remote/audio.dart | 3 +- lib/src/track/remote/video.dart | 3 +- lib/src/widgets/screen_select_dialog.dart | 3 +- test/core/data_stream_test.dart | 92 ++++++++-------------- web/e2ee.data_packet_cryptor.dart | 36 +++------ web/e2ee.keyhandler.dart | 3 +- web/e2ee.utils.dart | 8 +- 10 files changed, 53 insertions(+), 104 deletions(-) diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index 55367183..6144b46f 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -53,9 +53,8 @@ import '../types/other.dart'; import '../types/participant_permissions.dart'; import '../types/rpc.dart'; import '../types/video_dimensions.dart'; -import 'participant.dart'; - import '../utils.dart' show buildStreamId, mimeTypeToVideoCodecString, Utils, compareVersions, isSVCCodec; +import 'participant.dart'; /// Represents the current participant in the room. Instance of [LocalParticipant] is automatically /// created after successfully connecting to a [Room] and will be accessible from [Room.localParticipant]. diff --git a/lib/src/publication/track_publication.dart b/lib/src/publication/track_publication.dart index 858d2084..febd7801 100644 --- a/lib/src/publication/track_publication.dart +++ b/lib/src/publication/track_publication.dart @@ -119,8 +119,7 @@ abstract class TrackPublication extends Disposable { void _attachTrackListener(T track) { // listen for Track's muted events final listener = track.createListener() - ..on( - (event) => _onTrackMuteUpdatedEvent(event)); + ..on((event) => _onTrackMuteUpdatedEvent(event)); // dispose listener when the track is disposed track.onDispose(() => listener.dispose()); } diff --git a/lib/src/track/local/audio.dart b/lib/src/track/local/audio.dart index 571847f4..e3675a34 100644 --- a/lib/src/track/local/audio.dart +++ b/lib/src/track/local/audio.dart @@ -62,8 +62,7 @@ class LocalAudioTrack extends LocalTrack with AudioTrack, LocalAudioManagementMi if (stats != null && prevStats != null && sender != null) { final bitrate = computeBitrateForSenderStats(stats, prevStats); _currentBitrate = bitrate; - events - .emit(AudioSenderStatsEvent(stats: stats, currentBitrate: bitrate)); + events.emit(AudioSenderStatsEvent(stats: stats, currentBitrate: bitrate)); } prevStats = stats; diff --git a/lib/src/track/remote/audio.dart b/lib/src/track/remote/audio.dart index 71137e42..dd217f09 100644 --- a/lib/src/track/remote/audio.dart +++ b/lib/src/track/remote/audio.dart @@ -89,8 +89,7 @@ class RemoteAudioTrack extends RemoteTrack with AudioTrack, RemoteAudioManagemen if (stats != null && prevStats != null && receiver != null) { final bitrate = computeBitrateForReceiverStats(stats, prevStats); _currentBitrate = bitrate; - events.emit( - AudioReceiverStatsEvent(stats: stats, currentBitrate: bitrate)); + events.emit(AudioReceiverStatsEvent(stats: stats, currentBitrate: bitrate)); } prevStats = stats; diff --git a/lib/src/track/remote/video.dart b/lib/src/track/remote/video.dart index 17d1bf84..fbb8f72d 100644 --- a/lib/src/track/remote/video.dart +++ b/lib/src/track/remote/video.dart @@ -55,8 +55,7 @@ class RemoteVideoTrack extends RemoteTrack with VideoTrack { if (stats != null && prevStats != null && receiver != null) { final bitrate = computeBitrateForReceiverStats(stats, prevStats); _currentBitrate = bitrate; - events.emit( - VideoReceiverStatsEvent(stats: stats, currentBitrate: bitrate)); + events.emit(VideoReceiverStatsEvent(stats: stats, currentBitrate: bitrate)); } prevStats = stats; diff --git a/lib/src/widgets/screen_select_dialog.dart b/lib/src/widgets/screen_select_dialog.dart index 1b36842a..01b742bb 100644 --- a/lib/src/widgets/screen_select_dialog.dart +++ b/lib/src/widgets/screen_select_dialog.dart @@ -138,8 +138,7 @@ class ScreenSelectDialog extends Dialog { Future _getSources() async { try { - final sources = - await rtc.desktopCapturer.getSources(types: [_sourceType]); + final sources = await rtc.desktopCapturer.getSources(types: [_sourceType]); for (var element in sources) { if (kDebugMode) { print('name: ${element.name}, id: ${element.id}, type: ${element.type}'); diff --git a/test/core/data_stream_test.dart b/test/core/data_stream_test.dart index a29e480f..0e760b86 100644 --- a/test/core/data_stream_test.dart +++ b/test/core/data_stream_test.dart @@ -42,11 +42,9 @@ void main() { group('Stream Handler Registration', () { test('Register And Unregister Text And Byte Stream Handlers', () async { - room.registerTextStreamHandler( - 'chat', (TextStreamReader reader, String participantIdentity) {}); + room.registerTextStreamHandler('chat', (TextStreamReader reader, String participantIdentity) {}); - room.registerByteStreamHandler( - 'file', (ByteStreamReader reader, String participantIdentity) {}); + room.registerByteStreamHandler('file', (ByteStreamReader reader, String participantIdentity) {}); expect(room.textStreamHandlers.keys.first, 'chat'); @@ -62,8 +60,7 @@ void main() { group('Text Streaming', () { test('Send Basic Text Message', () async { - room.registerTextStreamHandler('chat', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); print('received chat message from $participantIdentity: $text'); expect('some text !!!', text); @@ -78,11 +75,9 @@ void main() { test('Send Large Text Message With Progress Tracking', () async { final longText = 'a' * 100000; - room.registerTextStreamHandler('chat-long-text', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat-long-text', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); - print( - 'received chat message from $participantIdentity: long text length: ${text.length}'); + print('received chat message from $participantIdentity: long text length: ${text.length}'); expect(longText, text); }); @@ -99,8 +94,7 @@ void main() { }); test('Stream Text With Multiple Chunks', () async { - room.registerTextStreamHandler('chat-stream', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat-stream', (TextStreamReader reader, String participantIdentity) async { reader.listen((chunk) { print( 'received chunk: ${chunk.content.length}, total: ${reader.info?.size}, progress: ${utf8.decode(chunk.content)}'); @@ -130,16 +124,14 @@ void main() { for (var file in files) { final randomFile = File(file); final random = Random(); - final bytes = - List.generate(100000, (index) => random.nextInt(256)); + final bytes = List.generate(100000, (index) => random.nextInt(256)); randomFile.writeAsBytesSync(bytes); } room.registerTextStreamHandler('chat-stream-with-files', (TextStreamReader reader, String participantIdentity) async { final receivedText = await reader.readAll(); - print( - 'received chat message from $participantIdentity: long text length: ${receivedText.length}'); + print('received chat message from $participantIdentity: long text length: ${receivedText.length}'); expect(longText, receivedText); }); @@ -178,22 +170,19 @@ void main() { final receivedMessages = []; for (var operationType in operationTypes) { - room.registerTextStreamHandler('chat-operations', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat-operations', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); receivedMessages.add('${operationType}: ${text}'); print('received ${operationType} message: ${text}'); }); - final info = - await room.localParticipant?.sendText('Test ${operationType}', - options: SendTextOptions( - topic: 'chat-operations', - )); + final info = await room.localParticipant?.sendText('Test ${operationType}', + options: SendTextOptions( + topic: 'chat-operations', + )); // Test with streamText and different operation types - final stream = - await room.localParticipant?.streamText(StreamTextOptions( + final stream = await room.localParticipant?.streamText(StreamTextOptions( topic: 'chat-operations', type: operationType, version: operationType == TextStreamOperationType.update ? 2 : null, @@ -207,14 +196,9 @@ void main() { }); test('Text Stream With Attributes And Metadata', () async { - final testAttributes = { - 'userId': '12345', - 'sessionId': 'abc123', - 'priority': 'high' - }; + final testAttributes = {'userId': '12345', 'sessionId': 'abc123', 'priority': 'high'}; - room.registerTextStreamHandler('chat-metadata', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat-metadata', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); print('received message with text: ${text}'); print('received message attributes: ${reader.info?.attributes}'); @@ -228,12 +212,11 @@ void main() { expect(reader.info!.attributes['priority'], 'high'); }); - final info = - await room.localParticipant?.sendText('Test message with metadata', - options: SendTextOptions( - topic: 'chat-metadata', - attributes: testAttributes, - )); + final info = await room.localParticipant?.sendText('Test message with metadata', + options: SendTextOptions( + topic: 'chat-metadata', + attributes: testAttributes, + )); expect(info, isNotNull); }); @@ -241,8 +224,7 @@ void main() { const originalStreamId = 'original-stream-123'; const replyStreamId = 'reply-stream-456'; - room.registerTextStreamHandler('chat-replies', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat-replies', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); print('received reply message: ${text}'); expect(text, 'This is a reply to the original message'); @@ -266,8 +248,7 @@ void main() { }); test('Text Stream With AI Generated Flag', () async { - room.registerTextStreamHandler('chat-ai-generated', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('chat-ai-generated', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); print('received AI-generated message: ${text}'); expect(text, 'This message was generated by AI'); @@ -313,8 +294,7 @@ void main() { final bytes = List.generate(100000, (index) => random.nextInt(256)); randomFile.writeAsBytesSync(bytes); - room.registerByteStreamHandler('file', - (ByteStreamReader reader, String participantIdentity) async { + room.registerByteStreamHandler('file', (ByteStreamReader reader, String participantIdentity) async { final file = await reader.readAll(); final fileName = 'testfiles/copy-${reader.info!.name}'; print('received file from $participantIdentity: ${file.length}'); @@ -341,16 +321,13 @@ void main() { }); test('Stream Raw Bytes With UTF8 Content', () async { - room.registerByteStreamHandler('bytes-stream', - (ByteStreamReader reader, String participantIdentity) async { + room.registerByteStreamHandler('bytes-stream', (ByteStreamReader reader, String participantIdentity) async { final chunks = await reader.readAll(); final content = chunks.expand((element) => element).toList(); - print( - 'bytes content = ${content}, \n string content = ${utf8.decode(content)}'); + print('bytes content = ${content}, \n string content = ${utf8.decode(content)}'); }); - final stream = - await room.localParticipant?.streamBytes(StreamBytesOptions( + final stream = await room.localParticipant?.streamBytes(StreamBytesOptions( topic: 'bytes-stream', totalSize: 30, )); @@ -389,8 +366,7 @@ void main() { expect(content, expectedContent); }); - final stream = - await room.localParticipant?.streamBytes(StreamBytesOptions( + final stream = await room.localParticipant?.streamBytes(StreamBytesOptions( topic: 'files-with-metadata', name: testFileName, mimeType: testMimeType, @@ -410,8 +386,7 @@ void main() { var receivedCount = 0; const expectedCount = 3; - room.registerTextStreamHandler('concurrent-streams', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('concurrent-streams', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); receivedCount++; print('received concurrent message ${receivedCount}: ${text}'); @@ -421,8 +396,7 @@ void main() { final futures = []; for (int i = 0; i < expectedCount; i++) { futures.add(() async { - final stream = - await room.localParticipant?.streamText(StreamTextOptions( + final stream = await room.localParticipant?.streamText(StreamTextOptions( topic: 'concurrent-streams', streamId: 'stream-${i}', type: TextStreamOperationType.create, @@ -443,8 +417,7 @@ void main() { const chunkSize = 50000; // Larger than normal chunk size final largeData = 'x' * chunkSize; - room.registerTextStreamHandler('large-chunks', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('large-chunks', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); print('received large text, length: ${text.length}'); expect(text.length, chunkSize); @@ -465,8 +438,7 @@ void main() { // Test comprehensive header data transmission final testCompleter = Completer(); - room.registerTextStreamHandler('header-validation', - (TextStreamReader reader, String participantIdentity) async { + room.registerTextStreamHandler('header-validation', (TextStreamReader reader, String participantIdentity) async { final text = await reader.readAll(); print('=== Header Validation Test ==='); print('Received text: ${text}'); diff --git a/web/e2ee.data_packet_cryptor.dart b/web/e2ee.data_packet_cryptor.dart index 939f688a..fc9796a3 100644 --- a/web/e2ee.data_packet_cryptor.dart +++ b/web/e2ee.data_packet_cryptor.dart @@ -59,8 +59,7 @@ class E2EEDataPacketCryptor { } final sendCount = sendCount_; - final randomBytes = - Random.secure().nextInt(max(0, 0xffffffff)).toUnsigned(32); + final randomBytes = Random.secure().nextInt(max(0, 0xffffffff)).toUnsigned(32); iv.setUint32(0, randomBytes); iv.setUint32(4, timestamp); @@ -85,8 +84,7 @@ class E2EEDataPacketCryptor { final keyIndex = currentKeyIndex; if (secretKey == null) { - logger.warning( - 'encodeFunction: no secretKey for index $keyIndex, cannot encrypt'); + logger.warning('encodeFunction: no secretKey for index $keyIndex, cannot encrypt'); return null; } @@ -128,8 +126,7 @@ class E2EEDataPacketCryptor { ) async { var ratchetCount = 0; - logger.fine( - 'decodeFunction: data packet lenght ${encryptedPacket.data.length}'); + logger.fine('decodeFunction: data packet lenght ${encryptedPacket.data.length}'); ByteBuffer? decrypted; KeySet? initialKeySet; @@ -168,34 +165,26 @@ class E2EEDataPacketCryptor { ) .toDart) as JSArrayBuffer) .toDart; - logger.finer( - 'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}'); + logger.finer('decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}'); if (decrypted == null) { throw Exception('[decryptFrameInternal] could not decrypt'); } - logger.finer( - 'decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}'); + logger.finer('decodeFunction::decryptFrameInternal: decrypted: ${decrypted!.asUint8List().length}'); if (currentkeySet != initialKeySet) { - logger.fine( - 'decodeFunction::decryptFrameInternal: ratchetKey: decryption ok, newState: kKeyRatcheted'); - await keyHandler.setKeySetFromMaterial( - currentkeySet, initialKeyIndex); + logger.fine('decodeFunction::decryptFrameInternal: ratchetKey: decryption ok, newState: kKeyRatcheted'); + await keyHandler.setKeySetFromMaterial(currentkeySet, initialKeyIndex); } } Future ratchedKeyInternal() async { - if (ratchetCount >= keyOptions.ratchetWindowSize || - keyOptions.ratchetWindowSize <= 0) { + if (ratchetCount >= keyOptions.ratchetWindowSize || keyOptions.ratchetWindowSize <= 0) { throw Exception('[ratchedKeyInternal] cannot ratchet anymore'); } - final newKeyBuffer = await keyHandler.ratchet( - currentkeySet.material, keyOptions.ratchetSalt); - final newMaterial = await keyHandler.ratchetMaterial( - currentkeySet.material, newKeyBuffer.buffer); - currentkeySet = - await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt); + final newKeyBuffer = await keyHandler.ratchet(currentkeySet.material, keyOptions.ratchetSalt); + final newMaterial = await keyHandler.ratchetMaterial(currentkeySet.material, newKeyBuffer.buffer); + currentkeySet = await keyHandler.deriveKeys(newMaterial, keyOptions.ratchetSalt); ratchetCount++; await decryptFrameInternal(); } @@ -211,8 +200,7 @@ class E2EEDataPacketCryptor { } if (decrypted == null) { - throw Exception( - '[decodeFunction] decryption failed even after ratchting'); + throw Exception('[decodeFunction] decryption failed even after ratchting'); } // we can now be sure that decryption was a success diff --git a/web/e2ee.keyhandler.dart b/web/e2ee.keyhandler.dart index c49539a6..9e2f1003 100644 --- a/web/e2ee.keyhandler.dart +++ b/web/e2ee.keyhandler.dart @@ -208,8 +208,7 @@ class ParticipantKeyHandler { /// Derives a set of keys from the master key. /// See https://tools.ietf.org/html/draft-omara-sframe-00#section-4.3.1 Future deriveKeys(web.CryptoKey material, Uint8List salt) async { - final algorithmName = - material.algorithm.getProperty('name'.toJS) as JSString; + final algorithmName = material.algorithm.getProperty('name'.toJS) as JSString; final algorithmOptions = getAlgoOptions(algorithmName.toDart, salt); // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey#HKDF // https://developer.mozilla.org/en-US/docs/Web/API/HkdfParams diff --git a/web/e2ee.utils.dart b/web/e2ee.utils.dart index 996fba12..ed17016a 100644 --- a/web/e2ee.utils.dart +++ b/web/e2ee.utils.dart @@ -70,10 +70,7 @@ Uint8List parseRbsp(Uint8List stream) { // i + 3 can overflow, but byte_length_ - i can't, because i < byte_length_ // above, and that expression will produce the number of bytes left in // the stream including the byte at i. - if (length - i >= 3 && - stream[i] == 0 && - stream[i + 1] == 0 && - stream[i + 2] == 3) { + if (length - i >= 3 && stream[i] == 0 && stream[i + 1] == 0 && stream[i + 2] == 3) { // Two rbsp bytes. dataOut.add(stream[i++]); dataOut.add(stream[i++]); @@ -95,8 +92,7 @@ Uint8List writeRbsp(Uint8List dataIn) { var numConsecutiveZeros = 0; for (var i = 0; i < dataIn.length; ++i) { var byte = dataIn[i]; - if (byte <= kEmulationByte && - numConsecutiveZeros >= kZerosInStartSequence) { + if (byte <= kEmulationByte && numConsecutiveZeros >= kZerosInStartSequence) { // Need to escape. dataOut.add(kEmulationByte); numConsecutiveZeros = 0; From 723f4ac5dfdd12b9ac40f4fdc304d9dc6836f20f Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Fri, 24 Oct 2025 03:45:16 +0700 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9edb9747..a703c25c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ ## 2.5.3 -feat: Data Packet Cryptor (#873) -feat: Add E2EE support for H265 (#864) -fix: fix async call for update participant info (#897) +* Feat: Data Packet Cryptor (#873) +* Feat: Add E2EE support for H265 (#864) +* Fix: fix async call for update participant info (#897) +* Move the accessToken to header (#891) +* Bumpup and clamp connection timeout, fix the comment (#893) +* Fix events emit order (#902) ## 2.5.2