Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adaptive frame rate depending on thermalState #174

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import StreamVideoSwiftUI

struct ThermalStateViewModifier: ViewModifier {

@StateObject private var thermalStateObserver: ThermalStateObserver = .shared
@Injected(\.thermalStateObserver) var thermalStateObserver
@State private var toast: Toast? = nil

func body(content: Content) -> some View {
content
.toastView(toast: $toast)
.onReceive(thermalStateObserver.$state) { state in
.onReceive(thermalStateObserver.statePublisher) { state in
switch state {
case .nominal:
toast = nil
Expand Down
38 changes: 33 additions & 5 deletions Sources/StreamVideo/Utils/ThermalStateObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ extension LogSubsystem {
public static let thermalState = Self(rawValue: 1 << 6)
}

public final class ThermalStateObserver: ObservableObject {
public static let shared = ThermalStateObserver()
public protocol ThermalStateObserving: ObservableObject {
ipavlidakis marked this conversation as resolved.
Show resolved Hide resolved

var state: ProcessInfo.ThermalState { get }

var statePublisher: AnyPublisher<ProcessInfo.ThermalState, Never> { get }

var scale: CGFloat { get }
}

final class ThermalStateObserver: ObservableObject, ThermalStateObserving {
static let shared = ThermalStateObserver()

/// Published property to observe the thermal state
@Published public private(set) var state: ProcessInfo.ThermalState {
@Published private(set) var state: ProcessInfo.ThermalState {
didSet {
// Determine the appropriate log level based on the thermal state
let logLevel: LogLevel
Expand All @@ -37,11 +46,13 @@ public final class ThermalStateObserver: ObservableObject {
}
}

var statePublisher: AnyPublisher<ProcessInfo.ThermalState, Never> { $state.eraseToAnyPublisher() }

/// Cancellable object to manage notifications
private var notificationCenterCancellable: AnyCancellable?
private var thermalStateProvider: () -> ProcessInfo.ThermalState

convenience init() {
private convenience init() {
self.init { ProcessInfo.processInfo.thermalState }
}

Expand All @@ -62,7 +73,7 @@ public final class ThermalStateObserver: ObservableObject {
}

/// Depending on the Device's thermal state we adapt the request participants resolution.
public var scale: CGFloat {
var scale: CGFloat {
// Determine the appropriate scaling factor based on the thermal state
switch state {
case .nominal:
Expand All @@ -78,3 +89,20 @@ public final class ThermalStateObserver: ObservableObject {
}
}
}

/// Provides the default value of the `Appearance` class.
public struct ThermalStateObserverKey: InjectionKey {
public static var currentValue: any ThermalStateObserving = ThermalStateObserver.shared
}

extension InjectedValues {

public var thermalStateObserver: any ThermalStateObserving {
get {
Self[ThermalStateObserverKey.self]
}
set {
Self[ThermalStateObserverKey.self] = newValue
}
}
}
4 changes: 3 additions & 1 deletion Sources/StreamVideo/WebRTC/WebRTCClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ class WebRTCClient: NSObject, @unchecked Sendable {
private var tempSubscriber: PeerConnection?
private var currentScreenhsareType: ScreensharingType?

@Injected(\.thermalStateObserver) private var thermalStateObserver

var onParticipantsUpdated: (([String: CallParticipant]) -> Void)?
var onSignalConnectionStateChange: ((WebSocketConnectionState) -> ())?
var onParticipantCountUpdated: ((UInt32) -> ())?
Expand Down Expand Up @@ -965,7 +967,7 @@ class WebRTCClient: NSObject, @unchecked Sendable {
log.debug("updating video subscription for user \(value.id) with size \(value.trackSize)", subsystems: .webRTC)
var dimension = Stream_Video_Sfu_Models_VideoDimension()

let scale = ThermalStateObserver.shared.scale
let scale = thermalStateObserver.scale
dimension.height = UInt32(value.trackSize.height / scale)
dimension.width = UInt32(value.trackSize.width / scale)
let trackSubscriptionDetails = trackSubscriptionDetails(
Expand Down
53 changes: 41 additions & 12 deletions Sources/StreamVideoSwiftUI/CallView/VideoRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import StreamVideo
import SwiftUI
import WebRTC
import MetalKit
import Combine

public struct LocalVideoView<Factory: ViewFactory>: View {

Expand Down Expand Up @@ -60,7 +61,7 @@ public struct VideoRendererView: UIViewRepresentable {
public typealias UIViewType = VideoRenderer

@Injected(\.utils) var utils

var id: String
var size: CGSize
var contentMode: UIView.ContentMode
Expand Down Expand Up @@ -93,21 +94,54 @@ public struct VideoRendererView: UIViewRepresentable {

public class VideoRenderer: RTCMTLVideoView {

@Injected(\.thermalStateObserver) private var thermalStateObserver

let queue = DispatchQueue(label: "video-track")

weak var track: RTCVideoTrack?

var feedFrames: ((CMSampleBuffer) -> ())?

private var skipNextFrameRendering = true

var trackId: String? {
self.track?.trackId
private var cancellable: AnyCancellable?

private(set) var preferredFramesPerSecond: Int = UIScreen.main.maximumFramesPerSecond {
didSet {
metalView?.preferredFramesPerSecond = preferredFramesPerSecond
log.debug("🔄 preferredFramesPerSecond was updated to \(preferredFramesPerSecond).")
}
}

private lazy var metalView: MTKView? = { subviews.compactMap { $0 as? MTKView }.first }()
martinmitrevski marked this conversation as resolved.
Show resolved Hide resolved
var trackId: String? { self.track?.trackId }
private var viewSize: CGSize?

private lazy var scale: CGFloat = UIScreen.main.scale
private var scale: CGFloat { thermalStateObserver.scale }

@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }

public override init(frame: CGRect) {
super.init(frame: frame)
cancellable = thermalStateObserver
.statePublisher
.sink { [weak self] in
switch $0 {
case .nominal, .fair:
self?.preferredFramesPerSecond = UIScreen.main.maximumFramesPerSecond
case .serious:
self?.preferredFramesPerSecond = Int(Double(UIScreen.main.maximumFramesPerSecond) * 0.5)
case .critical:
self?.preferredFramesPerSecond = Int(Double(UIScreen.main.maximumFramesPerSecond) * 0.4)
@unknown default:
self?.preferredFramesPerSecond = UIScreen.main.maximumFramesPerSecond
}
}
}

deinit {
cancellable?.cancel()
log.debug("Deinit of video view")
track?.remove(self)
}

public func add(track: RTCVideoTrack) {
queue.sync {
Expand Down Expand Up @@ -167,11 +201,6 @@ public class VideoRenderer: RTCMTLVideoView {
}
}
}

deinit {
log.debug("Deinit of video view")
track?.remove(self)
}
}

extension VideoRenderer {
Expand Down
4 changes: 4 additions & 0 deletions StreamVideo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
40B499CC2AC1A90F00A53B60 /* DeeplinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B499CB2AC1A90F00A53B60 /* DeeplinkTests.swift */; };
40B499CE2AC1AA0900A53B60 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4030E59F2A9DF5BD003E8CBA /* AppEnvironment.swift */; };
40B713692A275F1400D1FE67 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8456E6C5287EB55F004E180E /* AppState.swift */; };
40D6ADDD2ACDB51C00EF5336 /* VideoRenderer_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D6ADDC2ACDB51C00EF5336 /* VideoRenderer_Tests.swift */; };
40D7E7962AB1C9590017095E /* ThermalStateViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D7E7952AB1C9580017095E /* ThermalStateViewModifier.swift */; };
40D946412AA5ECEF00C8861B /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D946402AA5ECEF00C8861B /* CodeScanner.swift */; };
40D946432AA5F65300C8861B /* DemoQRCodeScannerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40D946422AA5F65300C8861B /* DemoQRCodeScannerButton.swift */; };
Expand Down Expand Up @@ -903,6 +904,7 @@
40AB31252A49838000C270E1 /* EventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTests.swift; sourceTree = "<group>"; };
40B499C92AC1A5E100A53B60 /* OSLogDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogDestination.swift; sourceTree = "<group>"; };
40B499CB2AC1A90F00A53B60 /* DeeplinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkTests.swift; sourceTree = "<group>"; };
40D6ADDC2ACDB51C00EF5336 /* VideoRenderer_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRenderer_Tests.swift; sourceTree = "<group>"; };
40D7E7952AB1C9580017095E /* ThermalStateViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThermalStateViewModifier.swift; sourceTree = "<group>"; };
40D946402AA5ECEF00C8861B /* CodeScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = "<group>"; };
40D946422AA5F65300C8861B /* DemoQRCodeScannerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoQRCodeScannerButton.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2107,6 +2109,7 @@
82FF40B62A17C6CD00B4D95E /* ReconnectionView_Tests.swift */,
82FF40B82A17C6D600B4D95E /* RecordingView_Tests.swift */,
82FF40BA2A17C6DF00B4D95E /* ScreenSharingView_Tests.swift */,
40D6ADDC2ACDB51C00EF5336 /* VideoRenderer_Tests.swift */,
);
path = CallView;
sourceTree = "<group>";
Expand Down Expand Up @@ -4370,6 +4373,7 @@
84DCA20C2A38300C000C3411 /* StreamVideoTestCase.swift in Sources */,
82E3BA362A0BABA2001AB93E /* HTTPClient_Mock.swift in Sources */,
82E3BA382A0BADB8001AB93E /* CallController_Mock.swift in Sources */,
40D6ADDD2ACDB51C00EF5336 /* VideoRenderer_Tests.swift in Sources */,
82FF40B92A17C6D600B4D95E /* RecordingView_Tests.swift in Sources */,
82E3BA562A0BAF64001AB93E /* WebSocketEngine_Mock.swift in Sources */,
829F7BFD29FAC116003EBACE /* ParticipantsFullScreenLayout_Tests.swift in Sources */,
Expand Down
57 changes: 57 additions & 0 deletions StreamVideoSwiftUITests/CallView/VideoRenderer_Tests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright © 2023 Stream.io Inc. All rights reserved.
//

@testable import StreamVideoSwiftUI
@testable import StreamVideo
import XCTest
import Combine

final class VideoRenderer_Tests: XCTestCase {

private var mockThermalStateObserver: MockThermalStateObserver! = .init()
private var subject: VideoRenderer!

override func setUp() {
super.setUp()

InjectedValues[\.thermalStateObserver] = mockThermalStateObserver
subject = .init(frame: .zero)
}

override func tearDown() {
mockThermalStateObserver = nil
super.tearDown()
}

// MARK: - preferredFramesPerSecond

func testFPSForNominalThermalState() {
mockThermalStateObserver.state = .nominal
XCTAssertEqual(subject.preferredFramesPerSecond, UIScreen.main.maximumFramesPerSecond)
}

func testFPSForFairThermalState() {
mockThermalStateObserver.state = .fair
XCTAssertEqual(subject.preferredFramesPerSecond, UIScreen.main.maximumFramesPerSecond)
}

func testFPSForSeriousThermalState() {
mockThermalStateObserver.state = .serious
XCTAssertEqual(subject.preferredFramesPerSecond, Int(Double(UIScreen.main.maximumFramesPerSecond) * 0.5))
}

func testFPSForCriticalThermalState() {
mockThermalStateObserver.state = .critical
XCTAssertEqual(subject.preferredFramesPerSecond, Int(Double(UIScreen.main.maximumFramesPerSecond) * 0.4))
}
}

// MARK: - Private Helpers

private final class MockThermalStateObserver: ThermalStateObserving {
var state: ProcessInfo.ThermalState = .nominal { didSet { stateSubject.send(state) } }
lazy var stateSubject: CurrentValueSubject<ProcessInfo.ThermalState, Never> = .init(state)
var statePublisher: AnyPublisher<ProcessInfo.ThermalState, Never> { stateSubject.eraseToAnyPublisher() }
var scale: CGFloat = 1
}
4 changes: 4 additions & 0 deletions StreamVideoTests/Utils/ThermalStateObserverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ final class ThermalStateObserverTests: XCTestCase {
XCTAssertEqual(ThermalStateObserver.shared.state, ProcessInfo.processInfo.thermalState)
}

func test_injectedValueWasSetCorrectly() {
XCTAssertTrue(InjectedValues[\.thermalStateObserver] === ThermalStateObserver.shared)
}

// MARK: - notificationObserver

func test_notificationObserver_stateChangesWhenSystemPostsNotification() {
Expand Down
Loading