Skip to content

Commit 4ebc9b7

Browse files
feat(android): add surface type prop (#4719)
1 parent a97581a commit 4ebc9b7

23 files changed

+355
-15
lines changed

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs/video-view.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default App;
5353
| `autoEnterPictureInPicture` | `boolean` | No | `false` | Whether the video should automatically enter PiP mode when it starts playing and the app is backgrounded (behavior might vary by platform). |
5454
| `resizeMode` | `'contain' \| 'cover' \| 'stretch' \| 'none'` | No | `'none'` | How the video should be resized to fit the view. |
5555
| `keepScreenAwake` | `boolean` | No | `true` | Whether to keep the device screen awake while the video view is mounted. |
56+
| `surfaceType` | `'surface' \| 'texture'` | No (Android only) | `'surface'` | (Android) Underlying native view type. `'surface'` uses a SurfaceView (better performance, no transforms/overlap), `'texture'` uses a TextureView (supports animations, transforms, overlapping UI) at a small performance cost. Ignored on iOS. |
5657

5758
## Events
5859

@@ -104,4 +105,26 @@ Available methods on the `VideoViewRef`:
104105
| `exitFullscreen()` | `() => void` | Programmatically requests the video view to exit fullscreen mode. |
105106
| `enterPictureInPicture()` | `() => void` | Programmatically requests the video view to enter picture-in-picture mode. |
106107
| `exitPictureInPicture()` | `() => void` | Programmatically requests the video view to exit picture-in-picture mode. |
107-
| `canEnterPictureInPicture()` | `() => boolean` | Checks if picture-in-picture mode is currently available and supported. Returns `true` if PiP can be entered, `false` otherwise. |
108+
| `canEnterPictureInPicture()` | `() => boolean` | Checks if picture-in-picture mode is currently available and supported. Returns `true` if PiP can be entered, `false` otherwise. |
109+
110+
## Android: Choosing a surface type
111+
112+
On Android the default rendering path uses a `SurfaceView` (set via `surfaceType="surface"`) for optimal decoding performance and lower latency. However `SurfaceView` lives in a separate window and can't be:
113+
114+
- Animated with transforms (scale, rotate, opacity fade)
115+
- Clipped by parent views (rounded corners, masks)
116+
- Overlapped reliably with sibling views (z-order issues)
117+
118+
If you need those UI effects, switch to `TextureView`:
119+
120+
```tsx
121+
<VideoView
122+
player={player}
123+
surfaceType="texture"
124+
style={{ width: 300, height: 170, borderRadius: 16, overflow: 'hidden' }}
125+
resizeMode="cover"
126+
controls
127+
/>
128+
```
129+
130+
Use `TextureView` only when required, as it can be slightly less performant and may increase power consumption on some devices.

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,7 +1565,7 @@ PODS:
15651565
- React-logger (= 0.77.2)
15661566
- React-perflogger (= 0.77.2)
15671567
- React-utils (= 0.77.2)
1568-
- ReactNativeVideo (7.0.0-alpha.4):
1568+
- ReactNativeVideo (7.0.0-alpha.5):
15691569
- DoubleConversion
15701570
- glog
15711571
- hermes-engine
@@ -1904,7 +1904,7 @@ SPEC CHECKSUMS:
19041904
ReactAppDependencyProvider: f334cebc0beed0a72490492e978007082c03d533
19051905
ReactCodegen: 474fbb3e4bb0f1ee6c255d1955db76e13d509269
19061906
ReactCommon: 7763e59534d58e15f8f22121cdfe319040e08888
1907-
ReactNativeVideo: f365bc4f1a57ab50ddb655cda2f47bc06698a53b
1907+
ReactNativeVideo: 705a2a90d9f04afff9afd90d4ef194e1bc1135d5
19081908
ReactNativeVideoDrm: 62840ae0e184f711a2e6495c18e342a74cb598f8
19091909
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
19101910
Yoga: 31a098f74c16780569aebd614a0f37a907de0189

packages/react-native-video/android/src/main/java/com/twg/video/hybrids/videoviewviewmanager/HybridVideoViewViewManager.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ class HybridVideoViewViewManager(nitroId: Int): HybridVideoViewViewManagerSpec()
7878
videoView.get()?.keepScreenAwake = value
7979
}
8080

81+
override var surfaceType: SurfaceType
82+
get() = videoView.get()?.surfaceType ?: SurfaceType.SURFACE
83+
set(value) {
84+
videoView.get()?.surfaceType = value
85+
}
86+
8187
// View callbacks
8288
override var onPictureInPictureChange: ((Boolean) -> Unit)? = null
8389
set(value) {

packages/react-native-video/android/src/main/java/com/twg/video/view/VideoView.kt

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.os.Build
88
import android.util.AttributeSet
99
import android.util.DisplayMetrics
1010
import android.util.Log
11+
import android.view.LayoutInflater
1112
import android.view.View
1213
import android.view.ViewGroup
1314
import android.view.WindowManager
@@ -22,6 +23,7 @@ import com.facebook.react.bridge.ReactApplicationContext
2223
import com.margelo.nitro.NitroModules
2324
import com.margelo.nitro.video.HybridVideoPlayer
2425
import com.margelo.nitro.video.ResizeMode
26+
import com.margelo.nitro.video.SurfaceType
2527
import com.margelo.nitro.video.VideoViewEvents
2628
import com.twg.video.core.LibraryError
2729
import com.twg.video.core.VideoManager
@@ -36,6 +38,8 @@ import com.twg.video.core.extensions.toAspectRatioFrameLayout
3638
import com.twg.video.core.utils.PictureInPictureUtils
3739
import com.twg.video.core.utils.PictureInPictureUtils.createDisabledPictureInPictureParams
3840
import com.twg.video.core.utils.SmallVideoPlayerOptimizer
41+
import com.twg.video.R.layout.player_view_surface
42+
import com.twg.video.R.layout.player_view_texture
3943

4044
@UnstableApi
4145
class VideoView @JvmOverloads constructor(
@@ -90,6 +94,20 @@ class VideoView @JvmOverloads constructor(
9094

9195
var pictureInPictureEnabled: Boolean = false
9296

97+
var surfaceType: SurfaceType = SurfaceType.SURFACE
98+
set(value) {
99+
if (field == value) return
100+
field = value
101+
102+
runOnMainThread {
103+
removeView(playerView)
104+
playerView.player = null
105+
playerView = createPlayerView()
106+
playerView.player = hybridPlayer?.player
107+
addView(playerView)
108+
}
109+
}
110+
93111
var resizeMode: ResizeMode = ResizeMode.NONE
94112
set(value) {
95113
field = value
@@ -101,7 +119,9 @@ class VideoView @JvmOverloads constructor(
101119
var keepScreenAwake: Boolean
102120
get() = playerView.keepScreenOn
103121
set(value) {
104-
playerView.keepScreenOn = value
122+
runOnMainThread {
123+
playerView.keepScreenOn = value
124+
}
105125
}
106126

107127
var events = object : VideoViewEvents {
@@ -114,15 +134,7 @@ class VideoView @JvmOverloads constructor(
114134
}
115135

116136
var onNitroIdChange: ((Int?) -> Unit)? = null
117-
var playerView = PlayerView(context).apply {
118-
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
119-
setShutterBackgroundColor(Color.TRANSPARENT)
120-
setShowSubtitleButton(true)
121-
useController = false
122-
123-
// Apply optimizations based on video player size if needed
124-
configureForSmallPlayer()
125-
}
137+
var playerView = createPlayerView()
126138
var isInFullscreen: Boolean = false
127139
set(value) {
128140
field = value
@@ -153,6 +165,22 @@ class VideoView @JvmOverloads constructor(
153165
playerView.resizeMode = resizeMode.toAspectRatioFrameLayout()
154166
}
155167

168+
@SuppressLint("InflateParams")
169+
private fun createPlayerView(): PlayerView {
170+
return when (surfaceType) {
171+
SurfaceType.SURFACE -> LayoutInflater.from(context).inflate(player_view_surface, null) as PlayerView
172+
SurfaceType.TEXTURE -> LayoutInflater.from(context).inflate(player_view_texture, null) as PlayerView
173+
}.apply {
174+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
175+
setShutterBackgroundColor(Color.TRANSPARENT)
176+
setShowSubtitleButton(true)
177+
useController = false
178+
179+
// Apply optimizations based on video player size if needed
180+
configureForSmallPlayer()
181+
}
182+
}
183+
156184
private val layoutRunnable = Runnable {
157185
measure(
158186
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<androidx.media3.ui.PlayerView
2+
android:id="@+id/player_view_surface"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent"
5+
xmlns:android="http://schemas.android.com/apk/res/android"
6+
xmlns:app="http://schemas.android.com/apk/res-auto"
7+
app:surface_type="surface_view" />
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<androidx.media3.ui.PlayerView
2+
android:id="@+id/player_view_texture"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent"
5+
xmlns:android="http://schemas.android.com/apk/res/android"
6+
xmlns:app="http://schemas.android.com/apk/res-auto"
7+
app:surface_type="texture_view" />

packages/react-native-video/ios/hybrids/VideoViewViewManager/HybridVideoViewViewManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ class HybridVideoViewViewManager: HybridVideoViewViewManagerSpec {
138138
}
139139
}
140140

141+
// Android only - no-op on iOS
142+
var surfaceType: SurfaceType = .surface
143+
141144
func enterFullscreen() throws {
142145
guard let view else {
143146
throw VideoViewError.viewIsDeallocated.error()

packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.cpp

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-native-video/nitrogen/generated/android/c++/JHybridVideoViewViewManagerSpec.hpp

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)