NuvioStreaming/patches/react-native-video+6.19.0.patch
2026-03-13 07:10:40 +05:30

842 lines
38 KiB
Diff

diff --git a/node_modules/react-native-video/android/.classpath b/node_modules/react-native-video/android/.classpath
new file mode 100644
index 0000000..bbe97e5
--- /dev/null
+++ b/node_modules/react-native-video/android/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
+ <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
+ <classpathentry kind="output" path="bin/default"/>
+</classpath>
diff --git a/node_modules/react-native-video/android/.project b/node_modules/react-native-video/android/.project
new file mode 100644
index 0000000..2633130
--- /dev/null
+++ b/node_modules/react-native-video/android/.project
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>react-native-video</name>
+ <comment>Project react-native-video created by Buildship.</comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
+ </natures>
+ <filteredResources>
+ <filter>
+ <id>1772755755997</id>
+ <name></name>
+ <type>30</type>
+ <matcher>
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
+ <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+ </matcher>
+ </filter>
+ </filteredResources>
+</projectDescription>
diff --git a/node_modules/react-native-video/android/.settings/org.eclipse.buildship.core.prefs b/node_modules/react-native-video/android/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..1675490
--- /dev/null
+++ b/node_modules/react-native-video/android/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,2 @@
+connection.project.dir=../../../android
+eclipse.preferences.version=1
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
index 1ac0fd0..953eb59 100644
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
@@ -1,5 +1,6 @@
package com.brentvatne.common.api
+import android.graphics.Color
import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap
@@ -22,6 +23,17 @@ class SubtitleStyle public constructor() {
var subtitlesFollowVideo = true
private set
+ // Extended styling (used by ExoPlayerView via Media3 SubtitleView)
+ // Stored as Android color ints to avoid parsing multiple times.
+ var textColor: Int? = null
+ private set
+ var backgroundColor: Int? = null
+ private set
+ var edgeType: String? = null
+ private set
+ var edgeColor: Int? = null
+ private set
+
companion object {
private const val PROP_FONT_SIZE_TRACK = "fontSize"
private const val PROP_PADDING_BOTTOM = "paddingBottom"
@@ -31,6 +43,21 @@ class SubtitleStyle public constructor() {
private const val PROP_OPACITY = "opacity"
private const val PROP_SUBTITLES_FOLLOW_VIDEO = "subtitlesFollowVideo"
+ // Extended props (optional)
+ private const val PROP_TEXT_COLOR = "textColor"
+ private const val PROP_BACKGROUND_COLOR = "backgroundColor"
+ private const val PROP_EDGE_TYPE = "edgeType"
+ private const val PROP_EDGE_COLOR = "edgeColor"
+
+ private fun parseColorOrNull(value: String?): Int? {
+ if (value.isNullOrBlank()) return null
+ return try {
+ Color.parseColor(value)
+ } catch (_: IllegalArgumentException) {
+ null
+ }
+ }
+
@JvmStatic
fun parse(src: ReadableMap?): SubtitleStyle {
val subtitleStyle = SubtitleStyle()
@@ -41,6 +68,13 @@ class SubtitleStyle public constructor() {
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0)
subtitleStyle.opacity = ReactBridgeUtils.safeGetFloat(src, PROP_OPACITY, 1f)
subtitleStyle.subtitlesFollowVideo = ReactBridgeUtils.safeGetBool(src, PROP_SUBTITLES_FOLLOW_VIDEO, true)
+
+ // Extended styling
+ subtitleStyle.textColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_TEXT_COLOR, null))
+ subtitleStyle.backgroundColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_BACKGROUND_COLOR, null))
+ subtitleStyle.edgeType = ReactBridgeUtils.safeGetString(src, PROP_EDGE_TYPE, null)
+ subtitleStyle.edgeColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_EDGE_COLOR, null))
+
return subtitleStyle
}
}
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt
index 96a7887..6e5cf08 100644
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt
@@ -67,8 +67,21 @@ object DataSourceUtil {
.setTransferListener(bandwidthMeter)
if (requestHeaders != null) {
- okHttpDataSourceFactory.setDefaultRequestProperties(requestHeaders)
- if (!requestHeaders.containsKey("User-Agent")) {
+ // IMPORTANT:
+ // If `Accept-Encoding` is explicitly set (e.g. to "gzip"), OkHttp will not
+ // transparently decompress the response body. This can cause ExoPlayer to
+ // receive gzipped playlist bytes and fail HLS parsing with:
+ // "Input does not start with the #EXTM3U header".
+ // Remove any user-supplied `Accept-Encoding` so OkHttp can manage it.
+ val sanitizedHeaders = HashMap<String, String>(requestHeaders.size)
+ for ((k, v) in requestHeaders) {
+ if (!k.equals("Accept-Encoding", ignoreCase = true)) {
+ sanitizedHeaders[k] = v
+ }
+ }
+
+ okHttpDataSourceFactory.setDefaultRequestProperties(sanitizedHeaders)
+ if (!sanitizedHeaders.containsKey("User-Agent")) {
okHttpDataSourceFactory.setUserAgent(getUserAgent(context))
}
} else {
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
index bb945fe..5a6b554 100644
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
@@ -1,29 +1,43 @@
package com.brentvatne.exoplayer
+import android.os.Build
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.SurfaceView
import android.view.View
import android.view.View.MeasureSpec
import android.widget.FrameLayout
import android.widget.TextView
import androidx.media3.common.Player
import androidx.media3.common.Timeline
+import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.AspectRatioFrameLayout
+import androidx.media3.ui.CaptionStyleCompat
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.PlayerView
+import androidx.media3.ui.SubtitleView
import com.brentvatne.common.api.ResizeMode
import com.brentvatne.common.api.SubtitleStyle
+import com.brentvatne.common.api.ViewType
+import com.brentvatne.react.R
@UnstableApi
class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
private var localStyle = SubtitleStyle()
+ private var currentViewType = ViewType.VIEW_TYPE_SURFACE
private var pendingResizeMode: Int? = null
+ private var player: ExoPlayer? = null
+ private var showSubtitleButton = false
+ private var shutterColor = Color.TRANSPARENT
+ private var controllerVisibilityListener: PlayerView.ControllerVisibilityListener? = null
+ private var fullscreenButtonClickListener: PlayerView.FullscreenButtonClickListener? = null
private val liveBadge: TextView = TextView(context).apply {
text = "LIVE"
setTextColor(Color.WHITE)
@@ -36,20 +50,39 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
visibility = View.GONE
}
- private val playerView = PlayerView(context).apply {
+ private var playerView = createPlayerView(currentViewType)
+
+ /**
+ * Subtitles rendered in a full-size overlay (NOT inside PlayerView's content frame).
+ * This keeps subtitles anchored in-place even when the video surface/content frame moves
+ * due to aspect ratio / resizeMode changes.
+ *
+ * Controlled by SubtitleStyle.subtitlesFollowVideo.
+ */
+ private val overlaySubtitleView = SubtitleView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
- setShutterBackgroundColor(Color.TRANSPARENT)
- useController = true
- controllerAutoShow = true
- controllerHideOnTouch = true
- controllerShowTimeoutMs = 5000
- // Don't show subtitle button by default - will be enabled when tracks are available
- setShowSubtitleButton(false)
- // Enable proper surface view handling to prevent rendering issues
- setUseArtwork(false)
- setDefaultArtwork(null)
- // Ensure proper video scaling - start with FIT mode
- resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
+ visibility = View.GONE
+ // We control styling via SubtitleStyle; don't pull Android system caption defaults.
+ setApplyEmbeddedStyles(true)
+ setApplyEmbeddedFontSizes(true)
+ }
+
+ private fun updateSubtitleRenderingMode() {
+ val internalSubtitleView = playerView.subtitleView
+ val followVideo = localStyle.subtitlesFollowVideo
+ val shouldShow = localStyle.opacity != 0.0f
+
+ if (followVideo) {
+ internalSubtitleView?.visibility = if (shouldShow) View.VISIBLE else View.GONE
+ overlaySubtitleView.visibility = View.GONE
+ } else {
+ // Hard-disable PlayerView's internal subtitle view. PlayerView can recreate/toggle this view
+ // during resize/layout, so we re-assert this in multiple lifecycle points.
+ internalSubtitleView?.visibility = View.GONE
+ internalSubtitleView?.alpha = 0f
+ overlaySubtitleView.visibility = if (shouldShow) View.VISIBLE else View.GONE
+ overlaySubtitleView.alpha = 1f
+ }
}
init {
@@ -57,74 +90,193 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
addView(playerView, playerViewLayoutParams)
+ // Add overlay subtitles above PlayerView (so it doesn't move with video content frame)
+ val subtitleOverlayLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
+ addView(overlaySubtitleView, subtitleOverlayLayoutParams)
+
// Add live badge with its own layout parameters
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
addView(liveBadge, liveBadgeLayoutParams)
+
+ // PlayerView may internally recreate its subtitle view during relayouts (e.g. resizeMode changes).
+ // Ensure our rendering mode is re-applied whenever PlayerView lays out.
+ playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ updateSubtitleRenderingMode()
+ }
}
fun setPlayer(player: ExoPlayer?) {
- val currentPlayer = playerView.player
+ this.player?.removeListener(playerListener)
+ this.player = player
+ playerView.player = player
+ player?.addListener(playerListener)
+ }
- if (currentPlayer != null) {
- currentPlayer.removeListener(playerListener)
+ fun setResizeMode(@ResizeMode.Mode mode: Int) {
+ val resizeMode = when (mode) {
+ ResizeMode.RESIZE_MODE_FIT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
+ ResizeMode.RESIZE_MODE_FIXED_WIDTH -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
+ ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
+ ResizeMode.RESIZE_MODE_FILL -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL
+ ResizeMode.RESIZE_MODE_CENTER_CROP -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM
+ else -> androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
+ }
+ if (playerView.width > 0 && playerView.height > 0) {
+ playerView.resizeMode = resizeMode
+ } else {
+ pendingResizeMode = resizeMode
}
- playerView.player = player
+ // Re-assert subtitle rendering mode for the current style.
+ updateSubtitleRenderingMode()
+ applySubtitleStyle(localStyle)
+ }
- if (player != null) {
- player.addListener(playerListener)
+ fun getPlayerView(): PlayerView = playerView
- // Apply pending resize mode if we have one
- pendingResizeMode?.let { resizeMode ->
- playerView.resizeMode = resizeMode
- }
- }
+ fun isPlaying(): Boolean = playerView.player?.isPlaying == true
+
+ fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) {
+ controllerVisibilityListener = listener
+ playerView.setControllerVisibilityListener(listener)
}
- fun getPlayerView(): PlayerView = playerView
+ fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) {
+ fullscreenButtonClickListener = listener
+ playerView.setFullscreenButtonClickListener(listener)
+ }
+
+ fun setShowSubtitleButton(show: Boolean) {
+ showSubtitleButton = show
+ playerView.setShowSubtitleButton(show)
+ }
+
+ fun setUseController(useController: Boolean) {
+ playerView.useController = useController
+ }
+
+ fun setControllerHideOnTouch(hideOnTouch: Boolean) {
+ playerView.setControllerHideOnTouch(hideOnTouch)
+ }
+
+ fun setControllerAutoShow(autoShow: Boolean) {
+ playerView.setControllerAutoShow(autoShow)
+ }
+
+ fun setControllerShowTimeoutMs(timeoutMs: Int) {
+ playerView.controllerShowTimeoutMs = timeoutMs
+ }
+
+ fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
- fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
- val targetResizeMode = when (resizeMode) {
- ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
- ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
- ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
- ResizeMode.RESIZE_MODE_FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
- ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
- else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
+ fun hideController() {
+ playerView.hideController()
+ }
+
+ fun showController() {
+ playerView.showController()
+ }
+
+ fun updateSurfaceView(@ViewType.ViewType viewType: Int) {
+ if (currentViewType == viewType) {
+ return
}
- // Apply the resize mode to PlayerView immediately
- playerView.resizeMode = targetResizeMode
+ currentViewType = viewType
- // Store it for reapplication if needed
- pendingResizeMode = targetResizeMode
+ val previousPlayerView = playerView
+ val previousLayoutParams = previousPlayerView.layoutParams ?: LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT
+ )
+ val previousResizeMode = previousPlayerView.resizeMode
+ val previousUseController = previousPlayerView.useController
+ val previousControllerAutoShow = previousPlayerView.controllerAutoShow
+ val previousControllerHideOnTouch = previousPlayerView.controllerHideOnTouch
+ val previousControllerShowTimeoutMs = previousPlayerView.controllerShowTimeoutMs
+
+ val replacementPlayerView = createPlayerView(viewType).apply {
+ layoutParams = previousLayoutParams
+ resizeMode = previousResizeMode
+ useController = previousUseController
+ controllerAutoShow = previousControllerAutoShow
+ controllerHideOnTouch = previousControllerHideOnTouch
+ controllerShowTimeoutMs = previousControllerShowTimeoutMs
+ setShowSubtitleButton(showSubtitleButton)
+ setControllerVisibilityListener(controllerVisibilityListener)
+ setFullscreenButtonClickListener(fullscreenButtonClickListener)
+ setShutterBackgroundColor(shutterColor)
+ player = this@ExoPlayerView.player
+ }
- // Force PlayerView to recalculate its layout
- playerView.requestLayout()
+ removeView(previousPlayerView)
+ playerView = replacementPlayerView
+ addView(playerView, 0, previousLayoutParams)
- // Also request layout on the parent to ensure proper sizing
- requestLayout()
+ updateSubtitleRenderingMode()
+ applySubtitleStyle(localStyle)
+ playerView.requestLayout()
}
fun setSubtitleStyle(style: SubtitleStyle) {
+ localStyle = style
+ applySubtitleStyle(localStyle)
+ }
+
+ private fun applySubtitleStyle(style: SubtitleStyle) {
+ updateSubtitleRenderingMode()
+
playerView.subtitleView?.let { subtitleView ->
- // Reset to defaults
- subtitleView.setUserDefaultStyle()
- subtitleView.setUserDefaultTextSize()
+ // Important:
+ // Avoid inheriting Android system caption settings via setUserDefaultStyle(),
+ // because those can force a background/window that the app doesn't want.
+ val resolvedTextColor = style.textColor ?: CaptionStyleCompat.DEFAULT.foregroundColor
+ val resolvedBackgroundColor = style.backgroundColor ?: Color.TRANSPARENT
+ val resolvedEdgeColor = style.edgeColor ?: Color.BLACK
+
+ val resolvedEdgeType = when (style.edgeType?.lowercase()) {
+ "outline" -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
+ "shadow" -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
+ else -> CaptionStyleCompat.EDGE_TYPE_NONE
+ }
+
+ // windowColor MUST be transparent to avoid the "caption window" background.
+ val captionStyle = CaptionStyleCompat(
+ resolvedTextColor,
+ resolvedBackgroundColor,
+ Color.TRANSPARENT,
+ resolvedEdgeType,
+ resolvedEdgeColor,
+ null
+ )
+ subtitleView.setStyle(captionStyle)
- // Apply custom styling
+ // Text size: if not provided, fall back to user default size.
if (style.fontSize > 0) {
- subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
+ // Use DIP so the value matches React Native's dp-based fontSize more closely.
+ // SP would multiply by system fontScale and makes "30" look larger than RN "30".
+ subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, style.fontSize.toFloat())
+ } else {
+ subtitleView.setUserDefaultTextSize()
}
+ // Horizontal padding is still useful (safe area); vertical offset is handled via bottomPaddingFraction.
subtitleView.setPadding(
style.paddingLeft,
style.paddingTop,
style.paddingRight,
- style.paddingBottom
+ 0
)
+ // Bottom offset for *internal* subtitles:
+ // Use Media3 SubtitleView's bottomPaddingFraction (moves cues up) rather than raw view padding.
+ if (style.paddingBottom > 0 && playerView.height > 0) {
+ val fraction = (style.paddingBottom.toFloat() / playerView.height.toFloat())
+ .coerceIn(0f, 0.9f)
+ subtitleView.setBottomPaddingFraction(fraction)
+ }
+
if (style.opacity != 0.0f) {
subtitleView.alpha = style.opacity
subtitleView.visibility = android.view.View.VISIBLE
@@ -132,157 +284,125 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
subtitleView.visibility = android.view.View.GONE
}
}
- localStyle = style
- }
-
- fun setShutterColor(color: Int) {
- playerView.setShutterBackgroundColor(color)
- }
-
- fun updateSurfaceView(viewType: Int) {
- // TODO: Implement proper surface type switching if needed
- }
-
- val isPlaying: Boolean
- get() = playerView.player?.isPlaying ?: false
-
- fun invalidateAspectRatio() {
- // PlayerView handles aspect ratio automatically through its internal AspectRatioFrameLayout
- playerView.requestLayout()
-
- // Reapply the current resize mode to ensure it's properly set
- pendingResizeMode?.let { resizeMode ->
- playerView.resizeMode = resizeMode
- }
- }
- fun setUseController(useController: Boolean) {
- playerView.useController = useController
- if (useController) {
- // Ensure proper touch handling when controls are enabled
- playerView.controllerAutoShow = true
- playerView.controllerHideOnTouch = true
- // Show controls immediately when enabled
- playerView.showController()
- }
- }
-
- fun showController() {
- playerView.showController()
- }
-
- fun hideController() {
- playerView.hideController()
- }
+ // Apply the same styling to the overlay subtitle view.
+ run {
+ val subtitleView = overlaySubtitleView
- fun setControllerShowTimeoutMs(showTimeoutMs: Int) {
- playerView.controllerShowTimeoutMs = showTimeoutMs
- }
+ val resolvedTextColor = style.textColor ?: CaptionStyleCompat.DEFAULT.foregroundColor
+ val resolvedBackgroundColor = style.backgroundColor ?: Color.TRANSPARENT
+ val resolvedEdgeColor = style.edgeColor ?: Color.BLACK
- fun setControllerAutoShow(autoShow: Boolean) {
- playerView.controllerAutoShow = autoShow
- }
+ val resolvedEdgeType = when (style.edgeType?.lowercase()) {
+ "outline" -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
+ "shadow" -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
+ else -> CaptionStyleCompat.EDGE_TYPE_NONE
+ }
- fun setControllerHideOnTouch(hideOnTouch: Boolean) {
- playerView.controllerHideOnTouch = hideOnTouch
- }
+ val captionStyle = CaptionStyleCompat(
+ resolvedTextColor,
+ resolvedBackgroundColor,
+ Color.TRANSPARENT,
+ resolvedEdgeType,
+ resolvedEdgeColor,
+ null
+ )
+ subtitleView.setStyle(captionStyle)
- fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) {
- playerView.setFullscreenButtonClickListener(listener)
- }
+ if (style.fontSize > 0) {
+ // Use DIP so the value matches React Native's dp-based fontSize more closely.
+ subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, style.fontSize.toFloat())
+ } else {
+ subtitleView.setUserDefaultTextSize()
+ }
- fun setShowSubtitleButton(show: Boolean) {
- playerView.setShowSubtitleButton(show)
- }
+ subtitleView.setPadding(
+ style.paddingLeft,
+ style.paddingTop,
+ style.paddingRight,
+ 0
+ )
- fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
+ // Bottom offset relative to the full view height (stable even when video content frame moves).
+ val h = height.takeIf { it > 0 } ?: subtitleView.height
+ if (style.paddingBottom > 0 && h > 0) {
+ val fraction = (style.paddingBottom.toFloat() / h.toFloat())
+ .coerceIn(0f, 0.9f)
+ subtitleView.setBottomPaddingFraction(fraction)
+ } else {
+ subtitleView.setBottomPaddingFraction(0f)
+ }
- fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) {
- playerView.setControllerVisibilityListener(listener)
+ if (style.opacity != 0.0f) {
+ subtitleView.alpha = style.opacity
+ }
+ }
}
- override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) {
- playerView.addOnLayoutChangeListener(listener)
+ fun setShutterColor(color: Int) {
+ shutterColor = color
+ playerView.setShutterBackgroundColor(color)
}
- override fun setFocusable(focusable: Boolean) {
- playerView.isFocusable = focusable
+ fun setShowLiveBadge(show: Boolean) {
+ liveBadge.visibility = if (show) View.VISIBLE else View.GONE
}
- private fun updateLiveUi() {
- val player = playerView.player ?: return
- val isLive = player.isCurrentMediaItemLive
- val seekable = player.isCurrentMediaItemSeekable
-
- // Show/hide badge
- liveBadge.visibility = if (isLive) View.VISIBLE else View.GONE
-
- // Disable/enable scrubbing based on seekable
- val timeBar = playerView.findViewById<DefaultTimeBar?>(androidx.media3.ui.R.id.exo_progress)
- timeBar?.isEnabled = !isLive || seekable
+ fun invalidateAspectRatio() {
+ playerView.post {
+ playerView.requestLayout()
+ }
}
private val playerListener = object : Player.Listener {
+ override fun onCues(cueGroup: CueGroup) {
+ // Keep overlay subtitles in sync. This does NOT interfere with PlayerView's own subtitle rendering.
+ // When subtitlesFollowVideo=false, overlaySubtitleView is the visible one.
+ updateSubtitleRenderingMode()
+ overlaySubtitleView.setCues(cueGroup.cues)
+ }
+
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
playerView.post {
playerView.requestLayout()
- // Reapply resize mode to ensure it's properly set after timeline changes
- pendingResizeMode?.let { resizeMode ->
- playerView.resizeMode = resizeMode
- }
}
- updateLiveUi()
}
+ }
- override fun onEvents(player: Player, events: Player.Events) {
- if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) ||
- events.contains(Player.EVENT_IS_PLAYING_CHANGED)
- ) {
- updateLiveUi()
- }
-
- // Handle video size changes which affect aspect ratio
- if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) {
- pendingResizeMode?.let { resizeMode ->
- playerView.resizeMode = resizeMode
- }
- playerView.requestLayout()
- requestLayout()
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = MeasureSpec.getSize(widthMeasureSpec)
+ val height = MeasureSpec.getSize(heightMeasureSpec)
+ if (width > 0 && height > 0) {
+ pendingResizeMode?.let { resizeMode ->
+ playerView.resizeMode = resizeMode
}
+ // Re-apply bottomPaddingFraction once we have a concrete height.
+ updateSubtitleRenderingMode()
+ applySubtitleStyle(localStyle)
}
}
- companion object {
- private const val TAG = "ExoPlayerView"
- }
-
- /**
- * React Native (Yoga) can sometimes defer layout passes that are required by
- * PlayerView for its child views (controller overlay, surface view, subtitle view, …).
- * This helper forces a second measure / layout after RN finishes, ensuring the
- * internal views receive the final size. The same approach is used in the v7
- * implementation (see VideoView.kt) and in React Native core (Toolbar example [link]).
- */
- private val layoutRunnable = Runnable {
- measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
- )
- layout(left, top, right, bottom)
- }
-
- override fun requestLayout() {
- super.requestLayout()
- // Post a second layout pass so the ExoPlayer internal views get correct bounds.
- post(layoutRunnable)
- }
-
- override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- super.onLayout(changed, left, top, right, bottom)
+ private fun createPlayerView(@ViewType.ViewType viewType: Int): PlayerView {
+ val layoutRes = when (viewType) {
+ ViewType.VIEW_TYPE_TEXTURE -> R.layout.exo_player_view_texture
+ else -> R.layout.exo_player_view_surface
+ }
- if (changed) {
- pendingResizeMode?.let { resizeMode ->
- playerView.resizeMode = resizeMode
+ return (LayoutInflater.from(context).inflate(layoutRes, this, false) as PlayerView).apply {
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
+ setShutterBackgroundColor(shutterColor)
+ useController = true
+ controllerAutoShow = true
+ controllerHideOnTouch = true
+ controllerShowTimeoutMs = 5000
+ setShowSubtitleButton(showSubtitleButton)
+ setUseArtwork(false)
+ setDefaultArtwork(null)
+ resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
+
+ if (viewType == ViewType.VIEW_TYPE_SURFACE_SECURE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ (videoSurfaceView as? SurfaceView)?.setSecure(true)
}
}
}
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt
index b5d786b..3c7ed65 100644
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt
@@ -50,7 +50,7 @@ class FullScreenPlayerView(
if (fullscreenVideoPlayer != null) {
val window = fullscreenVideoPlayer.window
if (window != null) {
- val isPlaying = fullscreenVideoPlayer.exoPlayerView.isPlaying
+ val isPlaying = fullscreenVideoPlayer.exoPlayerView.isPlaying()
if (isPlaying) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
index e16ac96..773535a 100644
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
@@ -385,7 +385,7 @@ public class ReactExoplayerView extends FrameLayout implements
Activity activity = themedReactContext.getCurrentActivity();
boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode();
boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode();
- if (playInBackground || isInPictureInPicture || isInMultiWindowMode) {
+ if (playInBackground || isInPictureInPicture || isInMultiWindowMode || enterPictureInPictureOnLeave) {
return;
}
setPlayWhenReady(false);
@@ -1567,6 +1567,11 @@ public class ReactExoplayerView extends FrameLayout implements
Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group);
audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
audioTrack.setSelected(isSelected);
+ // Encode channel count into title so JS can read it e.g. "English|ch:6"
+ if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) {
+ String existing = audioTrack.getTitle() != null ? audioTrack.getTitle() : "";
+ audioTrack.setTitle(existing + "|ch:" + format.channelCount);
+ }
audioTracks.add(audioTrack);
}
@@ -1753,7 +1758,11 @@ public class ReactExoplayerView extends FrameLayout implements
Track track = new Track();
track.setIndex(groupIndex);
track.setLanguage(format.language != null ? format.language : "unknown");
- track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1));
+ String baseTitle = format.label != null ? format.label : "";
+ if (format.channelCount != Format.NO_VALUE && format.channelCount > 0) {
+ baseTitle = baseTitle + "|ch:" + format.channelCount;
+ }
+ track.setTitle(baseTitle);
track.setSelected(false); // Don't report selection status - let PlayerView handle it
if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType);
track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
@@ -2127,7 +2136,8 @@ public class ReactExoplayerView extends FrameLayout implements
}
private void selectTextTrackInternal(String type, String value) {
- if (player == null || trackSelector == null) return;
+ if (player == null || trackSelector == null)
+ return;
DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value);
@@ -2146,6 +2156,10 @@ public class ReactExoplayerView extends FrameLayout implements
if (textRendererIndex != C.INDEX_UNSET) {
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
boolean trackFound = false;
+ // react-native-video uses a flattened `textTracks` list on the JS side.
+ // For HLS/DASH, each TrackGroup often contains a single track at index 0,
+ // so comparing against `trackIndex` alone makes only the first subtitle selectable.
+ int flattenedIndex = 0;
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
TrackGroup group = groups.get(groupIndex);
@@ -2159,11 +2173,13 @@ public class ReactExoplayerView extends FrameLayout implements
isMatch = true;
} else if ("index".equals(type)) {
int targetIndex = ReactBridgeUtils.safeParseInt(value, -1);
- if (targetIndex == trackIndex) {
+ if (targetIndex == flattenedIndex) {
isMatch = true;
}
}
+ flattenedIndex++;
+
if (isMatch) {
TrackSelectionOverride override = new TrackSelectionOverride(group,
java.util.Arrays.asList(trackIndex));
diff --git a/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml
new file mode 100644
index 0000000..4ea3c30
--- /dev/null
+++ b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_surface.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:surface_type="surface_view" />
diff --git a/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml
new file mode 100644
index 0000000..53c1909
--- /dev/null
+++ b/node_modules/react-native-video/android/src/main/res/layout/exo_player_view_texture.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:surface_type="texture_view" />