@bluebillywig/react-native-bb-player 8.44.0 → 8.45.4

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 (55) hide show
  1. package/README.md +80 -59
  2. package/android/build.gradle +4 -3
  3. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerModule.kt +174 -28
  4. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerView.kt +70 -167
  5. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerViewManager.kt +9 -7
  6. package/android/src/paper/java/com/bluebillywig/bbplayer/NativeBBPlayerModuleSpec.java +19 -8
  7. package/ios/BBPlayerModule.mm +17 -8
  8. package/ios/BBPlayerModule.swift +192 -26
  9. package/ios/BBPlayerView.swift +55 -140
  10. package/ios/BBPlayerViewManager.m +2 -2
  11. package/ios/BBPlayerViewManager.swift +12 -0
  12. package/lib/commonjs/BBModalPlayer.js +21 -0
  13. package/lib/commonjs/BBModalPlayer.js.map +1 -0
  14. package/lib/commonjs/BBOutstreamView.js +2 -1
  15. package/lib/commonjs/BBOutstreamView.js.map +1 -1
  16. package/lib/commonjs/BBPlayerView.js +0 -1
  17. package/lib/commonjs/BBPlayerView.js.map +1 -1
  18. package/lib/commonjs/NativeCommands.js +32 -24
  19. package/lib/commonjs/NativeCommands.js.map +1 -1
  20. package/lib/commonjs/index.js +9 -1
  21. package/lib/commonjs/index.js.map +1 -1
  22. package/lib/commonjs/specs/NativeBBPlayerModule.js.map +1 -1
  23. package/lib/module/BBModalPlayer.js +17 -0
  24. package/lib/module/BBModalPlayer.js.map +1 -0
  25. package/lib/module/BBOutstreamView.js +2 -1
  26. package/lib/module/BBOutstreamView.js.map +1 -1
  27. package/lib/module/BBPlayerView.js +0 -1
  28. package/lib/module/BBPlayerView.js.map +1 -1
  29. package/lib/module/NativeCommands.js +32 -24
  30. package/lib/module/NativeCommands.js.map +1 -1
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/specs/NativeBBPlayerModule.js.map +1 -1
  34. package/lib/typescript/src/BBModalPlayer.d.ts +13 -0
  35. package/lib/typescript/src/BBModalPlayer.d.ts.map +1 -0
  36. package/lib/typescript/src/BBOutstreamView.d.ts.map +1 -1
  37. package/lib/typescript/src/BBPlayer.types.d.ts +28 -20
  38. package/lib/typescript/src/BBPlayer.types.d.ts.map +1 -1
  39. package/lib/typescript/src/BBPlayerView.d.ts +0 -2
  40. package/lib/typescript/src/BBPlayerView.d.ts.map +1 -1
  41. package/lib/typescript/src/NativeCommands.d.ts +10 -9
  42. package/lib/typescript/src/NativeCommands.d.ts.map +1 -1
  43. package/lib/typescript/src/index.d.ts +2 -0
  44. package/lib/typescript/src/index.d.ts.map +1 -1
  45. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts +13 -8
  46. package/lib/typescript/src/specs/NativeBBPlayerModule.d.ts.map +1 -1
  47. package/package.json +9 -10
  48. package/src/BBModalPlayer.ts +33 -0
  49. package/src/BBOutstreamView.tsx +2 -1
  50. package/src/BBPlayer.types.ts +35 -17
  51. package/src/BBPlayerView.tsx +0 -12
  52. package/src/NativeCommands.ts +45 -26
  53. package/src/index.ts +2 -0
  54. package/src/specs/NativeBBPlayerModule.ts +23 -8
  55. package/android/proguard-rules.pro +0 -59
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import React
3
+ import BBNativePlayerKit
3
4
 
4
5
  /**
5
6
  * Global registry for BBPlayerView instances.
@@ -39,21 +40,45 @@ class BBPlayerViewRegistry: NSObject {
39
40
 
40
41
  /**
41
42
  * Native Module for BBPlayer commands.
43
+ * Extends RCTEventEmitter to support module-level events (modal player).
42
44
  * This module looks up BBPlayerView instances by their React tag and dispatches commands to them.
43
45
  */
44
46
  @objc(BBPlayerModule)
45
- class BBPlayerModule: NSObject {
47
+ class BBPlayerModule: RCTEventEmitter {
46
48
 
47
- @objc var bridge: RCTBridge?
49
+ private var modalPlayerView: BBNativePlayerView?
50
+ private var modalDelegate: ModalPlayerDelegate?
51
+ private var pendingModalLoad: (() -> Void)?
52
+ private var hasListeners = false
48
53
 
49
- @objc static func requiresMainQueueSetup() -> Bool {
54
+ @objc override static func requiresMainQueueSetup() -> Bool {
50
55
  return true
51
56
  }
52
57
 
53
- @objc static func moduleName() -> String {
58
+ @objc override static func moduleName() -> String! {
54
59
  return "BBPlayerModule"
55
60
  }
56
61
 
62
+ override func supportedEvents() -> [String]! {
63
+ return [
64
+ "modalPlayerDismissed",
65
+ "modalPlayerPlay",
66
+ "modalPlayerPause",
67
+ "modalPlayerEnded",
68
+ "modalPlayerError",
69
+ "modalPlayerApiReady",
70
+ "modalPlayerCanPlay",
71
+ ]
72
+ }
73
+
74
+ override func startObserving() {
75
+ hasListeners = true
76
+ }
77
+
78
+ override func stopObserving() {
79
+ hasListeners = false
80
+ }
81
+
57
82
  // MARK: - Helper to get view by tag
58
83
 
59
84
  private func getView(_ reactTag: NSNumber) -> BBPlayerView? {
@@ -159,50 +184,50 @@ class BBPlayerModule: NSObject {
159
184
  }
160
185
  }
161
186
 
162
- @objc func loadWithClipId(_ viewTag: NSNumber, clipId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
187
+ @objc func loadWithClipId(_ viewTag: NSNumber, clipId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
163
188
  DispatchQueue.main.async {
164
- self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
189
+ self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
165
190
  }
166
191
  }
167
192
 
168
- @objc func loadWithClipListId(_ viewTag: NSNumber, clipListId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
193
+ @objc func loadWithClipListId(_ viewTag: NSNumber, clipListId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
169
194
  DispatchQueue.main.async {
170
- self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
195
+ self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
171
196
  }
172
197
  }
173
198
 
174
- @objc func loadWithProjectId(_ viewTag: NSNumber, projectId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
199
+ @objc func loadWithProjectId(_ viewTag: NSNumber, projectId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
175
200
  DispatchQueue.main.async {
176
- self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
201
+ self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
177
202
  }
178
203
  }
179
204
 
180
- @objc func loadWithClipJson(_ viewTag: NSNumber, clipJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
205
+ @objc func loadWithClipJson(_ viewTag: NSNumber, clipJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
181
206
  DispatchQueue.main.async {
182
- self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
207
+ self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
183
208
  }
184
209
  }
185
210
 
186
- @objc func loadWithClipListJson(_ viewTag: NSNumber, clipListJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
211
+ @objc func loadWithClipListJson(_ viewTag: NSNumber, clipListJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
187
212
  DispatchQueue.main.async {
188
- self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
213
+ self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
189
214
  }
190
215
  }
191
216
 
192
- @objc func loadWithProjectJson(_ viewTag: NSNumber, projectJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
217
+ @objc func loadWithProjectJson(_ viewTag: NSNumber, projectJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
193
218
  DispatchQueue.main.async {
194
- self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
219
+ self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
195
220
  }
196
221
  }
197
222
 
198
- @objc func loadWithJsonUrl(_ viewTag: NSNumber, jsonUrl: String?, autoPlay: Bool) {
199
- NSLog("BBPlayerModule.loadWithJsonUrl called - viewTag: %@, jsonUrl: %@, autoPlay: %d", viewTag, jsonUrl ?? "nil", autoPlay)
223
+ @objc func loadWithJsonUrl(_ viewTag: NSNumber, jsonUrl: String?, autoPlay: Bool, contextJson: String?) {
224
+ NSLog("BBPlayerModule.loadWithJsonUrl called - viewTag: %@, jsonUrl: %@, autoPlay: %d, context: %@", viewTag, jsonUrl ?? "nil", autoPlay, contextJson ?? "nil")
200
225
  DispatchQueue.main.async {
201
226
  let view = self.getView(viewTag)
202
227
  NSLog("BBPlayerModule.loadWithJsonUrl - view found: %@", view != nil ? "YES" : "NO")
203
228
  if let view = view, let url = jsonUrl {
204
229
  NSLog("BBPlayerModule.loadWithJsonUrl - calling view.loadWithJsonUrl with url: %@", url)
205
- view.loadWithJsonUrl(url, autoPlay: autoPlay)
230
+ view.loadWithJsonUrl(url, autoPlay: autoPlay, contextJson: contextJson)
206
231
  } else {
207
232
  NSLog("BBPlayerModule.loadWithJsonUrl - FAILED: view=%@, url=%@", view != nil ? "found" : "nil", jsonUrl ?? "nil")
208
233
  }
@@ -218,13 +243,6 @@ class BBPlayerModule: NSObject {
218
243
  }
219
244
  }
220
245
 
221
- @objc func getCurrentTime(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
222
- DispatchQueue.main.async {
223
- let currentTime = self.getView(viewTag)?.currentTime()
224
- resolver(currentTime)
225
- }
226
- }
227
-
228
246
  @objc func getMuted(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
229
247
  DispatchQueue.main.async {
230
248
  let muted = self.getView(viewTag)?.muted()
@@ -280,4 +298,152 @@ class BBPlayerModule: NSObject {
280
298
  resolver(playoutData)
281
299
  }
282
300
  }
301
+
302
+ // MARK: - Modal Player API
303
+
304
+ @objc func presentModalPlayer(_ jsonUrl: String, optionsJson: String?, contextJson: String?) {
305
+ DispatchQueue.main.async {
306
+ guard let rootVC = RCTPresentedViewController() else {
307
+ NSLog("BBPlayerModule: No root view controller found")
308
+ return
309
+ }
310
+
311
+ // Parse options from JSON string
312
+ var options: [String: Any]? = nil
313
+ if let json = optionsJson, let data = json.data(using: .utf8) {
314
+ options = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
315
+ }
316
+
317
+ // Parse context for playlist auto-advance
318
+ var context: [String: Any]? = nil
319
+ if let ctxJson = contextJson, let data = ctxJson.data(using: .utf8) {
320
+ context = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
321
+ }
322
+
323
+ // Create modal player view
324
+ let playerView = BBNativePlayerView(frame: rootVC.view.frame)
325
+ playerView.showBackArrow = options?["showBackArrow"] as? Bool ?? false
326
+
327
+ var loadOptions: [String: Any] = options ?? [:]
328
+ if loadOptions["autoPlay"] == nil {
329
+ loadOptions["autoPlay"] = true
330
+ }
331
+
332
+ // When context has a cliplist (contextCollectionId), load clip by ID
333
+ // with cliplist context. ProgramController will swap to loading the
334
+ // cliplist and find the clip by ID (matching web standardplayer pattern).
335
+ // Deferred until apiReady to avoid racing with initial jsonUrl load.
336
+ let collectionId = context?["contextCollectionId"] as? String
337
+ let clipId = context?["contextEntityId"] as? String
338
+
339
+ // Set up player with options (playout config, JWT, etc.)
340
+ playerView.setupWithJsonUrl(jsonUrl: jsonUrl, options: loadOptions)
341
+ playerView.presentModal(uiViewContoller: rootVC, animated: true)
342
+
343
+ if let collectionId = collectionId, let clipId = clipId {
344
+ let clipContext: [String: Any] = [
345
+ "contextCollectionType": context?["contextCollectionType"] as? String ?? "MediaClipList",
346
+ "contextCollectionId": collectionId,
347
+ ]
348
+ self.pendingModalLoad = {
349
+ playerView.player.loadWithClipId(clipId: clipId, initiator: "external", autoPlay: true, seekTo: nil, context: clipContext)
350
+ }
351
+ }
352
+
353
+ // Set up delegate for event forwarding
354
+ let delegate = ModalPlayerDelegate(module: self)
355
+ playerView.delegate = delegate
356
+
357
+ self.modalPlayerView = playerView
358
+ self.modalDelegate = delegate
359
+
360
+ NSLog("BBPlayerModule: Modal player presented with URL: %@, context: %@", jsonUrl, contextJson ?? "nil")
361
+ }
362
+ }
363
+
364
+ private func extractIdFromUrl(_ url: String, pattern: String) -> String? {
365
+ do {
366
+ let regex = try NSRegularExpression(pattern: pattern, options: [])
367
+ let range = NSRange(url.startIndex..., in: url)
368
+ if let match = regex.firstMatch(in: url, options: [], range: range) {
369
+ for i in 1..<match.numberOfRanges {
370
+ if let groupRange = Range(match.range(at: i), in: url) {
371
+ let extracted = String(url[groupRange])
372
+ if !extracted.isEmpty {
373
+ return extracted
374
+ }
375
+ }
376
+ }
377
+ }
378
+ } catch {
379
+ NSLog("BBPlayerModule: Regex error: %@", error.localizedDescription)
380
+ }
381
+ return nil
382
+ }
383
+
384
+ @objc func dismissModalPlayer() {
385
+ DispatchQueue.main.async {
386
+ self.modalPlayerView?.player.closeModalPlayer()
387
+ self.modalPlayerView = nil
388
+ self.modalDelegate = nil
389
+ self.pendingModalLoad = nil
390
+ }
391
+ }
392
+
393
+ @objc override func addListener(_ eventName: String) {
394
+ // Required for RCTEventEmitter
395
+ }
396
+
397
+ @objc override func removeListeners(_ count: Double) {
398
+ // Required for RCTEventEmitter
399
+ }
400
+
401
+ private func emitEvent(_ name: String, body: Any? = nil) {
402
+ if hasListeners {
403
+ sendEvent(withName: name, body: body)
404
+ }
405
+ }
406
+
407
+ // MARK: - Modal Player Delegate
408
+
409
+ private class ModalPlayerDelegate: NSObject, BBNativePlayerViewDelegate {
410
+ weak var module: BBPlayerModule?
411
+
412
+ init(module: BBPlayerModule) {
413
+ self.module = module
414
+ }
415
+
416
+ func bbNativePlayerView(didTriggerPlay playerView: BBNativePlayerView) {
417
+ module?.emitEvent("modalPlayerPlay")
418
+ }
419
+
420
+ func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
421
+ module?.emitEvent("modalPlayerPause")
422
+ }
423
+
424
+ func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
425
+ module?.emitEvent("modalPlayerEnded")
426
+ }
427
+
428
+ func bbNativePlayerView(playerView: BBNativePlayerView, didFailWithError error: String?) {
429
+ module?.emitEvent("modalPlayerError", body: ["error": error ?? "Unknown error"])
430
+ }
431
+
432
+ func bbNativePlayerView(didTriggerApiReady playerView: BBNativePlayerView) {
433
+ module?.pendingModalLoad?()
434
+ module?.pendingModalLoad = nil
435
+ module?.emitEvent("modalPlayerApiReady")
436
+ }
437
+
438
+ func bbNativePlayerView(didTriggerCanPlay playerView: BBNativePlayerView) {
439
+ module?.emitEvent("modalPlayerCanPlay")
440
+ }
441
+
442
+ func bbNativePlayerView(didCloseModalPlayer playerView: BBNativePlayerView) {
443
+ module?.emitEvent("modalPlayerDismissed")
444
+ module?.modalPlayerView = nil
445
+ module?.modalDelegate = nil
446
+ module?.pendingModalLoad = nil
447
+ }
448
+ }
283
449
  }
@@ -42,16 +42,9 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
42
42
  private var playerView: BBNativePlayerView?
43
43
  private var hasSetup: Bool = false
44
44
 
45
- // Timer for periodic time updates (opt-in for performance)
46
- private var timeUpdateTimer: Timer?
47
45
  private var isPlaying: Bool = false
48
46
  private var currentDuration: Double = 0.0
49
- private var lastKnownTime: Double = 0.0
50
- private var playbackStartTimestamp: CFTimeInterval = 0
51
- private var lastEmittedTime: Double = 0.0
52
47
  private var isInFullscreen: Bool = false
53
- private var backgroundObserver: NSObjectProtocol?
54
- private var foregroundObserver: NSObjectProtocol?
55
48
  // Independent Google Cast button for showing the cast picker
56
49
  private var independentCastButton: GCKUICastButton?
57
50
  // Store parent view controller reference for SDK
@@ -71,18 +64,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
71
64
  }
72
65
  }
73
66
 
74
- @objc var enableTimeUpdates: Bool = false {
75
- didSet {
76
- log("Time updates \(enableTimeUpdates ? "enabled" : "disabled")")
77
-
78
- if !enableTimeUpdates && timeUpdateTimer != nil {
79
- stopTimeUpdates()
80
- } else if enableTimeUpdates && isPlaying && timeUpdateTimer == nil {
81
- startTimeUpdates()
82
- }
83
- }
84
- }
85
-
86
67
  // MARK: - Event handlers (RCTDirectEventBlock)
87
68
 
88
69
  @objc var onDidFailWithError: RCTDirectEventBlock?
@@ -123,7 +104,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
123
104
  @objc var onDidTriggerViewFinished: RCTDirectEventBlock?
124
105
  @objc var onDidTriggerViewStarted: RCTDirectEventBlock?
125
106
  @objc var onDidTriggerVolumeChange: RCTDirectEventBlock?
126
- @objc var onDidTriggerTimeUpdate: RCTDirectEventBlock?
127
107
  @objc var onDidTriggerApiReady: RCTDirectEventBlock?
128
108
 
129
109
  override var intrinsicContentSize: CGSize {
@@ -153,8 +133,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
153
133
  BBPlayerViewRegistry.shared.register(self, tag: tag.intValue)
154
134
  }
155
135
 
156
- setupAppLifecycleObservers()
157
-
158
136
  // Find the parent view controller from the responder chain
159
137
  var responder = self.next
160
138
  while responder != nil {
@@ -180,91 +158,19 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
180
158
  BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
181
159
  }
182
160
 
183
- if !isInFullscreen {
184
- stopTimeUpdates()
185
- }
161
+ // No cleanup needed when not in fullscreen
186
162
  }
187
163
  }
188
164
 
189
- // Start periodic time updates (1x per second, only if enabled)
190
- private func startTimeUpdates() {
191
- guard enableTimeUpdates, timeUpdateTimer == nil else { return }
192
-
193
- timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
194
- guard let self = self, self.isPlaying else { return }
195
-
196
- let elapsedSeconds = CACurrentMediaTime() - self.playbackStartTimestamp
197
- let estimatedTime = self.lastKnownTime + elapsedSeconds
198
- let currentTime = min(estimatedTime, self.currentDuration)
199
-
200
- if self.currentDuration > 0 && abs(currentTime - self.lastEmittedTime) >= 0.5 {
201
- self.lastEmittedTime = currentTime
202
- self.onDidTriggerTimeUpdate?([
203
- "currentTime": currentTime,
204
- "duration": self.currentDuration
205
- ])
206
- }
207
- }
208
- }
209
-
210
- private func stopTimeUpdates() {
211
- timeUpdateTimer?.invalidate()
212
- timeUpdateTimer = nil
213
- }
214
-
215
165
  deinit {
216
166
  // Unregister from view registry
217
167
  if let tag = self.reactTag {
218
168
  BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
219
169
  }
220
- stopTimeUpdates()
221
- removeAppLifecycleObservers()
222
170
  independentCastButton?.removeFromSuperview()
223
171
  independentCastButton = nil
224
172
  }
225
173
 
226
- // MARK: - App Lifecycle Management
227
-
228
- private func setupAppLifecycleObservers() {
229
- guard backgroundObserver == nil else { return }
230
-
231
- backgroundObserver = NotificationCenter.default.addObserver(
232
- forName: UIApplication.didEnterBackgroundNotification,
233
- object: nil,
234
- queue: .main
235
- ) { [weak self] _ in
236
- guard let self = self else { return }
237
- if self.isPlaying {
238
- self.lastKnownTime = self.calculateCurrentTime()
239
- }
240
- self.stopTimeUpdates()
241
- log("Timer paused - app entered background", level: .debug)
242
- }
243
-
244
- foregroundObserver = NotificationCenter.default.addObserver(
245
- forName: UIApplication.willEnterForegroundNotification,
246
- object: nil,
247
- queue: .main
248
- ) { [weak self] _ in
249
- guard let self = self else { return }
250
- if self.isPlaying && self.enableTimeUpdates {
251
- self.playbackStartTimestamp = CACurrentMediaTime()
252
- self.startTimeUpdates()
253
- log("Timer resumed - app entered foreground", level: .debug)
254
- }
255
- }
256
- }
257
-
258
- private func removeAppLifecycleObservers() {
259
- if let observer = backgroundObserver {
260
- NotificationCenter.default.removeObserver(observer)
261
- backgroundObserver = nil
262
- }
263
- if let observer = foregroundObserver {
264
- NotificationCenter.default.removeObserver(observer)
265
- foregroundObserver = nil
266
- }
267
- }
268
174
 
269
175
  // MARK: - Player Setup (Simplified - no intermediate view controller)
270
176
 
@@ -409,7 +315,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
409
315
 
410
316
  func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
411
317
  isPlaying = false
412
- stopTimeUpdates()
413
318
  onDidTriggerEnded?([:])
414
319
  }
415
320
 
@@ -451,7 +356,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
451
356
 
452
357
  func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
453
358
  isPlaying = false
454
- stopTimeUpdates()
455
359
  onDidTriggerPause?([:])
456
360
  }
457
361
 
@@ -465,10 +369,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
465
369
 
466
370
  func bbNativePlayerView(didTriggerPlaying playerView: BBNativePlayerView) {
467
371
  isPlaying = true
468
- playbackStartTimestamp = CACurrentMediaTime()
469
- lastEmittedTime = 0.0
470
- lastKnownTime = calculateCurrentTime()
471
- startTimeUpdates()
472
372
  onDidTriggerPlaying?([:])
473
373
  }
474
374
 
@@ -500,9 +400,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
500
400
  }
501
401
 
502
402
  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerSeeked seekOffset: Double) {
503
- lastKnownTime = seekOffset
504
- playbackStartTimestamp = CACurrentMediaTime()
505
- lastEmittedTime = 0.0
506
403
  onDidTriggerSeeked?(["payload": seekOffset as Any])
507
404
  }
508
405
 
@@ -557,18 +454,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
557
454
  addSubview(castButton)
558
455
  }
559
456
 
560
- // MARK: - Private Helper Methods
561
-
562
- private func calculateCurrentTime() -> Double {
563
- if isPlaying && playbackStartTimestamp > 0 {
564
- let elapsedSeconds = CACurrentMediaTime() - playbackStartTimestamp
565
- let estimatedTime = lastKnownTime + elapsedSeconds
566
- return min(estimatedTime, currentDuration)
567
- } else {
568
- return lastKnownTime
569
- }
570
- }
571
-
572
457
  // MARK: - Public API Methods
573
458
 
574
459
  func adMediaHeight() -> Int? {
@@ -587,10 +472,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
587
472
  return nil
588
473
  }
589
474
 
590
- func currentTime() -> Double? {
591
- return calculateCurrentTime()
592
- }
593
-
594
475
  func duration() -> Double? {
595
476
  return playerView?.player.duration
596
477
  }
@@ -639,6 +520,20 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
639
520
  playerView?.player.expand()
640
521
  }
641
522
 
523
+ func presentModal() {
524
+ guard let parentVC = parentViewController else {
525
+ log("Cannot present modal - no parent view controller", level: .warning)
526
+ return
527
+ }
528
+ isInFullscreen = true
529
+ playerView?.presentModal(uiViewContoller: parentVC, animated: true)
530
+ }
531
+
532
+ func closeModal() {
533
+ playerView?.player.closeModalPlayer()
534
+ isInFullscreen = false
535
+ }
536
+
642
537
  func enterFullscreen() {
643
538
  enterFullscreenWithLandscapeForce(forceLandscape: false)
644
539
  }
@@ -691,9 +586,7 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
691
586
  }
692
587
 
693
588
  func seekRelative(_ offsetInSeconds: Double) {
694
- let currentTime = calculateCurrentTime()
695
- let newPosition = max(0, min(currentDuration, currentTime + offsetInSeconds))
696
- playerView?.player.seek(offsetInSeconds: newPosition as NSNumber)
589
+ playerView?.player.seekRelative(offsetInSeconds: offsetInSeconds as NSNumber)
697
590
  }
698
591
 
699
592
  func setMuted(_ muted: Bool) {
@@ -704,28 +597,48 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
704
597
  playerView?.setApiProperty(property: .volume, value: Float(volume))
705
598
  }
706
599
 
707
- func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
708
- playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
600
+ // Helper to parse context JSON into a dictionary for the native SDK
601
+ private func parseContext(_ contextJson: String?) -> [String: Any]? {
602
+ guard let jsonString = contextJson, !jsonString.isEmpty else { return nil }
603
+ guard let data = jsonString.data(using: .utf8) else { return nil }
604
+ do {
605
+ if let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
606
+ return dict
607
+ }
608
+ } catch {
609
+ log("Failed to parse context JSON: \(error)", level: .warning)
610
+ }
611
+ return nil
709
612
  }
710
613
 
711
- func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
712
- playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
614
+ func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
615
+ let context = parseContext(contextJson)
616
+ playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
713
617
  }
714
618
 
715
- func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
716
- playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
619
+ func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
620
+ let context = parseContext(contextJson)
621
+ playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
717
622
  }
718
623
 
719
- func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
720
- playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
624
+ func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
625
+ let context = parseContext(contextJson)
626
+ playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
721
627
  }
722
628
 
723
- func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
724
- playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
629
+ func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
630
+ let context = parseContext(contextJson)
631
+ playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
725
632
  }
726
633
 
727
- func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
728
- playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
634
+ func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
635
+ let context = parseContext(contextJson)
636
+ playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
637
+ }
638
+
639
+ func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
640
+ let context = parseContext(contextJson)
641
+ playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
729
642
  }
730
643
 
731
644
  /**
@@ -735,8 +648,8 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
735
648
  *
736
649
  * Note: Shorts URLs (/sh/{id}.json) are NOT supported here - use BBShortsView instead.
737
650
  */
738
- func loadWithJsonUrl(_ url: String, autoPlay: Bool) {
739
- NSLog("BBPlayerView.loadWithJsonUrl called - url: %@, autoPlay: %d", url, autoPlay)
651
+ func loadWithJsonUrl(_ url: String, autoPlay: Bool, contextJson: String? = nil) {
652
+ NSLog("BBPlayerView.loadWithJsonUrl called - url: %@, autoPlay: %d, context: %@", url, autoPlay, contextJson ?? "nil")
740
653
  guard playerView != nil else {
741
654
  NSLog("BBPlayerView.loadWithJsonUrl ERROR - playerView not initialized")
742
655
  return
@@ -754,6 +667,8 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
754
667
  let projectIdPattern = "/pj/([0-9]+)\\.json|/project/([0-9]+)"
755
668
  let shortsIdPattern = "/sh/([0-9]+)\\.json"
756
669
 
670
+ let context = parseContext(contextJson)
671
+
757
672
  if let shortsMatch = url.range(of: shortsIdPattern, options: .regularExpression) {
758
673
  // Shorts require a separate BBShortsView component
759
674
  log("ERROR - Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead.", level: .error)
@@ -765,7 +680,7 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
765
680
  // Extract the cliplist ID
766
681
  if let clipListId = extractIdFromUrl(url, pattern: clipListIdPattern) {
767
682
  NSLog("BBPlayerView.loadWithJsonUrl - Loading ClipList by ID: %@", clipListId)
768
- playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: "external", autoPlay: autoPlay, seekTo: nil)
683
+ playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
769
684
  } else {
770
685
  NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract cliplist ID from URL: %@", url)
771
686
  }
@@ -776,7 +691,7 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
776
691
  // Extract the project ID
777
692
  if let projectId = extractIdFromUrl(url, pattern: projectIdPattern) {
778
693
  NSLog("BBPlayerView.loadWithJsonUrl - Loading Project by ID: %@", projectId)
779
- playerView?.player.loadWithProjectId(projectId: projectId, initiator: "external", autoPlay: autoPlay, seekTo: nil)
694
+ playerView?.player.loadWithProjectId(projectId: projectId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
780
695
  } else {
781
696
  NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract project ID from URL: %@", url)
782
697
  }
@@ -787,7 +702,7 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
787
702
  // Extract the clip ID
788
703
  if let clipId = extractIdFromUrl(url, pattern: clipIdPattern) {
789
704
  NSLog("BBPlayerView.loadWithJsonUrl - Loading Clip by ID: %@", clipId)
790
- playerView?.player.loadWithClipId(clipId: clipId, initiator: "external", autoPlay: autoPlay, seekTo: nil)
705
+ playerView?.player.loadWithClipId(clipId: clipId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
791
706
  } else {
792
707
  NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract clip ID from URL: %@", url)
793
708
  }
@@ -7,7 +7,6 @@
7
7
  // Props
8
8
  RCT_EXPORT_VIEW_PROPERTY(jsonUrl, NSString)
9
9
  RCT_EXPORT_VIEW_PROPERTY(options, NSDictionary)
10
- RCT_EXPORT_VIEW_PROPERTY(enableTimeUpdates, BOOL)
11
10
 
12
11
  // Event handlers
13
12
  RCT_EXPORT_VIEW_PROPERTY(onDidFailWithError, RCTDirectEventBlock)
@@ -48,7 +47,6 @@ RCT_EXPORT_VIEW_PROPERTY(onDidTriggerStateChange, RCTDirectEventBlock)
48
47
  RCT_EXPORT_VIEW_PROPERTY(onDidTriggerViewFinished, RCTDirectEventBlock)
49
48
  RCT_EXPORT_VIEW_PROPERTY(onDidTriggerViewStarted, RCTDirectEventBlock)
50
49
  RCT_EXPORT_VIEW_PROPERTY(onDidTriggerVolumeChange, RCTDirectEventBlock)
51
- RCT_EXPORT_VIEW_PROPERTY(onDidTriggerTimeUpdate, RCTDirectEventBlock)
52
50
  RCT_EXPORT_VIEW_PROPERTY(onDidTriggerApiReady, RCTDirectEventBlock)
53
51
 
54
52
  // Commands (methods callable from JS)
@@ -58,6 +56,8 @@ RCT_EXTERN_METHOD(seek:(nonnull NSNumber *)reactTag offsetInSeconds:(nonnull NSN
58
56
  RCT_EXTERN_METHOD(seekRelative:(nonnull NSNumber *)reactTag offsetInSeconds:(nonnull NSNumber *)offsetInSeconds)
59
57
  RCT_EXTERN_METHOD(setMuted:(nonnull NSNumber *)reactTag muted:(BOOL)muted)
60
58
  RCT_EXTERN_METHOD(setVolume:(nonnull NSNumber *)reactTag volume:(nonnull NSNumber *)volume)
59
+ RCT_EXTERN_METHOD(presentModal:(nonnull NSNumber *)reactTag)
60
+ RCT_EXTERN_METHOD(closeModal:(nonnull NSNumber *)reactTag)
61
61
  RCT_EXTERN_METHOD(enterFullscreen:(nonnull NSNumber *)reactTag)
62
62
  RCT_EXTERN_METHOD(enterFullscreenLandscape:(nonnull NSNumber *)reactTag)
63
63
  RCT_EXTERN_METHOD(exitFullscreen:(nonnull NSNumber *)reactTag)
@@ -62,6 +62,18 @@ class BBPlayerViewManager: RCTViewManager {
62
62
  }
63
63
  }
64
64
 
65
+ @objc func presentModal(_ reactTag: NSNumber) {
66
+ DispatchQueue.main.async {
67
+ self.getView(reactTag)?.presentModal()
68
+ }
69
+ }
70
+
71
+ @objc func closeModal(_ reactTag: NSNumber) {
72
+ DispatchQueue.main.async {
73
+ self.getView(reactTag)?.closeModal()
74
+ }
75
+ }
76
+
65
77
  @objc func enterFullscreen(_ reactTag: NSNumber) {
66
78
  DispatchQueue.main.async {
67
79
  self.getView(reactTag)?.enterFullscreen()