Skip to content

Commit d94c575

Browse files
authored
Multicam fixes (#851)
Resolves #843 - Reverts the core part of #833 for multi cam devices, as it may cause timeout when mixing wrong format vs session type - Adds `aspectRatio` criterion to minimize cropping - Fixes `VideoView` jumps when using square formats in fill mode due to floating point comparisons (flipping between 1+/-eps) Waiting for a better fix for square cropping (if any). <img width="1019" height="305" alt="image" src="https://github.com/user-attachments/assets/0159893e-df35-4b79-bf93-fb0c86584b77" />
1 parent a093108 commit d94c575

File tree

5 files changed

+57
-27
lines changed

5 files changed

+57
-27
lines changed

.changes/camera-timeout

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="fixed" "Timeouts when publishing camera tracks"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2025 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
19+
extension CGFloat {
20+
static let aspectRatioTolerance: Self = 0.01
21+
}

Sources/LiveKit/Extensions/CustomStringConvertible.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ extension AVCaptureDevice.Format {
204204
var values: [String] = []
205205
values.append("fps: \(fpsRange())")
206206
#if os(iOS)
207-
values.append("isMulticamSupported: \(isMultiCamSupported)")
207+
values.append("isMultiCamSupported: \(isMultiCamSupported)")
208208
#endif
209209
return "Format(\(values.joined(separator: ", ")))"
210210
}

Sources/LiveKit/Track/Capturers/CameraCapturer.swift

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ public class CameraCapturer: VideoCapturer, @unchecked Sendable {
9090
capturer.captureSession
9191
}
9292

93+
private var isMultiCamSession: Bool {
94+
#if os(iOS) || os(tvOS)
95+
captureSession is AVCaptureMultiCamSession
96+
#else
97+
false
98+
#endif
99+
}
100+
93101
// RTCCameraVideoCapturer used internally for now
94102
private lazy var capturer: LKRTCCameraVideoCapturer = .init(delegate: adapter)
95103

@@ -154,21 +162,17 @@ public class CameraCapturer: VideoCapturer, @unchecked Sendable {
154162
var device: AVCaptureDevice? = options.device
155163

156164
if device == nil {
157-
#if os(iOS) || os(tvOS)
158165
var devices: [AVCaptureDevice]
159-
if AVCaptureMultiCamSession.isMultiCamSupported {
166+
if isMultiCamSession {
160167
// Get the list of devices already on the shared multi-cam session.
161168
let existingDevices = captureSession.inputs.compactMap { $0 as? AVCaptureDeviceInput }.map(\.device)
162-
log("Existing multicam devices: \(existingDevices)")
169+
log("Existing multiCam devices: \(existingDevices)")
163170
// Compute other multi-cam compatible devices.
164171
devices = try await DeviceManager.shared.multiCamCompatibleDevices(for: Set(existingDevices))
165-
log("Compabible multicam devices: \(devices)")
172+
log("Compatible multiCam devices: \(devices)")
166173
} else {
167174
devices = try await CameraCapturer.captureDevices()
168175
}
169-
#else
170-
var devices = try await CameraCapturer.captureDevices()
171-
#endif
172176

173177
#if !os(visionOS)
174178
// Filter by deviceType if specified in options.
@@ -217,26 +221,27 @@ public class CameraCapturer: VideoCapturer, @unchecked Sendable {
217221

218222
let any: (FormatTuple) -> Bool = { _ in true }
219223
let matchesFps: (FormatTuple) -> Bool = { $0.format.fpsRange().contains(targetFps) }
220-
let supportsMulticam: (FormatTuple) -> Bool = { $0.format.filterForMulticamSupport }
224+
let matchesAspectRatio: (FormatTuple) -> Bool = {
225+
let sourceRatio = Double($0.dimensions.width) / Double($0.dimensions.height)
226+
let targetRatio = Double(targetDimensions.width) / Double(targetDimensions.height)
227+
return abs(sourceRatio - targetRatio) / targetRatio < CGFloat.aspectRatioTolerance
228+
}
229+
let supportsMultiCam: (FormatTuple) -> Bool = { $0.format.filterForMultiCamSupport }
221230
let byManhattanDistance: (FormatTuple, FormatTuple) -> Bool = { manhattanDistance($0) < manhattanDistance($1) }
222231

223-
#if os(iOS) || os(tvOS)
224-
let isMulticamActive = AVCaptureMultiCamSession.isMultiCamSupported && !captureSession.inputs.isEmpty
225-
let criteria: [(name: String, filter: (FormatTuple) -> Bool)] = isMulticamActive ? [
226-
(name: "fps, multicam", filter: { matchesFps($0) && supportsMulticam($0) }),
227-
(name: "multicam", filter: supportsMulticam),
228-
(name: "fps", filter: matchesFps),
229-
(name: "(fallback)", filter: any),
230-
] : [
231-
(name: "fps", filter: matchesFps),
232-
(name: "(fallback)", filter: any),
233-
]
234-
#else
235-
let criteria: [(name: String, filter: (FormatTuple) -> Bool)] = [
232+
var criteria: [(name: String, filter: (FormatTuple) -> Bool)] = isMultiCamSession ? [
233+
(name: "multiCam, aspectRatio, fps", filter: { supportsMultiCam($0) && matchesAspectRatio($0) && matchesFps($0) }),
234+
(name: "multiCam, aspectRatio", filter: { supportsMultiCam($0) && matchesAspectRatio($0) }),
235+
(name: "multiCam, fps", filter: { supportsMultiCam($0) && matchesFps($0) }),
236+
(name: "multiCam", filter: supportsMultiCam),
237+
] : []
238+
239+
criteria.append(contentsOf: [
240+
(name: "aspectRatio, fps", filter: { matchesAspectRatio($0) && matchesFps($0) }),
241+
(name: "aspectRatio", filter: matchesAspectRatio),
236242
(name: "fps", filter: matchesFps),
237243
(name: "(fallback)", filter: any),
238-
]
239-
#endif
244+
])
240245

241246
for (name, filter) in criteria {
242247
if let foundFormat = sortedFormats.sorted(by: byManhattanDistance).first(where: filter) {
@@ -384,7 +389,7 @@ extension AVCaptureDevice.Format {
384389

385390
// Used for filtering.
386391
// Only include multi-cam supported devices if in multi-cam mode. Otherwise, always include the devices.
387-
var filterForMulticamSupport: Bool {
392+
var filterForMultiCamSupport: Bool {
388393
#if os(iOS) || os(tvOS)
389394
return AVCaptureMultiCamSession.isMultiCamSupported ? isMultiCamSupported : true
390395
#else

Sources/LiveKit/Views/VideoView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,10 +467,13 @@ public class VideoView: NativeView, Loggable {
467467
let hDim = CGFloat(dimensions.height)
468468
let wRatio = size.width / wDim
469469
let hRatio = size.height / hDim
470+
let ratioDiff = abs(hRatio - wRatio)
470471

471-
if state.layoutMode == .fill ? hRatio > wRatio : hRatio < wRatio {
472+
if ratioDiff < CGFloat.aspectRatioTolerance {
473+
// no-op
474+
} else if state.layoutMode == .fill ? hRatio > wRatio : hRatio < wRatio {
472475
size.width = size.height / hDim * wDim
473-
} else if state.layoutMode == .fill ? wRatio > hRatio : wRatio < hRatio {
476+
} else {
474477
size.height = size.width / wDim * hDim
475478
}
476479

0 commit comments

Comments
 (0)