Skip to content

Commit a398d6f

Browse files
authored
Merge pull request #1765 from HaishinKit/feature/capture-session-mode
Add CaptureSessionMode feature.
2 parents 895107f + 26f40ce commit a398d6f

File tree

9 files changed

+128
-47
lines changed

9 files changed

+128
-47
lines changed

Examples/iOS/IngestViewModel.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ final class IngestViewModel: ObservableObject {
1010
@Published var isShowError = false
1111
@Published private(set) var isTorchEnabled = false
1212
@Published private(set) var readyState: SessionReadyState = .closed
13-
// If you want to use the multi-camera feature, please make create a MediaMixer with a multiCamSession mode.
14-
// let mixer = MediaMixer(multiCamSessionEnabled: true)
15-
private(set) var mixer = MediaMixer(multiCamSessionEnabled: true, multiTrackAudioMixingEnabled: false)
13+
// If you want to use the multi-camera feature, please make create a MediaMixer with a capture mode.
14+
// let mixer = MediaMixer(captureSesionMode: .multi)
15+
private(set) var mixer = MediaMixer(captureSessionMode: .multi, multiTrackAudioMixingEnabled: false)
1616
private var session: (any Session)?
1717
private var currentPosition: AVCaptureDevice.Position = .back
1818
@ScreenActor private var videoScreenObject: VideoTrackScreenObject?

Examples/iOS/Screencast/SampleHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ final class SampleHandler: RPBroadcastSampleHandler, @unchecked Sendable {
3333
}
3434
}
3535
private var session: Session?
36-
private var mixer = MediaMixer(multiCamSessionEnabled: false, multiTrackAudioMixingEnabled: true)
36+
private var mixer = MediaMixer(captureSessionMode: .manual, multiTrackAudioMixingEnabled: true)
3737
private var needVideoConfiguration = true
3838

3939
override init() {

HaishinKit/Sources/Mixer/AudioCaptureUnit.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ final class AudioCaptureUnit: CaptureUnit {
5151
var devices: [UInt8: AudioDeviceUnit] = [:]
5252
#endif
5353

54-
private let session: CaptureSession
54+
private let session: (any CaptureSessionConvertible)
5555
private var continutation: AsyncStream<(AVAudioPCMBuffer, AVAudioTime)>.Continuation?
5656

57-
init(_ session: CaptureSession) {
57+
init(_ session: (some CaptureSessionConvertible)) {
5858
self.session = session
5959
}
6060

HaishinKit/Sources/Mixer/AudioDeviceUnit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public final class AudioDeviceUnit: DeviceUnit {
3232
self.track = track
3333
}
3434

35-
func attachDevice(_ device: AVCaptureDevice?, session: CaptureSession, audioUnit: AudioCaptureUnit) throws {
35+
func attachDevice(_ device: AVCaptureDevice?, session: (some CaptureSessionConvertible), audioUnit: AudioCaptureUnit) throws {
3636
setSampleBufferDelegate(nil)
3737
session.detachCapture(self)
3838
guard let device else {

HaishinKit/Sources/Mixer/CaptureSession.swift

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,28 @@
11
import AVFoundation
22

3-
final class CaptureSession {
3+
protocol CaptureSessionConvertible: Runner {
4+
#if !os(visionOS)
5+
@available(tvOS 17.0, *)
6+
var sessionPreset: AVCaptureSession.Preset { get set }
7+
#endif
8+
9+
var isInturreped: AsyncStream<Bool> { get }
10+
var runtimeError: AsyncStream<AVError> { get }
11+
var isMultiCamSessionEnabled: Bool { get set }
12+
@available(tvOS 17.0, *)
13+
var isMultitaskingCameraAccessEnabled: Bool { get }
14+
15+
@available(tvOS 17.0, *)
16+
func attachCapture(_ capture: (any DeviceUnit)?)
17+
@available(tvOS 17.0, *)
18+
func detachCapture(_ capture: (any DeviceUnit)?)
19+
@available(tvOS 17.0, *)
20+
func configuration(_ lambda: (_ session: AVCaptureSession) throws -> Void ) rethrows
21+
@available(tvOS 17.0, *)
22+
func startRunningIfNeeded()
23+
}
24+
25+
final class CaptureSession: CaptureSessionConvertible {
426
#if os(iOS) || os(tvOS)
527
static var isMultiCamSupported: Bool {
628
if #available(tvOS 17.0, *) {
@@ -30,10 +52,10 @@ final class CaptureSession {
3052
}
3153

3254
#elseif os(macOS)
33-
let isMultiCamSessionEnabled = true
55+
var isMultiCamSessionEnabled = true
3456
let isMultitaskingCameraAccessEnabled = true
3557
#elseif os(visionOS)
36-
let isMultiCamSessionEnabled = false
58+
var isMultiCamSessionEnabled = false
3759
let isMultitaskingCameraAccessEnabled = false
3860
#endif
3961

@@ -293,3 +315,44 @@ extension CaptureSession: Runner {
293315
}
294316
}
295317
}
318+
319+
final class NullCaptureSession: CaptureSessionConvertible {
320+
private(set) var isRunning: Bool = false
321+
322+
#if !os(visionOS)
323+
@available(tvOS 17.0, *)
324+
var sessionPreset: AVCaptureSession.Preset {
325+
get {
326+
return .default
327+
}
328+
set {
329+
}
330+
}
331+
#endif
332+
333+
@AsyncStreamed(false) var isInturreped: AsyncStream<Bool>
334+
@AsyncStreamedFlow var runtimeError: AsyncStream<AVError>
335+
var isMultiCamSessionEnabled: Bool = false
336+
var isMultitaskingCameraAccessEnabled: Bool = false
337+
338+
@available(tvOS 17.0, *)
339+
func attachCapture(_ capture: (any DeviceUnit)?) {
340+
}
341+
342+
@available(tvOS 17.0, *)
343+
func detachCapture(_ capture: (any DeviceUnit)?) {
344+
}
345+
346+
@available(tvOS 17.0, *)
347+
func configuration(_ lambda: (AVCaptureSession) throws -> Void) rethrows {
348+
}
349+
350+
func startRunningIfNeeded() {
351+
}
352+
353+
func startRunning() {
354+
}
355+
356+
func stopRunning() {
357+
}
358+
}

HaishinKit/Sources/Mixer/MediaMixer.swift

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,38 @@ public final actor MediaMixer {
1717
case deviceNotFound
1818
}
1919

20+
/// An enumeration defines the capture session mode used for video/audio input.
21+
public enum CaptureSessionMode: Sendable {
22+
/// Uses a standard `AVCaptureSession`
23+
case single
24+
/// Uses an `AVCaptureMultiCamSession`
25+
case multi
26+
/// Does not use a `AVCaptureSession`. Set this when using ReplayKit, as AVCaptureSession is not required.
27+
case manual
28+
29+
func makeSession() -> (any CaptureSessionConvertible) {
30+
switch self {
31+
case .single:
32+
var session = CaptureSession()
33+
session.isMultiCamSessionEnabled = false
34+
return session
35+
case .multi:
36+
var session = CaptureSession()
37+
session.isMultiCamSessionEnabled = true
38+
return session
39+
case .manual:
40+
return NullCaptureSession()
41+
}
42+
}
43+
}
44+
2045
/// The offscreen rendering object.
2146
@ScreenActor
2247
public private(set) lazy var screen = Screen()
2348

49+
/// The capture session mode.
50+
public let captureSessionMode: CaptureSessionMode
51+
2452
#if os(iOS) || os(tvOS)
2553
/// The AVCaptureMultiCamSession enabled.
2654
@available(tvOS 17.0, *)
@@ -99,57 +127,27 @@ public final actor MediaMixer {
99127
private var cancellables: Set<AnyCancellable> = []
100128
private lazy var audioIO = AudioCaptureUnit(session)
101129
private lazy var videoIO = VideoCaptureUnit(session)
102-
private lazy var session = CaptureSession()
130+
private lazy var session: (any CaptureSessionConvertible) = captureSessionMode.makeSession()
103131
@ScreenActor
104132
private lazy var displayLink = DisplayLinkChoreographer()
105133

106-
#if os(iOS) || os(tvOS)
107134
/// Creates a new instance.
108135
///
109136
/// - Parameters:
110-
/// - multiCamSessionEnabled: Specifies the AVCaptureMultiCamSession enabled.
137+
/// - captureSessionMode: Specifies the capture session mode.
111138
/// - multiTrackAudioMixingEnabled: Specifies the feature to mix multiple audio tracks. For example, it is possible to mix .appAudio and .micAudio from ReplayKit.
112139
public init(
113-
multiCamSessionEnabled: Bool = true,
140+
captureSessionMode: CaptureSessionMode = .single,
114141
multiTrackAudioMixingEnabled: Bool = false
115142
) {
116-
Task {
117-
await _init(
118-
multiCamSessionEnabled: multiCamSessionEnabled,
119-
multiTrackAudioMixingEnabled: multiTrackAudioMixingEnabled
120-
)
121-
}
122-
}
123-
124-
private func _init(
125-
multiCamSessionEnabled: Bool,
126-
multiTrackAudioMixingEnabled: Bool
127-
) async {
128-
session.isMultiCamSessionEnabled = multiCamSessionEnabled
129-
audioIO.isMultiTrackAudioMixingEnabled = multiTrackAudioMixingEnabled
130-
}
131-
132-
#else
133-
/// Creates a new instance.
134-
///
135-
/// - Parameters:
136-
/// - multiTrackAudioMixingEnabled: Specifies the feature to mix multiple audio tracks. For example, it is possible to mix .appAudio and .micAudio from ReplayKit.
137-
public init(
138-
multiTrackAudioMixingEnabled: Bool = false,
139-
) {
140-
Task {
141-
await _init(
142-
multiTrackAudioMixingEnabled: multiTrackAudioMixingEnabled
143-
)
144-
}
143+
self.captureSessionMode = captureSessionMode
145144
}
146145

147146
private func _init(
148147
multiTrackAudioMixingEnabled: Bool
149148
) async {
150149
audioIO.isMultiTrackAudioMixingEnabled = multiTrackAudioMixingEnabled
151150
}
152-
#endif
153151

154152
/// Attaches a video device.
155153
///

HaishinKit/Sources/Mixer/VideoCaptureUnit.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ final class VideoCaptureUnit: CaptureUnit {
8181
var devices: [UInt8: VideoDeviceUnit] = [:]
8282
#endif
8383

84-
private let session: CaptureSession
84+
private let session: (any CaptureSessionConvertible)
8585

86-
init(_ session: CaptureSession) {
86+
init(_ session: (some CaptureSessionConvertible)) {
8787
self.session = session
8888
}
8989

HaishinKit/Sources/Mixer/VideoDeviceUnit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public final class VideoDeviceUnit: DeviceUnit {
121121
self.frameRate = frameRate
122122
}
123123

124-
func attachDevice(_ device: AVCaptureDevice?, session: CaptureSession, videoUnit: VideoCaptureUnit) throws {
124+
func attachDevice(_ device: AVCaptureDevice?, session: some CaptureSessionConvertible, videoUnit: VideoCaptureUnit) throws {
125125
setSampleBufferDelegate(nil)
126126
session.detachCapture(self)
127127
guard let device else {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
3+
@propertyWrapper
4+
package struct AsyncStreamedFlow<T: Sendable & Equatable> {
5+
package var wrappedValue: AsyncStream<T> {
6+
get {
7+
return stream
8+
}
9+
@available(*, unavailable)
10+
set { _ = newValue }
11+
}
12+
private let stream: AsyncStream<T>
13+
private let continuation: AsyncStream<T>.Continuation
14+
15+
package init(_ bufferingPolicy: AsyncStream<T>.Continuation.BufferingPolicy = .unbounded) {
16+
let (stream, continuation) = AsyncStream.makeStream(of: T.self, bufferingPolicy: bufferingPolicy)
17+
self.stream = stream
18+
self.continuation = continuation
19+
}
20+
}

0 commit comments

Comments
 (0)