@bluebillywig/react-native-bb-player 8.42.15 → 8.45.0

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 +2 -1
  3. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerModule.kt +146 -28
  4. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerView.kt +74 -165
  5. package/android/src/main/java/com/bluebillywig/bbplayer/BBPlayerViewManager.kt +0 -6
  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 +193 -31
  9. package/ios/BBPlayerView.swift +141 -132
  10. package/ios/BBPlayerViewManager.m +0 -2
  11. package/ios/BBPlayerViewManager.swift +8 -0
  12. package/lib/commonjs/BBModalPlayer.js +21 -0
  13. package/lib/commonjs/BBModalPlayer.js.map +1 -0
  14. package/lib/commonjs/BBOutstreamView.js +0 -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 +24 -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 +0 -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 +24 -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 +24 -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 +8 -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 +8 -11
  48. package/src/BBModalPlayer.ts +32 -0
  49. package/src/BBOutstreamView.tsx +0 -1
  50. package/src/BBPlayer.types.ts +31 -17
  51. package/src/BBPlayerView.tsx +0 -12
  52. package/src/NativeCommands.ts +37 -26
  53. package/src/index.ts +2 -0
  54. package/src/specs/NativeBBPlayerModule.ts +25 -8
  55. package/android/proguard-rules.pro +0 -59
@@ -1,31 +1,100 @@
1
1
  import Foundation
2
2
  import React
3
+ import BBNativePlayerKit
4
+
5
+ /**
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.
9
+ */
10
+ class BBPlayerViewRegistry: NSObject {
11
+ static let shared = BBPlayerViewRegistry()
12
+
13
+ private var views: [Int: BBPlayerView] = [:]
14
+ private let lock = NSLock()
15
+
16
+ private override init() {
17
+ super.init()
18
+ }
19
+
20
+ func register(_ view: BBPlayerView, tag: Int) {
21
+ lock.lock()
22
+ defer { lock.unlock() }
23
+ views[tag] = view
24
+ NSLog("BBPlayerViewRegistry: Registered view with tag %d (total: %d)", tag, views.count)
25
+ }
26
+
27
+ func unregister(tag: Int) {
28
+ lock.lock()
29
+ defer { lock.unlock() }
30
+ views.removeValue(forKey: tag)
31
+ NSLog("BBPlayerViewRegistry: Unregistered view with tag %d (total: %d)", tag, views.count)
32
+ }
33
+
34
+ func getView(tag: Int) -> BBPlayerView? {
35
+ lock.lock()
36
+ defer { lock.unlock() }
37
+ return views[tag]
38
+ }
39
+ }
3
40
 
4
41
  /**
5
42
  * Native Module for BBPlayer commands.
43
+ * Extends RCTEventEmitter to support module-level events (modal player).
6
44
  * This module looks up BBPlayerView instances by their React tag and dispatches commands to them.
7
45
  */
8
46
  @objc(BBPlayerModule)
9
- class BBPlayerModule: NSObject {
47
+ class BBPlayerModule: RCTEventEmitter {
10
48
 
11
- @objc var bridge: RCTBridge?
49
+ private var modalPlayerView: BBNativePlayerView?
50
+ private var modalDelegate: ModalPlayerDelegate?
51
+ private var hasListeners = false
12
52
 
13
- @objc static func requiresMainQueueSetup() -> Bool {
53
+ @objc override static func requiresMainQueueSetup() -> Bool {
14
54
  return true
15
55
  }
16
56
 
17
- @objc static func moduleName() -> String {
57
+ @objc override static func moduleName() -> String! {
18
58
  return "BBPlayerModule"
19
59
  }
20
60
 
61
+ override func supportedEvents() -> [String]! {
62
+ return [
63
+ "modalPlayerDismissed",
64
+ "modalPlayerPlay",
65
+ "modalPlayerPause",
66
+ "modalPlayerEnded",
67
+ "modalPlayerError",
68
+ "modalPlayerApiReady",
69
+ "modalPlayerCanPlay",
70
+ ]
71
+ }
72
+
73
+ override func startObserving() {
74
+ hasListeners = true
75
+ }
76
+
77
+ override func stopObserving() {
78
+ hasListeners = false
79
+ }
80
+
21
81
  // MARK: - Helper to get view by tag
22
82
 
23
83
  private func getView(_ reactTag: NSNumber) -> BBPlayerView? {
24
- guard let bridge = self.bridge else {
25
- NSLog("BBPlayerModule: Bridge is nil")
26
- return nil
84
+ // First try the view registry (works with both old and new architecture)
85
+ if let view = BBPlayerViewRegistry.shared.getView(tag: reactTag.intValue) {
86
+ return view
27
87
  }
28
- return bridge.uiManager.view(forReactTag: reactTag) as? BBPlayerView
88
+
89
+ // Fallback to bridge.uiManager for old architecture
90
+ if let bridge = self.bridge {
91
+ if let view = bridge.uiManager.view(forReactTag: reactTag) as? BBPlayerView {
92
+ return view
93
+ }
94
+ }
95
+
96
+ NSLog("BBPlayerModule: Could not find view with tag %@", reactTag)
97
+ return nil
29
98
  }
30
99
 
31
100
  // MARK: - Commands
@@ -114,47 +183,52 @@ class BBPlayerModule: NSObject {
114
183
  }
115
184
  }
116
185
 
117
- @objc func loadWithClipId(_ viewTag: NSNumber, clipId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
186
+ @objc func loadWithClipId(_ viewTag: NSNumber, clipId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
118
187
  DispatchQueue.main.async {
119
- self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
188
+ self.getView(viewTag)?.loadWithClipId(clipId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
120
189
  }
121
190
  }
122
191
 
123
- @objc func loadWithClipListId(_ viewTag: NSNumber, clipListId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
192
+ @objc func loadWithClipListId(_ viewTag: NSNumber, clipListId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
124
193
  DispatchQueue.main.async {
125
- self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
194
+ self.getView(viewTag)?.loadWithClipListId(clipListId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
126
195
  }
127
196
  }
128
197
 
129
- @objc func loadWithProjectId(_ viewTag: NSNumber, projectId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
198
+ @objc func loadWithProjectId(_ viewTag: NSNumber, projectId: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
130
199
  DispatchQueue.main.async {
131
- self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
200
+ self.getView(viewTag)?.loadWithProjectId(projectId ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
132
201
  }
133
202
  }
134
203
 
135
- @objc func loadWithClipJson(_ viewTag: NSNumber, clipJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
204
+ @objc func loadWithClipJson(_ viewTag: NSNumber, clipJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
136
205
  DispatchQueue.main.async {
137
- self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
206
+ self.getView(viewTag)?.loadWithClipJson(clipJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
138
207
  }
139
208
  }
140
209
 
141
- @objc func loadWithClipListJson(_ viewTag: NSNumber, clipListJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
210
+ @objc func loadWithClipListJson(_ viewTag: NSNumber, clipListJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
142
211
  DispatchQueue.main.async {
143
- self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
212
+ self.getView(viewTag)?.loadWithClipListJson(clipListJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
144
213
  }
145
214
  }
146
215
 
147
- @objc func loadWithProjectJson(_ viewTag: NSNumber, projectJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?) {
216
+ @objc func loadWithProjectJson(_ viewTag: NSNumber, projectJson: String?, initiator: String?, autoPlay: Bool, seekTo: NSNumber?, contextJson: String?) {
148
217
  DispatchQueue.main.async {
149
- self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue)
218
+ self.getView(viewTag)?.loadWithProjectJson(projectJson ?? "", initiator: initiator, autoPlay: autoPlay, seekTo: seekTo?.doubleValue, contextJson: contextJson)
150
219
  }
151
220
  }
152
221
 
153
- @objc func loadWithJsonUrl(_ viewTag: NSNumber, jsonUrl: String?, autoPlay: Bool) {
222
+ @objc func loadWithJsonUrl(_ viewTag: NSNumber, jsonUrl: String?, autoPlay: Bool, contextJson: String?) {
223
+ NSLog("BBPlayerModule.loadWithJsonUrl called - viewTag: %@, jsonUrl: %@, autoPlay: %d, context: %@", viewTag, jsonUrl ?? "nil", autoPlay, contextJson ?? "nil")
154
224
  DispatchQueue.main.async {
155
- // Load new JSON URL - update the jsonUrl property and re-setup
156
- if let view = self.getView(viewTag), let url = jsonUrl {
157
- view.jsonUrl = url
225
+ let view = self.getView(viewTag)
226
+ NSLog("BBPlayerModule.loadWithJsonUrl - view found: %@", view != nil ? "YES" : "NO")
227
+ if let view = view, let url = jsonUrl {
228
+ NSLog("BBPlayerModule.loadWithJsonUrl - calling view.loadWithJsonUrl with url: %@", url)
229
+ view.loadWithJsonUrl(url, autoPlay: autoPlay, contextJson: contextJson)
230
+ } else {
231
+ NSLog("BBPlayerModule.loadWithJsonUrl - FAILED: view=%@, url=%@", view != nil ? "found" : "nil", jsonUrl ?? "nil")
158
232
  }
159
233
  }
160
234
  }
@@ -168,13 +242,6 @@ class BBPlayerModule: NSObject {
168
242
  }
169
243
  }
170
244
 
171
- @objc func getCurrentTime(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
172
- DispatchQueue.main.async {
173
- let currentTime = self.getView(viewTag)?.currentTime()
174
- resolver(currentTime)
175
- }
176
- }
177
-
178
245
  @objc func getMuted(_ viewTag: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
179
246
  DispatchQueue.main.async {
180
247
  let muted = self.getView(viewTag)?.muted()
@@ -230,4 +297,99 @@ class BBPlayerModule: NSObject {
230
297
  resolver(playoutData)
231
298
  }
232
299
  }
300
+
301
+ // MARK: - Modal Player API
302
+
303
+ @objc func presentModalPlayer(_ jsonUrl: String, optionsJson: String?) {
304
+ DispatchQueue.main.async {
305
+ guard let rootVC = RCTPresentedViewController() else {
306
+ NSLog("BBPlayerModule: No root view controller found")
307
+ return
308
+ }
309
+
310
+ // Parse options from JSON string
311
+ var options: [String: Any]? = nil
312
+ if let json = optionsJson, let data = json.data(using: .utf8) {
313
+ options = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
314
+ }
315
+
316
+ // Create modal player via native SDK
317
+ let playerView = BBNativePlayer.createModalPlayerView(
318
+ uiViewContoller: rootVC,
319
+ jsonUrl: jsonUrl,
320
+ options: options
321
+ )
322
+
323
+ // Set up delegate for event forwarding
324
+ let delegate = ModalPlayerDelegate(module: self)
325
+ playerView.delegate = delegate
326
+
327
+ self.modalPlayerView = playerView
328
+ self.modalDelegate = delegate
329
+
330
+ NSLog("BBPlayerModule: Modal player presented with URL: %@", jsonUrl)
331
+ }
332
+ }
333
+
334
+ @objc func dismissModalPlayer() {
335
+ DispatchQueue.main.async {
336
+ self.modalPlayerView?.player.closeModalPlayer()
337
+ self.modalPlayerView = nil
338
+ self.modalDelegate = nil
339
+ }
340
+ }
341
+
342
+ @objc override func addListener(_ eventName: String) {
343
+ // Required for RCTEventEmitter
344
+ }
345
+
346
+ @objc override func removeListeners(_ count: Double) {
347
+ // Required for RCTEventEmitter
348
+ }
349
+
350
+ private func emitEvent(_ name: String, body: Any? = nil) {
351
+ if hasListeners {
352
+ sendEvent(withName: name, body: body)
353
+ }
354
+ }
355
+
356
+ // MARK: - Modal Player Delegate
357
+
358
+ private class ModalPlayerDelegate: NSObject, BBNativePlayerViewDelegate {
359
+ weak var module: BBPlayerModule?
360
+
361
+ init(module: BBPlayerModule) {
362
+ self.module = module
363
+ }
364
+
365
+ func bbNativePlayerView(didTriggerPlay playerView: BBNativePlayerView) {
366
+ module?.emitEvent("modalPlayerPlay")
367
+ }
368
+
369
+ func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
370
+ module?.emitEvent("modalPlayerPause")
371
+ }
372
+
373
+ func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
374
+ module?.emitEvent("modalPlayerEnded")
375
+ }
376
+
377
+ func bbNativePlayerView(playerView: BBNativePlayerView, didFailWithError error: String?) {
378
+ module?.emitEvent("modalPlayerError", body: ["error": error ?? "Unknown error"])
379
+ }
380
+
381
+ func bbNativePlayerView(didTriggerApiReady playerView: BBNativePlayerView) {
382
+ module?.emitEvent("modalPlayerApiReady")
383
+ }
384
+
385
+ func bbNativePlayerView(didTriggerCanPlay playerView: BBNativePlayerView) {
386
+ module?.emitEvent("modalPlayerCanPlay")
387
+ }
388
+
389
+ func bbNativePlayerView(didCloseModalPlayer playerView: BBNativePlayerView) {
390
+ module?.emitEvent("modalPlayerDismissed")
391
+ module?.modalPlayerView = nil
392
+ module?.modalDelegate = nil
393
+ }
394
+ }
233
395
  }
@@ -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 {
@@ -148,7 +128,10 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
148
128
  self.layer.drawsAsynchronously = true
149
129
  self.isOpaque = true
150
130
 
151
- setupAppLifecycleObservers()
131
+ // Register with view registry for command dispatch (supports New Architecture)
132
+ if let tag = self.reactTag {
133
+ BBPlayerViewRegistry.shared.register(self, tag: tag.intValue)
134
+ }
152
135
 
153
136
  // Find the parent view controller from the responder chain
154
137
  var responder = self.next
@@ -170,87 +153,24 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
170
153
  } else {
171
154
  log("BBPlayerView.didMoveToWindow - view removed from window, isInFullscreen: \(isInFullscreen)")
172
155
 
173
- if !isInFullscreen {
174
- stopTimeUpdates()
156
+ // Unregister from view registry when removed from window (unless in fullscreen)
157
+ if !isInFullscreen, let tag = self.reactTag {
158
+ BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
175
159
  }
176
- }
177
- }
178
160
 
179
- // Start periodic time updates (1x per second, only if enabled)
180
- private func startTimeUpdates() {
181
- guard enableTimeUpdates, timeUpdateTimer == nil else { return }
182
-
183
- timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
184
- guard let self = self, self.isPlaying else { return }
185
-
186
- let elapsedSeconds = CACurrentMediaTime() - self.playbackStartTimestamp
187
- let estimatedTime = self.lastKnownTime + elapsedSeconds
188
- let currentTime = min(estimatedTime, self.currentDuration)
189
-
190
- if self.currentDuration > 0 && abs(currentTime - self.lastEmittedTime) >= 0.5 {
191
- self.lastEmittedTime = currentTime
192
- self.onDidTriggerTimeUpdate?([
193
- "currentTime": currentTime,
194
- "duration": self.currentDuration
195
- ])
196
- }
161
+ // No cleanup needed when not in fullscreen
197
162
  }
198
163
  }
199
164
 
200
- private func stopTimeUpdates() {
201
- timeUpdateTimer?.invalidate()
202
- timeUpdateTimer = nil
203
- }
204
-
205
165
  deinit {
206
- stopTimeUpdates()
207
- removeAppLifecycleObservers()
166
+ // Unregister from view registry
167
+ if let tag = self.reactTag {
168
+ BBPlayerViewRegistry.shared.unregister(tag: tag.intValue)
169
+ }
208
170
  independentCastButton?.removeFromSuperview()
209
171
  independentCastButton = nil
210
172
  }
211
173
 
212
- // MARK: - App Lifecycle Management
213
-
214
- private func setupAppLifecycleObservers() {
215
- guard backgroundObserver == nil else { return }
216
-
217
- backgroundObserver = NotificationCenter.default.addObserver(
218
- forName: UIApplication.didEnterBackgroundNotification,
219
- object: nil,
220
- queue: .main
221
- ) { [weak self] _ in
222
- guard let self = self else { return }
223
- if self.isPlaying {
224
- self.lastKnownTime = self.calculateCurrentTime()
225
- }
226
- self.stopTimeUpdates()
227
- log("Timer paused - app entered background", level: .debug)
228
- }
229
-
230
- foregroundObserver = NotificationCenter.default.addObserver(
231
- forName: UIApplication.willEnterForegroundNotification,
232
- object: nil,
233
- queue: .main
234
- ) { [weak self] _ in
235
- guard let self = self else { return }
236
- if self.isPlaying && self.enableTimeUpdates {
237
- self.playbackStartTimestamp = CACurrentMediaTime()
238
- self.startTimeUpdates()
239
- log("Timer resumed - app entered foreground", level: .debug)
240
- }
241
- }
242
- }
243
-
244
- private func removeAppLifecycleObservers() {
245
- if let observer = backgroundObserver {
246
- NotificationCenter.default.removeObserver(observer)
247
- backgroundObserver = nil
248
- }
249
- if let observer = foregroundObserver {
250
- NotificationCenter.default.removeObserver(observer)
251
- foregroundObserver = nil
252
- }
253
- }
254
174
 
255
175
  // MARK: - Player Setup (Simplified - no intermediate view controller)
256
176
 
@@ -395,7 +315,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
395
315
 
396
316
  func bbNativePlayerView(didTriggerEnded playerView: BBNativePlayerView) {
397
317
  isPlaying = false
398
- stopTimeUpdates()
399
318
  onDidTriggerEnded?([:])
400
319
  }
401
320
 
@@ -437,7 +356,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
437
356
 
438
357
  func bbNativePlayerView(didTriggerPause playerView: BBNativePlayerView) {
439
358
  isPlaying = false
440
- stopTimeUpdates()
441
359
  onDidTriggerPause?([:])
442
360
  }
443
361
 
@@ -451,10 +369,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
451
369
 
452
370
  func bbNativePlayerView(didTriggerPlaying playerView: BBNativePlayerView) {
453
371
  isPlaying = true
454
- playbackStartTimestamp = CACurrentMediaTime()
455
- lastEmittedTime = 0.0
456
- lastKnownTime = calculateCurrentTime()
457
- startTimeUpdates()
458
372
  onDidTriggerPlaying?([:])
459
373
  }
460
374
 
@@ -486,9 +400,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
486
400
  }
487
401
 
488
402
  func bbNativePlayerView(playerView: BBNativePlayerView, didTriggerSeeked seekOffset: Double) {
489
- lastKnownTime = seekOffset
490
- playbackStartTimestamp = CACurrentMediaTime()
491
- lastEmittedTime = 0.0
492
403
  onDidTriggerSeeked?(["payload": seekOffset as Any])
493
404
  }
494
405
 
@@ -543,18 +454,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
543
454
  addSubview(castButton)
544
455
  }
545
456
 
546
- // MARK: - Private Helper Methods
547
-
548
- private func calculateCurrentTime() -> Double {
549
- if isPlaying && playbackStartTimestamp > 0 {
550
- let elapsedSeconds = CACurrentMediaTime() - playbackStartTimestamp
551
- let estimatedTime = lastKnownTime + elapsedSeconds
552
- return min(estimatedTime, currentDuration)
553
- } else {
554
- return lastKnownTime
555
- }
556
- }
557
-
558
457
  // MARK: - Public API Methods
559
458
 
560
459
  func adMediaHeight() -> Int? {
@@ -573,10 +472,6 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
573
472
  return nil
574
473
  }
575
474
 
576
- func currentTime() -> Double? {
577
- return calculateCurrentTime()
578
- }
579
-
580
475
  func duration() -> Double? {
581
476
  return playerView?.player.duration
582
477
  }
@@ -677,9 +572,7 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
677
572
  }
678
573
 
679
574
  func seekRelative(_ offsetInSeconds: Double) {
680
- let currentTime = calculateCurrentTime()
681
- let newPosition = max(0, min(currentDuration, currentTime + offsetInSeconds))
682
- playerView?.player.seek(offsetInSeconds: newPosition as NSNumber)
575
+ playerView?.player.seekRelative(offsetInSeconds: offsetInSeconds as NSNumber)
683
576
  }
684
577
 
685
578
  func setMuted(_ muted: Bool) {
@@ -690,28 +583,144 @@ class BBPlayerView: UIView, BBNativePlayerViewDelegate {
690
583
  playerView?.setApiProperty(property: .volume, value: Float(volume))
691
584
  }
692
585
 
693
- func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
694
- playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
586
+ // Helper to parse context JSON into a dictionary for the native SDK
587
+ private func parseContext(_ contextJson: String?) -> [String: Any]? {
588
+ guard let jsonString = contextJson, !jsonString.isEmpty else { return nil }
589
+ guard let data = jsonString.data(using: .utf8) else { return nil }
590
+ do {
591
+ if let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
592
+ return dict
593
+ }
594
+ } catch {
595
+ log("Failed to parse context JSON: \(error)", level: .warning)
596
+ }
597
+ return nil
598
+ }
599
+
600
+ func loadWithClipId(_ clipId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
601
+ let context = parseContext(contextJson)
602
+ playerView?.player.loadWithClipId(clipId: clipId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
695
603
  }
696
604
 
697
- func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
698
- playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
605
+ func loadWithClipListId(_ clipListId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
606
+ let context = parseContext(contextJson)
607
+ playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
699
608
  }
700
609
 
701
- func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
702
- playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
610
+ func loadWithProjectId(_ projectId: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
611
+ let context = parseContext(contextJson)
612
+ playerView?.player.loadWithProjectId(projectId: projectId, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
703
613
  }
704
614
 
705
- func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
706
- playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
615
+ func loadWithClipJson(_ clipJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
616
+ let context = parseContext(contextJson)
617
+ playerView?.player.loadWithClipJson(clipJson: clipJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
707
618
  }
708
619
 
709
- func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
710
- playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
620
+ func loadWithClipListJson(_ clipListJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
621
+ let context = parseContext(contextJson)
622
+ playerView?.player.loadWithClipListJson(clipListJson: clipListJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
711
623
  }
712
624
 
713
- func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?) {
714
- playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?)
625
+ func loadWithProjectJson(_ projectJson: String, initiator: String?, autoPlay: Bool?, seekTo: Double?, contextJson: String? = nil) {
626
+ let context = parseContext(contextJson)
627
+ playerView?.player.loadWithProjectJson(projectJson: projectJson, initiator: initiator, autoPlay: autoPlay, seekTo: seekTo as NSNumber?, context: context)
628
+ }
629
+
630
+ /**
631
+ * Load content from a JSON URL into the existing player.
632
+ * Extracts IDs from the URL and uses the native SDK's loadWithXxxId methods.
633
+ * This is more reliable than parsing JSON because the SDK handles all the loading internally.
634
+ *
635
+ * Note: Shorts URLs (/sh/{id}.json) are NOT supported here - use BBShortsView instead.
636
+ */
637
+ func loadWithJsonUrl(_ url: String, autoPlay: Bool, contextJson: String? = nil) {
638
+ NSLog("BBPlayerView.loadWithJsonUrl called - url: %@, autoPlay: %d, context: %@", url, autoPlay, contextJson ?? "nil")
639
+ guard playerView != nil else {
640
+ NSLog("BBPlayerView.loadWithJsonUrl ERROR - playerView not initialized")
641
+ return
642
+ }
643
+
644
+ NSLog("BBPlayerView.loadWithJsonUrl - playerView exists, parsing URL")
645
+
646
+ // Extract ID from URL patterns like:
647
+ // /c/{id}.json or /mediaclip/{id}.json -> clip ID
648
+ // /l/{id}.json or /mediacliplist/{id}.json -> clip list ID
649
+ // /pj/{id}.json or /project/{id}.json -> project ID
650
+
651
+ let clipIdPattern = "/c/([0-9]+)\\.json|/mediaclip/([0-9]+)"
652
+ let clipListIdPattern = "/l/([0-9]+)\\.json|/mediacliplist/([0-9]+)"
653
+ let projectIdPattern = "/pj/([0-9]+)\\.json|/project/([0-9]+)"
654
+ let shortsIdPattern = "/sh/([0-9]+)\\.json"
655
+
656
+ let context = parseContext(contextJson)
657
+
658
+ if let shortsMatch = url.range(of: shortsIdPattern, options: .regularExpression) {
659
+ // Shorts require a separate BBShortsView component
660
+ log("ERROR - Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead.", level: .error)
661
+ onDidFailWithError?(["payload": "Shorts URLs are not supported in BBPlayerView. Use BBShortsView instead."])
662
+ return
663
+ }
664
+
665
+ if let match = url.range(of: clipListIdPattern, options: .regularExpression) {
666
+ // Extract the cliplist ID
667
+ if let clipListId = extractIdFromUrl(url, pattern: clipListIdPattern) {
668
+ NSLog("BBPlayerView.loadWithJsonUrl - Loading ClipList by ID: %@", clipListId)
669
+ playerView?.player.loadWithClipListId(clipListId: clipListId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
670
+ } else {
671
+ NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract cliplist ID from URL: %@", url)
672
+ }
673
+ return
674
+ }
675
+
676
+ if let match = url.range(of: projectIdPattern, options: .regularExpression) {
677
+ // Extract the project ID
678
+ if let projectId = extractIdFromUrl(url, pattern: projectIdPattern) {
679
+ NSLog("BBPlayerView.loadWithJsonUrl - Loading Project by ID: %@", projectId)
680
+ playerView?.player.loadWithProjectId(projectId: projectId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
681
+ } else {
682
+ NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract project ID from URL: %@", url)
683
+ }
684
+ return
685
+ }
686
+
687
+ if let match = url.range(of: clipIdPattern, options: .regularExpression) {
688
+ // Extract the clip ID
689
+ if let clipId = extractIdFromUrl(url, pattern: clipIdPattern) {
690
+ NSLog("BBPlayerView.loadWithJsonUrl - Loading Clip by ID: %@", clipId)
691
+ playerView?.player.loadWithClipId(clipId: clipId, initiator: "external", autoPlay: autoPlay, seekTo: nil, context: context)
692
+ } else {
693
+ NSLog("BBPlayerView.loadWithJsonUrl ERROR - Failed to extract clip ID from URL: %@", url)
694
+ }
695
+ return
696
+ }
697
+
698
+ NSLog("BBPlayerView.loadWithJsonUrl ERROR - Unknown URL format, cannot extract ID: %@", url)
699
+ onDidFailWithError?(["payload": "Cannot load content: unsupported URL format"])
700
+ }
701
+
702
+ /**
703
+ * Helper to extract numeric ID from URL using regex pattern with capture groups
704
+ */
705
+ private func extractIdFromUrl(_ url: String, pattern: String) -> String? {
706
+ do {
707
+ let regex = try NSRegularExpression(pattern: pattern, options: [])
708
+ let range = NSRange(url.startIndex..., in: url)
709
+ if let match = regex.firstMatch(in: url, options: [], range: range) {
710
+ // Try each capture group (pattern has multiple alternatives with |)
711
+ for i in 1..<match.numberOfRanges {
712
+ if let groupRange = Range(match.range(at: i), in: url) {
713
+ let extracted = String(url[groupRange])
714
+ if !extracted.isEmpty {
715
+ return extracted
716
+ }
717
+ }
718
+ }
719
+ }
720
+ } catch {
721
+ log("ERROR - Regex error: \(error)", level: .error)
722
+ }
723
+ return nil
715
724
  }
716
725
 
717
726
  func showCastPicker() {