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 645e3d2
Show file tree
Hide file tree
Showing 44 changed files with 1,926 additions and 407 deletions.
Binary file removed .DS_Store
Binary file not shown.
4 changes: 2 additions & 2 deletions .github/workflows/cd_interactive_player.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:

jobs:
Build-And-Deploy-To-Firebase:
runs-on: macos-13
runs-on: macos-14
steps:
- name: Checkout source code
uses: actions/checkout@v3
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
retention-days: 30

Build-And-Deploy-iOS-App-To-Appstore:
runs-on: macos-13
runs-on: macos-14
steps:
- name: Checkout source code
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cd_rts_viewer_tvos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:

jobs:
Build-And-Deploy-TVOS-App-To-Appstore:
runs-on: macos-13
runs-on: macos-14
steps:
- name: Checkout source code
uses: actions/checkout@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci_rts_viewer_tvos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:

jobs:
Run-UnitTests:
runs-on: macos-13
runs-on: macos-14
steps:
- name: Checkout source code
uses: actions/checkout@v3
Expand All @@ -18,7 +18,7 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.2

- name: Run Unit tests
run: |
cd rts-viewer-tvos
Expand Down
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.
184 changes: 149 additions & 35 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.

143 changes: 143 additions & 0 deletions rts-viewer-tvos/RTSViewer/Models/Channel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//
// 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 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")
}
}

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
Loading

0 comments on commit 645e3d2

Please sign in to comment.