@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
|
|
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
|
-
*
|
|
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
|
-
//
|
|
96
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
118
|
+
* Override requestLayout to ensure layout propagates to children using ReactChoreographer.
|
|
126
119
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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",
|