From cce2376d9198f74362defa3f52bc06ef352f9aea Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Wed, 11 Oct 2023 16:09:16 +1100 Subject: [PATCH 01/10] Fix the scenario where a reconnection is attempted after the subscription is stopped --- Sources/DolbyIORTSCore/StreamOrchestrator.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/DolbyIORTSCore/StreamOrchestrator.swift b/Sources/DolbyIORTSCore/StreamOrchestrator.swift index 1477b4f..8a78c23 100644 --- a/Sources/DolbyIORTSCore/StreamOrchestrator.swift +++ b/Sources/DolbyIORTSCore/StreamOrchestrator.swift @@ -237,13 +237,17 @@ private extension StreamOrchestrator { } func scheduleReconnection() { - guard Self.configuration.retryOnConnectionError, let streamDetail = self.activeStreamDetail else { + guard Self.configuration.retryOnConnectionError else { return } Self.logger.debug("👮‍♂️ Scheduling a reconnect") taskScheduler.scheduleTask(timeInterval: Defaults.retryConnectionTimeInterval) { [weak self] in guard let self = self else { return } Task { + guard let streamDetail = await self.activeStreamDetail else { + return + } + self.taskScheduler.invalidate() _ = await self.connect( streamName: streamDetail.streamName, From 559136a97d5f8f497a7e485d964c049f5d3fd9ef Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Fri, 13 Oct 2023 13:50:22 +1100 Subject: [PATCH 02/10] Add AppConfiguration protocol definition --- .../Configurations/AppConfigurationProviding.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift diff --git a/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift b/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift new file mode 100644 index 0000000..1c048f9 --- /dev/null +++ b/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift @@ -0,0 +1,10 @@ +// +// AppConfigurationProviding.swift +// + +import Combine +import Foundation + +public protocol AppConfigurationProviding: AnyObject { + var showDebugFeatures: Bool { get } +} From f7fec6906afdcb9fed458efc5ec331bbee44e734 Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Fri, 13 Oct 2023 14:41:10 +1100 Subject: [PATCH 03/10] Add AppConfiguration and SwiftUI accessor for app config --- .../AppConfigurationProviding.swift | 10 -- .../Configurations/AppConfigurations.swift | 104 ++++++++++++++++++ 2 files changed, 104 insertions(+), 10 deletions(-) delete mode 100644 Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift create mode 100644 Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurations.swift diff --git a/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift b/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift deleted file mode 100644 index 1c048f9..0000000 --- a/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurationProviding.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// AppConfigurationProviding.swift -// - -import Combine -import Foundation - -public protocol AppConfigurationProviding: AnyObject { - var showDebugFeatures: Bool { get } -} diff --git a/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurations.swift b/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurations.swift new file mode 100644 index 0000000..5822df6 --- /dev/null +++ b/Sources/DolbyIORTSUIKit/Public/Configurations/AppConfigurations.swift @@ -0,0 +1,104 @@ +// +// AppConfigurations.swift +// + +import Combine +import Foundation +import SwiftUI + +public final class AppConfigurations { + + public static let standard = AppConfigurations(userDefaults: .standard) + fileprivate let userDefaults: UserDefaults + + fileprivate let _appConfigurationsChangedSubject = PassthroughSubject() + fileprivate lazy var appConfigurationsChangedSubject = { + _appConfigurationsChangedSubject.eraseToAnyPublisher() + }() + + init(userDefaults: UserDefaults) { + self.userDefaults = userDefaults + } + + @UserDefault("show_debug_features") + public var showDebugFeatures: Bool = false +} + +@propertyWrapper +public struct UserDefault { + let key: String + let defaultValue: Value + + public var wrappedValue: Value { + get { fatalError("Wrapped value should not be used.") } + set { fatalError("Wrapped value should not be used.") } + } + + init(wrappedValue: Value, _ key: String) { + self.defaultValue = wrappedValue + self.key = key + } + + public static subscript( + _enclosingInstance instance: AppConfigurations, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> Value { + get { + let container = instance.userDefaults + let key = instance[keyPath: storageKeyPath].key + let defaultValue = instance[keyPath: storageKeyPath].defaultValue + return container.object(forKey: key) as? Value ?? defaultValue + } + set { + let container = instance.userDefaults + let key = instance[keyPath: storageKeyPath].key + container.set(newValue, forKey: key) + instance._appConfigurationsChangedSubject.send(wrappedKeyPath) + } + } +} + +@propertyWrapper +public struct AppConfiguration: DynamicProperty { + + @ObservedObject private var appConfigurationsObserver: PublisherObservableObject + private let keyPath: ReferenceWritableKeyPath + private let appConfigurations: AppConfigurations + + public init(_ keyPath: ReferenceWritableKeyPath, appConfigurations: AppConfigurations = .standard) { + self.keyPath = keyPath + self.appConfigurations = appConfigurations + let publisher = appConfigurations + .appConfigurationsChangedSubject + .filter { changedKeyPath in + changedKeyPath == keyPath + }.map { _ in () } + .eraseToAnyPublisher() + self.appConfigurationsObserver = .init(publisher: publisher) + } + + public var wrappedValue: Value { + get { appConfigurations[keyPath: keyPath] } + nonmutating set { appConfigurations[keyPath: keyPath] = newValue } + } + + public var projectedValue: Binding { + Binding( + get: { wrappedValue }, + set: { wrappedValue = $0 } + ) + } +} + +final class PublisherObservableObject: ObservableObject { + + var subscriber: AnyCancellable? + + init(publisher: AnyPublisher) { + subscriber = publisher.sink(receiveValue: { [weak self] _ in + self?.objectWillChange.send() + }) + } +} + From 2313ebfe4403bbcb42346b618d0039f411bc48be Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Fri, 13 Oct 2023 15:20:43 +1100 Subject: [PATCH 04/10] Expose subscription configuration on connect --- .../Manager/MillicastLoggerHandler.swift | 29 ++++++++++- .../Manager/SubscriptionManager.swift | 26 ++++------ .../DolbyIORTSCore/Model/StreamDetail.swift | 1 - .../Model/SubscriptionConfiguration.swift | 45 ++++++++++++++++++ .../DolbyIORTSCore/Model/VideoQuality.swift | 7 ++- .../DolbyIORTSCore/StreamOrchestrator.swift | 20 ++------ .../Private/ViewModels/StreamViewModel.swift | 11 +++-- .../Views/Settings/SettingsButton.swift | 7 ++- .../Buttons/settings.imageset/settings.pdf | Bin 5722 -> 5576 bytes 9 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift diff --git a/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift b/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift index 97dddec..531ceef 100644 --- a/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift +++ b/Sources/DolbyIORTSCore/Manager/MillicastLoggerHandler.swift @@ -9,16 +9,43 @@ import os final class MillicastLoggerHandler: NSObject { private static let logger = Logger.make(category: String(describing: MillicastLoggerHandler.self)) - + private var logFilePath: String? + override init() { super.init() MCLogger.setDelegate(self) MCLogger.disableWebsocketLogs(true) } + + func setLogFilePath(filePath: String?) { + logFilePath = filePath + } } extension MillicastLoggerHandler: MCLoggerDelegate { func onLog(withMessage message: String!, level: MCLogLevel) { Self.logger.debug("🪵 onLog - \(message), log-level - \(level.rawValue)") + + guard + let logFilePath = logFilePath, + let messageData = "\(String(describing: message))\n".data(using: .utf8) + else { + Self.logger.error("🪵 Error writing file - no file path provided") + return + } + + let fileURL = URL(fileURLWithPath: logFilePath, isDirectory: false) + do { + if FileManager.default.fileExists(atPath: fileURL.path) { + let fileHandle = try FileHandle(forWritingTo: fileURL) + fileHandle.seekToEndOfFile() + fileHandle.write(messageData) + fileHandle.closeFile() + } else { + try messageData.write(to: fileURL, options: .atomicWrite) + } + } catch { + Self.logger.error("🪵 Error writing file - \(error.localizedDescription)") + } } } diff --git a/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift b/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift index 78680cd..172dd29 100644 --- a/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift +++ b/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift @@ -50,19 +50,13 @@ 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" + static let productionSubscribeURL = "https://director.millicast.com/api/director/subscribe" + static let developmentSubscribeURL = "https://director-dev.millicast.com/api/director/subscribe" } + private static let logger = Logger.make(category: String(describing: SubscriptionManager.self)) private var subscriber: MCSubscriber! @@ -99,7 +93,7 @@ final class SubscriptionManager: SubscriptionManagerProtocol { return false } - let credentials = self.makeCredentials(streamName: streamName, accountID: accountID) + let credentials = self.makeCredentials(streamName: streamName, accountID: accountID, useDevelopmentServer: configuration.useDevelopmentServer) self.subscriber.setCredentials(credentials) @@ -231,13 +225,13 @@ private extension SubscriptionManager { let options = MCClientOptions() options.autoReconnect = configuration.autoReconnect - options.videoJitterMinimumDelayMs = Int32(configuration.videoJitterMinimumDelayMs) + options.videoJitterMinimumDelayMs = Int32(configuration.videoJitterMinimumDelayInMs) options.statsDelayMs = Int32(configuration.statsDelayMs) - if let rtcEventLogOutputPath = configuration.rtcEventLogOutputPath { + if let rtcEventLogOutputPath = configuration.rtcEventLogPath { options.rtcEventLogOutputPath = rtcEventLogOutputPath } options.disableAudio = configuration.disableAudio - options.forcePlayoutDelay = configuration.forcePlayoutDelay + options.forcePlayoutDelay = configuration.noPlayoutDelay subscriber?.setOptions(options) subscriber?.enableStats(true) @@ -245,12 +239,12 @@ private extension SubscriptionManager { return subscriber } - func makeCredentials(streamName: String, accountID: String) -> MCSubscriberCredentials { + func makeCredentials(streamName: String, accountID: String, useDevelopmentServer: Bool) -> MCSubscriberCredentials { let credentials = MCSubscriberCredentials() credentials.accountId = accountID credentials.streamName = streamName credentials.token = "" - credentials.apiUrl = Defaults.subscribeURL + credentials.apiUrl = useDevelopmentServer ? Defaults.developmentSubscribeURL : Defaults.productionSubscribeURL return credentials } diff --git a/Sources/DolbyIORTSCore/Model/StreamDetail.swift b/Sources/DolbyIORTSCore/Model/StreamDetail.swift index f4c520e..7b4e9aa 100644 --- a/Sources/DolbyIORTSCore/Model/StreamDetail.swift +++ b/Sources/DolbyIORTSCore/Model/StreamDetail.swift @@ -8,7 +8,6 @@ public struct StreamDetail: Equatable, Identifiable { public let id = UUID() public let streamName: String public let accountID: String - public var streamId: String { "\(self.accountID)/\(self.streamName)" } public init(streamName: String, accountID: String) { self.streamName = streamName diff --git a/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift b/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift new file mode 100644 index 0000000..9c269ed --- /dev/null +++ b/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift @@ -0,0 +1,45 @@ +// +// SubscriptionConfiguration.swift +// + +import Foundation + +public struct SubscriptionConfiguration { + public enum Constants { + public static let useDevelopmentServer = false + public static let autoReconnect = false + public static let videoJitterMinimumDelayInMs: UInt = 20 + public static let statsDelayMs: UInt = 1000 + public static let noPlayoutDelay = false + public static let disableAudio = false + } + + public let useDevelopmentServer: Bool + public let autoReconnect: Bool + public let videoJitterMinimumDelayInMs: UInt + public let statsDelayMs: UInt + public let noPlayoutDelay: Bool + public let disableAudio: Bool + public let rtcEventLogPath: String? + public let sdkLogPath: String? + + public init( + useDevelopmentServer: Bool = Constants.useDevelopmentServer, + autoReconnect: Bool = Constants.autoReconnect, + videoJitterMinimumDelayInMs: UInt = Constants.videoJitterMinimumDelayInMs, + statsDelayMs: UInt = Constants.statsDelayMs, + noPlayoutDelay: Bool = Constants.noPlayoutDelay, + disableAudio: Bool = Constants.disableAudio, + rtcEventLogPath: String? = nil, + sdkLogPath: String? = nil + ) { + self.useDevelopmentServer = useDevelopmentServer + self.autoReconnect = autoReconnect + self.videoJitterMinimumDelayInMs = videoJitterMinimumDelayInMs + self.statsDelayMs = statsDelayMs + self.noPlayoutDelay = noPlayoutDelay + self.disableAudio = disableAudio + self.rtcEventLogPath = rtcEventLogPath + self.sdkLogPath = sdkLogPath + } +} diff --git a/Sources/DolbyIORTSCore/Model/VideoQuality.swift b/Sources/DolbyIORTSCore/Model/VideoQuality.swift index 8496123..bc4acb1 100644 --- a/Sources/DolbyIORTSCore/Model/VideoQuality.swift +++ b/Sources/DolbyIORTSCore/Model/VideoQuality.swift @@ -4,7 +4,8 @@ import Foundation -public enum VideoQuality: Equatable { +public enum VideoQuality: String, Equatable, CaseIterable, Identifiable { + case auto case high case medium @@ -35,4 +36,8 @@ public enum VideoQuality: Equatable { return "Low" } } + + public var id: String { + return rawValue + } } diff --git a/Sources/DolbyIORTSCore/StreamOrchestrator.swift b/Sources/DolbyIORTSCore/StreamOrchestrator.swift index 8a78c23..e53ee61 100644 --- a/Sources/DolbyIORTSCore/StreamOrchestrator.swift +++ b/Sources/DolbyIORTSCore/StreamOrchestrator.swift @@ -9,10 +9,7 @@ import os @globalActor public final actor StreamOrchestrator { - public struct Configuration { - let retryOnConnectionError = true - } - + private static let logger = Logger.make(category: String(describing: StreamOrchestrator.self)) private enum Defaults { @@ -32,7 +29,6 @@ public final actor StreamOrchestrator { .removeDuplicates() .eraseToAnyPublisher() private var activeStreamDetail: StreamDetail? - private static var configuration: StreamOrchestrator.Configuration = .init() private let logHandler: MillicastLoggerHandler = .init() private init() { @@ -42,10 +38,6 @@ public final actor StreamOrchestrator { rendererRegistry: RendererRegistry() ) } - - static func setStreamOrchestratorConfiguration(_ configuration: StreamOrchestrator.Configuration) { - Self.configuration = configuration - } init( subscriptionManager: SubscriptionManagerProtocol, @@ -66,11 +58,11 @@ public final actor StreamOrchestrator { } } - public func connect(streamName: String, accountID: String) async -> Bool { + public func connect(streamName: String, accountID: String, configuration: SubscriptionConfiguration = .init()) async -> Bool { Self.logger.debug("👮‍♂️ Start subscribe") - + logHandler.setLogFilePath(filePath: configuration.sdkLogPath) async let startConnectionStateUpdate: Void = stateMachine.startConnection(streamName: streamName, accountID: accountID) - async let startConnection = subscriptionManager.connect(streamName: streamName, accountID: accountID, configuration: .init()) + async let startConnection = subscriptionManager.connect(streamName: streamName, accountID: accountID, configuration: configuration) let (_, connectionResult) = await (startConnectionStateUpdate, startConnection) if connectionResult { @@ -84,6 +76,7 @@ public final actor StreamOrchestrator { public func stopConnection() async -> Bool { Self.logger.debug("👮‍♂️ Stop subscribe") activeStreamDetail = nil + logHandler.setLogFilePath(filePath: nil) async let stopSubscribeOnStateMachine: Void = stateMachine.stopSubscribe() async let resetRegistry: Void = rendererRegistry.reset() async let stopSubscription: Bool = await subscriptionManager.stopSubscribe() @@ -237,9 +230,6 @@ private extension StreamOrchestrator { } func scheduleReconnection() { - guard Self.configuration.retryOnConnectionError else { - return - } Self.logger.debug("👮‍♂️ Scheduling a reconnect") taskScheduler.scheduleTask(timeInterval: Defaults.retryConnectionTimeInterval) { [weak self] in guard let self = self else { return } diff --git a/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift b/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift index cefc0a7..ae0eaa9 100644 --- a/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift +++ b/Sources/DolbyIORTSUIKit/Private/ViewModels/StreamViewModel.swift @@ -238,23 +238,24 @@ final class StreamViewModel: ObservableObject { } func playAudio(for source: StreamSource) { - Task { + Task { @StreamOrchestrator [weak self] in + guard let self = self else { return } await self.streamOrchestrator.playAudio(for: source) } } func stopAudio(for source: StreamSource) { - Task { + Task { @StreamOrchestrator [weak self] in + guard let self = self else { return } await self.streamOrchestrator.stopAudio(for: source) } } private func startObservers() { let settingsPublisher = settingsManager.publisher(for: settingsMode) - - Task { [weak self] in + Task { @StreamOrchestrator [weak self] in guard let self = self else { return } - await streamOrchestrator.statePublisher + await self.streamOrchestrator.statePublisher .combineLatest(settingsPublisher) .receive(on: DispatchQueue.main) .sink { state, settings in diff --git a/Sources/DolbyIORTSUIKit/Private/Views/Settings/SettingsButton.swift b/Sources/DolbyIORTSUIKit/Private/Views/Settings/SettingsButton.swift index 1b138d9..4eec3c5 100644 --- a/Sources/DolbyIORTSUIKit/Private/Views/Settings/SettingsButton.swift +++ b/Sources/DolbyIORTSUIKit/Private/Views/Settings/SettingsButton.swift @@ -5,18 +5,17 @@ import DolbyIOUIKit import SwiftUI -struct SettingsButton: View { +public struct SettingsButton: View { private let onAction: () -> Void - init(onAction: @escaping (() -> Void) = {}) { + public init(onAction: @escaping (() -> Void) = {}) { self.onAction = onAction } - var body: some View { + public var body: some View { IconButton(iconAsset: .settings, action: { onAction() }) - .scaleEffect(0.5, anchor: .trailing) } } diff --git a/Sources/DolbyIOUIKit/Resources/Media.xcassets/Buttons/settings.imageset/settings.pdf b/Sources/DolbyIOUIKit/Resources/Media.xcassets/Buttons/settings.imageset/settings.pdf index 301b342291ede25a736416110bc71d6db78c6fda..a7260e674c849296ad056b0034a4d7e2f7c1831c 100644 GIT binary patch literal 5576 zcma)=OK&8{5rpskEBdm4bWl%ay$cWoSi6>C*oN1XZ-Ni>>}Zz;$+1bwkp1iZMRiY2 zQ|p5$KuF6MbL{`6f|L(i5Y$?-ZvzyO<|Hm}XUw=J+^UZX6{GfkMes4eg@$~rd z< zOsp$42AsP-n6s8`em*`<9@kwiG0bL5$d)b54|}U$a?yYD{p7D~2sCmWpLJA15Cd7d;Nino{*x^)`XH23F2zJ#~2$(p;F`Rw%~V;z4+T9bt%Q+ z&ns{05@U5LL;!%@?MjVv^ZV%PVi8YpZ=~Tkl_fXQh$|yw_ZWQ(fg)fewavvjFU^HU zjn)dJ=r92eLGxOw;}n<3yXpWco!W}WxcHa|UPfA>QgmWWF|dnFK6pGJkr|Y%t1fT~ z>=MiB--f%e_~f$hE??lewCYpp_`2eFp)I3Y=L&doY<0!^r7xBcii;~q*9JPm89s&N z_q}#6B8erYyf!6}inB<_tCqca<$V*)gxrh`K~8a{z(TxhEKI)BL80|C>u9a0?jUE7 z3aC_Tr6|xrFDdyT`1-}yNE6OR*>bgF>hNKNv`7K~lY`sq;LCwsEhLgw3#YV&R<#Nj zFRs>1e>Q4cOLDS+R>G-Eg-WU*s>ouf#bJykdzegtRp<{3JtU7tnhL2dB(%G`<&&bb zJv|_Ax3sGVwAt)0wV~*at-#f#gmjt6OIaJ0hId=TNied(0gJU5+7&h3VnGZhaAlvh zJR1GtfSZr-N0ue#5(1VDsBGyH>Eb;QU1ITlxoZt{-eYTE7wUnN3pFfr0Wn5SuoS8V z#}Gb@Om*2S1DncTbQ?7lEiAMHM9E$>CBaiw@ooqdgXGo_c!Ao*QfW?V;G{xWC<&Ef zuZGyv1}y<2Y6!uu3odT4s9RxkS&iZo>j5V{7_y2N{fz zN+FJJ6tXZ?1Qj@7THz6b+Yry3;Ul`LDAvZHw}H?q z>Z61DltJ4RTwP_uSD_L9CSZ@nv|_o<)Jemi(o!rli$IzP*7%|`zW1ZR%sb&OYf4CUM(trl)LsRF- z2opADHKxXd;-NiP6MPpLDOCN-tE3!3MWPAzat4hOQvi$XH=Hrb*&q%@mU;9Ev8M&d ztSDOIoGG^SMa;}N(m`Pil&DapkrS0s{ntd%6QrPdMLaUf1Q5^#U_{9Hz2^QG)~!t1 zpwz(F%32YCmu%}Ku<5B7WiOR!STfk^txF>4gsN(dqqqSwvFXd1b@NFM?_YdU1GBVXeA z#yAJIg(xIOU9Y(xrWCaoI6>nd#mkWgHmHow36ipg`WWwt8@U{)n>a5cOp^VU=_XnS z?xTdfh(&CfJNGdZ_4!EmoJ6NNWCNAsz}#t;deb8?B)%|l0ESA})Eaf5Un~diGr$x1 zqtWmN#3YK}sheAk+6bN$bmU&)4#11bRtzKwqj%1jceojW8c~L_g+eEFxuIjmW`*&$ zgO1MeJs^&ZXra1=a0&wrvoseOI0=2_b^(L3xSG*O>J{h^7E_2ql~#$F$OwsO-mjXw zT9m91x~JW1CP57mgn=k4v}(EQLt!xN@{M>g2C5j9Au-;pqaac(xJ<(iJ^J{MFuR!Phkoj=kUt4njuWJMqe4M zu`sA0A9z7?J9Io_)BS=>FbHy21H(un|JE&p8-ha#u`n$W2=75)gbn{QFECD1O_G$6 zIhBW(q!C6pf?(kSh0^5O$u}@cWWk5~CF#};8c+r-a2IosEc`LC?aO*u75UL~QDIeC zcS6sB70btYkU4^cEfX^ACl?5bBT|s*fR{Osv`?%325&V$CfEI5s234 zyXBKZG?}iW@0N9(fi@}cs0Seppb+qrNR>;Wj8z z8Ru~eZyHFjZC=qYnHf%a>cU@RGx!NP=&bnNOkR*fo;ody!dc{{ueuLi1y=L2~oz8yYC76yt(rUueS zK6m{vvOcs(KfatEIZ+=6FUI$`PuOONtDAP85M>&l77u^8`+%#nBDk3^=e#}t#Jl#H z|Ib)?W6#o$AQ95-k(3de)Z?4(~lFpG_A4f|1%%GAZX0n9~Z^`G(Qd# z67|{yg%gkd#;Syj{?gR%+^<*Z^WWvgm#5pi`_t3Jx7Ne&-;s_lp3e`bFXu1rK76?* m<#2O8zsw4_C;0IF-G5Jb-@m!uKE3pC@IFp&-u(8@-~A6NFj^4+ literal 5722 zcma)=OOGVQ6@~ZtD`GPeEXa!YLqbSq1}w|A7#i=87wwv2XjENgcQ+RHujf0FS&=ym z3wklixs?%dALpKXB453I^Yv$LF4N?UHP?UrG#T^x=jMwqrqkt}{yX`-{qVQb<^9W# zCSbhNThA{~r~Ak0@aFX2hvVt)&tIC?-|hc*e4hR@x!LwVV{Z?iji=xLE!bM4IX-Sn z$9v=9`FNRPn!Wd-Cga1LbE~zQ$GtVr(YAv1JjFVf6iO+^w>i2RY><^_8%t=iwvt<@ z23rlTd1r#1OLRGA*;;DVWfR<-O7X>;7~ zo^eXzfoe@5&%TBf@yN}-IY&0mQ%G}ij)-I}&BjD#h|%U&h|$kJG(u8lD#^CpmO~_& z+EX59TiCNGN0u|$lT$Ng?bcRY3cUis5(d6gY1|A+wPL5@*t6*z&wK1?=a#*iZgw=-NZBjPZ%;6E}YB8wRJ;$CLJ8)Ea zwb!3oq@0YSb1aR)o}+5ViO_v%j>Em0!L!YIs&kFerUf&eaIL9xjJcPbLXj7eUT2$Z zS#Ss17GxFLEOj^y2xj5aMa~^a1wbz3)K49dPIFF)Ce(W?_7$3AYq@-Dk-26_18k07 zI!<;@zB+v0+U|_eLKM`m_h&BjwD>67oPFbmz?M8#(w95azzHOU-l9=zq{KeYAih?sW*ffLtd|G1vqxYTlTESkIz1`|5D=>i-BN7=hXb!=rcs?29|_s5sFP``(!ox0;~c3G*;gmJ zU?~?yj7k&5Fv5y# z11qN8VzBFMtM4{O8SJJD0HpE=$4AB^$V5h zrUe!dgoRTWRXg;EU6_(|LN^Lz49SoZgB#O|L`<4OU~30h#%?TL2_3k`#5~IM!2rKR zNNNs8Mvo8$1zINl)=gb))QYcF04pt7_^Qtf*c}^3hYZ9Q4MD@g&H{hdEbDt}ia;U2 z1bT6cvIx<7fGS?_5#rShL24vg8XKws#@FiK(Msp(N2PXE0uH1BDoFoPQ#(^=3n0i} z?P{o(75Yo^0HuH{jk*dtxd=LN6hj}dfkYyK8t6-=JiWDGHUtL(Oqf%oe+un`hwC)m z8O3KnAAYADT7@^4Mk7f8Qs+>;gkqs{6(`Lnf6!diMKi82i|Io=chBB-kHRPDEu$b|T8{`{9U&a6sXk-uC9S!AG|J9U$4#Gs_bz545XU#zR<8}y% zA~~$?`>pN-LZw7NtW#4!K>(xxE`d9l#7c|-FUYhw9RW)Q3S5oeB$ePSqDw)~NE#g^ zzNh*2Y9(gYkPR_7^s$y=Z{4cK4i1c?kVq2sMTLMc&Ps?wVyC?kSeE$kDKR#nE+Gaj ztMbygiTZZT4s`7!U&>n-+Mvh14Y&JYWfq?bh zYV=NH#&^_@llC>{-H$M@Uie|UOyB;|8w)^7v=;0o$Yt&Zf8@#n{&b^&-lQcceWBaH z5A)y|MFAXSdO?5#YT^Q*o$6i2?HbA258p6%RGwa!)G}4h7 z$(h#eOaegA;WRqcLz0jwG=jG%B{YIMGOSYxB9@pyR%UjSnBZ6Bf-s0fOkwmJXSRqq z5UHV-(S`T}zAIHB>kY9?&0rwlQ_vrRs-_Lj5hg?dj^F{xh)?RP8VOk@_77YV7`^-q zSu*H7Ab|vcF7E%-`Sm7B)Rppz{O(QTqXFB#NV)5oX~Nfa6+ zcBZB<=u*ZC>X3qIKL|uLPLJ-)Q*2odSHchyub_SOrB%eG&XwA+re;PXd8w ztf4V_#3T_CU{Z4jLY1ZQ0p@3PRCOMPh^>;Ts8@)*9{4R^dZZX4c?`iN}2{^2d~S(7a`BRo+bp8 zhysUr?l(jR8Q$X^7|Y~^D=SBkJl+HDgd@Q(bFpHAot5VNz?fHTO$V#*z(836iACM#?au7BV2&WfnN`RRF#N zbZY@H1#}qkj4H_ls6=eLYHlxXW#9JO(6Jr7htcLCVXkq z%35in)tFuD)cwr;!DCuZ!yw;`&MLl-xj_+-9Ma9PZM8%4}W<>IzD;6 wKAt``pFF(#bV Date: Tue, 17 Oct 2023 04:47:01 +1100 Subject: [PATCH 05/10] Add more streaming statistics --- .../Model/StreamingStatistics.swift | 83 +++++--- Sources/DolbyIORTSCore/State/State.swift | 14 +- .../DolbyIORTSCore/State/StateMachine.swift | 2 +- .../DolbyIORTSCore/StreamOrchestrator.swift | 2 +- .../Views/StatsInfo/StatisticsInfoView.swift | 66 ++++-- .../Views/StatsInfo/StatsInfoViewModel.swift | 193 +++++++++++++++--- .../Resources/en.lproj/Localizable.strings | 33 +++ .../Resources/fr.lproj/Localizable.strings | 4 - .../Views/Components/SelectionsGroup.swift | 15 +- .../Views/Components/SelectionsScreen.swift | 14 +- .../Views/Components/SettingsCell.swift | 11 +- .../DolbyIOUIKit/Views/Components/Text.swift | 7 - 12 files changed, 342 insertions(+), 102 deletions(-) delete mode 100644 Sources/DolbyIOUIKit/Resources/fr.lproj/Localizable.strings diff --git a/Sources/DolbyIORTSCore/Model/StreamingStatistics.swift b/Sources/DolbyIORTSCore/Model/StreamingStatistics.swift index 4d1a7f4..58b2511 100644 --- a/Sources/DolbyIORTSCore/Model/StreamingStatistics.swift +++ b/Sources/DolbyIORTSCore/Model/StreamingStatistics.swift @@ -5,23 +5,27 @@ import Foundation import MillicastSDK -public struct AllStreamingStatistics : Equatable, Hashable { +public struct StreamingStatistics : Equatable, Hashable { public let roundTripTime: Double? - public var videoStatsInboundRtpList: [StatsInboundRtp]? - public var audioStatsInboundRtpList: [StatsInboundRtp]? + public var videoStatsInboundRtp: StatsInboundRtp? + public var audioStatsInboundRtp: StatsInboundRtp? } -public struct StreamingStatistics : Equatable, Hashable { +public struct AllStreamStatistics : Equatable, Hashable { public let roundTripTime: Double? - public let audioStatsInboundRtp: StatsInboundRtp? - public let videoStatsInboundRtp: StatsInboundRtp? + public var videoStatsInboundRtpList: [StatsInboundRtp] + public var audioStatsInboundRtpList: [StatsInboundRtp] } public struct StatsInboundRtp : Equatable, Hashable { - public let sid: String public let kind: String + public let sid: String public let mid: String + public let decoderImplementation: String? + public let trackIdentifier: String public let decoder: String? + public let processingDelay: Double + public let decodeTime: Double public let frameWidth: Int public let frameHeight: Int public let fps: Int @@ -29,13 +33,18 @@ public struct StatsInboundRtp : Equatable, Hashable { public let totalEnergy: Double public let framesReceived: Int public let framesDecoded: Int + public let framesDropped: Int + public let jitterBufferEmittedCount: Int + public let jitterBufferDelay: Double + public let jitterBufferTargetDelay: Double + public let jitterBufferMinimumDelay: Double public let nackCount: Int public let bytesReceived: Int public let totalSampleDuration: Double public let codec: String? public let jitter: Double - public let packetsReceived: Double - public let packetsLost: Double + public let packetsReceived: Int + public let packetsLost: Int public let timestamp: Double public var codecName: String? @@ -44,7 +53,7 @@ public struct StatsInboundRtp : Equatable, Hashable { } } -extension AllStreamingStatistics { +extension AllStreamStatistics { init?(_ report: MCStatsReport) { let receivedType = MCRemoteInboundRtpStreamStats.get_type() guard let remoteInboundStreamStatsList = report.getStatsOf(receivedType) as? [MCRemoteInboundRtpStreamStats] else { @@ -60,50 +69,76 @@ extension AllStreamingStatistics { let codecType = MCCodecsStats.get_type() let codecStatsList = report.getStatsOf(codecType) as? [MCCodecsStats] + videoStatsInboundRtpList = [StatsInboundRtp]() let videos = inboundRtpStreamStatsList .filter { $0.kind == "video" } .map { StatsInboundRtp($0, codecStatsList: codecStatsList) } - videoStatsInboundRtpList = [StatsInboundRtp]() - audioStatsInboundRtpList = [StatsInboundRtp]() - videoStatsInboundRtpList?.append(contentsOf: videos) + videoStatsInboundRtpList.append(contentsOf: videos) + audioStatsInboundRtpList = [StatsInboundRtp]() let audios = inboundRtpStreamStatsList .filter { $0.kind == "audio" } .map { StatsInboundRtp($0, codecStatsList: codecStatsList) } - audioStatsInboundRtpList?.append(contentsOf: audios) + audioStatsInboundRtpList.append(contentsOf: audios) } } extension StatsInboundRtp { init(_ stats: MCInboundRtpStreamStats, codecStatsList: [MCCodecsStats]?) { - sid = stats.sid as String kind = stats.kind as String + sid = stats.sid as String mid = stats.mid as String - decoder = stats.decoder_implementation as String? + decoderImplementation = stats.decoder_implementation as String? frameWidth = Int(stats.frame_width) frameHeight = Int(stats.frame_height) fps = Int(stats.frames_per_second) - audioLevel = Int(stats.audio_level) - totalEnergy = stats.total_audio_energy + bytesReceived = Int(stats.bytes_received) framesReceived = Int(stats.frames_received) + packetsReceived = Int(stats.packets_received) framesDecoded = Int(stats.frames_decoded) + framesDropped = Int(stats.frames_dropped) + jitterBufferEmittedCount = Int(stats.jitter_buffer_emitted_count) + jitter = stats.jitter * 1000 + processingDelay = Self.msNormalised( + numerator: stats.total_processing_delay, + denominator: stats.frames_decoded + ) + decodeTime = Self.msNormalised( + numerator: stats.total_decode_time, + denominator: stats.frames_decoded + ) + jitterBufferDelay = Self.msNormalised( + numerator: stats.jitter_buffer_delay, + denominator: stats.jitter_buffer_emitted_count + ) + jitterBufferTargetDelay = Self.msNormalised( + numerator: stats.jitter_buffer_target_delay, + denominator: stats.jitter_buffer_emitted_count + ) + jitterBufferMinimumDelay = Self.msNormalised( + numerator: stats.jitter_buffer_minimum_delay, + denominator: stats.jitter_buffer_emitted_count + ) nackCount = Int(stats.nack_count) - bytesReceived = Int(stats.bytes_received) + packetsLost = Int(stats.packets_lost) + trackIdentifier = stats.track_identifier as String + decoder = stats.decoder_implementation as String? + audioLevel = Int(stats.audio_level) + totalEnergy = stats.total_audio_energy totalSampleDuration = stats.total_samples_duration codec = stats.codec_id as String? - jitter = stats.jitter - packetsReceived = Double(stats.packets_received) - packetsLost = Double(stats.packets_lost) timestamp = Double(stats.timestamp) if let codecStats = codecStatsList?.first(where: { $0.sid == stats.codec_id }) { codecName = codecStats.mime_type as String - } else { - codecName = nil } } + + private static func msNormalised(numerator: Double, denominator: UInt) -> Double { + denominator == 0 ? 0 : numerator * 1000 / Double(denominator) + } } diff --git a/Sources/DolbyIORTSCore/State/State.swift b/Sources/DolbyIORTSCore/State/State.swift index 2276bf3..7a30897 100644 --- a/Sources/DolbyIORTSCore/State/State.swift +++ b/Sources/DolbyIORTSCore/State/State.swift @@ -48,7 +48,7 @@ struct SubscribedState { private(set) var streamSourceBuilders: [StreamSourceBuilder] private(set) var numberOfStreamViewers: Int - private(set) var streamingStats: AllStreamingStatistics? + private(set) var streamingStats: AllStreamStatistics? private(set) var cachedSourceZeroVideoTrackAndMid: VideoTrackAndMid? private(set) var cachedSourceZeroAudioTrackAndMid: AudioTrackAndMid? @@ -131,15 +131,19 @@ struct SubscribedState { numberOfStreamViewers = count } - mutating func updateStreamingStatistics(_ stats: AllStreamingStatistics) { + mutating func updateStreamingStatistics(_ stats: AllStreamStatistics) { streamingStats = stats - stats.videoStatsInboundRtpList?.forEach { eachVideoStats in + stats.videoStatsInboundRtpList.forEach { videoStats in guard let builder = streamSourceBuilders.first( - where: { $0.videoTrack?.trackInfo.mid == eachVideoStats.mid } + where: { $0.videoTrack?.trackInfo.mid == videoStats.mid } ) else { return } - let sourceStatistics = StreamingStatistics(roundTripTime: stats.roundTripTime, audioStatsInboundRtp: stats.audioStatsInboundRtpList?.first, videoStatsInboundRtp: eachVideoStats) + let sourceStatistics = StreamingStatistics( + roundTripTime: stats.roundTripTime, + videoStatsInboundRtp: videoStats, + audioStatsInboundRtp: stats.audioStatsInboundRtpList.first + ) builder.setStatistics(sourceStatistics) } } diff --git a/Sources/DolbyIORTSCore/State/StateMachine.swift b/Sources/DolbyIORTSCore/State/StateMachine.swift index 3ad56ad..ae09929 100644 --- a/Sources/DolbyIORTSCore/State/StateMachine.swift +++ b/Sources/DolbyIORTSCore/State/StateMachine.swift @@ -185,7 +185,7 @@ final class StateMachine { } } - func onStatsReport(_ streamingStats: AllStreamingStatistics) { + func onStatsReport(_ streamingStats: AllStreamStatistics) { switch currentState { case var .subscribed(state): state.updateStreamingStatistics(streamingStats) diff --git a/Sources/DolbyIORTSCore/StreamOrchestrator.swift b/Sources/DolbyIORTSCore/StreamOrchestrator.swift index e53ee61..f983be5 100644 --- a/Sources/DolbyIORTSCore/StreamOrchestrator.swift +++ b/Sources/DolbyIORTSCore/StreamOrchestrator.swift @@ -332,7 +332,7 @@ extension StreamOrchestrator: SubscriptionManagerDelegate { } nonisolated public func onStatsReport(_ report: MCStatsReport) { - guard let streamingStats = AllStreamingStatistics(report) else { + guard let streamingStats = AllStreamStatistics(report) else { return } Task { @StreamOrchestrator [weak self] in diff --git a/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatisticsInfoView.swift b/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatisticsInfoView.swift index 3695c8d..e4b36b1 100644 --- a/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatisticsInfoView.swift +++ b/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatisticsInfoView.swift @@ -24,34 +24,70 @@ struct StatisticsInfoView: View { private var theme: Theme { themeManager.theme } + + private var pullDownIndicatorView: some View { + RoundedRectangle(cornerRadius: Layout.cornerRadius4x) + .fill(Color.gray) + .frame(width: Layout.spacing6x, height: Layout.spacing1x) + .padding([.top], Layout.spacing0_5x) + } + + private var titleView: some View { + Text("stream.media-stats.label", bundle: .module, font: fontTitle) + .frame(maxWidth: .infinity, alignment: .center) + .padding([.top, .bottom], Layout.spacing3x) + } var body: some View { ScrollView { VStack { - RoundedRectangle(cornerRadius: 3) - .fill(Color.gray) - .frame(width: 48, height: 5) - .padding([.top], 5) - Text("stream.media-stats.label", font: fontTitle) - .frame(maxWidth: .infinity, alignment: .center) - .padding([.top], 20) - .padding([.bottom], 25) + pullDownIndicatorView + + titleView HStack { - Text("stream.stats.name.label", font: fontCaption).frame(width: 170, alignment: .leading) - Text("stream.stats.value.label", font: fontCaption).frame(width: 170, alignment: .leading) + Text("stream.stats.name.label", bundle: .module, font: fontCaption) + .frame(minWidth: Layout.spacing0x, maxWidth: .infinity, alignment: .leading) + + Text("stream.stats.value.label", bundle: .module, font: fontCaption) + .frame(minWidth: Layout.spacing0x, maxWidth: .infinity, alignment: .leading) } ForEach(viewModel.data) { item in HStack { - Text(item.key).font(fontTable).foregroundColor(Color(theme.neutral200)).frame(width: 170, alignment: .leading) - Text(item.value).font(fontTableValue).foregroundColor(Color(theme.onBackground)).frame(width: 170, alignment: .leading) + Text(verbatim: item.key, font: fontTable) + .foregroundColor(Color(theme.neutral200)) + .frame(minWidth: Layout.spacing0x, maxWidth: .infinity, alignment: .leading) + + Text(verbatim: item.value, font: fontTableValue) + .foregroundColor(Color(theme.onBackground)) + .frame(minWidth: Layout.spacing0x, maxWidth: .infinity, alignment: .leading) } - .padding([.top], 5) + .padding([.top], Layout.spacing0_5x) } } - .padding([.leading, .trailing], 15) - .padding([.bottom], 10) + .padding([.leading, .trailing], Layout.spacing2x) + .padding(.bottom, Layout.spacing3x) } + .contextMenu { + Button(action: { + copyToPasteboard(text: formattedStatisticsText()) + }) { + Text("stream.stats.copy.label", bundle: .module) + Image(systemName: "doc.on.doc") + } + } + } + + private func formattedStatisticsText() -> String { + var text = "" + viewModel.data.forEach { item in + text += "\(item.key): \(item.value)\n" + } + return text + } + + private func copyToPasteboard(text: String) { + UIPasteboard.general.string = text } } diff --git a/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatsInfoViewModel.swift b/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatsInfoViewModel.swift index 256955c..45bb5f3 100644 --- a/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatsInfoViewModel.swift +++ b/Sources/DolbyIORTSUIKit/Private/Views/StatsInfo/StatsInfoViewModel.swift @@ -8,7 +8,7 @@ import Foundation final class StatsInfoViewModel: ObservableObject { private let streamSource: StreamSource - + init(streamSource: StreamSource) { self.streamSource = streamSource } @@ -18,43 +18,181 @@ final class StatsInfoViewModel: ObservableObject { var key: String var value: String } - + var data: [StatData] { guard let stats = streamSource.streamingStatistics else { return [] } - + var result = [StatData]() - - if let rtt = stats.roundTripTime { - result.append(StatData(key: String(localized: "stream.stats.rtt.label"), value: String(rtt))) + + if let mid = stats.videoStatsInboundRtp?.mid { + result.append( + StatData( + key: String(localized: "stream.stats.mid.label", bundle: .module), + value: String(mid) + ) + ) + } + if let decoderImplementation = stats.videoStatsInboundRtp?.decoderImplementation { + result.append( + StatData( + key: String(localized: "stream.stats.decoder-impl.label", bundle: .module), + value: String(decoderImplementation) + ) + ) + } + if let processingDelay = stats.videoStatsInboundRtp?.processingDelay { + result.append( + StatData( + key: String(localized: "stream.stats.processing-delay.label", bundle: .module), + value: String(format:"%.2f ms", processingDelay) + ) + ) + } + if let decodeTime = stats.videoStatsInboundRtp?.decodeTime { + result.append( + StatData( + key: String(localized: "stream.stats.decode-time.label", bundle: .module), + value: String(format:"%.2f ms", decodeTime) + ) + ) } if let videoResolution = stats.videoStatsInboundRtp?.videoResolution { - result.append(StatData(key: String(localized: "stream.stats.video-resolution.label"), value: videoResolution)) + result.append( + StatData( + key: String(localized: "stream.stats.video-resolution.label", bundle: .module), + value: videoResolution + ) + ) } if let fps = stats.videoStatsInboundRtp?.fps { - result.append(StatData(key: String(localized: "stream.stats.fps.label"), value: String(fps))) + result.append( + StatData( + key: String(localized: "stream.stats.fps.label", bundle: .module), + value: String(fps) + ) + ) + } + if let videoBytesReceived = stats.videoStatsInboundRtp?.bytesReceived { + result.append( + StatData( + key: String(localized: "stream.stats.video-total-received.label", bundle: .module), + value: formatBytes(bytes: videoBytesReceived) + ) + ) } if let audioBytesReceived = stats.audioStatsInboundRtp?.bytesReceived { - result.append(StatData(key: String(localized: "stream.stats.audio-total-received.label"), value: formatBytes(bytes: audioBytesReceived))) + result.append( + StatData( + key: String(localized: "stream.stats.audio-total-received.label", bundle: .module), + value: formatBytes(bytes: audioBytesReceived) + ) + ) } - if let videoBytesReceived = stats.videoStatsInboundRtp?.bytesReceived { - result.append(StatData(key: String(localized: "stream.stats.video-total-received.label"), value: formatBytes(bytes: videoBytesReceived))) + if let packetsReceived = stats.videoStatsInboundRtp?.packetsReceived { + result.append( + StatData( + key: String(localized: "stream.stats.packets-received.label", bundle: .module), + value: String(packetsReceived) + ) + ) } - if let audioPacketsLost = stats.audioStatsInboundRtp?.packetsLost { - result.append(StatData(key: String(localized: "stream.stats.audio-packet-loss.label"), value: String(audioPacketsLost))) + if let framesDecoded = stats.videoStatsInboundRtp?.framesDecoded { + result.append( + StatData( + key: String(localized: "stream.stats.frames-decoded.label", bundle: .module), + value: String(framesDecoded) + ) + ) } - if let videoPacketsLost = stats.videoStatsInboundRtp?.packetsLost { - result.append(StatData(key: String(localized: "stream.stats.video-packet-loss.label"), value: String(videoPacketsLost))) + if let framesDropped = stats.videoStatsInboundRtp?.framesDropped { + result.append( + StatData( + key: String(localized: "stream.stats.frames-dropped.label", bundle: .module), + value: String(framesDropped) + ) + ) } - if let audioJitter = stats.audioStatsInboundRtp?.jitter { - result.append(StatData(key: String(localized: "stream.stats.audio-jitter.label"), value: "\(Int(audioJitter * 1000)) ms")) + if let jitterBufferEmittedCount = stats.videoStatsInboundRtp?.jitterBufferEmittedCount { + result.append( + StatData( + key: String(localized: "stream.stats.jitter-buffer-est-count.label", bundle: .module), + value: String(jitterBufferEmittedCount) + ) + ) } if let videoJitter = stats.videoStatsInboundRtp?.jitter { - result.append(StatData(key: String(localized: "stream.stats.video-jitter.label"), value: "\(Int(videoJitter * 1000)) ms")) + result.append( + StatData( + key: String(localized: "stream.stats.video-jitter.label", bundle: .module), + value: "\(videoJitter) ms" + ) + ) + } + if let audioJitter = stats.audioStatsInboundRtp?.jitter { + result.append( + StatData( + key: String(localized: "stream.stats.audio-jitter.label", bundle: .module), + value: "\(Int(audioJitter * 1000)) ms" + ) + ) + } + if let jitterBufferDelay = stats.videoStatsInboundRtp?.jitterBufferDelay { + result.append( + StatData( + key: String(localized: "stream.stats.jitter-buffer-delay.label", bundle: .module), + value: String(format:"%.2f ms", jitterBufferDelay) + ) + ) + } + if let jitterBufferTargetDelay = stats.videoStatsInboundRtp?.jitterBufferTargetDelay { + result.append( + StatData( + key: String(localized: "stream.stats.jitter-buffer-target-delay.label", bundle: .module), + value: String(format:"%.2f ms", jitterBufferTargetDelay) + ) + ) + } + if let jitterBufferMinimumDelay = stats.videoStatsInboundRtp?.jitterBufferMinimumDelay { + result.append( + StatData( + key: String(localized: "stream.stats.jitter-buffer-minimum-delay.label", bundle: .module), + value: String(format:"%.2f ms", jitterBufferMinimumDelay) + ) + ) + } + if let videoPacketsLost = stats.videoStatsInboundRtp?.packetsLost { + result.append( + StatData( + key: String(localized: "stream.stats.video-packet-loss.label", bundle: .module), + value: String(videoPacketsLost) + ) + ) + } + if let audioPacketsLost = stats.audioStatsInboundRtp?.packetsLost { + result.append( + StatData( + key: String(localized: "stream.stats.audio-packet-loss.label", bundle: .module), + value: String(audioPacketsLost) + ) + ) + } + if let rtt = stats.roundTripTime { + result.append( + StatData( + key: String(localized: "stream.stats.rtt.label", bundle: .module), + value: String(rtt) + ) + ) } if let timestamp = stats.audioStatsInboundRtp?.timestamp { - result.append(StatData(key: String(localized: "stream.stats.timestamp.label"), value: dateStr(timestamp / 1000))) + result.append( + StatData( + key: String(localized: "stream.stats.timestamp.label", bundle: .module), + value: dateStr(timestamp / 1000) + ) + ) } let audioCodec = stats.audioStatsInboundRtp?.codecName let videoCodec = stats.videoStatsInboundRtp?.codecName @@ -64,30 +202,35 @@ final class StatsInfoViewModel: ObservableObject { delimiter = "" } let codecs = "\(audioCodec ?? "")\(delimiter)\(videoCodec ?? "")" - result.append(StatData(key: String(localized: "stream.stats.codecs.label"), value: codecs)) + result.append( + StatData( + key: String(localized: "stream.stats.codecs.label", bundle: .module), + value: codecs + ) + ) } return result } - + private func dateStr(_ timestamp: Double) -> String { let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(abbreviation: "GMT") dateFormatter.locale = NSLocale.current dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS'Z'" - + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) return dateFormatter.string(from: date) } - + private func formatBytes(bytes: Int) -> String { return "\(formatNumber(input: bytes))B" } - + private func formatBitRate(bitRate: Int) -> String { let value = formatNumber(input: bitRate).lowercased() return "\(value)bps" } - + private func formatNumber(input: Int) -> String { if input < KILOBYTES { return String(input) } if input >= KILOBYTES && input < MEGABYTES { return "\(input / KILOBYTES) K"} else { return "\(input / MEGABYTES) M" } diff --git a/Sources/DolbyIORTSUIKit/Resources/en.lproj/Localizable.strings b/Sources/DolbyIORTSUIKit/Resources/en.lproj/Localizable.strings index f977111..8ade778 100644 --- a/Sources/DolbyIORTSUIKit/Resources/en.lproj/Localizable.strings +++ b/Sources/DolbyIORTSUIKit/Resources/en.lproj/Localizable.strings @@ -63,3 +63,36 @@ Follow video: Switch audio source when a video is selected. If the selected vide "network.disconnected.title.label" = "Disconnected from the network"; "technical-error.title.label" = "We are currently facing some technical difficulties. Please try again later."; + +/** Streaming Statistics */ + +"stream.media-stats.label" = "Streaming statistics"; +"stream.stats.copy.label" = "Copy Stream statistics"; +"stream.stats.name.label" = "Name"; +"stream.stats.value.label" = "Value"; +"stream.stats.mid.label" = "MID"; +"stream.stats.rtt.label" = "RTT"; +"stream.stats.video-resolution.label" = "Video Resolution"; +"stream.stats.fps.label" = "FPS"; +"stream.stats.video-bitrate.label" = "Video Bitrate"; +"stream.stats.audio-bitrate.label" = "Audio Bitrate"; +"stream.stats.video-total-received.label" = "Video Total Received"; +"stream.stats.audio-total-received.label" = "Audio Total Received"; +"stream.stats.video-packet-loss.label" = "Video Packet Loss"; +"stream.stats.audio-packet-loss.label" = "Audio Packet Loss"; +"stream.stats.video-jitter.label" = "Video Jitter"; +"stream.stats.audio-jitter.label" = "Audio Jitter"; +"stream.stats.codecs.label" = "Codecs"; +"stream.stats.timestamp.label" = "Timestamp"; +"stream.stats.server.label" = "Server"; +"stream.stats.cluster.label" = "Cluster"; +"stream.stats.decoder-impl.label" = "Decoder Impl"; +"stream.stats.processing-delay.label" = "Processing delay"; +"stream.stats.decode-time.label" = "Decode time"; +"stream.stats.packets-received.label" = "Packets received"; +"stream.stats.frames-decoded.label" = "Frames decoded"; +"stream.stats.frames-dropped.label" = "Frames dropped"; +"stream.stats.jitter-buffer-est-count.label" = "Jitter buffer Est count"; +"stream.stats.jitter-buffer-delay.label" = "Jitter buffer delay"; +"stream.stats.jitter-buffer-target-delay.label" = "Jitter buffer target delay"; +"stream.stats.jitter-buffer-minimum-delay.label" = "Jitter buffer min delay"; diff --git a/Sources/DolbyIOUIKit/Resources/fr.lproj/Localizable.strings b/Sources/DolbyIOUIKit/Resources/fr.lproj/Localizable.strings deleted file mode 100644 index a4e87a7..0000000 --- a/Sources/DolbyIOUIKit/Resources/fr.lproj/Localizable.strings +++ /dev/null @@ -1,4 +0,0 @@ -"testA.localized.key" = "testA.fr"; -"testB.localized.key" = "testB.fr"; -"testC.localized.key" = "testC.fr"; -"testD.localized.key" = "testD.fr"; diff --git a/Sources/DolbyIOUIKit/Views/Components/SelectionsGroup.swift b/Sources/DolbyIOUIKit/Views/Components/SelectionsGroup.swift index 2a267c3..6050acf 100644 --- a/Sources/DolbyIOUIKit/Views/Components/SelectionsGroup.swift +++ b/Sources/DolbyIOUIKit/Views/Components/SelectionsGroup.swift @@ -84,12 +84,15 @@ struct SelectionsGroup_Previews: PreviewProvider { static var previews: some View { VStack { - SelectionsGroup(settings: [ - .init(key: "testA.localized.key", bundle: .module, selected: true), - .init(key: "testB.localized.key", bundle: .module, selected: false), - .init(key: "testC.localized.key", bundle: .module, selected: false) - ], footer: "testD.localized.key", bundle: .module) { index in - print("index: \(index)") + SelectionsGroup( + settings: [ + .init(key: "Key 1", selected: true), + .init(key: "Key 2", selected: false), + .init(key: "Key 3", selected: false) + ], + footer: "testD.localized.key" + ) { _ in + // No-op } } } diff --git a/Sources/DolbyIOUIKit/Views/Components/SelectionsScreen.swift b/Sources/DolbyIOUIKit/Views/Components/SelectionsScreen.swift index f3f9e3c..e729d6b 100644 --- a/Sources/DolbyIOUIKit/Views/Components/SelectionsScreen.swift +++ b/Sources/DolbyIOUIKit/Views/Components/SelectionsScreen.swift @@ -48,10 +48,14 @@ public struct SelectionsScreen: View { @available(tvOS, unavailable) struct SelectionsScreen_Previews: PreviewProvider { static var previews: some View { - SelectionsScreen(settings: [ - .init(key: "testA.localized.key", bundle: .module, selected: true), - .init(key: "testB.localized.key", bundle: .module, selected: false), - .init(key: "testC.localized.key", bundle: .module, selected: false) - ], footer: "testD.localized.key", footerBundle: .module) + SelectionsScreen( + settings: [ + .init(key: "Key 1", selected: true), + .init(key: "Key 2", selected: false), + .init(key: "Key 3", selected: false) + ], + footer: "testD.localized.key", + footerBundle: .module + ) } } diff --git a/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift b/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift index d9ab22c..d8076fb 100644 --- a/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift +++ b/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift @@ -70,19 +70,12 @@ struct SettingsCell_Previews: PreviewProvider { static var previews: some View { List { Section { - SettingsCell(text: "testA.localized.key", - value: "Connection order", - image: .arrowRight, - bundle: .module, - action: {} - ) - .listRowSeparator(.visible) - SettingsCell(text: "Audio selection", value: "First source", image: .textLink, action: {} - ).listRowSeparator(.visible) + ) + .listRowSeparator(.visible) } .listRowBackground(Color.gray) } diff --git a/Sources/DolbyIOUIKit/Views/Components/Text.swift b/Sources/DolbyIOUIKit/Views/Components/Text.swift index 8ecc805..c9491b0 100644 --- a/Sources/DolbyIOUIKit/Views/Components/Text.swift +++ b/Sources/DolbyIOUIKit/Views/Components/Text.swift @@ -84,13 +84,6 @@ struct Text_Previews: PreviewProvider { static var previews: some View { Group { VStack { - Text( - "testA.localized.key", - bundle: .module, - style: .titleMedium, - font: .custom("AvenirNext-Regular", size: FontSize.title1, relativeTo: .title) - ) - Text( verbatim: "This is a regular text", style: .titleMedium, From 4aec008dee1205d5ca54bb895c065ad6a7797ace Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Tue, 17 Oct 2023 04:52:34 +1100 Subject: [PATCH 06/10] Add Video quality indicator on VideoView when debug features are enabled --- .../VideoRenderer/VideoRendererView.swift | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Sources/DolbyIORTSUIKit/Private/Views/VideoRenderer/VideoRendererView.swift b/Sources/DolbyIORTSUIKit/Private/Views/VideoRenderer/VideoRendererView.swift index 2651d0b..5d8b725 100644 --- a/Sources/DolbyIORTSUIKit/Private/Views/VideoRenderer/VideoRendererView.swift +++ b/Sources/DolbyIORTSUIKit/Private/Views/VideoRenderer/VideoRendererView.swift @@ -16,6 +16,7 @@ struct VideoRendererView: View { @State var isViewVisible = false @ObservedObject private var themeManager = ThemeManager.shared + @AppConfiguration(\.showDebugFeatures) var showDebugFeatures init( viewModel: VideoRendererViewModel, @@ -51,10 +52,27 @@ struct VideoRendererView: View { } @ViewBuilder - private func showLabel(for source: StreamSource) -> some View { + private var sourceLabelView: some View { if viewModel.showSourceLabel { - SourceLabel(sourceId: source.sourceId.displayLabel) - .padding(5) + SourceLabel(sourceId: viewModel.streamSource.sourceId.displayLabel) + .padding(Layout.spacing0_5x) + } else { + EmptyView() + } + } + + @ViewBuilder + private var videoQualityIndicatorView: some View { + if showDebugFeatures, let videoQualityIndicatorText = viewModel.videoQuality.description.first?.uppercased() { + Text( + verbatim: videoQualityIndicatorText, + font: .custom("AvenirNext-Regular", size: FontSize.caption1, relativeTo: .caption) + ) + .foregroundColor(Color(uiColor: themeManager.theme.onPrimary)) + .padding(.horizontal, Layout.spacing1x) + .background(Color(uiColor: themeManager.theme.neutral400)) + .cornerRadius(Layout.cornerRadius4x) + .padding(Layout.spacing0_5x) } else { EmptyView() } @@ -83,7 +101,10 @@ struct VideoRendererView: View { VideoRendererViewInteral(viewRenderer: viewRenderer) .frame(width: videoSize.width, height: videoSize.height) .overlay(alignment: .bottomLeading) { - showLabel(for: viewModel.streamSource) + sourceLabelView + } + .overlay(alignment: .bottomTrailing) { + videoQualityIndicatorView } .overlay { audioPlaybackIndicatorView From 1be8b25b33c141a874fa0dd150ac29fb53f0ec9d Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Tue, 17 Oct 2023 05:01:43 +1100 Subject: [PATCH 07/10] Remove mandatory check on Audio track --- Sources/DolbyIORTSCore/Builder/StreamSourceBuilder.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Sources/DolbyIORTSCore/Builder/StreamSourceBuilder.swift b/Sources/DolbyIORTSCore/Builder/StreamSourceBuilder.swift index 8f5dbcb..76fc820 100644 --- a/Sources/DolbyIORTSCore/Builder/StreamSourceBuilder.swift +++ b/Sources/DolbyIORTSCore/Builder/StreamSourceBuilder.swift @@ -125,10 +125,6 @@ final class StreamSourceBuilder { } func build() throws -> StreamSource { - guard !hasMissingAudioTrack else { - throw BuildError.missingAudioTrack - } - guard !hasMissingVideoTrack, let videoTrack = videoTrack else { throw BuildError.missingVideoTrack } From 23f363459dd99940c738b1822bc50b05c1e2f2e9 Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Tue, 17 Oct 2023 14:19:43 +1100 Subject: [PATCH 08/10] Enable SDK's autoreconnect --- .../Manager/NetworkMonitor.swift | 31 ++++++++ .../Manager/SubscriptionManager.swift | 6 +- .../DolbyIORTSCore/Model/StreamState.swift | 13 +++- .../Model/SubscriptionConfiguration.swift | 8 +- .../DolbyIORTSCore/State/StateMachine.swift | 23 +++--- .../DolbyIORTSCore/StreamOrchestrator.swift | 78 +++++++++---------- 6 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift diff --git a/Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift b/Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift new file mode 100644 index 0000000..65a5210 --- /dev/null +++ b/Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift @@ -0,0 +1,31 @@ +// +// NetworkMonitor.swift +// +// +// Created by Raveendran, Aravind on 22/8/2023. +// + +import Foundation +import Network + +final class NetworkMonitor: ObservableObject { + + private let monitor = NWPathMonitor() + + static let shared = NetworkMonitor() + + @Published var isReachable: Bool = true + private let queue = DispatchQueue(label: "NetworkConnectivityMonitor") + + func startMonitoring() { + monitor.pathUpdateHandler = { [weak self] path in + self?.isReachable = path.status != .unsatisfied + } + + monitor.start(queue: queue) + } + + func stopMonitoring() { + monitor.cancel() + } +} diff --git a/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift b/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift index 172dd29..64e8004 100644 --- a/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift +++ b/Sources/DolbyIORTSCore/Manager/SubscriptionManager.swift @@ -220,9 +220,7 @@ private extension SubscriptionManager { func makeSubscriber(with configuration: SubscriptionConfiguration) -> MCSubscriber? { let subscriber = MCSubscriber.create() - - subscriber?.enableStats(true) - + let options = MCClientOptions() options.autoReconnect = configuration.autoReconnect options.videoJitterMinimumDelayMs = Int32(configuration.videoJitterMinimumDelayInMs) @@ -234,7 +232,7 @@ private extension SubscriptionManager { options.forcePlayoutDelay = configuration.noPlayoutDelay subscriber?.setOptions(options) - subscriber?.enableStats(true) + subscriber?.enableStats(configuration.enableStats) return subscriber } diff --git a/Sources/DolbyIORTSCore/Model/StreamState.swift b/Sources/DolbyIORTSCore/Model/StreamState.swift index e12d10e..b8c68c4 100644 --- a/Sources/DolbyIORTSCore/Model/StreamState.swift +++ b/Sources/DolbyIORTSCore/Model/StreamState.swift @@ -25,10 +25,15 @@ public enum StreamState: Equatable { self = .connected case let .subscribed(state): - self = .subscribed( - sources: state.sources, - numberOfStreamViewers: state.numberOfStreamViewers - ) + let streamSources = state.sources + if !streamSources.isEmpty { + self = .subscribed( + sources: streamSources, + numberOfStreamViewers: state.numberOfStreamViewers + ) + } else { + self = .error(.connectFailed(reason: "")) + } case .stopped: self = .stopped diff --git a/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift b/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift index 9c269ed..cf20f2a 100644 --- a/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift +++ b/Sources/DolbyIORTSCore/Model/SubscriptionConfiguration.swift @@ -7,11 +7,12 @@ import Foundation public struct SubscriptionConfiguration { public enum Constants { public static let useDevelopmentServer = false - public static let autoReconnect = false + public static let autoReconnect = true public static let videoJitterMinimumDelayInMs: UInt = 20 public static let statsDelayMs: UInt = 1000 public static let noPlayoutDelay = false public static let disableAudio = false + public static let enableStats = true } public let useDevelopmentServer: Bool @@ -22,6 +23,7 @@ public struct SubscriptionConfiguration { public let disableAudio: Bool public let rtcEventLogPath: String? public let sdkLogPath: String? + public let enableStats: Bool public init( useDevelopmentServer: Bool = Constants.useDevelopmentServer, @@ -31,7 +33,8 @@ public struct SubscriptionConfiguration { noPlayoutDelay: Bool = Constants.noPlayoutDelay, disableAudio: Bool = Constants.disableAudio, rtcEventLogPath: String? = nil, - sdkLogPath: String? = nil + sdkLogPath: String? = nil, + enableStats: Bool = Constants.enableStats ) { self.useDevelopmentServer = useDevelopmentServer self.autoReconnect = autoReconnect @@ -41,5 +44,6 @@ public struct SubscriptionConfiguration { self.disableAudio = disableAudio self.rtcEventLogPath = rtcEventLogPath self.sdkLogPath = sdkLogPath + self.enableStats = enableStats } } diff --git a/Sources/DolbyIORTSCore/State/StateMachine.swift b/Sources/DolbyIORTSCore/State/StateMachine.swift index ae09929..0fae1fa 100644 --- a/Sources/DolbyIORTSCore/State/StateMachine.swift +++ b/Sources/DolbyIORTSCore/State/StateMachine.swift @@ -71,6 +71,9 @@ final class StateMachine { } func onSubscribed() { + if case .subscribed = currentState { + return + } currentState = .subscribed( .init( cachedVideoTrackDetail: cachedSourceZeroVideoTrackAndMid, @@ -79,7 +82,6 @@ final class StateMachine { ) cachedSourceZeroAudioTrackAndMid = nil cachedSourceZeroAudioTrackAndMid = nil - } func onSubscribedError(_ reason: String) { @@ -91,10 +93,17 @@ final class StateMachine { } func onActive(_ streamId: String, tracks: [String], sourceId: String?) { + // This is a workaround for an SDK behaviour where the some `onActive` callbacks arrive even before the `onSubscribed` + // In this case it's safe to assume a state change to `.subscribed` provided the current state is `.subscribing` + if case .subscribing = currentState { + // Mimic an `onSubscribed` callback + onSubscribed() + } + switch currentState { case var .subscribed(state): state.add(streamId: streamId, sourceId: sourceId, tracks: tracks) - currentState = .subscribed(state) + self.currentState = .subscribed(state) default: Self.logger.error("🛑 Unexpected state on onActive - \(self.currentState.description)") } @@ -104,15 +113,7 @@ final class StateMachine { switch currentState { case var .subscribed(state): state.remove(streamId: streamId, sourceId: sourceId) - - // FIXME: Currently SDK does not have a callback for Publisher stopping the publishing - // What we get instead is `onInactive` callbacks for all the video sources - ie, `onInactive` is called `n` times if we have `n` sources - // This workaround checks for active `source` count to decide the expected `state` transition - if state.sources.isEmpty { - currentState = .stopped - } else { - currentState = .subscribed(state) - } + currentState = .subscribed(state) default: Self.logger.error("🛑 Unexpected state on onInactive - \(self.currentState.description)") } diff --git a/Sources/DolbyIORTSCore/StreamOrchestrator.swift b/Sources/DolbyIORTSCore/StreamOrchestrator.swift index f983be5..b19eb43 100644 --- a/Sources/DolbyIORTSCore/StreamOrchestrator.swift +++ b/Sources/DolbyIORTSCore/StreamOrchestrator.swift @@ -21,7 +21,7 @@ public final actor StreamOrchestrator { private let stateMachine: StateMachine = StateMachine(initialState: .disconnected) private let subscriptionManager: SubscriptionManagerProtocol private let rendererRegistry: RendererRegistryProtocol - private let taskScheduler: TaskSchedulerProtocol + private let networkMonitor: NetworkMonitor private var subscriptions: Set = [] private lazy var stateSubject: CurrentValueSubject = CurrentValueSubject(.disconnected) @@ -34,19 +34,19 @@ public final actor StreamOrchestrator { private init() { self.init( subscriptionManager: SubscriptionManager(), - taskScheduler: TaskScheduler(), - rendererRegistry: RendererRegistry() + rendererRegistry: RendererRegistry(), + networkMonitor: .shared ) } init( subscriptionManager: SubscriptionManagerProtocol, - taskScheduler: TaskSchedulerProtocol, - rendererRegistry: RendererRegistryProtocol + rendererRegistry: RendererRegistryProtocol, + networkMonitor: NetworkMonitor ) { self.subscriptionManager = subscriptionManager - self.taskScheduler = taskScheduler self.rendererRegistry = rendererRegistry + self.networkMonitor = networkMonitor self.subscriptionManager.delegate = self @@ -61,6 +61,8 @@ public final actor StreamOrchestrator { public func connect(streamName: String, accountID: String, configuration: SubscriptionConfiguration = .init()) async -> Bool { Self.logger.debug("👮‍♂️ Start subscribe") logHandler.setLogFilePath(filePath: configuration.sdkLogPath) + networkMonitor.startMonitoring() + async let startConnectionStateUpdate: Void = stateMachine.startConnection(streamName: streamName, accountID: accountID) async let startConnection = subscriptionManager.connect(streamName: streamName, accountID: accountID, configuration: configuration) @@ -75,8 +77,8 @@ public final actor StreamOrchestrator { public func stopConnection() async -> Bool { Self.logger.debug("👮‍♂️ Stop subscribe") - activeStreamDetail = nil - logHandler.setLogFilePath(filePath: nil) + reset() + async let stopSubscribeOnStateMachine: Void = stateMachine.stopSubscribe() async let resetRegistry: Void = rendererRegistry.reset() async let stopSubscription: Bool = await subscriptionManager.stopSubscribe() @@ -200,28 +202,29 @@ public final actor StreamOrchestrator { // MARK: Private helper methods private extension StreamOrchestrator { + func startNetworkObserver() { + networkMonitor.$isReachable + .removeDuplicates() + .filter { $0 == true } + .sink { _ in + Task { @StreamOrchestrator [weak self] in + guard let self = self, let streamDetail = await self.activeStreamDetail else { return } + + switch await self.stateSubject.value { + case .error(StreamError.connectFailed(reason: _)), .stopped: + await self.reconnectToStream(streamDetail: streamDetail) + default: break + } + } + } + .store(in: &subscriptions) + } + func startStateObservation() { stateMachine.statePublisher .sink { state in Task { [weak self] in guard let self = self else { return } - switch state { - case let .error(errorState): - switch errorState.error { - case .connectFailed: - await self.scheduleReconnection() - default: - //No-op - break - } - case .stopped: - await self.scheduleReconnection() - - default: - // No-op - break - } - // Populate updates public facing states await self.stateSubject.send(StreamState(state: state)) } @@ -229,22 +232,9 @@ private extension StreamOrchestrator { .store(in: &subscriptions) } - func scheduleReconnection() { - Self.logger.debug("👮‍♂️ Scheduling a reconnect") - taskScheduler.scheduleTask(timeInterval: Defaults.retryConnectionTimeInterval) { [weak self] in - guard let self = self else { return } - Task { - guard let streamDetail = await self.activeStreamDetail else { - return - } - - self.taskScheduler.invalidate() - _ = await self.connect( - streamName: streamDetail.streamName, - accountID: streamDetail.accountID - ) - } - } + func reconnectToStream(streamDetail: StreamDetail) async { + Self.logger.debug("👮‍♂️ Attempting a reconnect") + _ = await connect(streamName: streamDetail.streamName, accountID: streamDetail.accountID) } func startSubscribe() async -> Bool { @@ -261,6 +251,12 @@ private extension StreamOrchestrator { default: break } } + + func reset() { + activeStreamDetail = nil + logHandler.setLogFilePath(filePath: nil) + networkMonitor.startMonitoring() + } } // MARK: SubscriptionManagerDelegate implementation From 207d87440dc58ea37e01602fb9d462b7f8f81ba4 Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Tue, 17 Oct 2023 15:01:05 +1100 Subject: [PATCH 09/10] SettingsScreen updates to pass in an added ViewBuilder's in-order to accomodate more settings options --- .../Screens/Settings/SettingsScreen.swift | 18 +++++++++++++++--- .../Views/Components/SettingsCell.swift | 4 ++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/DolbyIORTSUIKit/Public/Screens/Settings/SettingsScreen.swift b/Sources/DolbyIORTSUIKit/Public/Screens/Settings/SettingsScreen.swift index fc4a0ba..341f83e 100644 --- a/Sources/DolbyIORTSUIKit/Public/Screens/Settings/SettingsScreen.swift +++ b/Sources/DolbyIORTSUIKit/Public/Screens/Settings/SettingsScreen.swift @@ -5,7 +5,7 @@ import SwiftUI import DolbyIOUIKit -public struct SettingsScreen: View { +public struct SettingsScreen: View { @Environment(\.presentationMode) var presentationMode @StateObject private var viewModel: SettingsViewModel @@ -14,8 +14,17 @@ public struct SettingsScreen: View { @State private var isShowingStreamSortOrderScreen: Bool = false @State private var isShowingAudioSelectionScreen: Bool = false - public init(mode: SettingsMode) { + @ViewBuilder private let moreSettings: Content + + public init(mode: SettingsMode, @ViewBuilder moreSettings: () -> Content) { _viewModel = StateObject(wrappedValue: SettingsViewModel(mode: mode)) + self.moreSettings = moreSettings() + } + + public init(mode: SettingsMode) where Content == EmptyView { + self.init(mode: mode) { + EmptyView() + } } public var body: some View { @@ -42,6 +51,8 @@ public struct SettingsScreen: View { .hidden() List { + moreSettings + Toggle(isOn: Binding( get: { viewModel.showSourceLabels }, set: { viewModel.setShowSourceLabels($0) }) @@ -50,7 +61,7 @@ public struct SettingsScreen: View { "settings.show-source-labels.label", bundle: .module, style: .titleMedium, - font: .custom("AvenirNext-Regular", size: CGFloat(14.0), relativeTo: .body) + font: .custom("AvenirNext-Regular", size: FontSize.body) ) } @@ -79,6 +90,7 @@ public struct SettingsScreen: View { action: { isShowingAudioSelectionScreen = true } ) } + .environment(\.defaultMinListRowHeight, Layout.spacing6x) .navigationBarBackButtonHidden() .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift b/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift index d8076fb..3c4bddd 100644 --- a/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift +++ b/Sources/DolbyIOUIKit/Views/Components/SettingsCell.swift @@ -40,7 +40,7 @@ public struct SettingsCell: View { Text( text, bundle: bundle, - font: .custom("AvenirNext-Regular", size: CGFloat(14.0), relativeTo: .body), + font: .custom("AvenirNext-Regular", size: FontSize.body), textColor: textColor ) } @@ -51,7 +51,7 @@ public struct SettingsCell: View { Text( value, bundle: bundle, - font: .custom("AvenirNext-Regular", size: CGFloat(14.0), relativeTo: .body), + font: .custom("AvenirNext-Regular", size: FontSize.body), textColor: valueColor ) } From d301e72417297001866c7cc949ab23dccdee7c3b Mon Sep 17 00:00:00 2001 From: Aravind Raveendran Date: Tue, 17 Oct 2023 15:31:49 +1100 Subject: [PATCH 10/10] Remove app side network monitoring --- .../Manager/NetworkMonitor.swift | 31 ------------------- .../DolbyIORTSCore/StreamOrchestrator.swift | 28 ++--------------- 2 files changed, 2 insertions(+), 57 deletions(-) delete mode 100644 Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift diff --git a/Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift b/Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift deleted file mode 100644 index 65a5210..0000000 --- a/Sources/DolbyIORTSCore/Manager/NetworkMonitor.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// NetworkMonitor.swift -// -// -// Created by Raveendran, Aravind on 22/8/2023. -// - -import Foundation -import Network - -final class NetworkMonitor: ObservableObject { - - private let monitor = NWPathMonitor() - - static let shared = NetworkMonitor() - - @Published var isReachable: Bool = true - private let queue = DispatchQueue(label: "NetworkConnectivityMonitor") - - func startMonitoring() { - monitor.pathUpdateHandler = { [weak self] path in - self?.isReachable = path.status != .unsatisfied - } - - monitor.start(queue: queue) - } - - func stopMonitoring() { - monitor.cancel() - } -} diff --git a/Sources/DolbyIORTSCore/StreamOrchestrator.swift b/Sources/DolbyIORTSCore/StreamOrchestrator.swift index b19eb43..8438181 100644 --- a/Sources/DolbyIORTSCore/StreamOrchestrator.swift +++ b/Sources/DolbyIORTSCore/StreamOrchestrator.swift @@ -21,7 +21,6 @@ public final actor StreamOrchestrator { private let stateMachine: StateMachine = StateMachine(initialState: .disconnected) private let subscriptionManager: SubscriptionManagerProtocol private let rendererRegistry: RendererRegistryProtocol - private let networkMonitor: NetworkMonitor private var subscriptions: Set = [] private lazy var stateSubject: CurrentValueSubject = CurrentValueSubject(.disconnected) @@ -34,19 +33,16 @@ public final actor StreamOrchestrator { private init() { self.init( subscriptionManager: SubscriptionManager(), - rendererRegistry: RendererRegistry(), - networkMonitor: .shared + rendererRegistry: RendererRegistry() ) } init( subscriptionManager: SubscriptionManagerProtocol, - rendererRegistry: RendererRegistryProtocol, - networkMonitor: NetworkMonitor + rendererRegistry: RendererRegistryProtocol ) { self.subscriptionManager = subscriptionManager self.rendererRegistry = rendererRegistry - self.networkMonitor = networkMonitor self.subscriptionManager.delegate = self @@ -61,7 +57,6 @@ public final actor StreamOrchestrator { public func connect(streamName: String, accountID: String, configuration: SubscriptionConfiguration = .init()) async -> Bool { Self.logger.debug("👮‍♂️ Start subscribe") logHandler.setLogFilePath(filePath: configuration.sdkLogPath) - networkMonitor.startMonitoring() async let startConnectionStateUpdate: Void = stateMachine.startConnection(streamName: streamName, accountID: accountID) async let startConnection = subscriptionManager.connect(streamName: streamName, accountID: accountID, configuration: configuration) @@ -202,24 +197,6 @@ public final actor StreamOrchestrator { // MARK: Private helper methods private extension StreamOrchestrator { - func startNetworkObserver() { - networkMonitor.$isReachable - .removeDuplicates() - .filter { $0 == true } - .sink { _ in - Task { @StreamOrchestrator [weak self] in - guard let self = self, let streamDetail = await self.activeStreamDetail else { return } - - switch await self.stateSubject.value { - case .error(StreamError.connectFailed(reason: _)), .stopped: - await self.reconnectToStream(streamDetail: streamDetail) - default: break - } - } - } - .store(in: &subscriptions) - } - func startStateObservation() { stateMachine.statePublisher .sink { state in @@ -255,7 +232,6 @@ private extension StreamOrchestrator { func reset() { activeStreamDetail = nil logHandler.setLogFilePath(filePath: nil) - networkMonitor.startMonitoring() } }