@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
|
-
*
|
|
35
|
-
*
|
|
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
|
|
61
|
-
*
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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",
|