diff --git a/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift b/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift new file mode 100644 index 0000000..97dddec --- /dev/null +++ b/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift @@ -0,0 +1,24 @@ +// +// MillicastLoggerHandler.swift +// + +import Foundation +import MillicastSDK +import os + +final class MillicastLoggerHandler: NSObject { + + private static let logger = Logger.make(category: String(describing: MillicastLoggerHandler.self)) + + override init() { + super.init() + MCLogger.setDelegate(self) + MCLogger.disableWebsocketLogs(true) + } +} + +extension MillicastLoggerHandler: MCLoggerDelegate { + func onLog(withMessage message: String!, level: MCLogLevel) { + Self.logger.debug("🪵 onLog - \(message), log-level - \(level.rawValue)") + } +} diff --git a/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift b/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift index bc810b5..78680cd 100644 --- a/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift +++ b/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift @@ -40,7 +40,7 @@ protocol SubscriptionManagerDelegate: AnyObject { protocol SubscriptionManagerProtocol: AnyObject { var delegate: SubscriptionManagerDelegate? { get set } - func connect(streamName: String, accountID: String) async -> Bool + func connect(streamName: String, accountID: String, configuration: SubscriptionConfiguration) async -> Bool func startSubscribe() async -> Bool func stopSubscribe() async -> Bool func addRemoteTrack(_ sourceBuilder: StreamSourceBuilder) @@ -50,6 +50,15 @@ protocol SubscriptionManagerProtocol: AnyObject { func unprojectAudio(for source: StreamSource) } +struct SubscriptionConfiguration { + let autoReconnect = false + let videoJitterMinimumDelayMs: UInt = 20 + let statsDelayMs: UInt = 1000 + let forcePlayoutDelay = false + let disableAudio = false + let rtcEventLogOutputPath: String? = nil +} + final class SubscriptionManager: SubscriptionManagerProtocol { private enum Defaults { static let subscribeURL = "https://director.millicast.com/api/director/subscribe" @@ -60,12 +69,12 @@ final class SubscriptionManager: SubscriptionManagerProtocol { weak var delegate: SubscriptionManagerDelegate? - func connect(streamName: String, accountID: String) async -> Bool { + func connect(streamName: String, accountID: String, configuration: SubscriptionConfiguration) async -> Bool { if subscriber != nil { _ = await stopSubscribe() } - guard let subscriber = makeSubscriber() else { + guard let subscriber = makeSubscriber(with: configuration) else { Self.logger.error("💼 Failed to initialise subscriber") return false } @@ -160,7 +169,7 @@ final class SubscriptionManager: SubscriptionManagerProtocol { } func addRemoteTrack(_ sourceBuilder: StreamSourceBuilder) { - Self.logger.debug("💼 Add remote track for source - \(sourceBuilder.sourceId.value ?? "MAIN")") + Self.logger.debug("💼 Add remote track for source - \(sourceBuilder.sourceId)") sourceBuilder.supportedTrackItems.forEach { subscriber.addRemoteTrack($0.mediaType.rawValue) } } @@ -168,7 +177,7 @@ final class SubscriptionManager: SubscriptionManagerProtocol { let videoTrack = source.videoTrack let matchingVideoQuality = source.lowLevelVideoQualityList.matching(videoQuality: quality) - Self.logger.debug("💼 Project video for source \(String(describing: source.sourceId.value)) with quality - \(String(describing: matchingVideoQuality?.description))") + Self.logger.debug("💼 Project video for source \(source.sourceId) with quality - \(String(describing: matchingVideoQuality?.description))") let projectionData = MCProjectionData() projectionData.media = videoTrack.trackInfo.mediaType.rawValue @@ -180,13 +189,13 @@ final class SubscriptionManager: SubscriptionManagerProtocol { } func unprojectVideo(for source: StreamSource) { - Self.logger.debug("💼 Unproject video for source \(source.sourceId.value ?? "N/A")") + Self.logger.debug("💼 Unproject video for source \(source.sourceId)") let videoTrack = source.videoTrack subscriber.unproject([videoTrack.trackInfo.mid]) } func projectAudio(for source: StreamSource) { - Self.logger.debug("💼 Project audio for source \(source.sourceId.value ?? "N/A")") + Self.logger.debug("💼 Project audio for source \(source.sourceId)") guard let audioTrack = source.audioTracks.first else { return } @@ -202,7 +211,7 @@ final class SubscriptionManager: SubscriptionManagerProtocol { } func unprojectAudio(for source: StreamSource) { - Self.logger.debug("💼 Unproject audio for source \(source.sourceId.value ?? "N/A")") + Self.logger.debug("💼 Unproject audio for source \(source.sourceId)") guard let audioTrack = source.audioTracks.first else { return } @@ -215,15 +224,24 @@ final class SubscriptionManager: SubscriptionManagerProtocol { private extension SubscriptionManager { - func makeSubscriber() -> MCSubscriber? { + func makeSubscriber(with configuration: SubscriptionConfiguration) -> MCSubscriber? { let subscriber = MCSubscriber.create() subscriber?.enableStats(true) let options = MCClientOptions() - options.autoReconnect = false + options.autoReconnect = configuration.autoReconnect + options.videoJitterMinimumDelayMs = Int32(configuration.videoJitterMinimumDelayMs) + options.statsDelayMs = Int32(configuration.statsDelayMs) + if let rtcEventLogOutputPath = configuration.rtcEventLogOutputPath { + options.rtcEventLogOutputPath = rtcEventLogOutputPath + } + options.disableAudio = configuration.disableAudio + options.forcePlayoutDelay = configuration.forcePlayoutDelay + subscriber?.setOptions(options) - + subscriber?.enableStats(true) + return subscriber } @@ -241,7 +259,6 @@ private extension SubscriptionManager { // MARK: MCSubscriberListener implementation extension SubscriptionManager: MCSubscriberListener { - func onDisconnected() { Self.logger.debug("💼 Delegate - onDisconnected") delegate?.onDisconnected() @@ -252,28 +269,28 @@ extension SubscriptionManager: MCSubscriberListener { delegate?.onSubscribed() } - func onSubscribedError(_ reason: String!) { + func onSubscribedError(_ reason: String) { Self.logger.error("💼 Delegate - onSubscribedError \(reason)") delegate?.onSubscribedError(reason) } - func onVideoTrack(_ track: MCVideoTrack!, withMid mid: String!) { + func onVideoTrack(_ track: MCVideoTrack, withMid mid: String) { Self.logger.debug("💼 Delegate - onVideoTrack with mid \(mid)") delegate?.onVideoTrack(track, withMid: mid) } - func onAudioTrack(_ track: MCAudioTrack!, withMid mid: String!) { + func onAudioTrack(_ track: MCAudioTrack, withMid mid: String) { Self.logger.debug("💼 Delegate - onAudioTrack with mid \(mid)") delegate?.onAudioTrack(track, withMid: mid) } - func onActive(_ streamId: String!, tracks: [String]!, sourceId: String!) { - Self.logger.debug("💼 Delegate - onActive with sourceId \(sourceId ?? "NULL"), tracks - \(tracks)") + func onActive(_ streamId: String, tracks: [String], sourceId: String) { + Self.logger.debug("💼 Delegate - onActive with sourceId \(sourceId), tracks - \(tracks)") delegate?.onActive(streamId, tracks: tracks, sourceId: sourceId) } - func onInactive(_ streamId: String!, sourceId: String!) { - Self.logger.debug("💼 Delegate - onInactive with sourceId \(sourceId ?? "NULL")") + func onInactive(_ streamId: String, sourceId: String) { + Self.logger.debug("💼 Delegate - onInactive with sourceId \(sourceId)") delegate?.onInactive(streamId, sourceId: sourceId) } @@ -282,11 +299,11 @@ extension SubscriptionManager: MCSubscriberListener { delegate?.onStopped() } - func onVad(_ mid: String!, sourceId: String!) { + func onVad(_ mid: String, sourceId: String) { Self.logger.debug("💼 Delegate - onVad with mid \(mid), sourceId \(sourceId)") } - func onLayers(_ mid: String!, activeLayers: [MCLayerData]!, inactiveLayers: [MCLayerData]!) { + func onLayers(_ mid: String, activeLayers: [MCLayerData], inactiveLayers: [MCLayerData]) { Self.logger.debug("💼 Delegate - onLayers for mid - \(mid) with activeLayers \(activeLayers), inactiveLayers \(inactiveLayers)") delegate?.onLayers(mid, activeLayers: activeLayers, inactiveLayers: inactiveLayers) } @@ -296,17 +313,17 @@ extension SubscriptionManager: MCSubscriberListener { delegate?.onConnected() } - func onConnectionError(_ status: Int32, withReason reason: String!) { + func onConnectionError(_ status: Int32, withReason reason: String) { Self.logger.error("💼 Delegate - onConnectionError") delegate?.onConnectionError(status, withReason: reason) } - func onSignalingError(_ message: String!) { + func onSignalingError(_ message: String) { Self.logger.error("💼 Delegate - onSignalingError") delegate?.onSignalingError(message) } - func onStatsReport(_ report: MCStatsReport!) { + func onStatsReport(_ report: MCStatsReport) { Self.logger.debug("💼 Delegate - onStatsReport") delegate?.onStatsReport(report) } diff --git a/Sources/DolbyIORTSCore/Model/StreamSource.swift b/Sources/DolbyIORTSCore/Model/StreamSource.swift index a9e280e..995524e 100644 --- a/Sources/DolbyIORTSCore/Model/StreamSource.swift +++ b/Sources/DolbyIORTSCore/Model/StreamSource.swift @@ -13,7 +13,7 @@ public struct StreamSource: Equatable, Hashable, Identifiable { init(id: String?) { switch id { - case .none: + case .none, .some(""): self = .main case let .some(id): self = .other(sourceId: id) diff --git a/Sources/DolbyIORTSCore/State/State.swift b/Sources/DolbyIORTSCore/State/State.swift index f3d7335..2276bf3 100644 --- a/Sources/DolbyIORTSCore/State/State.swift +++ b/Sources/DolbyIORTSCore/State/State.swift @@ -5,6 +5,16 @@ import Foundation import MillicastSDK +struct VideoTrackAndMid { + let videoTrack: MCVideoTrack + let mid: String +} + +struct AudioTrackAndMid { + let audioTrack: MCAudioTrack + let mid: String +} + enum State: CustomStringConvertible { case disconnected case connecting @@ -39,16 +49,27 @@ struct SubscribedState { private(set) var streamSourceBuilders: [StreamSourceBuilder] private(set) var numberOfStreamViewers: Int private(set) var streamingStats: AllStreamingStatistics? + private(set) var cachedSourceZeroVideoTrackAndMid: VideoTrackAndMid? + private(set) var cachedSourceZeroAudioTrackAndMid: AudioTrackAndMid? - init() { + init(cachedVideoTrackDetail: VideoTrackAndMid?, cachedAudioTrackDetail: AudioTrackAndMid?) { + cachedSourceZeroVideoTrackAndMid = cachedVideoTrackDetail + cachedSourceZeroAudioTrackAndMid = cachedAudioTrackDetail streamSourceBuilders = [] numberOfStreamViewers = 0 } mutating func add(streamId: String, sourceId: String?, tracks: [String]) { - streamSourceBuilders.append( - StreamSourceBuilder.init(streamId: streamId, sourceId: sourceId, tracks: tracks) - ) + let streamSourceBuilder = StreamSourceBuilder.init(streamId: streamId, sourceId: sourceId, tracks: tracks) + if let videoTrackAndMid = cachedSourceZeroVideoTrackAndMid { + streamSourceBuilder.addVideoTrack(videoTrackAndMid.videoTrack, mid: videoTrackAndMid.mid) + cachedSourceZeroVideoTrackAndMid = nil + } + if let audioTrackAndMid = cachedSourceZeroAudioTrackAndMid { + streamSourceBuilder.addAudioTrack(audioTrackAndMid.audioTrack, mid: audioTrackAndMid.mid) + cachedSourceZeroAudioTrackAndMid = nil + } + streamSourceBuilders.append(streamSourceBuilder) } mutating func remove(streamId: String, sourceId: String?) { diff --git a/Sources/DolbyIORTSCore/State/StateMachine.swift b/Sources/DolbyIORTSCore/State/StateMachine.swift index 76775db..3ad56ad 100644 --- a/Sources/DolbyIORTSCore/State/StateMachine.swift +++ b/Sources/DolbyIORTSCore/State/StateMachine.swift @@ -19,6 +19,8 @@ final class StateMachine { private let stateSubject: PassthroughSubject = PassthroughSubject() lazy var statePublisher: AnyPublisher = stateSubject.eraseToAnyPublisher() + private(set) var cachedSourceZeroVideoTrackAndMid: VideoTrackAndMid? + private(set) var cachedSourceZeroAudioTrackAndMid: AudioTrackAndMid? init(initialState: State) { currentState = initialState @@ -69,7 +71,15 @@ final class StateMachine { } func onSubscribed() { - currentState = .subscribed(.init()) + currentState = .subscribed( + .init( + cachedVideoTrackDetail: cachedSourceZeroVideoTrackAndMid, + cachedAudioTrackDetail: cachedSourceZeroAudioTrackAndMid + ) + ) + cachedSourceZeroAudioTrackAndMid = nil + cachedSourceZeroAudioTrackAndMid = nil + } func onSubscribedError(_ reason: String) { @@ -115,6 +125,7 @@ final class StateMachine { currentState = .subscribed(state) default: + self.cachedSourceZeroVideoTrackAndMid = VideoTrackAndMid(videoTrack: track, mid: mid) Self.logger.error("🛑 Unexpected state on onVideoTrack - \(self.currentState.description)") } } @@ -125,6 +136,7 @@ final class StateMachine { state.addAudioTrack(track, mid: mid) currentState = .subscribed(state) default: + self.cachedSourceZeroAudioTrackAndMid = AudioTrackAndMid(audioTrack: track, mid: mid) Self.logger.error("🛑 Unexpected state on onAudioTrack - \(self.currentState.description)") } } diff --git a/Sources/DolbyIORTSCore/StreamOrchestrator.swift b/Sources/DolbyIORTSCore/StreamOrchestrator.swift index 8324e6a..1477b4f 100644 --- a/Sources/DolbyIORTSCore/StreamOrchestrator.swift +++ b/Sources/DolbyIORTSCore/StreamOrchestrator.swift @@ -33,7 +33,8 @@ public final actor StreamOrchestrator { .eraseToAnyPublisher() private var activeStreamDetail: StreamDetail? private static var configuration: StreamOrchestrator.Configuration = .init() - + private let logHandler: MillicastLoggerHandler = .init() + private init() { self.init( subscriptionManager: SubscriptionManager(), @@ -69,7 +70,7 @@ public final actor StreamOrchestrator { Self.logger.debug("👮‍♂️ Start subscribe") async let startConnectionStateUpdate: Void = stateMachine.startConnection(streamName: streamName, accountID: accountID) - async let startConnection = subscriptionManager.connect(streamName: streamName, accountID: accountID) + async let startConnection = subscriptionManager.connect(streamName: streamName, accountID: accountID, configuration: .init()) let (_, connectionResult) = await (startConnectionStateUpdate, startConnection) if connectionResult { @@ -257,10 +258,10 @@ private extension StreamOrchestrator { return await subscriptionManager.startSubscribe() } - func stopAudio(for sourceId: String) { + func stopAudio(for sourceId: String?) { switch stateSubject.value { case let .subscribed(sources: sources, numberOfStreamViewers: _): - if let source = sources.first (where: { $0.sourceId.value == sourceId }), source.isPlayingAudio { + if let source = sources.first (where: { $0.sourceId == StreamSource.SourceId(id: sourceId) }), source.isPlayingAudio { subscriptionManager.unprojectAudio(for: source) } default: break @@ -367,10 +368,11 @@ extension StreamOrchestrator: SubscriptionManagerDelegate { let stateMachineState = self.stateMachine.currentState switch stateMachineState { case let .subscribed(state): - guard let sourceBuilder = state.streamSourceBuilders.first(where: { $0.sourceId.value == sourceId }) else { + guard let sourceBuilder = state.streamSourceBuilders.first(where: { $0.sourceId == StreamSource.SourceId(id: sourceId) }) else { return } self.subscriptionManager.addRemoteTrack(sourceBuilder) + default: return } @@ -382,10 +384,8 @@ extension StreamOrchestrator: SubscriptionManagerDelegate { guard let self = self else { return } // Unproject audio whose source is inactive - if let sourceId = sourceId { - await self.stopAudio(for: sourceId) - } - + await self.stopAudio(for: sourceId) + self.stateMachine.onInactive(streamId, sourceId: sourceId) } } diff --git a/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift b/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift index 76e6fe9..cefc0a7 100644 --- a/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift +++ b/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift @@ -267,9 +267,6 @@ final class StreamViewModel: ObservableObject { self.internalState = .error(ErrorViewModel(error: streamError)) case .stopped, .disconnected: self.internalState = .error(.streamOffline) - default: - // Handle's scenario where there is no sources - self.internalState = .error(.genericError) } } .store(in: &self.subscriptions)