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 13, 2024
1 parent 4ea71ae commit e211a41
Show file tree
Hide file tree
Showing 40 changed files with 2,321 additions and 319 deletions.
Binary file modified .DS_Store
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import MillicastSDK
import os
import SwiftUI

public actor SubscriptionManager {
public actor SubscriptionManager: ObservableObject {
private static let logger = Logger(
subsystem: Bundle.module.bundleIdentifier!,
category: String(describing: SubscriptionManager.self)
Expand Down
File renamed without changes.
178 changes: 158 additions & 20 deletions rts-viewer-tvos/RTSViewer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"originHash" : "55c5bb51783dcbfad1b8b5bd2888d3846f22b84ddc7759242400a1260ba1846f",
"pins" : [
{
"identity" : "millicast-sdk-swift-package",
"kind" : "remoteSourceControl",
"location" : "https://github.com/millicast/millicast-sdk-swift-package",
"state" : {
"revision" : "d20cb45ff24acbc16191d4df6a0e4be9daa26e24",
"version" : "2.0.0-beta.7"
"revision" : "a36748fd8b8d8fe8d94608f42972e5002d2dd9f5",
"version" : "2.0.0"
}
}
],
"version" : 2
"version" : 3
}
16 changes: 0 additions & 16 deletions rts-viewer-tvos/RTSViewer/ContentView.swift

This file was deleted.

395 changes: 395 additions & 0 deletions rts-viewer-tvos/RTSViewer/Managers/VideoTracksManager.swift

Large diffs are not rendered by default.

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

import Combine
import Foundation
import os
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 videoTracksManager: VideoTracksManager
let source: StreamSource
private var cancellables = [AnyCancellable]()
private var layersEventsObserver: Task<Void, Never>?

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

observeStreamStatistics()
observeLayerEvents()
startSelectedQualityObserver()
}

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

func enableVideo(with quality: VideoQuality = .auto) {
let displayLabel = source.sourceId.displayLabel
let viewId = "\(ChannelGridView.self).\(displayLabel)"
Task {
Self.logger.debug("♼ Channel Grid view: Video view appear for \(self.source.sourceId)")
await self.videoTracksManager.enableTrack(for: self.source, with: quality, on: viewId)
}
}

func disableVideo() {
let displayLabel = source.sourceId.displayLabel
let viewId = "\(ChannelGridView.self).\(displayLabel)"
Task {
Self.logger.debug("♼ Channel Grid view: Video view disappear for \(self.source.sourceId)")
await self.videoTracksManager.disableTrack(for: self.source, on: viewId)
}
}

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
}
}

func startSelectedQualityObserver() {
Task { [weak self] in
guard let self else { return }
await self.videoTracksManager.selectedVideoQualityPublisher
.map { $0[self.source.sourceId] ?? .auto }
.receive(on: DispatchQueue.main)
.sink { quality in
self.selectedVideoQuality = quality
}
.store(in: &cancellables)
}
}
}
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
}
21 changes: 21 additions & 0 deletions rts-viewer-tvos/RTSViewer/Models/UnsourcedChannel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// UnsourcedChannel.swift
//

import Foundation
import RTSCore

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

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

public func hash(into hasher: inout Hasher) {
return hasher.combine(id)
}
}
35 changes: 24 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, Comparable {
case auto
case quality(MCRTSRemoteTrackLayer)

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

extension VideoQuality {
static func < (lhs: VideoQuality, rhs: VideoQuality) -> Bool {
return switch (lhs, rhs) {
case(.quality, .auto):
false
case(.auto, .quality):
false
case let (.quality(lhsQuality), .quality(rhsQuality)):
lhsQuality == rhsQuality
default:
true
}
}

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 +70,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
Loading

0 comments on commit e211a41

Please sign in to comment.