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
|
package com.brentvatne.exoplayer
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.graphics.Color
|
||||||
import android.util.TypedValue
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.view.Gravity
|
import android.util.AttributeSet
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
import android.view.TextureView
|
import android.view.TextureView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.View.MeasureSpec
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.content.ContextCompat
|
import android.widget.TextView
|
||||||
import androidx.media3.common.AdViewProvider
|
|
||||||
import androidx.media3.common.C
|
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Tracks
|
import androidx.media3.common.Timeline
|
||||||
import androidx.media3.common.VideoSize
|
|
||||||
import androidx.media3.common.text.Cue
|
|
||||||
import androidx.media3.common.util.Assertions
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
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.ResizeMode
|
||||||
import com.brentvatne.common.api.SubtitleStyle
|
import com.brentvatne.common.api.SubtitleStyle
|
||||||
import com.brentvatne.common.api.ViewType
|
import com.brentvatne.common.api.ViewType
|
||||||
import com.brentvatne.common.toolbox.DebugLog
|
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class ExoPlayerView(private val context: Context) :
|
class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||||
FrameLayout(context, null, 0),
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
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
|
|
||||||
|
|
||||||
private var localStyle = SubtitleStyle()
|
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 {
|
init {
|
||||||
componentListener = ComponentListener()
|
addView(playerView)
|
||||||
|
val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
val aspectRatioParams = LayoutParams(
|
lp.setMargins(16, 16, 16, 16)
|
||||||
LayoutParams.MATCH_PARENT,
|
addView(liveBadge, lp)
|
||||||
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)
|
fun setPlayer(player: ExoPlayer?) {
|
||||||
if (!localStyle.subtitlesFollowVideo) {
|
val currentPlayer = playerView.player
|
||||||
addViewInLayout(subtitleLayout, 1, layoutParams)
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearVideoView() {
|
playerView.player = player
|
||||||
when (val view = surfaceView) {
|
|
||||||
is TextureView -> player?.clearVideoTextureView(view)
|
|
||||||
|
|
||||||
is SurfaceView -> player?.clearVideoSurfaceView(view)
|
if (player != null) {
|
||||||
|
player.addListener(playerListener)
|
||||||
else -> {
|
// Set the surface view for the new player
|
||||||
Log.w(
|
when (surfaceView) {
|
||||||
"clearVideoView",
|
is SurfaceView -> player.setVideoSurfaceView(surfaceView as SurfaceView)
|
||||||
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
|
is TextureView -> player.setVideoTextureView(surfaceView as TextureView)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setVideoView() {
|
fun getPlayerView(): PlayerView = playerView
|
||||||
when (val view = surfaceView) {
|
|
||||||
is TextureView -> player?.setVideoTextureView(view)
|
|
||||||
|
|
||||||
is SurfaceView -> player?.setVideoSurfaceView(view)
|
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
||||||
|
playerView.resizeMode = when (resizeMode) {
|
||||||
else -> {
|
ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||||
Log.w(
|
ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||||
"setVideoView",
|
ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
"Unexpected surfaceView type: ${surfaceView?.javaClass?.name}"
|
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) {
|
fun setSubtitleStyle(style: SubtitleStyle) {
|
||||||
// ensure we reset subtitle style before reapplying it
|
playerView.subtitleView?.let { subtitleView ->
|
||||||
subtitleLayout.setUserDefaultStyle()
|
// Reset to defaults
|
||||||
subtitleLayout.setUserDefaultTextSize()
|
subtitleView.setUserDefaultStyle()
|
||||||
|
subtitleView.setUserDefaultTextSize()
|
||||||
|
|
||||||
|
// Apply custom styling
|
||||||
if (style.fontSize > 0) {
|
if (style.fontSize > 0) {
|
||||||
subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
||||||
}
|
}
|
||||||
subtitleLayout.setPadding(
|
|
||||||
|
subtitleView.setPadding(
|
||||||
style.paddingLeft,
|
style.paddingLeft,
|
||||||
style.paddingTop,
|
style.paddingTop,
|
||||||
style.paddingTop,
|
style.paddingRight,
|
||||||
style.paddingBottom
|
style.paddingBottom
|
||||||
)
|
)
|
||||||
|
|
||||||
if (style.opacity != 0.0f) {
|
if (style.opacity != 0.0f) {
|
||||||
subtitleLayout.alpha = style.opacity
|
subtitleView.alpha = style.opacity
|
||||||
subtitleLayout.visibility = View.VISIBLE
|
subtitleView.visibility = android.view.View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
subtitleLayout.visibility = View.GONE
|
subtitleView.visibility = android.view.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)
|
|
||||||
}
|
|
||||||
requestLayout()
|
|
||||||
}
|
}
|
||||||
localStyle = style
|
localStyle = style
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShutterColor(color: Int) {
|
fun setShutterColor(color: Int) {
|
||||||
shutterView.setBackgroundColor(color)
|
playerView.setShutterBackgroundColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSurfaceView(@ViewType.ViewType viewType: Int) {
|
fun updateSurfaceView(viewType: Int) {
|
||||||
this.viewType = viewType
|
val currentSurfaceView = surfaceView
|
||||||
var viewNeedRefresh = false
|
val newSurfaceView: View = when (viewType) {
|
||||||
when (viewType) {
|
ViewType.VIEW_TYPE_TEXTURE -> TextureView(context)
|
||||||
ViewType.VIEW_TYPE_SURFACE, ViewType.VIEW_TYPE_SURFACE_SECURE -> {
|
|
||||||
if (surfaceView !is SurfaceView) {
|
|
||||||
surfaceView = SurfaceView(context)
|
|
||||||
viewNeedRefresh = true
|
|
||||||
}
|
|
||||||
(surfaceView as SurfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE)
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewType.VIEW_TYPE_TEXTURE -> {
|
ViewType.VIEW_TYPE_SURFACE_SECURE -> SurfaceView(context).apply {
|
||||||
if (surfaceView !is TextureView) {
|
// Requires API 17+
|
||||||
surfaceView = TextureView(context)
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
viewNeedRefresh = true
|
(this as SurfaceView).setSecure(true)
|
||||||
}
|
|
||||||
// Support opacity properly:
|
|
||||||
(surfaceView as TextureView).isOpaque = false
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
DebugLog.wtf(TAG, "Unexpected texture view type: $viewType")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewNeedRefresh) {
|
else -> SurfaceView(context)
|
||||||
surfaceView?.layoutParams = layoutParams
|
|
||||||
|
|
||||||
if (layout.getChildAt(0) != null) {
|
|
||||||
layout.removeViewAt(0)
|
|
||||||
}
|
|
||||||
layout.addView(surfaceView, 0, layoutParams)
|
|
||||||
|
|
||||||
if (this.player != null) {
|
|
||||||
setVideoView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var adsShown = false
|
if (currentSurfaceView === newSurfaceView) {
|
||||||
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) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.player != null) {
|
|
||||||
this.player!!.removeListener(componentListener)
|
|
||||||
clearVideoView()
|
|
||||||
}
|
|
||||||
this.player = player
|
|
||||||
|
|
||||||
updateShutterViewVisibility()
|
// Remove the old surface view
|
||||||
if (player != null) {
|
if (currentSurfaceView != null) {
|
||||||
setVideoView()
|
removeView(currentSurfaceView)
|
||||||
player.addListener(componentListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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}
|
* React Native (Yoga) can sometimes defer layout passes that are required by
|
||||||
*
|
* PlayerView for its child views (controller overlay, surface view, subtitle view, …).
|
||||||
* @param resizeMode The resize mode.
|
* 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) {
|
private val layoutRunnable = Runnable {
|
||||||
if (layout.resizeMode != resizeMode) {
|
|
||||||
layout.resizeMode = resizeMode
|
|
||||||
post(measureAndLayout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHideShutterView(hideShutterView: Boolean) {
|
|
||||||
this.hideShutterView = hideShutterView
|
|
||||||
updateShutterViewVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val measureAndLayout: Runnable = Runnable {
|
|
||||||
measure(
|
measure(
|
||||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||||
@@ -278,60 +277,9 @@ class ExoPlayerView(private val context: Context) :
|
|||||||
layout(left, top, right, bottom)
|
layout(left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateForCurrentTrackSelections(tracks: Tracks?) {
|
override fun requestLayout() {
|
||||||
if (tracks == null) {
|
super.requestLayout()
|
||||||
return
|
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
|
||||||
}
|
post(layoutRunnable)
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import android.view.ViewGroup
|
|||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
@@ -91,7 +89,6 @@ class FullScreenPlayerView(
|
|||||||
parent?.removeView(exoPlayerView)
|
parent?.removeView(exoPlayerView)
|
||||||
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
|
containerView.addView(exoPlayerView, generateDefaultLayoutParams())
|
||||||
playerControlView?.let {
|
playerControlView?.let {
|
||||||
updateFullscreenButton(playerControlView, true)
|
|
||||||
parent?.removeView(it)
|
parent?.removeView(it)
|
||||||
containerView.addView(it, generateDefaultLayoutParams())
|
containerView.addView(it, generateDefaultLayoutParams())
|
||||||
}
|
}
|
||||||
@@ -104,7 +101,6 @@ class FullScreenPlayerView(
|
|||||||
containerView.removeView(exoPlayerView)
|
containerView.removeView(exoPlayerView)
|
||||||
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
|
parent?.addView(exoPlayerView, generateDefaultLayoutParams())
|
||||||
playerControlView?.let {
|
playerControlView?.let {
|
||||||
updateFullscreenButton(playerControlView, false)
|
|
||||||
containerView.removeView(it)
|
containerView.removeView(it)
|
||||||
parent?.addView(it, generateDefaultLayoutParams())
|
parent?.addView(it, generateDefaultLayoutParams())
|
||||||
}
|
}
|
||||||
@@ -141,20 +137,6 @@ class FullScreenPlayerView(
|
|||||||
androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter
|
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() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
|
if (reactExoplayerView.preventsDisplaySleepDuringVideoPlayback) {
|
||||||
@@ -226,13 +208,7 @@ class FullScreenPlayerView(
|
|||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (controlsConfig.hideNotificationBarOnFullScreenMode) {
|
// Note: Live container adjustment is no longer needed since we're using PlayerView's built-in controls
|
||||||
val liveContainer = playerControlView?.findViewById<LinearLayout?>(com.brentvatne.react.R.id.exo_live_container)
|
// PlayerView handles UI adjustments automatically
|
||||||
liveContainer?.let {
|
|
||||||
val layoutParams = it.layoutParams as LinearLayout.LayoutParams
|
|
||||||
layoutParams.topMargin = 40
|
|
||||||
it.layoutParams = layoutParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,9 +141,10 @@ object PictureInPictureUtil {
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun calcRectHint(playerView: ExoPlayerView): Rect {
|
private fun calcRectHint(playerView: ExoPlayerView): Rect {
|
||||||
val hint = Rect()
|
val hint = Rect()
|
||||||
playerView.surfaceView?.getGlobalVisibleRect(hint)
|
// Use the PlayerView itself since surfaceView is private
|
||||||
|
playerView.getGlobalVisibleRect(hint)
|
||||||
val location = IntArray(2)
|
val location = IntArray(2)
|
||||||
playerView.surfaceView?.getLocationOnScreen(location)
|
playerView.getLocationOnScreen(location)
|
||||||
|
|
||||||
val height = hint.bottom - hint.top
|
val height = hint.bottom - hint.top
|
||||||
hint.top = location[1]
|
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.Id3Frame;
|
||||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
|
import androidx.media3.extractor.metadata.id3.TextInformationFrame;
|
||||||
import androidx.media3.session.MediaSessionService;
|
import androidx.media3.session.MediaSessionService;
|
||||||
import androidx.media3.ui.LegacyPlayerControlView;
|
|
||||||
|
|
||||||
import com.brentvatne.common.api.AdsProps;
|
import com.brentvatne.common.api.AdsProps;
|
||||||
import com.brentvatne.common.api.BufferConfig;
|
import com.brentvatne.common.api.BufferConfig;
|
||||||
@@ -178,8 +177,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
protected final VideoEventEmitter eventEmitter;
|
protected final VideoEventEmitter eventEmitter;
|
||||||
private final ReactExoplayerConfig config;
|
private final ReactExoplayerConfig config;
|
||||||
private DefaultBandwidthMeter bandwidthMeter;
|
private DefaultBandwidthMeter bandwidthMeter;
|
||||||
private LegacyPlayerControlView playerControlView;
|
|
||||||
private View playPauseControlContainer;
|
|
||||||
private Player.Listener eventListener;
|
private Player.Listener eventListener;
|
||||||
|
|
||||||
private ExoPlayerView exoPlayerView;
|
private ExoPlayerView exoPlayerView;
|
||||||
@@ -248,7 +245,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
private float mProgressUpdateInterval = 250.0f;
|
private float mProgressUpdateInterval = 250.0f;
|
||||||
protected boolean playInBackground = false;
|
protected boolean playInBackground = false;
|
||||||
private boolean mReportBandwidth = false;
|
private boolean mReportBandwidth = false;
|
||||||
private boolean controls;
|
private boolean controls = false;
|
||||||
|
|
||||||
private boolean showNotificationControls = false;
|
private boolean showNotificationControls = false;
|
||||||
// \ End props
|
// \ End props
|
||||||
@@ -278,8 +275,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
private void updateProgress() {
|
private void updateProgress() {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
if (playerControlView != null && isPlayingAd() && controls) {
|
if (exoPlayerView != null && isPlayingAd() && controls) {
|
||||||
playerControlView.hide();
|
exoPlayerView.hideController();
|
||||||
}
|
}
|
||||||
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
|
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
|
||||||
long duration = player.getDuration();
|
long duration = player.getDuration();
|
||||||
@@ -423,103 +420,45 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
*/
|
*/
|
||||||
private void togglePlayerControlVisibility() {
|
private void togglePlayerControlVisibility() {
|
||||||
if (player == null) return;
|
if (player == null) return;
|
||||||
reLayoutControls();
|
if (exoPlayerView.isControllerVisible()) {
|
||||||
if (playerControlView.isVisible()) {
|
exoPlayerView.hideController();
|
||||||
playerControlView.hide();
|
|
||||||
} else {
|
} else {
|
||||||
playerControlView.show();
|
exoPlayerView.showController();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializing Player control
|
|
||||||
*/
|
|
||||||
private void initializePlayerControl() {
|
private void initializePlayerControl() {
|
||||||
if (playerControlView == null) {
|
exoPlayerView.setPlayer(player);
|
||||||
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
|
exoPlayerView.setControllerVisibilityListener(visibility -> {
|
||||||
playerControlView.setPlayer(player);
|
boolean isVisible = visibility == View.VISIBLE;
|
||||||
playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container);
|
eventEmitter.onControlsVisibilityChange.invoke(isVisible);
|
||||||
|
|
||||||
// Invoking onClick event for exoplayerView
|
|
||||||
exoPlayerView.setOnClickListener((View v) -> {
|
|
||||||
if (!isPlayingAd()) {
|
|
||||||
togglePlayerControlVisibility();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Handling the playButton click event
|
exoPlayerView.setFullscreenButtonClickListener(isFullscreen -> {
|
||||||
ImageButton playButton = playerControlView.findViewById(R.id.exo_play);
|
setFullscreen(!this.isFullscreen);
|
||||||
|
|
||||||
playButton.setOnClickListener((View v) -> {
|
|
||||||
if (player != null && player.getPlaybackState() == Player.STATE_ENDED) {
|
|
||||||
player.seekTo(0);
|
|
||||||
}
|
|
||||||
setPausedModifier(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Handling the rewind and forward button click events
|
updateControllerConfig();
|
||||||
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);
|
private void updateControllerConfig() {
|
||||||
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
|
if (exoPlayerView == null) return;
|
||||||
player.removeListener(eventListener);
|
|
||||||
|
exoPlayerView.setControllerShowTimeoutMs(5000);
|
||||||
|
|
||||||
|
exoPlayerView.setControllerAutoShow(true);
|
||||||
|
exoPlayerView.setControllerHideOnTouch(true);
|
||||||
|
|
||||||
|
updateControllerVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void updateControllerVisibility() {
|
||||||
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
|
if (exoPlayerView == null) return;
|
||||||
reLayout(playPauseControlContainer);
|
|
||||||
//Remove this eventListener once its executed. since UI will work fine once after the reLayout is done
|
exoPlayerView.setUseController(!controlsConfig.getHideFullscreen());
|
||||||
player.removeListener(eventListener);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
player.addListener(eventListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openSettings() {
|
private void openSettings() {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
|
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
|
||||||
builder.setTitle(R.string.settings);
|
builder.setTitle(R.string.settings);
|
||||||
@@ -558,21 +497,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adding Player control to the frame layout
|
|
||||||
*/
|
|
||||||
private void addPlayerControl() {
|
private void addPlayerControl() {
|
||||||
if (playerControlView == null) return;
|
updateControllerConfig();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -589,81 +515,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshControlsStyles() {
|
private void refreshControlsStyles() {
|
||||||
if (playerControlView == null || player == null || !controls) return;
|
if (exoPlayerView == null || player == null || !controls) return;
|
||||||
updateLiveContent();
|
updateControllerVisibility();
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLiveContent() {
|
// Note: The following methods for live content and button visibility are no longer needed
|
||||||
LinearLayout exoLiveContainer = playerControlView.findViewById(R.id.exo_live_container);
|
// as PlayerView handles controls automatically. Some functionality may need to be
|
||||||
TextView exoLiveLabel = playerControlView.findViewById(R.id.exo_live_label);
|
// reimplemented using PlayerView's APIs if custom behavior is required.
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void reLayoutControls() {
|
private void reLayoutControls() {
|
||||||
reLayout(exoPlayerView);
|
reLayout(exoPlayerView);
|
||||||
reLayout(playerControlView);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns true is adaptive bitrate shall be used
|
/// returns true is adaptive bitrate shall be used
|
||||||
@@ -791,7 +652,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
if (playerNeedsSource) {
|
if (playerNeedsSource) {
|
||||||
// Will force display of shutter view if needed
|
// Will force display of shutter view if needed
|
||||||
exoPlayerView.updateShutterViewVisibility();
|
|
||||||
exoPlayerView.invalidateAspectRatio();
|
exoPlayerView.invalidateAspectRatio();
|
||||||
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
|
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
|
||||||
ExecutorService es = Executors.newSingleThreadExecutor();
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
||||||
@@ -875,7 +735,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
|
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
|
||||||
|
|
||||||
player = new ExoPlayer.Builder(getContext(), renderersFactory)
|
player = new ExoPlayer.Builder(getContext(), renderersFactory)
|
||||||
.setTrackSelector(self.trackSelector)
|
.setTrackSelector(self.trackSelector)
|
||||||
@@ -910,7 +770,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (adProps != null && uri != null) {
|
if (adProps != null && uri != null) {
|
||||||
Uri adTagUrl = adProps.getAdTagUrl();
|
Uri adTagUrl = adProps.getAdTagUrl();
|
||||||
if (adTagUrl != null) {
|
if (adTagUrl != null) {
|
||||||
exoPlayerView.showAds();
|
|
||||||
// Create an AdsLoader.
|
// Create an AdsLoader.
|
||||||
ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
|
ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
|
||||||
.Builder(themedReactContext)
|
.Builder(themedReactContext)
|
||||||
@@ -926,16 +785,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
adsLoader.setPlayer(player);
|
adsLoader.setPlayer(player);
|
||||||
if (adsLoader != null) {
|
if (adsLoader != null) {
|
||||||
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
|
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
|
||||||
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
|
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
|
||||||
DataSpec adTagDataSpec = new DataSpec(adTagUrl);
|
DataSpec adTagDataSpec = new DataSpec(adTagUrl);
|
||||||
return new AdsMediaSource(videoSource,
|
return new AdsMediaSource(videoSource,
|
||||||
adTagDataSpec,
|
adTagDataSpec,
|
||||||
ImmutableList.of(uri, adTagUrl),
|
ImmutableList.of(uri, adTagUrl),
|
||||||
mediaSourceFactory, adsLoader, exoPlayerView);
|
mediaSourceFactory, adsLoader, exoPlayerView.getPlayerView());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exoPlayerView.hideAds();
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -981,7 +840,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
|
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
|
||||||
return;
|
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(),
|
MediaSource videoSource = buildMediaSource(runningSource.getUri(),
|
||||||
runningSource.getExtension(),
|
runningSource.getExtension(),
|
||||||
drmSessionManager,
|
drmSessionManager,
|
||||||
@@ -990,12 +849,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
MediaSource mediaSourceWithAds = initializeAds(videoSource, runningSource);
|
MediaSource mediaSourceWithAds = initializeAds(videoSource, runningSource);
|
||||||
MediaSource mediaSource = Objects.requireNonNullElse(mediaSourceWithAds, videoSource);
|
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
|
// wait for player to be set
|
||||||
while (player == null) {
|
while (player == null) {
|
||||||
try {
|
try {
|
||||||
@@ -1151,6 +1004,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (customMetadata != null) {
|
if (customMetadata != null) {
|
||||||
mediaItemBuilder.setMediaMetadata(customMetadata);
|
mediaItemBuilder.setMediaMetadata(customMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add external subtitles to MediaItem
|
||||||
|
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = buildSubtitleConfigurations();
|
||||||
|
if (subtitleConfigurations != null) {
|
||||||
|
mediaItemBuilder.setSubtitleConfigurations(subtitleConfigurations);
|
||||||
|
}
|
||||||
|
|
||||||
if (source.getAdsProps() != null) {
|
if (source.getAdsProps() != null) {
|
||||||
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
||||||
if (adTagUrl != null) {
|
if (adTagUrl != null) {
|
||||||
@@ -1285,29 +1145,59 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private MediaSource buildTextSource() {
|
private List<MediaItem.SubtitleConfiguration> buildSubtitleConfigurations() {
|
||||||
if (source.getSideLoadedTextTracks() == null) {
|
if (source.getSideLoadedTextTracks() == null || source.getSideLoadedTextTracks().getTracks().isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = new ArrayList<>();
|
List<MediaItem.SubtitleConfiguration> subtitleConfigurations = new ArrayList<>();
|
||||||
|
int trackIndex = 0;
|
||||||
|
|
||||||
for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
|
for (SideLoadedTextTrack track : source.getSideLoadedTextTracks().getTracks()) {
|
||||||
MediaItem.SubtitleConfiguration subtitleConfiguration = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
|
try {
|
||||||
.setMimeType(track.getType())
|
// Create a more descriptive ID that PlayerView can use
|
||||||
.setLanguage(track.getLanguage())
|
String trackId = "external-subtitle-" + trackIndex;
|
||||||
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
|
String label = track.getTitle();
|
||||||
.setRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
if (label == null || label.isEmpty()) {
|
||||||
.setLabel(track.getTitle())
|
label = "External " + (trackIndex + 1);
|
||||||
.build();
|
if (track.getLanguage() != null && !track.getLanguage().isEmpty()) {
|
||||||
subtitleConfigurations.add(subtitleConfiguration);
|
label += " (" + track.getLanguage() + ")";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaItem subtitlesMediaItem = new MediaItem.Builder()
|
MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
|
||||||
.setUri(source.getUri())
|
.setId(trackId)
|
||||||
.setSubtitleConfigurations(subtitleConfigurations).build();
|
.setMimeType(track.getType())
|
||||||
|
.setLabel(label)
|
||||||
|
.setRoleFlags(C.ROLE_FLAG_SUBTITLE);
|
||||||
|
|
||||||
return new DefaultMediaSourceFactory(mediaDataSourceFactory).createMediaSource(subtitlesMediaItem);
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subtitleConfigurations.isEmpty()) {
|
||||||
|
DebugLog.d(TAG, "Built " + subtitleConfigurations.size() + " external subtitle configurations");
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitleConfigurations.isEmpty() ? null : subtitleConfigurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releasePlayer() {
|
private void releasePlayer() {
|
||||||
@@ -1530,9 +1420,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
selectTrackWhenReady = false;
|
selectTrackWhenReady = false;
|
||||||
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
||||||
}
|
}
|
||||||
// Setting the visibility for the playerControlView
|
// Setting the visibility for the player controls
|
||||||
if (playerControlView != null) {
|
if (exoPlayerView != null) {
|
||||||
playerControlView.show();
|
exoPlayerView.showController();
|
||||||
}
|
}
|
||||||
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
|
setKeepScreenOn(preventsDisplaySleepDuringVideoPlayback);
|
||||||
break;
|
break;
|
||||||
@@ -1599,6 +1489,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
||||||
audioTracks, textTracks, videoTracks, trackId );
|
audioTracks, textTracks, videoTracks, trackId );
|
||||||
|
|
||||||
|
updateSubtitleButtonVisibility();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1607,6 +1498,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
||||||
audioTracks, textTracks, videoTracks, trackId);
|
audioTracks, textTracks, videoTracks, trackId);
|
||||||
|
|
||||||
|
updateSubtitleButtonVisibility();
|
||||||
refreshControlsStyles();
|
refreshControlsStyles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1620,7 +1513,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
private ArrayList<Track> getAudioTrackInfo() {
|
private ArrayList<Track> getAudioTrackInfo() {
|
||||||
ArrayList<Track> audioTracks = new ArrayList<>();
|
ArrayList<Track> audioTracks = new ArrayList<>();
|
||||||
if (trackSelector == null) {
|
if (trackSelector == null) {
|
||||||
// Likely player is unmounting so no audio tracks are available anymore
|
|
||||||
return audioTracks;
|
return audioTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,15 +1523,25 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
TrackGroupArray groups = info.getTrackGroups(index);
|
TrackGroupArray groups = info.getTrackGroups(index);
|
||||||
TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
|
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);
|
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.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||||
|
audioTrack.setSelected(isSelected);
|
||||||
audioTracks.add(audioTrack);
|
audioTracks.add(audioTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioTracks;
|
return audioTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1765,21 +1667,114 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (trackSelector == null) {
|
if (trackSelector == null) {
|
||||||
return textTracks;
|
return textTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||||
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
int index = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||||
if (info == null || index == C.INDEX_UNSET) {
|
if (info == null || index == C.INDEX_UNSET) {
|
||||||
return textTracks;
|
return textTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
|
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);
|
TrackGroupArray groups = info.getTrackGroups(index);
|
||||||
|
|
||||||
for (int i = 0; i < groups.length; ++i) {
|
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||||
TrackGroup group = groups.get(i);
|
TrackGroup group = groups.get(groupIndex);
|
||||||
Format format = group.getFormat(0);
|
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||||
Track textTrack = exoplayerTrackToGenericTrack(format, i, selection, group);
|
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);
|
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);
|
||||||
|
|
||||||
|
// 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;
|
return textTracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1835,9 +1830,70 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTracksChanged(@NonNull Tracks tracks) {
|
public void onTracksChanged(@NonNull Tracks tracks) {
|
||||||
eventEmitter.onTextTracks.invoke(getTextTrackInfo());
|
DebugLog.d(TAG, "onTracksChanged called - updating track information, controls=" + controls);
|
||||||
eventEmitter.onAudioTracks.invoke(getAudioTrackInfo());
|
|
||||||
eventEmitter.onVideoTracks.invoke(getVideoTrackInfo());
|
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
|
@Override
|
||||||
@@ -1988,7 +2044,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
player.clearMediaItems();
|
player.clearMediaItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exoPlayerView.hideAds();
|
|
||||||
this.source = new Source();
|
this.source = new Source();
|
||||||
this.mediaDataSourceFactory = null;
|
this.mediaDataSourceFactory = null;
|
||||||
clearResumePosition();
|
clearResumePosition();
|
||||||
@@ -2029,6 +2085,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void disableTrack(int rendererIndex) {
|
public void disableTrack(int rendererIndex) {
|
||||||
|
if (trackSelector == null) return;
|
||||||
|
|
||||||
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.setRendererDisabled(rendererIndex, true)
|
.setRendererDisabled(rendererIndex, true)
|
||||||
@@ -2036,12 +2094,89 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
trackSelector.setParameters(disableParameters);
|
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) {
|
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);
|
int rendererIndex = getTrackRendererIndex(trackType);
|
||||||
if (rendererIndex == C.INDEX_UNSET) {
|
if (rendererIndex == C.INDEX_UNSET) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||||
if (info == null) {
|
if (info == null) {
|
||||||
return;
|
return;
|
||||||
@@ -2146,7 +2281,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
if (captioningManager != null && captioningManager.isEnabled()) {
|
if (captioningManager != null && captioningManager.isEnabled()) {
|
||||||
groupIndex = getGroupIndexForDefaultLocale(groups);
|
groupIndex = getGroupIndexForDefaultLocale(groups);
|
||||||
}
|
}
|
||||||
} else if (rendererIndex == C.TRACK_TYPE_AUDIO) { // Audio default
|
} else if (trackType == C.TRACK_TYPE_AUDIO) { // Audio default
|
||||||
groupIndex = getGroupIndexForDefaultLocale(groups);
|
groupIndex = getGroupIndexForDefaultLocale(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2186,6 +2321,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);
|
TrackSelectionOverride selectionOverride = new TrackSelectionOverride(groups.get(groupIndex), tracks);
|
||||||
|
|
||||||
DefaultTrackSelector.Parameters.Builder selectionParameters = trackSelector.getParameters()
|
DefaultTrackSelector.Parameters.Builder selectionParameters = trackSelector.getParameters()
|
||||||
@@ -2193,8 +2329,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
.setExceedAudioConstraintsIfNecessary(true)
|
.setExceedAudioConstraintsIfNecessary(true)
|
||||||
.setExceedRendererCapabilitiesIfNecessary(true)
|
.setExceedRendererCapabilitiesIfNecessary(true)
|
||||||
.setExceedVideoConstraintsIfNecessary(true)
|
.setExceedVideoConstraintsIfNecessary(true)
|
||||||
.setRendererDisabled(rendererIndex, false)
|
.setRendererDisabled(rendererIndex, false);
|
||||||
.clearOverridesOfType(selectionOverride.getType());
|
|
||||||
|
// 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()) {
|
if (trackType == C.TRACK_TYPE_VIDEO && isUsingVideoABR()) {
|
||||||
selectionParameters.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate);
|
selectionParameters.setMaxVideoBitrate(maxBitRate == 0 ? Integer.MAX_VALUE : maxBitRate);
|
||||||
@@ -2202,7 +2343,20 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
selectionParameters.addOverride(selectionOverride);
|
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());
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFormatSupported(Format format) {
|
private boolean isFormatSupported(Format format) {
|
||||||
@@ -2252,13 +2406,17 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
public void setSelectedAudioTrack(String type, String value) {
|
public void setSelectedAudioTrack(String type, String value) {
|
||||||
audioTrackType = type;
|
audioTrackType = type;
|
||||||
audioTrackValue = value;
|
audioTrackValue = value;
|
||||||
|
|
||||||
|
if (!controls && player != null && trackSelector != null) {
|
||||||
setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
|
setSelectedTrack(C.TRACK_TYPE_AUDIO, audioTrackType, audioTrackValue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setSelectedTextTrack(String type, String value) {
|
public void setSelectedTextTrack(String type, String value) {
|
||||||
textTrackType = type;
|
textTrackType = type;
|
||||||
textTrackValue = value;
|
textTrackValue = value;
|
||||||
setSelectedTrack(C.TRACK_TYPE_TEXT, textTrackType, textTrackValue);
|
|
||||||
|
selectTextTrackInternal(type, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPausedModifier(boolean paused) {
|
public void setPausedModifier(boolean paused) {
|
||||||
@@ -2452,18 +2610,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
return preventsDisplaySleepDuringVideoPlayback;
|
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) {
|
public void setDisableDisconnectError(boolean disableDisconnectError) {
|
||||||
this.disableDisconnectError = disableDisconnectError;
|
this.disableDisconnectError = disableDisconnectError;
|
||||||
}
|
}
|
||||||
@@ -2480,7 +2626,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, playerControlView, new OnBackPressedCallback(true) {
|
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, new OnBackPressedCallback(true) {
|
||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
setFullscreen(false);
|
setFullscreen(false);
|
||||||
@@ -2504,12 +2650,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
eventEmitter.onVideoFullscreenPlayerDidDismiss.invoke();
|
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
|
@Override
|
||||||
@@ -2550,14 +2690,17 @@ public class ReactExoplayerView extends FrameLayout implements
|
|||||||
*/
|
*/
|
||||||
public void setControls(boolean controls) {
|
public void setControls(boolean controls) {
|
||||||
this.controls = 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) {
|
if (controls) {
|
||||||
addPlayerControl();
|
addPlayerControl();
|
||||||
updateFullScreenButtonVisibility();
|
|
||||||
} else {
|
|
||||||
int indexOfPC = indexOfChild(playerControlView);
|
|
||||||
if (indexOfPC != -1) {
|
|
||||||
removeViewAt(indexOfPC);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
refreshControlsStyles();
|
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 = "selectedVideoTrack"
|
||||||
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
|
private const val PROP_SELECTED_VIDEO_TRACK_TYPE = "type"
|
||||||
private const val PROP_SELECTED_VIDEO_TRACK_VALUE = "value"
|
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_CONTROLS = "controls"
|
||||||
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
|
private const val PROP_SUBTITLE_STYLE = "subtitleStyle"
|
||||||
private const val PROP_SHUTTER_COLOR = "shutterColor"
|
private const val PROP_SHUTTER_COLOR = "shutterColor"
|
||||||
@@ -227,11 +226,6 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
|
|||||||
videoView.setViewType(viewType)
|
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)
|
@ReactProp(name = PROP_CONTROLS, defaultBoolean = false)
|
||||||
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
|
fun setControls(videoView: ReactExoplayerView, controls: Boolean) {
|
||||||
videoView.setControls(controls)
|
videoView.setControls(controls)
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ const BasicExample = () => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [isSeeking, setIsSeeking] = 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 videoRef = useRef<VideoRef>(null);
|
||||||
const viewStyle = fullscreen ? styles.fullScreen : styles.halfScreen;
|
const viewStyle = fullscreen ? styles.fullScreen : styles.halfScreen;
|
||||||
const currentSrc = srcList[srcListId];
|
const currentSrc = srcList[srcListId];
|
||||||
@@ -107,7 +111,36 @@ const BasicExample = () => {
|
|||||||
|
|
||||||
const onAudioTracks = (data: OnAudioTracksData) => {
|
const onAudioTracks = (data: OnAudioTracksData) => {
|
||||||
console.log('onAudioTracks', data);
|
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;
|
return x.selected;
|
||||||
});
|
});
|
||||||
let value;
|
let value;
|
||||||
@@ -118,7 +151,7 @@ const BasicExample = () => {
|
|||||||
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
} else if (audioTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||||
value = selectedTrack?.title;
|
value = selectedTrack?.title;
|
||||||
}
|
}
|
||||||
setAudioTracks(data.audioTracks);
|
setAudioTracks(currentTracks);
|
||||||
setSelectedAudioTrack({
|
setSelectedAudioTrack({
|
||||||
type: audioTracksSelectionBy,
|
type: audioTracksSelectionBy,
|
||||||
value: value,
|
value: value,
|
||||||
@@ -131,11 +164,39 @@ const BasicExample = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onTextTracks = (data: OnTextTracksData) => {
|
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;
|
return x?.selected;
|
||||||
});
|
});
|
||||||
|
|
||||||
setTextTracks(data.textTracks);
|
setTextTracks(currentTracks);
|
||||||
let value;
|
let value;
|
||||||
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
if (textTracksSelectionBy === SelectedTrackType.INDEX) {
|
||||||
value = selectedTrack?.index;
|
value = selectedTrack?.index;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const AudioTrackSelector = ({
|
|||||||
style={styles.picker}
|
style={styles.picker}
|
||||||
itemStyle={styles.pickerItem}
|
itemStyle={styles.pickerItem}
|
||||||
selectedValue={selectedAudioTrack?.value}
|
selectedValue={selectedAudioTrack?.value}
|
||||||
onValueChange={itemValue => {
|
onValueChange={(itemValue) => {
|
||||||
if (itemValue !== 'empty') {
|
if (itemValue !== 'empty') {
|
||||||
console.log('on audio value change ' + itemValue);
|
console.log('on audio value change ' + itemValue);
|
||||||
onValueChange(itemValue);
|
onValueChange(itemValue);
|
||||||
@@ -39,7 +39,7 @@ export const AudioTrackSelector = ({
|
|||||||
) : (
|
) : (
|
||||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||||
)}
|
)}
|
||||||
{audioTracks.map(track => {
|
{audioTracks.map((track) => {
|
||||||
if (!track) {
|
if (!track) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,9 @@ export const AudioTrackSelector = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Picker.Item
|
<Picker.Item
|
||||||
label={`${value} - ${track.selected}`}
|
label={`${
|
||||||
|
track.title || track.language || `Track ${track.index}`
|
||||||
|
}${track.selected ? ' (Selected)' : ''}`}
|
||||||
value={`${value}`}
|
value={`${value}`}
|
||||||
key={`${value}`}
|
key={`${value}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const TextTrackSelector = ({
|
|||||||
style={styles.picker}
|
style={styles.picker}
|
||||||
itemStyle={styles.pickerItem}
|
itemStyle={styles.pickerItem}
|
||||||
selectedValue={`${selectedTextTrack?.value}`}
|
selectedValue={`${selectedTextTrack?.value}`}
|
||||||
onValueChange={itemValue => {
|
onValueChange={(itemValue) => {
|
||||||
if (itemValue !== 'empty') {
|
if (itemValue !== 'empty') {
|
||||||
onValueChange(itemValue);
|
onValueChange(itemValue);
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ export const TextTrackSelector = ({
|
|||||||
) : (
|
) : (
|
||||||
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
<Picker.Item label={'none'} value={'none'} key={'none'} />
|
||||||
)}
|
)}
|
||||||
{textTracks.map(track => {
|
{textTracks.map((track) => {
|
||||||
if (!track) {
|
if (!track) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,15 @@ export const TextTrackSelector = ({
|
|||||||
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
} else if (textTracksSelectionBy === SelectedTrackType.TITLE) {
|
||||||
value = track.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>
|
</Picker>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -123,6 +123,25 @@ export const srcAllPlatformList = [
|
|||||||
},
|
},
|
||||||
uri: 'https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4',
|
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[] = [];
|
export const srcIosList: SampleVideoSource[] = [];
|
||||||
@@ -172,6 +191,25 @@ export const srcAndroidList: SampleVideoSource[] = [
|
|||||||
uri: 'rtsp://rtspstream:3cfa3c36a9c00f4aa38f3cd35816b287@zephyr.rtsp.stream/movie',
|
uri: 'rtsp://rtspstream:3cfa3c36a9c00f4aa38f3cd35816b287@zephyr.rtsp.stream/movie',
|
||||||
type: 'rtsp',
|
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
|
const platformSrc: SampleVideoSource[] = isAndroid
|
||||||
|
|||||||
@@ -357,11 +357,11 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.ExpoExample;
|
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.ExpoExample;
|
||||||
PRODUCT_NAME = "ExpoExample";
|
PRODUCT_NAME = ExpoExample;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "ExpoExample/ExpoExample-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "ExpoExample/ExpoExample-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -385,10 +385,10 @@
|
|||||||
);
|
);
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.ExpoExample;
|
PRODUCT_BUNDLE_IDENTIFIER = com.anonymous.ExpoExample;
|
||||||
PRODUCT_NAME = "ExpoExample";
|
PRODUCT_NAME = ExpoExample;
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "ExpoExample/ExpoExample-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "ExpoExample/ExpoExample-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1";
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
};
|
};
|
||||||
name = Release;
|
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")
|
DebugLog("setSrc Stopping playback")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure UI operations are performed on main thread
|
||||||
|
DispatchQueue.main.sync {
|
||||||
self.removePlayerLayer()
|
self.removePlayerLayer()
|
||||||
|
}
|
||||||
self._playerObserver.player = nil
|
self._playerObserver.player = nil
|
||||||
self._drmManager = nil
|
self._drmManager = nil
|
||||||
self._playerObserver.playerItem = nil
|
self._playerObserver.playerItem = nil
|
||||||
@@ -1226,17 +1230,21 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
|||||||
if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) {
|
if _controls != controls || ((_playerLayer == nil) && (_playerViewController == nil)) {
|
||||||
_controls = controls
|
_controls = controls
|
||||||
if _controls {
|
if _controls {
|
||||||
|
DispatchQueue.main.async {
|
||||||
self.removePlayerLayer()
|
self.removePlayerLayer()
|
||||||
self.usePlayerViewController()
|
self.usePlayerViewController()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_playerViewController?.view.removeFromSuperview()
|
DispatchQueue.main.async {
|
||||||
_playerViewController?.removeFromParent()
|
self._playerViewController?.view.removeFromSuperview()
|
||||||
_playerViewController = nil
|
self._playerViewController?.removeFromParent()
|
||||||
_playerObserver.playerViewController = nil
|
self._playerViewController = nil
|
||||||
|
self._playerObserver.playerViewController = nil
|
||||||
self.usePlayerLayer()
|
self.usePlayerLayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
func setShowNotificationControls(_ showNotificationControls: Bool) {
|
func setShowNotificationControls(_ showNotificationControls: Bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user