Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ extension Livekit_DataStream.Header {
$0.totalLength = UInt64(totalLength)
}
$0.attributes = streamInfo.attributes
$0.encryptionType = streamInfo.encryptionType.toPBType()
$0.contentHeader = Livekit_DataStream.Header.OneOf_ContentHeader(streamInfo)
}
}
Expand Down
24 changes: 21 additions & 3 deletions Sources/LiveKit/Types/ParticipantPermissions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,31 @@ public class ParticipantPermissions: NSObject, @unchecked Sendable {
@objc
public let recorder: Bool

/// Indicates participant can update own metadata and attributes
@objc
public let canUpdateMetadata: Bool

/// Indicates participant can subscribe to metrics
@objc
public let canSubscribeMetrics: Bool

init(canSubscribe: Bool = false,
canPublish: Bool = false,
canPublishData: Bool = false,
canPublishSources: Set<Track.Source> = [],
hidden: Bool = false,
recorder: Bool = false)
recorder: Bool = false,
canUpdateMetadata: Bool = false,
canSubscribeMetrics: Bool = false)
{
self.canSubscribe = canSubscribe
self.canPublish = canPublish
self.canPublishData = canPublishData
self.canPublishSources = Set(canPublishSources.map(\.rawValue))
self.hidden = hidden
self.recorder = recorder
self.canUpdateMetadata = canUpdateMetadata
self.canSubscribeMetrics = canSubscribeMetrics
}

// MARK: - Equal
Expand All @@ -66,7 +78,9 @@ public class ParticipantPermissions: NSObject, @unchecked Sendable {
canPublishData == other.canPublishData &&
canPublishSources == other.canPublishSources &&
hidden == other.hidden &&
recorder == other.recorder
recorder == other.recorder &&
canUpdateMetadata == other.canUpdateMetadata &&
canSubscribeMetrics == other.canSubscribeMetrics
}

override public var hash: Int {
Expand All @@ -77,6 +91,8 @@ public class ParticipantPermissions: NSObject, @unchecked Sendable {
hasher.combine(canPublishSources)
hasher.combine(hidden)
hasher.combine(recorder)
hasher.combine(canUpdateMetadata)
hasher.combine(canSubscribeMetrics)
return hasher.finalize()
}
}
Expand All @@ -88,6 +104,8 @@ extension Livekit_ParticipantPermission {
canPublishData: canPublishData,
canPublishSources: Set(canPublishSources.map { $0.toLKType() }),
hidden: hidden,
recorder: recorder)
recorder: recorder,
canUpdateMetadata: canUpdateMetadata,
canSubscribeMetrics: canSubscribeMetrics)
}
}
6 changes: 4 additions & 2 deletions Tests/LiveKitCoreTests/DataStream/ByteStreamInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ByteStreamInfoTests: LKTestCase {
timestamp: Date(timeIntervalSince1970: 100),
totalLength: 128,
attributes: ["key": "value"],
encryptionType: .none,
encryptionType: .gcm,
mimeType: "image/jpeg",
name: "filename.bin"
)
Expand All @@ -36,15 +36,17 @@ class ByteStreamInfoTests: LKTestCase {
XCTAssertEqual(header.timestamp, Int64(info.timestamp.timeIntervalSince1970 * TimeInterval(1000)))
XCTAssertEqual(header.totalLength, UInt64(info.totalLength ?? -1))
XCTAssertEqual(header.attributes, info.attributes)
XCTAssertEqual(header.encryptionType.rawValue, info.encryptionType.rawValue)
XCTAssertEqual(header.byteHeader.name, info.name)

let newInfo = ByteStreamInfo(header, header.byteHeader, .none)
let newInfo = ByteStreamInfo(header, header.byteHeader, .gcm)
XCTAssertEqual(newInfo.id, info.id)
XCTAssertEqual(newInfo.mimeType, info.mimeType)
XCTAssertEqual(newInfo.topic, info.topic)
XCTAssertEqual(newInfo.timestamp, info.timestamp)
XCTAssertEqual(newInfo.totalLength, info.totalLength)
XCTAssertEqual(newInfo.attributes, info.attributes)
XCTAssertEqual(newInfo.encryptionType, info.encryptionType)
XCTAssertEqual(newInfo.name, info.name)
}
}
6 changes: 4 additions & 2 deletions Tests/LiveKitCoreTests/DataStream/TextStreamInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class TextStreamInfoTests: LKTestCase {
timestamp: Date(timeIntervalSince1970: 100),
totalLength: 128,
attributes: ["key": "value"],
encryptionType: .none,
encryptionType: .gcm,
operationType: .reaction,
version: 10,
replyToStreamID: "replyID",
Expand All @@ -38,18 +38,20 @@ class TextStreamInfoTests: LKTestCase {
XCTAssertEqual(header.timestamp, Int64(info.timestamp.timeIntervalSince1970 * TimeInterval(1000)))
XCTAssertEqual(header.totalLength, UInt64(info.totalLength ?? -1))
XCTAssertEqual(header.attributes, info.attributes)
XCTAssertEqual(header.encryptionType.rawValue, info.encryptionType.rawValue)
XCTAssertEqual(header.textHeader.operationType.rawValue, info.operationType.rawValue)
XCTAssertEqual(header.textHeader.version, Int32(info.version))
XCTAssertEqual(header.textHeader.replyToStreamID, info.replyToStreamID)
XCTAssertEqual(header.textHeader.attachedStreamIds, info.attachedStreamIDs)
XCTAssertEqual(header.textHeader.generated, info.generated)

let newInfo = TextStreamInfo(header, header.textHeader, .none)
let newInfo = TextStreamInfo(header, header.textHeader, .gcm)
XCTAssertEqual(newInfo.id, info.id)
XCTAssertEqual(newInfo.topic, info.topic)
XCTAssertEqual(newInfo.timestamp, info.timestamp)
XCTAssertEqual(newInfo.totalLength, info.totalLength)
XCTAssertEqual(newInfo.attributes, info.attributes)
XCTAssertEqual(newInfo.encryptionType, info.encryptionType)
XCTAssertEqual(newInfo.operationType, info.operationType)
XCTAssertEqual(newInfo.version, info.version)
XCTAssertEqual(newInfo.replyToStreamID, info.replyToStreamID)
Expand Down
151 changes: 151 additions & 0 deletions Tests/LiveKitCoreTests/Proto/ProtoConverterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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.
*/

@testable import LiveKit
#if canImport(LiveKitTestSupport)
import LiveKitTestSupport
#endif

class ProtoConverterTests: LKTestCase {
func testParticipantPermissions() {
let errors = Comparator.compareStructures(
proto: Livekit_ParticipantPermission(),
custom: ParticipantPermissions(),
excludedFields: ["agent"], // deprecated
allowedTypeMismatches: ["canPublishSources"] // Array vs Set
)

XCTAssert(errors.isEmpty, errors.description)
}
}

enum Comparator {
enum ComparisonError: Error, CustomStringConvertible {
case missingField(String)
case extraField(String)
case typeMismatch(field: String, proto: String, custom: String)

var description: String {
switch self {
case let .missingField(field):
"Missing field: '\(field)'"
case let .extraField(field):
"Extra field: '\(field)'"
case let .typeMismatch(field, proto, custom):
"Type mismatch for '\(field)': proto has \(proto), custom has \(custom)"
}
}
}

struct FieldInfo {
let name: String
let type: String
let nonOptionalType: String
}

static func extractFields(from instance: some Any, excludedFields: Set<String> = []) -> [FieldInfo] {
let mirror = Mirror(reflecting: instance)
var fields: [FieldInfo] = []
var backingFields: Set<String> = []

// Collect all backing fields
for child in mirror.children {
guard let label = child.label, label.hasPrefix("_") else { continue }
backingFields.insert(String(label.dropFirst())) // Remove the underscore
}

for child in mirror.children {
guard let label = child.label else { continue }

// Skip excluded/unknown fields
if excludedFields.contains(label) || label == "unknownFields" {
continue
}

// Skip private backing fields (they have public computed properties)
if label.hasPrefix("_"), backingFields.contains(String(label.dropFirst())) {
// But add the public version instead
let publicName = String(label.dropFirst())
let typeString = String(describing: type(of: child.value))
let nonOptional = extractNonOptionalType(from: typeString)

if !fields.contains(where: { $0.name == publicName }) {
fields.append(FieldInfo(name: publicName, type: typeString, nonOptionalType: nonOptional))
}
continue
}

// Skip other private fields
if label.hasPrefix("_") {
continue
}

let typeString = String(describing: type(of: child.value))
let nonOptional = extractNonOptionalType(from: typeString)

fields.append(FieldInfo(name: label, type: typeString, nonOptionalType: nonOptional))
}

return fields.sorted { $0.name < $1.name }
}

static func extractNonOptionalType(from typeString: String) -> String {
if typeString.hasPrefix("Optional<"), typeString.hasSuffix(">") {
let start = typeString.index(typeString.startIndex, offsetBy: 9)
let end = typeString.index(before: typeString.endIndex)
return String(typeString[start ..< end])
}
return typeString
}

static func compareStructures(
proto: some Any,
custom: some Any,
excludedFields: Set<String> = [],
allowedTypeMismatches: Set<String> = []
) -> [ComparisonError] {
let protoFields = extractFields(from: proto, excludedFields: excludedFields)
let customFields = extractFields(from: custom, excludedFields: excludedFields)

var errors: [ComparisonError] = []

let protoFieldMap = Dictionary(uniqueKeysWithValues: protoFields.map { ($0.name, $0) })
let customFieldMap = Dictionary(uniqueKeysWithValues: customFields.map { ($0.name, $0) })

for protoField in protoFields {
guard let customField = customFieldMap[protoField.name] else {
errors.append(.missingField(protoField.name))
continue
}

if protoField.nonOptionalType != customField.nonOptionalType, !allowedTypeMismatches.contains(protoField.name) {
errors.append(.typeMismatch(
field: protoField.name,
proto: protoField.type,
custom: customField.type
))
}
}

for customField in customFields {
if protoFieldMap[customField.name] == nil {
errors.append(.extraField(customField.name))
}
}

return errors
}
}
Loading