@bluebillywig/react-native-bb-player 8.42.10 → 8.42.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to react-native-bb-player will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [8.42.10] - 2026-01-28
9
+
10
+ ### Added
11
+ - **TurboModule support** for React Native New Architecture (Fabric)
12
+ - Added `NativeBBPlayerModule` TurboModule spec for React Native codegen
13
+ - Native module now uses `TurboModuleRegistry` when available, with automatic fallback to legacy `NativeModules`
14
+ - Added architecture-specific source sets (`newarch`/`paper`) for Android
15
+ - iOS module converted to Objective-C++ (`.mm`) for C++ interop required by TurboModules
16
+
17
+ ### Changed
18
+ - `BBPlayerPackage.kt` now implements `TurboReactPackage` for New Architecture compatibility
19
+ - `BBPlayerModule.kt` extends generated `NativeBBPlayerModuleSpec` for type-safe TurboModule implementation
20
+ - Updated `react-native-bb-player.podspec` with New Architecture compiler flags and configuration
21
+ - Added `codegenConfig` to `package.json` for React Native codegen integration
22
+
23
+ ### Technical Details
24
+ - Supports both Old Architecture (Paper) and New Architecture (Fabric/TurboModules)
25
+ - Tested with React Native 0.82.1
26
+ - No breaking changes - existing apps continue to work without modification
27
+ - New Architecture is automatically detected and used when enabled in the app
28
+
8
29
  ## [2.0.0] - 2026-01-20
9
30
 
10
31
  ### Changed
@@ -44,5 +65,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
44
65
  - Android fullscreen landscape: proper orientation locking and state restoration
45
66
  - Performance optimizations: reduced bridge traffic and memory leak fixes
46
67
 
68
+ [8.42.10]: https://github.com/bluebillywig/react-native-bb-player/releases/tag/v8.42.10
47
69
  [2.0.0]: https://github.com/bluebillywig/react-native-bb-player/releases/tag/v2.0.0
48
70
  [1.0.0]: https://github.com/bluebillywig/react-native-bb-player/releases/tag/v1.0.0
package/README.md CHANGED
@@ -18,7 +18,7 @@ Native video player for React Native - powered by Blue Billywig's iOS and Androi
18
18
  |----------|-------------|---------------|
19
19
  | **iOS** | 12.0+ | AVPlayer |
20
20
  | **Android** | API 21+ (5.0+) | ExoPlayer |
21
- | **React Native** | 0.73+ | Old & New Architecture |
21
+ | **React Native** | 0.73+ | Old & New Architecture (TurboModules) |
22
22
  | **Expo** | SDK 51+ | With config plugin (optional) |
23
23
 
24
24
  ## Installation
@@ -673,6 +673,63 @@ function CustomScreen() {
673
673
  }
674
674
  ```
675
675
 
676
+ ## New Architecture (Fabric & TurboModules)
677
+
678
+ This package fully supports React Native's New Architecture, including:
679
+
680
+ - **Fabric** - The new rendering system
681
+ - **TurboModules** - The new native module system with synchronous access and lazy loading
682
+
683
+ ### Automatic Detection
684
+
685
+ The package automatically detects which architecture your app uses:
686
+ - **New Architecture enabled**: Uses `TurboModuleRegistry` for optimal performance
687
+ - **Old Architecture**: Falls back to `NativeModules` (no changes needed)
688
+
689
+ ### Enabling New Architecture
690
+
691
+ #### React Native 0.76+
692
+ New Architecture is enabled by default in React Native 0.76 and later.
693
+
694
+ #### React Native 0.73-0.75
695
+ Enable in your app's configuration:
696
+
697
+ **Android** (`android/gradle.properties`):
698
+ ```properties
699
+ newArchEnabled=true
700
+ ```
701
+
702
+ **iOS** (`ios/Podfile`):
703
+ ```ruby
704
+ ENV['RCT_NEW_ARCH_ENABLED'] = '1'
705
+ ```
706
+
707
+ Then rebuild your app:
708
+ ```bash
709
+ # iOS
710
+ cd ios && pod install && cd ..
711
+ npx react-native run-ios
712
+
713
+ # Android
714
+ cd android && ./gradlew clean && cd ..
715
+ npx react-native run-android
716
+ ```
717
+
718
+ ### No Code Changes Required
719
+
720
+ Your existing code works with both architectures. The package handles the architecture detection internally:
721
+
722
+ ```tsx
723
+ // This works on both Old and New Architecture
724
+ import { BBPlayerView } from '@bluebillywig/react-native-bb-player';
725
+
726
+ <BBPlayerView
727
+ ref={playerRef}
728
+ jsonUrl="https://demo.bbvms.com/p/default/c/4701337.json"
729
+ onDidTriggerPlay={() => console.log('Playing')}
730
+ />
731
+ ```
732
+
676
733
  ## FAQ
677
734
 
678
735
  ### Can I use this in production?
@@ -11,6 +11,11 @@ def isNewArchitectureEnabled() {
11
11
  return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
12
12
  }
13
13
 
14
+ // Apply React Native plugin for codegen when new arch is enabled
15
+ if (isNewArchitectureEnabled()) {
16
+ apply plugin: "com.facebook.react"
17
+ }
18
+
14
19
  group = 'com.bluebillywig.bbplayer'
15
20
  version = '2.0.0'
16
21
 
@@ -49,7 +54,8 @@ android {
49
54
  main {
50
55
  java.srcDirs = ['src/main/java']
51
56
  if (isNewArchitectureEnabled()) {
52
- java.srcDirs += ['src/newarch/java']
57
+ // Include codegen-generated specs first (takes precedence)
58
+ java.srcDirs += ["build/generated/source/codegen/java"]
53
59
  } else {
54
60
  java.srcDirs += ['src/paper/java']
55
61
  }
@@ -61,85 +61,85 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
61
61
  }
62
62
 
63
63
  @ReactMethod
64
- override fun play(viewTag: Int) {
65
- runOnUiThread(viewTag) { it.play() }
64
+ override fun play(viewTag: Double) {
65
+ runOnUiThread(viewTag.toInt()) { it.play() }
66
66
  }
67
67
 
68
68
  @ReactMethod
69
- override fun pause(viewTag: Int) {
70
- runOnUiThread(viewTag) { it.pause() }
69
+ override fun pause(viewTag: Double) {
70
+ runOnUiThread(viewTag.toInt()) { it.pause() }
71
71
  }
72
72
 
73
73
  @ReactMethod
74
- override fun seek(viewTag: Int, position: Double) {
75
- runOnUiThread(viewTag) { it.seek(position) }
74
+ override fun seek(viewTag: Double, position: Double) {
75
+ runOnUiThread(viewTag.toInt()) { it.seek(position) }
76
76
  }
77
77
 
78
78
  @ReactMethod
79
- override fun seekRelative(viewTag: Int, offsetSeconds: Double) {
80
- runOnUiThread(viewTag) { it.seekRelative(offsetSeconds) }
79
+ override fun seekRelative(viewTag: Double, offsetSeconds: Double) {
80
+ runOnUiThread(viewTag.toInt()) { it.seekRelative(offsetSeconds) }
81
81
  }
82
82
 
83
83
  @ReactMethod
84
- override fun setVolume(viewTag: Int, volume: Double) {
85
- runOnUiThread(viewTag) { it.setVolume(volume) }
84
+ override fun setVolume(viewTag: Double, volume: Double) {
85
+ runOnUiThread(viewTag.toInt()) { it.setVolume(volume) }
86
86
  }
87
87
 
88
88
  @ReactMethod
89
- override fun setMuted(viewTag: Int, muted: Boolean) {
90
- runOnUiThread(viewTag) { it.setMuted(muted) }
89
+ override fun setMuted(viewTag: Double, muted: Boolean) {
90
+ runOnUiThread(viewTag.toInt()) { it.setMuted(muted) }
91
91
  }
92
92
 
93
93
  @ReactMethod
94
- override fun enterFullscreen(viewTag: Int) {
95
- runOnUiThread(viewTag) { it.enterFullscreen() }
94
+ override fun enterFullscreen(viewTag: Double) {
95
+ runOnUiThread(viewTag.toInt()) { it.enterFullscreen() }
96
96
  }
97
97
 
98
98
  @ReactMethod
99
- override fun enterFullscreenLandscape(viewTag: Int) {
100
- runOnUiThread(viewTag) { it.enterFullscreenLandscape() }
99
+ override fun enterFullscreenLandscape(viewTag: Double) {
100
+ runOnUiThread(viewTag.toInt()) { it.enterFullscreenLandscape() }
101
101
  }
102
102
 
103
103
  @ReactMethod
104
- override fun exitFullscreen(viewTag: Int) {
105
- runOnUiThread(viewTag) { it.exitFullscreen() }
104
+ override fun exitFullscreen(viewTag: Double) {
105
+ runOnUiThread(viewTag.toInt()) { it.exitFullscreen() }
106
106
  }
107
107
 
108
108
  @ReactMethod
109
- override fun collapse(viewTag: Int) {
110
- runOnUiThread(viewTag) { it.collapse() }
109
+ override fun collapse(viewTag: Double) {
110
+ runOnUiThread(viewTag.toInt()) { it.collapse() }
111
111
  }
112
112
 
113
113
  @ReactMethod
114
- override fun expand(viewTag: Int) {
115
- runOnUiThread(viewTag) { it.expand() }
114
+ override fun expand(viewTag: Double) {
115
+ runOnUiThread(viewTag.toInt()) { it.expand() }
116
116
  }
117
117
 
118
118
  @ReactMethod
119
- override fun autoPlayNextCancel(viewTag: Int) {
120
- runOnUiThread(viewTag) { it.autoPlayNextCancel() }
119
+ override fun autoPlayNextCancel(viewTag: Double) {
120
+ runOnUiThread(viewTag.toInt()) { it.autoPlayNextCancel() }
121
121
  }
122
122
 
123
123
  @ReactMethod
124
- override fun destroy(viewTag: Int) {
125
- runOnUiThread(viewTag) { it.destroy() }
124
+ override fun destroy(viewTag: Double) {
125
+ runOnUiThread(viewTag.toInt()) { it.destroy() }
126
126
  }
127
127
 
128
128
  @ReactMethod
129
- override fun showCastPicker(viewTag: Int) {
130
- runOnUiThread(viewTag) { it.showCastPicker() }
129
+ override fun showCastPicker(viewTag: Double) {
130
+ runOnUiThread(viewTag.toInt()) { it.showCastPicker() }
131
131
  }
132
132
 
133
133
  @ReactMethod
134
134
  override fun loadWithClipId(
135
- viewTag: Int,
135
+ viewTag: Double,
136
136
  clipId: String,
137
137
  initiator: String?,
138
138
  autoPlay: Boolean,
139
139
  seekTo: Double
140
140
  ) {
141
141
  Log.d("BBPlayerModule", "loadWithClipId called - viewTag: $viewTag, clipId: $clipId, autoPlay: $autoPlay")
142
- runOnUiThread(viewTag) {
142
+ runOnUiThread(viewTag.toInt()) {
143
143
  it.loadWithClipId(
144
144
  clipId,
145
145
  initiator,
@@ -151,13 +151,13 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
151
151
 
152
152
  @ReactMethod
153
153
  override fun loadWithClipListId(
154
- viewTag: Int,
154
+ viewTag: Double,
155
155
  clipListId: String,
156
156
  initiator: String?,
157
157
  autoPlay: Boolean,
158
158
  seekTo: Double
159
159
  ) {
160
- runOnUiThread(viewTag) {
160
+ runOnUiThread(viewTag.toInt()) {
161
161
  it.loadWithClipListId(
162
162
  clipListId,
163
163
  initiator,
@@ -169,13 +169,13 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
169
169
 
170
170
  @ReactMethod
171
171
  override fun loadWithProjectId(
172
- viewTag: Int,
172
+ viewTag: Double,
173
173
  projectId: String,
174
174
  initiator: String?,
175
175
  autoPlay: Boolean,
176
176
  seekTo: Double
177
177
  ) {
178
- runOnUiThread(viewTag) {
178
+ runOnUiThread(viewTag.toInt()) {
179
179
  it.loadWithProjectId(
180
180
  projectId,
181
181
  initiator,
@@ -187,13 +187,13 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
187
187
 
188
188
  @ReactMethod
189
189
  override fun loadWithClipJson(
190
- viewTag: Int,
190
+ viewTag: Double,
191
191
  clipJson: String,
192
192
  initiator: String?,
193
193
  autoPlay: Boolean,
194
194
  seekTo: Double
195
195
  ) {
196
- runOnUiThread(viewTag) {
196
+ runOnUiThread(viewTag.toInt()) {
197
197
  it.loadWithClipJson(
198
198
  clipJson,
199
199
  initiator,
@@ -205,13 +205,13 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
205
205
 
206
206
  @ReactMethod
207
207
  override fun loadWithClipListJson(
208
- viewTag: Int,
208
+ viewTag: Double,
209
209
  clipListJson: String,
210
210
  initiator: String?,
211
211
  autoPlay: Boolean,
212
212
  seekTo: Double
213
213
  ) {
214
- runOnUiThread(viewTag) {
214
+ runOnUiThread(viewTag.toInt()) {
215
215
  it.loadWithClipListJson(
216
216
  clipListJson,
217
217
  initiator,
@@ -223,13 +223,13 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
223
223
 
224
224
  @ReactMethod
225
225
  override fun loadWithProjectJson(
226
- viewTag: Int,
226
+ viewTag: Double,
227
227
  projectJson: String,
228
228
  initiator: String?,
229
229
  autoPlay: Boolean,
230
230
  seekTo: Double
231
231
  ) {
232
- runOnUiThread(viewTag) {
232
+ runOnUiThread(viewTag.toInt()) {
233
233
  it.loadWithProjectJson(
234
234
  projectJson,
235
235
  initiator,
@@ -241,11 +241,11 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
241
241
 
242
242
  @ReactMethod
243
243
  override fun loadWithJsonUrl(
244
- viewTag: Int,
244
+ viewTag: Double,
245
245
  jsonUrl: String,
246
246
  autoPlay: Boolean
247
247
  ) {
248
- runOnUiThread(viewTag) {
248
+ runOnUiThread(viewTag.toInt()) {
249
249
  it.loadWithJsonUrl(jsonUrl, autoPlay)
250
250
  }
251
251
  }
@@ -255,9 +255,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
255
255
 
256
256
  // Getter methods with Promise support
257
257
  @ReactMethod
258
- override fun getDuration(viewTag: Int, promise: Promise) {
258
+ override fun getDuration(viewTag: Double, promise: Promise) {
259
259
  UiThreadUtil.runOnUiThread {
260
- val view = findPlayerView(viewTag)
260
+ val view = findPlayerView(viewTag.toInt())
261
261
  if (view != null) {
262
262
  val duration = view.getDuration()
263
263
  promise.resolve(duration)
@@ -268,9 +268,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
268
268
  }
269
269
 
270
270
  @ReactMethod
271
- override fun getCurrentTime(viewTag: Int, promise: Promise) {
271
+ override fun getCurrentTime(viewTag: Double, promise: Promise) {
272
272
  UiThreadUtil.runOnUiThread {
273
- val view = findPlayerView(viewTag)
273
+ val view = findPlayerView(viewTag.toInt())
274
274
  if (view != null) {
275
275
  val currentTime = view.getCurrentTime()
276
276
  promise.resolve(currentTime)
@@ -281,9 +281,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
281
281
  }
282
282
 
283
283
  @ReactMethod
284
- override fun getMuted(viewTag: Int, promise: Promise) {
284
+ override fun getMuted(viewTag: Double, promise: Promise) {
285
285
  UiThreadUtil.runOnUiThread {
286
- val view = findPlayerView(viewTag)
286
+ val view = findPlayerView(viewTag.toInt())
287
287
  if (view != null) {
288
288
  val muted = view.getMuted()
289
289
  promise.resolve(muted)
@@ -294,9 +294,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
294
294
  }
295
295
 
296
296
  @ReactMethod
297
- override fun getVolume(viewTag: Int, promise: Promise) {
297
+ override fun getVolume(viewTag: Double, promise: Promise) {
298
298
  UiThreadUtil.runOnUiThread {
299
- val view = findPlayerView(viewTag)
299
+ val view = findPlayerView(viewTag.toInt())
300
300
  if (view != null) {
301
301
  val volume = view.getVolume()
302
302
  promise.resolve(volume)
@@ -307,9 +307,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
307
307
  }
308
308
 
309
309
  @ReactMethod
310
- override fun getPhase(viewTag: Int, promise: Promise) {
310
+ override fun getPhase(viewTag: Double, promise: Promise) {
311
311
  UiThreadUtil.runOnUiThread {
312
- val view = findPlayerView(viewTag)
312
+ val view = findPlayerView(viewTag.toInt())
313
313
  if (view != null) {
314
314
  val phase = view.getPhase()
315
315
  promise.resolve(phase?.name)
@@ -320,9 +320,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
320
320
  }
321
321
 
322
322
  @ReactMethod
323
- override fun getState(viewTag: Int, promise: Promise) {
323
+ override fun getState(viewTag: Double, promise: Promise) {
324
324
  UiThreadUtil.runOnUiThread {
325
- val view = findPlayerView(viewTag)
325
+ val view = findPlayerView(viewTag.toInt())
326
326
  if (view != null) {
327
327
  val state = view.getState()
328
328
  promise.resolve(state?.name)
@@ -333,9 +333,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
333
333
  }
334
334
 
335
335
  @ReactMethod
336
- override fun getMode(viewTag: Int, promise: Promise) {
336
+ override fun getMode(viewTag: Double, promise: Promise) {
337
337
  UiThreadUtil.runOnUiThread {
338
- val view = findPlayerView(viewTag)
338
+ val view = findPlayerView(viewTag.toInt())
339
339
  if (view != null) {
340
340
  val mode = view.getMode()
341
341
  promise.resolve(mode)
@@ -346,9 +346,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
346
346
  }
347
347
 
348
348
  @ReactMethod
349
- override fun getClipData(viewTag: Int, promise: Promise) {
349
+ override fun getClipData(viewTag: Double, promise: Promise) {
350
350
  UiThreadUtil.runOnUiThread {
351
- val view = findPlayerView(viewTag)
351
+ val view = findPlayerView(viewTag.toInt())
352
352
  if (view != null) {
353
353
  val clipData = view.getClipData()
354
354
  if (clipData != null) {
@@ -369,9 +369,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
369
369
  }
370
370
 
371
371
  @ReactMethod
372
- override fun getProjectData(viewTag: Int, promise: Promise) {
372
+ override fun getProjectData(viewTag: Double, promise: Promise) {
373
373
  UiThreadUtil.runOnUiThread {
374
- val view = findPlayerView(viewTag)
374
+ val view = findPlayerView(viewTag.toInt())
375
375
  if (view != null) {
376
376
  val projectData = view.getProjectData()
377
377
  if (projectData != null) {
@@ -390,9 +390,9 @@ class BBPlayerModule(private val reactContext: ReactApplicationContext) :
390
390
  }
391
391
 
392
392
  @ReactMethod
393
- override fun getPlayoutData(viewTag: Int, promise: Promise) {
393
+ override fun getPlayoutData(viewTag: Double, promise: Promise) {
394
394
  UiThreadUtil.runOnUiThread {
395
- val view = findPlayerView(viewTag)
395
+ val view = findPlayerView(viewTag.toInt())
396
396
  if (view != null) {
397
397
  val playoutData = view.getPlayoutData()
398
398
  if (playoutData != null) {
@@ -42,9 +42,11 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
42
42
  private var isPlaying: Bool = false
43
43
  private var currentDuration: Double = 0.0
44
44
  private var lastKnownTime: Double = 0.0
45
- private var playbackStartTimestamp: TimeInterval = 0
45
+ private var playbackStartTimestamp: CFTimeInterval = 0 // Use CFTimeInterval for CACurrentMediaTime
46
46
  private var lastEmittedTime: Double = 0.0
47
47
  private var isInFullscreen: Bool = false
48
+ private var backgroundObserver: NSObjectProtocol?
49
+ private var foregroundObserver: NSObjectProtocol?
48
50
  // Independent Google Cast button for showing the cast picker
49
51
  private var independentCastButton: GCKUICastButton?
50
52
 
@@ -144,6 +146,9 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
144
146
  // Ensure this view respects its frame from React Native layout
145
147
  self.clipsToBounds = false // Allow settings overlay to render outside bounds
146
148
 
149
+ // Set up lifecycle observers to pause timer when app goes to background (saves battery)
150
+ setupAppLifecycleObservers()
151
+
147
152
  // Find the parent view controller from the responder chain
148
153
  var parentVC: UIViewController?
149
154
  var responder = self.next
@@ -211,8 +216,8 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
211
216
  timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
212
217
  guard let self = self, self.isPlaying else { return }
213
218
 
214
- // Calculate current time based on elapsed time since playback started
215
- let elapsedSeconds = Date().timeIntervalSince1970 - self.playbackStartTimestamp
219
+ // Use CACurrentMediaTime() instead of Date() - much more efficient (no system call overhead)
220
+ let elapsedSeconds = CACurrentMediaTime() - self.playbackStartTimestamp
216
221
  let estimatedTime = self.lastKnownTime + elapsedSeconds
217
222
  let currentTime = min(estimatedTime, self.currentDuration)
218
223
 
@@ -234,13 +239,62 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
234
239
  timeUpdateTimer = nil
235
240
  }
236
241
 
237
- // Clean up timers and views to prevent memory leaks
242
+ // Clean up timers, observers, and views to prevent memory leaks
238
243
  deinit {
239
244
  stopTimeUpdates()
245
+ removeAppLifecycleObservers()
240
246
  independentCastButton?.removeFromSuperview()
241
247
  independentCastButton = nil
242
248
  }
243
249
 
250
+ // MARK: - App Lifecycle Management (Battery Optimization)
251
+
252
+ private func setupAppLifecycleObservers() {
253
+ // Only set up once
254
+ guard backgroundObserver == nil else { return }
255
+
256
+ // Pause timer when app goes to background to save battery
257
+ backgroundObserver = NotificationCenter.default.addObserver(
258
+ forName: UIApplication.didEnterBackgroundNotification,
259
+ object: nil,
260
+ queue: .main
261
+ ) { [weak self] _ in
262
+ guard let self = self else { return }
263
+ // Save current time before pausing timer so we can resume accurately
264
+ if self.isPlaying {
265
+ self.lastKnownTime = self.calculateCurrentTime()
266
+ }
267
+ self.stopTimeUpdates()
268
+ log("Timer paused - app entered background", level: .debug)
269
+ }
270
+
271
+ // Resume timer when app comes back to foreground
272
+ foregroundObserver = NotificationCenter.default.addObserver(
273
+ forName: UIApplication.willEnterForegroundNotification,
274
+ object: nil,
275
+ queue: .main
276
+ ) { [weak self] _ in
277
+ guard let self = self else { return }
278
+ if self.isPlaying && self.enableTimeUpdates {
279
+ // Reset timestamp to now for accurate time calculation after background
280
+ self.playbackStartTimestamp = CACurrentMediaTime()
281
+ self.startTimeUpdates()
282
+ log("Timer resumed - app entered foreground", level: .debug)
283
+ }
284
+ }
285
+ }
286
+
287
+ private func removeAppLifecycleObservers() {
288
+ if let observer = backgroundObserver {
289
+ NotificationCenter.default.removeObserver(observer)
290
+ backgroundObserver = nil
291
+ }
292
+ if let observer = foregroundObserver {
293
+ NotificationCenter.default.removeObserver(observer)
294
+ foregroundObserver = nil
295
+ }
296
+ }
297
+
244
298
  func bbPlayerViewController(
245
299
  _ controller: BBPlayerViewController, didTriggerEvent event: BBPlayerEvent
246
300
  ) {
@@ -341,7 +395,7 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
341
395
 
342
396
  case .playing:
343
397
  isPlaying = true
344
- playbackStartTimestamp = Date().timeIntervalSince1970
398
+ playbackStartTimestamp = CACurrentMediaTime() // More efficient than Date()
345
399
  lastEmittedTime = 0.0 // Reset to ensure immediate time update on play
346
400
  lastKnownTime = calculateCurrentTime() // Update to ensure accuracy between events
347
401
  startTimeUpdates()
@@ -352,21 +406,14 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
352
406
 
353
407
  case .retractFullscreen:
354
408
  isInFullscreen = false
355
-
356
- // Force rotation back to portrait when exiting fullscreen
357
- if #available(iOS 16.0, *) {
358
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
359
- windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
360
- log("Requested portrait rotation on retractFullscreen event", level: .info)
361
- }
362
- }
363
-
409
+ // Note: Orientation reset is handled by BBPlayerViewController.bbNativePlayerView(didTriggerRetractFullscreen:)
410
+ // to avoid duplicate calls which cause unnecessary CPU/GPU work
364
411
  onDidTriggerRetractFullscreen?([:])
365
412
 
366
413
  case .seeked(let seekOffset):
367
414
  // Update lastKnownTime based on seek and reset playback timestamp
368
415
  lastKnownTime = seekOffset ?? 0.0
369
- playbackStartTimestamp = Date().timeIntervalSince1970
416
+ playbackStartTimestamp = CACurrentMediaTime() // More efficient than Date()
370
417
  lastEmittedTime = 0.0 // Reset to ensure immediate time update after seek
371
418
  onDidTriggerSeeked?(["payload": seekOffset as Any])
372
419
 
@@ -482,7 +529,8 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
482
529
  /// iOS SDK doesn't expose direct currentTime property, so we estimate it
483
530
  private func calculateCurrentTime() -> Double {
484
531
  if isPlaying && playbackStartTimestamp > 0 {
485
- let elapsedSeconds = Date().timeIntervalSince1970 - playbackStartTimestamp
532
+ // Use CACurrentMediaTime() - more efficient than Date() (no system call overhead)
533
+ let elapsedSeconds = CACurrentMediaTime() - playbackStartTimestamp
486
534
  let estimatedTime = lastKnownTime + elapsedSeconds
487
535
  return min(estimatedTime, currentDuration)
488
536
  } else {
@@ -596,15 +644,9 @@ class BBPlayerView: UIView, BBPlayerViewControllerDelegate {
596
644
  }
597
645
 
598
646
  func exitFullscreen() {
599
- // Force rotation back to portrait before exiting fullscreen
600
- if #available(iOS 16.0, *) {
601
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
602
- windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
603
- log("Requested portrait rotation before exitFullscreen", level: .info)
604
- }
605
- }
606
-
607
647
  // iOS SDK Note: The iOS SDK uses exitFullScreen() method
648
+ // Orientation reset is handled by BBPlayerViewController.bbNativePlayerView(didTriggerRetractFullscreen:)
649
+ // to avoid duplicate orientation update calls which cause unnecessary CPU/GPU work
608
650
  playerController.playerView?.player.exitFullScreen()
609
651
  }
610
652
 
@@ -30,37 +30,8 @@ class BBPlayerViewController: UIViewController, BBNativePlayerViewDelegate {
30
30
  /// Don't override preferredInterfaceOrientationForPresentation
31
31
  /// Let the fullscreen modal (AVPlayerViewController) use its own preferred orientation
32
32
 
33
- func refreshPlayerViewHierarchy() {
34
- guard let playerView = playerView else {
35
- print("BBPlayer: refreshPlayerViewHierarchy - no playerView")
36
- return
37
- }
38
-
39
- print("BBPlayer: Starting view hierarchy refresh")
40
-
41
- // Force all sublayers to redraw by traversing the entire layer hierarchy
42
- func refreshLayerTree(_ layer: CALayer) {
43
- layer.setNeedsDisplay()
44
- layer.displayIfNeeded()
45
- for sublayer in layer.sublayers ?? [] {
46
- refreshLayerTree(sublayer)
47
- }
48
- }
49
-
50
- // Refresh the entire layer tree of the player view
51
- refreshLayerTree(playerView.layer)
52
-
53
- // Force the player view and all its subviews to re-layout
54
- playerView.setNeedsLayout()
55
- playerView.layoutIfNeeded()
56
-
57
- for subview in playerView.subviews {
58
- subview.setNeedsLayout()
59
- subview.layoutIfNeeded()
60
- }
61
-
62
- print("BBPlayer: View hierarchy refresh complete")
63
- }
33
+ // NOTE: Removed refreshPlayerViewHierarchy() - it was unused dead code that forced
34
+ // GPU redraw on every layer in the hierarchy, which would cause severe heat/battery drain
64
35
 
65
36
  override func viewDidLayoutSubviews() {
66
37
  super.viewDidLayoutSubviews()
@@ -308,23 +279,29 @@ class BBPlayerViewController: UIViewController, BBNativePlayerViewDelegate {
308
279
  NSLog("BBPlayer: Refreshing view hierarchy after fullscreen exit")
309
280
 
310
281
  // Reset any transforms that might be lingering from fullscreen
311
- // Only reset view transforms, avoid touching layers to prevent visual glitches
312
- func resetTransforms(_ view: UIView) {
313
- view.transform = .identity
314
-
315
- // Recursively reset subviews
316
- view.subviews.forEach { subview in
317
- resetTransforms(subview)
282
+ // Use iterative approach instead of recursion to avoid deep stack calls
283
+ // Only reset views that actually have non-identity transforms to save CPU
284
+ var viewsToProcess: [UIView] = [playerView]
285
+ var transformsReset = 0
286
+
287
+ while !viewsToProcess.isEmpty {
288
+ let view = viewsToProcess.removeLast()
289
+ // Only reset if transform is not already identity (avoids unnecessary work)
290
+ if view.transform != .identity {
291
+ view.transform = .identity
292
+ transformsReset += 1
318
293
  }
294
+ viewsToProcess.append(contentsOf: view.subviews)
319
295
  }
320
296
 
321
- resetTransforms(playerView)
297
+ if transformsReset > 0 {
298
+ NSLog("BBPlayer: Reset \(transformsReset) non-identity transforms")
299
+ }
322
300
 
323
- // Force layout update
301
+ // Single batched layout update instead of multiple calls
324
302
  playerView.setNeedsLayout()
325
- playerView.layoutIfNeeded()
326
303
  self.view.setNeedsLayout()
327
- self.view.layoutIfNeeded()
304
+ self.view.layoutIfNeeded() // This will also layout playerView as it's a subview
328
305
 
329
306
  // Log the player state for debugging
330
307
  let player = playerView.player
@@ -5,5 +5,6 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = void 0;
7
7
  var _reactNative = require("react-native");
8
- var _default = exports.default = _reactNative.TurboModuleRegistry.getEnforcing("BBPlayerModule");
8
+ // Use get() instead of getEnforcing() to avoid crash when module not registered
9
+ var _default = exports.default = _reactNative.TurboModuleRegistry.get("BBPlayerModule");
9
10
  //# sourceMappingURL=NativeBBPlayerModule.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../../src","sources":["specs/NativeBBPlayerModule.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAAmD,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAmFpCC,gCAAmB,CAACC,YAAY,CAAO,gBAAgB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_reactNative","require","_default","exports","default","TurboModuleRegistry","get"],"sourceRoot":"../../../src","sources":["specs/NativeBBPlayerModule.ts"],"mappings":";;;;;;AACA,IAAAA,YAAA,GAAAC,OAAA;AAmFA;AAAA,IAAAC,QAAA,GAAAC,OAAA,CAAAC,OAAA,GACeC,gCAAmB,CAACC,GAAG,CAAO,gBAAgB,CAAC","ignoreList":[]}
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  import { TurboModuleRegistry } from "react-native";
4
- export default TurboModuleRegistry.getEnforcing("BBPlayerModule");
4
+ // Use get() instead of getEnforcing() to avoid crash when module not registered
5
+ export default TurboModuleRegistry.get("BBPlayerModule");
5
6
  //# sourceMappingURL=NativeBBPlayerModule.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../../src","sources":["specs/NativeBBPlayerModule.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAmFlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,gBAAgB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","get"],"sourceRoot":"../../../src","sources":["specs/NativeBBPlayerModule.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAmFlD;AACA,eAAeA,mBAAmB,CAACC,GAAG,CAAO,gBAAgB,CAAC","ignoreList":[]}
@@ -32,6 +32,6 @@ export interface Spec extends TurboModule {
32
32
  getProjectData(viewTag: number): Promise<Object | null>;
33
33
  getPlayoutData(viewTag: number): Promise<Object | null>;
34
34
  }
35
- declare const _default: Spec;
35
+ declare const _default: Spec | null;
36
36
  export default _default;
37
37
  //# sourceMappingURL=NativeBBPlayerModule.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NativeBBPlayerModule.d.ts","sourceRoot":"","sources":["../../../../src/specs/NativeBBPlayerModule.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"NativeBBPlayerModule.d.ts","sourceRoot":"","sources":["../../../../src/specs/NativeBBPlayerModule.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluebillywig/react-native-bb-player",
3
- "version": "8.42.10",
3
+ "version": "8.42.14",
4
4
  "description": "Blue Billywig Native Video Player for React Native - iOS AVPlayer and Android ExoPlayer integration",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -79,10 +79,10 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@types/react": "~19.1.0",
82
+ "@types/react": "~19.2.0",
83
83
  "expo": "~51.0.0",
84
- "react": "19.1.1",
85
- "react-native": "0.82.1",
84
+ "react": "19.2.0",
85
+ "react-native": "0.83.1",
86
86
  "react-native-builder-bob": "^0.31.0",
87
87
  "typedoc": "^0.28.16",
88
88
  "typedoc-plugin-markdown": "^4.9.0",
@@ -82,4 +82,5 @@ export interface Spec extends TurboModule {
82
82
  getPlayoutData(viewTag: number): Promise<Object | null>;
83
83
  }
84
84
 
85
- export default TurboModuleRegistry.getEnforcing<Spec>("BBPlayerModule");
85
+ // Use get() instead of getEnforcing() to avoid crash when module not registered
86
+ export default TurboModuleRegistry.get<Spec>("BBPlayerModule");