* perf: ensure we do not provide callback to native if no callback provided from app * chore: rework bufferConfig to make it more generic and reduce ReactExoplayerView code size * chore: improve issue template * fix(android): avoid video view flickering at playback startup
		
			
				
	
	
		
			301 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| package com.brentvatne.exoplayer;
 | |
| 
 | |
| import android.content.Context;
 | |
| import androidx.core.content.ContextCompat;
 | |
| import androidx.media3.common.AdViewProvider;
 | |
| import androidx.media3.common.C;
 | |
| import androidx.media3.common.Format;
 | |
| import androidx.media3.common.Player;
 | |
| import androidx.media3.common.Tracks;
 | |
| import androidx.media3.common.VideoSize;
 | |
| import androidx.media3.common.text.Cue;
 | |
| import androidx.media3.common.util.Assertions;
 | |
| import androidx.media3.exoplayer.ExoPlayer;
 | |
| import androidx.media3.ui.SubtitleView;
 | |
| 
 | |
| import android.util.AttributeSet;
 | |
| import android.util.TypedValue;
 | |
| import android.view.Gravity;
 | |
| import android.view.SurfaceView;
 | |
| import android.view.TextureView;
 | |
| import android.view.View;
 | |
| import android.view.ViewGroup;
 | |
| import android.widget.FrameLayout;
 | |
| 
 | |
| import com.brentvatne.common.api.ResizeMode;
 | |
| import com.brentvatne.common.api.SubtitleStyle;
 | |
| import com.google.common.collect.ImmutableList;
 | |
| 
 | |
| import java.util.List;
 | |
| 
 | |
| public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
 | |
| 
 | |
|     private View surfaceView;
 | |
|     private final View shutterView;
 | |
|     private final SubtitleView subtitleLayout;
 | |
|     private final AspectRatioFrameLayout layout;
 | |
|     private final ComponentListener componentListener;
 | |
|     private ExoPlayer player;
 | |
|     private Context context;
 | |
|     private ViewGroup.LayoutParams layoutParams;
 | |
|     private final FrameLayout adOverlayFrameLayout;
 | |
| 
 | |
|     private boolean useTextureView = true;
 | |
|     private boolean useSecureView = false;
 | |
|     private boolean hideShutterView = false;
 | |
| 
 | |
|     public ExoPlayerView(Context context) {
 | |
|         this(context, null);
 | |
|     }
 | |
| 
 | |
|     public ExoPlayerView(Context context, AttributeSet attrs) {
 | |
|         this(context, attrs, 0);
 | |
|     }
 | |
| 
 | |
|     public ExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
 | |
|         super(context, attrs, defStyleAttr);
 | |
| 
 | |
|         this.context = context;
 | |
| 
 | |
|         layoutParams = new ViewGroup.LayoutParams(
 | |
|                 ViewGroup.LayoutParams.MATCH_PARENT,
 | |
|                 ViewGroup.LayoutParams.MATCH_PARENT);
 | |
| 
 | |
|         componentListener = new ComponentListener();
 | |
| 
 | |
|         FrameLayout.LayoutParams aspectRatioParams = new FrameLayout.LayoutParams(
 | |
|                 FrameLayout.LayoutParams.MATCH_PARENT,
 | |
|                 FrameLayout.LayoutParams.MATCH_PARENT);
 | |
|         aspectRatioParams.gravity = Gravity.CENTER;
 | |
|         layout = new AspectRatioFrameLayout(context);
 | |
|         layout.setLayoutParams(aspectRatioParams);
 | |
| 
 | |
|         shutterView = new View(getContext());
 | |
|         shutterView.setLayoutParams(layoutParams);
 | |
|         shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
 | |
| 
 | |
|         subtitleLayout = new SubtitleView(context);
 | |
|         subtitleLayout.setLayoutParams(layoutParams);
 | |
|         subtitleLayout.setUserDefaultStyle();
 | |
|         subtitleLayout.setUserDefaultTextSize();
 | |
| 
 | |
|         updateSurfaceView();
 | |
| 
 | |
|         adOverlayFrameLayout = new FrameLayout(context);
 | |
| 
 | |
|         layout.addView(shutterView, 1, layoutParams);
 | |
|         layout.addView(subtitleLayout, 2, layoutParams);
 | |
|         layout.addView(adOverlayFrameLayout, 3, layoutParams);
 | |
| 
 | |
|         addViewInLayout(layout, 0, aspectRatioParams);
 | |
|     }
 | |
| 
 | |
|     private void clearVideoView() {
 | |
|         if (surfaceView instanceof TextureView) {
 | |
|             player.clearVideoTextureView((TextureView) surfaceView);
 | |
|         } else if (surfaceView instanceof SurfaceView) {
 | |
|             player.clearVideoSurfaceView((SurfaceView) surfaceView);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private void setVideoView() {
 | |
|         if (surfaceView instanceof TextureView) {
 | |
|             player.setVideoTextureView((TextureView) surfaceView);
 | |
|         } else if (surfaceView instanceof SurfaceView) {
 | |
|             player.setVideoSurfaceView((SurfaceView) surfaceView);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public boolean isPlaying() {
 | |
|         return player != null && player.isPlaying();
 | |
|     }
 | |
| 
 | |
|     public void setSubtitleStyle(SubtitleStyle style) {
 | |
|         // ensure we reset subtile style before reapplying it
 | |
|         subtitleLayout.setUserDefaultStyle();
 | |
|         subtitleLayout.setUserDefaultTextSize();
 | |
| 
 | |
|         if (style.getFontSize() > 0) {
 | |
|             subtitleLayout.setFixedTextSize(TypedValue.COMPLEX_UNIT_SP, style.getFontSize());
 | |
|         }
 | |
|         subtitleLayout.setPadding(style.getPaddingLeft(), style.getPaddingTop(), style.getPaddingRight(), style.getPaddingBottom());
 | |
|         if (style.getOpacity() != 0) {
 | |
|             subtitleLayout.setAlpha(style.getOpacity());
 | |
|             subtitleLayout.setVisibility(View.VISIBLE);
 | |
|         } else {
 | |
|             subtitleLayout.setVisibility(View.GONE);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     public void setShutterColor(Integer color) {
 | |
|         shutterView.setBackgroundColor(color);
 | |
|     }
 | |
| 
 | |
|     private void updateSurfaceView() {
 | |
|         View view;
 | |
|         if (!useTextureView || useSecureView) {
 | |
|             view = new SurfaceView(context);
 | |
|             if (useSecureView) {
 | |
|                 ((SurfaceView)view).setSecure(true);
 | |
|             }
 | |
|         } else {
 | |
|             view = new TextureView(context);
 | |
|             // Support opacity properly:
 | |
|             ((TextureView) view).setOpaque(false);
 | |
|         }
 | |
|         view.setLayoutParams(layoutParams);
 | |
| 
 | |
|         surfaceView = view;
 | |
|         if (layout.getChildAt(0) != null) {
 | |
|             layout.removeViewAt(0);
 | |
|         }
 | |
|         layout.addView(surfaceView, 0, layoutParams);
 | |
| 
 | |
|         if (this.player != null) {
 | |
|             setVideoView();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private void updateShutterViewVisibility() {
 | |
|         shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
 | |
|     }
 | |
| 
 | |
|     @Override
 | |
|     public void requestLayout() {
 | |
|         super.requestLayout();
 | |
|         post(measureAndLayout);
 | |
|     }
 | |
| 
 | |
|     // AdsLoader.AdViewProvider implementation.
 | |
| 
 | |
|     @Override
 | |
|     public ViewGroup getAdViewGroup() {
 | |
|         return Assertions.checkNotNull(adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback");
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
 | |
|      * player will be called and previous
 | |
|      * assignments are overridden.
 | |
|      *
 | |
|      * @param player The {@link ExoPlayer} to use.
 | |
|      */
 | |
|     public void setPlayer(ExoPlayer player) {
 | |
|         if (this.player == player) {
 | |
|             return;
 | |
|         }
 | |
|         if (this.player != null) {
 | |
|             this.player.removeListener(componentListener);
 | |
|             clearVideoView();
 | |
|         }
 | |
|         this.player = player;
 | |
|         shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
 | |
|         if (player != null) {
 | |
|             setVideoView();
 | |
|             player.addListener(componentListener);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets the resize mode which can be of value {@link ResizeMode.Mode}
 | |
|      *
 | |
|      * @param resizeMode The resize mode.
 | |
|      */
 | |
|     public void setResizeMode(@ResizeMode.Mode int resizeMode) {
 | |
|         if (layout.getResizeMode() != resizeMode) {
 | |
|             layout.setResizeMode(resizeMode);
 | |
|             post(measureAndLayout);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get the view onto which video is rendered. This is either a {@link SurfaceView} (default)
 | |
|      * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
 | |
|      *
 | |
|      * @return either a {@link SurfaceView} or a {@link TextureView}.
 | |
|      */
 | |
|     public View getVideoSurfaceView() {
 | |
|         return surfaceView;
 | |
|     }
 | |
| 
 | |
|     public void setUseTextureView(boolean useTextureView) {
 | |
|         if (useTextureView != this.useTextureView) {
 | |
|             this.useTextureView = useTextureView;
 | |
|             updateSurfaceView();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public void useSecureView(boolean useSecureView) {
 | |
|         if (useSecureView != this.useSecureView) {
 | |
|             this.useSecureView = useSecureView;
 | |
|             updateSurfaceView();
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public void setHideShutterView(boolean hideShutterView) {
 | |
|         this.hideShutterView = hideShutterView;
 | |
|         updateShutterViewVisibility();
 | |
|     }
 | |
| 
 | |
|     private final Runnable measureAndLayout = () -> {
 | |
|         measure(
 | |
|                 MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
 | |
|                 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
 | |
|         layout(getLeft(), getTop(), getRight(), getBottom());
 | |
|     };
 | |
| 
 | |
|     private void updateForCurrentTrackSelections(Tracks tracks) {
 | |
|         if (tracks == null) {
 | |
|             return;
 | |
|         }
 | |
|         ImmutableList<Tracks.Group> groups = tracks.getGroups();
 | |
|         for (Tracks.Group group: groups) {
 | |
|             if (group.getType() == C.TRACK_TYPE_VIDEO && group.length > 0) {
 | |
|                 // get the first track of the group to identify aspect ratio
 | |
|                 Format format = group.getTrackFormat(0);
 | |
| 
 | |
|                 // update aspect ratio !
 | |
|                 layout.setAspectRatio(format.height == 0 ? 1 : (format.width * format.pixelWidthHeightRatio) / format.height);
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         // no video tracks, in that case refresh shutterView visibility
 | |
|         shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
 | |
|     }
 | |
| 
 | |
|     public void invalidateAspectRatio() {
 | |
|         // Resetting aspect ratio will force layout refresh on next video size changed
 | |
|         layout.invalidateAspectRatio();
 | |
|     }
 | |
| 
 | |
|     private final class ComponentListener implements Player.Listener {
 | |
| 
 | |
|         @Override
 | |
|         public void onCues(List<Cue> cues) {
 | |
|             subtitleLayout.setCues(cues);
 | |
|         }
 | |
| 
 | |
|         @Override
 | |
|         public void onVideoSizeChanged(VideoSize videoSize) {
 | |
|             boolean isInitialRatio = layout.getAspectRatio() == 0;
 | |
|             layout.setAspectRatio(videoSize.height == 0 ? 1 : (videoSize.width * videoSize.pixelWidthHeightRatio) / videoSize.height);
 | |
| 
 | |
|             // React native workaround for measuring and layout on initial load.
 | |
|             if (isInitialRatio) {
 | |
|                 post(measureAndLayout);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         @Override
 | |
|         public void onRenderedFirstFrame() {
 | |
|             shutterView.setVisibility(INVISIBLE);
 | |
|         }
 | |
| 
 | |
|         @Override
 | |
|         public void onTracksChanged(Tracks tracks) {
 | |
|             updateForCurrentTrackSelections(tracks);
 | |
|         }
 | |
|     }
 | |
| }
 |