@bluebillywig/react-native-bb-player 8.42.7 → 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,7 +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
8
+ import android.view.MotionEvent
9
+ import android.view.View
10
+ import android.view.ViewGroup
7
11
  import android.widget.FrameLayout
12
+ import com.facebook.react.modules.core.ReactChoreographer
8
13
  import androidx.collection.ArrayMap
9
14
  import androidx.mediarouter.app.MediaRouteButton
10
15
  import com.bluebillywig.bbnativeplayersdk.BBNativePlayer
@@ -31,8 +36,30 @@ private inline fun debugLog(tag: String, message: () -> String) {
31
36
  /**
32
37
  * React Native View for Blue Billywig Native Player
33
38
  *
34
- * Uses FrameLayout as base for proper native Android layout handling.
35
- * Events are sent via React Native's RCTEventEmitter.
39
+ * This view wraps the native BBNativePlayerView and handles the integration with
40
+ * React Native's view system. The key challenge is that React Native uses Yoga for
41
+ * layout, which can interfere with native Android views that have their own layout
42
+ * and touch handling requirements (like ExoPlayer's StyledPlayerView with controlbar).
43
+ *
44
+ * ## Native Layout Integration
45
+ *
46
+ * To ensure the native player controls work correctly, this view:
47
+ *
48
+ * 1. **Overrides onLayout()** - Explicitly layouts child views to fill the container,
49
+ * which is necessary because React Native's Yoga layout doesn't automatically
50
+ * propagate layout to native child views.
51
+ *
52
+ * 2. **Overrides onInterceptTouchEvent()** - Returns false to ensure touch events
53
+ * always reach the child BBNativePlayerView, allowing the player's controlbar
54
+ * to respond to taps.
55
+ *
56
+ * ## Why This Is Necessary
57
+ *
58
+ * React Native's Yoga layout system is designed for flexbox-based UI, not for native
59
+ * views with complex internal view hierarchies. The BBNativePlayerView contains an
60
+ * ExoPlayer StyledPlayerView which has its own gesture detectors and controlbar that
61
+ * need to receive touch events directly. Without these overrides, React Native's
62
+ * touch handling system intercepts events before they reach the native player controls.
36
63
  */
37
64
  class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext),
38
65
  BBNativePlayerViewDelegate {
@@ -51,23 +78,104 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
51
78
  private var wasLandscapeFullscreen = false
52
79
  private var savedOrientation: Int? = null
53
80
 
81
+ // ==================================================================================
82
+ // NATIVE LAYOUT INTEGRATION
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
90
+ // ==================================================================================
91
+
92
+ private var isLayoutEnqueued = false
93
+ // Flag to prevent requestLayout during super constructor
94
+ private var constructorComplete = false
95
+
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
+
54
105
  init {
55
106
  // Default to black background (playout data may override with bgColor)
56
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
115
+ }
116
+
117
+ /**
118
+ * Override requestLayout to ensure layout propagates to children using ReactChoreographer.
119
+ *
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.
123
+ *
124
+ * Combined with ViewGroupManager.needsCustomLayoutForChildren() = true, this ensures
125
+ * the native player view and controlbar layout correctly without margin artifacts.
126
+ */
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
+ )
138
+ }
57
139
  }
58
140
 
59
141
  /**
60
- * Override onLayout to ensure child views receive proper bounds.
61
- * This is critical for React Native Fabric where layout isn't automatically propagated.
142
+ * Override onLayout to explicitly position child views to fill the container.
143
+ *
144
+ * React Native's Yoga layout calculates positions but doesn't automatically apply
145
+ * them to native child views.
62
146
  */
63
147
  override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
64
148
  super.onLayout(changed, left, top, right, bottom)
65
- // Explicitly layout all children to fill the container
149
+ // Layout all children to fill the entire container
150
+ val w = right - left
151
+ val h = bottom - top
66
152
  for (i in 0 until childCount) {
67
- getChildAt(i)?.layout(0, 0, right - left, bottom - top)
153
+ getChildAt(i)?.layout(0, 0, w, h)
68
154
  }
69
155
  }
70
156
 
157
+ /**
158
+ * Never intercept touch events - let them pass through to child views.
159
+ *
160
+ * This is CRITICAL for the player controlbar to work. React Native's gesture
161
+ * handling system can intercept touch events before they reach native views.
162
+ * By always returning false, we ensure:
163
+ *
164
+ * 1. Single taps reach the PlayerView to toggle the controlbar
165
+ * 2. Double taps reach the PlayerView for seek functionality
166
+ * 3. Swipes and other gestures work for any interactive elements
167
+ *
168
+ * The BBNativePlayerView handles its own touch events internally through
169
+ * ExoPlayer's StyledPlayerView and custom gesture detectors.
170
+ */
171
+ override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
172
+ return false
173
+ }
174
+
175
+ // ==================================================================================
176
+ // END NATIVE LAYOUT INTEGRATION
177
+ // ==================================================================================
178
+
71
179
  // Timer for periodic time updates (opt-in for performance)
72
180
  private val timeUpdateHandler = Handler(Looper.getMainLooper())
73
181
  private var timeUpdateRunnable: Runnable? = null
@@ -162,13 +270,99 @@ class BBPlayerView(private val reactContext: ThemedReactContext) : FrameLayout(r
162
270
  debugLog("BBPlayerView") { "Creating BBNativePlayerView with factory method" }
163
271
  playerView = BBNativePlayer.createPlayerView(currentActivity, jsonUrl, options)
164
272
  playerView.delegate = this@BBPlayerView
273
+ // Remove any padding from playerView
274
+ playerView.setPadding(0, 0, 0, 0)
165
275
 
166
276
  addView(playerView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
167
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
+
168
284
  playerSetup = true
169
285
  debugLog("BBPlayerView") { "Player setup complete with URL: $jsonUrl" }
170
286
  }
171
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
+
172
366
  /**
173
367
  * Load content from a JSON URL into the existing player.
174
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.7",
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",