Skip to content

Commit

Permalink
LongPress to focus on LocalVideo (#221)
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis authored Oct 24, 2023
1 parent 219343a commit 1d8a152
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 79 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
# Upcoming

### 🔄 Changed
- You can now focus on a desired point in the local video stream. [#221](https://github.com/GetStream/stream-video-swift/pull/221)

# [0.4.1](https://github.com/GetStream/stream-video-swift/releases/tag/0.4.1)
_October 16, 2023_
Expand Down
2 changes: 1 addition & 1 deletion DemoApp/Sources/Components/DemoAppViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class DemoAppViewFactory: ViewFactory {
func makeCallTopView(viewModel: CallViewModel) -> DemoCallTopView {
DemoCallTopView(viewModel: viewModel)
}

func makeVideoCallParticipantModifier(
participant: CallParticipant,
call: Call?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// Copyright © 2023 Stream.io Inc. All rights reserved.
//

import StreamVideo
import SwiftUI
import AVFoundation

/// A `ViewModifier` that adds a long press to focus gesture to a SwiftUI view.
struct LongPressToFocusViewModifier: ViewModifier {

/// The frame within which the focus gesture can be recognized.
var availableFrame: CGRect

/// The handler to call with the focus point when the gesture is recognized.
var handler: (CGPoint) -> Void

/// Modifies the content by adding a long press gesture recognizer.
///
/// - Parameter content: The content to be modified.
/// - Returns: The modified content with the long press to focus gesture added.
func body(content: Content) -> some View {
content
.gesture(
// A long press gesture requiring a minimum of 0.5 seconds to be recognized.
LongPressGesture(minimumDuration: 0.5)
// Sequence the long press gesture with a drag gesture in the local coordinate space.
.sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .local))
.onEnded { value in
// Handle the end of the gesture sequence.
switch value {
// If the long press gesture was succeeded by a drag gesture.
case .second(true, let drag):
// If the drag gesture has a valid location.
if let location = drag?.location {
// Convert the point to the focus interest point and call the handler.
handler(convertToPointOfInterest(location))
}
// All other cases do nothing.
default:
break
}
}
)
}

/// Converts a point within the view's coordinate space to a point of interest for camera focus.
///
/// The conversion is based on the `availableFrame` property, flipping the axis
/// and normalizing the point to the range [0, 1].
///
/// - Parameter point: The point to convert.
/// - Returns: The converted point of interest.
func convertToPointOfInterest(_ point: CGPoint) -> CGPoint {
CGPoint(
x: point.y / availableFrame.height,
y: 1.0 - point.x / availableFrame.width
)
}
}

extension View {

/// Adds a long press to focus gesture to the view.
///
/// - Parameters:
/// - availableFrame: The frame within which the focus gesture can be recognized.
/// - handler: The closure to call with the focus point when the gesture is recognized.
/// - Returns: A modified view with the long press to focus gesture added.
@ViewBuilder
func longPressToFocus(
availableFrame: CGRect,
handler: @escaping (CGPoint) -> Void
) -> some View {
// Apply the view modifier to add the gesture to the view.
modifier(
LongPressToFocusViewModifier(
availableFrame: availableFrame,
handler: handler
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,66 +34,23 @@ struct DemoVideoCallParticipantModifier: ViewModifier {

func body(content: Content) -> some View {
content
.adjustVideoFrame(to: availableFrame.size.width, ratio: ratio)
.overlay(
ZStack {
BottomView(content: {
HStack {
ParticipantInfoView(
participant: participant,
isPinned: participant.isPinned
)

if showAllInfo {
Spacer()
ConnectionQualityIndicator(
connectionQuality: participant.connectionQuality
)
}
}
.padding(.bottom, 2)
})
.padding(.all, showAllInfo ? 16 : 8)

if participant.isSpeaking && participantCount > 1 {
Rectangle()
.strokeBorder(Color.blue.opacity(0.7), lineWidth: 2)
}

if popoverShown {
ParticipantPopoverView(
participant: participant,
call: call,
popoverShown: $popoverShown
) {
PopoverButton(
title: "Show stats",
popoverShown: $popoverShown) {
statsShown = true
}
}
}

VStack(alignment: .center) {
Spacer()
if statsShown, let call {
ParticipantStatsView(call: call, participant: participant)
.padding(.bottom)
}
}
}
.modifier(
VideoCallParticipantModifier(
participant: participant,
call: call,
availableFrame: availableFrame,
ratio: ratio,
showAllInfo: showAllInfo)
)
.modifier(ReactionsViewModifier(participant: participant, availableSize: availableFrame.size))
.onTapGesture(count: 2, perform: {
popoverShown = true
})
.onTapGesture(count: 1) {
if popoverShown {
popoverShown = false
}
if statsShown {
statsShown = false
}
.modifier(
ReactionsViewModifier(
participant: participant,
availableSize: availableFrame.size
)
)
.longPressToFocus(availableFrame: availableFrame) {
guard call?.state.sessionId == participant.sessionId else { return }
try? call?.focus(at: $0)
}
}

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Video roadmap and changelog is available [here](https://github.com/GetStream/pro
- [ ] Video UIKit tutorial
- [ ] Improve logging / Sentry integration
- [ ] Camera controls
- [ ] Tap to focus
- [x] Tap to focus
- [ ] Complete reconnection flows
- [ ] Complete Livestreaming APIs and Tutorials for hosting & watching
- [ ] Dynascale 2.0
Expand Down
20 changes: 19 additions & 1 deletion Sources/StreamVideo/Call.swift
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,25 @@ public class Call: @unchecked Sendable, WSEventsSubscriber {
unpinRequest: unpinRequest
)
}


/// Tries to focus the camera at the specified point within the view.
///
/// The method delegates the focus action to the `callController`'s `focus(at:)`
/// method, which is expected to handle the camera focus logic. If an error occurs during the process,
/// it throws an exception.
///
/// - Parameter point: A `CGPoint` value representing the location within the view where the
/// camera should focus. The point (0, 0) is at the top-left corner of the view, and the point (1, 1) is at
/// the bottom-right corner.
/// - Throws: An error if the focus operation cannot be completed. The type of error depends on
/// the underlying implementation in the `callController`.
///
/// - Note: Ensure that the device supports tap to focus and that it is enabled before calling this
/// method. Otherwise, it might result in an error.
public func focus(at point: CGPoint) throws {
try callController.focus(at: point)
}

//MARK: - Internal

internal func update(reconnectionStatus: ReconnectionStatus) {
Expand Down
23 changes: 22 additions & 1 deletion Sources/StreamVideo/Controllers/CallController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,28 @@ class CallController {
webRTCClient?.ownCapabilities = ownCapabilities
}
}


/// Initiates a focus operation at a specific point on the camera's view.
///
/// This method attempts to focus the camera at the given point by calling the `focus(at:)`
/// method on the current WebRTC client. The focus point is specified as a `CGPoint` within the
/// coordinate space of the view.
///
/// - Parameter point: A `CGPoint` value representing the location within the view where the
/// camera should attempt to focus. The coordinate space of the point is typically normalized to the
/// range [0, 1], where (0, 0) represents the top-left corner of the view, and (1, 1) represents the
/// bottom-right corner.
/// - Throws: An error if the focus operation cannot be completed. This might occur if there is no
/// current WebRTC client available, if the camera does not support tap to focus, or if an internal error
/// occurs within the WebRTC client.
///
/// - Note: Before calling this method, ensure that the device's camera supports tap to focus
/// functionality and that the current WebRTC client is properly configured and connected. Otherwise,
/// the method may throw an error.
func focus(at point: CGPoint) throws {
try currentWebRTCClient().focus(at: point)
}

/// Cleans up the call controller.
func cleanUp() {
guard call != nil else { return }
Expand Down
56 changes: 55 additions & 1 deletion Sources/StreamVideo/WebRTC/VideoCapturer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,61 @@ class VideoCapturer: CameraVideoCapturing {
}
}
}


/// Initiates a focus and exposure operation at the specified point on the camera's view.
///
/// This method attempts to focus the camera and set the exposure at a specific point by interacting
/// with the device's capture session.
/// It requires the `videoCapturer` property to be cast to `RTCCameraVideoCapturer`, and for
/// a valid `AVCaptureDeviceInput` to be accessible.
/// If these conditions are not met, it throws a `ClientError.Unexpected` error.
///
/// - Parameter point: A `CGPoint` representing the location within the view where the camera
/// should adjust focus and exposure.
/// - Throws: A `ClientError.Unexpected` error if the necessary video capture components are
/// not available or properly configured.
///
/// - Note: Ensure that the `point` is normalized to the camera's coordinate space, ranging
/// from (0,0) at the top-left to (1,1) at the bottom-right.
func focus(at point: CGPoint) throws {
guard
let captureSession = (videoCapturer as? RTCCameraVideoCapturer)?.captureSession,
let device = captureSession.inputs.first as? AVCaptureDeviceInput
else {
throw ClientError.Unexpected()
}

do {
try device.device.lockForConfiguration()

if device.device.isFocusPointOfInterestSupported {
log.debug("Will focus at point: \(point)")
device.device.focusPointOfInterest = point

if device.device.isFocusModeSupported(.autoFocus) {
device.device.focusMode = .autoFocus
} else {
log.warning("There are no supported focusMode.")
}

log.debug("Will set exposure at point: \(point)")
if device.device.isExposurePointOfInterestSupported {
device.device.exposurePointOfInterest = point

if device.device.isExposureModeSupported(.autoExpose) {
device.device.exposureMode = .autoExpose
} else {
log.warning("There are no supported exposureMode.")
}
}
}

device.device.unlockForConfiguration()
} catch {
log.error(error)
}
}

//MARK: - private

private func checkForBackgroundCameraAccess() {
Expand Down
24 changes: 23 additions & 1 deletion Sources/StreamVideo/WebRTC/WebRTCClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,29 @@ class WebRTCClient: NSObject, @unchecked Sendable {
datacenter: signalService.hostname
)
}


/// Initiates a camera focus operation at the specified point.
///
/// This method attempts to focus the camera at a specific point on the screen.
/// It requires the `videoCapturer` property to be properly cast to `VideoCapturer` type.
/// If the casting fails, it throws a `ClientError.Unexpected` error.
///
/// - Parameter point: A `CGPoint` representing the location within the view where the camera
/// should focus.
/// - Throws: A `ClientError.Unexpected` error if `videoCapturer` cannot be cast to
/// `VideoCapturer`.
///
/// - Note: The `point` parameter should be provided in the coordinate space of the view, where
/// (0,0) is the top-left corner, and (1,1) is the bottom-right corner. Make sure the camera supports
/// tap-to-focus functionality before invoking this method.
func focus(at point: CGPoint) throws {
guard let videoCapturer = videoCapturer as? VideoCapturer else {
throw ClientError.Unexpected()
}

try videoCapturer.focus(at: point)
}

// MARK: - private

private func handleOnSocketConnected() {
Expand Down
30 changes: 16 additions & 14 deletions Sources/StreamVideoSwiftUI/CallContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,22 @@ public struct WaitingLocalUserView<Factory: ViewFactory>: View {

public var body: some View {
ZStack {
if let localParticipant = viewModel.localParticipant {
LocalVideoView(
viewFactory: viewFactory,
participant: localParticipant,
idSuffix: "waiting",
callSettings: viewModel.callSettings,
call: viewModel.call
)
} else {
DefaultBackgroundGradient()
}

VStack {
Spacer()
DefaultBackgroundGradient()
.edgesIgnoringSafeArea(.all)

VStack() {
if let localParticipant = viewModel.localParticipant {
LocalVideoView(
viewFactory: viewFactory,
participant: localParticipant,
idSuffix: "waiting",
callSettings: viewModel.callSettings,
call: viewModel.call
)
} else {
Spacer()
}

viewFactory.makeCallControlsView(viewModel: viewModel)
.opacity(viewModel.callingState == .reconnecting ? 0 : 1)
}
Expand Down
Loading

0 comments on commit 1d8a152

Please sign in to comment.