294 lines
10 KiB
Java
Raw Normal View History

2017-01-11 12:51:45 +00:00
package com.brentvatne.exoplayer;
refactor: move view type and drm in source (#3867) * 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 * chore(android): refactor DRM props into a dedicated class * Update android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java * chore: fix linter * fix: ensure drm prop is correctly cleaned * feat(android): move viewType (secure texture) & drm inside the source The origianl behavior has been kept for interoperability, but marked as deprecated in doc * chore: fix linter * chore(ios): move drm prop in source like on android * chore: fix linter * chore: clean log * fix: allow to disable secure View * chore: fix viewType resolution (source value was not handled) * chore: use contentDeepEquals instead of manual checks * chore: fix linter * fix: ensure player doesn't start when view is unmounted * Fix/ensure view drop stop playback startup (#3875) * fix: ensure player doesn't start when view is unmounted * chore: revert change * chore: add warning in case of invalid Surface configuration * chore: code clean * fix: simplify surface management * chore: restore previous code * chore: fix typo * chore: code cleanup * feat(android): add multiDrm flag support * docs: update docs * chore: fix ios build * chore: fix deprecated declaration --------- Co-authored-by: Krzysztof Moch <krzysmoch.programs@gmail.com>
2024-07-10 12:17:22 +02:00
import android.annotation.SuppressLint;
2017-01-11 12:51:45 +00:00
import android.content.Context;
import androidx.annotation.NonNull;
2019-07-28 15:42:32 +02:00
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.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;
import com.brentvatne.common.api.ResizeMode;
import com.brentvatne.common.api.SubtitleStyle;
import com.brentvatne.common.api.ViewType;
import com.brentvatne.common.toolbox.DebugLog;
import com.google.common.collect.ImmutableList;
2017-01-11 12:51:45 +00:00
import java.util.List;
refactor: move view type and drm in source (#3867) * 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 * chore(android): refactor DRM props into a dedicated class * Update android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java * chore: fix linter * fix: ensure drm prop is correctly cleaned * feat(android): move viewType (secure texture) & drm inside the source The origianl behavior has been kept for interoperability, but marked as deprecated in doc * chore: fix linter * chore(ios): move drm prop in source like on android * chore: fix linter * chore: clean log * fix: allow to disable secure View * chore: fix viewType resolution (source value was not handled) * chore: use contentDeepEquals instead of manual checks * chore: fix linter * fix: ensure player doesn't start when view is unmounted * Fix/ensure view drop stop playback startup (#3875) * fix: ensure player doesn't start when view is unmounted * chore: revert change * chore: add warning in case of invalid Surface configuration * chore: code clean * fix: simplify surface management * chore: restore previous code * chore: fix typo * chore: code cleanup * feat(android): add multiDrm flag support * docs: update docs * chore: fix ios build * chore: fix deprecated declaration --------- Co-authored-by: Krzysztof Moch <krzysmoch.programs@gmail.com>
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 {
private final static String TAG = "ExoPlayerView";
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;
private ExoPlayer player;
private final Context context;
private final ViewGroup.LayoutParams layoutParams;
2022-11-09 14:26:39 +01:00
private final FrameLayout adOverlayFrameLayout;
private @ViewType.ViewType int viewType = ViewType.VIEW_TYPE_SURFACE;
private boolean hideShutterView = false;
2017-01-11 12:51:45 +00:00
public ExoPlayerView(Context context) {
refactor: move view type and drm in source (#3867) * 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 * chore(android): refactor DRM props into a dedicated class * Update android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java * chore: fix linter * fix: ensure drm prop is correctly cleaned * feat(android): move viewType (secure texture) & drm inside the source The origianl behavior has been kept for interoperability, but marked as deprecated in doc * chore: fix linter * chore(ios): move drm prop in source like on android * chore: fix linter * chore: clean log * fix: allow to disable secure View * chore: fix viewType resolution (source value was not handled) * chore: use contentDeepEquals instead of manual checks * chore: fix linter * fix: ensure player doesn't start when view is unmounted * Fix/ensure view drop stop playback startup (#3875) * fix: ensure player doesn't start when view is unmounted * chore: revert change * chore: add warning in case of invalid Surface configuration * chore: code clean * fix: simplify surface management * chore: restore previous code * chore: fix typo * chore: code cleanup * feat(android): add multiDrm flag support * docs: update docs * chore: fix ios build * chore: fix deprecated declaration --------- Co-authored-by: Krzysztof Moch <krzysmoch.programs@gmail.com>
2024-07-10 12:17:22 +02:00
super(context, null, 0);
2017-01-11 12:51:45 +00:00
this.context = context;
2017-01-11 12:51:45 +00: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());
shutterView.setLayoutParams(layoutParams);
2017-01-11 12:51:45 +00:00
shutterView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.black));
subtitleLayout = new SubtitleView(context);
subtitleLayout.setLayoutParams(layoutParams);
2017-01-11 12:51:45 +00:00
subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize();
updateSurfaceView(viewType);
2017-01-11 12:51:45 +00:00
adOverlayFrameLayout = new FrameLayout(context);
layout.addView(shutterView, 1, layoutParams);
layout.addView(adOverlayFrameLayout, 2, layoutParams);
2017-01-11 12:51:45 +00:00
addViewInLayout(layout, 0, aspectRatioParams);
addViewInLayout(subtitleLayout, 1, layoutParams);
2017-01-11 12:51:45 +00:00
}
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);
}
}
2023-07-23 21:38:26 +05:30
public void setShutterColor(Integer color) {
shutterView.setBackgroundColor(color);
}
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;
}
// Support opacity properly:
((TextureView) surfaceView).setOpaque(false);
} else {
DebugLog.wtf(TAG, "wtf is this texture " + viewType);
}
if (viewNeedRefresh) {
surfaceView.setLayoutParams(layoutParams);
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);
}
2022-11-09 14:26:39 +01:00
@Override
public void requestLayout() {
super.requestLayout();
post(measureAndLayout);
}
// 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
/**
* 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.
*
* @param player The {@link ExoPlayer} to use.
2017-01-11 12:51:45 +00: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);
clearVideoView();
2017-01-11 12:51:45 +00:00
}
this.player = player;
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
2017-01-11 12:51:45 +00:00
if (player != null) {
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) {
refactor: move view type and drm in source (#3867) * 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 * chore(android): refactor DRM props into a dedicated class * Update android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java * chore: fix linter * fix: ensure drm prop is correctly cleaned * feat(android): move viewType (secure texture) & drm inside the source The origianl behavior has been kept for interoperability, but marked as deprecated in doc * chore: fix linter * chore(ios): move drm prop in source like on android * chore: fix linter * chore: clean log * fix: allow to disable secure View * chore: fix viewType resolution (source value was not handled) * chore: use contentDeepEquals instead of manual checks * chore: fix linter * fix: ensure player doesn't start when view is unmounted * Fix/ensure view drop stop playback startup (#3875) * fix: ensure player doesn't start when view is unmounted * chore: revert change * chore: add warning in case of invalid Surface configuration * chore: code clean * fix: simplify surface management * chore: restore previous code * chore: fix typo * chore: code cleanup * feat(android): add multiDrm flag support * docs: update docs * chore: fix ios build * chore: fix deprecated declaration --------- Co-authored-by: Krzysztof Moch <krzysmoch.programs@gmail.com>
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);
}
}
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());
2017-01-11 12:51:45 +00:00
};
private void updateForCurrentTrackSelections(Tracks tracks) {
if (tracks == null) {
2017-01-11 12:51:45 +00:00
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);
// 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;
}
}
// no video tracks, in that case refresh shutterView visibility
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
2017-01-11 12:51:45 +00:00
}
public void invalidateAspectRatio() {
// Resetting aspect ratio will force layout refresh on next video size changed
layout.invalidateAspectRatio();
}
private final class ComponentListener implements Player.Listener {
2017-01-11 12:51:45 +00:00
@Override
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
public void onVideoSizeChanged(VideoSize videoSize) {
boolean isInitialRatio = layout.getVideoAspectRatio() == 0;
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;
}
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() {
shutterView.setVisibility(INVISIBLE);
2017-01-11 12:51:45 +00:00
}
@Override
public void onTracksChanged(@NonNull Tracks tracks) {
updateForCurrentTrackSelections(tracks);
}
2017-01-11 12:51:45 +00:00
}
}