@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
@@ -0,0 +1,250 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import Combine
6
+ import Foundation
7
+
8
+ /// A view that can be used to render an instance of `RTCVideoTrack`
9
+ final class StreamPictureInPictureVideoRenderer: UIView, RTCVideoRenderer {
10
+
11
+ /// The rendering track.
12
+ var track: RTCVideoTrack? {
13
+ didSet {
14
+ // Whenever the track changes we perform the following operations if possible:
15
+ // - stopFrameStreaming for the old track
16
+ // - startFrameStreaming for the new track and only if we are already
17
+ // in picture-in-picture.
18
+ guard oldValue != track else { return }
19
+ prepareForTrackRendering(oldValue)
20
+ }
21
+ }
22
+
23
+ /// The layer that renders the track's frames.
24
+ var displayLayer: CALayer { contentView.layer }
25
+
26
+ /// A policy defining how the Picture in Picture window should be resized in order to better fit
27
+ /// the rendering frame size.
28
+ var pictureInPictureWindowSizePolicy: PictureInPictureWindowSizePolicy
29
+
30
+ /// The publisher which is used to streamline the frames received from the track.
31
+ private let bufferPublisher: PassthroughSubject<CMSampleBuffer, Never> = .init()
32
+
33
+ /// The view that contains the rendering layer.
34
+ private lazy var contentView: SampleBufferVideoCallView = {
35
+ let contentView = SampleBufferVideoCallView()
36
+ contentView.translatesAutoresizingMaskIntoConstraints = false
37
+ contentView.contentMode = .scaleAspectFill
38
+ contentView.videoGravity = .resizeAspectFill
39
+ contentView.preventsDisplaySleepDuringVideoPlayback = true
40
+ return contentView
41
+ }()
42
+
43
+ /// The transformer used to transform and downsample a RTCVideoFrame's buffer.
44
+ private var bufferTransformer = StreamBufferTransformer()
45
+
46
+ /// The cancellable used to control the bufferPublisher stream.
47
+ private var bufferUpdatesCancellable: AnyCancellable?
48
+
49
+ /// The view's size.
50
+ /// - Note: We are using this property instead for `frame.size` or `bounds.size` so we can
51
+ /// access it from any thread.
52
+ private var contentSize: CGSize = .zero
53
+
54
+ /// The track's size.
55
+ private var trackSize: CGSize = .zero {
56
+ didSet {
57
+ guard trackSize != oldValue else { return }
58
+ didUpdateTrackSize()
59
+ }
60
+ }
61
+
62
+ /// A property that defines if the RTCVideoFrame instances that will be rendered need to be resized
63
+ /// to fid the view's contentSize.
64
+ private var requiresResize = false {
65
+ didSet { bufferTransformer.requiresResize = requiresResize }
66
+ }
67
+
68
+ /// As we are operate in smaller rendering bounds we skip frames depending on this property's value
69
+ /// to improve performance.
70
+ /// - Note: The number of frames to skip is being calculated based on the ``trackSize`` and
71
+ /// ``contentSize``. It takes into account also the ``sizeRatioThreshold``
72
+ private var noOfFramesToSkipAfterRendering = 1
73
+
74
+ /// The number of frames that we have skipped so far. This is used as a step counter in the
75
+ /// ``renderFrame(_:)``.
76
+ private var skippedFrames = 0
77
+
78
+ /// We render frames every time the stepper/counter value is 0 and have a valid trackSize.
79
+ private var shouldRenderFrame: Bool { skippedFrames == 0 && trackSize != .zero }
80
+
81
+ /// A size ratio threshold used to determine if resizing is required.
82
+ /// - Note: It seems that Picture-in-Picture doesn't like rendering frames that are bigger than its
83
+ /// window size. For this reason, we are setting the resizeThreshold to `1`.
84
+ private let resizeRequiredSizeRatioThreshold: CGFloat = 1
85
+
86
+ /// A size ratio threshold used to determine if skipping frames is required.
87
+ private let sizeRatioThreshold: CGFloat = 15
88
+
89
+ // MARK: - Lifecycle
90
+
91
+ @available(*, unavailable)
92
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
93
+
94
+ init(windowSizePolicy: PictureInPictureWindowSizePolicy) {
95
+ pictureInPictureWindowSizePolicy = windowSizePolicy
96
+ super.init(frame: .zero)
97
+ setUp()
98
+ }
99
+
100
+ override func willMove(toWindow newWindow: UIWindow?) {
101
+ super.willMove(toWindow: newWindow)
102
+ // Depending on the window we are moving we either start or stop
103
+ // streaming frames from the track.
104
+ if newWindow != nil {
105
+ startFrameStreaming(for: track, on: newWindow)
106
+ } else {
107
+ stopFrameStreaming(for: track)
108
+ }
109
+ }
110
+
111
+ override func layoutSubviews() {
112
+ super.layoutSubviews()
113
+ contentSize = frame.size
114
+ }
115
+
116
+ // MARK: - Rendering lifecycle
117
+
118
+ /// This method is being called from WebRTC and asks the container to set its size to the track's size.
119
+ func setSize(_ size: CGSize) {
120
+ trackSize = size
121
+ }
122
+
123
+ func renderFrame(_ frame: RTCVideoFrame?) {
124
+ guard let frame = frame else {
125
+ return
126
+ }
127
+
128
+ // Update the trackSize and re-calculate rendering properties if the size
129
+ // has changed.
130
+ trackSize = .init(width: Int(frame.width), height: Int(frame.height))
131
+
132
+ defer {
133
+ handleFrameSkippingIfRequired()
134
+ }
135
+
136
+ guard shouldRenderFrame else {
137
+ return
138
+ }
139
+
140
+ if
141
+ let yuvBuffer = bufferTransformer.transformAndResizeIfRequired(frame, targetSize: contentSize)?
142
+ .buffer as? StreamRTCYUVBuffer,
143
+ let sampleBuffer = yuvBuffer.sampleBuffer {
144
+ bufferPublisher.send(sampleBuffer)
145
+ }
146
+ }
147
+
148
+ // MARK: - Private helpers
149
+
150
+ /// Set up the view's hierarchy.
151
+ private func setUp() {
152
+ addSubview(contentView)
153
+ NSLayoutConstraint.activate([
154
+ contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
155
+ contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
156
+ contentView.topAnchor.constraint(equalTo: topAnchor),
157
+ contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
158
+ ])
159
+ }
160
+
161
+ /// A method used to process the frame's buffer and enqueue on the rendering view.
162
+ private func process(_ buffer: CMSampleBuffer) {
163
+ guard
164
+ bufferUpdatesCancellable != nil,
165
+ let trackId = track?.trackId,
166
+ buffer.isValid
167
+ else {
168
+ contentView.renderingComponent.flush()
169
+ return
170
+ }
171
+
172
+ if #available(iOS 14.0, *) {
173
+ if contentView.renderingComponent.requiresFlushToResumeDecoding == true {
174
+ contentView.renderingComponent.flush()
175
+ }
176
+ }
177
+
178
+ if contentView.renderingComponent.isReadyForMoreMediaData {
179
+ contentView.renderingComponent.enqueue(buffer)
180
+ }
181
+ }
182
+
183
+ /// A method used to start consuming frames from the track.
184
+ /// - Note: In order to avoid unnecessary processing, we only start consuming track's frames when
185
+ /// the view has been added on a window (which means that picture-in-picture view is visible).
186
+ private func startFrameStreaming(
187
+ for track: RTCVideoTrack?,
188
+ on window: UIWindow?
189
+ ) {
190
+ guard window != nil, let track else { return }
191
+
192
+ bufferUpdatesCancellable = bufferPublisher
193
+ .receive(on: DispatchQueue.main)
194
+ .sink { [weak self] in self?.process($0) }
195
+
196
+ track.add(self)
197
+ }
198
+
199
+ /// A method that stops the frame consumption from the track. Used automatically when the rendering
200
+ /// view move's away from the window or when the track changes.
201
+ private func stopFrameStreaming(for track: RTCVideoTrack?) {
202
+ guard bufferUpdatesCancellable != nil else { return }
203
+ bufferUpdatesCancellable?.cancel()
204
+ bufferUpdatesCancellable = nil
205
+ track?.remove(self)
206
+ contentView.renderingComponent.flush()
207
+ }
208
+
209
+ /// A method used to calculate rendering required properties, every time the trackSize changes.
210
+ private func didUpdateTrackSize() {
211
+ guard contentSize != .zero, trackSize != .zero else { return }
212
+
213
+ let widthDiffRatio = trackSize.width / contentSize.width
214
+ let heightDiffRatio = trackSize.height / contentSize.height
215
+ requiresResize = widthDiffRatio >= resizeRequiredSizeRatioThreshold || heightDiffRatio >= resizeRequiredSizeRatioThreshold
216
+ let requiresFramesSkipping = widthDiffRatio >= sizeRatioThreshold || heightDiffRatio >= sizeRatioThreshold
217
+
218
+ /// Skipping frames is decided based on how much bigger is the incoming frame's size compared
219
+ /// to PiP window's size.
220
+ noOfFramesToSkipAfterRendering = requiresFramesSkipping ? max(Int(max(Int(widthDiffRatio), Int(heightDiffRatio)) / 2), 1) :
221
+ 0
222
+ skippedFrames = 0
223
+
224
+ /// We update the provided windowSizePolicy with the size of the track we received, transformed
225
+ /// to the value that fits.
226
+ pictureInPictureWindowSizePolicy.trackSize = trackSize
227
+ }
228
+
229
+ /// A method used to handle the frameSkipping(step) during frame consumption.
230
+ private func handleFrameSkippingIfRequired() {
231
+ if noOfFramesToSkipAfterRendering > 0 {
232
+ if skippedFrames == noOfFramesToSkipAfterRendering {
233
+ skippedFrames = 0
234
+ } else {
235
+ skippedFrames += 1
236
+ }
237
+ } else if skippedFrames > 0 {
238
+ skippedFrames = 0
239
+ }
240
+ }
241
+
242
+ /// A method used to prepare the view for a new track rendering.
243
+ private func prepareForTrackRendering(_ oldValue: RTCVideoTrack?) {
244
+ stopFrameStreaming(for: oldValue)
245
+ noOfFramesToSkipAfterRendering = 0
246
+ skippedFrames = 0
247
+ requiresResize = false
248
+ startFrameStreaming(for: track, on: window)
249
+ }
250
+ }
@@ -0,0 +1,118 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import CoreVideo
6
+ import Foundation
7
+
8
+ /// A class managing a pool of pixel buffers to optimize memory usage during video processing.
9
+ ///
10
+ /// A CVPixelBufferPool is instrumental in optimizing video processing and real-time image manipulation by
11
+ /// enhancing memory efficiency and performance. It achieves this by reusing pixel buffers, which curtails
12
+ /// the frequent allocation and deallocation of memory, a common bottleneck in real-time processing.
13
+ ///
14
+ /// This reuse not only boosts performance but also aids in effective resource management, maintaining a
15
+ /// controlled memory footprint and preventing memory pressure. The pool ensures that all buffers share
16
+ /// consistent attributes like size and format, crucial for operations expecting uniform input.
17
+ ///
18
+ /// Additionally, it reduces memory fragmentation, contributing to overall system stability and efficiency.
19
+ /// Essentially, CVPixelBufferPool streamlines handling high volumes of image or video frames, ensuring
20
+ /// optimized, consistent, and fast processing.
21
+ final class StreamPixelBufferPool {
22
+
23
+ /// Errors that can be encountered while managing the pixel buffer pool.
24
+ enum Error: LocalizedError {
25
+
26
+ /// Error indicating the pixel buffer pool cannot allocate more buffers without exceeding its limit.
27
+ case returnWouldExceedAllocationThreshold
28
+
29
+ /// Error indicating an unknown issue occurred, including the size of the buffer attempted.
30
+ case unknown(CGSize)
31
+
32
+ /// Error indicating that the bufferPool failed to initialise.
33
+ case unavailableBufferPool
34
+
35
+ /// A human-readable description of the error.
36
+ var errorDescription: String? {
37
+ switch self {
38
+ case .returnWouldExceedAllocationThreshold:
39
+ return "BufferPool is out of buffers, dropping frame"
40
+ case let .unknown(bufferSize):
41
+ return "An unknown error occurred while trying to dequeue a pixelBuffer for size: \(bufferSize)"
42
+ case .unavailableBufferPool:
43
+ return "BufferPool is unavailable."
44
+ }
45
+ }
46
+ }
47
+
48
+ /// The maximum number of pixel buffers that the pool can allocate.
49
+ let maxNoOfBuffers: Int
50
+
51
+ /// The size of each pixel buffer in the pool.
52
+ let bufferSize: CGSize
53
+
54
+ /// The pixel format type of each pixel buffer in the pool.
55
+ let pixelFormat: OSType
56
+
57
+ /// The underlying CVPixelBufferPool managed by this class.
58
+ private var pool: CVPixelBufferPool?
59
+
60
+ /// Initializes a new pixel buffer pool with specified characteristics.
61
+ ///
62
+ /// - Parameters:
63
+ /// - bufferSize: The size for each buffer in the pool.
64
+ /// - pixelFormat: The pixel format type, defaults to 32-bit BGRA.
65
+ /// - maxNoOfBuffers: The maximum number of buffers the pool can allocate, defaults to 5.
66
+ init(
67
+ bufferSize: CGSize,
68
+ pixelFormat: OSType = kCVPixelFormatType_32BGRA,
69
+ maxNoOfBuffers: Int = 5
70
+ ) {
71
+ self.bufferSize = bufferSize
72
+ self.pixelFormat = pixelFormat
73
+ self.maxNoOfBuffers = maxNoOfBuffers
74
+
75
+ var cvPool: CVPixelBufferPool?
76
+ let poolAttributes: [String: Any] = [
77
+ kCVPixelBufferPoolMinimumBufferCountKey as String: maxNoOfBuffers
78
+ ]
79
+ let pixelBufferAttributes: [String: Any] = [
80
+ kCVPixelBufferPixelFormatTypeKey as String: Int(pixelFormat),
81
+ kCVPixelBufferWidthKey as String: Int(bufferSize.width),
82
+ kCVPixelBufferHeightKey as String: Int(bufferSize.height),
83
+ kCVPixelBufferIOSurfacePropertiesKey as String: [:]
84
+ ]
85
+ CVPixelBufferPoolCreate(
86
+ nil,
87
+ poolAttributes as CFDictionary,
88
+ pixelBufferAttributes as CFDictionary,
89
+ &cvPool
90
+ )
91
+ pool = cvPool
92
+ }
93
+
94
+ /// Dequeues a pixel buffer from the pool or throws an error if none are available.
95
+ ///
96
+ /// - Returns: A pixel buffer from the pool.
97
+ /// - Throws: An `Error` if it cannot dequeue a pixel buffer.
98
+ func dequeuePixelBuffer() throws -> CVPixelBuffer {
99
+ guard let pool = self.pool else {
100
+ throw Error.unavailableBufferPool
101
+ }
102
+
103
+ var pixelBuffer: CVPixelBuffer?
104
+ let error = CVPixelBufferPoolCreatePixelBuffer(
105
+ nil,
106
+ pool,
107
+ &pixelBuffer
108
+ )
109
+
110
+ if error == kCVReturnWouldExceedAllocationThreshold {
111
+ throw Error.returnWouldExceedAllocationThreshold
112
+ } else if let pixelBuffer {
113
+ return pixelBuffer
114
+ } else {
115
+ throw Error.unknown(bufferSize)
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,98 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import CoreVideo
6
+ import Foundation
7
+
8
+ /// A repository class that manages multiple pools of pixel buffers to efficiently handle various sizes and formats.
9
+ final class StreamPixelBufferRepository {
10
+
11
+ /// A private struct to use as a key in the dictionary that maps buffer characteristics to their respective pools.
12
+ private struct Key: Hashable {
13
+ /// The width of the pixel buffers in the pool.
14
+ var width: Int
15
+ /// The height of the pixel buffers in the pool.
16
+ var height: Int
17
+ /// The pixel format type of the pixel buffers in the pool.
18
+ var pixelFormat: OSType
19
+
20
+ /// Initializes a new key with a given size and pixel format.
21
+ /// - Parameters:
22
+ /// - size: The size of the pixel buffers.
23
+ /// - pixelFormat: The pixel format type.
24
+ init(_ size: CGSize, pixelFormat: OSType) {
25
+ width = Int(size.width)
26
+ height = Int(size.height)
27
+ self.pixelFormat = pixelFormat
28
+ }
29
+ }
30
+
31
+ /// A dictionary that maps unique keys to specific `StreamPixelBufferPool` instances.
32
+ private var pools: [Key: StreamPixelBufferPool] = [:]
33
+
34
+ /// A dispatch queue used for synchronizing access to the pixel buffer pools.
35
+ private let queue = UnfairQueue()
36
+
37
+ /// Attempts to dequeue a pixel buffer from the appropriate pool or creates a new pool if necessary.
38
+ ///
39
+ /// This method looks up the pool corresponding to the specified size and pixel format.
40
+ /// If a pool doesn't exist yet, it creates a new one and then tries to dequeue a pixel buffer from it.
41
+ ///
42
+ /// - Parameters:
43
+ /// - size: The size of the pixel buffer required.
44
+ /// - pixelFormat: The pixel format of the buffer, defaults to 32-bit BGRA.
45
+ /// - Returns: A pixel buffer that matches the requested size and format.
46
+ /// - Throws: An error if a pixel buffer cannot be dequeued or created.
47
+ func dequeuePixelBuffer(
48
+ of size: CGSize,
49
+ pixelFormat: OSType = kCVPixelFormatType_32BGRA
50
+ ) throws -> CVPixelBuffer {
51
+ let key = Key(size, pixelFormat: pixelFormat)
52
+ return try queue.sync {
53
+ if let targetPool = pools[key] {
54
+ return try targetPool.dequeuePixelBuffer()
55
+ } else {
56
+ let targetPool = StreamPixelBufferPool(
57
+ bufferSize: size,
58
+ pixelFormat: pixelFormat
59
+ )
60
+ pools[key] = targetPool
61
+ return try targetPool.dequeuePixelBuffer()
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ final class UnfairQueue {
68
+
69
+ /// The unfair lock variable, managed as an unsafe mutable pointer to `os_unfair_lock`.
70
+ private let lock: os_unfair_lock_t
71
+
72
+ /// Initializes a new instance of `UnfairQueue`.
73
+ ///
74
+ /// It allocates memory for an `os_unfair_lock` and initializes it.
75
+ init() {
76
+ lock = UnsafeMutablePointer<os_unfair_lock>.allocate(capacity: 1)
77
+ lock.initialize(to: os_unfair_lock())
78
+ }
79
+
80
+ /// Deinitializes the instance, deallocating the unfair lock.
81
+ deinit {
82
+ lock.deallocate()
83
+ }
84
+
85
+ /// Executes a block of code, ensuring mutual exclusion via an unfair lock.
86
+ ///
87
+ /// The method locks before the block executes and unlocks after the block completes.
88
+ /// It's designed to be exception-safe, unlocking even if an error is thrown within the block.
89
+ ///
90
+ /// - Parameter block: The block of code to execute safely under the lock.
91
+ /// - Returns: The value returned by the block, if any.
92
+ /// - Throws: Rethrows any errors that are thrown by the block.
93
+ func sync<T>(_ block: () throws -> T) rethrows -> T {
94
+ os_unfair_lock_lock(lock)
95
+ defer { os_unfair_lock_unlock(lock) }
96
+ return try block()
97
+ }
98
+ }