@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.
- package/CHANGELOG.md +14 -83
- package/dist/commonjs/components/Call/CallContent/CallContent.js +10 -5
- package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +109 -0
- package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -0
- package/dist/commonjs/components/Call/CallContent/index.js +11 -0
- package/dist/commonjs/components/Call/CallContent/index.js.map +1 -1
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +4 -3
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +4 -3
- package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
- package/dist/commonjs/hooks/useAutoEnterPiPEffect.js +3 -3
- package/dist/commonjs/hooks/useAutoEnterPiPEffect.js.map +1 -1
- package/dist/commonjs/hooks/useIsInPiPMode.js +4 -4
- package/dist/commonjs/hooks/useIsInPiPMode.js.map +1 -1
- package/dist/commonjs/providers/StreamCall.js +5 -2
- package/dist/commonjs/providers/StreamCall.js.map +1 -1
- package/dist/commonjs/utils/internal/shouldDisableIOSLocalVideoOnBackground.js +10 -0
- package/dist/commonjs/utils/internal/shouldDisableIOSLocalVideoOnBackground.js.map +1 -0
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/module/components/Call/CallContent/CallContent.js +10 -5
- package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +101 -0
- package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -0
- package/dist/module/components/Call/CallContent/index.js +1 -0
- package/dist/module/components/Call/CallContent/index.js.map +1 -1
- package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +4 -3
- package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
- package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +4 -3
- package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
- package/dist/module/hooks/useAutoEnterPiPEffect.js +3 -3
- package/dist/module/hooks/useAutoEnterPiPEffect.js.map +1 -1
- package/dist/module/hooks/useIsInPiPMode.js +4 -4
- package/dist/module/hooks/useIsInPiPMode.js.map +1 -1
- package/dist/module/providers/StreamCall.js +5 -2
- package/dist/module/providers/StreamCall.js.map +1 -1
- package/dist/module/utils/internal/shouldDisableIOSLocalVideoOnBackground.js +4 -0
- package/dist/module/utils/internal/shouldDisableIOSLocalVideoOnBackground.js.map +1 -0
- package/dist/module/version.js +1 -1
- package/dist/module/version.js.map +1 -1
- package/dist/typescript/components/Call/CallContent/CallContent.d.ts +6 -1
- package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts +7 -0
- package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -0
- package/dist/typescript/components/Call/CallContent/index.d.ts +1 -0
- package/dist/typescript/components/Call/CallContent/index.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts +2 -2
- package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
- package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts +2 -2
- package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
- package/dist/typescript/hooks/useAutoEnterPiPEffect.d.ts +1 -1
- package/dist/typescript/hooks/useAutoEnterPiPEffect.d.ts.map +1 -1
- package/dist/typescript/hooks/useIsInPiPMode.d.ts +1 -1
- package/dist/typescript/hooks/useIsInPiPMode.d.ts.map +1 -1
- package/dist/typescript/providers/StreamCall.d.ts.map +1 -1
- package/dist/typescript/utils/internal/shouldDisableIOSLocalVideoOnBackground.d.ts +4 -0
- package/dist/typescript/utils/internal/shouldDisableIOSLocalVideoOnBackground.d.ts.map +1 -0
- package/dist/typescript/version.d.ts +1 -1
- package/dist/typescript/version.d.ts.map +1 -1
- package/ios/PictureInPicture/SampleBufferVideoCallView.swift +52 -0
- package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +83 -0
- package/ios/PictureInPicture/StreamBufferTransformer.swift +96 -0
- package/ios/PictureInPicture/StreamPictureInPictureController.swift +185 -0
- package/ios/PictureInPicture/StreamPictureInPictureTrackStateAdapter.swift +68 -0
- package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +250 -0
- package/ios/PictureInPicture/StreamPixelBufferPool.swift +118 -0
- package/ios/PictureInPicture/StreamPixelBufferRepository.swift +98 -0
- package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +249 -0
- package/ios/PictureInPicture/StreamYUVToARGBConversion.swift +128 -0
- package/ios/PictureInPicture/WindowSizePolicy/StreamPictureInPictureAdaptiveWindowSizePolicy.swift +25 -0
- package/ios/PictureInPicture/WindowSizePolicy/StreamPictureInPictureFixedWindowSizePolicy.swift +29 -0
- package/ios/PictureInPicture/WindowSizePolicy/StreamPictureInPictureWindowSizePolicy.swift +14 -0
- package/ios/PictureInPicture/YpCbCrPixelRange+Default.swift +32 -0
- package/ios/RTCViewPip.swift +69 -0
- package/ios/RTCViewPipManager.mm +16 -0
- package/ios/RTCViewPipManager.swift +34 -0
- package/ios/StreamVideoReactNative-Bridging-Header.h +11 -0
- package/package.json +4 -4
- package/src/components/Call/CallContent/CallContent.tsx +58 -40
- package/src/components/Call/CallContent/RTCViewPipIOS.tsx +138 -0
- package/src/components/Call/CallContent/index.ts +1 -0
- package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +7 -3
- package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +7 -3
- package/src/hooks/useAutoEnterPiPEffect.tsx +7 -3
- package/src/hooks/useIsInPiPMode.tsx +6 -4
- package/src/providers/StreamCall.tsx +5 -2
- package/src/utils/internal/shouldDisableIOSLocalVideoOnBackground.ts +3 -0
- package/src/version.ts +1 -1
- package/stream-video-react-native.podspec +27 -4
- package/ios/StreamVideoReactNative.xcodeproj/project.pbxproj +0 -274
- package/ios/StreamVideoReactNative.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- 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,
|
|
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,
|
|
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,
|
|
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;
|
|
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 @@
|
|
|
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.
|
|
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,
|
|
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
|
+
}
|