Skip to content

Commit

Permalink
Merge pull request #202 from dolbyio-samples/feature/targetBitrate
Browse files Browse the repository at this point in the history
DIOS-6236: Show Target Bitrate in Stats
  • Loading branch information
sheiladoherty-dolby authored Jul 25, 2024
2 parents 576031d + e51491d commit 0389bcd
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 45 deletions.
Binary file modified .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions interactive-player/Interactive Viewer/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,4 @@ Follow video: Switch audio source when a video is selected. If the selected vide
"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";
"stream.stats.target-bitrate.label" = "Target Bitrate";
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ final actor VideoTracksManager {
private var layerEventsObservationDictionary: [SourceID: Task<Void, Never>] = [:]
private var sourceToTasks: [SourceID: SerialTasks] = [:]
private let videoQualitySubject: CurrentValueSubject<[SourceID: VideoQuality], Never> = CurrentValueSubject([:])

let rendererRegistry: RendererRegistry
lazy var selectedVideoQualityPublisher = videoQualitySubject.eraseToAnyPublisher()
@Published var sourcedTargetBitrates: [SourceID: Int?] = [:]

init(rendererRegistry: RendererRegistry = RendererRegistry()) {
self.rendererRegistry = rendererRegistry
Expand All @@ -58,7 +58,7 @@ final actor VideoTracksManager {
Task { [weak self] in
guard
let self,
await self.layerEventsObservationDictionary[source.sourceId] == nil
await self.layerEventsObservationDictionary[source.sourceId] == nil
else {
return
}
Expand Down Expand Up @@ -264,11 +264,19 @@ private extension VideoTracksManager {
Self.logger.debug("♼ Has simulcast layer - \(layerToSelect) for source \(sourceId)")
Self.logger.debug("♼ Selecting videoquality \(selectedVideoQuality.displayText) for source \(sourceId) on view \(anyActiveView)")
self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair

let targetBitrateForLayer = self.getTargetBitrate(from: layerToSelect, sourceId: sourceId)
self.sourcedTargetBitrates[sourceId] = targetBitrateForLayer

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)")
self.sourceToSelectedVideoQualityAndLayerMapping[sourceId] = newVideoQualityAndLayerPair

let targetBitrateForLayer = self.getTargetBitrate(from: newVideoQualityAndLayerPair.layer, sourceId: sourceId)
self.sourcedTargetBitrates[sourceId] = targetBitrateForLayer

try await self.queueEnableTrack(for: source)
}
} catch {
Expand All @@ -284,6 +292,15 @@ private extension VideoTracksManager {
self.sourceToSimulcastLayersMapping[sourceId] = nil
self.sourceToActiveViewsMapping[sourceId] = nil
}

func getTargetBitrate(from layer: MCRTSRemoteTrackLayer?, sourceId: SourceID) -> Int? {
if let bitrate = layer?.targetBitrate {
let targetBitrate = Int(truncating: bitrate)
Self.logger.debug("♼ Updating target bitrate to \(targetBitrate) for source \(sourceId)")
return targetBitrate
}
return nil
}
}

// MARK: Helpers to manage `Event` Observations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ final class RecentStreamsViewModel: ObservableObject {
let sdkLogPath = streamDetail.saveLogs ? URL.sdkLogPath(for: currentDate) : nil
var playoutDelay: MCForcePlayoutDelay?
if let minPlayoutDelay = streamDetail.minPlayoutDelay,
let maxPlayoutDelay = streamDetail.maxPlayoutDelay
{
let maxPlayoutDelay = streamDetail.maxPlayoutDelay {
playoutDelay = MCForcePlayoutDelay(min: Int32(minPlayoutDelay), max: Int32(maxPlayoutDelay))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
// SingleStreamView.swift
//

import SwiftUI
import RTSCore
import DolbyIOUIKit
import MillicastSDK
import RTSCore
import SwiftUI

struct SingleStreamView: View {

private enum Animation {
static let duration: CGFloat = 0.75
static let blendDuration: CGFloat = 3.0
static let offset: CGFloat = 200.0
}

private let isShowingDetailPresentation: Bool
private let onSelect: ((StreamSource) -> Void)
private let onSelect: (StreamSource) -> Void
private let onClose: (() -> Void)?
private var viewModel: SingleStreamViewModel

@State private var showingScreenControls = false
@State private var isShowingSettingsScreen: Bool = false
Expand All @@ -26,7 +26,6 @@ struct SingleStreamView: View {

@StateObject private var userInteractionViewModel: UserInteractionViewModel = .init()

@ObservedObject private var viewModel: SingleStreamViewModel
@ObservedObject private var themeManager = ThemeManager.shared

init(
Expand Down Expand Up @@ -193,7 +192,8 @@ struct SingleStreamView: View {

private func statisticsView() -> some View {
HStack {
StatisticsInfoView(streamSource: viewModel.selectedVideoSource, streamStatistics: viewModel.streamStatistics)
StatisticsInfoView(statsInfoViewModel: viewModel.statsInfoViewModel)

Spacer()
}
.frame(alignment: Alignment.bottom)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// SingleStreamViewModel.swift
//

import Combine
import RTSCore
import Foundation
import MillicastSDK
import os
import RTSCore

@MainActor
final class SingleStreamViewModel: ObservableObject {
final class SingleStreamViewModel {
static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: SingleStreamViewModel.self)
Expand All @@ -21,9 +20,10 @@ final class SingleStreamViewModel: ObservableObject {
let settingsMode: SettingsMode
let subscriptionManager: SubscriptionManager
let videoTracksManager: VideoTracksManager
@Published private(set) var streamStatistics: StreamStatistics?

private var subscriptions: [AnyCancellable] = []
lazy var statsInfoViewModel = StatsInfoViewModel(streamSource: selectedVideoSource,
videoTracksManager: videoTracksManager,
subscriptionManager: subscriptionManager)

init(
sources: [StreamSource],
Expand All @@ -39,24 +39,9 @@ final class SingleStreamViewModel: ObservableObject {
self.settingsMode = settingsMode
self.subscriptionManager = subscriptionManager
self.videoTracksManager = videoTracksManager

observeStats()
}

func streamSource(for id: UUID) -> StreamSource? {
sources.first { $0.id == id }
}

private func observeStats() {
Task { [weak self] in
guard let self else { return }
await self.subscriptionManager.$streamStatistics
.receive(on: DispatchQueue.main)
.sink { [weak self] statistics in
guard let self else { return }
self.streamStatistics = statistics
}
.store(in: &subscriptions)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
//

import DolbyIOUIKit
import SwiftUI
import RTSCore
import SwiftUI

struct StatisticsInfoView: View {
@ObservedObject private var themeManager = ThemeManager.shared
private let viewModel: StatsInfoViewModel
@ObservedObject private var viewModel: StatsInfoViewModel

private let fontCaption = Font.custom("AvenirNext-Bold", size: FontSize.subhead)
private let fontTable = Font.custom("AvenirNext-Regular", size: FontSize.body)
private let fontTableValue = Font.custom("AvenirNext-DemiBold", size: FontSize.body)
private let fontTitle = Font.custom("AvenirNext-Bold", size: FontSize.title2)

init(streamSource: StreamSource, streamStatistics: StreamStatistics?) {
viewModel = StatsInfoViewModel(streamSource: streamSource, streamStatistics: streamStatistics)
init(statsInfoViewModel: StatsInfoViewModel) {
self.viewModel = statsInfoViewModel
}

private var theme: Theme {
Expand Down Expand Up @@ -66,6 +66,20 @@ struct StatisticsInfoView: View {
}
.padding([.top], Layout.spacing0_5x)
}

HStack {
Text("stream.stats.target-bitrate.label", font: fontTable)
.foregroundColor(Color(theme.neutral200))
.frame(minWidth: Layout.spacing0x, maxWidth: .infinity, alignment: .leading)
.accessibilityIdentifier("Key.stream.stats.target-bitrate.label")
.accessibilityValue("stream.stats.target-bitrate.label")
Text(verbatim: "\(viewModel.targetBitrate)", font: fontTableValue)
.foregroundColor(Color(theme.onBackground))
.frame(minWidth: Layout.spacing0x, maxWidth: .infinity, alignment: .leading)
.accessibilityIdentifier("Value.\(viewModel.targetBitrate)")
.accessibilityValue("\(viewModel.targetBitrate)")
}
.padding([.top], Layout.spacing0_5x)
}
.padding([.leading, .trailing], Layout.spacing2x)
.padding(.bottom, Layout.spacing3x)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,37 @@
// StatsInfoViewModel.swift
//

import RTSCore
import Combine
import Foundation
import RTSCore

final class StatsInfoViewModel {
@MainActor
final class StatsInfoViewModel: ObservableObject {
@Published private(set) var statsItems: [StatsItem] = []
@Published private(set) var targetBitrate: String = "N/A"
private let subscriptionManager: SubscriptionManager
private let videoTracksManager: VideoTracksManager
private let streamSource: StreamSource
let statsItems: [StatsItem]
private var subscriptions: [AnyCancellable] = []

init(streamSource: StreamSource, streamStatistics: StreamStatistics?) {
init(
streamSource: StreamSource,
videoTracksManager: VideoTracksManager,
subscriptionManager: SubscriptionManager
) {
self.streamSource = streamSource
self.statsItems = streamStatistics.map { Self.makeStatsItems(for: $0, streamSource: streamSource) } ?? []
self.videoTracksManager = videoTracksManager
self.subscriptionManager = subscriptionManager

Task {
self.statsItems = await subscriptionManager.streamStatistics.map { Self.makeStatsItems(for: $0, streamSource: streamSource) } ?? []
let sourcedBitrates = await videoTracksManager.sourcedTargetBitrates
guard let sourcedBitrate = sourcedBitrates[streamSource.sourceId],
let sourcedBitrate else { return }
self.targetBitrate = Self.formatBitRate(bitRate: sourcedBitrate)
}

observeStats()
}

struct StatsItem: Identifiable {
Expand All @@ -20,8 +41,36 @@ final class StatsInfoViewModel {
var value: String
}

private func observeStats() {
Task { [weak self] in
guard let self else { return }
await self.subscriptionManager.$streamStatistics
.receive(on: DispatchQueue.main)
.sink { [weak self] statistics in
guard let self else { return }
self.statsItems = statistics.map { Self.makeStatsItems(for: $0, streamSource: self.streamSource) } ?? []
}
.store(in: &subscriptions)

await self.videoTracksManager.$sourcedTargetBitrates
.receive(on: DispatchQueue.main)
.sink { [weak self] sourcedBitrates in
guard let self else { return }
if let sourcedBitrate = sourcedBitrates[streamSource.sourceId],
let sourcedBitrate {
self.targetBitrate = Self.formatBitRate(bitRate: sourcedBitrate)
} else {
self.targetBitrate = "N/A"
}
}
.store(in: &subscriptions)
}
}
}

private extension StatsInfoViewModel {
// swiftlint:disable function_body_length
private static func makeStatsItems(for streamStatistics: StreamStatistics, streamSource: StreamSource) -> [StatsItem] {
static func makeStatsItems(for streamStatistics: StreamStatistics, streamSource: StreamSource) -> [StatsItem] {
guard
let mid = streamSource.videoTrack.currentMID,
let videoStatsInboundRtp = streamStatistics.videoStatistics(matching: mid)
Expand Down Expand Up @@ -222,9 +271,10 @@ final class StatsInfoViewModel {
}
return result
}

// swiftlint:enable function_body_length

private static func dateString(_ timestamp: Double) -> String {
static func dateString(_ timestamp: Double) -> String {
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone(abbreviation: "GMT")
dateFormatter.locale = NSLocale.current
Expand All @@ -234,18 +284,18 @@ final class StatsInfoViewModel {
return dateFormatter.string(from: date)
}

private static func formatBytes(bytes: Int) -> String {
static func formatBytes(bytes: Int) -> String {
return "\(formatNumber(input: bytes))B"
}

private static func formatBitRate(bitRate: Int) -> String {
static func formatBitRate(bitRate: Int) -> String {
let value = formatNumber(input: bitRate).lowercased()
return "\(value)bps"
}

private static func formatNumber(input: Int) -> String {
static 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" }
if input >= KILOBYTES && input < MEGABYTES { return "\(input / KILOBYTES) K" } else { return "\(input / MEGABYTES) M" }
}
}

Expand Down
Binary file modified rts-viewer-tvos/.DS_Store
Binary file not shown.

0 comments on commit 0389bcd

Please sign in to comment.