From 149924ffcb0cbdeaa8c671ebb4b3b6115920131a Mon Sep 17 00:00:00 2001 From: Seyed Mostafa Hasani Date: Thu, 3 Oct 2024 01:07:18 +0330 Subject: [PATCH] feat(android): add live video label configuration (#4190) --- .../brentvatne/common/api/ControlsConfig.kt | 30 +++-- .../exoplayer/FullScreenPlayerView.kt | 9 ++ .../exoplayer/ReactExoplayerView.java | 126 ++++++++---------- android/src/main/res/drawable/circle.xml | 6 + .../layout/exo_legacy_player_control_view.xml | 35 ++++- android/src/main/res/values/colors.xml | 2 + android/src/main/res/values/dimens.xml | 1 + docs/pages/component/props.mdx | 28 ++-- examples/basic/src/VideoPlayer.tsx | 4 +- src/specs/VideoNativeComponent.ts | 1 + src/types/video.ts | 1 + 11 files changed, 146 insertions(+), 97 deletions(-) create mode 100644 android/src/main/res/drawable/circle.xml diff --git a/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt b/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt index ed231076..a09478c7 100644 --- a/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt +++ b/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt @@ -16,27 +16,29 @@ class ControlsConfig { var hideFullscreen: Boolean = false var hideNavigationBarOnFullScreenMode: Boolean = true var hideNotificationBarOnFullScreenMode: Boolean = true + var liveLabel: String? = null var seekIncrementMS: Int = 10000 companion object { @JvmStatic - fun parse(src: ReadableMap?): ControlsConfig { + fun parse(controlsConfig: ReadableMap?): ControlsConfig { val config = ControlsConfig() - if (src != null) { - config.hideSeekBar = ReactBridgeUtils.safeGetBool(src, "hideSeekBar", false) - config.hideDuration = ReactBridgeUtils.safeGetBool(src, "hideDuration", false) - config.hidePosition = ReactBridgeUtils.safeGetBool(src, "hidePosition", false) - config.hidePlayPause = ReactBridgeUtils.safeGetBool(src, "hidePlayPause", false) - config.hideForward = ReactBridgeUtils.safeGetBool(src, "hideForward", false) - config.hideRewind = ReactBridgeUtils.safeGetBool(src, "hideRewind", false) - config.hideNext = ReactBridgeUtils.safeGetBool(src, "hideNext", false) - config.hidePrevious = ReactBridgeUtils.safeGetBool(src, "hidePrevious", false) - config.hideFullscreen = ReactBridgeUtils.safeGetBool(src, "hideFullscreen", false) - config.seekIncrementMS = ReactBridgeUtils.safeGetInt(src, "seekIncrementMS", 10000) - config.hideNavigationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(src, "hideNavigationBarOnFullScreenMode", true) - config.hideNotificationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(src, "hideNotificationBarOnFullScreenMode", true) + if (controlsConfig != null) { + config.hideSeekBar = ReactBridgeUtils.safeGetBool(controlsConfig, "hideSeekBar", false) + config.hideDuration = ReactBridgeUtils.safeGetBool(controlsConfig, "hideDuration", false) + config.hidePosition = ReactBridgeUtils.safeGetBool(controlsConfig, "hidePosition", false) + config.hidePlayPause = ReactBridgeUtils.safeGetBool(controlsConfig, "hidePlayPause", false) + config.hideForward = ReactBridgeUtils.safeGetBool(controlsConfig, "hideForward", false) + config.hideRewind = ReactBridgeUtils.safeGetBool(controlsConfig, "hideRewind", false) + config.hideNext = ReactBridgeUtils.safeGetBool(controlsConfig, "hideNext", false) + config.hidePrevious = ReactBridgeUtils.safeGetBool(controlsConfig, "hidePrevious", false) + config.hideFullscreen = ReactBridgeUtils.safeGetBool(controlsConfig, "hideFullscreen", false) + config.seekIncrementMS = ReactBridgeUtils.safeGetInt(controlsConfig, "seekIncrementMS", 10000) + config.hideNavigationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(controlsConfig, "hideNavigationBarOnFullScreenMode", true) + config.hideNotificationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(controlsConfig, "hideNotificationBarOnFullScreenMode", true) + config.liveLabel = ReactBridgeUtils.safeGetString(controlsConfig, "liveLabel", null) } return config } diff --git a/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt b/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt index e6bad1b0..9eff62dd 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt +++ b/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt @@ -10,6 +10,7 @@ import android.view.Window import android.view.WindowManager import android.widget.FrameLayout import android.widget.ImageButton +import android.widget.LinearLayout import androidx.activity.OnBackPressedCallback import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -216,5 +217,13 @@ class FullScreenPlayerView( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE ) } + if (controlsConfig.hideNotificationBarOnFullScreenMode) { + val liveContainer = playerControlView?.findViewById(com.brentvatne.react.R.id.exo_live_container) + liveContainer?.let { + val layoutParams = it.layoutParams as LinearLayout.LayoutParams + layoutParams.topMargin = 40 + it.layoutParams = layoutParams + } + } } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 46ec5ed2..33f9d205 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -527,88 +527,78 @@ public class ReactExoplayerView extends FrameLayout implements view.layout(view.getLeft(), view.getTop(), view.getMeasuredWidth(), view.getMeasuredHeight()); } - private void refreshControlsStyles (){ - if(playerControlView == null) return; + private void refreshControlsStyles() { + if (playerControlView == null || player == null || !controls) return; + updateLiveContent(); + updatePlayPauseButtons(); + updateButtonVisibility(controlsConfig.getHideForward(), R.id.exo_ffwd); + updateButtonVisibility(controlsConfig.getHideRewind(), R.id.exo_rew); + updateButtonVisibility(controlsConfig.getHideNext(), R.id.exo_next); + updateButtonVisibility(controlsConfig.getHidePrevious(), R.id.exo_prev); + updateViewVisibility(playerControlView.findViewById(R.id.exo_fullscreen), controlsConfig.getHideFullscreen(), GONE); + updateViewVisibility(playerControlView.findViewById(R.id.exo_position), controlsConfig.getHidePosition(), GONE); + updateViewVisibility(playerControlView.findViewById(R.id.exo_progress), controlsConfig.getHideSeekBar(), INVISIBLE); + updateViewVisibility(playerControlView.findViewById(R.id.exo_duration), controlsConfig.getHideDuration(), GONE); + } + private void updateLiveContent() { + LinearLayout exoLiveContainer = playerControlView.findViewById(R.id.exo_live_container); + TextView exoLiveLabel = playerControlView.findViewById(R.id.exo_live_label); + + boolean isLive = false; + Timeline timeline = player.getCurrentTimeline(); + + // Determine if the content is live + if (!timeline.isEmpty()) { + Timeline.Window window = new Timeline.Window(); + timeline.getWindow(player.getCurrentMediaItemIndex(), window); + isLive = window.isLive(); + } + + if (isLive && controlsConfig.getLiveLabel() != null) { + exoLiveLabel.setText(controlsConfig.getLiveLabel()); + exoLiveContainer.setVisibility(VISIBLE); + } else { + exoLiveContainer.setVisibility(GONE); + } + } + + private void updatePlayPauseButtons() { final ImageButton playButton = playerControlView.findViewById(R.id.exo_play); final ImageButton pauseButton = playerControlView.findViewById(R.id.exo_pause); + if (controlsConfig.getHidePlayPause()) { playPauseControlContainer.setAlpha(0); - playButton.setClickable(false); pauseButton.setClickable(false); } else { playPauseControlContainer.setAlpha(1.0f); - playButton.setClickable(true); pauseButton.setClickable(true); } + } - final ImageButton forwardButton = playerControlView.findViewById(R.id.exo_ffwd); - if (controlsConfig.getHideForward()) { - forwardButton.setImageAlpha(0); - forwardButton.setClickable(false); + private void updateButtonVisibility(boolean hide, int buttonID) { + ImageButton button = playerControlView.findViewById(buttonID); + if (hide) { + button.setImageAlpha(0); + button.setClickable(false); } else { - forwardButton.setImageAlpha(255); - forwardButton.setClickable(true); - } - - final ImageButton rewindButton = playerControlView.findViewById(R.id.exo_rew); - if (controlsConfig.getHideRewind()) { - rewindButton.setImageAlpha(0); - rewindButton.setClickable(false); - } else { - rewindButton.setImageAlpha(255); - rewindButton.setClickable(true); - } - - final ImageButton nextButton = playerControlView.findViewById(R.id.exo_next); - if (controlsConfig.getHideNext()) { - nextButton.setClickable(false); - nextButton.setImageAlpha(0); - } else { - nextButton.setImageAlpha(255); - nextButton.setClickable(true); - } - - final ImageButton previousButton = playerControlView.findViewById(R.id.exo_prev); - if (controlsConfig.getHidePrevious()) { - previousButton.setImageAlpha(0); - previousButton.setClickable(false); - } else { - previousButton.setImageAlpha(255); - previousButton.setClickable(true); - } - - final ImageButton fullscreenButton = playerControlView.findViewById(R.id.exo_fullscreen); - if (controlsConfig.getHideFullscreen()) { - fullscreenButton.setVisibility(GONE); - } else if (fullscreenButton.getVisibility() == GONE) { - fullscreenButton.setVisibility(VISIBLE); - } - - final TextView positionText = playerControlView.findViewById(R.id.exo_position); - if(controlsConfig.getHidePosition()){ - positionText.setVisibility(GONE); - } else if (positionText.getVisibility() == GONE){ - positionText.setVisibility(VISIBLE); - } - - final DefaultTimeBar progressBar = playerControlView.findViewById(R.id.exo_progress); - if (controlsConfig.getHideSeekBar()) { - progressBar.setVisibility(INVISIBLE); - } else if (progressBar.getVisibility() == INVISIBLE) { - progressBar.setVisibility(VISIBLE); - } - - final TextView durationText = playerControlView.findViewById(R.id.exo_duration); - if (controlsConfig.getHideDuration()) { - durationText.setVisibility(GONE); - } else if (durationText.getVisibility() == GONE) { - durationText.setVisibility(VISIBLE); + button.setImageAlpha(255); + button.setClickable(true); } } + private void updateViewVisibility(View view, boolean hide, int hideVisibility) { + if (hide) { + view.setVisibility(hideVisibility); + } else if (view.getVisibility() == hideVisibility) { + view.setVisibility(VISIBLE); + } + } + + + private void reLayoutControls() { reLayout(exoPlayerView); reLayout(playerControlView); @@ -1508,6 +1498,7 @@ public class ReactExoplayerView extends FrameLayout implements eventEmitter.onVideoLoad.invoke(duration, currentPosition, width, height, audioTracks, textTracks, videoTracks, trackId); + refreshControlsStyles(); } } @@ -1757,7 +1748,7 @@ public class ReactExoplayerView extends FrameLayout implements } eventEmitter.onVideoPlaybackStateChanged.invoke(isPlaying, isSeeking); - + if (isPlaying) { isSeeking = false; } @@ -2406,6 +2397,7 @@ public class ReactExoplayerView extends FrameLayout implements removeViewAt(indexOfPC); } } + refreshControlsStyles(); } public void setSubtitleStyle(SubtitleStyle style) { @@ -2440,4 +2432,4 @@ public class ReactExoplayerView extends FrameLayout implements controlsConfig = controlsStyles; refreshControlsStyles(); } -} \ No newline at end of file +} diff --git a/android/src/main/res/drawable/circle.xml b/android/src/main/res/drawable/circle.xml new file mode 100644 index 00000000..9f06d7c7 --- /dev/null +++ b/android/src/main/res/drawable/circle.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/src/main/res/layout/exo_legacy_player_control_view.xml b/android/src/main/res/layout/exo_legacy_player_control_view.xml index 50ef45b2..0d4800a8 100644 --- a/android/src/main/res/layout/exo_legacy_player_control_view.xml +++ b/android/src/main/res/layout/exo_legacy_player_control_view.xml @@ -1,17 +1,48 @@ + + + + + + + + + #FFBEBEBE #CC000000 + #FFFFFF + #FF0000 \ No newline at end of file diff --git a/android/src/main/res/values/dimens.xml b/android/src/main/res/values/dimens.xml index 218dca64..4336a34c 100644 --- a/android/src/main/res/values/dimens.xml +++ b/android/src/main/res/values/dimens.xml @@ -3,6 +3,7 @@ 4dp 4dp + 12dp 4dp 4dp diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index f7ee531a..ebd862f2 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -144,20 +144,21 @@ If needed, you can also add your controls or use a package like [react-native-vi Adjust the control styles. This prop is need only if `controls={true}` and is an object. See the list of prop supported below. -| Property | Type | Description | -|-----------------------------------|---------|--------------------------------------------------------------------------------------------| -| hidePosition | boolean | Hides the position indicator. Default is `false`. | -| hidePlayPause | boolean | Hides the play/pause button. Default is `false`. | -| hideForward | boolean | Hides the forward button. Default is `false`. | -| hideRewind | boolean | Hides the rewind button. Default is `false`. | -| hideNext | boolean | Hides the next button. Default is `false`. | -| hidePrevious | boolean | Hides the previous button. Default is `false`. | -| hideFullscreen | boolean | Hides the fullscreen button. Default is `false`. | -| hideSeekBar | boolean | The default value is `false`, allowing you to hide the seek bar for live broadcasts. | -| hideDuration | boolean | The default value is `false`, allowing you to hide the duration. | -| hideNavigationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the navigation bar on full-screen mode. | +| Property | Type | Description | +|-------------------------------------|---------|---------------------------------------------------------------------------------------------| +| hidePosition | boolean | Hides the position indicator. Default is `false`. | +| hidePlayPause | boolean | Hides the play/pause button. Default is `false`. | +| hideForward | boolean | Hides the forward button. Default is `false`. | +| hideRewind | boolean | Hides the rewind button. Default is `false`. | +| hideNext | boolean | Hides the next button. Default is `false`. | +| hidePrevious | boolean | Hides the previous button. Default is `false`. | +| hideFullscreen | boolean | Hides the fullscreen button. Default is `false`. | +| hideSeekBar | boolean | The default value is `false`, allowing you to hide the seek bar for live broadcasts. | +| hideDuration | boolean | The default value is `false`, allowing you to hide the duration. | +| hideNavigationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the navigation bar on full-screen mode. | | hideNotificationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the notification bar on full-screen mode. | -| seekIncrementMS | number | The default value is `10000`. You can change the value to increment forward and rewind. | +| seekIncrementMS | number | The default value is `10000`. You can change the value to increment forward and rewind. | +| liveLabel | string | Allowing you to set a label for live video. | Example with default values: @@ -175,6 +176,7 @@ controlsStyles={{ hideNavigationBarOnFullScreenMode: true, hideNotificationBarOnFullScreenMode: true, seekIncrementMS: 10000, + liveLabel: "LIVE" }} ``` diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index 569f93ef..5956124d 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -31,6 +31,7 @@ import Video, { type SelectedVideoTrack, type EnumValues, OnBandwidthUpdateData, + ControlsStyles, } from 'react-native-video'; import styles from './styles'; import {type AdditionalSourceInfo} from './types'; @@ -241,9 +242,10 @@ const VideoPlayer: FC = ({}) => { const _renderLoader = showPoster ? () => : undefined; const _subtitleStyle = {subtitlesFollowVideo: true}; - const _controlsStyles = { + const _controlsStyles : ControlsStyles = { hideNavigationBarOnFullScreenMode: true, hideNotificationBarOnFullScreenMode: true, + liveLabel: "LIVE" }; const _bufferConfig = { ...bufferConfig, diff --git a/src/specs/VideoNativeComponent.ts b/src/specs/VideoNativeComponent.ts index 165c4949..e9e69dfa 100644 --- a/src/specs/VideoNativeComponent.ts +++ b/src/specs/VideoNativeComponent.ts @@ -309,6 +309,7 @@ type ControlsStyles = Readonly<{ hideNavigationBarOnFullScreenMode?: WithDefault; hideNotificationBarOnFullScreenMode?: WithDefault; seekIncrementMS?: Int32; + liveLabel?: string; }>; export type OnControlsVisibilityChange = Readonly<{ diff --git a/src/types/video.ts b/src/types/video.ts index e9fe3191..ab71d69f 100644 --- a/src/types/video.ts +++ b/src/types/video.ts @@ -261,6 +261,7 @@ export type ControlsStyles = { hideNavigationBarOnFullScreenMode?: boolean; hideNotificationBarOnFullScreenMode?: boolean; seekIncrementMS?: number; + liveLabel?: string; }; export interface ReactVideoRenderLoaderProps {