@stream-io/video-react-native-sdk 1.0.0-rc2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +14 -83
  2. package/dist/commonjs/components/Call/CallContent/CallContent.js +10 -5
  3. package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
  4. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +109 -0
  5. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -0
  6. package/dist/commonjs/components/Call/CallContent/index.js +11 -0
  7. package/dist/commonjs/components/Call/CallContent/index.js.map +1 -1
  8. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +4 -3
  9. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  10. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +4 -3
  11. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  12. package/dist/commonjs/hooks/useAutoEnterPiPEffect.js +3 -3
  13. package/dist/commonjs/hooks/useAutoEnterPiPEffect.js.map +1 -1
  14. package/dist/commonjs/hooks/useIsInPiPMode.js +4 -4
  15. package/dist/commonjs/hooks/useIsInPiPMode.js.map +1 -1
  16. package/dist/commonjs/providers/StreamCall.js +5 -2
  17. package/dist/commonjs/providers/StreamCall.js.map +1 -1
  18. package/dist/commonjs/utils/internal/shouldDisableIOSLocalVideoOnBackground.js +10 -0
  19. package/dist/commonjs/utils/internal/shouldDisableIOSLocalVideoOnBackground.js.map +1 -0
  20. package/dist/commonjs/version.js +1 -1
  21. package/dist/commonjs/version.js.map +1 -1
  22. package/dist/module/components/Call/CallContent/CallContent.js +10 -5
  23. package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
  24. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +101 -0
  25. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -0
  26. package/dist/module/components/Call/CallContent/index.js +1 -0
  27. package/dist/module/components/Call/CallContent/index.js.map +1 -1
  28. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +4 -3
  29. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  30. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +4 -3
  31. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  32. package/dist/module/hooks/useAutoEnterPiPEffect.js +3 -3
  33. package/dist/module/hooks/useAutoEnterPiPEffect.js.map +1 -1
  34. package/dist/module/hooks/useIsInPiPMode.js +4 -4
  35. package/dist/module/hooks/useIsInPiPMode.js.map +1 -1
  36. package/dist/module/providers/StreamCall.js +5 -2
  37. package/dist/module/providers/StreamCall.js.map +1 -1
  38. package/dist/module/utils/internal/shouldDisableIOSLocalVideoOnBackground.js +4 -0
  39. package/dist/module/utils/internal/shouldDisableIOSLocalVideoOnBackground.js.map +1 -0
  40. package/dist/module/version.js +1 -1
  41. package/dist/module/version.js.map +1 -1
  42. package/dist/typescript/components/Call/CallContent/CallContent.d.ts +6 -1
  43. package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
  44. package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts +7 -0
  45. package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -0
  46. package/dist/typescript/components/Call/CallContent/index.d.ts +1 -0
  47. package/dist/typescript/components/Call/CallContent/index.d.ts.map +1 -1
  48. package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts +2 -2
  49. package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
  50. package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts +2 -2
  51. package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
  52. package/dist/typescript/hooks/useAutoEnterPiPEffect.d.ts +1 -1
  53. package/dist/typescript/hooks/useAutoEnterPiPEffect.d.ts.map +1 -1
  54. package/dist/typescript/hooks/useIsInPiPMode.d.ts +1 -1
  55. package/dist/typescript/hooks/useIsInPiPMode.d.ts.map +1 -1
  56. package/dist/typescript/providers/StreamCall.d.ts.map +1 -1
  57. package/dist/typescript/utils/internal/shouldDisableIOSLocalVideoOnBackground.d.ts +4 -0
  58. package/dist/typescript/utils/internal/shouldDisableIOSLocalVideoOnBackground.d.ts.map +1 -0
  59. package/dist/typescript/version.d.ts +1 -1
  60. package/dist/typescript/version.d.ts.map +1 -1
  61. package/ios/PictureInPicture/SampleBufferVideoCallView.swift +52 -0
  62. package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +83 -0
  63. package/ios/PictureInPicture/StreamBufferTransformer.swift +96 -0
  64. package/ios/PictureInPicture/StreamPictureInPictureController.swift +185 -0
  65. package/ios/PictureInPicture/StreamPictureInPictureTrackStateAdapter.swift +68 -0
  66. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +250 -0
  67. package/ios/PictureInPicture/StreamPixelBufferPool.swift +118 -0
  68. package/ios/PictureInPicture/StreamPixelBufferRepository.swift +98 -0
  69. package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +249 -0
  70. package/ios/PictureInPicture/StreamYUVToARGBConversion.swift +128 -0
  71. package/ios/PictureInPicture/WindowSizePolicy/StreamPictureInPictureAdaptiveWindowSizePolicy.swift +25 -0
  72. package/ios/PictureInPicture/WindowSizePolicy/StreamPictureInPictureFixedWindowSizePolicy.swift +29 -0
  73. package/ios/PictureInPicture/WindowSizePolicy/StreamPictureInPictureWindowSizePolicy.swift +14 -0
  74. package/ios/PictureInPicture/YpCbCrPixelRange+Default.swift +32 -0
  75. package/ios/RTCViewPip.swift +69 -0
  76. package/ios/RTCViewPipManager.mm +16 -0
  77. package/ios/RTCViewPipManager.swift +34 -0
  78. package/ios/StreamVideoReactNative-Bridging-Header.h +11 -0
  79. package/package.json +4 -4
  80. package/src/components/Call/CallContent/CallContent.tsx +58 -40
  81. package/src/components/Call/CallContent/RTCViewPipIOS.tsx +138 -0
  82. package/src/components/Call/CallContent/index.ts +1 -0
  83. package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +7 -3
  84. package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +7 -3
  85. package/src/hooks/useAutoEnterPiPEffect.tsx +7 -3
  86. package/src/hooks/useIsInPiPMode.tsx +6 -4
  87. package/src/providers/StreamCall.tsx +5 -2
  88. package/src/utils/internal/shouldDisableIOSLocalVideoOnBackground.ts +3 -0
  89. package/src/version.ts +1 -1
  90. package/stream-video-react-native.podspec +27 -4
  91. package/ios/StreamVideoReactNative.xcodeproj/project.pbxproj +0 -274
  92. package/ios/StreamVideoReactNative.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  93. package/ios/StreamVideoReactNative.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
@@ -5,7 +5,7 @@ import { CallContentProps } from '../CallContent';
5
5
  /**
6
6
  * Props for the CallParticipantsSpotlight component.
7
7
  */
8
- export type CallParticipantsSpotlightProps = ParticipantViewComponentProps & Pick<CallContentProps, 'supportedReactions' | 'CallParticipantsList' | 'ScreenShareOverlay'> & Pick<CallParticipantsListComponentProps, 'ParticipantView'> & {
8
+ export type CallParticipantsSpotlightProps = ParticipantViewComponentProps & Pick<CallContentProps, 'supportedReactions' | 'CallParticipantsList' | 'ScreenShareOverlay' | 'disablePictureInPicture'> & Pick<CallParticipantsListComponentProps, 'ParticipantView'> & {
9
9
  /**
10
10
  * Check if device is in landscape mode.
11
11
  * This will apply the landscape mode styles to the component.
@@ -16,5 +16,5 @@ export type CallParticipantsSpotlightProps = ParticipantViewComponentProps & Pic
16
16
  * Component used to display the list of participants in a spotlight mode.
17
17
  * This can be used when you want to render the screen sharing stream.
18
18
  */
19
- export declare const CallParticipantsSpotlight: ({ CallParticipantsList, ParticipantLabel, ParticipantNetworkQualityIndicator, ParticipantReaction, ParticipantVideoFallback, ParticipantView, ScreenShareOverlay, VideoRenderer, supportedReactions, landscape, }: CallParticipantsSpotlightProps) => React.JSX.Element;
19
+ export declare const CallParticipantsSpotlight: ({ CallParticipantsList, ParticipantLabel, ParticipantNetworkQualityIndicator, ParticipantReaction, ParticipantVideoFallback, ParticipantView, ScreenShareOverlay, VideoRenderer, supportedReactions, landscape, disablePictureInPicture, }: CallParticipantsSpotlightProps) => React.JSX.Element;
20
20
  //# sourceMappingURL=CallParticipantsSpotlight.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CallParticipantsSpotlight.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallLayout/CallParticipantsSpotlight.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAS1B,OAAO,EAEL,kCAAkC,EACnC,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAEL,6BAA6B,EAC9B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,6BAA6B,GACxE,IAAI,CACF,gBAAgB,EAChB,oBAAoB,GAAG,sBAAsB,GAAG,oBAAoB,CACrE,GACD,IAAI,CAAC,kCAAkC,EAAE,iBAAiB,CAAC,GAAG;IAC5D;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,yBAAyB,sNAWnC,8BAA8B,sBAmGhC,CAAC"}
1
+ {"version":3,"file":"CallParticipantsSpotlight.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallLayout/CallParticipantsSpotlight.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAS1B,OAAO,EAEL,kCAAkC,EACnC,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAEL,6BAA6B,EAC9B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGlD;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,6BAA6B,GACxE,IAAI,CACF,gBAAgB,EACd,oBAAoB,GACpB,sBAAsB,GACtB,oBAAoB,GACpB,yBAAyB,CAC5B,GACD,IAAI,CAAC,kCAAkC,EAAE,iBAAiB,CAAC,GAAG;IAC5D;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,yBAAyB,+OAYnC,8BAA8B,sBAmGhC,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare function useAutoEnterPiPEffect(): void;
1
+ export declare function useAutoEnterPiPEffect(disablePictureInPicture: boolean | undefined): void;
2
2
  //# sourceMappingURL=useAutoEnterPiPEffect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAutoEnterPiPEffect.d.ts","sourceRoot":"","sources":["../../../src/hooks/useAutoEnterPiPEffect.tsx"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,SAYpC"}
1
+ {"version":3,"file":"useAutoEnterPiPEffect.d.ts","sourceRoot":"","sources":["../../../src/hooks/useAutoEnterPiPEffect.tsx"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,CACnC,uBAAuB,EAAE,OAAO,GAAG,SAAS,QAe7C"}
@@ -1,2 +1,2 @@
1
- export declare function useIsInPiPMode(): boolean;
1
+ export declare function useIsInPiPMode(disablePictureInPicture: boolean | undefined): boolean | undefined;
2
2
  //# sourceMappingURL=useIsInPiPMode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useIsInPiPMode.d.ts","sourceRoot":"","sources":["../../../src/hooks/useIsInPiPMode.tsx"],"names":[],"mappings":"AAYA,wBAAgB,cAAc,YA6C7B"}
1
+ {"version":3,"file":"useIsInPiPMode.d.ts","sourceRoot":"","sources":["../../../src/hooks/useIsInPiPMode.tsx"],"names":[],"mappings":"AAYA,wBAAgB,cAAc,CAAC,uBAAuB,EAAE,OAAO,GAAG,SAAS,uBA+C1E"}
@@ -1 +1 @@
1
- {"version":3,"file":"StreamCall.d.ts","sourceRoot":"","sources":["../../../src/providers/StreamCall.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,iBAAiB,EAAqB,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAS/C,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AACF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,wBAGpB,iBAAiB,CAAC,eAAe,CAAC,sBAUpC,CAAC"}
1
+ {"version":3,"file":"StreamCall.d.ts","sourceRoot":"","sources":["../../../src/providers/StreamCall.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,iBAAiB,EAAqB,MAAM,OAAO,CAAC;AACpE,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAU/C,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,IAAI,EAAE,IAAI,CAAC;CACZ,CAAC;AACF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,wBAGpB,iBAAiB,CAAC,eAAe,CAAC,sBAUpC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare const shouldDisableIOSLocalVideoOnBackgroundRef: {
2
+ current: boolean;
3
+ };
4
+ //# sourceMappingURL=shouldDisableIOSLocalVideoOnBackground.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shouldDisableIOSLocalVideoOnBackground.d.ts","sourceRoot":"","sources":["../../../../src/utils/internal/shouldDisableIOSLocalVideoOnBackground.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,yCAAyC;;CAErD,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const version = "1.0.0-rc2.0";
1
+ export declare const version = "1.0.1";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,gBAAgB,CAAC"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC"}
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVKit
6
+ import UIKit
7
+
8
+ final class SampleBufferVideoCallView: UIView {
9
+ override class var layerClass: AnyClass {
10
+ AVSampleBufferDisplayLayer.self
11
+ }
12
+
13
+ private var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer {
14
+ layer as! AVSampleBufferDisplayLayer
15
+ }
16
+
17
+ var renderingComponent: SampleBufferVideoRendering {
18
+ #if swift(>=5.9)
19
+ if #available(iOS 17.0, *) {
20
+ return sampleBufferDisplayLayer.sampleBufferRenderer
21
+ } else {
22
+ return sampleBufferDisplayLayer
23
+ }
24
+ #else
25
+ return sampleBufferDisplayLayer
26
+ #endif
27
+ }
28
+
29
+ var videoGravity: AVLayerVideoGravity {
30
+ get { sampleBufferDisplayLayer.videoGravity }
31
+ set { sampleBufferDisplayLayer.videoGravity = newValue }
32
+ }
33
+
34
+ var preventsDisplaySleepDuringVideoPlayback: Bool {
35
+ get { sampleBufferDisplayLayer.preventsDisplaySleepDuringVideoPlayback }
36
+ set { sampleBufferDisplayLayer.preventsDisplaySleepDuringVideoPlayback = newValue }
37
+ }
38
+ }
39
+
40
+ protocol SampleBufferVideoRendering {
41
+ @available(iOS 14.0, *)
42
+ var requiresFlushToResumeDecoding: Bool { get }
43
+ var isReadyForMoreMediaData: Bool { get }
44
+ func flush()
45
+ func enqueue(_ sampleBuffer: CMSampleBuffer)
46
+ }
47
+
48
+ extension AVSampleBufferDisplayLayer: SampleBufferVideoRendering {}
49
+ #if swift(>=5.9)
50
+ @available(iOS 17.0, *)
51
+ extension AVSampleBufferVideoRenderer: SampleBufferVideoRendering {}
52
+ #endif
@@ -0,0 +1,83 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVKit
6
+ import Foundation
7
+
8
+ /// Describes an object that can be used to present picture-in-picture content.
9
+ protocol StreamAVPictureInPictureViewControlling: AnyObject {
10
+
11
+ /// The closure to call whenever the picture-in-picture window size changes.
12
+ var onSizeUpdate: ((CGSize) -> Void)? { get set }
13
+
14
+ /// The track that will be rendered on picture-in-picture window.
15
+ var track: RTCVideoTrack? { get set }
16
+
17
+ /// The preferred size for the picture-in-picture window.
18
+ /// - Important: This should **always** be greater to ``CGSize.zero``. If not, iOS throws
19
+ /// a cryptic error with content `PGPegasus code:-1003`
20
+ var preferredContentSize: CGSize { get set }
21
+
22
+ /// The layer that renders the incoming frames from WebRTC.
23
+ var displayLayer: CALayer { get }
24
+ }
25
+
26
+ @available(iOS 15.0, *)
27
+ final class StreamAVPictureInPictureVideoCallViewController: AVPictureInPictureVideoCallViewController,
28
+ StreamAVPictureInPictureViewControlling {
29
+
30
+ private let contentView: StreamPictureInPictureVideoRenderer =
31
+ .init(windowSizePolicy: StreamPictureInPictureAdaptiveWindowSizePolicy())
32
+
33
+ var onSizeUpdate: ((CGSize) -> Void)?
34
+
35
+ var track: RTCVideoTrack? {
36
+ get { contentView.track }
37
+ set { contentView.track = newValue }
38
+ }
39
+
40
+ var displayLayer: CALayer { contentView.displayLayer }
41
+
42
+ // MARK: - Lifecycle
43
+
44
+ @available(*, unavailable)
45
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
46
+
47
+ /// Initializes a new instance and sets the `preferredContentSize` to `Self.defaultPreferredContentSize`
48
+ /// value.
49
+ required init() {
50
+ super.init(nibName: nil, bundle: nil)
51
+ contentView.pictureInPictureWindowSizePolicy.controller = self
52
+ }
53
+
54
+
55
+ override func viewDidLoad() {
56
+ super.viewDidLoad()
57
+ setUp()
58
+ }
59
+
60
+ override func viewDidLayoutSubviews() {
61
+ super.viewDidLayoutSubviews()
62
+ contentView.bounds = view.bounds
63
+ onSizeUpdate?(contentView.bounds.size)
64
+ }
65
+
66
+ // MARK: - Private helpers
67
+
68
+ private func setUp() {
69
+ view.subviews.forEach { $0.removeFromSuperview() }
70
+
71
+ contentView.translatesAutoresizingMaskIntoConstraints = false
72
+
73
+ view.addSubview(contentView)
74
+ NSLayoutConstraint.activate([
75
+ contentView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
76
+ contentView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
77
+ contentView.topAnchor.constraint(equalTo: view.topAnchor),
78
+ contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
79
+ ])
80
+
81
+ contentView.bounds = view.bounds
82
+ }
83
+ }
@@ -0,0 +1,96 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import Foundation
6
+
7
+ /// `StreamBufferTransformer` is a struct that provides methods for transforming RTCI420Buffer to
8
+ /// CVPixelBuffer, while performing downsampling when necessary.
9
+ struct StreamBufferTransformer {
10
+
11
+ var requiresResize = false
12
+
13
+ /// Transforms an RTCVideoFrame's buffer to a CMSampleBuffer with optional resizing.
14
+ /// - Note: The current implementation always handles an i420 buffer as RTCCVPixelBuffer have been
15
+ /// proven problematic.
16
+ /// - Parameters:
17
+ /// - source: The source RTCVideoFrameBuffer to be transformed.
18
+ /// - targetSize: The target size for the resulting CMSampleBuffer.
19
+ /// - Returns: A transformed RTCVideoFrame or nil if transformation fails.
20
+ func transformAndResizeIfRequired(
21
+ _ frame: RTCVideoFrame,
22
+ targetSize: CGSize
23
+ ) -> RTCVideoFrame? {
24
+ let pixelBuffer: RTCVideoFrameBuffer? = {
25
+ if let i420buffer = frame.buffer as? RTCI420Buffer {
26
+ return i420buffer
27
+ } else {
28
+ return frame.buffer
29
+ }
30
+ }()
31
+
32
+ if
33
+ let pixelBuffer = pixelBuffer,
34
+ let buffer = transformAndResizeIfRequired(pixelBuffer, targetSize: targetSize) {
35
+ return .init(
36
+ buffer: buffer,
37
+ rotation: frame.rotation,
38
+ timeStampNs: frame.timeStampNs
39
+ )
40
+ } else {
41
+ return nil
42
+ }
43
+ }
44
+
45
+ // MARK: - Private API
46
+
47
+ /// Transforms an RTCVideoFrameBuffer to a CMSampleBuffer with optional resizing.
48
+ /// - Note: The current implementation always handles an i420 buffer as RTCCVPixelBuffer have been
49
+ /// proven problematic.
50
+ /// - Parameters:
51
+ /// - source: The source RTCVideoFrameBuffer to be transformed.
52
+ /// - targetSize: The target size for the resulting CMSampleBuffer.
53
+ /// - Returns: A transformed CMSampleBuffer or nil if transformation fails.
54
+ func transformAndResizeIfRequired(
55
+ _ source: RTCVideoFrameBuffer,
56
+ targetSize: CGSize
57
+ ) -> StreamRTCYUVBuffer? {
58
+ let sourceSize = CGSize(width: Int(source.width), height: Int(source.height))
59
+ let resultBuffer: StreamRTCYUVBuffer? = {
60
+ if requiresResize {
61
+ return .init(source: source)
62
+ .resize(to: resizeSize(sourceSize, toFitWithin: targetSize))
63
+ } else {
64
+ return .init(source: source)
65
+ }
66
+ }()
67
+
68
+ return resultBuffer
69
+ }
70
+
71
+ /// Calculates the new size to fit within a container size while maintaining the aspect ratio.
72
+ ///
73
+ /// - Parameters:
74
+ /// - size: The original size.
75
+ /// - containerSize: The container size to fit within.
76
+ /// - Returns: The new size that fits within the container while preserving the aspect ratio.
77
+ private func resizeSize(
78
+ _ size: CGSize,
79
+ toFitWithin containerSize: CGSize
80
+ ) -> CGSize {
81
+ let widthRatio = containerSize.width / size.width
82
+ let heightRatio = containerSize.height / size.height
83
+
84
+ // Choose the smaller ratio to ensure that the entire original size fits
85
+ // within the container.
86
+ let ratioToUse = min(widthRatio, heightRatio)
87
+
88
+ // Calculate the new size while maintaining the aspect ratio.
89
+ let newSize = CGSize(
90
+ width: size.width * ratioToUse,
91
+ height: size.height * ratioToUse
92
+ )
93
+
94
+ return newSize
95
+ }
96
+ }
@@ -0,0 +1,185 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVKit
6
+ import Combine
7
+ import Foundation
8
+
9
+ /// A controller class for picture-in-picture whenever that is possible.
10
+ @objc final class StreamPictureInPictureController: NSObject, AVPictureInPictureControllerDelegate {
11
+
12
+ // MARK: - Properties
13
+
14
+ /// The RTCVideoTrack for which the picture-in-picture session is created.
15
+ @objc public var track: RTCVideoTrack? {
16
+ didSet {
17
+ didUpdate(track) // Called when the `track` property changes
18
+ }
19
+ }
20
+
21
+ /// The UIView that contains the video content.
22
+ @objc public var sourceView: UIView? {
23
+ didSet {
24
+ didUpdate(sourceView) // Called when the `sourceView` property changes
25
+ }
26
+ }
27
+
28
+ /// A closure called when the picture-in-picture view's size changes.
29
+ public var onSizeUpdate: ((CGSize) -> Void)? {
30
+ didSet {
31
+ contentViewController?.onSizeUpdate = onSizeUpdate // Updates the onSizeUpdate closure of the content view controller
32
+ }
33
+ }
34
+
35
+ /// A boolean value indicating whether the picture-in-picture session should start automatically when the app enters background.
36
+ public var canStartPictureInPictureAutomaticallyFromInline: Bool
37
+
38
+ // MARK: - Private Properties
39
+
40
+ /// The AVPictureInPictureController object.
41
+ private var pictureInPictureController: AVPictureInPictureController?
42
+
43
+ /// The StreamAVPictureInPictureViewControlling object that manages the picture-in-picture view.
44
+ private var contentViewController: StreamAVPictureInPictureViewControlling?
45
+
46
+ /// A set of `AnyCancellable` objects used to manage subscriptions.
47
+ private var cancellableBag: Set<AnyCancellable> = []
48
+
49
+ /// A `AnyCancellable` object used to ensure that the active track is enabled while in picture-in-picture
50
+ /// mode.
51
+ private var ensureActiveTrackIsEnabledCancellable: AnyCancellable?
52
+
53
+ /// A `StreamPictureInPictureTrackStateAdapter` object that manages the state of the
54
+ /// active track.
55
+ private let trackStateAdapter: StreamPictureInPictureTrackStateAdapter = .init()
56
+
57
+ // MARK: - Lifecycle
58
+
59
+ /// Initializes the controller and creates the content view
60
+ ///
61
+ /// - Parameter canStartPictureInPictureAutomaticallyFromInline A boolean value
62
+ /// indicating whether the picture-in-picture session should start automatically when the app enters
63
+ /// background.
64
+ ///
65
+ /// - Returns `nil` if AVPictureInPictureController is not supported, or the controller otherwise.
66
+ init?(canStartPictureInPictureAutomaticallyFromInline: Bool = true) {
67
+ guard AVPictureInPictureController.isPictureInPictureSupported() else {
68
+ return nil
69
+ }
70
+
71
+ let contentViewController: StreamAVPictureInPictureViewControlling? = {
72
+ if #available(iOS 15.0, *) {
73
+ return StreamAVPictureInPictureVideoCallViewController()
74
+ } else {
75
+ return nil
76
+ }
77
+ }()
78
+ // contentViewController?.preferredContentSize = .init(width: 400, height: 320)
79
+ self.contentViewController = contentViewController
80
+ self.canStartPictureInPictureAutomaticallyFromInline = canStartPictureInPictureAutomaticallyFromInline
81
+ super.init()
82
+ }
83
+
84
+ // MARK: - AVPictureInPictureControllerDelegate
85
+
86
+ func pictureInPictureController(
87
+ _ pictureInPictureController: AVPictureInPictureController,
88
+ restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
89
+ ) {
90
+ completionHandler(true)
91
+ }
92
+
93
+ public func pictureInPictureControllerWillStartPictureInPicture(
94
+ _ pictureInPictureController: AVPictureInPictureController
95
+ ) {
96
+ }
97
+
98
+ public func pictureInPictureControllerDidStartPictureInPicture(
99
+ _ pictureInPictureController: AVPictureInPictureController
100
+ ) {
101
+ }
102
+
103
+ public func pictureInPictureController(
104
+ _ pictureInPictureController: AVPictureInPictureController,
105
+ failedToStartPictureInPictureWithError error: Error
106
+ ) {
107
+ }
108
+
109
+ public func pictureInPictureControllerWillStopPictureInPicture(
110
+ _ pictureInPictureController: AVPictureInPictureController
111
+ ) {
112
+ }
113
+
114
+ public func pictureInPictureControllerDidStopPictureInPicture(
115
+ _ pictureInPictureController: AVPictureInPictureController
116
+ ) {
117
+ }
118
+
119
+ // MARK: - Private helpers
120
+
121
+ private func didUpdate(_ track: RTCVideoTrack?) {
122
+ contentViewController?.track = track
123
+ trackStateAdapter.activeTrack = track
124
+ }
125
+
126
+ @objc private func didUpdate(_ sourceView: UIView?) {
127
+ if let sourceView {
128
+ // If picture-in-picture isn't active, just create a new controller.
129
+ if pictureInPictureController?.isPictureInPictureActive != true {
130
+ makePictureInPictureController(with: sourceView)
131
+
132
+ pictureInPictureController?
133
+ .publisher(for: \.isPictureInPicturePossible)
134
+ .sink { NSLog("PiP - isPictureInPicturePossible:\($0)") }
135
+ .store(in: &cancellableBag)
136
+
137
+ pictureInPictureController?
138
+ .publisher(for: \.isPictureInPictureActive)
139
+ .removeDuplicates()
140
+ .sink { [weak self] in self?.didUpdatePictureInPictureActiveState($0) }
141
+ .store(in: &cancellableBag)
142
+ } else {
143
+ // If picture-in-picture is active, simply update the sourceView.
144
+ makePictureInPictureController(with: sourceView)
145
+ }
146
+ } else {
147
+ if #available(iOS 15.0, *) {
148
+ pictureInPictureController?.contentSource = nil
149
+ }
150
+ }
151
+ }
152
+
153
+ @objc func cleanup() {
154
+ contentViewController?.track = nil
155
+ if #available(iOS 15.0, *) {
156
+ pictureInPictureController?.contentSource = nil
157
+ }
158
+ pictureInPictureController?.delegate = nil
159
+ pictureInPictureController = nil
160
+
161
+ }
162
+
163
+ private func makePictureInPictureController(with sourceView: UIView) {
164
+ if #available(iOS 15.0, *),
165
+ let contentViewController = contentViewController as? StreamAVPictureInPictureVideoCallViewController {
166
+ pictureInPictureController = .init(
167
+ contentSource: .init(
168
+ activeVideoCallSourceView: sourceView,
169
+ contentViewController: contentViewController
170
+ )
171
+ )
172
+ }
173
+
174
+ if #available(iOS 14.2, *) {
175
+ pictureInPictureController?
176
+ .canStartPictureInPictureAutomaticallyFromInline = canStartPictureInPictureAutomaticallyFromInline
177
+ }
178
+
179
+ pictureInPictureController?.delegate = self
180
+ }
181
+
182
+ private func didUpdatePictureInPictureActiveState(_ isActive: Bool) {
183
+ trackStateAdapter.isEnabled = isActive
184
+ }
185
+ }
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import Combine
6
+ import Foundation
7
+
8
+ /// StreamPictureInPictureTrackStateAdapter serves as an adapter for managing the state of a video track
9
+ /// used for picture-in-picture functionality. It can enable or disable observers based on its isEnabled property
10
+ /// and ensures that the active track is always enabled when necessary.
11
+ final class StreamPictureInPictureTrackStateAdapter {
12
+
13
+ /// This property represents whether the adapter is enabled or not.
14
+ var isEnabled: Bool = false {
15
+ didSet {
16
+ /// When the 'isEnabled' property changes, this didSet observer is called.
17
+ /// It checks if the new value is different from the old value, and if so,
18
+ /// it calls the 'enableObserver' function.
19
+ guard isEnabled != oldValue else { return }
20
+ enableObserver(isEnabled)
21
+ }
22
+ }
23
+
24
+ /// This property represents the active RTCVideoTrack.
25
+ var activeTrack: RTCVideoTrack? {
26
+ didSet {
27
+ /// When the 'activeTrack' property changes, this didSet observer is called.
28
+ /// If the adapter is enabled and the new 'activeTrack' is different from the old one,
29
+ /// it disables the old track (if it exists).
30
+ if isEnabled, oldValue?.trackId != activeTrack?.trackId {
31
+ oldValue?.isEnabled = false
32
+ }
33
+ }
34
+ }
35
+
36
+ // MARK: - Private helpers
37
+
38
+ /// This property holds a reference to the observer cancellable.
39
+ private var observerCancellable: AnyCancellable?
40
+
41
+ /// This private function enables or disables an observer based on the 'isActive' parameter.
42
+ ///
43
+ /// - Parameter isActive: A Boolean value indicating whether the observer should be active.
44
+ private func enableObserver(_ isActive: Bool) {
45
+ if isActive {
46
+ /// If 'isActive' is true, it sets up an observer that checks tracks state periodically.
47
+ observerCancellable = Timer
48
+ .publish(every: 0.1, on: .main, in: .default)
49
+ .autoconnect()
50
+ .receive(on: DispatchQueue.main)
51
+ .sink { [weak self] _ in
52
+ self?.checkTracksState()
53
+ }
54
+ } else {
55
+ /// If 'isActive' is false, it cancels the observer.
56
+ observerCancellable?.cancel()
57
+ observerCancellable = nil
58
+ }
59
+ }
60
+
61
+ /// This private function checks the state of the active track and enables it if it's not already enabled.
62
+ private func checkTracksState() {
63
+ if let activeTrack, !activeTrack.isEnabled {
64
+ // log.info("⚙️Active track:\(activeTrack.trackId) for picture-in-picture will be enabled now.")
65
+ self.activeTrack?.isEnabled = true
66
+ }
67
+ }
68
+ }