Skip to content

Commit

Permalink
Fix stretchy tracks (#462)
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis authored Jul 17, 2024
1 parent c4ab29c commit fdfec37
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 95 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation
import StreamVideo
import SwiftUI

public struct LocalVideoView<Factory: ViewFactory>: View {

@Injected(\.streamVideo) var streamVideo

private let callSettings: CallSettings
private var viewFactory: Factory
private var participant: CallParticipant
private var idSuffix: String
private var call: Call?
private var availableFrame: CGRect

public init(
viewFactory: Factory,
participant: CallParticipant,
idSuffix: String = "local",
callSettings: CallSettings,
call: Call?,
availableFrame: CGRect
) {
self.viewFactory = viewFactory
self.participant = participant
self.idSuffix = idSuffix
self.callSettings = callSettings
self.call = call
self.availableFrame = availableFrame
}

public var body: some View {
viewFactory.makeVideoParticipantView(
participant: participant,
id: "\(streamVideo.user.id)-\(idSuffix)",
availableFrame: availableFrame,
contentMode: .scaleAspectFill,
customData: ["videoOn": .bool(callSettings.videoOn)],
call: call
)
.adjustVideoFrame(to: availableFrame.width, ratio: availableFrame.width / availableFrame.height)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,103 +8,18 @@ import StreamVideo
import StreamWebRTC
import SwiftUI

public struct LocalVideoView<Factory: ViewFactory>: View {

@Injected(\.streamVideo) var streamVideo

private let callSettings: CallSettings
private var viewFactory: Factory
private var participant: CallParticipant
private var idSuffix: String
private var call: Call?
private var availableFrame: CGRect

public init(
viewFactory: Factory,
participant: CallParticipant,
idSuffix: String = "local",
callSettings: CallSettings,
call: Call?,
availableFrame: CGRect
) {
self.viewFactory = viewFactory
self.participant = participant
self.idSuffix = idSuffix
self.callSettings = callSettings
self.call = call
self.availableFrame = availableFrame
}

public var body: some View {
viewFactory.makeVideoParticipantView(
participant: participant,
id: "\(streamVideo.user.id)-\(idSuffix)",
availableFrame: availableFrame,
contentMode: .scaleAspectFill,
customData: ["videoOn": .bool(callSettings.videoOn)],
call: call
)
.adjustVideoFrame(to: availableFrame.width, ratio: availableFrame.width / availableFrame.height)
}
}

public struct VideoRendererView: UIViewRepresentable {

public typealias UIViewType = VideoRenderer

@Injected(\.utils) var utils
@Injected(\.colors) var colors
@Injected(\.videoRendererPool) private var videoRendererPool

var id: String

var size: CGSize

var contentMode: UIView.ContentMode

/// The parameter is used as an optimisation that works with the ViewRenderer Cache that's in place.
/// In cases where there is no video available, we will render a dummy VideoRenderer that won't try
/// to get a handle on the cached VideoRenderer, resolving the issue where video tracks may get dark.
var showVideo: Bool

var handleRendering: (VideoRenderer) -> Void

public init(
id: String,
size: CGSize,
contentMode: UIView.ContentMode = .scaleAspectFill,
showVideo: Bool = true,
handleRendering: @escaping (VideoRenderer) -> Void
) {
self.id = id
self.size = size
self.handleRendering = handleRendering
self.showVideo = showVideo
self.contentMode = contentMode
}

public func makeUIView(context: Context) -> VideoRenderer {
let view = videoRendererPool.acquireRenderer(size: size)
view.videoContentMode = contentMode
view.backgroundColor = colors.participantBackground
if showVideo {
handleRendering(view)
}
return view
}

public func updateUIView(_ uiView: VideoRenderer, context: Context) {
if showVideo {
handleRendering(uiView)
}
}
}

/// A custom video renderer based on RTCMTLVideoView for rendering RTCVideoTrack objects.
public class VideoRenderer: RTCMTLVideoView {

@Injected(\.thermalStateObserver) private var thermalStateObserver
@Injected(\.videoRendererPool) private var videoRendererPool

private let _windowSubject: PassthroughSubject<UIWindow?, Never> = .init()
private let _superviewSubject: PassthroughSubject<UIView?, Never> = .init()
private let _frameSubject: PassthroughSubject<CGRect, Never> = .init()

var windowPublisher: AnyPublisher<UIWindow?, Never> { _windowSubject.eraseToAnyPublisher() }
var superviewPublisher: AnyPublisher<UIView?, Never> { _superviewSubject.eraseToAnyPublisher() }
var framePublisher: AnyPublisher<CGRect, Never> { _frameSubject.eraseToAnyPublisher() }

/// DispatchQueue for synchronizing access to the video track.
let queue = DispatchQueue(label: "video-track")
Expand Down Expand Up @@ -186,13 +101,19 @@ public class VideoRenderer: RTCMTLVideoView {
override public func layoutSubviews() {
super.layoutSubviews()
viewSize = bounds.size
_frameSubject.send(frame)
}

override public func willMove(toWindow newWindow: UIWindow?) {
_windowSubject.send(newWindow)
super.willMove(toWindow: newWindow)
}

/// Overrides the willMove(toSuperview:) method to release the renderer when removed from its superview.
override public func willMove(toSuperview newSuperview: UIView?) {
_superviewSubject.send(newSuperview)
super.willMove(toSuperview: newSuperview)
if newSuperview == nil {
videoRendererPool.releaseRenderer(self)
// Clean up any rendered frames.
setSize(.zero)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Combine
import Foundation
import StreamVideo
import SwiftUI

/// A view that wraps a `VideoRenderer` and integrates with SwiftUI.
public struct VideoRendererView: UIViewRepresentable {

/// The type of the `UIView` being represented.
public typealias UIViewType = VideoRenderer

/// Injected dependency for accessing color configurations.
@Injected(\.colors) var colors

/// The identifier for the video renderer.
var id: String

/// The size of the video renderer view.
var size: CGSize

/// The content mode for the video renderer.
var contentMode: UIView.ContentMode

/// A flag to determine whether video should be shown. Optimizes rendering by using a dummy renderer when false.
var showVideo: Bool

/// A closure to handle the rendering of the video.
var handleRendering: (VideoRenderer) -> Void

/// Initializes a new instance of `VideoRendererView`.
/// - Parameters:
/// - id: The identifier for the video renderer.
/// - size: The size of the video renderer view.
/// - contentMode: The content mode for the video renderer. Default is `.scaleAspectFill`.
/// - showVideo: A flag to determine whether video should be shown. Default is `true`.
/// - handleRendering: A closure to handle the rendering of the video.
public init(
id: String,
size: CGSize,
contentMode: UIView.ContentMode = .scaleAspectFill,
showVideo: Bool = true,
handleRendering: @escaping (VideoRenderer) -> Void
) {
self.id = id
self.size = size
self.handleRendering = handleRendering
self.showVideo = showVideo
self.contentMode = contentMode
}

/// Dismantles the `UIView` when it is no longer needed.
/// - Parameters:
/// - uiView: The `VideoRenderer` to dismantle.
/// - coordinator: The coordinator associated with the view.
public static func dismantleUIView(
_ uiView: VideoRenderer,
coordinator: Coordinator
) {
coordinator.dismantle()
}

/// Creates the `VideoRenderer` view.
/// - Parameter context: The context containing information about the current state of the system.
/// - Returns: A configured `VideoRenderer` instance.
public func makeUIView(context: Context) -> VideoRenderer {
context.coordinator.renderer.frame = .init(
origin: context.coordinator.renderer.frame.origin,
size: size
)
context.coordinator.renderer.videoContentMode = contentMode
context.coordinator.renderer.backgroundColor = colors.participantBackground

if showVideo {
handleRendering(context.coordinator.renderer)
}
return context.coordinator.renderer
}

/// Updates the `VideoRenderer` view when the state changes.
/// - Parameters:
/// - uiView: The `VideoRenderer` to update.
/// - context: The context containing information about the current state of the system.
public func updateUIView(_ uiView: VideoRenderer, context: Context) {
if showVideo {
handleRendering(uiView)
}
}

/// Creates the coordinator for managing the view.
/// - Returns: A new `Coordinator` instance.
public func makeCoordinator() -> Coordinator {
Coordinator(handleRendering: handleRendering)
}
}

/// Extension for `VideoRendererView` to define the `Coordinator` class.
extension VideoRendererView {
/// A class to coordinate the `VideoRendererView` and manage its lifecycle.
public final class Coordinator {
/// Injected dependency for accessing the video renderer pool.
@Injected(\.videoRendererPool) private var videoRendererPool

/// A closure to handle the rendering of the video.
private let handleRendering: ((VideoRenderer) -> Void)?
/// A disposable bag to manage cancellable subscriptions.
private let disposableBag = DisposableBag()

/// The video renderer managed by this coordinator.
fileprivate private(set) lazy var renderer: VideoRenderer = videoRendererPool
.acquireRenderer(size: .zero)

/// Initializes a new instance of the coordinator.
/// - Parameter handleRendering: A closure to handle the rendering of the video.
init(handleRendering: ((VideoRenderer) -> Void)?) {
self.handleRendering = handleRendering
_ = renderer
setupRendererObservation()
}

deinit {
dismantle()
}

/// Dismantles the video renderer and releases resources.
func dismantle() {
renderer.track?.remove(renderer)
disposableBag.removeAll()
videoRendererPool.releaseRenderer(renderer)
}

// MARK: Private API

/// Sets up observation for the renderer's window and superview.
private func setupRendererObservation() {
renderer
.windowPublisher
.map { $0 != nil }
.removeDuplicates()
.sink { [weak self] in
guard let self else { return }
if $0 { handleRendering?(renderer) }
}
.store(in: disposableBag)

renderer
.superviewPublisher
.map { $0 != nil }
.removeDuplicates()
.sink { [weak self] in
guard let self else { return }
if $0 { handleRendering?(renderer) }
}
.store(in: disposableBag)
}
}
}
Loading

0 comments on commit fdfec37

Please sign in to comment.