mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-19 16:31:44 +00:00
Added perefance for language and quality for autoplaying streams
This commit is contained in:
parent
5874a78ce0
commit
dee6bd3f52
11 changed files with 1230 additions and 1024 deletions
|
|
@ -2,12 +2,12 @@
|
||||||
<uses-sdk tools:overrideLibrary="dev.jdtech.mpv"/>
|
<uses-sdk tools:overrideLibrary="dev.jdtech.mpv"/>
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW"/>
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
defaults.url=https://sentry.io/
|
defaults.url=https://sentry.io/
|
||||||
defaults.org=tapframe
|
defaults.org=tapframe
|
||||||
defaults.project=react-native
|
defaults.project=react-native
|
||||||
auth.token=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
|
# Using SENTRY_AUTH_TOKEN environment variable
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.brentvatne.common.api
|
package com.brentvatne.common.api
|
||||||
|
|
||||||
import android.graphics.Color
|
|
||||||
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
||||||
import com.facebook.react.bridge.ReadableMap
|
import com.facebook.react.bridge.ReadableMap
|
||||||
|
|
||||||
|
|
@ -23,17 +22,6 @@ class SubtitleStyle public constructor() {
|
||||||
var subtitlesFollowVideo = true
|
var subtitlesFollowVideo = true
|
||||||
private set
|
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 {
|
companion object {
|
||||||
private const val PROP_FONT_SIZE_TRACK = "fontSize"
|
private const val PROP_FONT_SIZE_TRACK = "fontSize"
|
||||||
private const val PROP_PADDING_BOTTOM = "paddingBottom"
|
private const val PROP_PADDING_BOTTOM = "paddingBottom"
|
||||||
|
|
@ -43,21 +31,6 @@ class SubtitleStyle public constructor() {
|
||||||
private const val PROP_OPACITY = "opacity"
|
private const val PROP_OPACITY = "opacity"
|
||||||
private const val PROP_SUBTITLES_FOLLOW_VIDEO = "subtitlesFollowVideo"
|
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
|
@JvmStatic
|
||||||
fun parse(src: ReadableMap?): SubtitleStyle {
|
fun parse(src: ReadableMap?): SubtitleStyle {
|
||||||
val subtitleStyle = SubtitleStyle()
|
val subtitleStyle = SubtitleStyle()
|
||||||
|
|
@ -68,13 +41,6 @@ class SubtitleStyle public constructor() {
|
||||||
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0)
|
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0)
|
||||||
subtitleStyle.opacity = ReactBridgeUtils.safeGetFloat(src, PROP_OPACITY, 1f)
|
subtitleStyle.opacity = ReactBridgeUtils.safeGetFloat(src, PROP_OPACITY, 1f)
|
||||||
subtitleStyle.subtitlesFollowVideo = ReactBridgeUtils.safeGetBool(src, PROP_SUBTITLES_FOLLOW_VIDEO, true)
|
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
|
return subtitleStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,11 @@ import android.widget.FrameLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Timeline
|
import androidx.media3.common.Timeline
|
||||||
import androidx.media3.common.text.CueGroup
|
|
||||||
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.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.CaptionStyleCompat
|
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.media3.ui.SubtitleView
|
|
||||||
import com.brentvatne.common.api.ResizeMode
|
import com.brentvatne.common.api.ResizeMode
|
||||||
import com.brentvatne.common.api.SubtitleStyle
|
import com.brentvatne.common.api.SubtitleStyle
|
||||||
|
|
||||||
|
|
@ -55,58 +52,15 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||||
resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
resizeMode = androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Subtitles rendered in a full-size overlay (NOT inside PlayerView's content frame).
|
|
||||||
* This keeps subtitles anchored in-place even when the video surface/content frame moves
|
|
||||||
* due to aspect ratio / resizeMode changes.
|
|
||||||
*
|
|
||||||
* Controlled by SubtitleStyle.subtitlesFollowVideo.
|
|
||||||
*/
|
|
||||||
private val overlaySubtitleView = SubtitleView(context).apply {
|
|
||||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
||||||
visibility = View.GONE
|
|
||||||
// We control styling via SubtitleStyle; don't pull Android system caption defaults.
|
|
||||||
setApplyEmbeddedStyles(true)
|
|
||||||
setApplyEmbeddedFontSizes(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSubtitleRenderingMode() {
|
|
||||||
val internalSubtitleView = playerView.subtitleView
|
|
||||||
val followVideo = localStyle.subtitlesFollowVideo
|
|
||||||
val shouldShow = localStyle.opacity != 0.0f
|
|
||||||
|
|
||||||
if (followVideo) {
|
|
||||||
internalSubtitleView?.visibility = if (shouldShow) View.VISIBLE else View.GONE
|
|
||||||
overlaySubtitleView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
// Hard-disable PlayerView's internal subtitle view. PlayerView can recreate/toggle this view
|
|
||||||
// during resize/layout, so we re-assert this in multiple lifecycle points.
|
|
||||||
internalSubtitleView?.visibility = View.GONE
|
|
||||||
internalSubtitleView?.alpha = 0f
|
|
||||||
overlaySubtitleView.visibility = if (shouldShow) View.VISIBLE else View.GONE
|
|
||||||
overlaySubtitleView.alpha = 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Add PlayerView with explicit layout parameters
|
// Add PlayerView with explicit layout parameters
|
||||||
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||||
addView(playerView, playerViewLayoutParams)
|
addView(playerView, playerViewLayoutParams)
|
||||||
|
|
||||||
// Add overlay subtitles above PlayerView (so it doesn't move with video content frame)
|
|
||||||
val subtitleOverlayLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
||||||
addView(overlaySubtitleView, subtitleOverlayLayoutParams)
|
|
||||||
|
|
||||||
// Add live badge with its own layout parameters
|
// Add live badge with its own layout parameters
|
||||||
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
||||||
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
|
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
|
||||||
addView(liveBadge, liveBadgeLayoutParams)
|
addView(liveBadge, liveBadgeLayoutParams)
|
||||||
|
|
||||||
// PlayerView may internally recreate its subtitle view during relayouts (e.g. resizeMode changes).
|
|
||||||
// Ensure our rendering mode is re-applied whenever PlayerView lays out.
|
|
||||||
playerView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
||||||
updateSubtitleRenderingMode()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPlayer(player: ExoPlayer?) {
|
fun setPlayer(player: ExoPlayer?) {
|
||||||
|
|
@ -126,10 +80,6 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||||
playerView.resizeMode = resizeMode
|
playerView.resizeMode = resizeMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-assert subtitle rendering mode for the current style.
|
|
||||||
updateSubtitleRenderingMode()
|
|
||||||
applySubtitleStyle(localStyle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPlayerView(): PlayerView = playerView
|
fun getPlayerView(): PlayerView = playerView
|
||||||
|
|
@ -158,63 +108,23 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSubtitleStyle(style: SubtitleStyle) {
|
fun setSubtitleStyle(style: SubtitleStyle) {
|
||||||
localStyle = style
|
|
||||||
applySubtitleStyle(localStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applySubtitleStyle(style: SubtitleStyle) {
|
|
||||||
updateSubtitleRenderingMode()
|
|
||||||
|
|
||||||
playerView.subtitleView?.let { subtitleView ->
|
playerView.subtitleView?.let { subtitleView ->
|
||||||
// Important:
|
// Reset to defaults
|
||||||
// Avoid inheriting Android system caption settings via setUserDefaultStyle(),
|
subtitleView.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) {
|
|
||||||
// Use DIP so the value matches React Native's dp-based fontSize more closely.
|
|
||||||
// SP would multiply by system fontScale and makes "30" look larger than RN "30".
|
|
||||||
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, style.fontSize.toFloat())
|
|
||||||
} else {
|
|
||||||
subtitleView.setUserDefaultTextSize()
|
subtitleView.setUserDefaultTextSize()
|
||||||
|
|
||||||
|
// Apply custom styling
|
||||||
|
if (style.fontSize > 0) {
|
||||||
|
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontal padding is still useful (safe area); vertical offset is handled via bottomPaddingFraction.
|
|
||||||
subtitleView.setPadding(
|
subtitleView.setPadding(
|
||||||
style.paddingLeft,
|
style.paddingLeft,
|
||||||
style.paddingTop,
|
style.paddingTop,
|
||||||
style.paddingRight,
|
style.paddingRight,
|
||||||
0
|
style.paddingBottom
|
||||||
)
|
)
|
||||||
|
|
||||||
// Bottom offset for *internal* subtitles:
|
|
||||||
// Use Media3 SubtitleView's bottomPaddingFraction (moves cues up) rather than raw view padding.
|
|
||||||
if (style.paddingBottom > 0 && playerView.height > 0) {
|
|
||||||
val fraction = (style.paddingBottom.toFloat() / playerView.height.toFloat())
|
|
||||||
.coerceIn(0f, 0.9f)
|
|
||||||
subtitleView.setBottomPaddingFraction(fraction)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (style.opacity != 0.0f) {
|
if (style.opacity != 0.0f) {
|
||||||
subtitleView.alpha = style.opacity
|
subtitleView.alpha = style.opacity
|
||||||
subtitleView.visibility = android.view.View.VISIBLE
|
subtitleView.visibility = android.view.View.VISIBLE
|
||||||
|
|
@ -222,59 +132,7 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||||
subtitleView.visibility = android.view.View.GONE
|
subtitleView.visibility = android.view.View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
localStyle = style
|
||||||
// Apply the same styling to the overlay subtitle view.
|
|
||||||
run {
|
|
||||||
val subtitleView = overlaySubtitleView
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
val captionStyle = CaptionStyleCompat(
|
|
||||||
resolvedTextColor,
|
|
||||||
resolvedBackgroundColor,
|
|
||||||
Color.TRANSPARENT,
|
|
||||||
resolvedEdgeType,
|
|
||||||
resolvedEdgeColor,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
subtitleView.setStyle(captionStyle)
|
|
||||||
|
|
||||||
if (style.fontSize > 0) {
|
|
||||||
// Use DIP so the value matches React Native's dp-based fontSize more closely.
|
|
||||||
subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_DIP, style.fontSize.toFloat())
|
|
||||||
} else {
|
|
||||||
subtitleView.setUserDefaultTextSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
subtitleView.setPadding(
|
|
||||||
style.paddingLeft,
|
|
||||||
style.paddingTop,
|
|
||||||
style.paddingRight,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
// Bottom offset relative to the full view height (stable even when video content frame moves).
|
|
||||||
val h = height.takeIf { it > 0 } ?: subtitleView.height
|
|
||||||
if (style.paddingBottom > 0 && h > 0) {
|
|
||||||
val fraction = (style.paddingBottom.toFloat() / h.toFloat())
|
|
||||||
.coerceIn(0f, 0.9f)
|
|
||||||
subtitleView.setBottomPaddingFraction(fraction)
|
|
||||||
} else {
|
|
||||||
subtitleView.setBottomPaddingFraction(0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (style.opacity != 0.0f) {
|
|
||||||
subtitleView.alpha = style.opacity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShutterColor(color: Int) {
|
fun setShutterColor(color: Int) {
|
||||||
|
|
@ -365,13 +223,6 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
private val playerListener = object : Player.Listener {
|
private val playerListener = object : Player.Listener {
|
||||||
override fun onCues(cueGroup: CueGroup) {
|
|
||||||
// Keep overlay subtitles in sync. This does NOT interfere with PlayerView's own subtitle rendering.
|
|
||||||
// When subtitlesFollowVideo=false, overlaySubtitleView is the visible one.
|
|
||||||
updateSubtitleRenderingMode()
|
|
||||||
overlaySubtitleView.setCues(cueGroup.cues)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||||
playerView.post {
|
playerView.post {
|
||||||
playerView.requestLayout()
|
playerView.requestLayout()
|
||||||
|
|
@ -433,9 +284,6 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||||
pendingResizeMode?.let { resizeMode ->
|
pendingResizeMode?.let { resizeMode ->
|
||||||
playerView.resizeMode = resizeMode
|
playerView.resizeMode = resizeMode
|
||||||
}
|
}
|
||||||
// Re-apply bottomPaddingFraction once we have a concrete height.
|
|
||||||
updateSubtitleRenderingMode()
|
|
||||||
applySubtitleStyle(localStyle)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,8 +228,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
private ArrayList<Integer> rootViewChildrenOriginalVisibility = new ArrayList<Integer>();
|
private ArrayList<Integer> rootViewChildrenOriginalVisibility = new ArrayList<Integer>();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When user is seeking first called is on onPositionDiscontinuity ->
|
* When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
|
||||||
* DISCONTINUITY_REASON_SEEK
|
|
||||||
* Then we set if to false when playback is back in onIsPlayingChanged -> true
|
* Then we set if to false when playback is back in onIsPlayingChanged -> true
|
||||||
*/
|
*/
|
||||||
private boolean isSeeking = false;
|
private boolean isSeeking = false;
|
||||||
|
|
@ -299,8 +298,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
lastPos = pos;
|
lastPos = pos;
|
||||||
lastBufferDuration = bufferedDuration;
|
lastBufferDuration = bufferedDuration;
|
||||||
lastDuration = duration;
|
lastDuration = duration;
|
||||||
eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(),
|
eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
|
||||||
getPositionInFirstPeriodMsForCurrentWindow(pos));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -357,9 +355,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
LayoutParams.MATCH_PARENT,
|
LayoutParams.MATCH_PARENT,
|
||||||
LayoutParams.MATCH_PARENT);
|
LayoutParams.MATCH_PARENT);
|
||||||
exoPlayerView = new ExoPlayerView(getContext());
|
exoPlayerView = new ExoPlayerView(getContext());
|
||||||
exoPlayerView.addOnLayoutChangeListener(
|
exoPlayerView.addOnLayoutChangeListener( (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) ->
|
||||||
(View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> PictureInPictureUtil
|
PictureInPictureUtil.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView)
|
||||||
.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView));
|
);
|
||||||
exoPlayerView.setLayoutParams(layoutParams);
|
exoPlayerView.setLayoutParams(layoutParams);
|
||||||
addView(exoPlayerView, 0, layoutParams);
|
addView(exoPlayerView, 0, layoutParams);
|
||||||
|
|
||||||
|
|
@ -385,10 +383,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
public void onHostPause() {
|
public void onHostPause() {
|
||||||
isInBackground = true;
|
isInBackground = true;
|
||||||
Activity activity = themedReactContext.getCurrentActivity();
|
Activity activity = themedReactContext.getCurrentActivity();
|
||||||
boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null
|
boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode();
|
||||||
&& activity.isInPictureInPictureMode();
|
boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode();
|
||||||
boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null
|
|
||||||
&& activity.isInMultiWindowMode();
|
|
||||||
if (playInBackground || isInPictureInPicture || isInMultiWindowMode) {
|
if (playInBackground || isInPictureInPicture || isInMultiWindowMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -415,8 +411,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null);
|
eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null);
|
||||||
} else {
|
} else {
|
||||||
Format videoFormat = player.getVideoFormat();
|
Format videoFormat = player.getVideoFormat();
|
||||||
boolean isRotatedContent = videoFormat != null
|
boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
||||||
&& (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
|
||||||
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
|
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
|
||||||
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
|
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
|
||||||
String trackId = videoFormat != null ? videoFormat.id : null;
|
String trackId = videoFormat != null ? videoFormat.id : null;
|
||||||
|
|
@ -431,8 +426,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
* Toggling the visibility of the player control view
|
* Toggling the visibility of the player control view
|
||||||
*/
|
*/
|
||||||
private void togglePlayerControlVisibility() {
|
private void togglePlayerControlVisibility() {
|
||||||
if (player == null)
|
if (player == null) return;
|
||||||
return;
|
|
||||||
if (exoPlayerView.isControllerVisible()) {
|
if (exoPlayerView.isControllerVisible()) {
|
||||||
exoPlayerView.hideController();
|
exoPlayerView.hideController();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -456,8 +450,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateControllerConfig() {
|
private void updateControllerConfig() {
|
||||||
if (exoPlayerView == null)
|
if (exoPlayerView == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
exoPlayerView.setControllerShowTimeoutMs(5000);
|
exoPlayerView.setControllerShowTimeoutMs(5000);
|
||||||
|
|
||||||
|
|
@ -468,8 +461,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateControllerVisibility() {
|
private void updateControllerVisibility() {
|
||||||
if (exoPlayerView == null)
|
if (exoPlayerView == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
exoPlayerView.setUseController(controls && !controlsConfig.getHideFullscreen());
|
exoPlayerView.setUseController(controls && !controlsConfig.getHideFullscreen());
|
||||||
}
|
}
|
||||||
|
|
@ -505,10 +497,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
speed = 2.0f;
|
speed = 2.0f;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
speed = 1.0f;
|
speed = 1.0f;;
|
||||||
;
|
};
|
||||||
}
|
|
||||||
;
|
|
||||||
setRateModifier(speed);
|
setRateModifier(speed);
|
||||||
});
|
});
|
||||||
builder.show();
|
builder.show();
|
||||||
|
|
@ -520,30 +510,24 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the layout
|
* Update the layout
|
||||||
*
|
|
||||||
* @param view view needs to update layout
|
* @param view view needs to update layout
|
||||||
*
|
*
|
||||||
* This is a workaround for the open bug in react-native: <a href=
|
* This is a workaround for the open bug in react-native: <a href="https://github.com/facebook/react-native/issues/17968">...</a>
|
||||||
* "https://github.com/facebook/react-native/issues/17968">...</a>
|
|
||||||
*/
|
*/
|
||||||
private void reLayout(View view) {
|
private void reLayout(View view) {
|
||||||
if (view == null)
|
if (view == null) return;
|
||||||
return;
|
|
||||||
view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
|
view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
|
||||||
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
|
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
|
||||||
view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight());
|
view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshControlsStyles() {
|
private void refreshControlsStyles() {
|
||||||
if (exoPlayerView == null || player == null || !controls)
|
if (exoPlayerView == null || player == null || !controls) return;
|
||||||
return;
|
|
||||||
updateControllerVisibility();
|
updateControllerVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: The following methods for live content and button visibility are no
|
// Note: The following methods for live content and button visibility are no longer needed
|
||||||
// longer needed
|
// as PlayerView handles controls automatically. Some functionality may need to be
|
||||||
// as PlayerView handles controls automatically. Some functionality may need to
|
|
||||||
// be
|
|
||||||
// reimplemented using PlayerView's APIs if custom behavior is required.
|
// reimplemented using PlayerView's APIs if custom behavior is required.
|
||||||
|
|
||||||
private void reLayoutControls() {
|
private void reLayoutControls() {
|
||||||
|
|
@ -580,7 +564,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
private class RNVLoadControl extends DefaultLoadControl {
|
private class RNVLoadControl extends DefaultLoadControl {
|
||||||
private final int availableHeapInBytes;
|
private final int availableHeapInBytes;
|
||||||
private final Runtime runtime;
|
private final Runtime runtime;
|
||||||
|
|
||||||
public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) {
|
public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) {
|
||||||
super(allocator,
|
super(allocator,
|
||||||
config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
|
config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
|
||||||
|
|
@ -602,10 +585,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
: DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS,
|
: DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS,
|
||||||
DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
|
DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
|
||||||
runtime = Runtime.getRuntime();
|
runtime = Runtime.getRuntime();
|
||||||
ActivityManager activityManager = (ActivityManager) themedReactContext
|
ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
|
||||||
.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
|
double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
||||||
double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion
|
|
||||||
.getBufferConfigPropUnsetDouble()
|
|
||||||
? config.getMaxHeapAllocationPercent()
|
? config.getMaxHeapAllocationPercent()
|
||||||
: DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
: DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
||||||
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
|
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
|
||||||
|
|
@ -625,15 +606,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||||
long freeMemory = runtime.maxMemory() - usedMemory;
|
long freeMemory = runtime.maxMemory() - usedMemory;
|
||||||
double minBufferMemoryReservePercent = source.getBufferConfig()
|
double minBufferMemoryReservePercent = source.getBufferConfig().getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
||||||
.getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
|
||||||
? source.getBufferConfig().getMinBufferMemoryReservePercent()
|
? source.getBufferConfig().getMinBufferMemoryReservePercent()
|
||||||
: ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
: ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
||||||
long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory();
|
long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory();
|
||||||
long bufferedMs = bufferedDurationUs / (long) 1000;
|
long bufferedMs = bufferedDurationUs / (long) 1000;
|
||||||
if (reserveMemory > freeMemory && bufferedMs > 2000) {
|
if (reserveMemory > freeMemory && bufferedMs > 2000) {
|
||||||
// We don't have enough memory in reserve so we stop buffering to allow other
|
// We don't have enough memory in reserve so we stop buffering to allow other components to use it instead
|
||||||
// components to use it instead
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (runtime.freeMemory() == 0) {
|
if (runtime.freeMemory() == 0) {
|
||||||
|
|
@ -666,13 +645,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
// Initialize core configuration and listeners
|
// Initialize core configuration and listeners
|
||||||
initializePlayerCore(self);
|
initializePlayerCore(self);
|
||||||
pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(themedReactContext, this);
|
pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(themedReactContext, this);
|
||||||
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder,
|
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave);
|
||||||
this.enterPictureInPictureOnLeave);
|
|
||||||
}
|
}
|
||||||
if (!source.isLocalAssetFile() && !source.isAsset() && source.getBufferConfig().getCacheSize() > 0) {
|
if (!source.isLocalAssetFile() && !source.isAsset() && source.getBufferConfig().getCacheSize() > 0) {
|
||||||
RNVSimpleCache.INSTANCE.setSimpleCache(
|
RNVSimpleCache.INSTANCE.setSimpleCache(
|
||||||
this.getContext(),
|
this.getContext(),
|
||||||
source.getBufferConfig().getCacheSize());
|
source.getBufferConfig().getCacheSize()
|
||||||
|
);
|
||||||
useCache = true;
|
useCache = true;
|
||||||
} else {
|
} else {
|
||||||
useCache = false;
|
useCache = false;
|
||||||
|
|
@ -680,8 +659,7 @@ 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.invalidateAspectRatio();
|
exoPlayerView.invalidateAspectRatio();
|
||||||
// DRM session manager creation must be done on a different thread to prevent
|
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
|
||||||
// crashes so we start a new thread
|
|
||||||
ExecutorService es = Executors.newSingleThreadExecutor();
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
||||||
es.execute(() -> {
|
es.execute(() -> {
|
||||||
// DRM initialization must run on a different thread
|
// DRM initialization must run on a different thread
|
||||||
|
|
@ -690,8 +668,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
if (activity == null) {
|
if (activity == null) {
|
||||||
DebugLog.e(TAG, "Failed to initialize Player!, null activity");
|
DebugLog.e(TAG, "Failed to initialize Player!, null activity");
|
||||||
eventEmitter.onVideoError.invoke("Failed to initialize Player!",
|
eventEmitter.onVideoError.invoke("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001");
|
||||||
new Exception("Current Activity is null!"), "1001");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -744,7 +721,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||||
RNVLoadControl loadControl = new RNVLoadControl(
|
RNVLoadControl loadControl = new RNVLoadControl(
|
||||||
allocator,
|
allocator,
|
||||||
source.getBufferConfig());
|
source.getBufferConfig()
|
||||||
|
);
|
||||||
|
|
||||||
long initialBitrate = source.getBufferConfig().getInitialBitrate();
|
long initialBitrate = source.getBufferConfig().getInitialBitrate();
|
||||||
if (initialBitrate > 0) {
|
if (initialBitrate > 0) {
|
||||||
|
|
@ -752,8 +730,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
this.bandwidthMeter = config.getBandwidthMeter();
|
this.bandwidthMeter = config.getBandwidthMeter();
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext())
|
DefaultRenderersFactory renderersFactory =
|
||||||
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
|
new DefaultRenderersFactory(getContext())
|
||||||
|
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
|
||||||
.setEnableDecoderFallback(true)
|
.setEnableDecoderFallback(true)
|
||||||
.forceEnableMediaCodecAsynchronousQueueing();
|
.forceEnableMediaCodecAsynchronousQueueing();
|
||||||
|
|
||||||
|
|
@ -764,13 +743,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
} else {
|
} else {
|
||||||
mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
|
mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
|
||||||
|
|
||||||
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader,
|
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
|
||||||
exoPlayerView.getPlayerView());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useCache && !disableCache) {
|
if (useCache && !disableCache) {
|
||||||
mediaSourceFactory
|
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
||||||
.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player = new ExoPlayer.Builder(getContext(), renderersFactory)
|
player = new ExoPlayer.Builder(getContext(), renderersFactory)
|
||||||
|
|
@ -807,7 +784,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
Uri adTagUrl = adProps.getAdTagUrl();
|
Uri adTagUrl = adProps.getAdTagUrl();
|
||||||
if (adTagUrl != null) {
|
if (adTagUrl != null) {
|
||||||
// Create an AdsLoader.
|
// Create an AdsLoader.
|
||||||
ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader.Builder(themedReactContext)
|
ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
|
||||||
|
.Builder(themedReactContext)
|
||||||
.setAdEventListener(this)
|
.setAdEventListener(this)
|
||||||
.setAdErrorListener(this);
|
.setAdErrorListener(this);
|
||||||
|
|
||||||
|
|
@ -839,8 +817,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First check if there's a custom DRM manager registered through the plugin
|
// First check if there's a custom DRM manager registered through the plugin system
|
||||||
// system
|
|
||||||
DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager();
|
DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager();
|
||||||
if (drmManager == null) {
|
if (drmManager == null) {
|
||||||
// If no custom manager is registered, use the default implementation
|
// If no custom manager is registered, use the default implementation
|
||||||
|
|
@ -849,13 +826,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps);
|
DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps);
|
||||||
if (drmSessionManager == null) {
|
if (drmSessionManager == null) {
|
||||||
eventEmitter.onVideoError.invoke("Failed to build DRM session manager",
|
eventEmitter.onVideoError.invoke("Failed to build DRM session manager", new Exception("DRM session manager is null"), "3007");
|
||||||
new Exception("DRM session manager is null"), "3007");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow plugins to override the DrmSessionManager
|
// Allow plugins to override the DrmSessionManager
|
||||||
DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance()
|
DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance().overrideDrmSessionManager(source, drmSessionManager);
|
||||||
.overrideDrmSessionManager(source, drmSessionManager);
|
|
||||||
return overriddenManager != null ? overriddenManager : drmSessionManager;
|
return overriddenManager != null ? overriddenManager : drmSessionManager;
|
||||||
} catch (UnsupportedDrmException ex) {
|
} catch (UnsupportedDrmException ex) {
|
||||||
// Unsupported DRM exceptions are handled by the calling method
|
// Unsupported DRM exceptions are handled by the calling method
|
||||||
|
|
@ -878,8 +853,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
/// init DRM
|
/// init DRM
|
||||||
DrmSessionManager drmSessionManager = initializePlayerDrm();
|
DrmSessionManager drmSessionManager = initializePlayerDrm();
|
||||||
if (drmSessionManager == null && runningSource.getDrmProps() != null
|
if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) {
|
||||||
&& runningSource.getDrmProps().getDrmType() != null) {
|
|
||||||
// Failed to initialize DRM session manager - cannot continue
|
// Failed to initialize DRM session manager - cannot continue
|
||||||
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
|
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
|
||||||
return;
|
return;
|
||||||
|
|
@ -936,8 +910,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
} catch (UnsupportedDrmException e) {
|
} catch (UnsupportedDrmException e) {
|
||||||
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
||||||
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||||
? R.string.error_drm_unsupported_scheme
|
? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
|
||||||
: R.string.error_drm_unknown);
|
|
||||||
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
|
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -982,8 +955,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
if (playbackServiceBinder != null) {
|
if (playbackServiceBinder != null) {
|
||||||
playbackServiceBinder.getService().unregisterPlayer(player);
|
playbackServiceBinder.getService().unregisterPlayer(player);
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {}
|
||||||
}
|
|
||||||
|
|
||||||
playbackServiceBinder = null;
|
playbackServiceBinder = null;
|
||||||
}
|
}
|
||||||
|
|
@ -1029,8 +1001,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager,
|
private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) {
|
||||||
long cropStartMs, long cropEndMs) {
|
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
throw new IllegalStateException("Invalid video uri");
|
throw new IllegalStateException("Invalid video uri");
|
||||||
}
|
}
|
||||||
|
|
@ -1062,12 +1033,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
||||||
if (adTagUrl != null) {
|
if (adTagUrl != null) {
|
||||||
mediaItemBuilder.setAdsConfiguration(
|
mediaItemBuilder.setAdsConfiguration(
|
||||||
new MediaItem.AdsConfiguration.Builder(adTagUrl).build());
|
new MediaItem.AdsConfiguration.Builder(adTagUrl).build()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils
|
MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(source.getBufferConfig());
|
||||||
.getLiveConfiguration(source.getBufferConfig());
|
|
||||||
mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());
|
mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());
|
||||||
|
|
||||||
MediaSource.Factory mediaSourceFactory;
|
MediaSource.Factory mediaSourceFactory;
|
||||||
|
|
@ -1079,6 +1050,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
drmProvider = new DefaultDrmSessionManagerProvider();
|
drmProvider = new DefaultDrmSessionManagerProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CONTENT_TYPE_SS:
|
case CONTENT_TYPE_SS:
|
||||||
if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) {
|
if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) {
|
||||||
|
|
@ -1088,7 +1060,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
mediaSourceFactory = new SsMediaSource.Factory(
|
mediaSourceFactory = new SsMediaSource.Factory(
|
||||||
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
||||||
buildDataSourceFactory(false));
|
buildDataSourceFactory(false)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case CONTENT_TYPE_DASH:
|
case CONTENT_TYPE_DASH:
|
||||||
if(!BuildConfig.USE_EXOPLAYER_DASH) {
|
if(!BuildConfig.USE_EXOPLAYER_DASH) {
|
||||||
|
|
@ -1098,7 +1071,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
mediaSourceFactory = new DashMediaSource.Factory(
|
mediaSourceFactory = new DashMediaSource.Factory(
|
||||||
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
||||||
buildDataSourceFactory(false));
|
buildDataSourceFactory(false)
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case CONTENT_TYPE_HLS:
|
case CONTENT_TYPE_HLS:
|
||||||
if (!BuildConfig.USE_EXOPLAYER_HLS) {
|
if (!BuildConfig.USE_EXOPLAYER_HLS) {
|
||||||
|
|
@ -1113,14 +1087,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSourceFactory = new HlsMediaSource.Factory(
|
mediaSourceFactory = new HlsMediaSource.Factory(
|
||||||
dataSourceFactory)
|
dataSourceFactory
|
||||||
.setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation());
|
).setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation());
|
||||||
break;
|
break;
|
||||||
case CONTENT_TYPE_OTHER:
|
case CONTENT_TYPE_OTHER:
|
||||||
if ("asset".equals(uri.getScheme())) {
|
if ("asset".equals(uri.getScheme())) {
|
||||||
try {
|
try {
|
||||||
DataSource.Factory assetDataSourceFactory = DataSourceUtil
|
DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri);
|
||||||
.buildAssetDataSourceFactory(themedReactContext, uri);
|
|
||||||
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException("cannot open input file:" + uri);
|
throw new IllegalStateException("cannot open input file:" + uri);
|
||||||
|
|
@ -1128,10 +1101,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
} else if ("file".equals(uri.getScheme()) ||
|
} else if ("file".equals(uri.getScheme()) ||
|
||||||
!useCache) {
|
!useCache) {
|
||||||
mediaSourceFactory = new ProgressiveMediaSource.Factory(
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(
|
||||||
mediaDataSourceFactory);
|
mediaDataSourceFactory
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
mediaSourceFactory = new ProgressiveMediaSource.Factory(
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(
|
||||||
RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -1150,19 +1125,20 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
if (cmcdConfigurationFactory != null) {
|
if (cmcdConfigurationFactory != null) {
|
||||||
mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory(
|
mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory(
|
||||||
cmcdConfigurationFactory::createCmcdConfiguration);
|
cmcdConfigurationFactory::createCmcdConfiguration
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSourceFactory = Objects.requireNonNullElse(
|
mediaSourceFactory = Objects.requireNonNullElse(
|
||||||
ReactNativeVideoManager.Companion.getInstance()
|
ReactNativeVideoManager.Companion.getInstance()
|
||||||
.overrideMediaSourceFactory(source, mediaSourceFactory, mediaDataSourceFactory),
|
.overrideMediaSourceFactory(source, mediaSourceFactory, mediaDataSourceFactory),
|
||||||
mediaSourceFactory);
|
mediaSourceFactory
|
||||||
|
);
|
||||||
|
|
||||||
mediaItemBuilder.setStreamKeys(streamKeys);
|
mediaItemBuilder.setStreamKeys(streamKeys);
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance()
|
final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance().overrideMediaItemBuilder(source, mediaItemBuilder);
|
||||||
.overrideMediaItemBuilder(source, mediaItemBuilder);
|
|
||||||
|
|
||||||
MediaItem mediaItem = overridenMediaItemBuilder != null
|
MediaItem mediaItem = overridenMediaItemBuilder != null
|
||||||
? overridenMediaItemBuilder.build()
|
? overridenMediaItemBuilder.build()
|
||||||
|
|
@ -1171,7 +1147,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
MediaSource mediaSource = mediaSourceFactory
|
MediaSource mediaSource = mediaSourceFactory
|
||||||
.setDrmSessionManagerProvider(drmProvider)
|
.setDrmSessionManagerProvider(drmProvider)
|
||||||
.setLoadErrorHandlingPolicy(
|
.setLoadErrorHandlingPolicy(
|
||||||
config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount()))
|
config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount())
|
||||||
|
)
|
||||||
.createMediaSource(mediaItem);
|
.createMediaSource(mediaItem);
|
||||||
|
|
||||||
if (cropStartMs >= 0 && cropEndMs >= 0) {
|
if (cropStartMs >= 0 && cropEndMs >= 0) {
|
||||||
|
|
@ -1206,8 +1183,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(
|
MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
|
||||||
track.getUri())
|
|
||||||
.setId(trackId)
|
.setId(trackId)
|
||||||
.setMimeType(track.getType())
|
.setMimeType(track.getType())
|
||||||
.setLabel(label)
|
.setLabel(label)
|
||||||
|
|
@ -1218,8 +1194,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
configBuilder.setLanguage(track.getLanguage());
|
configBuilder.setLanguage(track.getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set selection flags - make first track default if no specific track is
|
// Set selection flags - make first track default if no specific track is selected
|
||||||
// selected
|
|
||||||
if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) {
|
if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) {
|
||||||
configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT);
|
configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1229,12 +1204,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build();
|
MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build();
|
||||||
subtitleConfigurations.add(subtitleConfiguration);
|
subtitleConfigurations.add(subtitleConfiguration);
|
||||||
|
|
||||||
DebugLog.d(TAG,
|
DebugLog.d(TAG, "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")");
|
||||||
"Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")");
|
|
||||||
trackIndex++;
|
trackIndex++;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
DebugLog.e(TAG,
|
DebugLog.e(TAG, "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage());
|
||||||
"Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1303,8 +1276,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
case AudioManager.AUDIOFOCUS_LOSS:
|
case AudioManager.AUDIOFOCUS_LOSS:
|
||||||
view.hasAudioFocus = false;
|
view.hasAudioFocus = false;
|
||||||
view.eventEmitter.onAudioFocusChanged.invoke(false);
|
view.eventEmitter.onAudioFocusChanged.invoke(false);
|
||||||
// FIXME this pause can cause issue if content doesn't have pause capability
|
// FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel)
|
||||||
// (can happen on live channel)
|
|
||||||
if (activity != null) {
|
if (activity != null) {
|
||||||
activity.runOnUiThread(view::pausePlayback);
|
activity.runOnUiThread(view::pausePlayback);
|
||||||
}
|
}
|
||||||
|
|
@ -1325,12 +1297,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
|
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
|
||||||
// Lower the volume
|
// Lower the volume
|
||||||
if (!view.muted) {
|
if (!view.muted) {
|
||||||
activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 0.8f));
|
activity.runOnUiThread(() ->
|
||||||
|
view.player.setVolume(view.audioVolume * 0.8f)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
|
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
|
||||||
// Raise it back to normal
|
// Raise it back to normal
|
||||||
if (!view.muted) {
|
if (!view.muted) {
|
||||||
activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 1));
|
activity.runOnUiThread(() ->
|
||||||
|
view.player.setVolume(view.audioVolume * 1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1403,8 +1379,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
/**
|
/**
|
||||||
* Returns a new DataSource factory.
|
* Returns a new DataSource factory.
|
||||||
*
|
*
|
||||||
* @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener
|
* @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
|
||||||
* to the new
|
|
||||||
* DataSource factory.
|
* DataSource factory.
|
||||||
* @return A new DataSource factory.
|
* @return A new DataSource factory.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1416,14 +1391,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
/**
|
/**
|
||||||
* Returns a new HttpDataSource factory.
|
* Returns a new HttpDataSource factory.
|
||||||
*
|
*
|
||||||
* @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener
|
* @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
|
||||||
* to the new
|
|
||||||
* DataSource factory.
|
* DataSource factory.
|
||||||
* @return A new HttpDataSource factory.
|
* @return A new HttpDataSource factory.
|
||||||
*/
|
*/
|
||||||
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
|
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
|
||||||
return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext,
|
return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, source.getHeaders());
|
||||||
useBandwidthMeter ? bandwidthMeter : null, source.getHeaders());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioBecomingNoisyListener implementation
|
// AudioBecomingNoisyListener implementation
|
||||||
|
|
@ -1440,13 +1413,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEvents(@NonNull Player player, Player.Events events) {
|
public void onEvents(@NonNull Player player, Player.Events events) {
|
||||||
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
|
if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
||||||
|| events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
|
||||||
int playbackState = player.getPlaybackState();
|
int playbackState = player.getPlaybackState();
|
||||||
boolean playWhenReady = player.getPlayWhenReady();
|
boolean playWhenReady = player.getPlayWhenReady();
|
||||||
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
||||||
eventEmitter.onPlaybackRateChange
|
eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
||||||
.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
|
||||||
switch (playbackState) {
|
switch (playbackState) {
|
||||||
case Player.STATE_IDLE:
|
case Player.STATE_IDLE:
|
||||||
text += "idle";
|
text += "idle";
|
||||||
|
|
@ -1503,10 +1474,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The progress message handler will duplicate recursions of the
|
* The progress message handler will duplicate recursions of the onProgressMessage handler
|
||||||
* onProgressMessage handler
|
* on change of player state from any state to STATE_READY with playWhenReady is true (when
|
||||||
* on change of player state from any state to STATE_READY with playWhenReady is
|
|
||||||
* true (when
|
|
||||||
* the video is not paused). This clears all existing messages.
|
* the video is not paused). This clears all existing messages.
|
||||||
*/
|
*/
|
||||||
private void clearProgressMessageHandler() {
|
private void clearProgressMessageHandler() {
|
||||||
|
|
@ -1526,8 +1495,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
setSelectedTextTrack(textTrackType, textTrackValue);
|
setSelectedTextTrack(textTrackType, textTrackValue);
|
||||||
}
|
}
|
||||||
Format videoFormat = player.getVideoFormat();
|
Format videoFormat = player.getVideoFormat();
|
||||||
boolean isRotatedContent = videoFormat != null
|
boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
||||||
&& (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
|
||||||
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
|
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
|
||||||
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
|
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
|
||||||
String trackId = videoFormat != null ? videoFormat.id : null;
|
String trackId = videoFormat != null ? videoFormat.id : null;
|
||||||
|
|
@ -1541,8 +1509,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
if (source.getContentStartTime() != -1) {
|
if (source.getContentStartTime() != -1) {
|
||||||
ExecutorService es = Executors.newSingleThreadExecutor();
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
||||||
es.execute(() -> {
|
es.execute(() -> {
|
||||||
// To prevent ANRs caused by getVideoTrackInfo we run this on a different thread
|
// To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done
|
||||||
// and notify the player only when we're done
|
|
||||||
ArrayList<VideoTrack> videoTracks = getVideoTrackInfoFromManifest();
|
ArrayList<VideoTrack> videoTracks = getVideoTrackInfoFromManifest();
|
||||||
if (videoTracks != null) {
|
if (videoTracks != null) {
|
||||||
isUsingContentResolution = true;
|
isUsingContentResolution = true;
|
||||||
|
|
@ -1586,6 +1553,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
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 groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
||||||
TrackGroup group = groups.get(groupIndex);
|
TrackGroup group = groups.get(groupIndex);
|
||||||
Format format = group.getFormat(0);
|
Format format = group.getFormat(0);
|
||||||
|
|
@ -1611,8 +1579,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height);
|
videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height);
|
||||||
videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||||
videoTrack.setRotation(format.rotationDegrees);
|
videoTrack.setRotation(format.rotationDegrees);
|
||||||
if (format.codecs != null)
|
if (format.codecs != null) videoTrack.setCodecs(format.codecs);
|
||||||
videoTrack.setCodecs(format.codecs);
|
|
||||||
videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id);
|
videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id);
|
||||||
videoTrack.setIndex(trackIndex);
|
videoTrack.setIndex(trackIndex);
|
||||||
return videoTrack;
|
return videoTrack;
|
||||||
|
|
@ -1649,8 +1616,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
return this.getVideoTrackInfoFromManifest(0);
|
return this.getVideoTrackInfoFromManifest(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need retry count to in case where minefest request fails from poor network
|
// We need retry count to in case where minefest request fails from poor network conditions
|
||||||
// conditions
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) {
|
private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) {
|
||||||
ExecutorService es = Executors.newSingleThreadExecutor();
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
||||||
|
|
@ -1670,15 +1636,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
int periodCount = manifest.getPeriodCount();
|
int periodCount = manifest.getPeriodCount();
|
||||||
for (int i = 0; i < periodCount; i++) {
|
for (int i = 0; i < periodCount; i++) {
|
||||||
Period period = manifest.getPeriod(i);
|
Period period = manifest.getPeriod(i);
|
||||||
for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets
|
for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) {
|
||||||
.size(); adaptationIndex++) {
|
|
||||||
AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex);
|
AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex);
|
||||||
if (adaptation.type != C.TRACK_TYPE_VIDEO) {
|
if (adaptation.type != C.TRACK_TYPE_VIDEO) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
boolean hasFoundContentPeriod = false;
|
boolean hasFoundContentPeriod = false;
|
||||||
for (int representationIndex = 0; representationIndex < adaptation.representations
|
for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) {
|
||||||
.size(); representationIndex++) {
|
|
||||||
Representation representation = adaptation.representations.get(representationIndex);
|
Representation representation = adaptation.representations.get(representationIndex);
|
||||||
Format format = representation.format;
|
Format format = representation.format;
|
||||||
if (isFormatSupported(format)) {
|
if (isFormatSupported(format)) {
|
||||||
|
|
@ -1686,8 +1650,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
hasFoundContentPeriod = true;
|
hasFoundContentPeriod = true;
|
||||||
VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format,
|
VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, representationIndex);
|
||||||
representationIndex);
|
|
||||||
videoTracks.add(videoTrack);
|
videoTracks.add(videoTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1717,16 +1680,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection,
|
private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, TrackGroup group) {
|
||||||
TrackGroup group) {
|
|
||||||
Track track = new Track();
|
Track track = new Track();
|
||||||
track.setIndex(trackIndex);
|
track.setIndex(trackIndex);
|
||||||
if (format.sampleMimeType != null)
|
if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType);
|
||||||
track.setMimeType(format.sampleMimeType);
|
if (format.language != null) track.setLanguage(format.language);
|
||||||
if (format.language != null)
|
if (format.label != null) track.setTitle(format.label);
|
||||||
track.setLanguage(format.language);
|
|
||||||
if (format.label != null)
|
|
||||||
track.setTitle(format.label);
|
|
||||||
track.setSelected(isTrackSelected(selection, group, trackIndex));
|
track.setSelected(isTrackSelected(selection, group, trackIndex));
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
@ -1796,8 +1755,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
track.setLanguage(format.language != null ? format.language : "unknown");
|
track.setLanguage(format.language != null ? format.language : "unknown");
|
||||||
track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1));
|
track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1));
|
||||||
track.setSelected(false); // Don't report selection status - let PlayerView handle it
|
track.setSelected(false); // Don't report selection status - let PlayerView handle it
|
||||||
if (format.sampleMimeType != null)
|
if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType);
|
||||||
track.setMimeType(format.sampleMimeType);
|
|
||||||
track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
||||||
|
|
||||||
tracks.add(track);
|
tracks.add(track);
|
||||||
|
|
@ -1828,10 +1786,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
Track textTrack = new Track();
|
Track textTrack = new Track();
|
||||||
textTrack.setIndex(textTracks.size());
|
textTrack.setIndex(textTracks.size());
|
||||||
if (format.sampleMimeType != null)
|
if (format.sampleMimeType != null) textTrack.setMimeType(format.sampleMimeType);
|
||||||
textTrack.setMimeType(format.sampleMimeType);
|
if (format.language != null) textTrack.setLanguage(format.language);
|
||||||
if (format.language != null)
|
|
||||||
textTrack.setLanguage(format.language);
|
|
||||||
|
|
||||||
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
||||||
|
|
||||||
|
|
@ -1865,34 +1821,28 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
|
public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
|
||||||
@NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
|
|
||||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
isSeeking = true;
|
isSeeking = true;
|
||||||
seekPosition = newPosition.positionMs;
|
seekPosition = newPosition.positionMs;
|
||||||
if (isUsingContentResolution) {
|
if (isUsingContentResolution) {
|
||||||
// We need to update the selected track to make sure that it still matches user
|
// We need to update the selected track to make sure that it still matches user selection if track list has changed in this period
|
||||||
// selection if track list has changed in this period
|
|
||||||
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerNeedsSource) {
|
if (playerNeedsSource) {
|
||||||
// This will only occur if the user has performed a seek whilst in the error
|
// This will only occur if the user has performed a seek whilst in the error state. Update the
|
||||||
// state. Update the
|
// resume position so that if the user then retries, playback will resume from the position to
|
||||||
// resume position so that if the user then retries, playback will resume from
|
|
||||||
// the position to
|
|
||||||
// which they seeked.
|
// which they seeked.
|
||||||
updateResumePosition();
|
updateResumePosition();
|
||||||
}
|
}
|
||||||
if (isUsingContentResolution) {
|
if (isUsingContentResolution) {
|
||||||
// Discontinuity events might have a different track list so we update the
|
// Discontinuity events might have a different track list so we update the selected track
|
||||||
// selected track
|
|
||||||
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
||||||
selectTrackWhenReady = true;
|
selectTrackWhenReady = true;
|
||||||
}
|
}
|
||||||
// When repeat is turned on, reaching the end of the video will not cause a
|
// When repeat is turned on, reaching the end of the video will not cause a state change
|
||||||
// state change
|
|
||||||
// so we need to explicitly detect it.
|
// so we need to explicitly detect it.
|
||||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
||||||
&& player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
|
&& player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
|
||||||
|
|
@ -1940,17 +1890,15 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
updateSubtitleButtonVisibility();
|
updateSubtitleButtonVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private boolean hasBuiltInTextTracks() {
|
private boolean hasBuiltInTextTracks() {
|
||||||
if (player == null || trackSelector == null)
|
if (player == null || trackSelector == null) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
||||||
if (info == null)
|
if (info == null) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
||||||
if (textRendererIndex == C.INDEX_UNSET)
|
if (textRendererIndex == C.INDEX_UNSET) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
||||||
|
|
||||||
|
|
@ -1970,8 +1918,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSubtitleButtonVisibility() {
|
private void updateSubtitleButtonVisibility() {
|
||||||
if (exoPlayerView == null)
|
if (exoPlayerView == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
boolean hasTextTracks = (source.getSideLoadedTextTracks() != null &&
|
boolean hasTextTracks = (source.getSideLoadedTextTracks() != null &&
|
||||||
!source.getSideLoadedTextTracks().getTracks().isEmpty()) ||
|
!source.getSideLoadedTextTracks().getTracks().isEmpty()) ||
|
||||||
|
|
@ -1995,8 +1942,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
if (isPlaying && isSeeking) {
|
if (isPlaying && isSeeking) {
|
||||||
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
||||||
}
|
}
|
||||||
PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder,
|
PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, pictureInPictureReceiver, !isPlaying);
|
||||||
pictureInPictureReceiver, !isPlaying);
|
|
||||||
eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);
|
eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
|
|
@ -2015,8 +1961,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
|
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
|
||||||
case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED:
|
case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED:
|
||||||
if (!hasDrmFailed) {
|
if (!hasDrmFailed) {
|
||||||
// When DRM fails to reach the app level certificate server it will fail with a
|
// When DRM fails to reach the app level certificate server it will fail with a source error so we assume that it is DRM related and try one more time
|
||||||
// source error so we assume that it is DRM related and try one more time
|
|
||||||
hasDrmFailed = true;
|
hasDrmFailed = true;
|
||||||
playerNeedsSource = true;
|
playerNeedsSource = true;
|
||||||
updateResumePosition();
|
updateResumePosition();
|
||||||
|
|
@ -2098,16 +2043,14 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
boolean isSourceEqual = source.isEquals(this.source);
|
boolean isSourceEqual = source.isEquals(this.source);
|
||||||
hasDrmFailed = false;
|
hasDrmFailed = false;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
final DataSource.Factory tmpMediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(
|
final DataSource.Factory tmpMediaDataSourceFactory =
|
||||||
this.themedReactContext, bandwidthMeter,
|
DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
|
||||||
source.getHeaders());
|
source.getHeaders());
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance()
|
final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance().overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory);
|
||||||
.overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory);
|
|
||||||
|
|
||||||
this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory,
|
this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory, tmpMediaDataSourceFactory);
|
||||||
tmpMediaDataSourceFactory);
|
|
||||||
|
|
||||||
if (source.getCmcdProps() != null) {
|
if (source.getCmcdProps() != null) {
|
||||||
CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps());
|
CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps());
|
||||||
|
|
@ -2126,7 +2069,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
clearSrc();
|
clearSrc();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearSrc() {
|
public void clearSrc() {
|
||||||
if (source.getUri() != null) {
|
if (source.getUri() != null) {
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
|
|
@ -2175,8 +2117,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disableTrack(int rendererIndex) {
|
public void disableTrack(int rendererIndex) {
|
||||||
if (trackSelector == null)
|
if (trackSelector == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
|
|
@ -2186,8 +2127,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectTextTrackInternal(String type, String value) {
|
private void selectTextTrackInternal(String type, String value) {
|
||||||
if (player == null || trackSelector == null)
|
if (player == null || trackSelector == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value);
|
DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value);
|
||||||
|
|
||||||
|
|
@ -2207,11 +2147,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
||||||
boolean trackFound = false;
|
boolean trackFound = false;
|
||||||
|
|
||||||
// react-native-video uses a flattened `textTracks` list on the JS side.
|
|
||||||
// For HLS/DASH, each TrackGroup often contains a single track at index 0,
|
|
||||||
// so comparing against `trackIndex` alone makes only the first subtitle selectable.
|
|
||||||
int flattenedIndex = 0;
|
|
||||||
|
|
||||||
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|
||||||
TrackGroup group = groups.get(groupIndex);
|
TrackGroup group = groups.get(groupIndex);
|
||||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||||
|
|
@ -2224,13 +2159,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
isMatch = true;
|
isMatch = true;
|
||||||
} else if ("index".equals(type)) {
|
} else if ("index".equals(type)) {
|
||||||
int targetIndex = ReactBridgeUtils.safeParseInt(value, -1);
|
int targetIndex = ReactBridgeUtils.safeParseInt(value, -1);
|
||||||
if (targetIndex == flattenedIndex) {
|
if (targetIndex == trackIndex) {
|
||||||
isMatch = true;
|
isMatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flattenedIndex++;
|
|
||||||
|
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
TrackSelectionOverride override = new TrackSelectionOverride(group,
|
TrackSelectionOverride override = new TrackSelectionOverride(group,
|
||||||
java.util.Arrays.asList(trackIndex));
|
java.util.Arrays.asList(trackIndex));
|
||||||
|
|
@ -2239,8 +2172,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trackFound)
|
if (trackFound) break;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!trackFound) {
|
if (!trackFound) {
|
||||||
|
|
@ -2266,8 +2198,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectedTrack(int trackType, String type, String value) {
|
public void setSelectedTrack(int trackType, String type, String value) {
|
||||||
if (player == null || trackSelector == null)
|
if (player == null || trackSelector == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (controls) {
|
if (controls) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2341,11 +2272,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
usingExactMatch = true;
|
usingExactMatch = true;
|
||||||
break;
|
break;
|
||||||
} else if (isUsingContentResolution) {
|
} else if (isUsingContentResolution) {
|
||||||
// When using content resolution rather than ads, we need to try and find the
|
// When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
|
||||||
// closest match if there is no exact match
|
|
||||||
if (closestFormat != null) {
|
if (closestFormat != null) {
|
||||||
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height)
|
if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
|
||||||
&& format.height < height) {
|
|
||||||
// Higher quality match
|
// Higher quality match
|
||||||
closestFormat = format;
|
closestFormat = format;
|
||||||
closestTrackIndex = j;
|
closestTrackIndex = j;
|
||||||
|
|
@ -2356,8 +2285,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// This is a fallback if the new period contains only higher resolutions than
|
// This is a fallback if the new period contains only higher resolutions than the user has selected
|
||||||
// the user has selected
|
|
||||||
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
|
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
|
||||||
// No close match found - so we pick the lowest quality
|
// No close match found - so we pick the lowest quality
|
||||||
int minHeight = Integer.MAX_VALUE;
|
int minHeight = Integer.MAX_VALUE;
|
||||||
|
|
@ -2380,8 +2308,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
|
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
|
||||||
// Use system settings if possible
|
// Use system settings if possible
|
||||||
CaptioningManager captioningManager = (CaptioningManager) themedReactContext
|
CaptioningManager captioningManager
|
||||||
.getSystemService(Context.CAPTIONING_SERVICE);
|
= (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE);
|
||||||
if (captioningManager != null && captioningManager.isEnabled()) {
|
if (captioningManager != null && captioningManager.isEnabled()) {
|
||||||
groupIndex = getGroupIndexForDefaultLocale(groups);
|
groupIndex = getGroupIndexForDefaultLocale(groups);
|
||||||
}
|
}
|
||||||
|
|
@ -2436,8 +2364,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
.setRendererDisabled(rendererIndex, false);
|
.setRendererDisabled(rendererIndex, false);
|
||||||
|
|
||||||
// Clear existing overrides for this track type to avoid conflicts
|
// Clear existing overrides for this track type to avoid conflicts
|
||||||
// But be careful with audio tracks - don't clear unless explicitly selecting a
|
// But be careful with audio tracks - don't clear unless explicitly selecting a different track
|
||||||
// different track
|
|
||||||
if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) {
|
if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) {
|
||||||
selectionParameters.clearOverridesOfType(selectionOverride.getType());
|
selectionParameters.clearOverridesOfType(selectionOverride.getType());
|
||||||
}
|
}
|
||||||
|
|
@ -2505,8 +2432,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
public void setSelectedVideoTrack(String type, String value) {
|
public void setSelectedVideoTrack(String type, String value) {
|
||||||
videoTrackType = type;
|
videoTrackType = type;
|
||||||
videoTrackValue = value;
|
videoTrackValue = value;
|
||||||
if (!loadVideoStarted)
|
if (!loadVideoStarted) setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
||||||
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectedAudioTrack(String type, String value) {
|
public void setSelectedAudioTrack(String type, String value) {
|
||||||
|
|
@ -2537,11 +2463,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) {
|
public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) {
|
||||||
this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && enterPictureInPictureOnLeave;
|
||||||
&& enterPictureInPictureOnLeave;
|
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder,
|
PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave);
|
||||||
this.enterPictureInPictureOnLeave);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2549,14 +2473,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture);
|
eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture);
|
||||||
|
|
||||||
if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) {
|
if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) {
|
||||||
if (isInPictureInPicture)
|
if (isInPictureInPicture) fullScreenPlayerView.hideWithoutPlayer();
|
||||||
fullScreenPlayerView.hideWithoutPlayer();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Activity currentActivity = themedReactContext.getCurrentActivity();
|
Activity currentActivity = themedReactContext.getCurrentActivity();
|
||||||
if (currentActivity == null)
|
if (currentActivity == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
View decorView = currentActivity.getWindow().getDecorView();
|
View decorView = currentActivity.getWindow().getDecorView();
|
||||||
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
||||||
|
|
@ -2592,12 +2514,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
public void enterPictureInPictureMode() {
|
public void enterPictureInPictureMode() {
|
||||||
PictureInPictureParams _pipParams = null;
|
PictureInPictureParams _pipParams = null;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
ArrayList<RemoteAction> actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext,
|
ArrayList<RemoteAction> actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver);
|
||||||
isPaused, pictureInPictureReceiver);
|
|
||||||
pictureInPictureParamsBuilder.setActions(actions);
|
pictureInPictureParamsBuilder.setActions(actions);
|
||||||
if (player.getPlaybackState() == Player.STATE_READY) {
|
if (player.getPlaybackState() == Player.STATE_READY) {
|
||||||
pictureInPictureParamsBuilder
|
pictureInPictureParamsBuilder.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player));
|
||||||
.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player));
|
|
||||||
}
|
}
|
||||||
_pipParams = pictureInPictureParamsBuilder.build();
|
_pipParams = pictureInPictureParamsBuilder.build();
|
||||||
}
|
}
|
||||||
|
|
@ -2606,15 +2526,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
|
|
||||||
public void exitPictureInPictureMode() {
|
public void exitPictureInPictureMode() {
|
||||||
Activity currentActivity = themedReactContext.getCurrentActivity();
|
Activity currentActivity = themedReactContext.getCurrentActivity();
|
||||||
if (currentActivity == null)
|
if (currentActivity == null) return;
|
||||||
return;
|
|
||||||
|
|
||||||
View decorView = currentActivity.getWindow().getDecorView();
|
View decorView = currentActivity.getWindow().getDecorView();
|
||||||
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
||||||
|
|
||||||
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
||||||
if (exoPlayerView.getParent().equals(rootView))
|
if (exoPlayerView.getParent().equals(rootView)) rootView.removeView(exoPlayerView);
|
||||||
rootView.removeView(exoPlayerView);
|
|
||||||
for (int i = 0; i < rootView.getChildCount(); i++) {
|
for (int i = 0; i < rootView.getChildCount(); i++) {
|
||||||
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
||||||
}
|
}
|
||||||
|
|
@ -2741,8 +2659,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null,
|
fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, new OnBackPressedCallback(true) {
|
||||||
new OnBackPressedCallback(true) {
|
|
||||||
@Override
|
@Override
|
||||||
public void handleOnBackPressed() {
|
public void handleOnBackPressed() {
|
||||||
setFullscreen(false);
|
setFullscreen(false);
|
||||||
|
|
@ -2784,8 +2701,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId,
|
public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, @NonNull Exception e) {
|
||||||
@NonNull Exception e) {
|
|
||||||
DebugLog.d("DRM Info", "onDrmSessionManagerError");
|
DebugLog.d("DRM Info", "onDrmSessionManagerError");
|
||||||
eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002");
|
eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002");
|
||||||
}
|
}
|
||||||
|
|
@ -2845,7 +2761,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
Map<String, String> errMap = Map.of(
|
Map<String, String> errMap = Map.of(
|
||||||
"message", error.getMessage(),
|
"message", error.getMessage(),
|
||||||
"code", String.valueOf(error.getErrorCode()),
|
"code", String.valueOf(error.getErrorCode()),
|
||||||
"type", String.valueOf(error.getErrorType()));
|
"type", String.valueOf(error.getErrorType())
|
||||||
|
);
|
||||||
eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap);
|
eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap);
|
||||||
|
|
||||||
handleDaiBackupStream();
|
handleDaiBackupStream();
|
||||||
|
|
@ -2879,8 +2796,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
* @return The configured IMA server-side ad insertion AdsLoader
|
* @return The configured IMA server-side ad insertion AdsLoader
|
||||||
*/
|
*/
|
||||||
private ImaServerSideAdInsertionMediaSource.AdsLoader createAdsLoader() {
|
private ImaServerSideAdInsertionMediaSource.AdsLoader createAdsLoader() {
|
||||||
ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(
|
ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder =
|
||||||
getContext(), exoPlayerView.getPlayerView())
|
new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(getContext(), exoPlayerView.getPlayerView())
|
||||||
.setAdEventListener(this)
|
.setAdEventListener(this)
|
||||||
.setAdErrorListener(this);
|
.setAdErrorListener(this);
|
||||||
|
|
||||||
|
|
@ -2898,8 +2815,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(getContext());
|
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(getContext());
|
||||||
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory);
|
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory);
|
||||||
|
|
||||||
ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = new ImaServerSideAdInsertionMediaSource.Factory(
|
ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory =
|
||||||
daiAdsLoader, mediaSourceFactory);
|
new ImaServerSideAdInsertionMediaSource.Factory(daiAdsLoader, mediaSourceFactory);
|
||||||
|
|
||||||
mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory);
|
mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory);
|
||||||
|
|
||||||
|
|
@ -2933,8 +2850,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
/**
|
/**
|
||||||
* Requests a DAI stream from Google IMA using the ExoPlayer IMA extension.
|
* Requests a DAI stream from Google IMA using the ExoPlayer IMA extension.
|
||||||
*
|
*
|
||||||
* Builds an SSAI URI based on the provided parameters and sets it on the
|
* Builds an SSAI URI based on the provided parameters and sets it on the player.
|
||||||
* player.
|
|
||||||
* Supports both VOD (contentSourceId + videoId) and Live (assetKey) streams.
|
* Supports both VOD (contentSourceId + videoId) and Live (assetKey) streams.
|
||||||
*
|
*
|
||||||
* @param runningSource The source containing DAI properties
|
* @param runningSource The source containing DAI properties
|
||||||
|
|
@ -2967,8 +2883,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
.build()
|
.build()
|
||||||
.buildUpon();
|
.buildUpon();
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException("Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided");
|
||||||
"Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> adTagParameters = adsProps.getAdTagParameters();
|
Map<String, String> adTagParameters = adsProps.getAdTagParameters();
|
||||||
|
|
@ -2991,8 +2906,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
||||||
/**
|
/**
|
||||||
* Handles fallback to backup stream when DAI stream fails.
|
* Handles fallback to backup stream when DAI stream fails.
|
||||||
*
|
*
|
||||||
* If a backup stream URI is available in the DAI properties, it cleans up DAI
|
* If a backup stream URI is available in the DAI properties, it cleans up DAI resources
|
||||||
* resources
|
|
||||||
* and switches to the backup stream.
|
* and switches to the backup stream.
|
||||||
*
|
*
|
||||||
* @return true if backup stream was successfully used, false otherwise
|
* @return true if backup stream was successfully used, false otherwise
|
||||||
|
|
|
||||||
1342
package-lock.json
generated
1342
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -40,7 +40,7 @@
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^54",
|
"expo": "^54.0.33",
|
||||||
"expo-application": "~7.0.7",
|
"expo-application": "~7.0.7",
|
||||||
"expo-auth-session": "~7.0.8",
|
"expo-auth-session": "~7.0.8",
|
||||||
"expo-blur": "~15.0.7",
|
"expo-blur": "~15.0.7",
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ export interface AppSettings {
|
||||||
tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code)
|
tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code)
|
||||||
episodeLayoutStyle: 'vertical' | 'horizontal'; // Layout style for episode cards
|
episodeLayoutStyle: 'vertical' | 'horizontal'; // Layout style for episode cards
|
||||||
autoplayBestStream: boolean; // Automatically play the best available stream
|
autoplayBestStream: boolean; // Automatically play the best available stream
|
||||||
|
autoplayPreferredQuality: string; // Preferred quality for autoplay (e.g., '4K', '1080p', '720p', '480p')
|
||||||
|
autoplayPreferredLanguage: string; // Preferred language for autoplay (e.g., 'English', 'Spanish', 'Any')
|
||||||
// Local scraper settings
|
// Local scraper settings
|
||||||
scraperRepositoryUrl: string; // URL to the scraper repository
|
scraperRepositoryUrl: string; // URL to the scraper repository
|
||||||
enableLocalScrapers: boolean; // Enable/disable local scraper functionality
|
enableLocalScrapers: boolean; // Enable/disable local scraper functionality
|
||||||
|
|
@ -135,6 +137,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||||
tmdbLanguagePreference: 'en', // Default to English
|
tmdbLanguagePreference: 'en', // Default to English
|
||||||
episodeLayoutStyle: 'vertical', // Default to vertical layout for new installs
|
episodeLayoutStyle: 'vertical', // Default to vertical layout for new installs
|
||||||
autoplayBestStream: false, // Disabled by default for user choice
|
autoplayBestStream: false, // Disabled by default for user choice
|
||||||
|
autoplayPreferredQuality: '1080p', // Default to 1080p
|
||||||
|
autoplayPreferredLanguage: 'Any', // Default to Any language
|
||||||
// Local scraper defaults
|
// Local scraper defaults
|
||||||
scraperRepositoryUrl: '',
|
scraperRepositoryUrl: '',
|
||||||
enableLocalScrapers: true,
|
enableLocalScrapers: true,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"try_again": "Try Again",
|
"try_again": "Try Again",
|
||||||
"go_back": "Go Back",
|
"go_back": "Go Back",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"any": "Any",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
|
|
@ -641,6 +642,9 @@
|
||||||
"chinese": "Chinese (Simplified)",
|
"chinese": "Chinese (Simplified)",
|
||||||
"hindi": "Hindi",
|
"hindi": "Hindi",
|
||||||
"serbian": "Serbian",
|
"serbian": "Serbian",
|
||||||
|
"russian": "Russian",
|
||||||
|
"japanese": "Japanese",
|
||||||
|
"korean": "Korean",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"content_discovery": "Content & Discovery",
|
"content_discovery": "Content & Discovery",
|
||||||
"appearance": "Appearance",
|
"appearance": "Appearance",
|
||||||
|
|
@ -1183,6 +1187,10 @@
|
||||||
"powered_by_introdb": "Powered by IntroDB",
|
"powered_by_introdb": "Powered by IntroDB",
|
||||||
"autoplay_title": "Auto-play First Stream",
|
"autoplay_title": "Auto-play First Stream",
|
||||||
"autoplay_desc": "Automatically start the first stream shown in the list.",
|
"autoplay_desc": "Automatically start the first stream shown in the list.",
|
||||||
|
"preferred_quality_title": "Preferred Quality",
|
||||||
|
"preferred_quality_desc": "Select preferred quality for autoplay.",
|
||||||
|
"preferred_language_title": "Preferred Language",
|
||||||
|
"preferred_language_desc": "Select preferred language for autoplay.",
|
||||||
"resume_title": "Always Resume",
|
"resume_title": "Always Resume",
|
||||||
"resume_desc": "Skip the resume prompt and automatically continue where you left off (if less than 85% watched).",
|
"resume_desc": "Skip the resume prompt and automatically continue where you left off (if less than 85% watched).",
|
||||||
"engine_title": "Video Player Engine",
|
"engine_title": "Video Player Engine",
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,138 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Preferred Quality for Autoplay */}
|
||||||
|
<View style={[styles.settingItem, styles.settingItemBorder, { borderTopColor: 'rgba(255,255,255,0.08)', borderTopWidth: 1 }]}>
|
||||||
|
<View style={styles.settingContent}>
|
||||||
|
<View style={[
|
||||||
|
styles.settingIconContainer,
|
||||||
|
{ backgroundColor: 'rgba(255,255,255,0.1)' }
|
||||||
|
]}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="high-quality"
|
||||||
|
size={20}
|
||||||
|
color={currentTheme.colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingText}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.settingTitle,
|
||||||
|
{ color: currentTheme.colors.text },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('player.preferred_quality_title') || 'Preferred Quality'}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.settingDescription,
|
||||||
|
{ color: currentTheme.colors.textMuted },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('player.preferred_quality_desc') || 'Select preferred quality for autoplay'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.optionButtonsRow}>
|
||||||
|
{([
|
||||||
|
{ id: '4K', label: '4K' },
|
||||||
|
{ id: '1080p', label: '1080p' },
|
||||||
|
{ id: '720p', label: '720p' },
|
||||||
|
{ id: '480p', label: '480p' },
|
||||||
|
] as const).map((option) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={option.id}
|
||||||
|
onPress={() => updateSetting('autoplayPreferredQuality', option.id)}
|
||||||
|
style={[
|
||||||
|
styles.optionButton,
|
||||||
|
settings.autoplayPreferredQuality === option.id && { backgroundColor: currentTheme.colors.primary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.optionButtonText,
|
||||||
|
{ color: settings.autoplayPreferredQuality === option.id ? '#fff' : currentTheme.colors.text },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Preferred Language for Autoplay */}
|
||||||
|
<View style={[styles.settingItem, styles.settingItemBorder, { borderTopColor: 'rgba(255,255,255,0.08)', borderTopWidth: 1 }]}>
|
||||||
|
<View style={styles.settingContent}>
|
||||||
|
<View style={[
|
||||||
|
styles.settingIconContainer,
|
||||||
|
{ backgroundColor: 'rgba(255,255,255,0.1)' }
|
||||||
|
]}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="language"
|
||||||
|
size={20}
|
||||||
|
color={currentTheme.colors.primary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingText}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.settingTitle,
|
||||||
|
{ color: currentTheme.colors.text },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('player.preferred_language_title') || 'Preferred Language'}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.settingDescription,
|
||||||
|
{ color: currentTheme.colors.textMuted },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t('player.preferred_language_desc') || 'Select preferred language for autoplay'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.optionButtonsRowScroll}
|
||||||
|
>
|
||||||
|
{([
|
||||||
|
{ id: 'Any', label: t('common.any') || 'Any' },
|
||||||
|
{ id: 'English', label: t('settings.english') || 'English' },
|
||||||
|
{ id: 'Spanish', label: t('settings.spanish') || 'Spanish' },
|
||||||
|
{ id: 'French', label: t('settings.french') || 'French' },
|
||||||
|
{ id: 'German', label: t('settings.german') || 'German' },
|
||||||
|
{ id: 'Italian', label: t('settings.italian') || 'Italian' },
|
||||||
|
{ id: 'Portuguese', label: t('settings.portuguese') || 'Portuguese' },
|
||||||
|
{ id: 'Russian', label: t('settings.russian') || 'Russian' },
|
||||||
|
{ id: 'Hindi', label: t('settings.hindi') || 'Hindi' },
|
||||||
|
{ id: 'Chinese', label: t('settings.chinese') || 'Chinese' },
|
||||||
|
{ id: 'Japanese', label: t('settings.japanese') || 'Japanese' },
|
||||||
|
{ id: 'Korean', label: t('settings.korean') || 'Korean' },
|
||||||
|
] as const).map((option) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={option.id}
|
||||||
|
onPress={() => updateSetting('autoplayPreferredLanguage', option.id)}
|
||||||
|
style={[
|
||||||
|
styles.optionButton,
|
||||||
|
styles.optionButtonLanguage,
|
||||||
|
settings.autoplayPreferredLanguage === option.id && { backgroundColor: currentTheme.colors.primary },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.optionButtonText,
|
||||||
|
{ color: settings.autoplayPreferredLanguage === option.id ? '#fff' : currentTheme.colors.text },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Video Player Engine for Android */}
|
{/* Video Player Engine for Android */}
|
||||||
{Platform.OS === 'android' && !settings.useExternalPlayer && (
|
{Platform.OS === 'android' && !settings.useExternalPlayer && (
|
||||||
|
|
@ -653,6 +784,12 @@ const styles = StyleSheet.create({
|
||||||
paddingHorizontal: 52,
|
paddingHorizontal: 52,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
},
|
},
|
||||||
|
optionButtonsRowScroll: {
|
||||||
|
paddingHorizontal: 52,
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 12,
|
||||||
|
paddingBottom: 4,
|
||||||
|
},
|
||||||
optionButton: {
|
optionButton: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
|
|
@ -662,6 +799,10 @@ const styles = StyleSheet.create({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
|
optionButtonLanguage: {
|
||||||
|
minWidth: 90,
|
||||||
|
flex: 0,
|
||||||
|
},
|
||||||
optionButtonWide: {
|
optionButtonWide: {
|
||||||
flex: 1.5,
|
flex: 1.5,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -246,20 +246,67 @@ export const useStreamsScreen = () => {
|
||||||
|
|
||||||
if (allStreams.length === 0) return null;
|
if (allStreams.length === 0) return null;
|
||||||
|
|
||||||
// Sort primarily by provider priority, then respect the addon's internal order (originalIndex)
|
// Map preferred quality to numeric value
|
||||||
// This ensures if an addon lists 1080p before 4K, we pick 1080p
|
const targetQuality = settings.autoplayPreferredQuality === '4K' ? 2160 : parseInt(settings.autoplayPreferredQuality, 10) || 1080;
|
||||||
allStreams.sort((a, b) => {
|
const preferredLanguage = settings.autoplayPreferredLanguage;
|
||||||
|
|
||||||
|
// 1. Try to find streams matching preferred language
|
||||||
|
let languageMatchedStreams = allStreams;
|
||||||
|
if (preferredLanguage && preferredLanguage !== 'Any') {
|
||||||
|
languageMatchedStreams = allStreams.filter(item => {
|
||||||
|
const streamLang = (item.stream.lang || '').toLowerCase();
|
||||||
|
const prefLang = preferredLanguage.toLowerCase();
|
||||||
|
// Match by name if lang is not set, or match by lang property
|
||||||
|
return streamLang === prefLang ||
|
||||||
|
(item.stream.name || '').toLowerCase().includes(prefLang) ||
|
||||||
|
(item.stream.title || '').toLowerCase().includes(prefLang) ||
|
||||||
|
(item.stream.description || '').toLowerCase().includes(prefLang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If no language match (and language wasn't 'Any'), just play the "first" stream
|
||||||
|
if (languageMatchedStreams.length === 0 && allStreams.length > 0) {
|
||||||
|
// Sort by provider priority and original index to find the "first" one
|
||||||
|
const sortedByPriority = [...allStreams].sort((a, b) => {
|
||||||
|
if (a.providerPriority !== b.providerPriority) return b.providerPriority - a.providerPriority;
|
||||||
|
return a.originalIndex - b.originalIndex;
|
||||||
|
});
|
||||||
|
logger.log(`🎯 Autoplay: No language match for ${preferredLanguage}, playing first available stream.`);
|
||||||
|
return sortedByPriority[0].stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageMatchedStreams.length === 0) return null;
|
||||||
|
|
||||||
|
// 3. Among language-matched streams, find the one closest to target quality
|
||||||
|
// Sort primarily by how close the stream quality is to the preferred quality
|
||||||
|
// If quality is identical, sort by provider priority and then addon's internal order
|
||||||
|
languageMatchedStreams.sort((a, b) => {
|
||||||
|
// Calculate absolute difference from target quality
|
||||||
|
// Note: 0 quality (unknown/auto) is treated as being far from any specific target
|
||||||
|
const qualityA = a.quality === 0 ? 0 : a.quality;
|
||||||
|
const qualityB = b.quality === 0 ? 0 : b.quality;
|
||||||
|
|
||||||
|
const diffA = Math.abs(qualityA - targetQuality);
|
||||||
|
const diffB = Math.abs(qualityB - targetQuality);
|
||||||
|
|
||||||
|
if (diffA !== diffB) return diffA - diffB;
|
||||||
|
|
||||||
|
// Tie-break: if both are equally close (e.g. 720p and 1440p to 1080p)
|
||||||
|
// prefer the higher quality one
|
||||||
|
if (qualityA !== qualityB) return qualityB - qualityA;
|
||||||
|
|
||||||
if (a.providerPriority !== b.providerPriority) return b.providerPriority - a.providerPriority;
|
if (a.providerPriority !== b.providerPriority) return b.providerPriority - a.providerPriority;
|
||||||
return a.originalIndex - b.originalIndex;
|
return a.originalIndex - b.originalIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selected = languageMatchedStreams[0];
|
||||||
logger.log(
|
logger.log(
|
||||||
`🎯 Best stream selected: ${allStreams[0].stream.name || allStreams[0].stream.title} (Quality: ${allStreams[0].quality}p)`
|
`🎯 Best stream selected: ${selected.stream.name || selected.stream.title} (Lang: ${selected.stream.lang || 'unknown'}, Quality: ${selected.quality}p, Target: ${targetQuality}p)`
|
||||||
);
|
);
|
||||||
|
|
||||||
return allStreams[0].stream;
|
return selected.stream;
|
||||||
},
|
},
|
||||||
[filterByQuality, filterByLanguage]
|
[filterByQuality, filterByLanguage, settings.autoplayPreferredQuality, settings.autoplayPreferredLanguage]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Current episode
|
// Current episode
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue