@@ -140,6 +140,18 @@ public class VideoView: NativeView, Loggable {
140140 set { _state. mutate { $0. transitionDuration = newValue } }
141141 }
142142
143+ @objc
144+ public var isPinchToZoomEnabled : Bool {
145+ get { _state. isPinchToZoomEnabled }
146+ set { _state. mutate { $0. isPinchToZoomEnabled = newValue } }
147+ }
148+
149+ @objc
150+ public var isAutoZoomResetEnabled : Bool {
151+ get { _state. isAutoZoomResetEnabled }
152+ set { _state. mutate { $0. isAutoZoomResetEnabled = newValue } }
153+ }
154+
143155 @objc
144156 public var isDebugMode : Bool {
145157 get { _state. isDebugMode }
@@ -192,9 +204,12 @@ public class VideoView: NativeView, Loggable {
192204 var renderTarget : RenderTarget = . primary
193205 var isSwapping : Bool = false
194206 var remainingRenderCountBeforeSwap : Int = 0 // Number of frames to be rendered on secondary until swap is initiated
195- var transitionMode : TransitionMode = . flip
207+ var transitionMode : TransitionMode = . crossDissolve
196208 var transitionDuration : TimeInterval = 0.3
197209
210+ var isPinchToZoomEnabled : Bool = false
211+ var isAutoZoomResetEnabled : Bool = true
212+
198213 // Only used for rendering local tracks
199214 var captureOptions : VideoCaptureOptions ? = nil
200215 var captureDevice : AVCaptureDevice ? = nil
@@ -219,6 +234,12 @@ public class VideoView: NativeView, Loggable {
219234 private var _currentFPS : Int = 0
220235 private var _frameCount : Int = 0
221236
237+ #if os(iOS) || os(visionOS)
238+ private lazy var _pinchGestureRecognizer = UIPinchGestureRecognizer ( target: self , action: #selector( _handlePinchGesture ( _: ) ) )
239+ // This should be thread safe so it's not required to be guarded by the lock
240+ private var _pinchStartZoomFactor : CGFloat = 0.0
241+ #endif
242+
222243 override public init( frame: CGRect = . zero) {
223244 // initial state
224245 _state = StateSync ( State ( viewSize: frame. size) )
@@ -326,6 +347,15 @@ public class VideoView: NativeView, Loggable {
326347 }
327348 }
328349
350+ #if os(iOS) || os(visionOS)
351+ let newIsPinchToZoomEnabled = newState. isPinchToZoomEnabled
352+ if newIsPinchToZoomEnabled != oldState. isPinchToZoomEnabled {
353+ Task . detached { @MainActor in
354+ self . _pinchGestureRecognizer. isEnabled = newIsPinchToZoomEnabled
355+ }
356+ }
357+ #endif
358+
329359 if newState. isDebugMode != oldState. isDebugMode {
330360 // fps timer
331361 if newState. isDebugMode {
@@ -359,8 +389,44 @@ public class VideoView: NativeView, Loggable {
359389
360390 await self. _renderTimer. restart ( )
361391 }
392+
393+ #if os(iOS) || os(visionOS)
394+ // Add pinch gesture recognizer
395+ addGestureRecognizer( _pinchGestureRecognizer)
396+ _pinchGestureRecognizer. isEnabled = _state. isPinchToZoomEnabled
397+ #endif
362398 }
363399
400+ #if os(iOS) || os(visionOS)
401+ @objc func _handlePinchGesture( _ sender: UIPinchGestureRecognizer) {
402+ if let track = _state. track as? LocalVideoTrack ,
403+ let capturer = track. capturer as? CameraCapturer ,
404+ let device = capturer. device
405+ {
406+ if sender. state == . began {
407+ _pinchStartZoomFactor = device. videoZoomFactor
408+ } else {
409+ do {
410+ try device. lockForConfiguration ( )
411+ defer { device. unlockForConfiguration ( ) }
412+
413+ if sender. state == . changed {
414+ let minZoom = device. minAvailableVideoZoomFactor
415+ let maxZoom = device. maxAvailableVideoZoomFactor
416+ device. videoZoomFactor = ( _pinchStartZoomFactor * sender. scale) . clamped ( to: minZoom ... maxZoom)
417+ } else if sender. state == . ended || sender. state == . cancelled, _state. isAutoZoomResetEnabled {
418+ // Zoom to default zoom factor
419+ let defaultZoomFactor = LKRTCCameraVideoCapturer . defaultZoomFactor ( forDeviceType: device. deviceType)
420+ device. ramp ( toVideoZoomFactor: defaultZoomFactor, withRate: 32.0 )
421+ }
422+ } catch {
423+ log ( " Failed to adjust videoZoomFactor " , . warning)
424+ }
425+ }
426+ }
427+ }
428+ #endif
429+
364430 @available( * , unavailable)
365431 required init? ( coder _: NSCoder) {
366432 fatalError ( " init(coder:) has not been implemented " )
@@ -527,7 +593,7 @@ private extension VideoView {
527593
528594 func _shouldMirror( ) -> Bool {
529595 switch _state. mirrorMode {
530- case . auto: return _state. captureDevice? . realPosition == . front
596+ case . auto: return _state. captureDevice? . facingPosition == . front
531597 case . off: return false
532598 case . mirror: return true
533599 }
@@ -661,7 +727,7 @@ extension VideoView: VideoRenderer {
661727
662728 // Currently only for iOS
663729 #if os(iOS)
664- let ( mode, duration, position) = _state. read { ( $0. transitionMode, $0. transitionDuration, $0. captureDevice? . realPosition ) }
730+ let ( mode, duration, position) = _state. read { ( $0. transitionMode, $0. transitionDuration, $0. captureDevice? . facingPosition ) }
665731 if let transitionOption = mode. toAnimationOption ( fromPosition: position) {
666732 UIView . transition ( with: self , duration: duration, options: transitionOption, animations: block, completion: nil )
667733 } else {
@@ -801,16 +867,6 @@ private extension VideoView {
801867 }
802868}
803869
804- extension AVCaptureDevice {
805- var realPosition : AVCaptureDevice . Position {
806- if deviceType == . builtInWideAngleCamera, position == . unspecified {
807- return . front
808- }
809-
810- return position
811- }
812- }
813-
814870#if os(iOS)
815871extension VideoView. TransitionMode {
816872 func toAnimationOption( fromPosition position: AVCaptureDevice . Position ? = nil ) -> UIView . AnimationOptions ? {
0 commit comments