feat(android): replace custom VideoView with media3 PlayerView (#4581)
This commit is contained in:
committed by
GitHub
parent
1a4e3b0637
commit
978683b645
@@ -1,276 +1,275 @@
|
||||
package com.brentvatne.exoplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.SurfaceView
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.View.MeasureSpec
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.AdViewProvider
|
||||
import androidx.media3.common.C
|
||||
import android.widget.TextView
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.Cue
|
||||
import androidx.media3.common.util.Assertions
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.brentvatne.common.api.ResizeMode
|
||||
import com.brentvatne.common.api.SubtitleStyle
|
||||
import com.brentvatne.common.api.ViewType
|
||||
import com.brentvatne.common.toolbox.DebugLog
|
||||
|
||||
@UnstableApi
|
||||
class ExoPlayerView(private val context: Context) :
|
||||
FrameLayout(context, null, 0),
|
||||
AdViewProvider {
|
||||
|
||||
var surfaceView: View? = null
|
||||
private set
|
||||
private var shutterView: View
|
||||
private var subtitleLayout: SubtitleView
|
||||
private var layout: AspectRatioFrameLayout
|
||||
private var componentListener: ComponentListener
|
||||
private var player: ExoPlayer? = null
|
||||
private var layoutParams: ViewGroup.LayoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
private var adOverlayFrameLayout: FrameLayout? = null
|
||||
val isPlaying: Boolean
|
||||
get() = player != null && player?.isPlaying == true
|
||||
|
||||
@ViewType.ViewType
|
||||
private var viewType = ViewType.VIEW_TYPE_SURFACE
|
||||
private var hideShutterView = false
|
||||
class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||
FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var localStyle = SubtitleStyle()
|
||||
private var surfaceView: View? = null
|
||||
private val liveBadge: TextView = TextView(context).apply {
|
||||
text = "LIVE"
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 12f
|
||||
val drawable = GradientDrawable()
|
||||
drawable.setColor(Color.RED)
|
||||
drawable.cornerRadius = 6f
|
||||
background = drawable
|
||||
setPadding(12, 4, 12, 4)
|
||||
visibility = View.GONE
|
||||
}
|
||||
|
||||
private val playerView = PlayerView(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
|
||||
resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
|
||||
init {
|
||||
componentListener = ComponentListener()
|
||||
|
||||
val aspectRatioParams = LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT
|
||||
)
|
||||
aspectRatioParams.gravity = Gravity.CENTER
|
||||
layout = AspectRatioFrameLayout(context)
|
||||
layout.layoutParams = aspectRatioParams
|
||||
|
||||
shutterView = View(context)
|
||||
shutterView.layoutParams = layoutParams
|
||||
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black))
|
||||
|
||||
subtitleLayout = SubtitleView(context)
|
||||
subtitleLayout.layoutParams = layoutParams
|
||||
subtitleLayout.setUserDefaultStyle()
|
||||
subtitleLayout.setUserDefaultTextSize()
|
||||
|
||||
updateSurfaceView(viewType)
|
||||
|
||||
layout.addView(shutterView, 1, layoutParams)
|
||||
if (localStyle.subtitlesFollowVideo) {
|
||||
layout.addView(subtitleLayout, layoutParams)
|
||||
}
|
||||
|
||||
addViewInLayout(layout, 0, aspectRatioParams)
|
||||
if (!localStyle.subtitlesFollowVideo) {
|
||||
addViewInLayout(subtitleLayout, 1, layoutParams)
|
||||
}
|
||||
addView(playerView)
|
||||
val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
lp.setMargins(16, 16, 16, 16)
|
||||
addView(liveBadge, lp)
|
||||
}
|
||||
|
||||
private fun clearVideoView() {
|
||||
when (val view = surfaceView) {
|
||||
is TextureView -> player?.clearVideoTextureView(view)
|
||||
fun setPlayer(player: ExoPlayer?) {
|
||||
val currentPlayer = playerView.player
|
||||
|
||||
is SurfaceView -> player?.clearVideoSurfaceView(view)
|
||||
if (currentPlayer != null) {
|
||||
currentPlayer.removeListener(playerListener)
|
||||
// Clear any existing surface from the player
|
||||
when (surfaceView) {
|
||||
is SurfaceView -> currentPlayer.clearVideoSurfaceView(surfaceView as SurfaceView)
|
||||
is TextureView -> currentPlayer.clearVideoTextureView(surfaceView as TextureView)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(
|
||||
"clearVideoView",
|
||||
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
|
||||
)
|
||||
playerView.player = player
|
||||
|
||||
if (player != null) {
|
||||
player.addListener(playerListener)
|
||||
// Set the surface view for the new player
|
||||
when (surfaceView) {
|
||||
is SurfaceView -> player.setVideoSurfaceView(surfaceView as SurfaceView)
|
||||
is TextureView -> player.setVideoTextureView(surfaceView as TextureView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setVideoView() {
|
||||
when (val view = surfaceView) {
|
||||
is TextureView -> player?.setVideoTextureView(view)
|
||||
fun getPlayerView(): PlayerView = playerView
|
||||
|
||||
is SurfaceView -> player?.setVideoSurfaceView(view)
|
||||
|
||||
else -> {
|
||||
Log.w(
|
||||
"setVideoView",
|
||||
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
|
||||
)
|
||||
}
|
||||
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
||||
playerView.resizeMode = 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 setSubtitleStyle(style: SubtitleStyle) {
|
||||
// ensure we reset subtitle style before reapplying it
|
||||
subtitleLayout.setUserDefaultStyle()
|
||||
subtitleLayout.setUserDefaultTextSize()
|
||||
playerView.subtitleView?.let { subtitleView ->
|
||||
// Reset to defaults
|
||||
subtitleView.setUserDefaultStyle()
|
||||
subtitleView.setUserDefaultTextSize()
|
||||
|
||||
if (style.fontSize > 0) {
|
||||
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
||||
}
|
||||
subtitleLayout.setPadding(
|
||||
style.paddingLeft,
|
||||
style.paddingTop,
|
||||
style.paddingTop,
|
||||
style.paddingBottom
|
||||
)
|
||||
if (style.opacity != 0.0f) {
|
||||
subtitleLayout.alpha = style.opacity
|
||||
subtitleLayout.visibility = View.VISIBLE
|
||||
} else {
|
||||
subtitleLayout.visibility = View.GONE
|
||||
}
|
||||
if (localStyle.subtitlesFollowVideo != style.subtitlesFollowVideo) {
|
||||
// No need to manipulate layout if value didn't change
|
||||
if (style.subtitlesFollowVideo) {
|
||||
removeViewInLayout(subtitleLayout)
|
||||
layout.addView(subtitleLayout, layoutParams)
|
||||
} else {
|
||||
layout.removeViewInLayout(subtitleLayout)
|
||||
addViewInLayout(subtitleLayout, 1, layoutParams, false)
|
||||
// Apply custom styling
|
||||
if (style.fontSize > 0) {
|
||||
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
||||
}
|
||||
|
||||
subtitleView.setPadding(
|
||||
style.paddingLeft,
|
||||
style.paddingTop,
|
||||
style.paddingRight,
|
||||
style.paddingBottom
|
||||
)
|
||||
|
||||
if (style.opacity != 0.0f) {
|
||||
subtitleView.alpha = style.opacity
|
||||
subtitleView.visibility = android.view.View.VISIBLE
|
||||
} else {
|
||||
subtitleView.visibility = android.view.View.GONE
|
||||
}
|
||||
requestLayout()
|
||||
}
|
||||
localStyle = style
|
||||
}
|
||||
|
||||
fun setShutterColor(color: Int) {
|
||||
shutterView.setBackgroundColor(color)
|
||||
playerView.setShutterBackgroundColor(color)
|
||||
}
|
||||
|
||||
fun updateSurfaceView(@ViewType.ViewType viewType: Int) {
|
||||
this.viewType = viewType
|
||||
var viewNeedRefresh = false
|
||||
when (viewType) {
|
||||
ViewType.VIEW_TYPE_SURFACE, ViewType.VIEW_TYPE_SURFACE_SECURE -> {
|
||||
if (surfaceView !is SurfaceView) {
|
||||
surfaceView = SurfaceView(context)
|
||||
viewNeedRefresh = true
|
||||
fun updateSurfaceView(viewType: Int) {
|
||||
val currentSurfaceView = surfaceView
|
||||
val newSurfaceView: View = when (viewType) {
|
||||
ViewType.VIEW_TYPE_TEXTURE -> TextureView(context)
|
||||
|
||||
ViewType.VIEW_TYPE_SURFACE_SECURE -> SurfaceView(context).apply {
|
||||
// Requires API 17+
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
(this as SurfaceView).setSecure(true)
|
||||
}
|
||||
(surfaceView as SurfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE)
|
||||
}
|
||||
|
||||
ViewType.VIEW_TYPE_TEXTURE -> {
|
||||
if (surfaceView !is TextureView) {
|
||||
surfaceView = TextureView(context)
|
||||
viewNeedRefresh = true
|
||||
}
|
||||
// Support opacity properly:
|
||||
(surfaceView as TextureView).isOpaque = false
|
||||
}
|
||||
|
||||
else -> {
|
||||
DebugLog.wtf(TAG, "Unexpected texture view type: $viewType")
|
||||
}
|
||||
else -> SurfaceView(context)
|
||||
}
|
||||
|
||||
if (viewNeedRefresh) {
|
||||
surfaceView?.layoutParams = layoutParams
|
||||
|
||||
if (layout.getChildAt(0) != null) {
|
||||
layout.removeViewAt(0)
|
||||
}
|
||||
layout.addView(surfaceView, 0, layoutParams)
|
||||
|
||||
if (this.player != null) {
|
||||
setVideoView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var adsShown = false
|
||||
fun showAds() {
|
||||
if (!adsShown) {
|
||||
adOverlayFrameLayout = FrameLayout(context)
|
||||
layout.addView(adOverlayFrameLayout, layoutParams)
|
||||
adsShown = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideAds() {
|
||||
if (adsShown) {
|
||||
layout.removeView(adOverlayFrameLayout)
|
||||
adOverlayFrameLayout = null
|
||||
adsShown = false
|
||||
}
|
||||
}
|
||||
|
||||
fun updateShutterViewVisibility() {
|
||||
shutterView.visibility = if (this.hideShutterView) {
|
||||
View.INVISIBLE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
super.requestLayout()
|
||||
post(measureAndLayout)
|
||||
}
|
||||
|
||||
// AdsLoader.AdViewProvider implementation.
|
||||
override fun getAdViewGroup(): ViewGroup =
|
||||
Assertions.checkNotNull(
|
||||
adOverlayFrameLayout,
|
||||
"exo_ad_overlay must be present for ad playback"
|
||||
)
|
||||
|
||||
/**
|
||||
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
|
||||
* player will be called and previous
|
||||
* assignments are overridden.
|
||||
*
|
||||
* @param player The {@link ExoPlayer} to use.
|
||||
*/
|
||||
fun setPlayer(player: ExoPlayer?) {
|
||||
if (this.player == player) {
|
||||
if (currentSurfaceView === newSurfaceView) {
|
||||
return
|
||||
}
|
||||
if (this.player != null) {
|
||||
this.player!!.removeListener(componentListener)
|
||||
clearVideoView()
|
||||
}
|
||||
this.player = player
|
||||
|
||||
updateShutterViewVisibility()
|
||||
if (player != null) {
|
||||
setVideoView()
|
||||
player.addListener(componentListener)
|
||||
// Remove the old surface view
|
||||
if (currentSurfaceView != null) {
|
||||
removeView(currentSurfaceView)
|
||||
}
|
||||
|
||||
// Add the new surface view
|
||||
surfaceView = newSurfaceView
|
||||
val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
addView(newSurfaceView, 0, layoutParams)
|
||||
|
||||
// Attach player to the new surface
|
||||
playerView.player?.let {
|
||||
when (newSurfaceView) {
|
||||
is SurfaceView -> it.setVideoSurfaceView(newSurfaceView)
|
||||
is TextureView -> it.setVideoTextureView(newSurfaceView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = playerView.player?.isPlaying ?: false
|
||||
|
||||
fun invalidateAspectRatio() {
|
||||
// PlayerView handles aspect ratio automatically
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
fun setControllerShowTimeoutMs(showTimeoutMs: Int) {
|
||||
playerView.controllerShowTimeoutMs = showTimeoutMs
|
||||
}
|
||||
|
||||
fun setControllerAutoShow(autoShow: Boolean) {
|
||||
playerView.controllerAutoShow = autoShow
|
||||
}
|
||||
|
||||
fun setControllerHideOnTouch(hideOnTouch: Boolean) {
|
||||
playerView.controllerHideOnTouch = hideOnTouch
|
||||
}
|
||||
|
||||
fun setFullscreenButtonClickListener(listener: PlayerView.FullscreenButtonClickListener?) {
|
||||
playerView.setFullscreenButtonClickListener(listener)
|
||||
}
|
||||
|
||||
fun setShowSubtitleButton(show: Boolean) {
|
||||
playerView.setShowSubtitleButton(show)
|
||||
}
|
||||
|
||||
fun isControllerVisible(): Boolean = playerView.isControllerFullyVisible
|
||||
|
||||
fun setControllerVisibilityListener(listener: PlayerView.ControllerVisibilityListener?) {
|
||||
playerView.setControllerVisibilityListener(listener)
|
||||
}
|
||||
|
||||
override fun addOnLayoutChangeListener(listener: View.OnLayoutChangeListener) {
|
||||
playerView.addOnLayoutChangeListener(listener)
|
||||
}
|
||||
|
||||
override fun setFocusable(focusable: Boolean) {
|
||||
playerView.isFocusable = focusable
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private val playerListener = object : Player.Listener {
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
playerView.post { playerView.requestLayout() }
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ExoPlayerView"
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resize mode which can be of value {@link ResizeMode.Mode}
|
||||
*
|
||||
* @param resizeMode The resize mode.
|
||||
* 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]).
|
||||
*/
|
||||
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
||||
if (layout.resizeMode != resizeMode) {
|
||||
layout.resizeMode = resizeMode
|
||||
post(measureAndLayout)
|
||||
}
|
||||
}
|
||||
|
||||
fun setHideShutterView(hideShutterView: Boolean) {
|
||||
this.hideShutterView = hideShutterView
|
||||
updateShutterViewVisibility()
|
||||
}
|
||||
|
||||
private val measureAndLayout: Runnable = Runnable {
|
||||
private val layoutRunnable = Runnable {
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||
@@ -278,60 +277,9 @@ class ExoPlayerView(private val context: Context) :
|
||||
layout(left, top, right, bottom)
|
||||
}
|
||||
|
||||
private fun updateForCurrentTrackSelections(tracks: Tracks?) {
|
||||
if (tracks == null) {
|
||||
return
|
||||
}
|
||||
val groups = tracks.groups
|
||||
|
||||
for (group in groups) {
|
||||
if (group.type == C.TRACK_TYPE_VIDEO && group.length > 0) {
|
||||
// get the first track of the group to identify aspect ratio
|
||||
val format = group.getTrackFormat(0)
|
||||
if (format.width > 0 || format.height > 0) {
|
||||
layout.updateAspectRatio(format)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
// no video tracks, in that case refresh shutterView visibility
|
||||
updateShutterViewVisibility()
|
||||
}
|
||||
|
||||
fun invalidateAspectRatio() {
|
||||
// Resetting aspect ratio will force layout refresh on next video size changed
|
||||
layout.invalidateAspectRatio()
|
||||
}
|
||||
|
||||
private inner class ComponentListener : Player.Listener {
|
||||
override fun onCues(cues: List<Cue>) {
|
||||
subtitleLayout.setCues(cues)
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
if (videoSize.height == 0 || videoSize.width == 0) {
|
||||
// When changing video track we receive an ghost state with height / width = 0
|
||||
// No need to resize the view in that case
|
||||
return
|
||||
}
|
||||
// Here we use updateForCurrentTrackSelections to have a consistent behavior.
|
||||
// according to: https://github.com/androidx/media/issues/1207
|
||||
// sometimes media3 send bad Video size information
|
||||
player?.let {
|
||||
updateForCurrentTrackSelections(it.currentTracks)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRenderedFirstFrame() {
|
||||
shutterView.visibility = INVISIBLE
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
updateForCurrentTrackSelections(tracks)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ExoPlayerView"
|
||||
override fun requestLayout() {
|
||||
super.requestLayout()
|
||||
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
|
||||
post(layoutRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
@@ -91,7 +89,6 @@ class FullScreenPlayerView(
|
||||
parent?.removeView(exoPlayerView)
|
||||
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
|
||||
playerControlView?.let {
|
||||
updateFullscreenButton(playerControlView, true)
|
||||
parent?.removeView(it)
|
||||
containerView.addView(it, generateDefaultLayoutParams())
|
||||
}
|
||||
@@ -104,7 +101,6 @@ class FullScreenPlayerView(
|
||||
containerView.removeView(exoPlayerView)
|
||||
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
|
||||
playerControlView?.let {
|
||||
updateFullscreenButton(playerControlView, false)
|
||||
containerView.removeView(it)
|
||||
parent?.addView(it, generateDefaultLayoutParams())
|
||||
}
|
||||
@@ -141,20 +137,6 @@ class FullScreenPlayerView(
|
||||
androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter
|
||||
}
|
||||
|
||||
private fun updateFullscreenButton(playerControlView: LegacyPlayerControlView, isFullscreen: Boolean) {
|
||||
val imageButton = playerControlView.findViewById<ImageButton?>(com.brentvatne.react.R.id.exo_fullscreen)
|
||||
imageButton?.let {
|
||||
val imgResource = getFullscreenIconResource(isFullscreen)
|
||||
val desc = if (isFullscreen) {
|
||||
context.getString(androidx.media3.ui.R.string.exo_controls_fullscreen_exit_description)
|
||||
} else {
|
||||
context.getString(androidx.media3.ui.R.string.exo_controls_fullscreen_enter_description)
|
||||
}
|
||||
imageButton.setImageResource(imgResource)
|
||||
imageButton.contentDescription = desc
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
|
||||
@@ -226,13 +208,7 @@ class FullScreenPlayerView(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
)
|
||||
}
|
||||
if (controlsConfig.hideNotificationBarOnFullScreenMode) {
|
||||
val liveContainer = playerControlView?.findViewById<LinearLayout?>(com.brentvatne.react.R.id.exo_live_container)
|
||||
liveContainer?.let {
|
||||
val layoutParams = it.layoutParams as LinearLayout.LayoutParams
|
||||
layoutParams.topMargin = 40
|
||||
it.layoutParams = layoutParams
|
||||
}
|
||||
}
|
||||
// Note: Live container adjustment is no longer needed since we're using PlayerView's built-in controls
|
||||
// PlayerView handles UI adjustments automatically
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,9 +141,10 @@ object PictureInPictureUtil {
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun calcRectHint(playerView: ExoPlayerView): Rect {
|
||||
val hint = Rect()
|
||||
playerView.surfaceView?.getGlobalVisibleRect(hint)
|
||||
// Use the PlayerView itself since surfaceView is private
|
||||
playerView.getGlobalVisibleRect(hint)
|
||||
val location = IntArray(2)
|
||||
playerView.surfaceView?.getLocationOnScreen(location)
|
||||
playerView.getLocationOnScreen(location)
|
||||
|
||||
val height = hint.bottom - hint.top
|
||||
hint.top = location[1]
|
||||
|
||||
@@ -103,7 +103,6 @@ import androidx.media3.extractor.metadata.emsg.EventMessage;
|
||||
import androidx.media3.extractor.metadata.id3.Id3Frame;
|
||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
|
||||
import androidx.media3.session.MediaSessionService;
|
||||
import androidx.media3.ui.LegacyPlayerControlView;
|
||||
|
||||
import com.brentvatne.common.api.AdsProps;
|
||||
import com.brentvatne.common.api.BufferConfig;
|
||||
@@ -178,8 +177,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
protected final VideoEventEmitter eventEmitter;
|
||||
private final ReactExoplayerConfig config;
|
||||
private DefaultBandwidthMeter bandwidthMeter;
|
||||
private LegacyPlayerControlView playerControlView;
|
||||
private View playPauseControlContainer;
|
||||
private Player.Listener eventListener;
|
||||
|
||||
private ExoPlayerView exoPlayerView;
|
||||
@@ -248,7 +245,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private float mProgressUpdateInterval = 250.0f;
|
||||
protected boolean playInBackground = false;
|
||||
private boolean mReportBandwidth = false;
|
||||
private boolean controls;
|
||||
private boolean controls = false;
|
||||
|
||||
private boolean showNotificationControls = false;
|
||||
// \ End props
|
||||
@@ -278,8 +275,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
private void updateProgress() {
|
||||
if (player != null) {
|
||||
if (playerControlView != null && isPlayingAd() && controls) {
|
||||
playerControlView.hide();
|
||||
if (exoPlayerView != null && isPlayingAd() && controls) {
|
||||
exoPlayerView.hideController();
|
||||
}
|
||||
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
|
||||
long duration = player.getDuration();
|
||||
@@ -423,103 +420,45 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
*/
|
||||
private void togglePlayerControlVisibility() {
|
||||
if (player == null) return;
|
||||
reLayoutControls();
|
||||
if (playerControlView.isVisible()) {
|
||||
playerControlView.hide();
|
||||
if (exoPlayerView.isControllerVisible()) {
|
||||
exoPlayerView.hideController();
|
||||
} else {
|
||||
playerControlView.show();
|
||||
exoPlayerView.showController();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializing Player control
|
||||
*/
|
||||
private void initializePlayerControl() {
|
||||
if (playerControlView == null) {
|
||||
playerControlView = new LegacyPlayerControlView(getContext());
|
||||
playerControlView.addVisibilityListener(new LegacyPlayerControlView.VisibilityListener() {
|
||||
@Override
|
||||
public void onVisibilityChange(int visibility) {
|
||||
eventEmitter.onControlsVisibilityChange.invoke(visibility == View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Setting the player for the playerControlView
|
||||
playerControlView.setPlayer(player);
|
||||
playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container);
|
||||
|
||||
// Invoking onClick event for exoplayerView
|
||||
exoPlayerView.setOnClickListener((View v) -> {
|
||||
if (!isPlayingAd()) {
|
||||
togglePlayerControlVisibility();
|
||||
}
|
||||
exoPlayerView.setPlayer(player);
|
||||
|
||||
exoPlayerView.setControllerVisibilityListener(visibility -> {
|
||||
boolean isVisible = visibility == View.VISIBLE;
|
||||
eventEmitter.onControlsVisibilityChange.invoke(isVisible);
|
||||
});
|
||||
|
||||
//Handling the playButton click event
|
||||
ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
|
||||
|
||||
playButton.setOnClickListener((View v) -> {
|
||||
if (player != null && player.getPlaybackState() == Player.STATE_ENDED) {
|
||||
player.seekTo(0);
|
||||
}
|
||||
setPausedModifier(false);
|
||||
exoPlayerView.setFullscreenButtonClickListener(isFullscreen -> {
|
||||
setFullscreen(!this.isFullscreen);
|
||||
});
|
||||
|
||||
//Handling the rewind and forward button click events
|
||||
ImageButton exoRewind = playerControlView.findViewById(R.id.exo_rew);
|
||||
ImageButton exoForward = playerControlView.findViewById(R.id.exo_ffwd);
|
||||
exoRewind.setOnClickListener((View v) -> {
|
||||
seekTo(player.getCurrentPosition() - controlsConfig.getSeekIncrementMS());
|
||||
});
|
||||
|
||||
exoForward.setOnClickListener((View v) -> {
|
||||
seekTo(player.getCurrentPosition() + controlsConfig.getSeekIncrementMS());
|
||||
});
|
||||
|
||||
//Handling the pauseButton click event
|
||||
ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);
|
||||
pauseButton.setOnClickListener((View v) ->
|
||||
setPausedModifier(true)
|
||||
);
|
||||
|
||||
//Handling the settingButton click event
|
||||
final ImageButton settingButton = playerControlView.findViewById(R.id.exo_settings);
|
||||
settingButton.setOnClickListener(v -> openSettings());
|
||||
|
||||
//Handling the fullScreenButton click event
|
||||
final ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
|
||||
fullScreenButton.setOnClickListener(v -> setFullscreen(!isFullscreen));
|
||||
updateFullScreenButtonVisibility();
|
||||
refreshControlsStyles();
|
||||
|
||||
// Invoking onPlaybackStateChanged and onPlayWhenReadyChanged events for Player
|
||||
eventListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onPlaybackStateChanged(int playbackState) {
|
||||
View playButton = playerControlView.findViewById(R.id.exo_play);
|
||||
View pauseButton = playerControlView.findViewById(R.id.exo_pause);
|
||||
if (playButton != null && playButton.getVisibility() == GONE) {
|
||||
playButton.setVisibility(INVISIBLE);
|
||||
}
|
||||
if (pauseButton != null && pauseButton.getVisibility() == GONE) {
|
||||
pauseButton.setVisibility(INVISIBLE);
|
||||
}
|
||||
|
||||
reLayout(playPauseControlContainer);
|
||||
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
|
||||
player.removeListener(eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
||||
reLayout(playPauseControlContainer);
|
||||
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
|
||||
player.removeListener(eventListener);
|
||||
}
|
||||
};
|
||||
player.addListener(eventListener);
|
||||
updateControllerConfig();
|
||||
}
|
||||
|
||||
private void updateControllerConfig() {
|
||||
if (exoPlayerView == null) return;
|
||||
|
||||
exoPlayerView.setControllerShowTimeoutMs(5000);
|
||||
|
||||
exoPlayerView.setControllerAutoShow(true);
|
||||
exoPlayerView.setControllerHideOnTouch(true);
|
||||
|
||||
updateControllerVisibility();
|
||||
}
|
||||
|
||||
private void updateControllerVisibility() {
|
||||
if (exoPlayerView == null) return;
|
||||
|
||||
exoPlayerView.setUseController(!controlsConfig.getHideFullscreen());
|
||||
}
|
||||
|
||||
private void openSettings() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
|
||||
builder.setTitle(R.string.settings);
|
||||
@@ -558,21 +497,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding Player control to the frame layout
|
||||
*/
|
||||
private void addPlayerControl() {
|
||||
if (playerControlView == null) return;
|
||||
LayoutParams layoutParams = new LayoutParams(
|
||||
LayoutParams.MATCH_PARENT,
|
||||
LayoutParams.MATCH_PARENT);
|
||||
playerControlView.setLayoutParams(layoutParams);
|
||||
int indexOfPC = indexOfChild(playerControlView);
|
||||
if (indexOfPC != -1) {
|
||||
removeViewAt(indexOfPC);
|
||||
}
|
||||
addView(playerControlView, 1, layoutParams);
|
||||
reLayout(playerControlView);
|
||||
updateControllerConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,81 +515,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
|
||||
private void refreshControlsStyles() {
|
||||
if (playerControlView == null || player == null || !controls) return;
|
||||
updateLiveContent();
|
||||
updatePlayPauseButtons();
|
||||
updateButtonVisibility(controlsConfig.getHideForward(), R.id.exo_ffwd);
|
||||
updateButtonVisibility(controlsConfig.getHideRewind(), R.id.exo_rew);
|
||||
updateButtonVisibility(controlsConfig.getHideNext(), R.id.exo_next);
|
||||
updateButtonVisibility(controlsConfig.getHidePrevious(), R.id.exo_prev);
|
||||
updateViewVisibility(playerControlView.findViewById(R.id.exo_fullscreen), controlsConfig.getHideFullscreen(), GONE);
|
||||
updateViewVisibility(playerControlView.findViewById(R.id.exo_position), controlsConfig.getHidePosition(), GONE);
|
||||
updateViewVisibility(playerControlView.findViewById(R.id.exo_progress), controlsConfig.getHideSeekBar(), INVISIBLE);
|
||||
updateViewVisibility(playerControlView.findViewById(R.id.exo_duration), controlsConfig.getHideDuration(), GONE);
|
||||
updateViewVisibility(playerControlView.findViewById(R.id.exo_settings), controlsConfig.getHideSettingButton(), GONE );
|
||||
if (exoPlayerView == null || player == null || !controls) return;
|
||||
updateControllerVisibility();
|
||||
}
|
||||
|
||||
private void updateLiveContent() {
|
||||
LinearLayout exoLiveContainer = playerControlView.findViewById(R.id.exo_live_container);
|
||||
TextView exoLiveLabel = playerControlView.findViewById(R.id.exo_live_label);
|
||||
|
||||
boolean isLive = false;
|
||||
Timeline timeline = player.getCurrentTimeline();
|
||||
|
||||
// Determine if the content is live
|
||||
if (!timeline.isEmpty()) {
|
||||
Timeline.Window window = new Timeline.Window();
|
||||
timeline.getWindow(player.getCurrentMediaItemIndex(), window);
|
||||
isLive = window.isLive();
|
||||
}
|
||||
|
||||
if (isLive && controlsConfig.getLiveLabel() != null) {
|
||||
exoLiveLabel.setText(controlsConfig.getLiveLabel());
|
||||
exoLiveContainer.setVisibility(VISIBLE);
|
||||
} else {
|
||||
exoLiveContainer.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlayPauseButtons() {
|
||||
final ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
|
||||
final ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause);
|
||||
|
||||
if (controlsConfig.getHidePlayPause()) {
|
||||
playPauseControlContainer.setAlpha(0);
|
||||
playButton.setClickable(false);
|
||||
pauseButton.setClickable(false);
|
||||
} else {
|
||||
playPauseControlContainer.setAlpha(1.0f);
|
||||
playButton.setClickable(true);
|
||||
pauseButton.setClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtonVisibility(boolean hide, int buttonID) {
|
||||
ImageButton button = playerControlView.findViewById(buttonID);
|
||||
if (hide) {
|
||||
button.setImageAlpha(0);
|
||||
button.setClickable(false);
|
||||
} else {
|
||||
button.setImageAlpha(255);
|
||||
button.setClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateViewVisibility(View view, boolean hide, int hideVisibility) {
|
||||
if (hide) {
|
||||
view.setVisibility(hideVisibility);
|
||||
} else if (view.getVisibility() == hideVisibility) {
|
||||
view.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Note: The following methods for live content and button visibility are no longer needed
|
||||
// as PlayerView handles controls automatically. Some functionality may need to be
|
||||
// reimplemented using PlayerView's APIs if custom behavior is required.
|
||||
|
||||
private void reLayoutControls() {
|
||||
reLayout(exoPlayerView);
|
||||
reLayout(playerControlView);
|
||||
}
|
||||
|
||||
/// returns true is adaptive bitrate shall be used
|
||||
@@ -791,7 +652,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
if (playerNeedsSource) {
|
||||
// Will force display of shutter view if needed
|
||||
exoPlayerView.updateShutterViewVisibility();
|
||||
exoPlayerView.invalidateAspectRatio();
|
||||
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
|
||||
ExecutorService es = Executors.newSingleThreadExecutor();
|
||||
@@ -875,7 +735,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
||||
}
|
||||
|
||||
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
|
||||
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
|
||||
|
||||
player = new ExoPlayer.Builder(getContext(), renderersFactory)
|
||||
.setTrackSelector(self.trackSelector)
|
||||
@@ -910,7 +770,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
if (adProps != null && uri != null) {
|
||||
Uri adTagUrl = adProps.getAdTagUrl();
|
||||
if (adTagUrl != null) {
|
||||
exoPlayerView.showAds();
|
||||
// Create an AdsLoader.
|
||||
ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
|
||||
.Builder(themedReactContext)
|
||||
@@ -926,16 +785,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
adsLoader.setPlayer(player);
|
||||
if (adsLoader != null) {
|
||||
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
|
||||
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
|
||||
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
|
||||
DataSpec adTagDataSpec = new DataSpec(adTagUrl);
|
||||
return new AdsMediaSource(videoSource,
|
||||
adTagDataSpec,
|
||||
ImmutableList.of(uri, adTagUrl),
|
||||
mediaSourceFactory, adsLoader, exoPlayerView);
|
||||
mediaSourceFactory, adsLoader, exoPlayerView.getPlayerView());
|
||||
}
|
||||
}
|
||||
}
|
||||
exoPlayerView.hideAds();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -981,7 +840,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
|
||||
return;
|
||||
}
|
||||
// init source to manage ads and external text tracks
|
||||
// init source to manage ads (external text tracks are now handled in MediaItem)
|
||||
MediaSource videoSource = buildMediaSource(runningSource.getUri(),
|
||||
runningSource.getExtension(),
|
||||
drmSessionManager,
|
||||
@@ -990,12 +849,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
MediaSource mediaSourceWithAds = initializeAds(videoSource, runningSource);
|
||||
MediaSource mediaSource = Objects.requireNonNullElse(mediaSourceWithAds, videoSource);
|
||||
|
||||
MediaSource subtitlesSource = buildTextSource();
|
||||
if (subtitlesSource != null) {
|
||||
MediaSource[] mediaSourceArray = {mediaSource, subtitlesSource};
|
||||
mediaSource = new MergingMediaSource(mediaSourceArray);
|
||||
}
|
||||
|
||||
// wait for player to be set
|
||||
while (player == null) {
|
||||
try {
|
||||
@@ -1151,6 +1004,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
if (customMetadata != null) {
|
||||
mediaItemBuilder.setMediaMetadata(customMetadata);
|
||||
}
|
||||
|
||||
// Add external subtitles to MediaItem
|
||||
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = buildSubtitleConfigurations();
|
||||
if (subtitleConfigurations != null) {
|
||||
mediaItemBuilder.setSubtitleConfigurations(subtitleConfigurations);
|
||||
}
|
||||
|
||||
if (source.getAdsProps() != null) {
|
||||
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
||||
if (adTagUrl != null) {
|
||||
@@ -1285,29 +1145,59 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MediaSource buildTextSource() {
|
||||
if (source.getSideLoadedTextTracks() == null) {
|
||||
private List<MediaItem.SubtitleConfiguration> buildSubtitleConfigurations() {
|
||||
if (source.getSideLoadedTextTracks() == null || source.getSideLoadedTextTracks().getTracks().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = new ArrayList<>();
|
||||
int trackIndex = 0;
|
||||
|
||||
for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
|
||||
MediaItem.SubtitleConfiguration subtitleConfiguration = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
|
||||
.setMimeType(track.getType())
|
||||
.setLanguage(track.getLanguage())
|
||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.setLabel(track.getTitle())
|
||||
.build();
|
||||
subtitleConfigurations.add(subtitleConfiguration);
|
||||
try {
|
||||
// Create a more descriptive ID that PlayerView can use
|
||||
String trackId = "external-subtitle-" + trackIndex;
|
||||
String label = track.getTitle();
|
||||
if (label == null || label.isEmpty()) {
|
||||
label = "External " + (trackIndex + 1);
|
||||
if (track.getLanguage() != null && !track.getLanguage().isEmpty()) {
|
||||
label += " (" + track.getLanguage() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
|
||||
.setId(trackId)
|
||||
.setMimeType(track.getType())
|
||||
.setLabel(label)
|
||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE);
|
||||
|
||||
// Set language if available
|
||||
if (track.getLanguage() != null && !track.getLanguage().isEmpty()) {
|
||||
configBuilder.setLanguage(track.getLanguage());
|
||||
}
|
||||
|
||||
// Set selection flags - make first track default if no specific track is selected
|
||||
if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) {
|
||||
configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT);
|
||||
} else {
|
||||
configBuilder.setSelectionFlags(0);
|
||||
}
|
||||
|
||||
MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build();
|
||||
subtitleConfigurations.add(subtitleConfiguration);
|
||||
|
||||
DebugLog.d(TAG, "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")");
|
||||
trackIndex++;
|
||||
} catch (Exception e) {
|
||||
DebugLog.e(TAG, "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
MediaItem subtitlesMediaItem = new MediaItem.Builder()
|
||||
.setUri(source.getUri())
|
||||
.setSubtitleConfigurations(subtitleConfigurations).build();
|
||||
if (!subtitleConfigurations.isEmpty()) {
|
||||
DebugLog.d(TAG, "Built " + subtitleConfigurations.size() + " external subtitle configurations");
|
||||
}
|
||||
|
||||
return new DefaultMediaSourceFactory(mediaDataSourceFactory).createMediaSource(subtitlesMediaItem);
|
||||
return subtitleConfigurations.isEmpty() ? null : subtitleConfigurations;
|
||||
}
|
||||
|
||||
private void releasePlayer() {
|
||||
@@ -1530,9 +1420,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
selectTrackWhenReady = false;
|
||||
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
||||
}
|
||||
// Setting the visibility for the playerControlView
|
||||
if (playerControlView != null) {
|
||||
playerControlView.show();
|
||||
// Setting the visibility for the player controls
|
||||
if (exoPlayerView != null) {
|
||||
exoPlayerView.showController();
|
||||
}
|
||||
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
|
||||
break;
|
||||
@@ -1598,7 +1488,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
||||
audioTracks, textTracks, videoTracks, trackId );
|
||||
|
||||
|
||||
updateSubtitleButtonVisibility();
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1607,6 +1498,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
||||
audioTracks, textTracks, videoTracks, trackId);
|
||||
|
||||
updateSubtitleButtonVisibility();
|
||||
refreshControlsStyles();
|
||||
}
|
||||
}
|
||||
@@ -1620,7 +1513,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
private ArrayList<Track> getAudioTrackInfo() {
|
||||
ArrayList<Track> audioTracks = new ArrayList<>();
|
||||
if (trackSelector == null) {
|
||||
// Likely player is unmounting so no audio tracks are available anymore
|
||||
return audioTracks;
|
||||
}
|
||||
|
||||
@@ -1631,15 +1523,25 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
|
||||
TrackSelection selection = selectionArray.get( C.TRACK_TYPE_AUDIO );
|
||||
TrackSelection selection = selectionArray.get(C.TRACK_TYPE_AUDIO);
|
||||
|
||||
for (int i = 0; i < groups.length; ++i) {
|
||||
TrackGroup group = groups.get(i);
|
||||
|
||||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
Format format = group.getFormat(0);
|
||||
Track audioTrack = exoplayerTrackToGenericTrack(format, i, selection, group);
|
||||
|
||||
// Check if this specific group is the currently selected one
|
||||
boolean isSelected = false;
|
||||
if (selection != null && selection.getTrackGroup() == group) {
|
||||
isSelected = true;
|
||||
}
|
||||
|
||||
Track audioTrack = exoplayerTrackToGenericTrack(format, groupIndex, selection, group);
|
||||
audioTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||
audioTrack.setSelected(isSelected);
|
||||
audioTracks.add(audioTrack);
|
||||
}
|
||||
|
||||
return audioTracks;
|
||||
}
|
||||
|
||||
@@ -1765,20 +1667,113 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
if (trackSelector == null) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (info == null || index == C.INDEX_UNSET) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
|
||||
TrackSelection selection = selectionArray.get( C.TRACK_TYPE_VIDEO );
|
||||
TrackSelection selection = selectionArray.get(C.TRACK_TYPE_TEXT);
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
|
||||
for (int i = 0; i < groups.length; ++i) {
|
||||
TrackGroup group = groups.get(i);
|
||||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
Format format = group.getFormat(trackIndex);
|
||||
Track textTrack = exoplayerTrackToGenericTrack(format, trackIndex, selection, group);
|
||||
|
||||
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
||||
boolean isSelected = isTrackSelected(selection, group, trackIndex);
|
||||
|
||||
textTrack.setIndex(textTracks.size());
|
||||
|
||||
if (textTrack.getTitle() == null || textTrack.getTitle().isEmpty()) {
|
||||
if (isExternal) {
|
||||
textTrack.setTitle("External " + (trackIndex + 1));
|
||||
} else {
|
||||
textTrack.setTitle("Track " + (textTracks.size() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
textTracks.add(textTrack);
|
||||
}
|
||||
}
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
private ArrayList<Track> getBasicAudioTrackInfo() {
|
||||
ArrayList<Track> tracks = new ArrayList<>();
|
||||
if (trackSelector == null) {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
int index = getTrackRendererIndex(C.TRACK_TYPE_AUDIO);
|
||||
if (info == null || index == C.INDEX_UNSET) {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
|
||||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
Format format = group.getFormat(0);
|
||||
Track textTrack = exoplayerTrackToGenericTrack(format, i, selection, group);
|
||||
textTracks.add(textTrack);
|
||||
|
||||
// Create track without trying to determine selection status
|
||||
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));
|
||||
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);
|
||||
|
||||
tracks.add(track);
|
||||
}
|
||||
|
||||
DebugLog.d(TAG, "getBasicAudioTrackInfo: returning " + tracks.size() + " audio tracks (no selection status)");
|
||||
return tracks;
|
||||
}
|
||||
|
||||
private ArrayList<Track> getBasicTextTrackInfo() {
|
||||
ArrayList<Track> textTracks = new ArrayList<>();
|
||||
if (trackSelector == null) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (info == null || index == C.INDEX_UNSET) {
|
||||
return textTracks;
|
||||
}
|
||||
|
||||
TrackGroupArray groups = info.getTrackGroups(index);
|
||||
|
||||
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
Format format = group.getFormat(trackIndex);
|
||||
|
||||
Track textTrack = new Track();
|
||||
textTrack.setIndex(textTracks.size());
|
||||
if (format.sampleMimeType != null) textTrack.setMimeType(format.sampleMimeType);
|
||||
if (format.language != null) textTrack.setLanguage(format.language);
|
||||
|
||||
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
||||
|
||||
if (format.label != null && !format.label.isEmpty()) {
|
||||
textTrack.setTitle(format.label);
|
||||
} else if (isExternal) {
|
||||
textTrack.setTitle("External " + (trackIndex + 1));
|
||||
} else {
|
||||
textTrack.setTitle("Track " + (textTracks.size() + 1));
|
||||
}
|
||||
|
||||
textTrack.setSelected(false); // Don't report selection status - let PlayerView handle it
|
||||
textTracks.add(textTrack);
|
||||
}
|
||||
}
|
||||
return textTracks;
|
||||
}
|
||||
@@ -1835,9 +1830,70 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(@NonNull Tracks tracks) {
|
||||
eventEmitter.onTextTracks.invoke(getTextTrackInfo());
|
||||
eventEmitter.onAudioTracks.invoke(getAudioTrackInfo());
|
||||
eventEmitter.onVideoTracks.invoke(getVideoTrackInfo());
|
||||
DebugLog.d(TAG, "onTracksChanged called - updating track information, controls=" + controls);
|
||||
|
||||
if (controls) {
|
||||
ArrayList<Track> textTracks = getBasicTextTrackInfo();
|
||||
ArrayList<Track> audioTracks = getBasicAudioTrackInfo();
|
||||
ArrayList<VideoTrack> videoTracks = getVideoTrackInfo();
|
||||
|
||||
eventEmitter.onTextTracks.invoke(textTracks);
|
||||
eventEmitter.onAudioTracks.invoke(audioTracks);
|
||||
eventEmitter.onVideoTracks.invoke(videoTracks);
|
||||
} else {
|
||||
ArrayList<Track> textTracks = getTextTrackInfo();
|
||||
ArrayList<Track> audioTracks = getAudioTrackInfo();
|
||||
ArrayList<VideoTrack> videoTracks = getVideoTrackInfo();
|
||||
|
||||
eventEmitter.onTextTracks.invoke(textTracks);
|
||||
eventEmitter.onAudioTracks.invoke(audioTracks);
|
||||
eventEmitter.onVideoTracks.invoke(videoTracks);
|
||||
int selectedAudioTracks = 0;
|
||||
for (Track track : audioTracks) {
|
||||
if (track.isSelected()) {
|
||||
selectedAudioTracks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSubtitleButtonVisibility();
|
||||
}
|
||||
|
||||
|
||||
private boolean hasBuiltInTextTracks() {
|
||||
if (player == null || trackSelector == null) return false;
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (info == null) return false;
|
||||
|
||||
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex == C.INDEX_UNSET) return false;
|
||||
|
||||
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
||||
|
||||
// Check if any groups have tracks that are NOT external subtitles
|
||||
for (int i = 0; i < groups.length; i++) {
|
||||
TrackGroup group = groups.get(i);
|
||||
for (int j = 0; j < group.length; j++) {
|
||||
Format format = group.getFormat(j);
|
||||
// If track ID doesn't start with "external-subtitle-", it's built-in
|
||||
if (format.id == null || !format.id.startsWith("external-subtitle-")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateSubtitleButtonVisibility() {
|
||||
if (exoPlayerView == null) return;
|
||||
|
||||
boolean hasTextTracks = (source.getSideLoadedTextTracks() != null &&
|
||||
!source.getSideLoadedTextTracks().getTracks().isEmpty()) ||
|
||||
hasBuiltInTextTracks();
|
||||
|
||||
exoPlayerView.setShowSubtitleButton(hasTextTracks);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1988,7 +2044,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
player.clearMediaItems();
|
||||
}
|
||||
}
|
||||
exoPlayerView.hideAds();
|
||||
|
||||
this.source = new Source();
|
||||
this.mediaDataSourceFactory = null;
|
||||
clearResumePosition();
|
||||
@@ -2029,6 +2085,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
|
||||
public void disableTrack(int rendererIndex) {
|
||||
if (trackSelector == null) return;
|
||||
|
||||
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
||||
.buildUpon()
|
||||
.setRendererDisabled(rendererIndex, true)
|
||||
@@ -2036,12 +2094,89 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
trackSelector.setParameters(disableParameters);
|
||||
}
|
||||
|
||||
private void selectTextTrackInternal(String type, String value) {
|
||||
if (player == null || trackSelector == null) return;
|
||||
|
||||
DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value);
|
||||
|
||||
DefaultTrackSelector.Parameters.Builder parametersBuilder = trackSelector.getParameters().buildUpon();
|
||||
|
||||
if ("disabled".equals(type) || value == null) {
|
||||
parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true);
|
||||
} else {
|
||||
parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false);
|
||||
|
||||
parametersBuilder.clearOverridesOfType(C.TRACK_TYPE_TEXT);
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (info != null) {
|
||||
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (textRendererIndex != C.INDEX_UNSET) {
|
||||
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
||||
boolean trackFound = false;
|
||||
|
||||
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
||||
TrackGroup group = groups.get(groupIndex);
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
Format format = group.getFormat(trackIndex);
|
||||
|
||||
boolean isMatch = false;
|
||||
if ("language".equals(type) && format.language != null && format.language.equals(value)) {
|
||||
isMatch = true;
|
||||
} else if ("title".equals(type) && format.label != null && format.label.equals(value)) {
|
||||
isMatch = true;
|
||||
} else if ("index".equals(type)) {
|
||||
int targetIndex = ReactBridgeUtils.safeParseInt(value, -1);
|
||||
if (targetIndex == trackIndex) {
|
||||
isMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMatch) {
|
||||
TrackSelectionOverride override = new TrackSelectionOverride(group,
|
||||
java.util.Arrays.asList(trackIndex));
|
||||
parametersBuilder.addOverride(override);
|
||||
trackFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (trackFound) break;
|
||||
}
|
||||
|
||||
if (!trackFound) {
|
||||
DebugLog.w(TAG, "Text track not found for type=" + type + ", value=" + value +
|
||||
". Keeping current selection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
trackSelector.setParameters(parametersBuilder.build());
|
||||
|
||||
// Give PlayerView time to update its controls
|
||||
mainHandler.postDelayed(() -> {
|
||||
if (exoPlayerView != null) {
|
||||
updateSubtitleButtonVisibility();
|
||||
}
|
||||
}, 100);
|
||||
} catch (Exception e) {
|
||||
DebugLog.e(TAG, "Error setting text track parameters: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedTrack(int trackType, String type, String value) {
|
||||
if (player == null) return;
|
||||
if (player == null || trackSelector == null) return;
|
||||
|
||||
if (controls) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rendererIndex = getTrackRendererIndex(trackType);
|
||||
if (rendererIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
|
||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||
if (info == null) {
|
||||
return;
|
||||
@@ -2146,7 +2281,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
if (captioningManager != null && captioningManager.isEnabled()) {
|
||||
groupIndex = getGroupIndexForDefaultLocale(groups);
|
||||
}
|
||||
} else if (rendererIndex == C.TRACK_TYPE_AUDIO) { // Audio default
|
||||
} else if (trackType == C.TRACK_TYPE_AUDIO) { // Audio default
|
||||
groupIndex = getGroupIndexForDefaultLocale(groups);
|
||||
}
|
||||
|
||||
@@ -2186,23 +2321,42 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
return;
|
||||
}
|
||||
|
||||
TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);
|
||||
try {
|
||||
TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);
|
||||
|
||||
DefaultTrackSelector.Parameters.Builder selectionParameters = trackSelector.getParameters()
|
||||
.buildUpon()
|
||||
.setExceedAudioConstraintsIfNecessary(true)
|
||||
.setExceedRendererCapabilitiesIfNecessary(true)
|
||||
.setExceedVideoConstraintsIfNecessary(true)
|
||||
.setRendererDisabled(rendererIndex, false)
|
||||
.clearOverridesOfType(selectionOverride.getType());
|
||||
DefaultTrackSelector.Parameters.Builder selectionParameters = trackSelector.getParameters()
|
||||
.buildUpon()
|
||||
.setExceedAudioConstraintsIfNecessary(true)
|
||||
.setExceedRendererCapabilitiesIfNecessary(true)
|
||||
.setExceedVideoConstraintsIfNecessary(true)
|
||||
.setRendererDisabled(rendererIndex, false);
|
||||
|
||||
if (trackType == C.TRACK_TYPE_VIDEO && isUsingVideoABR()) {
|
||||
selectionParameters.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate);
|
||||
} else {
|
||||
selectionParameters.addOverride(selectionOverride);
|
||||
// Clear existing overrides for this track type to avoid conflicts
|
||||
// But be careful with audio tracks - don't clear unless explicitly selecting a different track
|
||||
if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) {
|
||||
selectionParameters.clearOverridesOfType(selectionOverride.getType());
|
||||
}
|
||||
|
||||
if (trackType == C.TRACK_TYPE_VIDEO && isUsingVideoABR()) {
|
||||
selectionParameters.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate);
|
||||
} else {
|
||||
selectionParameters.addOverride(selectionOverride);
|
||||
}
|
||||
|
||||
// For audio tracks, ensure audio renderer stays enabled
|
||||
if (trackType == C.TRACK_TYPE_AUDIO) {
|
||||
selectionParameters.setForceHighestSupportedBitrate(false);
|
||||
selectionParameters.setForceLowestBitrate(false);
|
||||
DebugLog.d(TAG, "Audio track selection: group=" + groupIndex + ", tracks=" + tracks +
|
||||
", override=" + selectionOverride);
|
||||
}
|
||||
|
||||
trackSelector.setParameters(selectionParameters.build());
|
||||
DebugLog.d(TAG, "Applied track selection for type: " + trackType + ", group: " + groupIndex);
|
||||
} catch (Exception e) {
|
||||
DebugLog.e(TAG, "Error applying track selection: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
trackSelector.setParameters(selectionParameters.build());
|
||||
}
|
||||
|
||||
private boolean isFormatSupported(Format format) {
|
||||
@@ -2252,13 +2406,17 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
public void setSelectedAudioTrack(String type, String value) {
|
||||
audioTrackType = type;
|
||||
audioTrackValue = value;
|
||||
setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
|
||||
|
||||
if (!controls && player != null && trackSelector != null) {
|
||||
setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedTextTrack(String type, String value) {
|
||||
textTrackType = type;
|
||||
textTrackValue = value;
|
||||
setSelectedTrack(C.TRACK_TYPE_TEXT, textTrackType, textTrackValue);
|
||||
|
||||
selectTextTrackInternal(type, value);
|
||||
}
|
||||
|
||||
public void setPausedModifier(boolean paused) {
|
||||
@@ -2452,18 +2610,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
return preventsDisplaySleepDuringVideoPlayback;
|
||||
}
|
||||
|
||||
private void updateFullScreenButtonVisibility() {
|
||||
if (playerControlView != null) {
|
||||
final ImageButton fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
|
||||
//Handling the fullScreenButton click event
|
||||
if (isFullscreen && fullScreenPlayerView != null && !fullScreenPlayerView.isShowing()) {
|
||||
fullScreenButton.setVisibility(GONE);
|
||||
} else {
|
||||
fullScreenButton.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDisableDisconnectError(boolean disableDisconnectError) {
|
||||
this.disableDisconnectError = disableDisconnectError;
|
||||
}
|
||||
@@ -2480,7 +2626,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
}
|
||||
|
||||
if (isFullscreen) {
|
||||
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, playerControlView, new OnBackPressedCallback(true) {
|
||||
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
setFullscreen(false);
|
||||
@@ -2504,12 +2650,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
eventEmitter.onVideoFullscreenPlayerDidDismiss.invoke();
|
||||
});
|
||||
}
|
||||
// need to be done at the end to avoid hiding fullscreen control button when fullScreenPlayerView is shown
|
||||
updateFullScreenButtonVisibility();
|
||||
}
|
||||
|
||||
public void setHideShutterView(boolean hideShutterView) {
|
||||
exoPlayerView.setHideShutterView(hideShutterView);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2550,14 +2690,17 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||
*/
|
||||
public void setControls(boolean controls) {
|
||||
this.controls = controls;
|
||||
if (exoPlayerView != null) {
|
||||
exoPlayerView.setUseController(controls);
|
||||
// Additional configuration for proper touch handling
|
||||
if (controls) {
|
||||
exoPlayerView.setControllerAutoShow(true);
|
||||
exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide
|
||||
exoPlayerView.setControllerShowTimeoutMs(5000);
|
||||
}
|
||||
}
|
||||
if (controls) {
|
||||
addPlayerControl();
|
||||
updateFullScreenButtonVisibility();
|
||||
} else {
|
||||
int indexOfPC = indexOfChild(playerControlView);
|
||||
if (indexOfPC != -1) {
|
||||
removeViewAt(indexOfPC);
|
||||
}
|
||||
}
|
||||
refreshControlsStyles();
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
private const val PROP_SELECTED_VIDEO_TRACK = "selectedVideoTrack"
|
||||
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
|
||||
private const val PROP_SELECTED_VIDEO_TRACK_VALUE = "value"
|
||||
private const val PROP_HIDE_SHUTTER_VIEW = "hideShutterView"
|
||||
private const val PROP_CONTROLS = "controls"
|
||||
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
|
||||
private const val PROP_SHUTTER_COLOR = "shutterColor"
|
||||
@@ -227,11 +226,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
||||
videoView.setViewType(viewType)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_HIDE_SHUTTER_VIEW, defaultBoolean = false)
|
||||
fun setHideShutterView(videoView: ReactExoplayerView, hideShutterView: Boolean) {
|
||||
videoView.setHideShutterView(hideShutterView)
|
||||
}
|
||||
|
||||
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
|
||||
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
|
||||
videoView.setControls(controls)
|
||||
|
||||
@@ -75,6 +75,10 @@ const BasicExample = () => {
|
||||
useState(false);
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
|
||||
// Add refs to store previous track data for comparison
|
||||
const previousAudioTracksRef = useRef<AudioTrack[]>([]);
|
||||
const previousTextTracksRef = useRef<TextTrack[]>([]);
|
||||
|
||||
const videoRef = useRef<VideoRef>(null);
|
||||
const viewStyle = fullscreen ? styles.fullScreen : styles.halfScreen;
|
||||
const currentSrc = srcList[srcListId];
|
||||
@@ -107,7 +111,36 @@ const BasicExample = () => {
|
||||
|
||||
const onAudioTracks = (data: OnAudioTracksData) => {
|
||||
console.log('onAudioTracks', data);
|
||||
const selectedTrack = data.audioTracks?.find((x: AudioTrack) => {
|
||||
|
||||
// Check if audio tracks have actually changed
|
||||
const currentTracks = data.audioTracks || [];
|
||||
const previousTracks = previousAudioTracksRef.current;
|
||||
|
||||
// Simple comparison - check if tracks array length or selected track changed
|
||||
const tracksChanged =
|
||||
currentTracks.length !== previousTracks.length ||
|
||||
JSON.stringify(
|
||||
currentTracks.map((t) => ({
|
||||
index: t.index,
|
||||
selected: t.selected,
|
||||
language: t.language,
|
||||
})),
|
||||
) !==
|
||||
JSON.stringify(
|
||||
previousTracks.map((t) => ({
|
||||
index: t.index,
|
||||
selected: t.selected,
|
||||
language: t.language,
|
||||
})),
|
||||
);
|
||||
|
||||
if (!tracksChanged) {
|
||||
return; // Skip if tracks haven't changed
|
||||
}
|
||||
|
||||
previousAudioTracksRef.current = currentTracks;
|
||||
|
||||
const selectedTrack = currentTracks.find((x: AudioTrack) => {
|
||||
return x.selected;
|
||||
});
|
||||
let value;
|
||||
@@ -118,7 +151,7 @@ const BasicExample = () => {
|
||||
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = selectedTrack?.title;
|
||||
}
|
||||
setAudioTracks(data.audioTracks);
|
||||
setAudioTracks(currentTracks);
|
||||
setSelectedAudioTrack({
|
||||
type: audioTracksSelectionBy,
|
||||
value: value,
|
||||
@@ -131,11 +164,39 @@ const BasicExample = () => {
|
||||
};
|
||||
|
||||
const onTextTracks = (data: OnTextTracksData) => {
|
||||
const selectedTrack = data.textTracks?.find((x: TextTrack) => {
|
||||
// Check if text tracks have actually changed
|
||||
const currentTracks = data.textTracks || [];
|
||||
const previousTracks = previousTextTracksRef.current;
|
||||
|
||||
// Simple comparison - check if tracks array length or selected track changed
|
||||
const tracksChanged =
|
||||
currentTracks.length !== previousTracks.length ||
|
||||
JSON.stringify(
|
||||
currentTracks.map((t) => ({
|
||||
index: t.index,
|
||||
selected: t.selected,
|
||||
language: t.language,
|
||||
})),
|
||||
) !==
|
||||
JSON.stringify(
|
||||
previousTracks.map((t) => ({
|
||||
index: t.index,
|
||||
selected: t.selected,
|
||||
language: t.language,
|
||||
})),
|
||||
);
|
||||
|
||||
if (!tracksChanged) {
|
||||
return; // Skip if tracks haven't changed
|
||||
}
|
||||
|
||||
previousTextTracksRef.current = currentTracks;
|
||||
|
||||
const selectedTrack = currentTracks.find((x: TextTrack) => {
|
||||
return x?.selected;
|
||||
});
|
||||
|
||||
setTextTracks(data.textTracks);
|
||||
setTextTracks(currentTracks);
|
||||
let value;
|
||||
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||
value = selectedTrack?.index;
|
||||
|
||||
@@ -28,7 +28,7 @@ export const AudioTrackSelector = ({
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={selectedAudioTrack?.value}
|
||||
onValueChange={itemValue => {
|
||||
onValueChange={(itemValue) => {
|
||||
if (itemValue !== 'empty') {
|
||||
console.log('on audio value change ' + itemValue);
|
||||
onValueChange(itemValue);
|
||||
@@ -39,7 +39,7 @@ export const AudioTrackSelector = ({
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{audioTracks.map(track => {
|
||||
{audioTracks.map((track) => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +53,9 @@ export const AudioTrackSelector = ({
|
||||
}
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${value} - ${track.selected}`}
|
||||
label={`${
|
||||
track.title || track.language || `Track ${track.index}`
|
||||
}${track.selected ? ' (Selected)' : ''}`}
|
||||
value={`${value}`}
|
||||
key={`${value}`}
|
||||
/>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const TextTrackSelector = ({
|
||||
style={styles.picker}
|
||||
itemStyle={styles.pickerItem}
|
||||
selectedValue={`${selectedTextTrack?.value}`}
|
||||
onValueChange={itemValue => {
|
||||
onValueChange={(itemValue) => {
|
||||
if (itemValue !== 'empty') {
|
||||
onValueChange(itemValue);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export const TextTrackSelector = ({
|
||||
) : (
|
||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||
)}
|
||||
{textTracks.map(track => {
|
||||
{textTracks.map((track) => {
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +50,15 @@ export const TextTrackSelector = ({
|
||||
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||
value = track.title;
|
||||
}
|
||||
return <Picker.Item label={`${value}`} value={value} key={value} />;
|
||||
return (
|
||||
<Picker.Item
|
||||
label={`${
|
||||
track.title || track.language || `Subtitle ${track.index}`
|
||||
}${track.selected ? ' (Selected)' : ''}`}
|
||||
value={value}
|
||||
key={value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Picker>
|
||||
</>
|
||||
|
||||
@@ -120,9 +120,28 @@ export const srcAllPlatformList = [
|
||||
ad: {
|
||||
adTagUrl:
|
||||
'https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=',
|
||||
},
|
||||
},
|
||||
uri: 'https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
|
||||
},
|
||||
{
|
||||
description: 'sintel with subtitles',
|
||||
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
},
|
||||
{
|
||||
description: '(hls) Apple Advanced',
|
||||
uri: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8',
|
||||
type: 'm3u8',
|
||||
},
|
||||
{
|
||||
description: 'elephants dream - multi-language audio & subtitles',
|
||||
uri: 'https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8',
|
||||
type: 'm3u8',
|
||||
},
|
||||
{
|
||||
description: 'tears of steel',
|
||||
uri: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8',
|
||||
type: 'm3u8',
|
||||
},
|
||||
];
|
||||
|
||||
export const srcIosList: SampleVideoSource[] = [];
|
||||
@@ -172,6 +191,25 @@ export const srcAndroidList: SampleVideoSource[] = [
|
||||
uri: 'rtsp://rtspstream:3cfa3c36a9c00f4aa38f3cd35816b287@zephyr.rtsp.stream/movie',
|
||||
type: 'rtsp',
|
||||
},
|
||||
{
|
||||
description: 'sintel with subtitles',
|
||||
uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
|
||||
},
|
||||
{
|
||||
description: '(hls) Apple Advanced',
|
||||
uri: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8',
|
||||
type: 'm3u8',
|
||||
},
|
||||
{
|
||||
description: 'elephants dream - multi-language audio & subtitles',
|
||||
uri: 'https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8',
|
||||
type: 'm3u8',
|
||||
},
|
||||
{
|
||||
description: 'tears of steel',
|
||||
uri: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8',
|
||||
type: 'm3u8',
|
||||
},
|
||||
];
|
||||
|
||||
const platformSrc: SampleVideoSource[] = isAndroid
|
||||
|
||||
@@ -357,11 +357,11 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.ExpoExample;
|
||||
PRODUCT_NAME = "ExpoExample";
|
||||
PRODUCT_NAME = ExpoExample;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "ExpoExample/ExpoExample-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -385,10 +385,10 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.ExpoExample;
|
||||
PRODUCT_NAME = "ExpoExample";
|
||||
PRODUCT_NAME = ExpoExample;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "ExpoExample/ExpoExample-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1";
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -640,7 +640,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
DebugLog("setSrc Stopping playback")
|
||||
return
|
||||
}
|
||||
self.removePlayerLayer()
|
||||
|
||||
// Ensure UI operations are performed on main thread
|
||||
DispatchQueue.main.sync {
|
||||
self.removePlayerLayer()
|
||||
}
|
||||
self._playerObserver.player = nil
|
||||
self._drmManager = nil
|
||||
self._playerObserver.playerItem = nil
|
||||
@@ -1226,14 +1230,18 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) {
|
||||
_controls = controls
|
||||
if _controls {
|
||||
self.removePlayerLayer()
|
||||
self.usePlayerViewController()
|
||||
DispatchQueue.main.async {
|
||||
self.removePlayerLayer()
|
||||
self.usePlayerViewController()
|
||||
}
|
||||
} else {
|
||||
_playerViewController?.view.removeFromSuperview()
|
||||
_playerViewController?.removeFromParent()
|
||||
_playerViewController = nil
|
||||
_playerObserver.playerViewController = nil
|
||||
self.usePlayerLayer()
|
||||
DispatchQueue.main.async {
|
||||
self._playerViewController?.view.removeFromSuperview()
|
||||
self._playerViewController?.removeFromParent()
|
||||
self._playerViewController = nil
|
||||
self._playerObserver.playerViewController = nil
|
||||
self.usePlayerLayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user