Skip to content

Data streams #593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 100 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
9bbd075
Move `MockDataChannelPair` to seperate file
ladvoc Feb 13, 2025
0941ed2
Create test suite
ladvoc Feb 13, 2025
1e4717b
Update proto to 1.33.0
ladvoc Feb 14, 2025
9326959
Merge remote-tracking branch 'upstream/main' into proto-update
ladvoc Feb 14, 2025
590787a
Merge branch 'proto-update' into data-streams
ladvoc Feb 14, 2025
4efb4f0
Define steam info classes
ladvoc Feb 18, 2025
6e76d98
Define stream error
ladvoc Feb 18, 2025
2f0fd70
Define stream readers
ladvoc Feb 18, 2025
9d551c5
Define stream manager
ladvoc Feb 18, 2025
1b66c71
Integrate stream manager
ladvoc Feb 18, 2025
5c13aa1
Publicly expose handler registration methods
ladvoc Feb 18, 2025
5044030
Update protocol definition
ladvoc Feb 18, 2025
ecf4b49
Add default implementations
ladvoc Feb 18, 2025
4d350fe
Add error cases and documentation
ladvoc Feb 18, 2025
e2b6136
Remove skeleton methods
ladvoc Feb 18, 2025
18c1270
Implement read to file for byte stream
ladvoc Feb 18, 2025
21396b1
Test stream readers
ladvoc Feb 18, 2025
ea81b31
Merge remote-tracking branch 'upstream/main' into data-streams
ladvoc Feb 18, 2025
ddec23e
Rename property
ladvoc Feb 18, 2025
f277d5b
Keep track of bytes read
ladvoc Feb 18, 2025
08f90de
Terminate open streams on deinit
ladvoc Feb 18, 2025
7ef4dae
Terminate stream if manager is no longer available
ladvoc Feb 18, 2025
35e8342
Use typealias
ladvoc Feb 18, 2025
07b97fc
Implement default file name
ladvoc Feb 19, 2025
e4463f9
Refactor file name resolution
ladvoc Feb 19, 2025
cb765e1
Rename `StreamManager` to `IncomingStreamManager`
ladvoc Feb 19, 2025
9cb5ce4
Protocol type conversion
ladvoc Feb 19, 2025
219612c
Add iOS to version check
ladvoc Feb 20, 2025
26fe835
Move test case
ladvoc Feb 20, 2025
275b314
Update tests
ladvoc Feb 20, 2025
a332eb3
Rename type
ladvoc Feb 20, 2025
5a06e24
Refactor tests
ladvoc Feb 20, 2025
72422a4
Test incoming stream manager
ladvoc Feb 20, 2025
860eb85
Clean up
ladvoc Feb 20, 2025
58b6729
Refactor stream reader
ladvoc Feb 20, 2025
5bfdce3
Rename error case
ladvoc Feb 20, 2025
d8cd81f
Organize
ladvoc Feb 21, 2025
8cdc9b2
Create skeleton methods
ladvoc Feb 21, 2025
47b6c31
Merge remote-tracking branch 'upstream/main' into data-streams
ladvoc Feb 22, 2025
aae1b8f
Merge remote-tracking branch 'upstream/main' into data-streams
ladvoc Feb 24, 2025
3d6b9ca
Fix import issue on iOS
ladvoc Feb 24, 2025
9a9ab3d
Handle preferred extension special case
ladvoc Feb 24, 2025
500562d
Organize
ladvoc Feb 24, 2025
89c198b
Implement buffering on `DataChannelPair`
ladvoc Feb 26, 2025
136624c
Merge remote-tracking branch 'upstream/main' into data-streams
ladvoc Feb 26, 2025
292afd7
Define stream options
ladvoc Feb 26, 2025
741aa5d
Add additional error case
ladvoc Feb 26, 2025
b1016b1
Make send method async
ladvoc Feb 26, 2025
d205dff
Create stream writers
ladvoc Feb 26, 2025
1d3480f
Refactor protocol type conversion
ladvoc Feb 26, 2025
84ea7f7
Rename property
ladvoc Feb 26, 2025
4d463b8
Create `OutgoingStreamManager`
ladvoc Feb 26, 2025
ee0f02d
Implement public API
ladvoc Feb 26, 2025
8032835
Rename method
ladvoc Feb 26, 2025
68e2f49
Mark method async
ladvoc Feb 26, 2025
e61c438
Merge remote-tracking branch 'upstream/main' into data-streams
ladvoc Feb 26, 2025
318af6c
Make entire extension public
ladvoc Feb 26, 2025
31ce078
Rename tests
ladvoc Feb 26, 2025
7fc0f7c
Add external parameter name
ladvoc Feb 26, 2025
00d835b
Internal renaming and additional logging
ladvoc Feb 26, 2025
4a1aad6
Remove unused file
ladvoc Feb 26, 2025
068eb35
Add end-to-end tests
ladvoc Feb 26, 2025
7d14542
Add documentation
ladvoc Feb 26, 2025
509a586
Automatically chunk outgoing data
ladvoc Feb 26, 2025
61f2a31
Add support for reading file info
ladvoc Feb 26, 2025
9a464b0
Add support for sending files
ladvoc Feb 26, 2025
530249e
Add end-to-end test case for stream bytes
ladvoc Feb 26, 2025
e9d18dd
Make initializer non-throwing
ladvoc Feb 26, 2025
d4f8992
Improve end-to-end tests
ladvoc Feb 26, 2025
b27c067
Refactor stream reader API
ladvoc Feb 27, 2025
1d96abf
Remove unused test suite
ladvoc Feb 27, 2025
999d525
Organize sources in subdirectories
ladvoc Feb 27, 2025
9c926b0
Remove unused test suite
ladvoc Feb 27, 2025
bfd3b71
Add remaining Objective-C method
ladvoc Feb 27, 2025
69a6513
Rename parameter
ladvoc Feb 27, 2025
8d3f601
Mark extensions public
ladvoc Feb 27, 2025
0e2468d
Refactor outgoing stream handling
ladvoc Feb 27, 2025
d294977
Simplify file name resolution
ladvoc Feb 27, 2025
e36de84
Improve documentation
ladvoc Feb 27, 2025
789ff66
Set outgoing destination identities
ladvoc Feb 27, 2025
63e9734
Refactor stream info, add documentation
ladvoc Feb 27, 2025
9fad0a7
Apply SwiftFormat
ladvoc Feb 27, 2025
5f7d799
Handle protocol type conversion with stream managers
ladvoc Feb 27, 2025
0154792
Use deque for publish data request queue
ladvoc Feb 27, 2025
77733c7
Fix compilation issue with older Swift versions
ladvoc Feb 28, 2025
5b233aa
Use compiler synthesized Objective-C methods
ladvoc Feb 28, 2025
641d50b
Fix test issue
ladvoc Feb 28, 2025
08c474f
Open streams immediately
ladvoc Mar 4, 2025
fdc20c1
Apply SwiftFormat
ladvoc Mar 4, 2025
73b5faa
Add CocoaPods dependency
ladvoc Mar 4, 2025
ee47420
Merge remote-tracking branch 'origin/main' into data-streams
ladvoc Mar 4, 2025
70c8baa
Rename method
ladvoc Mar 5, 2025
63273e7
Apply SwiftFormat
ladvoc Mar 6, 2025
6ad9670
Create `AsyncFileStream`
ladvoc Mar 6, 2025
d1f9361
Integrate `AsyncFileStream`
ladvoc Mar 6, 2025
0a79504
Fix publisher data channel initially not open
ladvoc Mar 7, 2025
facb355
Apply SwiftFormat
ladvoc Mar 7, 2025
06e2f5b
Fix compilation for older versions of Swift
ladvoc Mar 7, 2025
9278691
Apply SwiftFormat
ladvoc Mar 7, 2025
e5be45f
Use regular task
ladvoc Mar 7, 2025
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 LiveKitClient.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Pod::Spec.new do |spec|
spec.dependency("LiveKitWebRTC", "= 125.6422.19")
spec.dependency("SwiftProtobuf")
spec.dependency("Logging")
spec.dependency("DequeModule", "= 1.1.4")

spec.resource_bundles = {"Privacy" => ["Sources/LiveKit/PrivacyInfo.xcprivacy"]}

Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ let package = Package(
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.19"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
// Only used for DocC generation
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"),
// Only used for Testing
Expand All @@ -36,6 +37,7 @@ let package = Package(
dependencies: [
.product(name: "LiveKitWebRTC", package: "webrtc-xcframework"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "DequeModule", package: "swift-collections"),
.product(name: "Logging", package: "swift-log"),
"LKObjCHelpers",
],
Expand Down
2 changes: 2 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let package = Package(
.package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "125.6422.19"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.26.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.0"),
// Only used for DocC generation
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"),
// Only used for Testing
Expand All @@ -38,6 +39,7 @@ let package = Package(
dependencies: [
.product(name: "LiveKitWebRTC", package: "webrtc-xcframework"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "DequeModule", package: "swift-collections"),
.product(name: "Logging", package: "swift-log"),
"LKObjCHelpers",
],
Expand Down
177 changes: 166 additions & 11 deletions Sources/LiveKit/Core/DataChannelPair.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import DequeModule
import Foundation

#if swift(>=5.9)
Expand Down Expand Up @@ -51,6 +52,115 @@

private let _state: StateSync<State>

fileprivate enum ChannelKind {
case lossy, reliable
}

private struct BufferingState {
var queue: Deque<PublishDataRequest> = []
var amount: UInt64 = 0
}

private struct PublishDataRequest {
let data: LKRTCDataBuffer
let continuation: CheckedContinuation<Void, any Error>?
}

private struct ChannelEvent {
let channelKind: ChannelKind
let detail: Detail

enum Detail {
case publishData(PublishDataRequest)
case bufferedAmountChanged(UInt64)
}
}

private var eventContinuation: AsyncStream<ChannelEvent>.Continuation?

@Sendable private func handleEvents(

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, macOS,variant=Mac Catalyst)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, macOS)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, macOS)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, tvOS Simulator,name=Apple TV)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, tvOS Simulator,name=Apple TV)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, iOS Simulator,OS=17.5,name=iPhone 15 Pro)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, visionOS Simulator,name=Apple Vision Pro)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6

Check warning on line 81 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, visionOS Simulator,name=Apple Vision Pro)

instance methods of non-Sendable types cannot be marked as '@sendable'; this is an error in Swift 6
events: AsyncStream<ChannelEvent>
) async {
var lossyBuffering = BufferingState()
var reliableBuffering = BufferingState()

for await event in events {
switch event.detail {
case let .publishData(request):
switch event.channelKind {
case .lossy: lossyBuffering.queue.append(request)
case .reliable: reliableBuffering.queue.append(request)
}
case let .bufferedAmountChanged(amount):
switch event.channelKind {
case .lossy: updateBufferingState(state: &lossyBuffering, newAmount: amount)
case .reliable: updateBufferingState(state: &reliableBuffering, newAmount: amount)
}
}

switch event.channelKind {
case .lossy:
processSendQueue(
threshold: Self.lossyLowThreshold,
state: &lossyBuffering,
kind: .lossy
)
case .reliable:
processSendQueue(
threshold: Self.reliableLowThreshold,
state: &reliableBuffering,
kind: .reliable
)
}
}
}

private func channel(for kind: ChannelKind) -> LKRTCDataChannel? {
_state.read {
guard let lossy = $0.lossy, let reliable = $0.reliable, $0.isOpen else { return nil }
return kind == .reliable ? reliable : lossy
}
}

private func processSendQueue(
threshold: UInt64,
state: inout BufferingState,
kind: ChannelKind
) {
while state.amount <= threshold {
guard !state.queue.isEmpty else { break }
let request = state.queue.removeFirst()

state.amount += UInt64(request.data.data.count)

guard let channel = channel(for: kind) else {
request.continuation?.resume(
throwing: LiveKitError(.invalidState, message: "Data channel is not open")
)
return
}
guard channel.sendData(request.data) else {
request.continuation?.resume(
throwing: LiveKitError(.invalidState, message: "sendData failed")
)
return
}
request.continuation?.resume()
}
}

private func updateBufferingState(
state: inout BufferingState,
newAmount: UInt64
) {
guard state.amount >= newAmount else {
log("Unexpected buffer size detected", .error)
state.amount = 0
return
}
state.amount -= newAmount
}

public init(delegate: DataChannelDelegate? = nil,
lossyChannel: LKRTCDataChannel? = nil,
reliableChannel: LKRTCDataChannel? = nil)
Expand All @@ -61,6 +171,14 @@
if let delegate {
delegates.add(delegate: delegate)
}
super.init()

Task {
let eventStream = AsyncStream<ChannelEvent> { continuation in
self.eventContinuation = continuation
}
await handleEvents(events: eventStream)
}
}

public func set(reliable channel: LKRTCDataChannel?) {
Expand Down Expand Up @@ -103,24 +221,27 @@
openCompleter.reset()
}

public func send(userPacket: Livekit_UserPacket, kind: Livekit_DataPacket.Kind) throws {
try send(dataPacket: .with {
$0.kind = kind
public func send(userPacket: Livekit_UserPacket, kind: Livekit_DataPacket.Kind) async throws {
try await send(dataPacket: .with {
$0.kind = kind // TODO: field is deprecated
$0.user = userPacket
})
}

public func send(dataPacket packet: Livekit_DataPacket) throws {
guard isOpen else {
throw LiveKitError(.invalidState, message: "Data channel is not open")
}

public func send(dataPacket packet: Livekit_DataPacket) async throws {
let serializedData = try packet.serializedData()
let rtcData = RTC.createDataBuffer(data: serializedData)

let channel = _state.read { packet.kind == .reliable ? $0.reliable : $0.lossy }
guard let sendDataResult = channel?.sendData(rtcData), sendDataResult else {
throw LiveKitError(.invalidState, message: "sendData failed")
try await withCheckedThrowingContinuation { continuation in
let request = PublishDataRequest(
data: rtcData,
continuation: continuation
)
let event = ChannelEvent(
channelKind: ChannelKind(packet.kind), // TODO: field is deprecated
detail: .publishData(request)
)
eventContinuation?.yield(event)
}
}

Expand All @@ -129,11 +250,26 @@
.compactMap { $0 }
.map { $0.toLKInfoType() }
}

private static let reliableLowThreshold: UInt64 = 2 * 1024 * 1024 // 2 MB
private static let lossyLowThreshold: UInt64 = reliableLowThreshold

deinit {
eventContinuation?.finish()
}
}

// MARK: - RTCDataChannelDelegate

extension DataChannelPair: LKRTCDataChannelDelegate {
func dataChannel(_ dataChannel: LKRTCDataChannel, didChangeBufferedAmount amount: UInt64) {
let event = ChannelEvent(
channelKind: dataChannel.kind,
detail: .bufferedAmountChanged(amount)
)
eventContinuation?.yield(event)
}

func dataChannelDidChangeState(_: LKRTCDataChannel) {
if isOpen {
openCompleter.resume(returning: ())
Expand All @@ -141,7 +277,7 @@
}

func dataChannel(_: LKRTCDataChannel, didReceiveMessageWith buffer: LKRTCDataBuffer) {
guard let dataPacket = try? Livekit_DataPacket(serializedData: buffer.data) else {

Check warning on line 280 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, macOS)

'init(serializedData:extensions:partial:options:)' is deprecated: replaced by 'init(serializedBytes:extensions:partial:options:)'

Check warning on line 280 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-15, 16.2, macOS)

'init(serializedData:extensions:partial:options:)' is deprecated: replaced by 'init(serializedBytes:extensions:partial:options:)'

Check warning on line 280 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-15, 16.2, macOS)

'init(serializedData:extensions:partial:options:)' is deprecated: replaced by 'init(serializedBytes:extensions:partial:options:)'

Check warning on line 280 in Sources/LiveKit/Core/DataChannelPair.swift

View workflow job for this annotation

GitHub Actions / test (macos-14, 15.4, visionOS Simulator,name=Apple Vision Pro)

'init(serializedData:extensions:partial:options:)' is deprecated: replaced by 'init(serializedBytes:extensions:partial:options:)'
log("Could not decode data message", .error)
return
}
Expand All @@ -151,3 +287,22 @@
}
}
}

private extension DataChannelPair.ChannelKind {
init(_ packetKind: Livekit_DataPacket.Kind) {
guard case .lossy = packetKind else {
self = .reliable
return
}
self = .lossy
}
}

private extension LKRTCDataChannel {
var kind: DataChannelPair.ChannelKind {
guard label == LKRTCDataChannel.labels.lossy else {
return .reliable
}
return .lossy
}
}
2 changes: 1 addition & 1 deletion Sources/LiveKit/Core/RPC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ actor RpcStateManager: Loggable {
log("No handler registered for RPC method '\(method)'", .warning)
}
}

func isRpcMethodRegistered(_ method: String) -> Bool {
handlers[method] != nil
}
Expand Down
89 changes: 89 additions & 0 deletions Sources/LiveKit/Core/Room+DataStream.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2025 LiveKit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import Foundation

public extension Room {
/// Registers a handler for incoming byte streams matching the given topic.
///
/// - Parameters:
/// - topic: Topic identifier that filters which streams will be handled.
/// Only streams with a matching topic will trigger the handler.
/// - onNewStream: Handler that is invoked whenever a remote participant
/// opens a new stream with the matching topic. The handler receives a
/// ``ByteStreamReader`` for consuming the stream data and the identity of
/// the remote participant who initiated the stream.
///
func registerByteStreamHandler(for topic: String, onNewStream: @escaping ByteStreamHandler) async throws {
try await incomingStreamManager.registerByteStreamHandler(for: topic, onNewStream)
}

/// Registers a handler for incoming text streams matching the given topic.
///
/// - Parameters:
/// - topic: Topic identifier that filters which streams will be handled.
/// Only streams with a matching topic will trigger the handler.
/// - onNewStream: Handler that is invoked whenever a remote participant
/// opens a new stream with the matching topic. The handler receives a
/// ``TextStreamReader`` for consuming the stream data and the identity of
/// the remote participant who initiated the stream.
///
func registerTextStreamHandler(for topic: String, onNewStream: @escaping TextStreamHandler) async throws {
try await incomingStreamManager.registerTextStreamHandler(for: topic, onNewStream)
}

/// Unregisters a byte stream handler that was previously registered for the given topic.
@objc
func unregisterByteStreamHandler(for topic: String) async {
await incomingStreamManager.unregisterByteStreamHandler(for: topic)
}

/// Unregisters a text stream handler that was previously registered for the given topic.
@objc
func unregisterTextStreamHandler(for topic: String) async {
await incomingStreamManager.unregisterTextStreamHandler(for: topic)
}
}

// MARK: - Objective-C Compatibility

public extension Room {
@objc
@available(*, deprecated, message: "Use async registerByteStreamHandler(for:onNewStream:) method instead.")
func registerByteStreamHandler(
for topic: String,
onNewStream: @escaping (ByteStreamReader, Participant.Identity) -> Void,
onError: ((Error) -> Void)?
) {
Task {
do { try await registerByteStreamHandler(for: topic, onNewStream: onNewStream) }
catch { onError?(error) }
}
}

@objc
@available(*, deprecated, message: "Use async registerTextStreamHandler(for:onNewStream:) method instead.")
func registerTextStreamHandler(
for topic: String,
onNewStream: @escaping (TextStreamReader, Participant.Identity) -> Void,
onError: ((Error) -> Void)?
) {
Task {
do { try await registerTextStreamHandler(for: topic, onNewStream: onNewStream) }
catch { onError?(error) }
}
}
}
3 changes: 1 addition & 2 deletions Sources/LiveKit/Core/Room+Engine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ extension Room {
packet.participantIdentity = identity
}

// Should return true if successful
try publisherDataChannel.send(dataPacket: packet)
try await publisherDataChannel.send(dataPacket: packet)
}
}

Expand Down
Loading
Loading