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(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 rootViewChildrenOriginalVisibility = new ArrayList(); /* - * 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: ... + * @param view view needs to update layout + * + * This is a workaround for the open bug in react-native: ... */ 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 audioTracks = getAudioTrackInfo(); - ArrayList textTracks = getTextTrackInfo(); + ArrayList 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 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 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 getVideoTrackInfoFromManifest(int retryCount) { ExecutorService es = Executors.newSingleThreadExecutor(); @@ -1631,18 +1665,20 @@ public class ReactExoplayerView extends FrameLayout implements public ArrayList call() { ArrayList 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 actions = PictureInPictureUtil.getPictureInPictureActions(themedReactContext, isPaused, pictureInPictureReceiver); + ArrayList 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 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 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