@bluebillywig/react-native-bb-player 8.42.8 → 8.42.9

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.
@@ -4,9 +4,12 @@ import android.app.Activity
4
4
  import android.os.Handler
5
5
  import android.os.Looper
6
6
  import android.util.Log
7
+ import android.view.Choreographer
7
8
  import android.view.MotionEvent
8
9
  import android.view.View
10
+ import android.view.ViewGroup
9
11
  import android.widget.FrameLayout
12
+ import com.facebook.react.modules.core.ReactChoreographer
10
13
  import androidx.collection.ArrayMap
11
14
  import androidx.mediarouter.app.MediaRouteButton
12
15
  import com.bluebillywig.bbnativeplayersdk.BBNativePlayer
@@ -42,18 +45,11 @@ private inline fun debugLog(tag: String, message: () -> String) {
42
45
  *
43
46
  * To ensure the native player controls work correctly, this view:
44
47
  *
45
- * 1. **Overrides requestLayout()** - Ensures layout requests propagate correctly through
46
- * React Native's view hierarchy by posting to the choreographer.
47
- *
48
- * 2. **Overrides onMeasure()** - Uses native Android measurement (MeasureSpec.EXACTLY)
49
- * instead of letting Yoga determine the size, ensuring the player and its controls
50
- * receive proper dimensions.
51
- *
52
- * 3. **Overrides onLayout()** - Explicitly layouts child views to fill the container,
48
+ * 1. **Overrides onLayout()** - Explicitly layouts child views to fill the container,
53
49
  * which is necessary because React Native's Yoga layout doesn't automatically
54
50
  * propagate layout to native child views.
55
51
  *
56
- * 4. **Overrides onInterceptTouchEvent()** - Returns false to ensure touch events
52
+ * 2. **Overrides onInterceptTouchEvent()** - Returns false to ensure touch events
57
53
  * always reach the child BBNativePlayerView, allowing the player's controlbar
58
54
  * to respond to taps.
59
55
  *
@@ -64,9 +60,6 @@ private inline fun debugLog(tag: String, message: () -> String) {
64
60
  * ExoPlayer StyledPlayerView which has its own gesture detectors and controlbar that
65
61
  * need to receive touch events directly. Without these overrides, React Native's
66
62
  * touch handling system intercepts events before they reach the native player controls.
67
- *
68
- * This approach is similar to how Expo's ExpoView handles native views with
69
- * `shouldUseAndroidLayout = true`.
70
63
  */
71
64
  class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext),
72
65
  BBNativePlayerViewDelegate {
@@ -85,88 +78,79 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
85
78
  private var wasLandscapeFullscreen = false
86
79
  private var savedOrientation: Int? = null
87
80
 
88
- init {
89
- // Default to black background (playout data may override with bgColor)
90
- setBackgroundColor(android.graphics.Color.BLACK)
91
- }
92
-
93
81
  // ==================================================================================
94
82
  // NATIVE LAYOUT INTEGRATION
95
- // These overrides ensure the native player view and its controls work correctly
96
- // within React Native's Yoga-based layout system.
83
+ // ViewGroupManager.needsCustomLayoutForChildren() = true tells React Native
84
+ // that this view handles its own child layout, allowing ExoPlayer's controlbar
85
+ // to work correctly.
86
+ //
87
+ // This uses the ReactChoreographer pattern from react-native-screens:
88
+ // https://github.com/software-mansion/react-native-screens
89
+ // See also: https://github.com/facebook/react-native/issues/17968
97
90
  // ==================================================================================
98
91
 
99
- /**
100
- * Override requestLayout to ensure layout requests are properly handled.
101
- *
102
- * React Native's layout system uses Yoga which processes layout asynchronously.
103
- * Native Android views expect requestLayout() to trigger a synchronous layout pass.
104
- * This override posts a layout request to ensure proper propagation through the
105
- * view hierarchy while being frame-aligned via Choreographer for performance.
106
- *
107
- * Without this, the native player view may not update its layout when needed,
108
- * causing issues with control positioning and visibility.
109
- */
110
- override fun requestLayout() {
111
- super.requestLayout()
92
+ private var isLayoutEnqueued = false
93
+ // Flag to prevent requestLayout during super constructor
94
+ private var constructorComplete = false
112
95
 
113
- // Post to ensure layout happens on the next frame, avoiding layout-during-layout issues
114
- // This is a common pattern for native views embedded in React Native
115
- post {
116
- measure(
117
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
118
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
119
- )
120
- layout(left, top, right, bottom)
121
- }
96
+ private val layoutCallback = Choreographer.FrameCallback {
97
+ isLayoutEnqueued = false
98
+ measure(
99
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
100
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
101
+ )
102
+ layout(left, top, right, bottom)
103
+ }
104
+
105
+ init {
106
+ // Default to black background (playout data may override with bgColor)
107
+ setBackgroundColor(android.graphics.Color.BLACK)
108
+ // Remove any padding
109
+ setPadding(0, 0, 0, 0)
110
+ // Allow children to render slightly outside bounds (helps with margin artifacts)
111
+ clipToPadding = false
112
+ clipChildren = false
113
+ // Mark constructor complete - enables requestLayout Choreographer callback
114
+ constructorComplete = true
122
115
  }
123
116
 
124
117
  /**
125
- * Override onMeasure to use native Android measurement.
118
+ * Override requestLayout to ensure layout propagates to children using ReactChoreographer.
126
119
  *
127
- * By default, React Native's Yoga layout may pass UNSPECIFIED or AT_MOST specs,
128
- * which can confuse native views expecting EXACTLY specs. This ensures the player
129
- * view receives precise dimensions matching the container size set by React Native.
120
+ * This is the proper React Native pattern for native views that need layout updates.
121
+ * Using NATIVE_ANIMATED_MODULE queue catches the current looper loop instead of
122
+ * enqueueing the update in the next loop, preventing one-frame delays.
130
123
  *
131
- * Performance note: This is called during the measure pass and is optimized to
132
- * avoid unnecessary work by only measuring children when we have valid dimensions.
124
+ * Combined with ViewGroupManager.needsCustomLayoutForChildren() = true, this ensures
125
+ * the native player view and controlbar layout correctly without margin artifacts.
133
126
  */
134
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
135
- // Get the exact dimensions from React Native's layout
136
- val width = MeasureSpec.getSize(widthMeasureSpec)
137
- val height = MeasureSpec.getSize(heightMeasureSpec)
138
-
139
- // Set our measured dimensions
140
- setMeasuredDimension(width, height)
141
-
142
- // Measure all children with EXACTLY specs to ensure they fill the container
143
- // This is critical for the player view to receive proper dimensions
144
- if (width > 0 && height > 0) {
145
- val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
146
- val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
147
-
148
- for (i in 0 until childCount) {
149
- getChildAt(i)?.measure(childWidthSpec, childHeightSpec)
150
- }
127
+ override fun requestLayout() {
128
+ super.requestLayout()
129
+ // Guard against calls during super constructor (before properties are initialized)
130
+ if (!constructorComplete) return
131
+ if (!isLayoutEnqueued) {
132
+ isLayoutEnqueued = true
133
+ ReactChoreographer.getInstance()
134
+ .postFrameCallback(
135
+ ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE,
136
+ layoutCallback
137
+ )
151
138
  }
152
139
  }
153
140
 
154
141
  /**
155
- * Override onLayout to explicitly position child views.
142
+ * Override onLayout to explicitly position child views to fill the container.
156
143
  *
157
144
  * React Native's Yoga layout calculates positions but doesn't automatically apply
158
- * them to native child views. This explicitly layouts all children to fill the
159
- * container, which is necessary for the BBNativePlayerView to render correctly.
145
+ * them to native child views.
160
146
  */
161
147
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
162
148
  super.onLayout(changed, left, top, right, bottom)
163
-
164
- val width = right - left
165
- val height = bottom - top
166
-
167
149
  // Layout all children to fill the entire container
150
+ val w = right - left
151
+ val h = bottom - top
168
152
  for (i in 0 until childCount) {
169
- getChildAt(i)?.layout(0, 0, width, height)
153
+ getChildAt(i)?.layout(0, 0, w, h)
170
154
  }
171
155
  }
172
156
 
@@ -286,13 +270,99 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
286
270
  debugLog("BBPlayerView") { "Creating BBNativePlayerView with factory method" }
287
271
  playerView = BBNativePlayer.createPlayerView(currentActivity, jsonUrl, options)
288
272
  playerView.delegate = this@BBPlayerView
273
+ // Remove any padding from playerView
274
+ playerView.setPadding(0, 0, 0, 0)
289
275
 
290
276
  addView(playerView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
291
277
 
278
+ // Set ExoPlayer's resize mode to FILL to prevent letterboxing margins
279
+ // This addresses top/bottom margin issues caused by AspectRatioFrameLayout
280
+ postDelayed({
281
+ setExoPlayerResizeMode(playerView, RESIZE_MODE_FILL)
282
+ }, 1000)
283
+
292
284
  playerSetup = true
293
285
  debugLog("BBPlayerView") { "Player setup complete with URL: $jsonUrl" }
294
286
  }
295
287
 
288
+ companion object {
289
+ // AspectRatioFrameLayout resize mode constants
290
+ private const val RESIZE_MODE_FILL = 3
291
+ }
292
+
293
+ /**
294
+ * Recursively find ExoPlayer's PlayerView or AspectRatioFrameLayout and set resize mode.
295
+ * Uses reflection since media3 classes aren't directly available in RN module classpath.
296
+ * This fixes top/bottom margin issues caused by AspectRatioFrameLayout letterboxing.
297
+ */
298
+ private fun setExoPlayerResizeMode(view: View, resizeMode: Int): Boolean {
299
+ // Try Media3 AspectRatioFrameLayout via reflection
300
+ try {
301
+ val aspectRatioClass = Class.forName("androidx.media3.ui.AspectRatioFrameLayout")
302
+ if (aspectRatioClass.isInstance(view)) {
303
+ val method = aspectRatioClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
304
+ method.invoke(view, resizeMode)
305
+ return true
306
+ }
307
+ } catch (_: ClassNotFoundException) {
308
+ // Media3 not available
309
+ } catch (_: Exception) {
310
+ // Failed to set resize mode
311
+ }
312
+
313
+ // Try Media3 PlayerView via reflection
314
+ try {
315
+ val playerViewClass = Class.forName("androidx.media3.ui.PlayerView")
316
+ if (playerViewClass.isInstance(view)) {
317
+ val method = playerViewClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
318
+ method.invoke(view, resizeMode)
319
+ // Continue searching - there might be an AspectRatioFrameLayout inside
320
+ }
321
+ } catch (_: ClassNotFoundException) {
322
+ // Media3 PlayerView not available
323
+ } catch (_: Exception) {
324
+ // Failed to set resize mode
325
+ }
326
+
327
+ // Try legacy ExoPlayer2 AspectRatioFrameLayout via reflection
328
+ try {
329
+ val legacyAspectRatioClass = Class.forName("com.google.android.exoplayer2.ui.AspectRatioFrameLayout")
330
+ if (legacyAspectRatioClass.isInstance(view)) {
331
+ val method = legacyAspectRatioClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
332
+ method.invoke(view, resizeMode)
333
+ return true
334
+ }
335
+ } catch (_: ClassNotFoundException) {
336
+ // ExoPlayer2 not available
337
+ } catch (_: Exception) {
338
+ // Failed to set resize mode
339
+ }
340
+
341
+ // Try legacy ExoPlayer2 StyledPlayerView via reflection
342
+ try {
343
+ val styledPlayerViewClass = Class.forName("com.google.android.exoplayer2.ui.StyledPlayerView")
344
+ if (styledPlayerViewClass.isInstance(view)) {
345
+ val method = styledPlayerViewClass.getMethod("setResizeMode", Int::class.javaPrimitiveType)
346
+ method.invoke(view, resizeMode)
347
+ }
348
+ } catch (_: ClassNotFoundException) {
349
+ // StyledPlayerView not available
350
+ } catch (_: Exception) {
351
+ // Failed to set resize mode
352
+ }
353
+
354
+ // Recursively search children
355
+ if (view is ViewGroup) {
356
+ for (i in 0 until view.childCount) {
357
+ if (setExoPlayerResizeMode(view.getChildAt(i), resizeMode)) {
358
+ return true
359
+ }
360
+ }
361
+ }
362
+
363
+ return false
364
+ }
365
+
296
366
  /**
297
367
  * Load content from a JSON URL into the existing player.
298
368
  * Extracts IDs from the URL and uses the native SDK's loadWithXxxId methods.
@@ -2,11 +2,29 @@ package com.bluebillywig.bbplayer
2
2
 
3
3
  import com.facebook.react.bridge.ReadableArray
4
4
  import com.facebook.react.bridge.ReadableMap
5
- import com.facebook.react.uimanager.SimpleViewManager
6
5
  import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewGroupManager
7
7
  import com.facebook.react.uimanager.annotations.ReactProp
8
8
 
9
- class BBPlayerViewManager : SimpleViewManager<BBPlayerView>() {
9
+ /**
10
+ * ViewGroupManager for BBPlayerView.
11
+ *
12
+ * Uses ViewGroupManager instead of SimpleViewManager because BBPlayerView contains
13
+ * child views (BBNativePlayerView with ExoPlayer). The needsCustomLayoutForChildren()
14
+ * override tells React Native that this view handles its own child layout, which is
15
+ * necessary for ExoPlayer's controlbar to work correctly.
16
+ *
17
+ * See: https://github.com/facebook/react-native/issues/17968
18
+ * See: https://github.com/reactwg/react-native-new-architecture/discussions/52
19
+ */
20
+ class BBPlayerViewManager : ViewGroupManager<BBPlayerView>() {
21
+
22
+ /**
23
+ * Tell React Native that this view handles its own child layout.
24
+ * This prevents Yoga from interfering with native child view layout,
25
+ * which is necessary for ExoPlayer's controlbar to work.
26
+ */
27
+ override fun needsCustomLayoutForChildren(): Boolean = true
10
28
 
11
29
  override fun getName(): String = REACT_CLASS
12
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluebillywig/react-native-bb-player",
3
- "version": "8.42.8",
3
+ "version": "8.42.9",
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",