/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media2.widget; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.Surface; import android.view.View; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.media2.common.BaseResult; import androidx.media2.common.MediaItem; import androidx.media2.common.MediaMetadata; import androidx.media2.common.SessionPlayer; import androidx.media2.common.SessionPlayer.TrackInfo; import androidx.media2.common.SubtitleData; import androidx.media2.common.VideoSize; import androidx.media2.session.MediaController; import androidx.media2.session.MediaSession; import androidx.palette.graphics.Palette; import com.google.common.util.concurrent.ListenableFuture; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; /** * A high level view for media playback that can be integrated with either a {@link SessionPlayer} * or a {@link MediaController}. Developers can easily implement a video rendering application * using this class. By default, a {@link MediaControlView} is attached so the playback * control buttons are displayed on top of VideoView. *

* Contents: *

    *
  1. Using VideoView with SessionPlayer or MediaController *
  2. Using VideoView with MediaControlView *
  3. Choosing a view type *
  4. Comparison with android.widget.VideoView *
  5. Displaying Metadata *
* *

Using VideoView with SessionPlayer or MediaController

* * *

Using VideoView with MediaControlView

* {@link VideoView} is working with {@link MediaControlView} and a MediaControlView * instance is attached to VideoView by default. *

* If you want to attach a custom {@link MediaControlView}, assign the custom media * control widget using {@link #setMediaControlView}. *

* If you don't want to use {@link MediaControlView}, set * the VideoView attribute {@link androidx.media2.widget.R.attr#enableControlView} to false. * *

Choosing a view type

* VideoView can render videos on a TextureView or SurfaceView. The * default is SurfaceView which can be changed by using the {@link #setViewType(int)} method or * by setting the {@link androidx.media2.widget.R.attr#viewType} attribute in the layout file. *

SurfaceView is recommended in most cases for saving battery life. * TextureView might be preferred for supporting various UIs such as animation and translucency. * *

Comparison with android.widget.VideoView

* These are the main differences between the media2 VideoView widget and the older android widget: * * *

Displaying Metadata

* When you play music only (sound with no video), VideoView can display album art and other * metadata by calling {@link MediaItem#setMetadata(MediaMetadata)}. * The following table shows the metadata displayed by the VideoView, and the default values * assigned if the keys are not set: * * * * * * * * *
KeyDefault
{@link MediaMetadata#METADATA_KEY_TITLE}{@link androidx.media2.widget.R.string#mcv2_music_title_unknown_text}
{@link MediaMetadata#METADATA_KEY_ARTIST}{@link androidx.media2.widget.R.string#mcv2_music_artist_unknown_text}
{@link MediaMetadata#METADATA_KEY_ALBUM_ART}{@link androidx.media2.widget.R.drawable#media2_widget_ic_default_album_image}
*

* Note: VideoView does not retain its full state when going into the background. In particular, it * does not save, and does not restore the current play state, play position, selected tracks. * Applications should save and restore these on their own in * {@link android.app.Activity#onSaveInstanceState} and * {@link android.app.Activity#onRestoreInstanceState}. *

Attributes : *

*

Example of attributes for a VideoView with TextureView and no attached control view: *

 {@code
 *  }
* * @see MediaControlView * @see SessionPlayer * @see MediaController */ public class VideoView extends SelectiveLayout { @IntDef({ VIEW_TYPE_TEXTUREVIEW, VIEW_TYPE_SURFACEVIEW }) @Retention(RetentionPolicy.SOURCE) /* package */ @interface ViewType {} /** * Indicates video is rendering on SurfaceView. * * @see #setViewType */ public static final int VIEW_TYPE_SURFACEVIEW = 0; /** * Indicates video is rendering on TextureView. * * @see #setViewType */ public static final int VIEW_TYPE_TEXTUREVIEW = 1; private static final String TAG = "VideoView"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); VideoView.OnViewTypeChangedListener mViewTypeChangedListener; VideoViewInterface mCurrentView; VideoViewInterface mTargetView; VideoTextureView mTextureView; VideoSurfaceView mSurfaceView; PlayerWrapper mPlayer; MediaControlView mMediaControlView; MusicView mMusicView; SelectiveLayout.LayoutParams mSelectiveLayoutParams; int mVideoTrackCount; int mAudioTrackCount; Map mSubtitleTracks; SubtitleController mSubtitleController; // selected subtitle track info as MediaPlayer returns TrackInfo mSelectedSubtitleTrackInfo; SubtitleAnchorView mSubtitleAnchorView; private final VideoViewInterface.SurfaceListener mSurfaceListener = new VideoViewInterface.SurfaceListener() { @Override public void onSurfaceCreated(@NonNull View view, int width, int height) { if (DEBUG) { Log.d(TAG, "onSurfaceCreated()" + ", width/height: " + width + "/" + height + ", " + view.toString()); } if (view == mTargetView && VideoView.this.isAggregatedVisible()) { mTargetView.assignSurfaceToPlayerWrapper(mPlayer); } } @Override public void onSurfaceDestroyed(@NonNull View view) { if (DEBUG) { Log.d(TAG, "onSurfaceDestroyed(). " + view.toString()); } } @Override public void onSurfaceChanged(@NonNull View view, int width, int height) { if (DEBUG) { Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height + ", " + view.toString()); } } @Override public void onSurfaceTakeOverDone(@NonNull VideoViewInterface view) { if (view != mTargetView) { Log.w(TAG, "onSurfaceTakeOverDone(). view is not targetView. ignore.: " + view); return; } if (DEBUG) { Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view); } if (view != mCurrentView) { ((View) mCurrentView).setVisibility(View.GONE); mCurrentView = view; if (mViewTypeChangedListener != null) { mViewTypeChangedListener.onViewTypeChanged(VideoView.this, view.getViewType()); } } } }; public VideoView(@NonNull Context context) { this(context, null); } public VideoView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public VideoView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(context, attrs); } private void initialize(Context context, @Nullable AttributeSet attrs) { mSelectedSubtitleTrackInfo = null; setFocusable(true); setFocusableInTouchMode(true); requestFocus(); mTextureView = new VideoTextureView(context); mSurfaceView = new VideoSurfaceView(context); mTextureView.setSurfaceListener(mSurfaceListener); mSurfaceView.setSurfaceListener(mSurfaceListener); addView(mTextureView); addView(mSurfaceView); mSelectiveLayoutParams = new SelectiveLayout.LayoutParams(); mSelectiveLayoutParams.forceMatchParent = true; mSubtitleAnchorView = new SubtitleAnchorView(context); mSubtitleAnchorView.setBackgroundColor(0); addView(mSubtitleAnchorView, mSelectiveLayoutParams); SubtitleController.Listener listener = new SubtitleController.Listener() { @Override public void onSubtitleTrackSelected(SubtitleTrack track) { // Track deselected if (track == null) { mSelectedSubtitleTrackInfo = null; mSubtitleAnchorView.setVisibility(View.GONE); return; } // Track selected TrackInfo info = null; for (Entry pair : mSubtitleTracks.entrySet()) { if (pair.getValue() == track) { info = pair.getKey(); break; } } if (info != null) { mSelectedSubtitleTrackInfo = info; mSubtitleAnchorView.setVisibility(View.VISIBLE); } } }; mSubtitleController = new SubtitleController(context, null, listener); mSubtitleController.registerRenderer(new Cea608CaptionRenderer(context)); mSubtitleController.registerRenderer(new Cea708CaptionRenderer(context)); mSubtitleController.setAnchor(mSubtitleAnchorView); mMusicView = new MusicView(context); mMusicView.setVisibility(View.GONE); addView(mMusicView, mSelectiveLayoutParams); boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue( "http://schemas.android.com/apk/res-auto", "enableControlView", true); if (enableControlView) { mMediaControlView = new MediaControlView(context); mMediaControlView.setAttachedToVideoView(true); addView(mMediaControlView, mSelectiveLayoutParams); } // Choose surface view by default int viewType = (attrs == null) ? VideoView.VIEW_TYPE_SURFACEVIEW : attrs.getAttributeIntValue( "http://schemas.android.com/apk/res-auto", "viewType", VideoView.VIEW_TYPE_SURFACEVIEW); if (viewType == VideoView.VIEW_TYPE_SURFACEVIEW) { if (DEBUG) { Log.d(TAG, "viewType attribute is surfaceView."); } mTextureView.setVisibility(View.GONE); mSurfaceView.setVisibility(View.VISIBLE); mCurrentView = mSurfaceView; } else if (viewType == VideoView.VIEW_TYPE_TEXTUREVIEW) { if (DEBUG) { Log.d(TAG, "viewType attribute is textureView."); } mTextureView.setVisibility(View.VISIBLE); mSurfaceView.setVisibility(View.GONE); mCurrentView = mTextureView; } mTargetView = mCurrentView; } /** * Sets {@link MediaController} to display media content. * Setting a {@link MediaController} will unset any {@link MediaController} or * {@link SessionPlayer} that was previously set. *

* If VideoView has a {@link MediaControlView} instance, this controller will also be set to it. *

* Calling this method will automatically set VideoView's surface to {@link MediaController} * by calling {@link MediaController#setSurface(Surface)}. If the {@link MediaController} is * connected to a {@link MediaSession} and that {@link MediaSession} is associated with a * {@link SessionPlayer}, VideoView's surface will be set to that {@link SessionPlayer}. * * @param controller the controller * @see #setPlayer */ public void setMediaController(@NonNull MediaController controller) { if (controller == null) { throw new NullPointerException("controller must not be null"); } if (mPlayer != null) { mPlayer.detachCallback(); } mPlayer = new PlayerWrapper(controller, ContextCompat.getMainExecutor(getContext()), new PlayerCallback()); if (ViewCompat.isAttachedToWindow(this)) { mPlayer.attachCallback(); } if (this.isAggregatedVisible()) { mTargetView.assignSurfaceToPlayerWrapper(mPlayer); } else { resetPlayerSurfaceWithNullAsync(); } if (mMediaControlView != null) { mMediaControlView.setMediaControllerInternal(controller); } } /** * Sets {@link SessionPlayer} to display media content. * Setting a SessionPlayer will unset any MediaController or SessionPlayer that was previously * set. *

* If VideoView has a {@link MediaControlView} instance, this player will also be set to it. *

* Calling this method will automatically set VideoView's surface to {@link SessionPlayer} * by calling {@link SessionPlayer#setSurface(Surface)}. * * @param player the player * @see #setMediaController */ public void setPlayer(@NonNull SessionPlayer player) { if (player == null) { throw new NullPointerException("player must not be null"); } if (mPlayer != null) { mPlayer.detachCallback(); } mPlayer = new PlayerWrapper(player, ContextCompat.getMainExecutor(getContext()), new PlayerCallback()); if (ViewCompat.isAttachedToWindow(this)) { mPlayer.attachCallback(); } if (this.isAggregatedVisible()) { mTargetView.assignSurfaceToPlayerWrapper(mPlayer); } else { resetPlayerSurfaceWithNullAsync(); } if (mMediaControlView != null) { mMediaControlView.setPlayerInternal(player); } } /** * Sets {@link MediaControlView} instance. It will replace the previously assigned * {@link MediaControlView} instance if any. *

* If a {@link MediaController} or a {@link SessionPlayer} instance has been set to * {@link VideoView}, the same instance will be set to {@link MediaControlView}. * * @param mediaControlView a {@link MediaControlView} instance. * @param intervalMs time interval in milliseconds until {@link MediaControlView} transitions * into a different mode. -1 can be set to disable all UI transitions. See * {@link MediaControlView} Javadoc Section "UI transitions" for details. */ public void setMediaControlView(@NonNull MediaControlView mediaControlView, long intervalMs) { if (mMediaControlView != null) { removeView(mMediaControlView); mMediaControlView.setAttachedToVideoView(false); } addView(mediaControlView, mSelectiveLayoutParams); mediaControlView.setAttachedToVideoView(true); mMediaControlView = mediaControlView; mMediaControlView.setDelayedAnimationInterval(intervalMs); if (mPlayer != null) { if (mPlayer.mController != null) { mMediaControlView.setMediaControllerInternal(mPlayer.mController); } else if (mPlayer.mPlayer != null) { mMediaControlView.setPlayerInternal(mPlayer.mPlayer); } } } /** * Returns {@link MediaControlView} instance which is currently attached to VideoView by default * or by {@link #setMediaControlView} method. */ @Nullable public MediaControlView getMediaControlView() { return mMediaControlView; } /** * Selects which view will be used to render video between SurfaceView and TextureView. *

* Note: There are two known issues on API level 28+ devices. *

* @param viewType the view type to render video * */ public void setViewType(@ViewType int viewType) { if (viewType == mTargetView.getViewType()) { Log.d(TAG, "setViewType with the same type (" + viewType + ") is ignored."); return; } VideoViewInterface targetView; if (viewType == VideoView.VIEW_TYPE_TEXTUREVIEW) { Log.d(TAG, "switching to TextureView"); targetView = mTextureView; } else if (viewType == VideoView.VIEW_TYPE_SURFACEVIEW) { Log.d(TAG, "switching to SurfaceView"); targetView = mSurfaceView; } else { throw new IllegalArgumentException("Unknown view type: " + viewType); } mTargetView = targetView; if (this.isAggregatedVisible()) { targetView.assignSurfaceToPlayerWrapper(mPlayer); } ((View) targetView).setVisibility(View.VISIBLE); requestLayout(); } /** * Returns view type. * * @return view type. See {@see setViewType}. */ @ViewType public int getViewType() { return mCurrentView.getViewType(); } /** * Sets a listener to be called when a view type change is done. * * @see #setViewType(int) * * @param listener The listener to be called. A value of null removes any existing * listener. */ public void setOnViewTypeChangedListener(@Nullable OnViewTypeChangedListener listener) { mViewTypeChangedListener = listener; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mPlayer != null) { mPlayer.attachCallback(); } } @Override void onVisibilityAggregatedCompat(boolean isVisible) { super.onVisibilityAggregatedCompat(isVisible); if (mPlayer == null) { return; } if (isVisible) { mTargetView.assignSurfaceToPlayerWrapper(mPlayer); } else { if (mPlayer == null || mPlayer.hasDisconnectedController()) { Log.w(TAG, "Surface is being destroyed, but player will not be informed " + "as the associated media controller is disconnected."); return; } resetPlayerSurfaceWithNull(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mPlayer != null) { mPlayer.detachCallback(); } } @Override public CharSequence getAccessibilityClassName() { // Class name may be obfuscated by Proguard. Hardcode the string for accessibility usage. return "androidx.media2.widget.VideoView"; } /////////////////////////////////////////////////// // Protected or private methods /////////////////////////////////////////////////// boolean isMediaPrepared() { return mPlayer != null && mPlayer.getPlayerState() != SessionPlayer.PLAYER_STATE_ERROR && mPlayer.getPlayerState() != SessionPlayer.PLAYER_STATE_IDLE; } boolean hasActualVideo() { if (mVideoTrackCount > 0) { return true; } VideoSize videoSize = mPlayer.getVideoSize(); if (videoSize.getHeight() > 0 && videoSize.getWidth() > 0) { Log.w(TAG, "video track count is zero, but it renders video. size: " + videoSize.getWidth() + "/" + videoSize.getHeight()); return true; } return false; } boolean isCurrentItemMusic() { return !hasActualVideo() && mAudioTrackCount > 0; } void updateTracks(PlayerWrapper player, List trackInfos) { mSubtitleTracks = new LinkedHashMap<>(); mVideoTrackCount = 0; mAudioTrackCount = 0; for (int i = 0; i < trackInfos.size(); i++) { TrackInfo trackInfo = trackInfos.get(i); int trackType = trackInfos.get(i).getTrackType(); if (trackType == TrackInfo.MEDIA_TRACK_TYPE_VIDEO) { mVideoTrackCount++; } else if (trackType == TrackInfo.MEDIA_TRACK_TYPE_AUDIO) { mAudioTrackCount++; } else if (trackType == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) { SubtitleTrack track = mSubtitleController.addTrack(trackInfo.getFormat()); if (track != null) { mSubtitleTracks.put(trackInfo, track); } } } mSelectedSubtitleTrackInfo = player.getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE); } void updateMusicView(MediaItem item) { boolean shouldShowMusicView = item != null && isCurrentItemMusic(); if (shouldShowMusicView) { mMusicView.setVisibility(View.VISIBLE); MediaMetadata metadata = item.getMetadata(); Resources resources = getResources(); Drawable albumDrawable = getAlbumArt(metadata, ContextCompat.getDrawable( getContext(), R.drawable.media2_widget_ic_default_album_image)); String title = getString(metadata, MediaMetadata.METADATA_KEY_TITLE, resources.getString(R.string.mcv2_music_title_unknown_text)); String artist = getString(metadata, MediaMetadata.METADATA_KEY_ARTIST, resources.getString(R.string.mcv2_music_artist_unknown_text)); mMusicView.setAlbumDrawable(albumDrawable); mMusicView.setTitleText(title); mMusicView.setArtistText(artist); } else { mMusicView.setVisibility(View.GONE); mMusicView.setAlbumDrawable(null); mMusicView.setTitleText(null); mMusicView.setArtistText(null); } } @SuppressWarnings("WeakerAccess") /* synthetic access */ void resetPlayerSurfaceWithNull() { try { int resultCode = mPlayer.setSurface(null).get(100, TimeUnit.MILLISECONDS) .getResultCode(); if (resultCode != BaseResult.RESULT_SUCCESS) { Log.e(TAG, "calling setSurface(null) was not " + "successful. ResultCode: " + resultCode); } } catch (ExecutionException | InterruptedException | TimeoutException e) { Log.e(TAG, "calling setSurface(null) was not successful.", e); } } @SuppressWarnings("WeakerAccess") /* synthetic access */ void resetPlayerSurfaceWithNullAsync() { ListenableFuture future = mPlayer.setSurface(null); future.addListener( new Runnable() { @Override public void run() { try { int resultCode = future.get().getResultCode(); if (resultCode != BaseResult.RESULT_SUCCESS) { Log.e(TAG, "calling setSurface(null) was not " + "successful. ResultCode: " + resultCode); } } catch (ExecutionException | InterruptedException e) { Log.e(TAG, "calling setSurface(null) was not successful.", e); } } }, ContextCompat.getMainExecutor(getContext())); } private Drawable getAlbumArt(@NonNull MediaMetadata metadata, Drawable defaultDrawable) { Drawable drawable = defaultDrawable; Bitmap bitmap = null; if (metadata != null && metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART)) { bitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); } if (bitmap != null) { Palette.Builder builder = Palette.from(bitmap); builder.generate(new Palette.PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { int dominantColor = palette.getDominantColor(0); mMusicView.setBackgroundColor(dominantColor); } }); drawable = new BitmapDrawable(getResources(), bitmap); } else { mMusicView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.media2_widget_music_view_default_background)); } return drawable; } private String getString(@NonNull MediaMetadata metadata, String stringKey, String defaultValue) { String value = (metadata == null) ? defaultValue : metadata.getString(stringKey); return value == null ? defaultValue : value; } class PlayerCallback extends PlayerWrapper.PlayerCallback { @Override void onConnected(@NonNull PlayerWrapper player) { if (DEBUG) { Log.d(TAG, "onConnected()"); } if (shouldIgnoreCallback(player)) return; if (VideoView.this.isAggregatedVisible()) { mTargetView.assignSurfaceToPlayerWrapper(mPlayer); } } @Override void onVideoSizeChanged(@NonNull PlayerWrapper player, @NonNull VideoSize videoSize) { if (DEBUG) { Log.d(TAG, "onVideoSizeChanged(): size: " + videoSize); } if (shouldIgnoreCallback(player)) return; if (mVideoTrackCount == 0 && videoSize.getHeight() > 0 && videoSize.getWidth() > 0) { if (isMediaPrepared()) { List trackInfos = player.getTracks(); if (trackInfos != null) { updateTracks(player, trackInfos); } } } mTextureView.forceLayout(); mSurfaceView.forceLayout(); requestLayout(); } @Override void onSubtitleData(@NonNull PlayerWrapper player, @NonNull MediaItem item, @NonNull TrackInfo track, @NonNull SubtitleData data) { if (DEBUG) { Log.d(TAG, "onSubtitleData():" + " TrackInfo: " + track + ", getCurrentPosition: " + player.getCurrentPosition() + ", getStartTimeUs(): " + data.getStartTimeUs() + ", diff: " + (data.getStartTimeUs() / 1000 - player.getCurrentPosition()) + "ms, getDurationUs(): " + data.getDurationUs()); } if (shouldIgnoreCallback(player)) return; if (!track.equals(mSelectedSubtitleTrackInfo)) { return; } SubtitleTrack subtitleTrack = mSubtitleTracks.get(track); if (subtitleTrack != null) { subtitleTrack.onData(data); } } @Override void onPlayerStateChanged(@NonNull PlayerWrapper player, int state) { if (DEBUG) { Log.d(TAG, "onPlayerStateChanged(): state: " + state); } if (shouldIgnoreCallback(player)) return; if (state == SessionPlayer.PLAYER_STATE_ERROR) { // TODO: Show error state (b/123498635) } } @Override void onCurrentMediaItemChanged(@NonNull PlayerWrapper player, @Nullable MediaItem item) { if (DEBUG) { Log.d(TAG, "onCurrentMediaItemChanged(): MediaItem: " + item); } if (shouldIgnoreCallback(player)) return; updateMusicView(item); } @Override void onTracksChanged(@NonNull PlayerWrapper player, @NonNull List tracks) { if (DEBUG) { Log.d(TAG, "onTrackInfoChanged(): tracks: " + tracks); } if (shouldIgnoreCallback(player)) return; updateTracks(player, tracks); updateMusicView(player.getCurrentMediaItem()); } @Override void onTrackSelected(@NonNull PlayerWrapper player, @NonNull TrackInfo trackInfo) { if (DEBUG) { Log.d(TAG, "onTrackSelected(): selected track: " + trackInfo); } if (shouldIgnoreCallback(player)) return; SubtitleTrack subtitleTrack = mSubtitleTracks.get(trackInfo); if (subtitleTrack != null) { mSubtitleController.selectTrack(subtitleTrack); } } @Override void onTrackDeselected(@NonNull PlayerWrapper player, @NonNull TrackInfo trackInfo) { if (DEBUG) { Log.d(TAG, "onTrackDeselected(): deselected track: " + trackInfo); } if (shouldIgnoreCallback(player)) return; SubtitleTrack subtitleTrack = mSubtitleTracks.get(trackInfo); if (subtitleTrack != null) { mSubtitleController.selectTrack(null); } } private boolean shouldIgnoreCallback(@NonNull PlayerWrapper player) { if (player != mPlayer) { if (DEBUG) { try { final String methodName = new Throwable().getStackTrace()[1].getMethodName(); Log.w(TAG, methodName + " should be ignored. player is already gone."); } catch (IndexOutOfBoundsException e) { Log.w(TAG, "A PlayerCallback should be ignored. player is already gone."); } } return true; } return false; } } /** * Interface definition of a callback to be invoked when the view type has been changed. */ public interface OnViewTypeChangedListener { /** * Called when the view type has been changed. * @see #setViewType(int) * @param view the View whose view type is changed * @param viewType *
    *
  • {@link #VIEW_TYPE_SURFACEVIEW} *
  • {@link #VIEW_TYPE_TEXTUREVIEW} *
*/ void onViewTypeChanged(@NonNull View view, @ViewType int viewType); } }