mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
updated exosub
This commit is contained in:
parent
d398c73214
commit
c20c2713d0
4 changed files with 412 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
# dependencies
|
||||
node_modules/
|
||||
# Un-ignore specific react-native-video source files we patch
|
||||
!node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
|
||||
!node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
|
||||
!node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
|
||||
|
||||
# Expo
|
||||
|
|
|
|||
81
node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
generated
vendored
Normal file
81
node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
generated
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package com.brentvatne.common.api
|
||||
|
||||
import android.graphics.Color
|
||||
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
||||
/**
|
||||
* Helper file to parse SubtitleStyle prop and build a dedicated class
|
||||
*/
|
||||
class SubtitleStyle public constructor() {
|
||||
var fontSize = -1
|
||||
private set
|
||||
var paddingLeft = 0
|
||||
private set
|
||||
var paddingRight = 0
|
||||
private set
|
||||
var paddingTop = 0
|
||||
private set
|
||||
var paddingBottom = 0
|
||||
private set
|
||||
var opacity = 1f
|
||||
private set
|
||||
var subtitlesFollowVideo = true
|
||||
private set
|
||||
|
||||
// Extended styling (used by ExoPlayerView via Media3 SubtitleView)
|
||||
// Stored as Android color ints to avoid parsing multiple times.
|
||||
var textColor: Int? = null
|
||||
private set
|
||||
var backgroundColor: Int? = null
|
||||
private set
|
||||
var edgeType: String? = null
|
||||
private set
|
||||
var edgeColor: Int? = null
|
||||
private set
|
||||
|
||||
companion object {
|
||||
private const val PROP_FONT_SIZE_TRACK = "fontSize"
|
||||
private const val PROP_PADDING_BOTTOM = "paddingBottom"
|
||||
private const val PROP_PADDING_TOP = "paddingTop"
|
||||
private const val PROP_PADDING_LEFT = "paddingLeft"
|
||||
private const val PROP_PADDING_RIGHT = "paddingRight"
|
||||
private const val PROP_OPACITY = "opacity"
|
||||
private const val PROP_SUBTITLES_FOLLOW_VIDEO = "subtitlesFollowVideo"
|
||||
|
||||
// Extended props (optional)
|
||||
private const val PROP_TEXT_COLOR = "textColor"
|
||||
private const val PROP_BACKGROUND_COLOR = "backgroundColor"
|
||||
private const val PROP_EDGE_TYPE = "edgeType"
|
||||
private const val PROP_EDGE_COLOR = "edgeColor"
|
||||
|
||||
private fun parseColorOrNull(value: String?): Int? {
|
||||
if (value.isNullOrBlank()) return null
|
||||
return try {
|
||||
Color.parseColor(value)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(src: ReadableMap?): SubtitleStyle {
|
||||
val subtitleStyle = SubtitleStyle()
|
||||
subtitleStyle.fontSize = ReactBridgeUtils.safeGetInt(src, PROP_FONT_SIZE_TRACK, -1)
|
||||
subtitleStyle.paddingBottom = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_BOTTOM, 0)
|
||||
subtitleStyle.paddingTop = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_TOP, 0)
|
||||
subtitleStyle.paddingLeft = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_LEFT, 0)
|
||||
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0)
|
||||
subtitleStyle.opacity = ReactBridgeUtils.safeGetFloat(src, PROP_OPACITY, 1f)
|
||||
subtitleStyle.subtitlesFollowVideo = ReactBridgeUtils.safeGetBool(src, PROP_SUBTITLES_FOLLOW_VIDEO, true)
|
||||
|
||||
// Extended styling
|
||||
subtitleStyle.textColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_TEXT_COLOR, null))
|
||||
subtitleStyle.backgroundColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_BACKGROUND_COLOR, null))
|
||||
subtitleStyle.edgeType = ReactBridgeUtils.safeGetString(src, PROP_EDGE_TYPE, null)
|
||||
subtitleStyle.edgeColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_EDGE_COLOR, null))
|
||||
|
||||
return subtitleStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
327
node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
generated
vendored
Normal file
327
node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
generated
vendored
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
package com.brentvatne.exoplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.CaptionStyleCompat
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerView
|
||||
import com.brentvatne.common.api.ResizeMode
|
||||
import com.brentvatne.common.api.SubtitleStyle
|
||||
|
||||
@UnstableApi
|
||||
class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
|
||||
FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var localStyle = SubtitleStyle()
|
||||
private var pendingResizeMode: Int? = 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 - start with FIT mode
|
||||
resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
|
||||
init {
|
||||
// Add PlayerView with explicit layout parameters
|
||||
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
addView(playerView, playerViewLayoutParams)
|
||||
|
||||
// Add live badge with its own layout parameters
|
||||
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
|
||||
addView(liveBadge, liveBadgeLayoutParams)
|
||||
}
|
||||
|
||||
fun setPlayer(player: ExoPlayer?) {
|
||||
val currentPlayer = playerView.player
|
||||
|
||||
if (currentPlayer != null) {
|
||||
currentPlayer.removeListener(playerListener)
|
||||
}
|
||||
|
||||
playerView.player = player
|
||||
|
||||
if (player != null) {
|
||||
player.addListener(playerListener)
|
||||
|
||||
// Apply pending resize mode if we have one
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlayerView(): PlayerView = playerView
|
||||
|
||||
fun setResizeMode(@ResizeMode.Mode resizeMode: Int) {
|
||||
val targetResizeMode = when (resizeMode) {
|
||||
ResizeMode.RESIZE_MODE_FILL -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
||||
ResizeMode.RESIZE_MODE_CENTER_CROP -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||
ResizeMode.RESIZE_MODE_FIT -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
ResizeMode.RESIZE_MODE_FIXED_WIDTH -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
ResizeMode.RESIZE_MODE_FIXED_HEIGHT -> AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
|
||||
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
}
|
||||
|
||||
// Apply the resize mode to PlayerView immediately
|
||||
playerView.resizeMode = targetResizeMode
|
||||
|
||||
// Store it for reapplication if needed
|
||||
pendingResizeMode = targetResizeMode
|
||||
|
||||
// Force PlayerView to recalculate its layout
|
||||
playerView.requestLayout()
|
||||
|
||||
// Also request layout on the parent to ensure proper sizing
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
fun setSubtitleStyle(style: SubtitleStyle) {
|
||||
localStyle = style
|
||||
applySubtitleStyle(localStyle)
|
||||
}
|
||||
|
||||
private fun applySubtitleStyle(style: SubtitleStyle) {
|
||||
playerView.subtitleView?.let { subtitleView ->
|
||||
// Important:
|
||||
// Avoid inheriting Android system caption settings via setUserDefaultStyle(),
|
||||
// because those can force a background/window that the app doesn't want.
|
||||
val resolvedTextColor = style.textColor ?: CaptionStyleCompat.DEFAULT.foregroundColor
|
||||
val resolvedBackgroundColor = style.backgroundColor ?: Color.TRANSPARENT
|
||||
val resolvedEdgeColor = style.edgeColor ?: Color.BLACK
|
||||
|
||||
val resolvedEdgeType = when (style.edgeType?.lowercase()) {
|
||||
"outline" -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
||||
"shadow" -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
|
||||
else -> CaptionStyleCompat.EDGE_TYPE_NONE
|
||||
}
|
||||
|
||||
// windowColor MUST be transparent to avoid the "caption window" background.
|
||||
val captionStyle = CaptionStyleCompat(
|
||||
resolvedTextColor,
|
||||
resolvedBackgroundColor,
|
||||
Color.TRANSPARENT,
|
||||
resolvedEdgeType,
|
||||
resolvedEdgeColor,
|
||||
null
|
||||
)
|
||||
subtitleView.setStyle(captionStyle)
|
||||
|
||||
// Text size: if not provided, fall back to user default size.
|
||||
if (style.fontSize > 0) {
|
||||
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
||||
} else {
|
||||
subtitleView.setUserDefaultTextSize()
|
||||
}
|
||||
|
||||
// Horizontal padding is still useful (safe area); vertical offset is handled via bottomPaddingFraction.
|
||||
subtitleView.setPadding(
|
||||
style.paddingLeft,
|
||||
style.paddingTop,
|
||||
style.paddingRight,
|
||||
0
|
||||
)
|
||||
|
||||
// Bottom offset for *internal* subtitles:
|
||||
// Use Media3 SubtitleView's bottomPaddingFraction (moves cues up) rather than raw view padding.
|
||||
if (style.paddingBottom > 0 && playerView.height > 0) {
|
||||
val fraction = (style.paddingBottom.toFloat() / playerView.height.toFloat())
|
||||
.coerceIn(0f, 0.5f)
|
||||
subtitleView.setBottomPaddingFraction(fraction)
|
||||
}
|
||||
|
||||
if (style.opacity != 0.0f) {
|
||||
subtitleView.alpha = style.opacity
|
||||
subtitleView.visibility = android.view.View.VISIBLE
|
||||
} else {
|
||||
subtitleView.visibility = android.view.View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setShutterColor(color: Int) {
|
||||
playerView.setShutterBackgroundColor(color)
|
||||
}
|
||||
|
||||
fun updateSurfaceView(viewType: Int) {
|
||||
// TODO: Implement proper surface type switching if needed
|
||||
}
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = playerView.player?.isPlaying ?: false
|
||||
|
||||
fun invalidateAspectRatio() {
|
||||
// PlayerView handles aspect ratio automatically through its internal AspectRatioFrameLayout
|
||||
playerView.requestLayout()
|
||||
|
||||
// Reapply the current resize mode to ensure it's properly set
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
}
|
||||
|
||||
fun setUseController(useController: Boolean) {
|
||||
playerView.useController = useController
|
||||
if (useController) {
|
||||
// Ensure proper touch handling when controls are enabled
|
||||
playerView.controllerAutoShow = true
|
||||
playerView.controllerHideOnTouch = true
|
||||
// Show controls immediately when enabled
|
||||
playerView.showController()
|
||||
}
|
||||
}
|
||||
|
||||
fun showController() {
|
||||
playerView.showController()
|
||||
}
|
||||
|
||||
fun hideController() {
|
||||
playerView.hideController()
|
||||
}
|
||||
|
||||
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()
|
||||
// Reapply resize mode to ensure it's properly set after timeline changes
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
}
|
||||
updateLiveUi()
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION) ||
|
||||
events.contains(Player.EVENT_IS_PLAYING_CHANGED)
|
||||
) {
|
||||
updateLiveUi()
|
||||
}
|
||||
|
||||
// Handle video size changes which affect aspect ratio
|
||||
if (events.contains(Player.EVENT_VIDEO_SIZE_CHANGED)) {
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
playerView.requestLayout()
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ExoPlayerView"
|
||||
}
|
||||
|
||||
/**
|
||||
* React Native (Yoga) can sometimes defer layout passes that are required by
|
||||
* PlayerView for its child views (controller overlay, surface view, subtitle view, …).
|
||||
* This helper forces a second measure / layout after RN finishes, ensuring the
|
||||
* internal views receive the final size. The same approach is used in the v7
|
||||
* implementation (see VideoView.kt) and in React Native core (Toolbar example [link]).
|
||||
*/
|
||||
private val layoutRunnable = Runnable {
|
||||
measure(
|
||||
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
|
||||
)
|
||||
layout(left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun requestLayout() {
|
||||
super.requestLayout()
|
||||
// Post a second layout pass so the ExoPlayer internal views get correct bounds.
|
||||
post(layoutRunnable)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
|
||||
if (changed) {
|
||||
pendingResizeMode?.let { resizeMode ->
|
||||
playerView.resizeMode = resizeMode
|
||||
}
|
||||
// Re-apply bottomPaddingFraction once we have a concrete height.
|
||||
applySubtitleStyle(localStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ const SplashScreen = ({ onFinish }: SplashScreenProps) => {
|
|||
// TEMPORARILY DISABLED
|
||||
useEffect(() => {
|
||||
// Immediately call onFinish to skip splash screen
|
||||
onFinish();
|
||||
onFinish();
|
||||
}, [onFinish]);
|
||||
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue