diff --git a/packages/react-native-video/android/src/main/java/com/video/core/fragments/FullscreenVideoFragment.kt b/packages/react-native-video/android/src/main/java/com/video/core/fragments/FullscreenVideoFragment.kt index 35d4eb2f..6b029d3e 100644 --- a/packages/react-native-video/android/src/main/java/com/video/core/fragments/FullscreenVideoFragment.kt +++ b/packages/react-native-video/android/src/main/java/com/video/core/fragments/FullscreenVideoFragment.kt @@ -1,6 +1,7 @@ package com.video.core.fragments import android.annotation.SuppressLint +import android.content.Context import android.content.res.Configuration import android.os.Build import android.os.Bundle @@ -9,6 +10,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowInsetsController +import android.view.WindowManager import android.widget.FrameLayout import android.widget.ImageButton import androidx.activity.OnBackPressedCallback @@ -17,6 +19,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.media3.common.util.UnstableApi import com.video.core.utils.PictureInPictureUtils.createPictureInPictureParams +import com.video.core.utils.SmallVideoPlayerOptimizer import com.video.view.VideoView import java.util.UUID @@ -131,6 +134,9 @@ class FullscreenVideoFragment(private val videoView: VideoView) : Fragment() { setupFullscreenButton() videoView.playerView.setShowSubtitleButton(true) + + // Apply optimizations based on video player size in fullscreen mode + SmallVideoPlayerOptimizer.applyOptimizations(videoView.playerView, requireContext(), isFullscreen = true) } @SuppressLint("PrivateResource") @@ -220,6 +226,8 @@ class FullscreenVideoFragment(private val videoView: VideoView) : Fragment() { videoView.isInFullscreen = false } + + override fun onDestroy() { super.onDestroy() diff --git a/packages/react-native-video/android/src/main/java/com/video/core/utils/SmallVideoPlayerOptimizer.kt b/packages/react-native-video/android/src/main/java/com/video/core/utils/SmallVideoPlayerOptimizer.kt new file mode 100644 index 00000000..dfe91389 --- /dev/null +++ b/packages/react-native-video/android/src/main/java/com/video/core/utils/SmallVideoPlayerOptimizer.kt @@ -0,0 +1,157 @@ +package com.video.core.utils + +import android.content.Context +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import androidx.media3.ui.PlayerView + +object SmallVideoPlayerOptimizer { + + fun isSmallVideoPlayer(playerView: PlayerView): Boolean { + // Check if the PlayerView dimensions are small enough to warrant optimizations + val width = playerView.width + val height = playerView.height + + // If view hasn't been measured yet, use layout params or return false + if (width <= 0 || height <= 0) { + val layoutParams = playerView.layoutParams + if (layoutParams != null) { + // Convert any specific dimensions to pixels if needed + val widthPx = if (layoutParams.width > 0) layoutParams.width else playerView.measuredWidth + val heightPx = if (layoutParams.height > 0) layoutParams.height else playerView.measuredHeight + + if (widthPx <= 0 || heightPx <= 0) return false + + return isSmallDimensions(widthPx, heightPx, playerView.context) + } + return false + } + + return isSmallDimensions(width, height, playerView.context) + } + + private fun isSmallDimensions(widthPx: Int, heightPx: Int, context: Context): Boolean { + val density = context.resources.displayMetrics.density + val widthDp = widthPx / density + val heightDp = heightPx / density + + // Consider the video player "small" if width <= 400dp or height <= 300dp + // These thresholds are more appropriate for actual video player sizes + return widthDp <= 400 || heightDp <= 300 + } + + fun applyOptimizations( + playerView: PlayerView, + context: Context, + isFullscreen: Boolean = false + ) { + playerView.post { + try { + if (isFullscreen) { + // For fullscreen mode, use system defaults - no custom optimizations + // Let ExoPlayer use its default controller timeout and styling + return@post + } + + // Only apply optimizations if the video player itself is small + if (!isSmallVideoPlayer(playerView)) { + return@post + } + + val controllerView = playerView.findViewById(androidx.media3.ui.R.id.exo_controller) + controllerView?.let { controller -> + optimizeControlElementsForSmallPlayer(controller, context) + } + } catch (e: Exception) { + Log.w("ReactNativeVideo", "Error applying small video player optimizations: ${e.message}") + } + } + } + + private fun optimizeControlElementsForSmallPlayer( + controller: ViewGroup, + context: Context + ) { + val density = context.resources.displayMetrics.density + val primaryButtonSize = (48 * density).toInt() + val secondaryButtonSize = (44 * density).toInt() + + optimizeButtons(controller, primaryButtonSize, secondaryButtonSize) + optimizeProgressBar(controller, context) + optimizeTextElements(controller) + } + + private fun optimizeButtons( + container: ViewGroup, + primarySize: Int, + secondarySize: Int + ) { + for (i in 0 until container.childCount) { + val child = container.getChildAt(i) + when (child) { + is ImageButton -> { + val buttonSize = when (child.id) { + androidx.media3.ui.R.id.exo_play_pause -> primarySize + androidx.media3.ui.R.id.exo_fullscreen -> primarySize + androidx.media3.ui.R.id.exo_settings -> primarySize + androidx.media3.ui.R.id.exo_rew -> secondarySize + androidx.media3.ui.R.id.exo_ffwd -> secondarySize + androidx.media3.ui.R.id.exo_subtitle -> secondarySize + androidx.media3.ui.R.id.exo_prev -> secondarySize + androidx.media3.ui.R.id.exo_next -> secondarySize + else -> secondarySize + } + + val params = child.layoutParams + params.width = buttonSize + params.height = buttonSize + child.layoutParams = params + + // Hide less essential buttons on small video players + when (child.id) { + androidx.media3.ui.R.id.exo_shuffle, + androidx.media3.ui.R.id.exo_repeat_toggle, + androidx.media3.ui.R.id.exo_vr -> { + child.visibility = View.GONE + } + } + } + is ViewGroup -> { + optimizeButtons(child, primarySize, secondarySize) + } + } + } + } + + private fun optimizeProgressBar( + controller: ViewGroup, + context: Context + ) { + val progressContainer = controller.findViewById(androidx.media3.ui.R.id.exo_progress) + progressContainer?.let { progress -> + val params = progress.layoutParams as? ViewGroup.MarginLayoutParams + params?.let { + it.height = (4 * context.resources.displayMetrics.density).toInt() + progress.layoutParams = it + } + } + } + + private fun optimizeTextElements( + controller: ViewGroup + ) { + val timeContainer = controller.findViewById(androidx.media3.ui.R.id.exo_time) + timeContainer?.let { time -> + val positionView = time.findViewById(androidx.media3.ui.R.id.exo_position) + val durationView = time.findViewById(androidx.media3.ui.R.id.exo_duration) + + listOf(positionView, durationView).forEach { textView -> + if (textView is android.widget.TextView) { + textView.textSize = 12f + } + } + } + } +} diff --git a/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt b/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt index aa34934d..37596b73 100644 --- a/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt +++ b/packages/react-native-video/android/src/main/java/com/video/view/VideoView.kt @@ -6,9 +6,11 @@ import android.content.Context import android.graphics.Color import android.os.Build import android.util.AttributeSet +import android.util.DisplayMetrics import android.util.Log import android.view.View import android.view.ViewGroup +import android.view.WindowManager import android.widget.FrameLayout import android.widget.ImageButton import androidx.annotation.RequiresApi @@ -32,6 +34,7 @@ import com.video.core.utils.Threading.runOnMainThread import com.video.core.extensions.toAspectRatioFrameLayout import com.video.core.utils.PictureInPictureUtils import com.video.core.utils.PictureInPictureUtils.createDisabledPictureInPictureParams +import com.video.core.utils.SmallVideoPlayerOptimizer @UnstableApi class VideoView @JvmOverloads constructor( @@ -109,6 +112,9 @@ class VideoView @JvmOverloads constructor( setShutterBackgroundColor(Color.TRANSPARENT) setShowSubtitleButton(true) useController = false + + // Apply optimizations based on video player size if needed + configureForSmallPlayer() } var isInFullscreen: Boolean = false set(value) { @@ -145,6 +151,9 @@ class VideoView @JvmOverloads constructor( MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) ) layout(left, top, right, bottom) + + // Additional layout fixes for small video players + applySmallPlayerLayoutFixes() } override fun requestLayout() { @@ -363,4 +372,17 @@ class VideoView @JvmOverloads constructor( setupPipHelper() super.onAttachedToWindow() } + + private fun PlayerView.configureForSmallPlayer() { + SmallVideoPlayerOptimizer.applyOptimizations(this, context, isFullscreen = false) + + // Also apply after any layout changes + viewTreeObserver.addOnGlobalLayoutListener { + SmallVideoPlayerOptimizer.applyOptimizations(this, context, isFullscreen = false) + } + } + + private fun applySmallPlayerLayoutFixes() { + SmallVideoPlayerOptimizer.applyOptimizations(playerView, context, isFullscreen = false) + } }