@bluebillywig/react-native-bb-player 8.47.1 → 8.47.3

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 (58) hide show
  1. package/android/build.gradle +0 -10
  2. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeplayersdk/8.46.0/bbnativeplayersdk-8.46.0.aar +0 -0
  3. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeplayersdk/8.46.0/bbnativeplayersdk-8.46.0.module +137 -0
  4. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeplayersdk/8.46.0/bbnativeplayersdk-8.46.0.pom +57 -0
  5. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeplayersdk-core/8.46.0/bbnativeplayersdk-core-8.46.0.aar +0 -0
  6. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeplayersdk-core/8.46.0/bbnativeplayersdk-core-8.46.0.module +264 -0
  7. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeplayersdk-core/8.46.0/bbnativeplayersdk-core-8.46.0.pom +171 -0
  8. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeshared/8.46.0/bbnativeshared-8.46.0.jar +0 -0
  9. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeshared/8.46.0/bbnativeshared-8.46.0.module +159 -0
  10. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeshared/8.46.0/bbnativeshared-8.46.0.pom +36 -0
  11. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeshared-android/8.46.0/bbnativeshared-android-8.46.0.aar +0 -0
  12. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeshared-android/8.46.0/bbnativeshared-android-8.46.0.module +329 -0
  13. package/android/repo/com/bluebillywig/bbnativeplayersdk/bbnativeshared-android/8.46.0/bbnativeshared-android-8.46.0.pom +267 -0
  14. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerModule.kt +5 -0
  15. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerView.kt +8 -0
  16. package/ios/BBCastButtonView.swift +1 -1
  17. package/ios/BBCastButtonViewManager.swift +3 -3
  18. package/ios/BBCastMiniControlsView.swift +1 -1
  19. package/ios/BBCastMiniControlsViewManager.swift +3 -3
  20. package/ios/BBPlayerExtensions.swift +1 -1
  21. package/ios/BBPlayerModule.mm +1 -0
  22. package/ios/BBPlayerModule.swift +72 -152
  23. package/ios/BBPlayerView.swift +21 -9
  24. package/ios/BBPlayerViewManager.swift +34 -73
  25. package/ios/BBShortsView.swift +6 -3
  26. package/ios/BBShortsViewManager.swift +11 -9
  27. package/ios/Frameworks/BBNativePlayerKit.xcframework/ios-arm64/BBNativePlayerKit.framework/_CodeSignature/CodeResources +256 -0
  28. package/ios/Frameworks/BBNativePlayerKit.xcframework/ios-arm64_x86_64-simulator/BBNativePlayerKit.framework/_CodeSignature/CodeResources +311 -0
  29. package/lib/commonjs/BBOutstreamView.js +1 -0
  30. package/lib/commonjs/BBOutstreamView.js.map +1 -1
  31. package/lib/commonjs/NativeCommands.js +4 -0
  32. package/lib/commonjs/NativeCommands.js.map +1 -1
  33. package/lib/commonjs/index.js +0 -4
  34. package/lib/commonjs/index.js.map +1 -1
  35. package/lib/commonjs/specs/NativeBBPlayerModule.js.map +1 -1
  36. package/lib/module/BBOutstreamView.js +1 -0
  37. package/lib/module/BBOutstreamView.js.map +1 -1
  38. package/lib/module/NativeCommands.js +4 -0
  39. package/lib/module/NativeCommands.js.map +1 -1
  40. package/lib/module/index.js +0 -8
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/specs/NativeBBPlayerModule.js.map +1 -1
  43. package/lib/typescript/src/BBOutstreamView.d.ts.map +1 -1
  44. package/lib/typescript/src/BBPlayer.types.d.ts +2 -0
  45. package/lib/typescript/src/BBPlayer.types.d.ts.map +1 -1
  46. package/lib/typescript/src/NativeCommands.d.ts +1 -0
  47. package/lib/typescript/src/NativeCommands.d.ts.map +1 -1
  48. package/lib/typescript/src/index.d.ts +0 -4
  49. package/lib/typescript/src/index.d.ts.map +1 -1
  50. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts +1 -0
  51. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts.map +1 -1
  52. package/package.json +1 -1
  53. package/react-native-bb-player.podspec +3 -2
  54. package/src/BBOutstreamView.tsx +1 -0
  55. package/src/BBPlayer.types.ts +2 -0
  56. package/src/NativeCommands.ts +4 -0
  57. package/src/index.ts +0 -8
  58. package/src/specs/NativeBBPlayerModule.ts +1 -0
@@ -1,13 +1,13 @@
1
1
  import Foundation
2
- import React
3
- import BBNativePlayerKit
2
+ @preconcurrency import React
3
+ @preconcurrency import BBNativePlayerKit
4
4
 
5
5
  /**
6
6
  * Global registry for BBPlayerView instances.
7
- * This is needed because the bridge.uiManager.view(forReactTag:) doesn't work
8
- * with the New Architecture (Fabric). Views register themselves when created.
7
+ * Needed because bridge.uiManager.view(forReactTag:) doesn't work
8
+ * with Fabric. Views register themselves when added to the window.
9
9
  */
10
- class BBPlayerViewRegistry: NSObject {
10
+ final class BBPlayerViewRegistry: NSObject, @unchecked Sendable {
11
11
  static let shared = BBPlayerViewRegistry()
12
12
 
13
13
  private var views: [Int: BBPlayerView] = [:]
@@ -41,10 +41,10 @@ class BBPlayerViewRegistry: NSObject {
41
41
  /**
42
42
  * Native Module for BBPlayer commands.
43
43
  * Extends RCTEventEmitter to support module-level events (modal player).
44
- * This module looks up BBPlayerView instances by their React tag and dispatches commands to them.
44
+ * Looks up BBPlayerView instances by React tag and dispatches commands.
45
45
  */
46
46
  @objc(BBPlayerModule)
47
- class BBPlayerModule: RCTEventEmitter {
47
+ class BBPlayerModule: RCTEventEmitter, @unchecked Sendable {
48
48
 
49
49
  private var modalPlayerView: BBNativePlayerView?
50
50
  private var modalDelegate: ModalPlayerDelegate?
@@ -79,150 +79,119 @@ class BBPlayerModule: RCTEventEmitter {
79
79
  hasListeners = false
80
80
  }
81
81
 
82
- // MARK: - Helper to get view by tag
82
+ // MARK: - Helpers
83
83
 
84
84
  private func getView(_ reactTag: NSNumber) -> BBPlayerView? {
85
- // First try the view registry (works with both old and new architecture)
86
85
  if let view = BBPlayerViewRegistry.shared.getView(tag: reactTag.intValue) {
87
86
  return view
88
87
  }
89
-
90
- // Fallback to bridge.uiManager for old architecture
91
88
  if let bridge = self.bridge {
92
89
  if let view = bridge.uiManager.view(forReactTag: reactTag) as? BBPlayerView {
93
90
  return view
94
91
  }
95
92
  }
96
-
97
93
  NSLog("BBPlayerModule: Could not find view with tag %@", reactTag)
98
94
  return nil
99
95
  }
100
96
 
97
+ /// Dispatch to main queue and enter @MainActor context.
98
+ private func onMainActor(_ work: @MainActor @Sendable @escaping () -> Void) {
99
+ DispatchQueue.main.async {
100
+ MainActor.assumeIsolated(work)
101
+ }
102
+ }
103
+
101
104
  // MARK: - Commands
102
105
 
103
106
  @objc func play(_ viewTag: NSNumber) {
104
- DispatchQueue.main.async {
105
- self.getView(viewTag)?.play()
106
- }
107
+ onMainActor { self.getView(viewTag)?.play() }
107
108
  }
108
109
 
109
110
  @objc func pause(_ viewTag: NSNumber) {
110
- DispatchQueue.main.async {
111
- self.getView(viewTag)?.pause()
112
- }
111
+ onMainActor { self.getView(viewTag)?.pause() }
113
112
  }
114
113
 
115
114
  @objc func seek(_ viewTag: NSNumber, position: NSNumber) {
116
- DispatchQueue.main.async {
117
- self.getView(viewTag)?.seek(position.intValue)
118
- }
115
+ onMainActor { self.getView(viewTag)?.seek(position.intValue) }
119
116
  }
120
117
 
121
118
  @objc func seekRelative(_ viewTag: NSNumber, offsetSeconds: NSNumber) {
122
- DispatchQueue.main.async {
123
- self.getView(viewTag)?.seekRelative(offsetSeconds.doubleValue)
124
- }
119
+ onMainActor { self.getView(viewTag)?.seekRelative(offsetSeconds.doubleValue) }
125
120
  }
126
121
 
127
122
  @objc func setMuted(_ viewTag: NSNumber, muted: Bool) {
128
- DispatchQueue.main.async {
129
- self.getView(viewTag)?.setMuted(muted)
130
- }
123
+ onMainActor { self.getView(viewTag)?.setMuted(muted) }
131
124
  }
132
125
 
133
126
  @objc func setVolume(_ viewTag: NSNumber, volume: NSNumber) {
134
- DispatchQueue.main.async {
135
- self.getView(viewTag)?.setVolume(volume.doubleValue)
136
- }
127
+ onMainActor { self.getView(viewTag)?.setVolume(volume.doubleValue) }
137
128
  }
138
129
 
139
130
  @objc func enterFullscreen(_ viewTag: NSNumber) {
140
- DispatchQueue.main.async {
141
- self.getView(viewTag)?.enterFullscreen()
142
- }
131
+ onMainActor { self.getView(viewTag)?.enterFullscreen() }
143
132
  }
144
133
 
145
134
  @objc func enterFullscreenLandscape(_ viewTag: NSNumber) {
146
- DispatchQueue.main.async {
147
- self.getView(viewTag)?.enterFullscreenLandscape()
148
- }
135
+ onMainActor { self.getView(viewTag)?.enterFullscreenLandscape() }
149
136
  }
150
137
 
151
138
  @objc func exitFullscreen(_ viewTag: NSNumber) {
152
- DispatchQueue.main.async {
153
- self.getView(viewTag)?.exitFullscreen()
154
- }
139
+ onMainActor { self.getView(viewTag)?.exitFullscreen() }
155
140
  }
156
141
 
157
142
  @objc func collapse(_ viewTag: NSNumber) {
158
- DispatchQueue.main.async {
159
- self.getView(viewTag)?.collapse()
160
- }
143
+ onMainActor { self.getView(viewTag)?.collapse() }
161
144
  }
162
145
 
163
146
  @objc func expand(_ viewTag: NSNumber) {
164
- DispatchQueue.main.async {
165
- self.getView(viewTag)?.expand()
166
- }
147
+ onMainActor { self.getView(viewTag)?.expand() }
167
148
  }
168
149
 
169
150
  @objc func autoPlayNextCancel(_ viewTag: NSNumber) {
170
- DispatchQueue.main.async {
171
- self.getView(viewTag)?.autoPlayNextCancel()
172
- }
151
+ onMainActor { self.getView(viewTag)?.autoPlayNextCancel() }
173
152
  }
174
153
 
175
154
  @objc func destroy(_ viewTag: NSNumber) {
176
- DispatchQueue.main.async {
177
- self.getView(viewTag)?.destroy()
178
- }
155
+ onMainActor { self.getView(viewTag)?.destroy() }
179
156
  }
180
157
 
181
158
  @objc func showCastPicker(_ viewTag: NSNumber) {
159
+ onMainActor { self.getView(viewTag)?.showCastPicker() }
160
+ }
161
+
162
+ @objc func updatePlayout(_ viewTag: NSNumber, playoutJson: String) {
182
163
  DispatchQueue.main.async {
183
- self.getView(viewTag)?.showCastPicker()
164
+ self.getView(viewTag)?.updatePlayout(playoutJson)
184
165
  }
185
166
  }
186
167
 
187
168
  @objc func loadWithClipId(_ viewTag: NSNumber, clipId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
188
- DispatchQueue.main.async {
189
- self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
190
- }
169
+ onMainActor { self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
191
170
  }
192
171
 
193
172
  @objc func loadWithClipListId(_ viewTag: NSNumber, clipListId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
194
- DispatchQueue.main.async {
195
- self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
196
- }
173
+ onMainActor { self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
197
174
  }
198
175
 
199
176
  @objc func loadWithProjectId(_ viewTag: NSNumber, projectId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
200
- DispatchQueue.main.async {
201
- self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
202
- }
177
+ onMainActor { self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
203
178
  }
204
179
 
205
180
  @objc func loadWithClipJson(_ viewTag: NSNumber, clipJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
206
- DispatchQueue.main.async {
207
- self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
208
- }
181
+ onMainActor { self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
209
182
  }
210
183
 
211
184
  @objc func loadWithClipListJson(_ viewTag: NSNumber, clipListJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
212
- DispatchQueue.main.async {
213
- self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
214
- }
185
+ onMainActor { self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
215
186
  }
216
187
 
217
188
  @objc func loadWithProjectJson(_ viewTag: NSNumber, projectJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
218
- DispatchQueue.main.async {
219
- self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
220
- }
189
+ onMainActor { self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson) }
221
190
  }
222
191
 
223
192
  @objc func loadWithJsonUrl(_ viewTag: NSNumber, jsonUrl: String?, autoPlay: Bool, contextJson: String?) {
224
193
  NSLog("BBPlayerModule.loadWithJsonUrl called - viewTag: %@, jsonUrl: %@, autoPlay: %d, context: %@", viewTag, jsonUrl ?? "nil", autoPlay, contextJson ?? "nil")
225
- DispatchQueue.main.async {
194
+ onMainActor {
226
195
  let view = self.getView(viewTag)
227
196
  NSLog("BBPlayerModule.loadWithJsonUrl - view found: %@", view != nil ? "YES" : "NO")
228
197
  if let view = view, let url = jsonUrl {
@@ -235,7 +204,7 @@ class BBPlayerModule: RCTEventEmitter {
235
204
  }
236
205
 
237
206
  @objc func loadWithContentIdAndType(_ viewTag: NSNumber, contentId: String?, contentType: String?, autoPlay: Bool, contextJson: String?) {
238
- DispatchQueue.main.async {
207
+ onMainActor {
239
208
  let view = self.getView(viewTag)
240
209
  guard let view = view, let id = contentId, let type = contentType else {
241
210
  NSLog("BBPlayerModule.loadWithContentIdAndType - FAILED: view=%@, id=%@, type=%@",
@@ -247,101 +216,77 @@ class BBPlayerModule: RCTEventEmitter {
247
216
  }
248
217
 
249
218
  // MARK: - Getter methods with Promise support
219
+ // Note: nonisolated(unsafe) on resolver because RCTPromiseResolveBlock is a
220
+ // non-Sendable ObjC block, but RN guarantees safe cross-thread usage.
250
221
 
251
222
  @objc func getDuration(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
252
- DispatchQueue.main.async {
253
- let duration = self.getView(viewTag)?.duration()
254
- resolver(duration)
255
- }
223
+ nonisolated(unsafe) let resolve = resolver
224
+ onMainActor { resolve(self.getView(viewTag)?.duration()) }
256
225
  }
257
226
 
258
227
  @objc func getMuted(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
259
- DispatchQueue.main.async {
260
- let muted = self.getView(viewTag)?.muted()
261
- resolver(muted)
262
- }
228
+ nonisolated(unsafe) let resolve = resolver
229
+ onMainActor { resolve(self.getView(viewTag)?.muted()) }
263
230
  }
264
231
 
265
232
  @objc func getVolume(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
266
- DispatchQueue.main.async {
267
- let volume = self.getView(viewTag)?.volume()
268
- resolver(volume)
269
- }
233
+ nonisolated(unsafe) let resolve = resolver
234
+ onMainActor { resolve(self.getView(viewTag)?.volume()) }
270
235
  }
271
236
 
272
237
  @objc func getPhase(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
273
- DispatchQueue.main.async {
274
- let phase = self.getView(viewTag)?.phase()
275
- resolver(phase)
276
- }
238
+ nonisolated(unsafe) let resolve = resolver
239
+ onMainActor { resolve(self.getView(viewTag)?.phase()) }
277
240
  }
278
241
 
279
242
  @objc func getState(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
280
- DispatchQueue.main.async {
281
- let state = self.getView(viewTag)?.state()
282
- resolver(state)
283
- }
243
+ nonisolated(unsafe) let resolve = resolver
244
+ onMainActor { resolve(self.getView(viewTag)?.state()) }
284
245
  }
285
246
 
286
247
  @objc func getMode(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
287
- DispatchQueue.main.async {
288
- let mode = self.getView(viewTag)?.mode()
289
- resolver(mode)
290
- }
248
+ nonisolated(unsafe) let resolve = resolver
249
+ onMainActor { resolve(self.getView(viewTag)?.mode()) }
291
250
  }
292
251
 
293
252
  @objc func getClipData(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
294
- DispatchQueue.main.async {
295
- let clipData = self.getView(viewTag)?.clipData()
296
- resolver(clipData)
297
- }
253
+ nonisolated(unsafe) let resolve = resolver
254
+ onMainActor { resolve(self.getView(viewTag)?.clipData()) }
298
255
  }
299
256
 
300
257
  @objc func getProjectData(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
301
- DispatchQueue.main.async {
302
- let projectData = self.getView(viewTag)?.projectData()
303
- resolver(projectData)
304
- }
258
+ nonisolated(unsafe) let resolve = resolver
259
+ onMainActor { resolve(self.getView(viewTag)?.projectData()) }
305
260
  }
306
261
 
307
262
  @objc func getPlayoutData(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
308
- DispatchQueue.main.async {
309
- let playoutData = self.getView(viewTag)?.playoutData()
310
- resolver(playoutData)
311
- }
263
+ nonisolated(unsafe) let resolve = resolver
264
+ onMainActor { resolve(self.getView(viewTag)?.playoutData()) }
312
265
  }
313
266
 
314
267
  @objc func setInView(_ viewTag: NSNumber, inView: Bool) {
315
- DispatchQueue.main.async {
316
- self.getView(viewTag)?.setInView(inView)
317
- }
268
+ onMainActor { self.getView(viewTag)?.setInView(inView) }
318
269
  }
319
270
 
320
271
  @objc func getInView(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
321
- DispatchQueue.main.async {
322
- let inView = self.getView(viewTag)?.inView()
323
- resolver(inView)
324
- }
272
+ nonisolated(unsafe) let resolve = resolver
273
+ onMainActor { resolve(self.getView(viewTag)?.inView()) }
325
274
  }
326
275
 
327
276
  @objc func getAdMediaWidth(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
328
- DispatchQueue.main.async {
329
- let width = self.getView(viewTag)?.adMediaWidth()
330
- resolver(width)
331
- }
277
+ nonisolated(unsafe) let resolve = resolver
278
+ onMainActor { resolve(self.getView(viewTag)?.adMediaWidth()) }
332
279
  }
333
280
 
334
281
  @objc func getAdMediaHeight(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
335
- DispatchQueue.main.async {
336
- let height = self.getView(viewTag)?.adMediaHeight()
337
- resolver(height)
338
- }
282
+ nonisolated(unsafe) let resolve = resolver
283
+ onMainActor { resolve(self.getView(viewTag)?.adMediaHeight()) }
339
284
  }
340
285
 
341
286
  // MARK: - Modal Player API
342
287
 
343
288
  @objc func presentModalPlayer(_ jsonUrl: String, optionsJson: String?, contextJson: String?) {
344
- DispatchQueue.main.async {
289
+ onMainActor {
345
290
  // RCTPresentedViewController() can return nil with RCTReactNativeFactory (RN 0.83+).
346
291
  // Fallback to UIScene-based lookup to find the root view controller.
347
292
  guard let rootVC = RCTPresentedViewController()
@@ -371,14 +316,9 @@ class BBPlayerModule: RCTEventEmitter {
371
316
  }
372
317
  loadOptions["showBackArrow"] = options?["showBackArrow"] as? Bool ?? false
373
318
 
374
- // When context has a cliplist (contextCollectionId), load clip by ID
375
- // with cliplist context. ProgramController will swap to loading the
376
- // cliplist and find the clip by ID (matching web standardplayer pattern).
377
- // Deferred until apiReady to avoid racing with initial jsonUrl load.
378
319
  let collectionId = context?["contextCollectionId"] as? String
379
320
  let clipId = context?["contextEntityId"] as? String
380
321
 
381
- // Create and present modal player using SDK factory method
382
322
  let playerView = BBNativePlayer.createModalPlayerView(uiViewContoller: rootVC, jsonUrl: jsonUrl, options: loadOptions)
383
323
 
384
324
  if let collectionId = collectionId, let clipId = clipId {
@@ -391,38 +331,16 @@ class BBPlayerModule: RCTEventEmitter {
391
331
  }
392
332
  }
393
333
 
394
- // Set up delegate for event forwarding
395
334
  let delegate = ModalPlayerDelegate(module: self)
396
335
  playerView.delegate = delegate
397
336
 
398
337
  self.modalPlayerView = playerView
399
338
  self.modalDelegate = delegate
400
-
401
- }
402
- }
403
-
404
- private func extractIdFromUrl(_ url: String, pattern: String) -> String? {
405
- do {
406
- let regex = try NSRegularExpression(pattern: pattern, options: [])
407
- let range = NSRange(url.startIndex..., in: url)
408
- if let match = regex.firstMatch(in: url, options: [], range: range) {
409
- for i in 1..<match.numberOfRanges {
410
- if let groupRange = Range(match.range(at: i), in: url) {
411
- let extracted = String(url[groupRange])
412
- if !extracted.isEmpty {
413
- return extracted
414
- }
415
- }
416
- }
417
- }
418
- } catch {
419
- NSLog("BBPlayerModule: Regex error: %@", error.localizedDescription)
420
339
  }
421
- return nil
422
340
  }
423
341
 
424
342
  @objc func dismissModalPlayer() {
425
- DispatchQueue.main.async {
343
+ onMainActor {
426
344
  self.modalPlayerView?.player.closeModalPlayer()
427
345
  self.modalPlayerView = nil
428
346
  self.modalDelegate = nil
@@ -446,7 +364,9 @@ class BBPlayerModule: RCTEventEmitter {
446
364
 
447
365
  // MARK: - Modal Player Delegate
448
366
 
367
+ @MainActor
449
368
  private class ModalPlayerDelegate: NSObject, BBNativePlayerViewDelegate {
369
+ // Module ref is @unchecked Sendable, safe to access from @MainActor
450
370
  weak var module: BBPlayerModule?
451
371
 
452
372
  init(module: BBPlayerModule) {
@@ -1,8 +1,9 @@
1
- import BBNativePlayerKit
1
+ @preconcurrency import BBNativePlayerKit
2
+ @preconcurrency import React
2
3
  import WebKit
3
- import bbnativeshared
4
+ @preconcurrency import bbnativeshared
4
5
  import os
5
- import GoogleCast
6
+ @preconcurrency import GoogleCast
6
7
 
7
8
  // MARK: - Logging Helper
8
9
  private enum LogLevel {
@@ -49,6 +50,8 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
49
50
  private var independentCastButton: GCKUICastButton?
50
51
  // Store parent view controller reference for SDK
51
52
  private weak var parentViewController: UIViewController?
53
+ // Cached tag for deinit (nonisolated(unsafe) because deinit is nonisolated in Swift 6)
54
+ nonisolated(unsafe) private var _cachedTag: Int?
52
55
 
53
56
  // MARK: - Props (set from React Native)
54
57
 
@@ -130,7 +133,9 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
130
133
 
131
134
  // Register with view registry for command dispatch (supports New Architecture)
132
135
  if let tag = self.reactTag {
133
- BBPlayerViewRegistry.shared.register(self, tag: tag.intValue)
136
+ let tagInt = tag.intValue
137
+ _cachedTag = tagInt
138
+ BBPlayerViewRegistry.shared.register(self, tag: tagInt)
134
139
  }
135
140
 
136
141
  // Find the parent view controller from the responder chain
@@ -163,12 +168,11 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
163
168
  }
164
169
 
165
170
  deinit {
166
- // Unregister from view registry
167
- if let tag = self.reactTag {
168
- BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
171
+ // Unregister from view registry using cached tag
172
+ // (deinit is nonisolated in Swift 6, can't access @MainActor properties)
173
+ if let tag = _cachedTag {
174
+ BBPlayerViewRegistry.shared.unregister(tag: tag)
169
175
  }
170
- independentCastButton?.removeFromSuperview()
171
- independentCastButton = nil
172
176
  }
173
177
 
174
178
 
@@ -577,6 +581,14 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
577
581
  // iOS SDK cleans up automatically when view is removed
578
582
  }
579
583
 
584
+ func updatePlayout(_ playoutJson: String) {
585
+ guard playerView != nil else {
586
+ NSLog("BBPlayerView.updatePlayout ERROR - playerView not initialized")
587
+ return
588
+ }
589
+ playerView?.player.updatePlayoutWithJson(playoutJson: playoutJson)
590
+ }
591
+
580
592
  func pause() {
581
593
  playerView?.player.pause()
582
594
  }