diff --git a/.DS_Store b/.DS_Store index 0f0324f9..5037a2d3 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/LocalPackages/RTSCore/Sources/RTSCore/Manager/SubscriptionManager.swift b/LocalPackages/RTSCore/Sources/RTSCore/Manager/SubscriptionManager.swift index 1d413dea..34b19268 100644 --- a/LocalPackages/RTSCore/Sources/RTSCore/Manager/SubscriptionManager.swift +++ b/LocalPackages/RTSCore/Sources/RTSCore/Manager/SubscriptionManager.swift @@ -80,6 +80,9 @@ public actor SubscriptionManager: ObservableObject { let clientOptions = MCClientOptions() clientOptions.jitterMinimumDelayMs = Int32(configuration.jitterMinimumDelayMs) clientOptions.statsDelayMs = Int32(configuration.statsDelayMs) + if configuration.maxBitrate > 0 { + clientOptions.maximumBitrate = NSNumber(value: configuration.maxBitrate) + } if let rtcEventLogOutputPath = configuration.rtcEventLogPath { clientOptions.rtcEventLogOutputPath = rtcEventLogOutputPath } diff --git a/LocalPackages/RTSCore/Sources/RTSCore/Models/SubscriptionConfiguration.swift b/LocalPackages/RTSCore/Sources/RTSCore/Models/SubscriptionConfiguration.swift index 63e83a4c..8abf57e8 100644 --- a/LocalPackages/RTSCore/Sources/RTSCore/Models/SubscriptionConfiguration.swift +++ b/LocalPackages/RTSCore/Sources/RTSCore/Models/SubscriptionConfiguration.swift @@ -10,6 +10,7 @@ public struct SubscriptionConfiguration { public static let autoReconnect = true public static let jitterMinimumDelayMs: UInt = 20 public static let statsDelayMs: UInt = 1000 + public static let maxBitrate: UInt = 0 public static let disableAudio = false public static let enableStats = true public static let playoutDelay: MCForcePlayoutDelay? = nil @@ -21,6 +22,7 @@ public struct SubscriptionConfiguration { public let autoReconnect: Bool public let jitterMinimumDelayMs: UInt public let statsDelayMs: UInt + public let maxBitrate: UInt public let disableAudio: Bool public let rtcEventLogPath: String? public let sdkLogPath: String? @@ -32,6 +34,7 @@ public struct SubscriptionConfiguration { autoReconnect: Bool = Constants.autoReconnect, jitterMinimumDelayMs: UInt = Constants.jitterMinimumDelayMs, statsDelayMs: UInt = Constants.statsDelayMs, + maxBitrate: UInt = Constants.maxBitrate, disableAudio: Bool = Constants.disableAudio, rtcEventLogPath: String? = nil, sdkLogPath: String? = nil, @@ -42,6 +45,7 @@ public struct SubscriptionConfiguration { self.autoReconnect = autoReconnect self.jitterMinimumDelayMs = jitterMinimumDelayMs self.statsDelayMs = statsDelayMs + self.maxBitrate = maxBitrate self.disableAudio = disableAudio self.rtcEventLogPath = rtcEventLogPath self.sdkLogPath = sdkLogPath diff --git a/interactive-player/.gitignore b/interactive-player/.gitignore index b8fcc057..8e36cb12 100644 --- a/interactive-player/.gitignore +++ b/interactive-player/.gitignore @@ -1,6 +1,7 @@ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +.DS_Store ## User settings xcuserdata/ diff --git a/interactive-player/Interactive Viewer/Localizable.strings b/interactive-player/Interactive Viewer/Localizable.strings index f4b655f2..e7e79965 100644 --- a/interactive-player/Interactive Viewer/Localizable.strings +++ b/interactive-player/Interactive Viewer/Localizable.strings @@ -25,6 +25,8 @@ "stream-detail-input.minimum-playout-delay-placeholder-label" = "Minimum Playout Delay"; "stream-detail-input.maximum-playout-delay-placeholder-label" = "Maximum Playout Delay"; "stream-detail-input.primary-video-quality-label" = "Primary video quality:"; +"stream-detail-input.set-max-bitrate-label" = "Set Maximum Bitrate"; +"stream-detail-input.max-bitrate-label" = "Maxium Bitrate (kbps):"; "stream-detail-server-url-label" = "Server URL"; /** App configuration Screen */ @@ -42,8 +44,9 @@ "recent-streams.account-id.title.label" = "ID:"; "recent-streams.server-url.label" = "Server URL:"; "recent-streams.video-jitter-buffer.label" = "Video Jitter buffer in ms:"; -"recent-streams.min-playout-delay.label" = "Min Playout delay:"; -"recent-streams.max-playout-delay.label" = "Max Playout delay:"; +"recent-streams.min-playout-delay.label" = "Min Playout Delay:"; +"recent-streams.max-playout-delay.label" = "Max Playout Delay:"; +"recent-streams.max-bitrate.label" = "Maxium Bitrate:"; "recent-streams.disable-audio.label" = "Disable Audio:"; "recent-streams.primary-video-quality.label" = "Primary video quality:"; "recent-streams.save-logs.label" = "Save Logs:"; diff --git a/interactive-player/Interactive Viewer/Managers/Persistence/RTSViewer.xcdatamodeld/RTSViewer.xcdatamodel/contents b/interactive-player/Interactive Viewer/Managers/Persistence/RTSViewer.xcdatamodeld/RTSViewer.xcdatamodel/contents index 6c955cde..577c5ddb 100644 --- a/interactive-player/Interactive Viewer/Managers/Persistence/RTSViewer.xcdatamodeld/RTSViewer.xcdatamodel/contents +++ b/interactive-player/Interactive Viewer/Managers/Persistence/RTSViewer.xcdatamodeld/RTSViewer.xcdatamodel/contents @@ -1,9 +1,10 @@ - + + diff --git a/interactive-player/Interactive Viewer/Managers/Persistence/StreamDataManager.swift b/interactive-player/Interactive Viewer/Managers/Persistence/StreamDataManager.swift index 8c351dea..8d463d55 100644 --- a/interactive-player/Interactive Viewer/Managers/Persistence/StreamDataManager.swift +++ b/interactive-player/Interactive Viewer/Managers/Persistence/StreamDataManager.swift @@ -17,7 +17,6 @@ protocol StreamDataManagerProtocol: AnyObject { } final class StreamDataManager: NSObject, StreamDataManagerProtocol { - enum StreamDataManagerType { case `default`, testing } @@ -55,10 +54,10 @@ final class StreamDataManager: NSObject, StreamDataManagerProtocol { } self.dateProvider = dateProvider - streamDetailFetchResultsController = NSFetchedResultsController(fetchRequest: Self.recentStreamsFetchRequest, - managedObjectContext: managedObjectContext, - sectionNameKeyPath: nil, - cacheName: nil) + self.streamDetailFetchResultsController = NSFetchedResultsController(fetchRequest: Self.recentStreamsFetchRequest, + managedObjectContext: managedObjectContext, + sectionNameKeyPath: nil, + cacheName: nil) super.init() @@ -139,13 +138,14 @@ final class StreamDataManager: NSObject, StreamDataManagerProtocol { streamDetailToSave.maxPlayoutDelay = streamDetail.maxPlayoutDelay.map { NSNumber(value: $0) } streamDetailToSave.disableAudio = streamDetail.disableAudio streamDetailToSave.primaryVideoQuality = streamDetail.primaryVideoQuality.rawValue + streamDetailToSave.maxBitrate = streamDetail.maxBitrate.map { NSNumber(value: $0) } streamDetailToSave.saveLogs = streamDetail.saveLogs // Delete streams that are older and exceeding the maximum allowed count let request: NSFetchRequest = Self.recentStreamsFetchRequest let updatedResults = try coreDataManager.context.fetch(request) if updatedResults.count > Constants.maximumAllowedStreams { - let streamsToDelete = updatedResults[(Constants.maximumAllowedStreams)..) { if let newStreamDetails = controller.fetchedObjects as? [StreamDetailManagedObject] { let streamDetails = newStreamDetails.compactMap { SavedStreamDetail(managedObject: $0) } diff --git a/interactive-player/Interactive Viewer/Managers/VideoTracksManager.swift b/interactive-player/Interactive Viewer/Managers/VideoTracksManager.swift index 0128eeaf..f3d75168 100644 --- a/interactive-player/Interactive Viewer/Managers/VideoTracksManager.swift +++ b/interactive-player/Interactive Viewer/Managers/VideoTracksManager.swift @@ -2,14 +2,13 @@ // VideoTracksManager.swift // -import Foundation import Combine -import RTSCore +import Foundation import MillicastSDK import os +import RTSCore final actor VideoTracksManager { - typealias ViewID = String private static let logger = Logger( @@ -36,13 +35,14 @@ final actor VideoTracksManager { private var sourceToSimulcastLayersMapping: [SourceID: [MCRTSRemoteTrackLayer]] = [:] private var sourceToSelectedVideoQualityAndLayerMapping: [SourceID: VideoQualityAndLayerPair] = [:] { didSet { - let sourceToVideoQuality = sourceToSelectedVideoQualityAndLayerMapping - .mapValues({ + let sourceToVideoQuality = self.sourceToSelectedVideoQualityAndLayerMapping + .mapValues { $0.videoQuality - }) - videoQualitySubject.send(sourceToVideoQuality) + } + self.videoQualitySubject.send(sourceToVideoQuality) } } + private var layerEventsObservationDictionary: [SourceID: Task] = [:] private var sourceToTasks: [SourceID: SerialTasks] = [:] private let videoQualitySubject: CurrentValueSubject<[SourceID: VideoQuality], Never> = CurrentValueSubject([:]) @@ -56,7 +56,10 @@ final actor VideoTracksManager { func observeLayerUpdates(for source: StreamSource) { Task { [weak self] in - guard let self, await self.layerEventsObservationDictionary[source.sourceId] == nil else { + guard + let self, + await self.layerEventsObservationDictionary[source.sourceId] == nil + else { return } let layerEventsObservationTask = Task { @@ -73,12 +76,12 @@ final actor VideoTracksManager { } func reset() { - sourceToTasks.removeAll() - layerEventsObservationDictionary.removeAll() - sourceToActiveViewsMapping.removeAll() - viewToRequestedVideoQualityMapping.removeAll() - sourceToSimulcastLayersMapping.removeAll() - sourceToSelectedVideoQualityAndLayerMapping.removeAll() + self.sourceToTasks.removeAll() + self.layerEventsObservationDictionary.removeAll() + self.sourceToActiveViewsMapping.removeAll() + self.viewToRequestedVideoQualityMapping.removeAll() + self.sourceToSimulcastLayersMapping.removeAll() + self.sourceToSelectedVideoQualityAndLayerMapping.removeAll() } func enableTrack(for source: StreamSource, with preferredVideoQuality: VideoQuality, on view: ViewID) async { @@ -86,19 +89,19 @@ final actor VideoTracksManager { Self.logger.debug("♼ Request to enable video track for source \(sourceId) with preferredVideoQuality \(preferredVideoQuality.displayText) from view \(view.description)") // If the view has already requested the same video quality before? if yes, exit - guard viewToRequestedVideoQualityMapping[view] != preferredVideoQuality else { + guard self.viewToRequestedVideoQualityMapping[view] != preferredVideoQuality else { Self.logger.debug("♼ Exiting - View already presented for source \(sourceId) with preferredVideoQuality \(preferredVideoQuality.displayText)") return } - let activeViewsForSource = sourceToActiveViewsMapping[sourceId] ?? [] + let activeViewsForSource = self.sourceToActiveViewsMapping[sourceId] ?? [] // Calculate the video quality to project from the requested list // Note: Only one layer can be selected for a source at a given time var videoQualitiesRequestedForSource = activeViewsForSource - .compactMap { viewToRequestedVideoQualityMapping[$0] } + .compactMap { self.viewToRequestedVideoQualityMapping[$0] } videoQualitiesRequestedForSource.append(preferredVideoQuality) let bestVideoQualityFromTheList = videoQualitiesRequestedForSource.bestVideoQualityFromTheRequestedList - let simulcastLayers = sourceToSimulcastLayersMapping[sourceId] + let simulcastLayers = self.sourceToSimulcastLayersMapping[sourceId] let layerToSelect = simulcastLayers.map { $0.matching(quality: bestVideoQualityFromTheList) } ?? nil Self.logger.debug("♼ Source \(sourceId) has \(simulcastLayers?.count ?? 0) simulcast layers") @@ -107,29 +110,29 @@ final actor VideoTracksManager { Self.logger.debug("♼ Add active view \(view.description) for source \(sourceId)") // Update view's requested video quality - viewToRequestedVideoQualityMapping[view] = preferredVideoQuality + self.viewToRequestedVideoQualityMapping[view] = preferredVideoQuality // Add new view to the list of active views for that source if var views = sourceToActiveViewsMapping[sourceId] { views.append(view) - sourceToActiveViewsMapping[source.sourceId] = views + self.sourceToActiveViewsMapping[source.sourceId] = views } else { - sourceToActiveViewsMapping[source.sourceId] = [view] + self.sourceToActiveViewsMapping[source.sourceId] = [view] } do { if let layerToSelect { Self.logger.debug("♼ Simulcast layer - \(layerToSelect) for source \(sourceId)") Self.logger.debug("♼ Selecting videoquality \(videoQualityToSelect.displayText) for source \(sourceId) on view \(view)") - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair - try await queueEnableTrack(for: source, layer: MCRTSRemoteVideoTrackLayer(layer: layerToSelect)) + try await self.queueEnableTrack(for: source, layer: MCRTSRemoteVideoTrackLayer(layer: layerToSelect)) } else { Self.logger.debug("♼ No simulcast layer for source \(sourceId) matching \(bestVideoQualityFromTheList.displayText)") Self.logger.debug("♼ Selecting videoquality 'Auto' for source \(sourceId) on view \(view)") - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair - try await queueEnableTrack(for: source) + try await self.queueEnableTrack(for: source) } } catch { Self.logger.debug("♼🛑 Enabling video track threw error \(error.localizedDescription)") @@ -141,20 +144,21 @@ final actor VideoTracksManager { Self.logger.debug("♼ Request to disable video track for source \(sourceId) on view \(view.description)") Self.logger.debug("♼ Remove view \(view.description) for source \(sourceId)") // Remove view from the list of active views for that source - if var activeViews = sourceToActiveViewsMapping[sourceId], activeViews.contains(where: { $0 == view }) { + if var activeViews = sourceToActiveViewsMapping[sourceId], + activeViews.contains(where: { $0 == view }) { activeViews.removeAll(where: { $0 == view }) - sourceToActiveViewsMapping[source.sourceId] = !activeViews.isEmpty ? activeViews : nil + self.sourceToActiveViewsMapping[source.sourceId] = !activeViews.isEmpty ? activeViews : nil } // Remove view from View to requested video quality mapping - viewToRequestedVideoQualityMapping[view] = nil + self.viewToRequestedVideoQualityMapping[view] = nil if let activeViews = sourceToActiveViewsMapping[sourceId] { // Calculate the video quality to project from the requested list // Note: Only one projection can exist for a source at a given time - let videoQualitiesRequestedForSource = activeViews.compactMap { viewToRequestedVideoQualityMapping[$0] } + let videoQualitiesRequestedForSource = activeViews.compactMap { self.viewToRequestedVideoQualityMapping[$0] } let bestVideoQualityFromRequested = videoQualitiesRequestedForSource.bestVideoQualityFromTheRequestedList - let simulcastLayers = sourceToSimulcastLayersMapping[sourceId] + let simulcastLayers = self.sourceToSimulcastLayersMapping[sourceId] let layerToSelect = simulcastLayers.map { $0.matching(quality: bestVideoQualityFromRequested) } ?? nil let selectedVideoQuality: VideoQuality = layerToSelect == nil ? .auto : bestVideoQualityFromRequested @@ -164,13 +168,13 @@ final actor VideoTracksManager { if let layerToSelect { Self.logger.debug("♼ Has simulcast layer - \(layerToSelect) for source \(sourceId)") Self.logger.debug("♼ Selecting videoquality \(selectedVideoQuality.displayText) for source \(sourceId); active view \(activeViews)") - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair - try await queueEnableTrack(for: source, layer: MCRTSRemoteVideoTrackLayer(layer: layerToSelect)) + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair + try await self.queueEnableTrack(for: source, layer: MCRTSRemoteVideoTrackLayer(layer: layerToSelect)) } else { Self.logger.debug("♼ No simulcast layer for source \(sourceId) matching \(bestVideoQualityFromRequested.displayText)") Self.logger.debug("♼ Selecting videoquality 'Auto' for source \(sourceId); active view \(activeViews)") - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair - try await queueEnableTrack(for: source) + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair + try await self.queueEnableTrack(for: source) } } catch { Self.logger.debug("♼🛑 Enabling video track threw error \(error.localizedDescription)") @@ -180,11 +184,11 @@ final actor VideoTracksManager { Self.logger.debug("♼ Disable video track for source \(sourceId) as there are no active views") // Remove selected Video quality for source - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = nil + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = nil removeAllStoredData(for: source) - try await queueDisableTrack(for: source) + try await self.queueDisableTrack(for: source) } catch { Self.logger.debug("♼🛑 Disabling video track threw error \(error.localizedDescription)") } @@ -192,12 +196,13 @@ final actor VideoTracksManager { } func queueEnableTrack(for source: StreamSource, layer: MCRTSRemoteVideoTrackLayer? = nil) async throws { - if sourceToTasks[source.sourceId] == nil { - sourceToTasks[source.sourceId] = SerialTasks() + if self.sourceToTasks[source.sourceId] == nil { + self.sourceToTasks[source.sourceId] = SerialTasks() } - let renderer = rendererRegistry.sampleBufferRenderer(for: source).underlyingRenderer - guard let serialTasks = sourceToTasks[source.sourceId], source.videoTrack.isActive else { return } + let renderer = self.rendererRegistry.sampleBufferRenderer(for: source).underlyingRenderer + guard let serialTasks = sourceToTasks[source.sourceId], + source.videoTrack.isActive else { return } try await serialTasks.enqueue { Self.logger.debug("♼ Queue: Enabling track for source \(source.sourceId) on renderer \(ObjectIdentifier(renderer).debugDescription)") guard !Task.isCancelled, source.videoTrack.isActive else { return } @@ -225,12 +230,14 @@ final actor VideoTracksManager { private extension VideoTracksManager { func addSimulcastLayers(_ layers: [MCRTSRemoteTrackLayer], for source: StreamSource) async { - sourceToSimulcastLayersMapping[source.sourceId] = layers + self.sourceToSimulcastLayersMapping[source.sourceId] = layers Self.logger.debug("♼ Add layers \(layers.count) for \(source.sourceId)") let sourceId = source.sourceId // Choose any active view to reenable the track - guard let activeViews = sourceToActiveViewsMapping[sourceId], let anyActiveView = activeViews.first else { + guard let activeViews = sourceToActiveViewsMapping[sourceId], + let anyActiveView = activeViews.first + else { Self.logger.debug("♼ No active views for \(source.sourceId)") return } @@ -238,14 +245,14 @@ private extension VideoTracksManager { // Calculate the video quality to project from the requested list // Note: Only one projection can exist for a source at a given time let videoQualitiesRequestedForSource = activeViews - .compactMap { viewToRequestedVideoQualityMapping[$0] } + .compactMap { self.viewToRequestedVideoQualityMapping[$0] } let bestVideoQualityFromRequested = videoQualitiesRequestedForSource.bestVideoQualityFromTheRequestedList let layerToSelect = layers.matching(quality: bestVideoQualityFromRequested) let selectedVideoQuality: VideoQuality = layerToSelect == nil ? .auto : bestVideoQualityFromRequested let newVideoQualityAndLayerPair = VideoQualityAndLayerPair(videoQuality: selectedVideoQuality, layer: layerToSelect) - let currentVideoQualityAndLayerPair = sourceToSelectedVideoQualityAndLayerMapping[source.sourceId] + let currentVideoQualityAndLayerPair = self.sourceToSelectedVideoQualityAndLayerMapping[source.sourceId] guard newVideoQualityAndLayerPair != currentVideoQualityAndLayerPair else { // Currently selected video quality matches the newly calculated one, no action needed Self.logger.debug("♼ Exiting - Currently selected videoquality \(selectedVideoQuality.displayText) for source \(sourceId) is already up to date") @@ -256,13 +263,13 @@ private extension VideoTracksManager { if let layerToSelect { Self.logger.debug("♼ Has simulcast layer - \(layerToSelect) for source \(sourceId)") Self.logger.debug("♼ Selecting videoquality \(selectedVideoQuality.displayText) for source \(sourceId) on view \(anyActiveView)") - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair - try await queueEnableTrack(for: source, layer: MCRTSRemoteVideoTrackLayer(layer: layerToSelect)) + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair + try await self.queueEnableTrack(for: source, layer: MCRTSRemoteVideoTrackLayer(layer: layerToSelect)) } else { Self.logger.debug("♼ No simulcast layer for source \(sourceId) matching \(bestVideoQualityFromRequested.displayText)") Self.logger.debug("♼ Selecting videoquality 'Auto' for source \(sourceId) on view \(anyActiveView)") - sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair - try await queueEnableTrack(for: source) + self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair + try await self.queueEnableTrack(for: source) } } catch { Self.logger.debug("♼🛑 Enabling video track threw error \(error.localizedDescription)") @@ -271,11 +278,11 @@ private extension VideoTracksManager { func removeAllStoredData(for source: StreamSource) { let sourceId = source.sourceId - let activeViews = sourceToActiveViewsMapping[sourceId] + let activeViews = self.sourceToActiveViewsMapping[sourceId] - activeViews?.forEach { viewToRequestedVideoQualityMapping[$0] = nil } - sourceToSimulcastLayersMapping[sourceId] = nil - sourceToActiveViewsMapping[sourceId] = nil + activeViews?.forEach { self.viewToRequestedVideoQualityMapping[$0] = nil } + self.sourceToSimulcastLayersMapping[sourceId] = nil + self.sourceToActiveViewsMapping[sourceId] = nil } } @@ -283,7 +290,7 @@ private extension VideoTracksManager { private extension VideoTracksManager { func addLayerEventsObservationTask(_ task: Task, for source: StreamSource) { - layerEventsObservationDictionary[source.sourceId] = task + self.layerEventsObservationDictionary[source.sourceId] = task } } diff --git a/interactive-player/Interactive Viewer/Models/SavedStreamDetail.swift b/interactive-player/Interactive Viewer/Models/SavedStreamDetail.swift index 5c758cc1..455c97c9 100644 --- a/interactive-player/Interactive Viewer/Models/SavedStreamDetail.swift +++ b/interactive-player/Interactive Viewer/Models/SavedStreamDetail.swift @@ -15,6 +15,7 @@ struct SavedStreamDetail: Identifiable, Equatable { let maxPlayoutDelay: UInt? let disableAudio: Bool let primaryVideoQuality: VideoQuality + let maxBitrate: UInt? let saveLogs: Bool init( @@ -26,6 +27,7 @@ struct SavedStreamDetail: Identifiable, Equatable { maxPlayoutDelay: UInt?, disableAudio: Bool, primaryVideoQuality: VideoQuality, + maxBitrate: UInt, saveLogs: Bool, dateProvider: DateProvider = DefaultDateProvider() ) { @@ -39,6 +41,7 @@ struct SavedStreamDetail: Identifiable, Equatable { self.maxPlayoutDelay = maxPlayoutDelay self.disableAudio = disableAudio self.primaryVideoQuality = primaryVideoQuality + self.maxBitrate = maxBitrate self.saveLogs = saveLogs } } @@ -59,6 +62,7 @@ extension SavedStreamDetail { let maxPlayoutDelay = managedObject.maxPlayoutDelay let disableAudio = managedObject.disableAudio let saveLogs = managedObject.saveLogs + let maxBitrate = managedObject.maxBitrate ?? 0 self.id = UUID() self.accountID = accountID @@ -70,6 +74,7 @@ extension SavedStreamDetail { self.maxPlayoutDelay = maxPlayoutDelay.map { UInt(truncating: $0) } self.disableAudio = disableAudio self.primaryVideoQuality = primaryVideoQuality + self.maxBitrate = UInt(truncating: maxBitrate) self.saveLogs = saveLogs } - } +} diff --git a/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamCell.swift b/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamCell.swift index cb258bb5..99c9562c 100644 --- a/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamCell.swift +++ b/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamCell.swift @@ -22,9 +22,14 @@ struct RecentStreamCell: View { (String(localized: "recent-streams.server-url.label"), String(streamDetail.subscribeAPI)), (String(localized: "recent-streams.video-jitter-buffer.label"), String(streamDetail.videoJitterMinimumDelayInMs)), (String(localized: "recent-streams.min-playout-delay.label"), streamDetail.minPlayoutDelay.map { String($0) } ?? "N/A"), - (String(localized: "recent-streams.max-playout-delay.label"), streamDetail.maxPlayoutDelay.map { String($0) } ?? "N/A"), + (String(localized: "recent-streams.max-playout-delay.label"), streamDetail.maxPlayoutDelay.map { String($0) } ?? "N/A") + ] + ) + fields.append( + contentsOf: [ (String(localized: "recent-streams.disable-audio.label"), String(streamDetail.disableAudio)), (String(localized: "recent-streams.primary-video-quality.label"), streamDetail.primaryVideoQuality.displayText), + (String(localized: "recent-streams.max-bitrate.label"), String(streamDetail.maxBitrate ?? 0)), (String(localized: "recent-streams.save-logs.label"), String(streamDetail.saveLogs)) ] ) @@ -34,10 +39,7 @@ struct RecentStreamCell: View { private let action: () -> Void - init( - streamDetail: SavedStreamDetail, - action: @escaping () -> Void - ) { + init(streamDetail: SavedStreamDetail, action: @escaping () -> Void) { self.streamDetail = streamDetail self.action = action } @@ -78,7 +80,6 @@ struct RecentStreamCell: View { private extension Font { static let streamDetailFont: Font = .custom("AvenirNext-Regular", size: FontSize.subhead, relativeTo: .subheadline) - } struct RecentStreamCell_Previews: PreviewProvider { @@ -93,6 +94,7 @@ struct RecentStreamCell_Previews: PreviewProvider { maxPlayoutDelay: 0, disableAudio: true, primaryVideoQuality: .auto, + maxBitrate: 0, saveLogs: false ), action: {} diff --git a/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamsViewModel.swift b/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamsViewModel.swift index bfd55ef0..435af69d 100644 --- a/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamsViewModel.swift +++ b/interactive-player/Interactive Viewer/Views/RecentStreams/RecentStreamsViewModel.swift @@ -4,12 +4,11 @@ import Combine import Foundation -import RTSCore import MillicastSDK +import RTSCore @MainActor final class RecentStreamsViewModel: ObservableObject { - private let streamDataManager: StreamDataManagerProtocol private let settingsManager: SettingsManager private let dateProvider: DateProvider @@ -21,6 +20,7 @@ final class RecentStreamsViewModel: ObservableObject { topStreamDetails = Array(streamDetails.prefix(3)) } } + @Published private(set) var topStreamDetails: [SavedStreamDetail] = [] @Published private(set) var lastPlayedStream: SavedStreamDetail? @@ -41,7 +41,7 @@ final class RecentStreamsViewModel: ObservableObject { } } } - .store(in: &subscriptions) + .store(in: &subscriptions) } func fetchAllStreams() { @@ -90,16 +90,17 @@ final class RecentStreamsViewModel: ObservableObject { let currentDate = dateProvider.now let rtcLogPath = streamDetail.saveLogs ? URL.rtcLogPath(for: currentDate) : nil let sdkLogPath = streamDetail.saveLogs ? URL.sdkLogPath(for: currentDate) : nil - let playoutDelay: MCForcePlayoutDelay? - if let minPlayoutDelay = streamDetail.minPlayoutDelay, let maxPlayoutDelay = streamDetail.maxPlayoutDelay { + var playoutDelay: MCForcePlayoutDelay? + if let minPlayoutDelay = streamDetail.minPlayoutDelay, + let maxPlayoutDelay = streamDetail.maxPlayoutDelay + { playoutDelay = MCForcePlayoutDelay(min: Int32(minPlayoutDelay), max: Int32(maxPlayoutDelay)) - } else { - playoutDelay = nil } return SubscriptionConfiguration( subscribeAPI: streamDetail.subscribeAPI, jitterMinimumDelayMs: streamDetail.videoJitterMinimumDelayInMs, + maxBitrate: streamDetail.maxBitrate ?? 0, disableAudio: streamDetail.disableAudio, rtcEventLogPath: rtcLogPath?.path, sdkLogPath: sdkLogPath?.path, diff --git a/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputScreen.swift b/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputScreen.swift index 0f34903a..64eda756 100644 --- a/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputScreen.swift +++ b/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputScreen.swift @@ -3,31 +3,34 @@ // import DolbyIOUIKit +import MillicastSDK import RTSCore import SwiftUI // swiftlint:disable type_body_length struct StreamDetailInputScreen: View { - enum InputFocusable: Hashable { - case accountID - case streamName + case accountID + case streamName } + @Binding private var streamingScreenContext: StreamingView.Context? @State private var streamName: String = "" @State private var accountID: String = "" @State private var showAlert = false @State private var useCustomServerURL: Bool = false + @State private var isShowingMaxBitrate: Bool = false @State private var subscribeAPI: String = SubscriptionConfiguration.Constants.developmentSubscribeURL @State private var disableAudio: Bool = false @State private var saveLogs: Bool = false - @State private var jitterBufferDelayInMs: Float = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) + @State private var jitterBufferDelayInMs = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) @State private var primaryVideoQuality: VideoQuality = .auto + @State private var maxBitrateString: String = "0" @State private var isShowingSettingsView: Bool = false @State private var showPlayoutDelay: Bool = false - @State private var minPlayoutDelay: Float = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) - @State private var maxPlayoutDelay: Float = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) + @State private var minPlayoutDelay = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) + @State private var maxPlayoutDelay = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) @FocusState private var inputFocus: InputFocusable? @@ -118,6 +121,7 @@ struct StreamDetailInputScreen: View { let videoJitterMinimumDelayInMs = UInt(jitterBufferDelayInMs) let minPlayoutDelay = showPlayoutDelay ? UInt(minPlayoutDelay) : nil let maxPlayoutDelay = showPlayoutDelay ? UInt(maxPlayoutDelay) : nil + let maxBitrate: UInt = UInt(maxBitrateString) ?? 0 let success = viewModel.validateAndSaveStream( streamName: streamName, @@ -126,6 +130,7 @@ struct StreamDetailInputScreen: View { videoJitterMinimumDelayInMs: videoJitterMinimumDelayInMs, minPlayoutDelay: minPlayoutDelay, maxPlayoutDelay: maxPlayoutDelay, + maxBitrate: maxBitrate, disableAudio: disableAudio, primaryVideoQuality: primaryVideoQuality, saveLogs: saveLogs, @@ -141,6 +146,7 @@ struct StreamDetailInputScreen: View { videoJitterMinimumDelayInMs: videoJitterMinimumDelayInMs, minPlayoutDelay: minPlayoutDelay, maxPlayoutDelay: maxPlayoutDelay, + maxBitrate: maxBitrate, disableAudio: disableAudio, primaryVideoQuality: primaryVideoQuality, saveLogs: saveLogs @@ -273,7 +279,7 @@ struct StreamDetailInputScreen: View { ) Slider( value: $jitterBufferDelayInMs, - in: (0...2000), + in: 0...2000, step: 50, label: {}, minimumValueLabel: { @@ -300,7 +306,7 @@ struct StreamDetailInputScreen: View { Slider( value: $minPlayoutDelay, - in: (0...2000), + in: 0...2000, step: 50, label: {}, minimumValueLabel: { @@ -319,7 +325,7 @@ struct StreamDetailInputScreen: View { Slider( value: $maxPlayoutDelay, - in: (0...2000), + in: 0...2000, step: 50, label: {}, minimumValueLabel: { @@ -347,6 +353,21 @@ struct StreamDetailInputScreen: View { } .pickerStyle(.automatic) } + + Toggle(isOn: $isShowingMaxBitrate) { + Text( + "stream-detail-input.set-max-bitrate-label", + font: .streamConfigurationItemsFont + ) + } + + if isShowingMaxBitrate { + DolbyIOUIKit.TextField(text: $maxBitrateString, placeholderText: "stream-detail-input.max-bitrate-label") + .keyboardType(.numberPad) + .accessibilityIdentifier("InputScreen.MaximumBitrate") + .font(.avenirNextRegular(withStyle: .caption, size: FontSize.caption1)) + .submitLabel(.next) + } } .padding() .background(Color(uiColor: themeManager.theme.neutral700)) @@ -404,8 +425,10 @@ struct StreamDetailInputScreen: View { maxPlayoutDelay: nil, disableAudio: disableAudio, primaryVideoQuality: videoQuality, + maxBitrate: 0, saveLogs: saveLogs )) { + let success = viewModel.validateAndSaveStream( streamName: streamName, accountID: accountID, @@ -413,6 +436,7 @@ struct StreamDetailInputScreen: View { videoJitterMinimumDelayInMs: jitterMinimumDelayMs, minPlayoutDelay: nil, maxPlayoutDelay: nil, + maxBitrate: 0, disableAudio: disableAudio, primaryVideoQuality: videoQuality, saveLogs: saveLogs, @@ -442,14 +466,15 @@ struct StreamDetailInputScreen: View { } func resetStreamConfigurationState() { - self.useCustomServerURL = false - self.showPlayoutDelay = false - self.disableAudio = false - self.jitterBufferDelayInMs = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) - self.primaryVideoQuality = .auto - self.saveLogs = false - self.minPlayoutDelay = 0 - self.maxPlayoutDelay = 0 + useCustomServerURL = false + showPlayoutDelay = false + disableAudio = false + jitterBufferDelayInMs = Float(SubscriptionConfiguration.Constants.jitterMinimumDelayMs) + primaryVideoQuality = .auto + saveLogs = false + minPlayoutDelay = 0 + maxPlayoutDelay = 0 + maxBitrateString = "0" } func syncMaxPlayoutDelay() { @@ -469,6 +494,7 @@ struct StreamDetailInputScreen: View { maxPlayoutDelay = 0 } } + // swiftlint:enable type_body_length extension Font { diff --git a/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputViewModel.swift b/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputViewModel.swift index 7f5626c9..bf20bd07 100644 --- a/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputViewModel.swift +++ b/interactive-player/Interactive Viewer/Views/StreamDetailInputScreen/StreamDetailInputViewModel.swift @@ -3,9 +3,9 @@ // import Combine -import RTSCore import Foundation import MillicastSDK +import RTSCore @MainActor final class StreamDetailInputViewModel: ObservableObject { @@ -44,6 +44,7 @@ final class StreamDetailInputViewModel: ObservableObject { videoJitterMinimumDelayInMs: UInt, minPlayoutDelay: UInt?, maxPlayoutDelay: UInt?, + maxBitrate: UInt, disableAudio: Bool, primaryVideoQuality: VideoQuality, saveLogs: Bool, @@ -64,6 +65,7 @@ final class StreamDetailInputViewModel: ObservableObject { maxPlayoutDelay: maxPlayoutDelay, disableAudio: disableAudio, primaryVideoQuality: primaryVideoQuality, + maxBitrate: maxBitrate, saveLogs: saveLogs ) ) @@ -76,15 +78,14 @@ final class StreamDetailInputViewModel: ObservableObject { videoJitterMinimumDelayInMs: UInt, minPlayoutDelay: UInt?, maxPlayoutDelay: UInt?, + maxBitrate: UInt, disableAudio: Bool, primaryVideoQuality: VideoQuality, saveLogs: Bool ) -> SubscriptionConfiguration { - let playoutDelay: MCForcePlayoutDelay? + var playoutDelay: MCForcePlayoutDelay? if let minPlayoutDelay, let maxPlayoutDelay { playoutDelay = MCForcePlayoutDelay(min: Int32(minPlayoutDelay), max: Int32(maxPlayoutDelay)) - } else { - playoutDelay = nil } let currentDate = dateProvider.now let rtcLogPath = saveLogs ? URL.rtcLogPath(for: currentDate) : nil @@ -93,12 +94,14 @@ final class StreamDetailInputViewModel: ObservableObject { return SubscriptionConfiguration( subscribeAPI: subscribeAPI, jitterMinimumDelayMs: videoJitterMinimumDelayInMs, + maxBitrate: maxBitrate, disableAudio: disableAudio, rtcEventLogPath: rtcLogPath?.path, sdkLogPath: sdkLogPath?.path, playoutDelay: playoutDelay ) } + // swiftlint:enable function_parameter_count func clearAllStreams() { diff --git a/rts-viewer-ios/.DS_Store b/rts-viewer-ios/.DS_Store new file mode 100644 index 00000000..6dd2aae5 Binary files /dev/null and b/rts-viewer-ios/.DS_Store differ diff --git a/rts-viewer-ios/.gitignore b/rts-viewer-ios/.gitignore index b8fcc057..8e36cb12 100644 --- a/rts-viewer-ios/.gitignore +++ b/rts-viewer-ios/.gitignore @@ -1,6 +1,7 @@ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +.DS_Store ## User settings xcuserdata/ diff --git a/rts-viewer-ios/RTSViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/rts-viewer-ios/RTSViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/rts-viewer-ios/RTSViewer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/rts-viewer-ios/RTSViewer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/rts-viewer-ios/RTSViewer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3fe42100..00000000 --- a/rts-viewer-ios/RTSViewer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "millicast-sdk-swift-package", - "kind" : "remoteSourceControl", - "location" : "https://github.com/millicast/millicast-sdk-swift-package", - "state" : { - "revision" : "83c18963b46cbabcb37bde44198acc8218d48d3a", - "version" : "1.4.2" - } - } - ], - "version" : 2 -} diff --git a/rts-viewer-tvos/.DS_Store b/rts-viewer-tvos/.DS_Store index f430b74f..60990a88 100644 Binary files a/rts-viewer-tvos/.DS_Store and b/rts-viewer-tvos/.DS_Store differ diff --git a/rts-viewer-tvos/.gitignore b/rts-viewer-tvos/.gitignore index b8fcc057..8e36cb12 100644 --- a/rts-viewer-tvos/.gitignore +++ b/rts-viewer-tvos/.gitignore @@ -1,6 +1,7 @@ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore +.DS_Store ## User settings xcuserdata/