2017-01-11 12:51:45 +00:00
|
|
|
package com.brentvatne.exoplayer;
|
|
|
|
|
2024-07-10 12:17:22 +02:00
|
|
|
import android.annotation.SuppressLint;
|
2017-01-11 12:51:45 +00:00
|
|
|
import android.content.Context;
|
2024-05-22 14:02:55 +02:00
|
|
|
|
|
|
|
import androidx.annotation.NonNull;
|
2019-07-28 15:42:32 +02:00
|
|
|
import androidx.core.content.ContextCompat;
|
2023-11-18 22:13:54 +09:00
|
|
|
import androidx.media3.common.AdViewProvider;
|
|
|
|
import androidx.media3.common.C;
|
2024-05-13 19:20:36 +02:00
|
|
|
import androidx.media3.common.Format;
|
2023-11-18 22:13:54 +09:00
|
|
|
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;
|
|
|
|
|
2022-07-05 23:58:30 +02:00
|
|
|
import android.util.TypedValue;
|
2017-01-11 12:51:45 +00:00
|
|
|
import android.view.Gravity;
|
|
|
|
import android.view.SurfaceView;
|
|
|
|
import android.view.TextureView;
|
|
|
|
import android.view.View;
|
|
|
|
import android.view.ViewGroup;
|
|
|
|
import android.widget.FrameLayout;
|
|
|
|
|
2023-12-07 08:47:40 +01:00
|
|
|
import com.brentvatne.common.api.ResizeMode;
|
|
|
|
import com.brentvatne.common.api.SubtitleStyle;
|
2024-06-27 11:58:06 +02:00
|
|
|
import com.brentvatne.common.api.ViewType;
|
|
|
|
import com.brentvatne.common.toolbox.DebugLog;
|
2024-05-13 19:20:36 +02:00
|
|
|
import com.google.common.collect.ImmutableList;
|
2017-01-11 12:51:45 +00:00
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
2024-07-10 12:17:22 +02:00
|
|
|
@SuppressLint("ViewConstructor")
|
2022-11-16 11:43:25 +01:00
|
|
|
public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
|
2024-06-27 11:58:06 +02:00
|
|
|
private final static String TAG = "ExoPlayerView";
|
2018-06-08 00:01:13 -07:00
|
|
|
private View surfaceView;
|
2017-01-11 12:51:45 +00:00
|
|
|
private final View shutterView;
|
|
|
|
private final SubtitleView subtitleLayout;
|
|
|
|
private final AspectRatioFrameLayout layout;
|
|
|
|
private final ComponentListener componentListener;
|
2022-06-16 00:24:55 +07:00
|
|
|
private ExoPlayer player;
|
2024-05-22 14:01:55 +02:00
|
|
|
private final Context context;
|
|
|
|
private final ViewGroup.LayoutParams layoutParams;
|
2022-11-09 14:26:39 +01:00
|
|
|
private final FrameLayout adOverlayFrameLayout;
|
2018-06-08 00:01:13 -07:00
|
|
|
|
2024-06-27 11:58:06 +02:00
|
|
|
private @ViewType.ViewType int viewType = ViewType.VIEW_TYPE_SURFACE;
|
2018-11-28 14:56:58 +02:00
|
|
|
private boolean hideShutterView = false;
|
2017-01-11 12:51:45 +00:00
|
|
|
|
|
|
|
public ExoPlayerView(Context context) {
|
2024-07-10 12:17:22 +02:00
|
|
|
super(context, null, 0);
|
2017-01-11 12:51:45 +00:00
|
|
|
|
2018-06-08 00:01:13 -07:00
|
|
|
this.context = context;
|
2017-01-11 12:51:45 +00:00
|
|
|
|
2018-06-08 00:01:13 -07:00
|
|
|
layoutParams = new ViewGroup.LayoutParams(
|
2017-01-11 12:51:45 +00:00
|
|
|
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());
|
2018-06-08 00:01:13 -07:00
|
|
|
shutterView.setLayoutParams(layoutParams);
|
2017-01-11 12:51:45 +00:00
|
|
|
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
|
|
|
|
|
|
|
|
subtitleLayout = new SubtitleView(context);
|
2018-06-08 00:01:13 -07:00
|
|
|
subtitleLayout.setLayoutParams(layoutParams);
|
2017-01-11 12:51:45 +00:00
|
|
|
subtitleLayout.setUserDefaultStyle();
|
|
|
|
subtitleLayout.setUserDefaultTextSize();
|
|
|
|
|
2024-06-27 11:58:06 +02:00
|
|
|
updateSurfaceView(viewType);
|
2017-01-11 12:51:45 +00:00
|
|
|
|
2022-11-16 20:16:37 +01:00
|
|
|
adOverlayFrameLayout = new FrameLayout(context);
|
|
|
|
|
2018-06-08 00:01:13 -07:00
|
|
|
layout.addView(shutterView, 1, layoutParams);
|
2024-05-28 16:29:40 +09:00
|
|
|
layout.addView(adOverlayFrameLayout, 2, layoutParams);
|
2017-01-11 12:51:45 +00:00
|
|
|
|
|
|
|
addViewInLayout(layout, 0, aspectRatioParams);
|
2024-05-28 16:29:40 +09:00
|
|
|
addViewInLayout(subtitleLayout, 1, layoutParams);
|
2017-01-11 12:51:45 +00:00
|
|
|
}
|
|
|
|
|
2021-05-07 11:37:57 +03:00
|
|
|
private void clearVideoView() {
|
|
|
|
if (surfaceView instanceof TextureView) {
|
|
|
|
player.clearVideoTextureView((TextureView) surfaceView);
|
|
|
|
} else if (surfaceView instanceof SurfaceView) {
|
|
|
|
player.clearVideoSurfaceView((SurfaceView) surfaceView);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-08 00:01:13 -07:00
|
|
|
private void setVideoView() {
|
|
|
|
if (surfaceView instanceof TextureView) {
|
|
|
|
player.setVideoTextureView((TextureView) surfaceView);
|
|
|
|
} else if (surfaceView instanceof SurfaceView) {
|
|
|
|
player.setVideoSurfaceView((SurfaceView) surfaceView);
|
|
|
|
}
|
|
|
|
}
|
2023-11-18 22:13:54 +09:00
|
|
|
|
2024-03-22 09:17:00 +01:00
|
|
|
public boolean isPlaying() {
|
|
|
|
return player != null && player.isPlaying();
|
|
|
|
}
|
|
|
|
|
2022-07-05 23:58:30 +02:00
|
|
|
public void setSubtitleStyle(SubtitleStyle style) {
|
2024-05-10 17:23:30 +05:00
|
|
|
// ensure we reset subtile style before reapplying it
|
2022-07-05 23:58:30 +02:00
|
|
|
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());
|
2024-03-14 19:29:50 +09:00
|
|
|
if (style.getOpacity() != 0) {
|
|
|
|
subtitleLayout.setAlpha(style.getOpacity());
|
|
|
|
subtitleLayout.setVisibility(View.VISIBLE);
|
|
|
|
} else {
|
|
|
|
subtitleLayout.setVisibility(View.GONE);
|
|
|
|
}
|
|
|
|
|
2022-07-05 23:58:30 +02:00
|
|
|
}
|
2018-06-08 00:01:13 -07:00
|
|
|
|
2023-07-23 21:38:26 +05:30
|
|
|
public void setShutterColor(Integer color) {
|
|
|
|
shutterView.setBackgroundColor(color);
|
|
|
|
}
|
|
|
|
|
2024-06-27 11:58:06 +02:00
|
|
|
public void updateSurfaceView(@ViewType.ViewType int viewType) {
|
|
|
|
this.viewType = viewType;
|
|
|
|
boolean viewNeedRefresh = false;
|
|
|
|
if (viewType == ViewType.VIEW_TYPE_SURFACE || viewType == ViewType.VIEW_TYPE_SURFACE_SECURE) {
|
|
|
|
if (!(surfaceView instanceof SurfaceView)) {
|
|
|
|
surfaceView = new SurfaceView(context);
|
|
|
|
viewNeedRefresh = true;
|
|
|
|
}
|
|
|
|
((SurfaceView)surfaceView).setSecure(viewType == ViewType.VIEW_TYPE_SURFACE_SECURE);
|
|
|
|
} else if (viewType == ViewType.VIEW_TYPE_TEXTURE) {
|
|
|
|
if (!(surfaceView instanceof TextureView)) {
|
|
|
|
surfaceView = new TextureView(context);
|
|
|
|
viewNeedRefresh = true;
|
2022-02-14 21:17:22 -04:00
|
|
|
}
|
2024-01-10 08:46:40 +01:00
|
|
|
// Support opacity properly:
|
2024-06-27 11:58:06 +02:00
|
|
|
((TextureView) surfaceView).setOpaque(false);
|
|
|
|
} else {
|
|
|
|
DebugLog.wtf(TAG, "wtf is this texture " + viewType);
|
2022-02-14 21:17:22 -04:00
|
|
|
}
|
2024-06-27 11:58:06 +02:00
|
|
|
if (viewNeedRefresh) {
|
|
|
|
surfaceView.setLayoutParams(layoutParams);
|
2018-06-08 00:01:13 -07:00
|
|
|
|
2024-06-27 11:58:06 +02:00
|
|
|
if (layout.getChildAt(0) != null) {
|
|
|
|
layout.removeViewAt(0);
|
|
|
|
}
|
|
|
|
layout.addView(surfaceView, 0, layoutParams);
|
2018-06-08 00:01:13 -07:00
|
|
|
|
2024-06-27 11:58:06 +02:00
|
|
|
if (this.player != null) {
|
|
|
|
setVideoView();
|
|
|
|
}
|
2018-06-08 00:01:13 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-28 14:56:58 +02:00
|
|
|
private void updateShutterViewVisibility() {
|
|
|
|
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
|
|
|
}
|
|
|
|
|
2022-11-09 14:26:39 +01:00
|
|
|
@Override
|
|
|
|
public void requestLayout() {
|
|
|
|
super.requestLayout();
|
|
|
|
post(measureAndLayout);
|
|
|
|
}
|
|
|
|
|
2023-11-18 22:13:54 +09:00
|
|
|
// AdsLoader.AdViewProvider implementation.
|
2022-11-09 14:26:39 +01:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public ViewGroup getAdViewGroup() {
|
|
|
|
return Assertions.checkNotNull(adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback");
|
|
|
|
}
|
|
|
|
|
2017-01-11 12:51:45 +00:00
|
|
|
/**
|
2022-06-16 00:24:55 +07:00
|
|
|
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
|
|
|
|
* player will be called and previous
|
2017-01-11 12:51:45 +00:00
|
|
|
* assignments are overridden.
|
|
|
|
*
|
2022-06-16 00:24:55 +07:00
|
|
|
* @param player The {@link ExoPlayer} to use.
|
2017-01-11 12:51:45 +00:00
|
|
|
*/
|
2022-06-16 00:24:55 +07:00
|
|
|
public void setPlayer(ExoPlayer player) {
|
2017-01-11 12:51:45 +00:00
|
|
|
if (this.player == player) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.player != null) {
|
|
|
|
this.player.removeListener(componentListener);
|
2021-05-07 11:37:57 +03:00
|
|
|
clearVideoView();
|
2017-01-11 12:51:45 +00:00
|
|
|
}
|
|
|
|
this.player = player;
|
2022-03-03 15:57:21 -08:00
|
|
|
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
2017-01-11 12:51:45 +00:00
|
|
|
if (player != null) {
|
2018-06-08 00:01:13 -07:00
|
|
|
setVideoView();
|
2017-01-11 12:51:45 +00:00
|
|
|
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) {
|
2024-07-10 12:17:22 +02:00
|
|
|
if (layout != null && layout.getResizeMode() != resizeMode) {
|
2017-01-11 12:51:45 +00:00
|
|
|
layout.setResizeMode(resizeMode);
|
|
|
|
post(measureAndLayout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-28 14:56:58 +02:00
|
|
|
public void setHideShutterView(boolean hideShutterView) {
|
|
|
|
this.hideShutterView = hideShutterView;
|
|
|
|
updateShutterViewVisibility();
|
|
|
|
}
|
|
|
|
|
2023-11-18 22:13:54 +09:00
|
|
|
private final Runnable measureAndLayout = () -> {
|
|
|
|
measure(
|
|
|
|
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
|
|
|
|
MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY));
|
|
|
|
layout(getLeft(), getTop(), getRight(), getBottom());
|
2017-01-11 12:51:45 +00:00
|
|
|
};
|
|
|
|
|
2024-05-13 19:20:36 +02:00
|
|
|
private void updateForCurrentTrackSelections(Tracks tracks) {
|
|
|
|
if (tracks == null) {
|
2017-01-11 12:51:45 +00:00
|
|
|
return;
|
|
|
|
}
|
2024-05-13 19:20:36 +02:00
|
|
|
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);
|
|
|
|
|
2024-07-15 10:15:29 +02:00
|
|
|
// There are weird cases when video height and width did not change with rotation so we need change aspect ration to fix it
|
|
|
|
switch (format.rotationDegrees) {
|
|
|
|
// update aspect ratio !
|
|
|
|
case 90, 270 -> {
|
|
|
|
layout.setVideoAspectRatio(format.width == 0 ? 1 : (format.height * format.pixelWidthHeightRatio) / format.width);
|
|
|
|
}
|
|
|
|
default -> {
|
|
|
|
layout.setVideoAspectRatio(format.height == 0 ? 1 : (format.width * format.pixelWidthHeightRatio) / format.height);
|
|
|
|
}
|
|
|
|
}
|
2017-01-11 12:51:45 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2024-05-13 19:20:36 +02:00
|
|
|
// no video tracks, in that case refresh shutterView visibility
|
2022-03-03 15:57:21 -08:00
|
|
|
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
|
2017-01-11 12:51:45 +00:00
|
|
|
}
|
|
|
|
|
2020-06-30 14:00:16 -03:00
|
|
|
public void invalidateAspectRatio() {
|
|
|
|
// Resetting aspect ratio will force layout refresh on next video size changed
|
|
|
|
layout.invalidateAspectRatio();
|
|
|
|
}
|
|
|
|
|
2022-06-16 00:24:55 +07:00
|
|
|
private final class ComponentListener implements Player.Listener {
|
2017-01-11 12:51:45 +00:00
|
|
|
|
|
|
|
@Override
|
2024-05-22 14:02:55 +02:00
|
|
|
public void onCues(@NonNull List<Cue> cues) {
|
2022-08-26 10:32:22 -04:00
|
|
|
subtitleLayout.setCues(cues);
|
2017-01-11 12:51:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2022-06-16 00:24:55 +07:00
|
|
|
public void onVideoSizeChanged(VideoSize videoSize) {
|
2024-07-11 11:36:22 +03:30
|
|
|
boolean isInitialRatio = layout.getVideoAspectRatio() == 0;
|
2024-05-17 14:47:03 +02:00
|
|
|
if (videoSize.height == 0 || videoSize.width == 0) {
|
|
|
|
// When changing video track we receive an ghost state with height / width = 0
|
|
|
|
// No need to resize the view in that case
|
|
|
|
return;
|
|
|
|
}
|
2024-07-11 11:36:22 +03:30
|
|
|
layout.setVideoAspectRatio((videoSize.width * videoSize.pixelWidthHeightRatio) / videoSize.height);
|
2017-01-11 12:51:45 +00:00
|
|
|
|
|
|
|
// React native workaround for measuring and layout on initial load.
|
|
|
|
if (isInitialRatio) {
|
|
|
|
post(measureAndLayout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onRenderedFirstFrame() {
|
2024-05-10 17:23:30 +05:00
|
|
|
shutterView.setVisibility(INVISIBLE);
|
2017-01-11 12:51:45 +00:00
|
|
|
}
|
|
|
|
|
2017-06-14 00:45:12 +02:00
|
|
|
@Override
|
2024-05-22 14:02:55 +02:00
|
|
|
public void onTracksChanged(@NonNull Tracks tracks) {
|
2024-05-13 19:20:36 +02:00
|
|
|
updateForCurrentTrackSelections(tracks);
|
2017-06-14 00:45:12 +02:00
|
|
|
}
|
2017-01-11 12:51:45 +00:00
|
|
|
}
|
|
|
|
}
|