Skip to content

Commit

Permalink
Add multichannel viewing support
Browse files Browse the repository at this point in the history
  • Loading branch information
sheiladoherty-dolby authored and aravind-raveendran committed Oct 14, 2024
1 parent 4ea71ae commit 2ca2b5e
Show file tree
Hide file tree
Showing 39 changed files with 1,854 additions and 331 deletions.
Binary file removed .DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion interactive-player/fastlane/.env.default
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// .env.default
XCODE_SELECT_PATH = /Applications/Xcode_15.2.app
XCODE_SELECT_PATH = /Applications/Xcode_15.4.app

//
// Temp paths - Build artifacts
Expand Down
Binary file removed rts-viewer-tvos/.DS_Store
Binary file not shown.
162 changes: 142 additions & 20 deletions rts-viewer-tvos/RTSViewer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

This file was deleted.

16 changes: 0 additions & 16 deletions rts-viewer-tvos/RTSViewer/ContentView.swift

This file was deleted.

154 changes: 154 additions & 0 deletions rts-viewer-tvos/RTSViewer/Models/Channel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//
// SourcedChannel.swift
//

import Combine
import Foundation
import os
import MillicastSDK
import RTSCore

class Channel: ObservableObject, Identifiable, Hashable, Equatable {
private static let logger = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: String(describing: Channel.self)
)

@Published var currentlyFocusedChannel: Channel? {
didSet {
guard let currentlyFocusedChannel else { return }
isFocusedChannel = currentlyFocusedChannel.id == id
}
}

@Published var isFocusedChannel: Bool = false {
didSet {
if isFocusedChannel {
enableSound()
} else {
disableSound()
}
}
}

@Published private(set) var streamStatistics: StreamStatistics?
@Published var showStatsView: Bool = false
@Published var videoQualityList = [VideoQuality]()
@Published var selectedVideoQuality: VideoQuality = .auto

let id: UUID
let streamConfig: StreamConfig
let subscriptionManager: SubscriptionManager
let source: StreamSource
let rendererRegistry: RendererRegistry
private var cancellables = [AnyCancellable]()
private var layersEventsObserver: Task<Void, Never>?

init(unsourcedChannel: UnsourcedChannel,
source: StreamSource,
rendererRegistry: RendererRegistry = RendererRegistry()) {
self.id = unsourcedChannel.id
self.streamConfig = unsourcedChannel.streamConfig
self.subscriptionManager = unsourcedChannel.subscriptionManager
self.source = source
self.rendererRegistry = rendererRegistry

observeStreamStatistics()
observeLayerEvents()
}

func shouldShowStatsView(showStats: Bool) {
showStatsView = showStats
}

func enableVideo(with quality: VideoQuality) {
let displayLabel = source.sourceId.displayLabel
let viewId = "\(ChannelGridView.self).\(displayLabel)"
Task {
Self.logger.debug("♼ Channel Grid view: Video view appear for \(self.source.sourceId)")
self.selectedVideoQuality = quality
if let layer = quality.layer {
try await self.source.videoTrack.enable(
renderer: rendererRegistry.sampleBufferRenderer(for: source).underlyingRenderer,
layer: MCRTSRemoteVideoTrackLayer(layer: layer)
)
} else {
try await self.source.videoTrack.enable(renderer: rendererRegistry.sampleBufferRenderer(for: source).underlyingRenderer)
}
}
}

func disableVideo() {
let displayLabel = source.sourceId.displayLabel
Task {
Self.logger.debug("♼ Channel Grid view: Video view disappear for \(self.source.sourceId)")
try await self.source.videoTrack.disable()
}
}

func enableSound() {
Task {
try? await self.source.audioTrack?.enable()
Self.logger.debug("♼ Channel \(self.source.sourceId) audio enabled")
}
}

func disableSound() {
Task {
try? await self.source.audioTrack?.disable()
Self.logger.debug("♼ Channel \(self.source.sourceId) audio disabled")
}
}

func updateFocusedChannel(with channel: Channel) {
currentlyFocusedChannel = channel
}

static func == (lhs: Channel, rhs: Channel) -> Bool {
return lhs.id == rhs.id
}

func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
}

private extension Channel {
func observeStreamStatistics() {
Task { [weak self] in
guard let self else { return }
await subscriptionManager.$streamStatistics
.sink { statistics in
guard let statistics else { return }
Task {
self.streamStatistics = statistics
}
}
.store(in: &cancellables)
}
}

func observeLayerEvents() {
Task { [weak self] in
guard let self,
layersEventsObserver == nil else { return }

Self.logger.debug("♼ Registering layer events for \(source.sourceId)")
let layerEventsObservationTask = Task {
for await layerEvent in self.source.videoTrack.layers() {
guard !Task.isCancelled else { return }

let videoQualities = layerEvent.layers()
.map(VideoQuality.init)
.reduce([.auto]) { $0 + [$1] }
Self.logger.debug("♼ Received layers \(videoQualities.count)")
self.videoQualityList = videoQualities
}
}

layersEventsObserver = layerEventsObservationTask

_ = await layerEventsObservationTask.value
}
}
}
11 changes: 11 additions & 0 deletions rts-viewer-tvos/RTSViewer/Models/StreamConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// PlayFromConfigs.swift
//

import Foundation

struct StreamConfig {
let apiUrl: String
let streamName: String
let accountId: String
}
20 changes: 20 additions & 0 deletions rts-viewer-tvos/RTSViewer/Models/UnsourcedChannel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// UnsourcedChannel.swift
//

import Foundation
import RTSCore

struct UnsourcedChannel: Identifiable, Hashable, Equatable {
let id = UUID()
let streamConfig: StreamConfig
let subscriptionManager: SubscriptionManager

static func == (lhs: UnsourcedChannel, rhs: UnsourcedChannel) -> Bool {
return lhs.id == rhs.id
}

public func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
}
22 changes: 11 additions & 11 deletions rts-viewer-tvos/RTSViewer/Models/VideoQuality.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,7 @@
import Foundation
import MillicastSDK

enum VideoQuality: Identifiable {
var id: String {
switch self {
case .auto:
"Auto"
case let .quality(videoTrackLayer):
videoTrackLayer.encodingId
}
}

enum VideoQuality: Identifiable, Equatable {
case auto
case quality(MCRTSRemoteTrackLayer)

Expand All @@ -24,6 +15,15 @@ enum VideoQuality: Identifiable {
}

extension VideoQuality {
var id: String {
switch self {
case .auto:
"Auto"
case let .quality(videoTrackLayer):
videoTrackLayer.encodingId
}
}

var displayText: String {
switch self {
case .auto:
Expand Down Expand Up @@ -57,7 +57,7 @@ extension VideoQuality {
}
var target: [String] = []
if let bitrate = layer.targetBitrate {
target.append("Bitrate: \(bitrate.intValue/1000) kbps")
target.append("Bitrate: \(bitrate.intValue / 1000) kbps")
}
if let resolution = layer.resolution {
target.append("Resolution: \(resolution.width)x\(resolution.height)")
Expand Down
8 changes: 5 additions & 3 deletions rts-viewer-tvos/RTSViewer/RTSViewer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import SwiftUI
*/
@main
struct RTSViewer: App {

var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(.dark)
NavigationView {
LandingView()
}
.navigationViewStyle(StackNavigationViewStyle())
.preferredColorScheme(.dark)
}
}
}
3 changes: 3 additions & 0 deletions rts-viewer-tvos/RTSViewer/Resources/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"stream-detail-input.streamName.placeholder.label" = "Enter your stream name";
"stream-detail-input.accountId.placeholder.label" = "Enter your account ID";
"stream-detail-input.play.button" = "Play";
"stream-detail-input.play-from-config.button" = "Play From Config";
"stream-detail-input.recent-streams.button" = "Saved Streams";
"stream-detail-input.clear-stream-history.button" = "Clear stream history";
"stream-detail-input.rememberStream.toggle" = "Remember stream";
Expand Down Expand Up @@ -88,3 +89,5 @@
"stream.stats.total-stream-time.label" = "Total Stream Time";
"stream.stats.target-bitrate.label" = "Target Bitrate";
"stream.stats.outgoing-bitrate.label" = "Outgoing Bitrate";

"video-view.main.label" = "Main";
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ struct SimulcastView: View {
private let onSelectVideoQuality: (VideoQuality) -> Void
@FocusState private var focusedVideoQuality: FocusableField?

init(source: StreamSource, videoQualityList: [VideoQuality], selectedVideoQuality: VideoQuality, onSelectVideoQuality: @escaping (VideoQuality) -> Void) {
init(
source: StreamSource,
videoQualityList: [VideoQuality],
selectedVideoQuality: VideoQuality,
onSelectVideoQuality: @escaping (VideoQuality) -> Void
) {
viewModel = SimulcastViewModel(source: source, videoQualityList: videoQualityList, selectedVideoQuality: selectedVideoQuality)
self.onSelectVideoQuality = onSelectVideoQuality
}
Expand Down Expand Up @@ -62,16 +67,16 @@ struct SimulcastView: View {
onSelectVideoQuality(videoQuality)
}, label: {
HStack {
VStack(alignment: .leading) {
Text(videoQuality.displayText)
.font(theme[.avenirNextDemiBold(size: FontSize.body, style: .body)])
if let targetInformation = videoQuality.targetInformation {
Text(targetInformation)
.font(theme[.avenirNextRegular(size: FontSize.caption2, style: .caption2)])
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
VStack(alignment: .leading) {
Text(videoQuality.displayText)
.font(theme[.avenirNextDemiBold(size: FontSize.body, style: .body)])
if let targetInformation = videoQuality.targetInformation {
Text(targetInformation)
.font(theme[.avenirNextRegular(size: FontSize.caption2, style: .caption2)])
.fixedSize(horizontal: false, vertical: true)
.multilineTextAlignment(.leading)
}
}
}

Spacer()
if videoQuality.encodingId == viewModel.selectedVideoQuality.encodingId {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,31 @@ import SwiftUI

struct StatisticsView: View {
private let viewModel: StatisticsViewModel
private let fontAssetTable = FontAsset.avenirNextRegular(size: FontSize.caption2, style: .caption2)
private let fontTable = Font.avenirNextRegular(withStyle: .caption2, size: FontSize.caption2)
private let fontAssetCaption = FontAsset.avenirNextDemiBold(size: FontSize.caption1, style: .caption)
private let fontAssetTitle: FontAsset
private let theme = ThemeManager.shared.theme
private let isMultiChannel: Bool

init(
source: StreamSource,
streamStatistics: StreamStatistics,
layers: [MCRTSRemoteTrackLayer],
projectedTimeStamp: Double?
projectedTimeStamp: Double?,
isMultiChannel: Bool = false
) {
viewModel = StatisticsViewModel(
source: source,
streamStatistics: streamStatistics,
layers: layers,
projectedTimeStamp: projectedTimeStamp
projectedTimeStamp: projectedTimeStamp,
isMultiChannel: isMultiChannel
)
fontAssetTitle = isMultiChannel ? FontAsset.avenirNextBold(size: FontSize.caption1, style: .caption) : FontAsset.avenirNextBold(size: FontSize.title3, style: .title3)
self.isMultiChannel = isMultiChannel
}

private let fontAssetTable = FontAsset.avenirNextRegular(size: FontSize.caption2, style: .caption2)
private let fontTable = Font.avenirNextRegular(withStyle: .caption2, size: FontSize.caption2)

private let fontAssetCaption = FontAsset.avenirNextDemiBold(size: FontSize.caption1, style: .caption)
private let fontAssetTitle = FontAsset.avenirNextBold(size: FontSize.title3, style: .title3)
private let theme = ThemeManager.shared.theme

var body: some View {
VStack {
Text(text: "stream.media-stats.label", fontAsset: fontAssetTitle)
Expand Down
Loading

0 comments on commit 2ca2b5e

Please sign in to comment.