-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c4ab29c
commit fdfec37
Showing
4 changed files
with
239 additions
and
95 deletions.
There are no files selected for viewing
47 changes: 47 additions & 0 deletions
47
Sources/StreamVideoSwiftUI/CallView/VideoRenderer/LocalVideoView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
Sources/StreamVideoSwiftUI/CallView/VideoRenderer/VideoRendererView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.