mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 09:35:42 +00:00
1639 lines
87 KiB
Diff
1639 lines
87 KiB
Diff
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
|
|
index 1ac0fd0..953eb59 100644
|
|
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
|
|
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/common/api/SubtitleStyle.kt
|
|
@@ -1,5 +1,6 @@
|
|
package com.brentvatne.common.api
|
|
|
|
+import android.graphics.Color
|
|
import com.brentvatne.common.toolbox.ReactBridgeUtils
|
|
import com.facebook.react.bridge.ReadableMap
|
|
|
|
@@ -22,6 +23,17 @@ class SubtitleStyle public constructor() {
|
|
var subtitlesFollowVideo = true
|
|
private set
|
|
|
|
+ // Extended styling (used by ExoPlayerView via Media3 SubtitleView)
|
|
+ // Stored as Android color ints to avoid parsing multiple times.
|
|
+ var textColor: Int? = null
|
|
+ private set
|
|
+ var backgroundColor: Int? = null
|
|
+ private set
|
|
+ var edgeType: String? = null
|
|
+ private set
|
|
+ var edgeColor: Int? = null
|
|
+ private set
|
|
+
|
|
companion object {
|
|
private const val PROP_FONT_SIZE_TRACK = "fontSize"
|
|
private const val PROP_PADDING_BOTTOM = "paddingBottom"
|
|
@@ -31,6 +43,21 @@ class SubtitleStyle public constructor() {
|
|
private const val PROP_OPACITY = "opacity"
|
|
private const val PROP_SUBTITLES_FOLLOW_VIDEO = "subtitlesFollowVideo"
|
|
|
|
+ // Extended props (optional)
|
|
+ private const val PROP_TEXT_COLOR = "textColor"
|
|
+ private const val PROP_BACKGROUND_COLOR = "backgroundColor"
|
|
+ private const val PROP_EDGE_TYPE = "edgeType"
|
|
+ private const val PROP_EDGE_COLOR = "edgeColor"
|
|
+
|
|
+ private fun parseColorOrNull(value: String?): Int? {
|
|
+ if (value.isNullOrBlank()) return null
|
|
+ return try {
|
|
+ Color.parseColor(value)
|
|
+ } catch (_: IllegalArgumentException) {
|
|
+ null
|
|
+ }
|
|
+ }
|
|
+
|
|
@JvmStatic
|
|
fun parse(src: ReadableMap?): SubtitleStyle {
|
|
val subtitleStyle = SubtitleStyle()
|
|
@@ -41,6 +68,13 @@ class SubtitleStyle public constructor() {
|
|
subtitleStyle.paddingRight = ReactBridgeUtils.safeGetInt(src, PROP_PADDING_RIGHT, 0)
|
|
subtitleStyle.opacity = ReactBridgeUtils.safeGetFloat(src, PROP_OPACITY, 1f)
|
|
subtitleStyle.subtitlesFollowVideo = ReactBridgeUtils.safeGetBool(src, PROP_SUBTITLES_FOLLOW_VIDEO, true)
|
|
+
|
|
+ // Extended styling
|
|
+ subtitleStyle.textColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_TEXT_COLOR, null))
|
|
+ subtitleStyle.backgroundColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_BACKGROUND_COLOR, null))
|
|
+ subtitleStyle.edgeType = ReactBridgeUtils.safeGetString(src, PROP_EDGE_TYPE, null)
|
|
+ subtitleStyle.edgeColor = parseColorOrNull(ReactBridgeUtils.safeGetString(src, PROP_EDGE_COLOR, null))
|
|
+
|
|
return subtitleStyle
|
|
}
|
|
}
|
|
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt
|
|
index 96a7887..6e5cf08 100644
|
|
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt
|
|
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/DataSourceUtil.kt
|
|
@@ -67,8 +67,21 @@ object DataSourceUtil {
|
|
.setTransferListener(bandwidthMeter)
|
|
|
|
if (requestHeaders != null) {
|
|
- okHttpDataSourceFactory.setDefaultRequestProperties(requestHeaders)
|
|
- if (!requestHeaders.containsKey("User-Agent")) {
|
|
+ // IMPORTANT:
|
|
+ // If `Accept-Encoding` is explicitly set (e.g. to "gzip"), OkHttp will not
|
|
+ // transparently decompress the response body. This can cause ExoPlayer to
|
|
+ // receive gzipped playlist bytes and fail HLS parsing with:
|
|
+ // "Input does not start with the #EXTM3U header".
|
|
+ // Remove any user-supplied `Accept-Encoding` so OkHttp can manage it.
|
|
+ val sanitizedHeaders = HashMap<String, String>(requestHeaders.size)
|
|
+ for ((k, v) in requestHeaders) {
|
|
+ if (!k.equals("Accept-Encoding", ignoreCase = true)) {
|
|
+ sanitizedHeaders[k] = v
|
|
+ }
|
|
+ }
|
|
+
|
|
+ okHttpDataSourceFactory.setDefaultRequestProperties(sanitizedHeaders)
|
|
+ if (!sanitizedHeaders.containsKey("User-Agent")) {
|
|
okHttpDataSourceFactory.setUserAgent(getUserAgent(context))
|
|
}
|
|
} else {
|
|
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
|
|
index bb945fe..2d3f8ca 100644
|
|
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
|
|
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ExoPlayerView.kt
|
|
@@ -10,11 +10,14 @@ import android.widget.FrameLayout
|
|
import android.widget.TextView
|
|
import androidx.media3.common.Player
|
|
import androidx.media3.common.Timeline
|
|
+import androidx.media3.common.text.CueGroup
|
|
import androidx.media3.common.util.UnstableApi
|
|
import androidx.media3.exoplayer.ExoPlayer
|
|
import androidx.media3.ui.AspectRatioFrameLayout
|
|
+import androidx.media3.ui.CaptionStyleCompat
|
|
import androidx.media3.ui.DefaultTimeBar
|
|
import androidx.media3.ui.PlayerView
|
|
+import androidx.media3.ui.SubtitleView
|
|
import com.brentvatne.common.api.ResizeMode
|
|
import com.brentvatne.common.api.SubtitleStyle
|
|
|
|
@@ -52,15 +55,58 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|
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 {
|
|
// Add PlayerView with explicit layout parameters
|
|
val playerViewLayoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
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
|
|
val liveBadgeLayoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
|
|
liveBadgeLayoutParams.setMargins(16, 16, 16, 16)
|
|
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?) {
|
|
@@ -80,6 +126,10 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|
playerView.resizeMode = resizeMode
|
|
}
|
|
}
|
|
+
|
|
+ // Re-assert subtitle rendering mode for the current style.
|
|
+ updateSubtitleRenderingMode()
|
|
+ applySubtitleStyle(localStyle)
|
|
}
|
|
|
|
fun getPlayerView(): PlayerView = playerView
|
|
@@ -108,23 +158,63 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|
}
|
|
|
|
fun setSubtitleStyle(style: SubtitleStyle) {
|
|
+ localStyle = style
|
|
+ applySubtitleStyle(localStyle)
|
|
+ }
|
|
+
|
|
+ private fun applySubtitleStyle(style: SubtitleStyle) {
|
|
+ updateSubtitleRenderingMode()
|
|
+
|
|
playerView.subtitleView?.let { subtitleView ->
|
|
- // Reset to defaults
|
|
- subtitleView.setUserDefaultStyle()
|
|
- subtitleView.setUserDefaultTextSize()
|
|
+ // Important:
|
|
+ // Avoid inheriting Android system caption settings via setUserDefaultStyle(),
|
|
+ // because those can force a background/window that the app doesn't want.
|
|
+ val resolvedTextColor = style.textColor ?: CaptionStyleCompat.DEFAULT.foregroundColor
|
|
+ val resolvedBackgroundColor = style.backgroundColor ?: Color.TRANSPARENT
|
|
+ val resolvedEdgeColor = style.edgeColor ?: Color.BLACK
|
|
+
|
|
+ val resolvedEdgeType = when (style.edgeType?.lowercase()) {
|
|
+ "outline" -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
|
+ "shadow" -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
|
|
+ else -> CaptionStyleCompat.EDGE_TYPE_NONE
|
|
+ }
|
|
+
|
|
+ // windowColor MUST be transparent to avoid the "caption window" background.
|
|
+ val captionStyle = CaptionStyleCompat(
|
|
+ resolvedTextColor,
|
|
+ resolvedBackgroundColor,
|
|
+ Color.TRANSPARENT,
|
|
+ resolvedEdgeType,
|
|
+ resolvedEdgeColor,
|
|
+ null
|
|
+ )
|
|
+ subtitleView.setStyle(captionStyle)
|
|
|
|
- // Apply custom styling
|
|
+ // Text size: if not provided, fall back to user default size.
|
|
if (style.fontSize > 0) {
|
|
- subtitleView.setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, style.fontSize.toFloat())
|
|
+ // 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()
|
|
}
|
|
|
|
+ // Horizontal padding is still useful (safe area); vertical offset is handled via bottomPaddingFraction.
|
|
subtitleView.setPadding(
|
|
style.paddingLeft,
|
|
style.paddingTop,
|
|
style.paddingRight,
|
|
- style.paddingBottom
|
|
+ 0
|
|
)
|
|
|
|
+ // Bottom offset for *internal* subtitles:
|
|
+ // Use Media3 SubtitleView's bottomPaddingFraction (moves cues up) rather than raw view padding.
|
|
+ if (style.paddingBottom > 0 && playerView.height > 0) {
|
|
+ val fraction = (style.paddingBottom.toFloat() / playerView.height.toFloat())
|
|
+ .coerceIn(0f, 0.9f)
|
|
+ subtitleView.setBottomPaddingFraction(fraction)
|
|
+ }
|
|
+
|
|
if (style.opacity != 0.0f) {
|
|
subtitleView.alpha = style.opacity
|
|
subtitleView.visibility = android.view.View.VISIBLE
|
|
@@ -132,7 +222,59 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|
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) {
|
|
@@ -223,6 +365,13 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|
}
|
|
|
|
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) {
|
|
playerView.post {
|
|
playerView.requestLayout()
|
|
@@ -284,6 +433,9 @@ class ExoPlayerView @JvmOverloads constructor(context: Context, attrs: Attribute
|
|
pendingResizeMode?.let { resizeMode ->
|
|
playerView.resizeMode = resizeMode
|
|
}
|
|
+ // Re-apply bottomPaddingFraction once we have a concrete height.
|
|
+ updateSubtitleRenderingMode()
|
|
+ applySubtitleStyle(localStyle)
|
|
}
|
|
}
|
|
}
|
|
diff --git a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
|
|
index e16ac96..54221ef 100644
|
|
--- a/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
|
|
+++ b/node_modules/react-native-video/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java
|
|
@@ -228,7 +228,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
private ArrayList<Integer> rootViewChildrenOriginalVisibility = new ArrayList<Integer>();
|
|
|
|
/*
|
|
- * When user is seeking first called is on onPositionDiscontinuity -> DISCONTINUITY_REASON_SEEK
|
|
+ * When user is seeking first called is on onPositionDiscontinuity ->
|
|
+ * DISCONTINUITY_REASON_SEEK
|
|
* Then we set if to false when playback is back in onIsPlayingChanged -> true
|
|
*/
|
|
private boolean isSeeking = false;
|
|
@@ -298,7 +299,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
lastPos = pos;
|
|
lastBufferDuration = bufferedDuration;
|
|
lastDuration = duration;
|
|
- eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(), getPositionInFirstPeriodMsForCurrentWindow(pos));
|
|
+ eventEmitter.onVideoProgress.invoke(pos, bufferedDuration, player.getDuration(),
|
|
+ getPositionInFirstPeriodMsForCurrentWindow(pos));
|
|
}
|
|
}
|
|
}
|
|
@@ -316,7 +318,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
public double getPositionInFirstPeriodMsForCurrentWindow(long currentPosition) {
|
|
Timeline.Window window = new Timeline.Window();
|
|
- if(!player.getCurrentTimeline().isEmpty()) {
|
|
+ if (!player.getCurrentTimeline().isEmpty()) {
|
|
player.getCurrentTimeline().getWindow(player.getCurrentMediaItemIndex(), window);
|
|
}
|
|
return window.windowStartTimeMs + currentPosition;
|
|
@@ -355,9 +357,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
LayoutParams.MATCH_PARENT,
|
|
LayoutParams.MATCH_PARENT);
|
|
exoPlayerView = new ExoPlayerView(getContext());
|
|
- exoPlayerView.addOnLayoutChangeListener( (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) ->
|
|
- PictureInPictureUtil.applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView)
|
|
- );
|
|
+ exoPlayerView.addOnLayoutChangeListener(
|
|
+ (View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) -> PictureInPictureUtil
|
|
+ .applySourceRectHint(themedReactContext, pictureInPictureParamsBuilder, exoPlayerView));
|
|
exoPlayerView.setLayoutParams(layoutParams);
|
|
addView(exoPlayerView, 0, layoutParams);
|
|
|
|
@@ -383,8 +385,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
public void onHostPause() {
|
|
isInBackground = true;
|
|
Activity activity = themedReactContext.getCurrentActivity();
|
|
- boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInPictureInPictureMode();
|
|
- boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null && activity.isInMultiWindowMode();
|
|
+ boolean isInPictureInPicture = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null
|
|
+ && activity.isInPictureInPictureMode();
|
|
+ boolean isInMultiWindowMode = Util.SDK_INT >= Build.VERSION_CODES.N && activity != null
|
|
+ && activity.isInMultiWindowMode();
|
|
if (playInBackground || isInPictureInPicture || isInMultiWindowMode) {
|
|
return;
|
|
}
|
|
@@ -403,7 +407,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
viewHasDropped = true;
|
|
}
|
|
|
|
- //BandwidthMeter.EventListener implementation
|
|
+ // BandwidthMeter.EventListener implementation
|
|
@Override
|
|
public void onBandwidthSample(int elapsedMs, long bytes, long bitrate) {
|
|
if (mReportBandwidth) {
|
|
@@ -411,7 +415,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
eventEmitter.onVideoBandwidthUpdate.invoke(bitrate, 0, 0, null);
|
|
} else {
|
|
Format videoFormat = player.getVideoFormat();
|
|
- boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
|
+ boolean isRotatedContent = videoFormat != null
|
|
+ && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
|
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
|
|
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
|
|
String trackId = videoFormat != null ? videoFormat.id : null;
|
|
@@ -426,7 +431,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
* Toggling the visibility of the player control view
|
|
*/
|
|
private void togglePlayerControlVisibility() {
|
|
- if (player == null) return;
|
|
+ if (player == null)
|
|
+ return;
|
|
if (exoPlayerView.isControllerVisible()) {
|
|
exoPlayerView.hideController();
|
|
} else {
|
|
@@ -450,7 +456,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private void updateControllerConfig() {
|
|
- if (exoPlayerView == null) return;
|
|
+ if (exoPlayerView == null)
|
|
+ return;
|
|
|
|
exoPlayerView.setControllerShowTimeoutMs(5000);
|
|
|
|
@@ -461,7 +468,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private void updateControllerVisibility() {
|
|
- if (exoPlayerView == null) return;
|
|
+ if (exoPlayerView == null)
|
|
+ return;
|
|
|
|
exoPlayerView.setUseController(controls && !controlsConfig.getHideFullscreen());
|
|
}
|
|
@@ -469,7 +477,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
private void openSettings() {
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
|
|
builder.setTitle(R.string.settings);
|
|
- String[] settingsOptions = {themedReactContext.getString(R.string.playback_speed)};
|
|
+ String[] settingsOptions = { themedReactContext.getString(R.string.playback_speed) };
|
|
builder.setItems(settingsOptions, (dialog, which) -> {
|
|
if (which == 0) {
|
|
showPlaybackSpeedOptions();
|
|
@@ -479,7 +487,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private void showPlaybackSpeedOptions() {
|
|
- String[] speedOptions = {"0.5x", "1.0x", "1.5x", "2.0x"};
|
|
+ String[] speedOptions = { "0.5x", "1.0x", "1.5x", "2.0x" };
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(themedReactContext);
|
|
builder.setTitle(R.string.select_playback_speed);
|
|
|
|
@@ -497,8 +505,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
speed = 2.0f;
|
|
break;
|
|
default:
|
|
- speed = 1.0f;;
|
|
- };
|
|
+ speed = 1.0f;
|
|
+ ;
|
|
+ }
|
|
+ ;
|
|
setRateModifier(speed);
|
|
});
|
|
builder.show();
|
|
@@ -510,24 +520,30 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
/**
|
|
* Update the layout
|
|
- * @param view view needs to update layout
|
|
*
|
|
- * This is a workaround for the open bug in react-native: <a href="https://github.com/facebook/react-native/issues/17968">...</a>
|
|
+ * @param view view needs to update layout
|
|
+ *
|
|
+ * This is a workaround for the open bug in react-native: <a href=
|
|
+ * "https://github.com/facebook/react-native/issues/17968">...</a>
|
|
*/
|
|
private void reLayout(View view) {
|
|
- if (view == null) return;
|
|
+ if (view == null)
|
|
+ return;
|
|
view.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
|
|
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
|
|
view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight());
|
|
}
|
|
|
|
private void refreshControlsStyles() {
|
|
- if (exoPlayerView == null || player == null || !controls) return;
|
|
+ if (exoPlayerView == null || player == null || !controls)
|
|
+ return;
|
|
updateControllerVisibility();
|
|
}
|
|
|
|
- // Note: The following methods for live content and button visibility are no longer needed
|
|
- // as PlayerView handles controls automatically. Some functionality may need to be
|
|
+ // Note: The following methods for live content and button visibility are no
|
|
+ // longer needed
|
|
+ // as PlayerView handles controls automatically. Some functionality may need to
|
|
+ // be
|
|
// reimplemented using PlayerView's APIs if custom behavior is required.
|
|
|
|
private void reLayoutControls() {
|
|
@@ -564,6 +580,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
private class RNVLoadControl extends DefaultLoadControl {
|
|
private final int availableHeapInBytes;
|
|
private final Runtime runtime;
|
|
+
|
|
public RNVLoadControl(DefaultAllocator allocator, BufferConfig config) {
|
|
super(allocator,
|
|
config.getMinBufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
|
|
@@ -574,7 +591,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
: DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
|
|
config.getBufferForPlaybackMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
|
|
? config.getBufferForPlaybackMs()
|
|
- : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS ,
|
|
+ : DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
|
config.getBufferForPlaybackAfterRebufferMs() != BufferConfig.Companion.getBufferConfigPropUnsetInt()
|
|
? config.getBufferForPlaybackAfterRebufferMs()
|
|
: DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
|
|
@@ -585,10 +602,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
: DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS,
|
|
DefaultLoadControl.DEFAULT_RETAIN_BACK_BUFFER_FROM_KEYFRAME);
|
|
runtime = Runtime.getRuntime();
|
|
- ActivityManager activityManager = (ActivityManager) themedReactContext.getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
|
|
- double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
|
- ? config.getMaxHeapAllocationPercent()
|
|
- : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
|
+ ActivityManager activityManager = (ActivityManager) themedReactContext
|
|
+ .getSystemService(ThemedReactContext.ACTIVITY_SERVICE);
|
|
+ double maxHeap = config.getMaxHeapAllocationPercent() != BufferConfig.Companion
|
|
+ .getBufferConfigPropUnsetDouble()
|
|
+ ? config.getMaxHeapAllocationPercent()
|
|
+ : DEFAULT_MAX_HEAP_ALLOCATION_PERCENT;
|
|
availableHeapInBytes = (int) Math.floor(activityManager.getMemoryClass() * maxHeap * 1024 * 1024);
|
|
}
|
|
|
|
@@ -606,13 +625,15 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
|
long freeMemory = runtime.maxMemory() - usedMemory;
|
|
- double minBufferMemoryReservePercent = source.getBufferConfig().getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
|
- ? source.getBufferConfig().getMinBufferMemoryReservePercent()
|
|
- : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
|
+ double minBufferMemoryReservePercent = source.getBufferConfig()
|
|
+ .getMinBufferMemoryReservePercent() != BufferConfig.Companion.getBufferConfigPropUnsetDouble()
|
|
+ ? source.getBufferConfig().getMinBufferMemoryReservePercent()
|
|
+ : ReactExoplayerView.DEFAULT_MIN_BUFFER_MEMORY_RESERVE;
|
|
long reserveMemory = (long) minBufferMemoryReservePercent * runtime.maxMemory();
|
|
long bufferedMs = bufferedDurationUs / (long) 1000;
|
|
if (reserveMemory > freeMemory && bufferedMs > 2000) {
|
|
- // We don't have enough memory in reserve so we stop buffering to allow other components to use it instead
|
|
+ // We don't have enough memory in reserve so we stop buffering to allow other
|
|
+ // components to use it instead
|
|
return false;
|
|
}
|
|
if (runtime.freeMemory() == 0) {
|
|
@@ -645,13 +666,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
// Initialize core configuration and listeners
|
|
initializePlayerCore(self);
|
|
pipListenerUnsubscribe = PictureInPictureUtil.addLifecycleEventListener(themedReactContext, this);
|
|
- PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave);
|
|
+ PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder,
|
|
+ this.enterPictureInPictureOnLeave);
|
|
}
|
|
if (!source.isLocalAssetFile() && !source.isAsset() && source.getBufferConfig().getCacheSize() > 0) {
|
|
RNVSimpleCache.INSTANCE.setSimpleCache(
|
|
this.getContext(),
|
|
- source.getBufferConfig().getCacheSize()
|
|
- );
|
|
+ source.getBufferConfig().getCacheSize());
|
|
useCache = true;
|
|
} else {
|
|
useCache = false;
|
|
@@ -659,7 +680,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
if (playerNeedsSource) {
|
|
// Will force display of shutter view if needed
|
|
exoPlayerView.invalidateAspectRatio();
|
|
- // DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
|
|
+ // DRM session manager creation must be done on a different thread to prevent
|
|
+ // crashes so we start a new thread
|
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
|
es.execute(() -> {
|
|
// DRM initialization must run on a different thread
|
|
@@ -668,7 +690,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
if (activity == null) {
|
|
DebugLog.e(TAG, "Failed to initialize Player!, null activity");
|
|
- eventEmitter.onVideoError.invoke("Failed to initialize Player!", new Exception("Current Activity is null!"), "1001");
|
|
+ eventEmitter.onVideoError.invoke("Failed to initialize Player!",
|
|
+ new Exception("Current Activity is null!"), "1001");
|
|
return;
|
|
}
|
|
|
|
@@ -721,8 +744,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
DefaultAllocator allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
|
RNVLoadControl loadControl = new RNVLoadControl(
|
|
allocator,
|
|
- source.getBufferConfig()
|
|
- );
|
|
+ source.getBufferConfig());
|
|
|
|
long initialBitrate = source.getBufferConfig().getInitialBitrate();
|
|
if (initialBitrate > 0) {
|
|
@@ -730,11 +752,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
this.bandwidthMeter = config.getBandwidthMeter();
|
|
}
|
|
|
|
- DefaultRenderersFactory renderersFactory =
|
|
- new DefaultRenderersFactory(getContext())
|
|
- .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF)
|
|
- .setEnableDecoderFallback(true)
|
|
- .forceEnableMediaCodecAsynchronousQueueing();
|
|
+ DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(getContext())
|
|
+ .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
|
|
+ .setEnableDecoderFallback(true)
|
|
+ .forceEnableMediaCodecAsynchronousQueueing();
|
|
|
|
DefaultMediaSourceFactory mediaSourceFactory;
|
|
|
|
@@ -743,11 +764,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
} else {
|
|
mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
|
|
|
|
- mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
|
|
+ mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader,
|
|
+ exoPlayerView.getPlayerView());
|
|
}
|
|
|
|
if (useCache && !disableCache) {
|
|
- mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
|
+ mediaSourceFactory
|
|
+ .setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
|
}
|
|
|
|
player = new ExoPlayer.Builder(getContext(), renderersFactory)
|
|
@@ -772,7 +795,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
player.setPlaybackParameters(params);
|
|
changeAudioOutput(this.audioOutput);
|
|
|
|
- if(showNotificationControls) {
|
|
+ if (showNotificationControls) {
|
|
setupPlaybackService();
|
|
}
|
|
}
|
|
@@ -784,8 +807,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
Uri adTagUrl = adProps.getAdTagUrl();
|
|
if (adTagUrl != null) {
|
|
// Create an AdsLoader.
|
|
- ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader
|
|
- .Builder(themedReactContext)
|
|
+ ImaAdsLoader.Builder imaLoaderBuilder = new ImaAdsLoader.Builder(themedReactContext)
|
|
.setAdEventListener(this)
|
|
.setAdErrorListener(this);
|
|
|
|
@@ -817,7 +839,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
try {
|
|
- // First check if there's a custom DRM manager registered through the plugin system
|
|
+ // First check if there's a custom DRM manager registered through the plugin
|
|
+ // system
|
|
DRMManagerSpec drmManager = ReactNativeVideoManager.Companion.getInstance().getDRMManager();
|
|
if (drmManager == null) {
|
|
// If no custom manager is registered, use the default implementation
|
|
@@ -826,11 +849,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
DrmSessionManager drmSessionManager = drmManager.buildDrmSessionManager(uuid, drmProps);
|
|
if (drmSessionManager == null) {
|
|
- eventEmitter.onVideoError.invoke("Failed to build DRM session manager", new Exception("DRM session manager is null"), "3007");
|
|
+ eventEmitter.onVideoError.invoke("Failed to build DRM session manager",
|
|
+ new Exception("DRM session manager is null"), "3007");
|
|
}
|
|
|
|
// Allow plugins to override the DrmSessionManager
|
|
- DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance().overrideDrmSessionManager(source, drmSessionManager);
|
|
+ DrmSessionManager overriddenManager = ReactNativeVideoManager.Companion.getInstance()
|
|
+ .overrideDrmSessionManager(source, drmSessionManager);
|
|
return overriddenManager != null ? overriddenManager : drmSessionManager;
|
|
} catch (UnsupportedDrmException ex) {
|
|
// Unsupported DRM exceptions are handled by the calling method
|
|
@@ -853,7 +878,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
/// init DRM
|
|
DrmSessionManager drmSessionManager = initializePlayerDrm();
|
|
- if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) {
|
|
+ if (drmSessionManager == null && runningSource.getDrmProps() != null
|
|
+ && runningSource.getDrmProps().getDrmType() != null) {
|
|
// Failed to initialize DRM session manager - cannot continue
|
|
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
|
|
return;
|
|
@@ -910,7 +936,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
} catch (UnsupportedDrmException e) {
|
|
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
|
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
|
- ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown);
|
|
+ ? R.string.error_drm_unsupported_scheme
|
|
+ : R.string.error_drm_unknown);
|
|
eventEmitter.onVideoError.invoke(getResources().getString(errorStringId), e, "3003");
|
|
}
|
|
}
|
|
@@ -955,7 +982,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
if (playbackServiceBinder != null) {
|
|
playbackServiceBinder.getService().unregisterPlayer(player);
|
|
}
|
|
- } catch (Exception ignored) {}
|
|
+ } catch (Exception ignored) {
|
|
+ }
|
|
|
|
playbackServiceBinder = null;
|
|
}
|
|
@@ -987,21 +1015,22 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
private void cleanupPlaybackService() {
|
|
try {
|
|
- if(player != null && playbackServiceBinder != null) {
|
|
+ if (player != null && playbackServiceBinder != null) {
|
|
playbackServiceBinder.getService().unregisterPlayer(player);
|
|
}
|
|
|
|
playbackServiceBinder = null;
|
|
|
|
- if(playbackServiceConnection != null) {
|
|
+ if (playbackServiceConnection != null) {
|
|
themedReactContext.unbindService(playbackServiceConnection);
|
|
}
|
|
- } catch(Exception e) {
|
|
+ } catch (Exception e) {
|
|
DebugLog.w(TAG, "Cloud not cleanup playback service");
|
|
}
|
|
}
|
|
|
|
- private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long cropStartMs, long cropEndMs) {
|
|
+ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager,
|
|
+ long cropStartMs, long cropEndMs) {
|
|
if (uri == null) {
|
|
throw new IllegalStateException("Invalid video uri");
|
|
}
|
|
@@ -1033,12 +1062,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
Uri adTagUrl = source.getAdsProps().getAdTagUrl();
|
|
if (adTagUrl != null) {
|
|
mediaItemBuilder.setAdsConfiguration(
|
|
- new MediaItem.AdsConfiguration.Builder(adTagUrl).build()
|
|
- );
|
|
+ new MediaItem.AdsConfiguration.Builder(adTagUrl).build());
|
|
}
|
|
}
|
|
|
|
- MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils.getLiveConfiguration(source.getBufferConfig());
|
|
+ MediaItem.LiveConfiguration.Builder liveConfiguration = ConfigurationUtils
|
|
+ .getLiveConfiguration(source.getBufferConfig());
|
|
mediaItemBuilder.setLiveConfiguration(liveConfiguration.build());
|
|
|
|
MediaSource.Factory mediaSourceFactory;
|
|
@@ -1050,29 +1079,26 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
drmProvider = new DefaultDrmSessionManagerProvider();
|
|
}
|
|
|
|
-
|
|
switch (type) {
|
|
case CONTENT_TYPE_SS:
|
|
- if(!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) {
|
|
+ if (!BuildConfig.USE_EXOPLAYER_SMOOTH_STREAMING) {
|
|
DebugLog.e("Exo Player Exception", "Smooth Streaming is not enabled!");
|
|
throw new IllegalStateException("Smooth Streaming is not enabled!");
|
|
}
|
|
|
|
mediaSourceFactory = new SsMediaSource.Factory(
|
|
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
|
|
- buildDataSourceFactory(false)
|
|
- );
|
|
+ buildDataSourceFactory(false));
|
|
break;
|
|
case CONTENT_TYPE_DASH:
|
|
- if(!BuildConfig.USE_EXOPLAYER_DASH) {
|
|
+ if (!BuildConfig.USE_EXOPLAYER_DASH) {
|
|
DebugLog.e("Exo Player Exception", "DASH is not enabled!");
|
|
throw new IllegalStateException("DASH is not enabled!");
|
|
}
|
|
|
|
mediaSourceFactory = new DashMediaSource.Factory(
|
|
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
|
|
- buildDataSourceFactory(false)
|
|
- );
|
|
+ buildDataSourceFactory(false));
|
|
break;
|
|
case CONTENT_TYPE_HLS:
|
|
if (!BuildConfig.USE_EXOPLAYER_HLS) {
|
|
@@ -1087,13 +1113,14 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
mediaSourceFactory = new HlsMediaSource.Factory(
|
|
- dataSourceFactory
|
|
- ).setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation());
|
|
+ dataSourceFactory)
|
|
+ .setAllowChunklessPreparation(source.getTextTracksAllowChunklessPreparation());
|
|
break;
|
|
case CONTENT_TYPE_OTHER:
|
|
if ("asset".equals(uri.getScheme())) {
|
|
try {
|
|
- DataSource.Factory assetDataSourceFactory = DataSourceUtil.buildAssetDataSourceFactory(themedReactContext, uri);
|
|
+ DataSource.Factory assetDataSourceFactory = DataSourceUtil
|
|
+ .buildAssetDataSourceFactory(themedReactContext, uri);
|
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(assetDataSourceFactory);
|
|
} catch (Exception e) {
|
|
throw new IllegalStateException("cannot open input file:" + uri);
|
|
@@ -1101,12 +1128,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
} else if ("file".equals(uri.getScheme()) ||
|
|
!useCache) {
|
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(
|
|
- mediaDataSourceFactory
|
|
- );
|
|
+ mediaDataSourceFactory);
|
|
} else {
|
|
mediaSourceFactory = new ProgressiveMediaSource.Factory(
|
|
- RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))
|
|
- );
|
|
+ RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
|
|
|
|
}
|
|
break;
|
|
@@ -1125,20 +1150,19 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
if (cmcdConfigurationFactory != null) {
|
|
mediaSourceFactory = mediaSourceFactory.setCmcdConfigurationFactory(
|
|
- cmcdConfigurationFactory::createCmcdConfiguration
|
|
- );
|
|
+ cmcdConfigurationFactory::createCmcdConfiguration);
|
|
}
|
|
|
|
mediaSourceFactory = Objects.requireNonNullElse(
|
|
ReactNativeVideoManager.Companion.getInstance()
|
|
.overrideMediaSourceFactory(source, mediaSourceFactory, mediaDataSourceFactory),
|
|
- mediaSourceFactory
|
|
- );
|
|
+ mediaSourceFactory);
|
|
|
|
mediaItemBuilder.setStreamKeys(streamKeys);
|
|
|
|
@Nullable
|
|
- final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance().overrideMediaItemBuilder(source, mediaItemBuilder);
|
|
+ final MediaItem.Builder overridenMediaItemBuilder = ReactNativeVideoManager.Companion.getInstance()
|
|
+ .overrideMediaItemBuilder(source, mediaItemBuilder);
|
|
|
|
MediaItem mediaItem = overridenMediaItemBuilder != null
|
|
? overridenMediaItemBuilder.build()
|
|
@@ -1147,8 +1171,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
MediaSource mediaSource = mediaSourceFactory
|
|
.setDrmSessionManagerProvider(drmProvider)
|
|
.setLoadErrorHandlingPolicy(
|
|
- config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount())
|
|
- )
|
|
+ config.buildLoadErrorHandlingPolicy(source.getMinLoadRetryCount()))
|
|
.createMediaSource(mediaItem);
|
|
|
|
if (cropStartMs >= 0 && cropEndMs >= 0) {
|
|
@@ -1183,7 +1206,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
}
|
|
|
|
- MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(track.getUri())
|
|
+ MediaItem.SubtitleConfiguration.Builder configBuilder = new MediaItem.SubtitleConfiguration.Builder(
|
|
+ track.getUri())
|
|
.setId(trackId)
|
|
.setMimeType(track.getType())
|
|
.setLabel(label)
|
|
@@ -1194,7 +1218,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
configBuilder.setLanguage(track.getLanguage());
|
|
}
|
|
|
|
- // Set selection flags - make first track default if no specific track is selected
|
|
+ // Set selection flags - make first track default if no specific track is
|
|
+ // selected
|
|
if (trackIndex == 0 && (textTrackType == null || "disabled".equals(textTrackType))) {
|
|
configBuilder.setSelectionFlags(C.SELECTION_FLAG_DEFAULT);
|
|
} else {
|
|
@@ -1204,10 +1229,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
MediaItem.SubtitleConfiguration subtitleConfiguration = configBuilder.build();
|
|
subtitleConfigurations.add(subtitleConfiguration);
|
|
|
|
- DebugLog.d(TAG, "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")");
|
|
+ DebugLog.d(TAG,
|
|
+ "Created subtitle configuration: " + trackId + " - " + label + " (" + track.getType() + ")");
|
|
trackIndex++;
|
|
} catch (Exception e) {
|
|
- DebugLog.e(TAG, "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage());
|
|
+ DebugLog.e(TAG,
|
|
+ "Error creating SubtitleConfiguration for URI " + track.getUri() + ": " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
@@ -1220,7 +1247,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
private void releasePlayer() {
|
|
if (player != null) {
|
|
- if(playbackServiceBinder != null) {
|
|
+ if (playbackServiceBinder != null) {
|
|
playbackServiceBinder.getService().unregisterPlayer(player);
|
|
themedReactContext.unbindService(playbackServiceConnection);
|
|
}
|
|
@@ -1276,7 +1303,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
case AudioManager.AUDIOFOCUS_LOSS:
|
|
view.hasAudioFocus = false;
|
|
view.eventEmitter.onAudioFocusChanged.invoke(false);
|
|
- // FIXME this pause can cause issue if content doesn't have pause capability (can happen on live channel)
|
|
+ // FIXME this pause can cause issue if content doesn't have pause capability
|
|
+ // (can happen on live channel)
|
|
if (activity != null) {
|
|
activity.runOnUiThread(view::pausePlayback);
|
|
}
|
|
@@ -1297,16 +1325,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
|
|
// Lower the volume
|
|
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) {
|
|
// Raise it back to normal
|
|
if (!view.muted) {
|
|
- activity.runOnUiThread(() ->
|
|
- view.player.setVolume(view.audioVolume * 1)
|
|
- );
|
|
+ activity.runOnUiThread(() -> view.player.setVolume(view.audioVolume * 1));
|
|
}
|
|
}
|
|
}
|
|
@@ -1379,7 +1403,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
/**
|
|
* Returns a new DataSource factory.
|
|
*
|
|
- * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
|
|
+ * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener
|
|
+ * to the new
|
|
* DataSource factory.
|
|
* @return A new DataSource factory.
|
|
*/
|
|
@@ -1391,12 +1416,14 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
/**
|
|
* Returns a new HttpDataSource factory.
|
|
*
|
|
- * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener to the new
|
|
- * DataSource factory.
|
|
+ * @param useBandwidthMeter Whether to set {@link #bandwidthMeter} as a listener
|
|
+ * to the new
|
|
+ * DataSource factory.
|
|
* @return A new HttpDataSource factory.
|
|
*/
|
|
private HttpDataSource.Factory buildHttpDataSourceFactory(boolean useBandwidthMeter) {
|
|
- return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext, useBandwidthMeter ? bandwidthMeter : null, source.getHeaders());
|
|
+ return DataSourceUtil.getDefaultHttpDataSourceFactory(this.themedReactContext,
|
|
+ useBandwidthMeter ? bandwidthMeter : null, source.getHeaders());
|
|
}
|
|
|
|
// AudioBecomingNoisyListener implementation
|
|
@@ -1413,11 +1440,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
@Override
|
|
public void onEvents(@NonNull Player player, Player.Events events) {
|
|
- if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
|
+ if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
|
|
+ || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
|
|
int playbackState = player.getPlaybackState();
|
|
boolean playWhenReady = player.getPlayWhenReady();
|
|
String text = "onStateChanged: playWhenReady=" + playWhenReady + ", playbackState=";
|
|
- eventEmitter.onPlaybackRateChange.invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
|
+ eventEmitter.onPlaybackRateChange
|
|
+ .invoke(playWhenReady && playbackState == ExoPlayer.STATE_READY ? 1.0f : 0.0f);
|
|
switch (playbackState) {
|
|
case Player.STATE_IDLE:
|
|
text += "idle";
|
|
@@ -1474,9 +1503,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
/**
|
|
- * The progress message handler will duplicate recursions of the onProgressMessage handler
|
|
- * 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 progress message handler will duplicate recursions of the
|
|
+ * onProgressMessage handler
|
|
+ * 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.
|
|
*/
|
|
private void clearProgressMessageHandler() {
|
|
progressHandler.removeMessages(SHOW_PROGRESS);
|
|
@@ -1495,7 +1526,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
setSelectedTextTrack(textTrackType, textTrackValue);
|
|
}
|
|
Format videoFormat = player.getVideoFormat();
|
|
- boolean isRotatedContent = videoFormat != null && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
|
+ boolean isRotatedContent = videoFormat != null
|
|
+ && (videoFormat.rotationDegrees == 90 || videoFormat.rotationDegrees == 270);
|
|
int width = videoFormat != null ? (isRotatedContent ? videoFormat.height : videoFormat.width) : 0;
|
|
int height = videoFormat != null ? (isRotatedContent ? videoFormat.width : videoFormat.height) : 0;
|
|
String trackId = videoFormat != null ? videoFormat.id : null;
|
|
@@ -1504,18 +1536,19 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
long duration = player.getDuration();
|
|
long currentPosition = player.getCurrentPosition();
|
|
ArrayList<Track> audioTracks = getAudioTrackInfo();
|
|
- ArrayList<Track> textTracks = getTextTrackInfo();
|
|
+ ArrayList<Track> textTracks = getTextTrackInfo();
|
|
|
|
if (source.getContentStartTime() != -1) {
|
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
|
es.execute(() -> {
|
|
- // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread and notify the player only when we're done
|
|
+ // To prevent ANRs caused by getVideoTrackInfo we run this on a different thread
|
|
+ // and notify the player only when we're done
|
|
ArrayList<VideoTrack> videoTracks = getVideoTrackInfoFromManifest();
|
|
if (videoTracks != null) {
|
|
isUsingContentResolution = true;
|
|
}
|
|
eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height,
|
|
- audioTracks, textTracks, videoTracks, trackId );
|
|
+ audioTracks, textTracks, videoTracks, trackId);
|
|
|
|
updateSubtitleButtonVisibility();
|
|
});
|
|
@@ -1533,9 +1566,9 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private static boolean isTrackSelected(TrackSelection selection, TrackGroup group,
|
|
- int trackIndex){
|
|
+ int trackIndex) {
|
|
return selection != null && selection.getTrackGroup() == group
|
|
- && selection.indexOf( trackIndex ) != C.INDEX_UNSET;
|
|
+ && selection.indexOf(trackIndex) != C.INDEX_UNSET;
|
|
}
|
|
|
|
private ArrayList<Track> getAudioTrackInfo() {
|
|
@@ -1553,7 +1586,6 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
TrackSelectionArray selectionArray = player.getCurrentTrackSelections();
|
|
TrackSelection selection = selectionArray.get(C.TRACK_TYPE_AUDIO);
|
|
|
|
-
|
|
for (int groupIndex = 0; groupIndex < groups.length; ++groupIndex) {
|
|
TrackGroup group = groups.get(groupIndex);
|
|
Format format = group.getFormat(0);
|
|
@@ -1579,7 +1611,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
videoTrack.setHeight(format.height == Format.NO_VALUE ? 0 : format.height);
|
|
videoTrack.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
|
videoTrack.setRotation(format.rotationDegrees);
|
|
- if (format.codecs != null) videoTrack.setCodecs(format.codecs);
|
|
+ if (format.codecs != null)
|
|
+ videoTrack.setCodecs(format.codecs);
|
|
videoTrack.setTrackId(format.id == null ? String.valueOf(trackIndex) : format.id);
|
|
videoTrack.setIndex(trackIndex);
|
|
return videoTrack;
|
|
@@ -1616,7 +1649,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
return this.getVideoTrackInfoFromManifest(0);
|
|
}
|
|
|
|
- // We need retry count to in case where minefest request fails from poor network conditions
|
|
+ // We need retry count to in case where minefest request fails from poor network
|
|
+ // conditions
|
|
@WorkerThread
|
|
private ArrayList<VideoTrack> getVideoTrackInfoFromManifest(int retryCount) {
|
|
ExecutorService es = Executors.newSingleThreadExecutor();
|
|
@@ -1631,18 +1665,20 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
public ArrayList<VideoTrack> call() {
|
|
ArrayList<VideoTrack> videoTracks = new ArrayList<>();
|
|
- try {
|
|
+ try {
|
|
DashManifest manifest = DashUtil.loadManifest(this.ds, this.uri);
|
|
int periodCount = manifest.getPeriodCount();
|
|
for (int i = 0; i < periodCount; i++) {
|
|
Period period = manifest.getPeriod(i);
|
|
- for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets.size(); adaptationIndex++) {
|
|
+ for (int adaptationIndex = 0; adaptationIndex < period.adaptationSets
|
|
+ .size(); adaptationIndex++) {
|
|
AdaptationSet adaptation = period.adaptationSets.get(adaptationIndex);
|
|
if (adaptation.type != C.TRACK_TYPE_VIDEO) {
|
|
continue;
|
|
}
|
|
boolean hasFoundContentPeriod = false;
|
|
- for (int representationIndex = 0; representationIndex < adaptation.representations.size(); representationIndex++) {
|
|
+ for (int representationIndex = 0; representationIndex < adaptation.representations
|
|
+ .size(); representationIndex++) {
|
|
Representation representation = adaptation.representations.get(representationIndex);
|
|
Format format = representation.format;
|
|
if (isFormatSupported(format)) {
|
|
@@ -1650,7 +1686,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
break;
|
|
}
|
|
hasFoundContentPeriod = true;
|
|
- VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format, representationIndex);
|
|
+ VideoTrack videoTrack = exoplayerVideoTrackToGenericVideoTrack(format,
|
|
+ representationIndex);
|
|
videoTracks.add(videoTrack);
|
|
}
|
|
}
|
|
@@ -1680,12 +1717,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
return null;
|
|
}
|
|
|
|
- private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection, TrackGroup group) {
|
|
+ private Track exoplayerTrackToGenericTrack(Format format, int trackIndex, TrackSelection selection,
|
|
+ TrackGroup group) {
|
|
Track track = new Track();
|
|
track.setIndex(trackIndex);
|
|
- if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType);
|
|
- if (format.language != null) track.setLanguage(format.language);
|
|
- if (format.label != null) track.setTitle(format.label);
|
|
+ if (format.sampleMimeType != null)
|
|
+ track.setMimeType(format.sampleMimeType);
|
|
+ if (format.language != null)
|
|
+ track.setLanguage(format.language);
|
|
+ if (format.label != null)
|
|
+ track.setTitle(format.label);
|
|
track.setSelected(isTrackSelected(selection, group, trackIndex));
|
|
return track;
|
|
}
|
|
@@ -1755,7 +1796,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
track.setLanguage(format.language != null ? format.language : "unknown");
|
|
track.setTitle(format.label != null ? format.label : "Track " + (groupIndex + 1));
|
|
track.setSelected(false); // Don't report selection status - let PlayerView handle it
|
|
- if (format.sampleMimeType != null) track.setMimeType(format.sampleMimeType);
|
|
+ if (format.sampleMimeType != null)
|
|
+ track.setMimeType(format.sampleMimeType);
|
|
track.setBitrate(format.bitrate == Format.NO_VALUE ? 0 : format.bitrate);
|
|
|
|
tracks.add(track);
|
|
@@ -1786,8 +1828,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
Track textTrack = new Track();
|
|
textTrack.setIndex(textTracks.size());
|
|
- if (format.sampleMimeType != null) textTrack.setMimeType(format.sampleMimeType);
|
|
- if (format.language != null) textTrack.setLanguage(format.language);
|
|
+ if (format.sampleMimeType != null)
|
|
+ textTrack.setMimeType(format.sampleMimeType);
|
|
+ if (format.language != null)
|
|
+ textTrack.setLanguage(format.language);
|
|
|
|
boolean isExternal = format.id != null && format.id.startsWith("external-subtitle-");
|
|
|
|
@@ -1821,28 +1865,34 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
@Override
|
|
- public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition, @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
|
|
+ public void onPositionDiscontinuity(@NonNull Player.PositionInfo oldPosition,
|
|
+ @NonNull Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {
|
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
|
isSeeking = true;
|
|
seekPosition = newPosition.positionMs;
|
|
if (isUsingContentResolution) {
|
|
- // We need to update the selected track to make sure that it still matches user selection if track list has changed in this period
|
|
+ // We need to update the selected track to make sure that it still matches user
|
|
+ // selection if track list has changed in this period
|
|
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
|
}
|
|
}
|
|
|
|
if (playerNeedsSource) {
|
|
- // This will only occur if the user has performed a seek whilst in the error state. Update the
|
|
- // resume position so that if the user then retries, playback will resume from the position to
|
|
+ // This will only occur if the user has performed a seek whilst in the error
|
|
+ // state. Update the
|
|
+ // resume position so that if the user then retries, playback will resume from
|
|
+ // the position to
|
|
// which they seeked.
|
|
updateResumePosition();
|
|
}
|
|
if (isUsingContentResolution) {
|
|
- // Discontinuity events might have a different track list so we update the selected track
|
|
+ // Discontinuity events might have a different track list so we update the
|
|
+ // selected track
|
|
setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
|
selectTrackWhenReady = true;
|
|
}
|
|
- // When repeat is turned on, reaching the end of the video will not cause a state change
|
|
+ // When repeat is turned on, reaching the end of the video will not cause a
|
|
+ // state change
|
|
// so we need to explicitly detect it.
|
|
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION
|
|
&& player.getRepeatMode() == Player.REPEAT_MODE_ONE) {
|
|
@@ -1890,15 +1940,17 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
updateSubtitleButtonVisibility();
|
|
}
|
|
|
|
-
|
|
private boolean hasBuiltInTextTracks() {
|
|
- if (player == null || trackSelector == null) return false;
|
|
+ if (player == null || trackSelector == null)
|
|
+ return false;
|
|
|
|
MappingTrackSelector.MappedTrackInfo info = trackSelector.getCurrentMappedTrackInfo();
|
|
- if (info == null) return false;
|
|
+ if (info == null)
|
|
+ return false;
|
|
|
|
int textRendererIndex = getTrackRendererIndex(C.TRACK_TYPE_TEXT);
|
|
- if (textRendererIndex == C.INDEX_UNSET) return false;
|
|
+ if (textRendererIndex == C.INDEX_UNSET)
|
|
+ return false;
|
|
|
|
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
|
|
|
@@ -1918,11 +1970,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private void updateSubtitleButtonVisibility() {
|
|
- if (exoPlayerView == null) return;
|
|
+ if (exoPlayerView == null)
|
|
+ return;
|
|
|
|
boolean hasTextTracks = (source.getSideLoadedTextTracks() != null &&
|
|
- !source.getSideLoadedTextTracks().getTracks().isEmpty()) ||
|
|
- hasBuiltInTextTracks();
|
|
+ !source.getSideLoadedTextTracks().getTracks().isEmpty()) ||
|
|
+ hasBuiltInTextTracks();
|
|
|
|
exoPlayerView.setShowSubtitleButton(hasTextTracks);
|
|
}
|
|
@@ -1942,7 +1995,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
if (isPlaying && isSeeking) {
|
|
eventEmitter.onVideoSeek.invoke(player.getCurrentPosition(), seekPosition);
|
|
}
|
|
- PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder, pictureInPictureReceiver, !isPlaying);
|
|
+ PictureInPictureUtil.applyPlayingStatus(themedReactContext, pictureInPictureParamsBuilder,
|
|
+ pictureInPictureReceiver, !isPlaying);
|
|
eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking);
|
|
|
|
if (isPlaying) {
|
|
@@ -1954,14 +2008,15 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
public void onPlayerError(@NonNull PlaybackException e) {
|
|
String errorString = "ExoPlaybackException: " + PlaybackException.getErrorCodeName(e.errorCode);
|
|
String errorCode = "2" + e.errorCode;
|
|
- switch(e.errorCode) {
|
|
+ switch (e.errorCode) {
|
|
case PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED:
|
|
case PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED:
|
|
case PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED:
|
|
case PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR:
|
|
case PlaybackException.ERROR_CODE_DRM_UNSPECIFIED:
|
|
if (!hasDrmFailed) {
|
|
- // 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
|
|
+ // 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
|
|
hasDrmFailed = true;
|
|
playerNeedsSource = true;
|
|
updateResumePosition();
|
|
@@ -2043,14 +2098,16 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
boolean isSourceEqual = source.isEquals(this.source);
|
|
hasDrmFailed = false;
|
|
this.source = source;
|
|
- final DataSource.Factory tmpMediaDataSourceFactory =
|
|
- DataSourceUtil.getDefaultDataSourceFactory(this.themedReactContext, bandwidthMeter,
|
|
- source.getHeaders());
|
|
+ final DataSource.Factory tmpMediaDataSourceFactory = DataSourceUtil.getDefaultDataSourceFactory(
|
|
+ this.themedReactContext, bandwidthMeter,
|
|
+ source.getHeaders());
|
|
|
|
@Nullable
|
|
- final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance().overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory);
|
|
+ final DataSource.Factory overriddenMediaDataSourceFactory = ReactNativeVideoManager.Companion.getInstance()
|
|
+ .overrideMediaDataSourceFactory(source, tmpMediaDataSourceFactory);
|
|
|
|
- this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory, tmpMediaDataSourceFactory);
|
|
+ this.mediaDataSourceFactory = Objects.requireNonNullElse(overriddenMediaDataSourceFactory,
|
|
+ tmpMediaDataSourceFactory);
|
|
|
|
if (source.getCmcdProps() != null) {
|
|
CMCDConfig cmcdConfig = new CMCDConfig(source.getCmcdProps());
|
|
@@ -2069,6 +2126,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
clearSrc();
|
|
}
|
|
}
|
|
+
|
|
public void clearSrc() {
|
|
if (source.getUri() != null) {
|
|
if (player != null) {
|
|
@@ -2117,7 +2175,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
public void disableTrack(int rendererIndex) {
|
|
- if (trackSelector == null) return;
|
|
+ if (trackSelector == null)
|
|
+ return;
|
|
|
|
DefaultTrackSelector.Parameters disableParameters = trackSelector.getParameters()
|
|
.buildUpon()
|
|
@@ -2127,7 +2186,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private void selectTextTrackInternal(String type, String value) {
|
|
- if (player == null || trackSelector == null) return;
|
|
+ if (player == null || trackSelector == null)
|
|
+ return;
|
|
|
|
DebugLog.d(TAG, "selectTextTrackInternal: type=" + type + ", value=" + value);
|
|
|
|
@@ -2147,6 +2207,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
TrackGroupArray groups = info.getTrackGroups(textRendererIndex);
|
|
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++) {
|
|
TrackGroup group = groups.get(groupIndex);
|
|
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
|
@@ -2159,25 +2224,28 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
isMatch = true;
|
|
} else if ("index".equals(type)) {
|
|
int targetIndex = ReactBridgeUtils.safeParseInt(value, -1);
|
|
- if (targetIndex == trackIndex) {
|
|
+ if (targetIndex == flattenedIndex) {
|
|
isMatch = true;
|
|
}
|
|
}
|
|
|
|
+ flattenedIndex++;
|
|
+
|
|
if (isMatch) {
|
|
TrackSelectionOverride override = new TrackSelectionOverride(group,
|
|
- java.util.Arrays.asList(trackIndex));
|
|
+ java.util.Arrays.asList(trackIndex));
|
|
parametersBuilder.addOverride(override);
|
|
trackFound = true;
|
|
break;
|
|
}
|
|
}
|
|
- if (trackFound) break;
|
|
+ if (trackFound)
|
|
+ break;
|
|
}
|
|
|
|
if (!trackFound) {
|
|
DebugLog.w(TAG, "Text track not found for type=" + type + ", value=" + value +
|
|
- ". Keeping current selection.");
|
|
+ ". Keeping current selection.");
|
|
}
|
|
}
|
|
}
|
|
@@ -2198,7 +2266,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
public void setSelectedTrack(int trackType, String type, String value) {
|
|
- if (player == null || trackSelector == null) return;
|
|
+ if (player == null || trackSelector == null)
|
|
+ return;
|
|
|
|
if (controls) {
|
|
return;
|
|
@@ -2272,9 +2341,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
usingExactMatch = true;
|
|
break;
|
|
} else if (isUsingContentResolution) {
|
|
- // When using content resolution rather than ads, we need to try and find the closest match if there is no exact match
|
|
+ // When using content resolution rather than ads, we need to try and find the
|
|
+ // closest match if there is no exact match
|
|
if (closestFormat != null) {
|
|
- if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height) && format.height < height) {
|
|
+ if ((format.bitrate > closestFormat.bitrate || format.height > closestFormat.height)
|
|
+ && format.height < height) {
|
|
// Higher quality match
|
|
closestFormat = format;
|
|
closestTrackIndex = j;
|
|
@@ -2285,7 +2356,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
}
|
|
}
|
|
- // This is a fallback if the new period contains only higher resolutions than the user has selected
|
|
+ // This is a fallback if the new period contains only higher resolutions than
|
|
+ // the user has selected
|
|
if (closestFormat == null && isUsingContentResolution && !usingExactMatch) {
|
|
// No close match found - so we pick the lowest quality
|
|
int minHeight = Integer.MAX_VALUE;
|
|
@@ -2308,8 +2380,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
} else if (trackType == C.TRACK_TYPE_TEXT && Util.SDK_INT > 18) { // Text default
|
|
// Use system settings if possible
|
|
- CaptioningManager captioningManager
|
|
- = (CaptioningManager)themedReactContext.getSystemService(Context.CAPTIONING_SERVICE);
|
|
+ CaptioningManager captioningManager = (CaptioningManager) themedReactContext
|
|
+ .getSystemService(Context.CAPTIONING_SERVICE);
|
|
if (captioningManager != null && captioningManager.isEnabled()) {
|
|
groupIndex = getGroupIndexForDefaultLocale(groups);
|
|
}
|
|
@@ -2338,7 +2410,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
// With only one tracks we can't remove any tracks so attempt to play it anyway
|
|
tracks = allTracks;
|
|
} else {
|
|
- tracks = new ArrayList<>(supportedFormatLength + 1);
|
|
+ tracks = new ArrayList<>(supportedFormatLength + 1);
|
|
for (int k = 0; k < allTracks.size(); k++) {
|
|
Format format = group.getFormat(k);
|
|
if (isFormatSupported(format)) {
|
|
@@ -2364,7 +2436,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
.setRendererDisabled(rendererIndex, false);
|
|
|
|
// Clear existing overrides for this track type to avoid conflicts
|
|
- // But be careful with audio tracks - don't clear unless explicitly selecting a different track
|
|
+ // But be careful with audio tracks - don't clear unless explicitly selecting a
|
|
+ // different track
|
|
if (trackType != C.TRACK_TYPE_AUDIO || !type.equals("default")) {
|
|
selectionParameters.clearOverridesOfType(selectionOverride.getType());
|
|
}
|
|
@@ -2380,7 +2453,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
selectionParameters.setForceHighestSupportedBitrate(false);
|
|
selectionParameters.setForceLowestBitrate(false);
|
|
DebugLog.d(TAG, "Audio track selection: group=" + groupIndex + ", tracks=" + tracks +
|
|
- ", override=" + selectionOverride);
|
|
+ ", override=" + selectionOverride);
|
|
}
|
|
|
|
trackSelector.setParameters(selectionParameters.build());
|
|
@@ -2411,7 +2484,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
private int getGroupIndexForDefaultLocale(TrackGroupArray groups) {
|
|
- if (groups.length == 0){
|
|
+ if (groups.length == 0) {
|
|
return C.INDEX_UNSET;
|
|
}
|
|
|
|
@@ -2432,7 +2505,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
public void setSelectedVideoTrack(String type, String value) {
|
|
videoTrackType = type;
|
|
videoTrackValue = value;
|
|
- if (!loadVideoStarted) setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
|
+ if (!loadVideoStarted)
|
|
+ setSelectedTrack(C.TRACK_TYPE_VIDEO, videoTrackType, videoTrackValue);
|
|
}
|
|
|
|
public void setSelectedAudioTrack(String type, String value) {
|
|
@@ -2463,9 +2537,11 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
public void setEnterPictureInPictureOnLeave(boolean enterPictureInPictureOnLeave) {
|
|
- this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && enterPictureInPictureOnLeave;
|
|
+ this.enterPictureInPictureOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
|
|
+ && enterPictureInPictureOnLeave;
|
|
if (player != null) {
|
|
- PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder, this.enterPictureInPictureOnLeave);
|
|
+ PictureInPictureUtil.applyAutoEnterEnabled(themedReactContext, pictureInPictureParamsBuilder,
|
|
+ this.enterPictureInPictureOnLeave);
|
|
}
|
|
}
|
|
|
|
@@ -2473,12 +2549,14 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
eventEmitter.onPictureInPictureStatusChanged.invoke(isInPictureInPicture);
|
|
|
|
if (fullScreenPlayerView != null && fullScreenPlayerView.isShowing()) {
|
|
- if (isInPictureInPicture) fullScreenPlayerView.hideWithoutPlayer();
|
|
+ if (isInPictureInPicture)
|
|
+ fullScreenPlayerView.hideWithoutPlayer();
|
|
return;
|
|
}
|
|
|
|
Activity currentActivity = themedReactContext.getCurrentActivity();
|
|
- if (currentActivity == null) return;
|
|
+ if (currentActivity == null)
|
|
+ return;
|
|
|
|
View decorView = currentActivity.getWindow().getDecorView();
|
|
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
|
@@ -2488,7 +2566,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
LayoutParams.MATCH_PARENT);
|
|
|
|
if (isInPictureInPicture) {
|
|
- ViewGroup parent = (ViewGroup)exoPlayerView.getParent();
|
|
+ ViewGroup parent = (ViewGroup) exoPlayerView.getParent();
|
|
if (parent != null) {
|
|
parent.removeView(exoPlayerView);
|
|
}
|
|
@@ -2514,10 +2592,12 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
public void enterPictureInPictureMode() {
|
|
PictureInPictureParams _pipParams = null;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
- ArrayList<RemoteAction> actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver);
|
|
+ ArrayList<RemoteAction> actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext,
|
|
+ isPaused, pictureInPictureReceiver);
|
|
pictureInPictureParamsBuilder.setActions(actions);
|
|
if (player.getPlaybackState() == Player.STATE_READY) {
|
|
- pictureInPictureParamsBuilder.setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player));
|
|
+ pictureInPictureParamsBuilder
|
|
+ .setAspectRatio(PictureInPictureUtil.calcPictureInPictureAspectRatio(player));
|
|
}
|
|
_pipParams = pictureInPictureParamsBuilder.build();
|
|
}
|
|
@@ -2526,13 +2606,15 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
public void exitPictureInPictureMode() {
|
|
Activity currentActivity = themedReactContext.getCurrentActivity();
|
|
- if (currentActivity == null) return;
|
|
+ if (currentActivity == null)
|
|
+ return;
|
|
|
|
View decorView = currentActivity.getWindow().getDecorView();
|
|
ViewGroup rootView = decorView.findViewById(android.R.id.content);
|
|
|
|
if (!rootViewChildrenOriginalVisibility.isEmpty()) {
|
|
- if (exoPlayerView.getParent().equals(rootView)) rootView.removeView(exoPlayerView);
|
|
+ if (exoPlayerView.getParent().equals(rootView))
|
|
+ rootView.removeView(exoPlayerView);
|
|
for (int i = 0; i < rootView.getChildCount(); i++) {
|
|
rootView.getChildAt(i).setVisibility(rootViewChildrenOriginalVisibility.get(i));
|
|
}
|
|
@@ -2630,7 +2712,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
|
|
if (playbackServiceConnection == null && showNotificationControls) {
|
|
setupPlaybackService();
|
|
- } else if(!showNotificationControls && playbackServiceConnection != null) {
|
|
+ } else if (!showNotificationControls && playbackServiceConnection != null) {
|
|
cleanupPlaybackService();
|
|
}
|
|
}
|
|
@@ -2659,12 +2741,13 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
if (isFullscreen) {
|
|
- fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null, new OnBackPressedCallback(true) {
|
|
- @Override
|
|
- public void handleOnBackPressed() {
|
|
- setFullscreen(false);
|
|
- }
|
|
- }, controlsConfig);
|
|
+ fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, null,
|
|
+ new OnBackPressedCallback(true) {
|
|
+ @Override
|
|
+ public void handleOnBackPressed() {
|
|
+ setFullscreen(false);
|
|
+ }
|
|
+ }, controlsConfig);
|
|
eventEmitter.onVideoFullscreenPlayerWillPresent.invoke();
|
|
if (fullScreenPlayerView != null) {
|
|
fullScreenPlayerView.show();
|
|
@@ -2701,7 +2784,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
}
|
|
|
|
@Override
|
|
- public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId, @NonNull Exception e) {
|
|
+ public void onDrmSessionManagerError(int windowIndex, MediaSource.MediaPeriodId mediaPeriodId,
|
|
+ @NonNull Exception e) {
|
|
DebugLog.d("DRM Info", "onDrmSessionManagerError");
|
|
eventEmitter.onVideoError.invoke("onDrmSessionManagerError", e, "3002");
|
|
}
|
|
@@ -2719,7 +2803,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
/**
|
|
* Handling controls prop
|
|
*
|
|
- * @param controls Controls prop, if true enable controls, if false disable them
|
|
+ * @param controls Controls prop, if true enable controls, if false disable them
|
|
*/
|
|
public void setControls(boolean controls) {
|
|
this.controls = controls;
|
|
@@ -2728,7 +2812,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
// Additional configuration for proper touch handling
|
|
if (controls) {
|
|
exoPlayerView.setControllerAutoShow(true);
|
|
- exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide
|
|
+ exoPlayerView.setControllerHideOnTouch(true); // Show controls on touch, don't hide
|
|
exoPlayerView.setControllerShowTimeoutMs(5000);
|
|
}
|
|
}
|
|
@@ -2761,8 +2845,7 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
Map<String, String> errMap = Map.of(
|
|
"message", error.getMessage(),
|
|
"code", String.valueOf(error.getErrorCode()),
|
|
- "type", String.valueOf(error.getErrorType())
|
|
- );
|
|
+ "type", String.valueOf(error.getErrorType()));
|
|
eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap);
|
|
|
|
handleDaiBackupStream();
|
|
@@ -2796,10 +2879,10 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
* @return The configured IMA server-side ad insertion AdsLoader
|
|
*/
|
|
private ImaServerSideAdInsertionMediaSource.AdsLoader createAdsLoader() {
|
|
- ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder =
|
|
- new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(getContext(), exoPlayerView.getPlayerView())
|
|
- .setAdEventListener(this)
|
|
- .setAdErrorListener(this);
|
|
+ ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(
|
|
+ getContext(), exoPlayerView.getPlayerView())
|
|
+ .setAdEventListener(this)
|
|
+ .setAdErrorListener(this);
|
|
|
|
return adsLoaderBuilder.build();
|
|
}
|
|
@@ -2815,8 +2898,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(getContext());
|
|
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory);
|
|
|
|
- ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory =
|
|
- new ImaServerSideAdInsertionMediaSource.Factory(daiAdsLoader, mediaSourceFactory);
|
|
+ ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = new ImaServerSideAdInsertionMediaSource.Factory(
|
|
+ daiAdsLoader, mediaSourceFactory);
|
|
|
|
mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory);
|
|
|
|
@@ -2850,7 +2933,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
/**
|
|
* 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 player.
|
|
+ * Builds an SSAI URI based on the provided parameters and sets it on the
|
|
+ * player.
|
|
* Supports both VOD (contentSourceId + videoId) and Live (assetKey) streams.
|
|
*
|
|
* @param runningSource The source containing DAI properties
|
|
@@ -2883,7 +2967,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
.build()
|
|
.buildUpon();
|
|
} else {
|
|
- throw new IllegalArgumentException("Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided");
|
|
+ throw new IllegalArgumentException(
|
|
+ "Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided");
|
|
}
|
|
|
|
Map<String, String> adTagParameters = adsProps.getAdTagParameters();
|
|
@@ -2906,7 +2991,8 @@ public class ReactExoplayerView extends FrameLayout implements
|
|
/**
|
|
* Handles fallback to backup stream when DAI stream fails.
|
|
*
|
|
- * If a backup stream URI is available in the DAI properties, it cleans up DAI resources
|
|
+ * If a backup stream URI is available in the DAI properties, it cleans up DAI
|
|
+ * resources
|
|
* and switches to the backup stream.
|
|
*
|
|
* @return true if backup stream was successfully used, false otherwise
|