/* * 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 com.android.widget; import android.content.Context; import android.content.pm.ActivityInfo; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.AudioAttributes; import android.media.AudioFocusRequest; import android.media.AudioManager; import android.media.DataSourceDesc; import android.media.MediaMetadata; import android.media.MediaPlayer2; import android.media.MediaPlayer2.MediaPlayer2EventCallback; import android.media.MediaPlayer2.OnSubtitleDataListener; import android.media.MediaPlayer2Impl; import android.media.SubtitleData; import android.media.MediaItem2; import android.media.MediaMetadata2; import android.media.MediaMetadataRetriever; import android.media.Metadata; import android.media.PlaybackParams; import android.media.TimedText; import android.media.session.MediaController; import android.media.session.MediaController.PlaybackInfo; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.media.SessionToken2; import android.media.update.VideoView2Provider; import android.media.update.ViewGroupProvider; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.ResultReceiver; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.Pair; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.widget.ImageView; import android.widget.MediaControlView2; import android.widget.TextView; import android.widget.VideoView2; import com.android.internal.graphics.palette.Palette; import com.android.media.RoutePlayer; import com.android.media.subtitle.ClosedCaptionRenderer; import com.android.media.subtitle.SubtitleController; import com.android.media.subtitle.SubtitleTrack; import com.android.media.update.ApiHelper; import com.android.media.update.R; import com.android.support.mediarouter.media.MediaItemStatus; import com.android.support.mediarouter.media.MediaControlIntent; import com.android.support.mediarouter.media.MediaRouter; import com.android.support.mediarouter.media.MediaRouteSelector; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Executor; public class VideoView2Impl extends BaseLayout implements VideoView2Provider, VideoViewInterface.SurfaceListener { private static final String TAG = "VideoView2"; private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG); private static final long DEFAULT_SHOW_CONTROLLER_INTERVAL_MS = 2000; private final VideoView2 mInstance; private static final int STATE_ERROR = -1; private static final int STATE_IDLE = 0; private static final int STATE_PREPARING = 1; private static final int STATE_PREPARED = 2; private static final int STATE_PLAYING = 3; private static final int STATE_PAUSED = 4; private static final int STATE_PLAYBACK_COMPLETED = 5; private static final int INVALID_TRACK_INDEX = -1; private static final float INVALID_SPEED = 0f; private static final int SIZE_TYPE_EMBEDDED = 0; private static final int SIZE_TYPE_FULL = 1; // TODO: add support for Minimal size type. private static final int SIZE_TYPE_MINIMAL = 2; private AccessibilityManager mAccessibilityManager; private AudioManager mAudioManager; private AudioAttributes mAudioAttributes; private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain private Pair mCustomActionListenerRecord; private VideoView2.OnViewTypeChangedListener mViewTypeChangedListener; private VideoView2.OnFullScreenRequestListener mFullScreenRequestListener; private VideoViewInterface mCurrentView; private VideoTextureView mTextureView; private VideoSurfaceView mSurfaceView; private MediaPlayer2 mMediaPlayer; private DataSourceDesc mDsd; private MediaControlView2 mMediaControlView; private MediaSession mMediaSession; private MediaController mMediaController; private Metadata mMetadata; private MediaMetadata2 mMediaMetadata; private MediaMetadataRetriever mRetriever; private boolean mNeedUpdateMediaType; private Bundle mMediaTypeData; private String mTitle; // TODO: move music view inside SurfaceView/TextureView or implement VideoViewInterface. private WindowManager mManager; private Resources mResources; private View mMusicView; private Drawable mMusicAlbumDrawable; private String mMusicTitleText; private String mMusicArtistText; private boolean mIsMusicMediaType; private int mPrevWidth; private int mPrevHeight; private int mDominantColor; private int mSizeType; private PlaybackState.Builder mStateBuilder; private List mCustomActionList; private int mTargetState = STATE_IDLE; private int mCurrentState = STATE_IDLE; private int mCurrentBufferPercentage; private long mSeekWhenPrepared; // recording the seek position while preparing private int mVideoWidth; private int mVideoHeight; private ArrayList mVideoTrackIndices; private ArrayList mAudioTrackIndices; private ArrayList> mSubtitleTrackIndices; private SubtitleController mSubtitleController; // selected video/audio/subtitle track index as MediaPlayer2 returns private int mSelectedVideoTrackIndex; private int mSelectedAudioTrackIndex; private int mSelectedSubtitleTrackIndex; private SubtitleView mSubtitleView; private boolean mSubtitleEnabled; private float mSpeed; // TODO: Remove mFallbackSpeed when integration with MediaPlayer2's new setPlaybackParams(). // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit private float mFallbackSpeed; // keep the original speed before 'pause' is called. private float mVolumeLevelFloat; private int mVolumeLevel; private long mShowControllerIntervalMs; private MediaRouter mMediaRouter; private MediaRouteSelector mRouteSelector; private MediaRouter.RouteInfo mRoute; private RoutePlayer mRoutePlayer; private final MediaRouter.Callback mRouterCallback = new MediaRouter.Callback() { @Override public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) { if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { // Stop local playback (if necessary) resetPlayer(); mRoute = route; mRoutePlayer = new RoutePlayer(mInstance.getContext(), route); mRoutePlayer.setPlayerEventCallback(new RoutePlayer.PlayerEventCallback() { @Override public void onPlayerStateChanged(MediaItemStatus itemStatus) { PlaybackState.Builder psBuilder = new PlaybackState.Builder(); psBuilder.setActions(RoutePlayer.PLAYBACK_ACTIONS); long position = itemStatus.getContentPosition(); switch (itemStatus.getPlaybackState()) { case MediaItemStatus.PLAYBACK_STATE_PENDING: psBuilder.setState(PlaybackState.STATE_NONE, position, 0); mCurrentState = STATE_IDLE; break; case MediaItemStatus.PLAYBACK_STATE_PLAYING: psBuilder.setState(PlaybackState.STATE_PLAYING, position, 1); mCurrentState = STATE_PLAYING; break; case MediaItemStatus.PLAYBACK_STATE_PAUSED: psBuilder.setState(PlaybackState.STATE_PAUSED, position, 0); mCurrentState = STATE_PAUSED; break; case MediaItemStatus.PLAYBACK_STATE_BUFFERING: psBuilder.setState(PlaybackState.STATE_BUFFERING, position, 0); mCurrentState = STATE_PAUSED; break; case MediaItemStatus.PLAYBACK_STATE_FINISHED: psBuilder.setState(PlaybackState.STATE_STOPPED, position, 0); mCurrentState = STATE_PLAYBACK_COMPLETED; break; } PlaybackState pbState = psBuilder.build(); mMediaSession.setPlaybackState(pbState); MediaMetadata.Builder mmBuilder = new MediaMetadata.Builder(); mmBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, itemStatus.getContentDuration()); mMediaSession.setMetadata(mmBuilder.build()); } }); // Start remote playback (if necessary) mRoutePlayer.openVideo(mDsd); } } @Override public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route, int reason) { if (mRoute != null && mRoutePlayer != null) { mRoutePlayer.release(); mRoutePlayer = null; } if (mRoute == route) { mRoute = null; } if (reason != MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) { // TODO: Resume local playback (if necessary) openVideo(mDsd); } } }; public VideoView2Impl(VideoView2 instance, ViewGroupProvider superProvider, ViewGroupProvider privateProvider) { super(instance, superProvider, privateProvider); mInstance = instance; } @Override public void initialize(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { mVideoWidth = 0; mVideoHeight = 0; mSpeed = 1.0f; mFallbackSpeed = mSpeed; mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX; // TODO: add attributes to get this value. mShowControllerIntervalMs = DEFAULT_SHOW_CONTROLLER_INTERVAL_MS; mAccessibilityManager = AccessibilityManager.getInstance(mInstance.getContext()); mAudioManager = (AudioManager) mInstance.getContext() .getSystemService(Context.AUDIO_SERVICE); mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build(); mInstance.setFocusable(true); mInstance.setFocusableInTouchMode(true); mInstance.requestFocus(); // TODO: try to keep a single child at a time rather than always having both. mTextureView = new VideoTextureView(mInstance.getContext()); mSurfaceView = new VideoSurfaceView(mInstance.getContext()); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); mTextureView.setLayoutParams(params); mSurfaceView.setLayoutParams(params); mTextureView.setSurfaceListener(this); mSurfaceView.setSurfaceListener(this); mInstance.addView(mTextureView); mInstance.addView(mSurfaceView); mSubtitleView = new SubtitleView(mInstance.getContext()); mSubtitleView.setLayoutParams(params); mSubtitleView.setBackgroundColor(0); mInstance.addView(mSubtitleView); boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue( "http://schemas.android.com/apk/res/android", "enableControlView", true); if (enableControlView) { mMediaControlView = new MediaControlView2(mInstance.getContext()); } mSubtitleEnabled = (attrs == null) || attrs.getAttributeBooleanValue( "http://schemas.android.com/apk/res/android", "enableSubtitle", false); // TODO: Choose TextureView when SurfaceView cannot be created. // Choose surface view by default int viewType = (attrs == null) ? VideoView2.VIEW_TYPE_SURFACEVIEW : attrs.getAttributeIntValue( "http://schemas.android.com/apk/res/android", "viewType", VideoView2.VIEW_TYPE_SURFACEVIEW); if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) { Log.d(TAG, "viewType attribute is surfaceView."); mTextureView.setVisibility(View.GONE); mSurfaceView.setVisibility(View.VISIBLE); mCurrentView = mSurfaceView; } else if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) { Log.d(TAG, "viewType attribute is textureView."); mTextureView.setVisibility(View.VISIBLE); mSurfaceView.setVisibility(View.GONE); mCurrentView = mTextureView; } MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder(); builder.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO); builder.addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO); mRouteSelector = builder.build(); } @Override public void setMediaControlView2_impl(MediaControlView2 mediaControlView, long intervalMs) { mMediaControlView = mediaControlView; mShowControllerIntervalMs = intervalMs; // TODO: Call MediaControlView2.setRouteSelector only when cast availalbe. ((MediaControlView2Impl) mMediaControlView.getProvider()).setRouteSelector(mRouteSelector); if (mInstance.isAttachedToWindow()) { attachMediaControlView(); } } @Override public MediaController getMediaController_impl() { if (mMediaSession == null) { throw new IllegalStateException("MediaSession instance is not available."); } return mMediaController; } @Override public SessionToken2 getMediaSessionToken_impl() { // TODO: implement this return null; } @Override public MediaControlView2 getMediaControlView2_impl() { return mMediaControlView; } @Override public MediaMetadata2 getMediaMetadata_impl() { return mMediaMetadata; } @Override public void setMediaMetadata_impl(MediaMetadata2 metadata) { // TODO: integrate this with MediaSession2#MediaItem2 mMediaMetadata = metadata; // TODO: add support for handling website link mMediaTypeData = new Bundle(); boolean isAd = metadata == null ? false : metadata.getLong(MediaMetadata2.METADATA_KEY_ADVERTISEMENT) != 0; mMediaTypeData.putBoolean( MediaControlView2Impl.KEY_STATE_IS_ADVERTISEMENT, isAd); if (mMediaSession != null) { mMediaSession.sendSessionEvent( MediaControlView2Impl.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData); } else { // Update later inside OnPreparedListener after MediaSession is initialized. mNeedUpdateMediaType = true; } } @Override public void setSubtitleEnabled_impl(boolean enable) { if (enable != mSubtitleEnabled) { selectOrDeselectSubtitle(enable); } mSubtitleEnabled = enable; } @Override public boolean isSubtitleEnabled_impl() { return mSubtitleEnabled; } // TODO: remove setSpeed_impl once MediaController2 is ready. @Override public void setSpeed_impl(float speed) { if (speed <= 0.0f) { Log.e(TAG, "Unsupported speed (" + speed + ") is ignored."); return; } mSpeed = speed; if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { applySpeed(); } updatePlaybackState(); } @Override public void setAudioFocusRequest_impl(int focusGain) { if (focusGain != AudioManager.AUDIOFOCUS_NONE && focusGain != AudioManager.AUDIOFOCUS_GAIN && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) { throw new IllegalArgumentException("Illegal audio focus type " + focusGain); } mAudioFocusType = focusGain; } @Override public void setAudioAttributes_impl(AudioAttributes attributes) { if (attributes == null) { throw new IllegalArgumentException("Illegal null AudioAttributes"); } mAudioAttributes = attributes; } @Override public void setVideoPath_impl(String path) { mInstance.setVideoUri(Uri.parse(path)); } @Override public void setVideoUri_impl(Uri uri) { mInstance.setVideoUri(uri, null); } @Override public void setVideoUri_impl(Uri uri, Map headers) { DataSourceDesc.Builder builder = new DataSourceDesc.Builder(); builder.setDataSource(mInstance.getContext(), uri, headers, null); mInstance.setDataSource(builder.build()); } @Override public void setMediaItem_impl(MediaItem2 mediaItem) { // TODO: implement this } @Override public void setDataSource_impl(DataSourceDesc dsd) { mDsd = dsd; mSeekWhenPrepared = 0; openVideo(dsd); } @Override public void setViewType_impl(int viewType) { if (viewType == mCurrentView.getViewType()) { return; } VideoViewInterface targetView; if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) { Log.d(TAG, "switching to TextureView"); targetView = mTextureView; } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) { Log.d(TAG, "switching to SurfaceView"); targetView = mSurfaceView; } else { throw new IllegalArgumentException("Unknown view type: " + viewType); } ((View) targetView).setVisibility(View.VISIBLE); targetView.takeOver(mCurrentView); mInstance.requestLayout(); } @Override public int getViewType_impl() { return mCurrentView.getViewType(); } @Override public void setCustomActions_impl( List actionList, Executor executor, VideoView2.OnCustomActionListener listener) { mCustomActionList = actionList; mCustomActionListenerRecord = new Pair<>(executor, listener); // Create a new playback builder in order to clear existing the custom actions. mStateBuilder = null; updatePlaybackState(); } @Override public void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l) { mViewTypeChangedListener = l; } @Override public void setFullScreenRequestListener_impl(VideoView2.OnFullScreenRequestListener l) { mFullScreenRequestListener = l; } @Override public void onAttachedToWindow_impl() { super.onAttachedToWindow_impl(); // Create MediaSession mMediaSession = new MediaSession(mInstance.getContext(), "VideoView2MediaSession"); mMediaSession.setCallback(new MediaSessionCallback()); mMediaSession.setActive(true); mMediaController = mMediaSession.getController(); mMediaRouter = MediaRouter.getInstance(mInstance.getContext()); mMediaRouter.setMediaSession(mMediaSession); mMediaRouter.addCallback(mRouteSelector, mRouterCallback); attachMediaControlView(); // TODO: remove this after moving MediaSession creating code inside initializing VideoView2 if (mCurrentState == STATE_PREPARED) { extractTracks(); extractMetadata(); extractAudioMetadata(); if (mNeedUpdateMediaType) { mMediaSession.sendSessionEvent( MediaControlView2Impl.EVENT_UPDATE_MEDIA_TYPE_STATUS, mMediaTypeData); mNeedUpdateMediaType = false; } } } @Override public void onDetachedFromWindow_impl() { super.onDetachedFromWindow_impl(); mMediaSession.release(); mMediaSession = null; mMediaController = null; } @Override public CharSequence getAccessibilityClassName_impl() { return VideoView2.class.getName(); } @Override public boolean onTouchEvent_impl(MotionEvent ev) { if (DEBUG) { Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState); } if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) { if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) { toggleMediaControlViewVisibility(); } } return super.onTouchEvent_impl(ev); } @Override public boolean onTrackballEvent_impl(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_UP && mMediaControlView != null) { if (!mIsMusicMediaType || mSizeType != SIZE_TYPE_FULL) { toggleMediaControlViewVisibility(); } } return super.onTrackballEvent_impl(ev); } @Override public boolean dispatchTouchEvent_impl(MotionEvent ev) { // TODO: Test touch event handling logic thoroughly and simplify the logic. return super.dispatchTouchEvent_impl(ev); } @Override public void onMeasure_impl(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure_impl(widthMeasureSpec, heightMeasureSpec); if (mIsMusicMediaType) { if (mPrevWidth != mInstance.getMeasuredWidth() || mPrevHeight != mInstance.getMeasuredHeight()) { int currWidth = mInstance.getMeasuredWidth(); int currHeight = mInstance.getMeasuredHeight(); Point screenSize = new Point(); mManager.getDefaultDisplay().getSize(screenSize); int screenWidth = screenSize.x; int screenHeight = screenSize.y; if (currWidth == screenWidth && currHeight == screenHeight) { int orientation = retrieveOrientation(); if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { inflateMusicView(R.layout.full_landscape_music); } else { inflateMusicView(R.layout.full_portrait_music); } if (mSizeType != SIZE_TYPE_FULL) { mSizeType = SIZE_TYPE_FULL; // Remove existing mFadeOut callback mMediaControlView.removeCallbacks(mFadeOut); mMediaControlView.setVisibility(View.VISIBLE); } } else { if (mSizeType != SIZE_TYPE_EMBEDDED) { mSizeType = SIZE_TYPE_EMBEDDED; inflateMusicView(R.layout.embedded_music); // Add new mFadeOut callback mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs); } } mPrevWidth = currWidth; mPrevHeight = currHeight; } } } /////////////////////////////////////////////////// // Implements VideoViewInterface.SurfaceListener /////////////////////////////////////////////////// @Override public void onSurfaceCreated(View view, int width, int height) { if (DEBUG) { Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height + ", " + view.toString()); } if (needToStart()) { mMediaController.getTransportControls().play(); } } @Override public void onSurfaceDestroyed(View view) { if (DEBUG) { Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState + ", " + view.toString()); } } @Override public void onSurfaceChanged(View view, int width, int height) { // TODO: Do we need to call requestLayout here? if (DEBUG) { Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height + ", " + view.toString()); } } @Override public void onSurfaceTakeOverDone(VideoViewInterface view) { if (DEBUG) { Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view); } mCurrentView = view; if (mViewTypeChangedListener != null) { mViewTypeChangedListener.onViewTypeChanged(mInstance, view.getViewType()); } if (needToStart()) { mMediaController.getTransportControls().play(); } } /////////////////////////////////////////////////// // Protected or private methods /////////////////////////////////////////////////// private void attachMediaControlView() { // Get MediaController from MediaSession and set it inside MediaControlView mMediaControlView.setController(mMediaSession.getController()); LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); mInstance.addView(mMediaControlView, params); } private boolean isInPlaybackState() { return (mMediaPlayer != null || mRoutePlayer != null) && mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE && mCurrentState != STATE_PREPARING; } private boolean needToStart() { return (mMediaPlayer != null || mRoutePlayer != null) && mCurrentState != STATE_PLAYING && mTargetState == STATE_PLAYING; } // Creates a MediaPlayer2 instance and prepare playback. private void openVideo(DataSourceDesc dsd) { Uri uri = dsd.getUri(); Map headers = dsd.getUriHeaders(); resetPlayer(); if (isRemotePlayback()) { mRoutePlayer.openVideo(dsd); return; } if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) { // TODO this should have a focus listener AudioFocusRequest focusRequest; focusRequest = new AudioFocusRequest.Builder(mAudioFocusType) .setAudioAttributes(mAudioAttributes) .build(); mAudioManager.requestAudioFocus(focusRequest); } try { Log.d(TAG, "openVideo(): creating new MediaPlayer2 instance."); mMediaPlayer = new MediaPlayer2Impl(); mSurfaceView.setMediaPlayer(mMediaPlayer); mTextureView.setMediaPlayer(mMediaPlayer); mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer); final Context context = mInstance.getContext(); // TODO: Add timely firing logic for more accurate sync between CC and video frame mSubtitleController = new SubtitleController(context); mSubtitleController.registerRenderer(new ClosedCaptionRenderer(context)); mSubtitleController.setAnchor((SubtitleController.Anchor) mSubtitleView); Executor executor = new Executor() { @Override public void execute(Runnable runnable) { runnable.run(); } }; mMediaPlayer.setMediaPlayer2EventCallback(executor, mMediaPlayer2Callback); mCurrentBufferPercentage = -1; mMediaPlayer.setDataSource(dsd); mMediaPlayer.setAudioAttributes(mAudioAttributes); mMediaPlayer.setOnSubtitleDataListener(mSubtitleListener); // we don't set the target state here either, but preserve the // target state that was there before. mCurrentState = STATE_PREPARING; mMediaPlayer.prepare(); // Save file name as title since the file may not have a title Metadata. mTitle = uri.getPath(); String scheme = uri.getScheme(); if (scheme != null && scheme.equals("file")) { mTitle = uri.getLastPathSegment(); } mRetriever = new MediaMetadataRetriever(); mRetriever.setDataSource(mInstance.getContext(), uri); if (DEBUG) { Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState); } } catch (IllegalArgumentException ex) { Log.w(TAG, "Unable to open content: " + uri, ex); mCurrentState = STATE_ERROR; mTargetState = STATE_ERROR; mMediaPlayer2Callback.onError(mMediaPlayer, dsd, MediaPlayer2.MEDIA_ERROR_UNKNOWN, MediaPlayer2.MEDIA_ERROR_IO); } } /* * Reset the media player in any state */ private void resetPlayer() { if (mMediaPlayer != null) { final MediaPlayer2 player = mMediaPlayer; new AsyncTask() { @Override protected Void doInBackground(MediaPlayer2... players) { // TODO: Fix NPE while MediaPlayer2.close() //players[0].close(); return null; } }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, player); mMediaPlayer = null; mTextureView.setMediaPlayer(null); mSurfaceView.setMediaPlayer(null); //mPendingSubtitleTracks.clear(); mCurrentState = STATE_IDLE; mTargetState = STATE_IDLE; if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) { mAudioManager.abandonAudioFocus(null); } } mVideoWidth = 0; mVideoHeight = 0; } private void updatePlaybackState() { if (mStateBuilder == null) { // Get the capabilities of the player for this stream mMetadata = mMediaPlayer.getMetadata(MediaPlayer2.METADATA_ALL, MediaPlayer2.BYPASS_METADATA_FILTER); // Add Play action as default long playbackActions = PlaybackState.ACTION_PLAY; if (mMetadata != null) { if (!mMetadata.has(Metadata.PAUSE_AVAILABLE) || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) { playbackActions |= PlaybackState.ACTION_PAUSE; } if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE) || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) { playbackActions |= PlaybackState.ACTION_REWIND; } if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE) || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) { playbackActions |= PlaybackState.ACTION_FAST_FORWARD; } if (!mMetadata.has(Metadata.SEEK_AVAILABLE) || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) { playbackActions |= PlaybackState.ACTION_SEEK_TO; } } else { playbackActions |= (PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_REWIND | PlaybackState.ACTION_FAST_FORWARD | PlaybackState.ACTION_SEEK_TO); } mStateBuilder = new PlaybackState.Builder(); mStateBuilder.setActions(playbackActions); if (mCustomActionList != null) { for (PlaybackState.CustomAction action : mCustomActionList) { mStateBuilder.addCustomAction(action); } } } mStateBuilder.setState(getCorrespondingPlaybackState(), mMediaPlayer.getCurrentPosition(), mSpeed); if (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE && mCurrentState != STATE_PREPARING) { // TODO: this should be replaced with MediaPlayer2.getBufferedPosition() once it is // implemented. if (mCurrentBufferPercentage == -1) { mStateBuilder.setBufferedPosition(-1); } else { mStateBuilder.setBufferedPosition( (long) (mCurrentBufferPercentage / 100.0 * mMediaPlayer.getDuration())); } } // Set PlaybackState for MediaSession if (mMediaSession != null) { PlaybackState state = mStateBuilder.build(); mMediaSession.setPlaybackState(state); } } private int getCorrespondingPlaybackState() { switch (mCurrentState) { case STATE_ERROR: return PlaybackState.STATE_ERROR; case STATE_IDLE: return PlaybackState.STATE_NONE; case STATE_PREPARING: return PlaybackState.STATE_CONNECTING; case STATE_PREPARED: return PlaybackState.STATE_PAUSED; case STATE_PLAYING: return PlaybackState.STATE_PLAYING; case STATE_PAUSED: return PlaybackState.STATE_PAUSED; case STATE_PLAYBACK_COMPLETED: return PlaybackState.STATE_STOPPED; default: return -1; } } private final Runnable mFadeOut = new Runnable() { @Override public void run() { if (mCurrentState == STATE_PLAYING) { mMediaControlView.setVisibility(View.GONE); } } }; private void showController() { // TODO: Decide what to show when the state is not in playback state if (mMediaControlView == null || !isInPlaybackState() || (mIsMusicMediaType && mSizeType == SIZE_TYPE_FULL)) { return; } mMediaControlView.removeCallbacks(mFadeOut); mMediaControlView.setVisibility(View.VISIBLE); if (mShowControllerIntervalMs != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) { mMediaControlView.postDelayed(mFadeOut, mShowControllerIntervalMs); } } private void toggleMediaControlViewVisibility() { if (mMediaControlView.getVisibility() == View.VISIBLE) { mMediaControlView.removeCallbacks(mFadeOut); mMediaControlView.setVisibility(View.GONE); } else { showController(); } } private void applySpeed() { PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults(); if (mSpeed != params.getSpeed()) { try { params.setSpeed(mSpeed); mMediaPlayer.setPlaybackParams(params); mFallbackSpeed = mSpeed; } catch (IllegalArgumentException e) { Log.e(TAG, "PlaybackParams has unsupported value: " + e); // TODO: should revise this part after integrating with MP2. // If mSpeed had an illegal value for speed rate, system will determine best // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT). // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will // use mFallbackSpeed instead. float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed(); if (fallbackSpeed > 0.0f) { mFallbackSpeed = fallbackSpeed; } mSpeed = mFallbackSpeed; } } } private boolean isRemotePlayback() { if (mMediaController == null) { return false; } PlaybackInfo playbackInfo = mMediaController.getPlaybackInfo(); return playbackInfo != null && playbackInfo.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE; } private void selectOrDeselectSubtitle(boolean select) { if (!isInPlaybackState()) { return; } if (select) { if (mSubtitleTrackIndices.size() > 0) { // TODO: make this selection dynamic mSelectedSubtitleTrackIndex = mSubtitleTrackIndices.get(0).first; mSubtitleController.selectTrack(mSubtitleTrackIndices.get(0).second); mMediaPlayer.selectTrack(mSelectedSubtitleTrackIndex); mSubtitleView.setVisibility(View.VISIBLE); } } else { if (mSelectedSubtitleTrackIndex != INVALID_TRACK_INDEX) { mMediaPlayer.deselectTrack(mSelectedSubtitleTrackIndex); mSelectedSubtitleTrackIndex = INVALID_TRACK_INDEX; mSubtitleView.setVisibility(View.GONE); } } } private void extractTracks() { List trackInfos = mMediaPlayer.getTrackInfo(); mVideoTrackIndices = new ArrayList<>(); mAudioTrackIndices = new ArrayList<>(); mSubtitleTrackIndices = new ArrayList<>(); mSubtitleController.reset(); for (int i = 0; i < trackInfos.size(); ++i) { int trackType = trackInfos.get(i).getTrackType(); if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_VIDEO) { mVideoTrackIndices.add(i); } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_AUDIO) { mAudioTrackIndices.add(i); } else if (trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE || trackType == MediaPlayer2.TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) { SubtitleTrack track = mSubtitleController.addTrack(trackInfos.get(i).getFormat()); if (track != null) { mSubtitleTrackIndices.add(new Pair<>(i, track)); } } } // Select first tracks as default if (mVideoTrackIndices.size() > 0) { mSelectedVideoTrackIndex = 0; } if (mAudioTrackIndices.size() > 0) { mSelectedAudioTrackIndex = 0; } if (mVideoTrackIndices.size() == 0 && mAudioTrackIndices.size() > 0) { mIsMusicMediaType = true; } Bundle data = new Bundle(); data.putInt(MediaControlView2Impl.KEY_VIDEO_TRACK_COUNT, mVideoTrackIndices.size()); data.putInt(MediaControlView2Impl.KEY_AUDIO_TRACK_COUNT, mAudioTrackIndices.size()); data.putInt(MediaControlView2Impl.KEY_SUBTITLE_TRACK_COUNT, mSubtitleTrackIndices.size()); if (mSubtitleTrackIndices.size() > 0) { selectOrDeselectSubtitle(mSubtitleEnabled); } mMediaSession.sendSessionEvent(MediaControlView2Impl.EVENT_UPDATE_TRACK_STATUS, data); } private void extractMetadata() { // Get and set duration and title values as MediaMetadata for MediaControlView2 MediaMetadata.Builder builder = new MediaMetadata.Builder(); if (mMetadata != null && mMetadata.has(Metadata.TITLE)) { mTitle = mMetadata.getString(Metadata.TITLE); } builder.putString(MediaMetadata.METADATA_KEY_TITLE, mTitle); builder.putLong( MediaMetadata.METADATA_KEY_DURATION, mMediaPlayer.getDuration()); if (mMediaSession != null) { mMediaSession.setMetadata(builder.build()); } } private void extractAudioMetadata() { if (!mIsMusicMediaType) { return; } mResources = ApiHelper.getLibResources(mInstance.getContext()); mManager = (WindowManager) mInstance.getContext().getApplicationContext() .getSystemService(Context.WINDOW_SERVICE); byte[] album = mRetriever.getEmbeddedPicture(); if (album != null) { Bitmap bitmap = BitmapFactory.decodeByteArray(album, 0, album.length); mMusicAlbumDrawable = new BitmapDrawable(bitmap); // TODO: replace with visualizer Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { public void onGenerated(Palette palette) { // TODO: add dominant color for default album image. mDominantColor = palette.getDominantColor(0); if (mMusicView != null) { mMusicView.setBackgroundColor(mDominantColor); } } }); } else { mMusicAlbumDrawable = mResources.getDrawable(R.drawable.ic_default_album_image); } String title = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); if (title != null) { mMusicTitleText = title; } else { mMusicTitleText = mResources.getString(R.string.mcv2_music_title_unknown_text); } String artist = mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); if (artist != null) { mMusicArtistText = artist; } else { mMusicArtistText = mResources.getString(R.string.mcv2_music_artist_unknown_text); } // Send title and artist string to MediaControlView2 MediaMetadata.Builder builder = new MediaMetadata.Builder(); builder.putString(MediaMetadata.METADATA_KEY_TITLE, mMusicTitleText); builder.putString(MediaMetadata.METADATA_KEY_ARTIST, mMusicArtistText); mMediaSession.setMetadata(builder.build()); // Display Embedded mode as default mInstance.removeView(mSurfaceView); mInstance.removeView(mTextureView); inflateMusicView(R.layout.embedded_music); } private int retrieveOrientation() { DisplayMetrics dm = Resources.getSystem().getDisplayMetrics(); int width = dm.widthPixels; int height = dm.heightPixels; return (height > width) ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } private void inflateMusicView(int layoutId) { mInstance.removeView(mMusicView); View v = ApiHelper.inflateLibLayout(mInstance.getContext(), layoutId); v.setBackgroundColor(mDominantColor); ImageView albumView = v.findViewById(R.id.album); if (albumView != null) { albumView.setImageDrawable(mMusicAlbumDrawable); } TextView titleView = v.findViewById(R.id.title); if (titleView != null) { titleView.setText(mMusicTitleText); } TextView artistView = v.findViewById(R.id.artist); if (artistView != null) { artistView.setText(mMusicArtistText); } mMusicView = v; mInstance.addView(mMusicView, 0); } OnSubtitleDataListener mSubtitleListener = new OnSubtitleDataListener() { @Override public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) { if (DEBUG) { Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex() + ", getCurrentPosition: " + mp.getCurrentPosition() + ", getStartTimeUs(): " + data.getStartTimeUs() + ", diff: " + (data.getStartTimeUs()/1000 - mp.getCurrentPosition()) + "ms, getDurationUs(): " + data.getDurationUs() ); } final int index = data.getTrackIndex(); if (index != mSelectedSubtitleTrackIndex) { Log.d(TAG, "onSubtitleData(): getTrackIndex: " + data.getTrackIndex() + ", selected track index: " + mSelectedSubtitleTrackIndex); return; } for (Pair p : mSubtitleTrackIndices) { if (p.first == index) { SubtitleTrack track = p.second; track.onData(data); } } } }; MediaPlayer2EventCallback mMediaPlayer2Callback = new MediaPlayer2EventCallback() { @Override public void onVideoSizeChanged( MediaPlayer2 mp, DataSourceDesc dsd, int width, int height) { if (DEBUG) { Log.d(TAG, "onVideoSizeChanged(): size: " + width + "/" + height); } mVideoWidth = mp.getVideoWidth(); mVideoHeight = mp.getVideoHeight(); if (DEBUG) { Log.d(TAG, "onVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/" + mVideoHeight); } if (mVideoWidth != 0 && mVideoHeight != 0) { mInstance.requestLayout(); } } // TODO: Remove timed text related code later once relevant Renderer is defined. // This is just for debugging purpose. @Override public void onTimedText( MediaPlayer2 mp, DataSourceDesc dsd, TimedText text) { Log.d(TAG, "TimedText: " + text.getText()); } @Override public void onInfo( MediaPlayer2 mp, DataSourceDesc dsd, int what, int extra) { if (what == MediaPlayer2.MEDIA_INFO_METADATA_UPDATE) { extractTracks(); } else if (what == MediaPlayer2.MEDIA_INFO_PREPARED) { this.onPrepared(mp, dsd); } else if (what == MediaPlayer2.MEDIA_INFO_PLAYBACK_COMPLETE) { this.onCompletion(mp, dsd); } else if (what == MediaPlayer2.MEDIA_INFO_BUFFERING_UPDATE) { this.onBufferingUpdate(mp, dsd, extra); } } @Override public void onError( MediaPlayer2 mp, DataSourceDesc dsd, int frameworkErr, int implErr) { if (DEBUG) { Log.d(TAG, "Error: " + frameworkErr + "," + implErr); } mCurrentState = STATE_ERROR; mTargetState = STATE_ERROR; updatePlaybackState(); if (mMediaControlView != null) { mMediaControlView.setVisibility(View.GONE); } } @Override public void onCallCompleted(MediaPlayer2 mp, DataSourceDesc dsd, int what, int status) { if (what == MediaPlayer2.CALL_COMPLETED_SEEK_TO && status == 0) { updatePlaybackState(); } } private void onPrepared(MediaPlayer2 mp, DataSourceDesc dsd) { if (DEBUG) { Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState); } mCurrentState = STATE_PREPARED; // Create and set playback state for MediaControlView2 updatePlaybackState(); // TODO: change this to send TrackInfos to MediaControlView2 // TODO: create MediaSession when initializing VideoView2 if (mMediaSession != null) { extractTracks(); extractMetadata(); extractAudioMetadata(); } if (mMediaControlView != null) { mMediaControlView.setEnabled(true); } int videoWidth = mp.getVideoWidth(); int videoHeight = mp.getVideoHeight(); // mSeekWhenPrepared may be changed after seekTo() call long seekToPosition = mSeekWhenPrepared; if (seekToPosition != 0) { mMediaController.getTransportControls().seekTo(seekToPosition); } if (videoWidth != 0 && videoHeight != 0) { if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) { if (DEBUG) { Log.i(TAG, "OnPreparedListener() : "); Log.i(TAG, " video size: " + videoWidth + "/" + videoHeight); Log.i(TAG, " measuredSize: " + mInstance.getMeasuredWidth() + "/" + mInstance.getMeasuredHeight()); Log.i(TAG, " viewSize: " + mInstance.getWidth() + "/" + mInstance.getHeight()); } mVideoWidth = videoWidth; mVideoHeight = videoHeight; mInstance.requestLayout(); } if (needToStart()) { mMediaController.getTransportControls().play(); } } else { // We don't know the video size yet, but should start anyway. // The video size might be reported to us later. if (needToStart()) { mMediaController.getTransportControls().play(); } } } private void onCompletion(MediaPlayer2 mp, DataSourceDesc dsd) { mCurrentState = STATE_PLAYBACK_COMPLETED; mTargetState = STATE_PLAYBACK_COMPLETED; updatePlaybackState(); if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) { mAudioManager.abandonAudioFocus(null); } } private void onBufferingUpdate(MediaPlayer2 mp, DataSourceDesc dsd, int percent) { mCurrentBufferPercentage = percent; updatePlaybackState(); } }; private class MediaSessionCallback extends MediaSession.Callback { @Override public void onCommand(String command, Bundle args, ResultReceiver receiver) { if (isRemotePlayback()) { mRoutePlayer.onCommand(command, args, receiver); } else { switch (command) { case MediaControlView2Impl.COMMAND_SHOW_SUBTITLE: int subtitleIndex = args.getInt( MediaControlView2Impl.KEY_SELECTED_SUBTITLE_INDEX, INVALID_TRACK_INDEX); if (subtitleIndex != INVALID_TRACK_INDEX) { int subtitleTrackIndex = mSubtitleTrackIndices.get(subtitleIndex).first; if (subtitleTrackIndex != mSelectedSubtitleTrackIndex) { mSelectedSubtitleTrackIndex = subtitleTrackIndex; mInstance.setSubtitleEnabled(true); } } break; case MediaControlView2Impl.COMMAND_HIDE_SUBTITLE: mInstance.setSubtitleEnabled(false); break; case MediaControlView2Impl.COMMAND_SET_FULLSCREEN: if (mFullScreenRequestListener != null) { mFullScreenRequestListener.onFullScreenRequest( mInstance, args.getBoolean(MediaControlView2Impl.ARGUMENT_KEY_FULLSCREEN)); } break; case MediaControlView2Impl.COMMAND_SELECT_AUDIO_TRACK: int audioIndex = args.getInt(MediaControlView2Impl.KEY_SELECTED_AUDIO_INDEX, INVALID_TRACK_INDEX); if (audioIndex != INVALID_TRACK_INDEX) { int audioTrackIndex = mAudioTrackIndices.get(audioIndex); if (audioTrackIndex != mSelectedAudioTrackIndex) { mSelectedAudioTrackIndex = audioTrackIndex; mMediaPlayer.selectTrack(mSelectedAudioTrackIndex); } } break; case MediaControlView2Impl.COMMAND_SET_PLAYBACK_SPEED: float speed = args.getFloat( MediaControlView2Impl.KEY_PLAYBACK_SPEED, INVALID_SPEED); if (speed != INVALID_SPEED && speed != mSpeed) { mInstance.setSpeed(speed); mSpeed = speed; } break; case MediaControlView2Impl.COMMAND_MUTE: mVolumeLevel = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0); break; case MediaControlView2Impl.COMMAND_UNMUTE: mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeLevel, 0); break; } } showController(); } @Override public void onCustomAction(String action, Bundle extras) { mCustomActionListenerRecord.first.execute(() -> mCustomActionListenerRecord.second.onCustomAction(action, extras)); showController(); } @Override public void onPlay() { if (isInPlaybackState() && (mCurrentView.hasAvailableSurface() || mIsMusicMediaType)) { if (isRemotePlayback()) { mRoutePlayer.onPlay(); } else { applySpeed(); mMediaPlayer.play(); mCurrentState = STATE_PLAYING; updatePlaybackState(); } mCurrentState = STATE_PLAYING; } mTargetState = STATE_PLAYING; if (DEBUG) { Log.d(TAG, "onPlay(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState); } showController(); } @Override public void onPause() { if (isInPlaybackState()) { if (isRemotePlayback()) { mRoutePlayer.onPause(); mCurrentState = STATE_PAUSED; } else if (mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); mCurrentState = STATE_PAUSED; updatePlaybackState(); } } mTargetState = STATE_PAUSED; if (DEBUG) { Log.d(TAG, "onPause(). mCurrentState=" + mCurrentState + ", mTargetState=" + mTargetState); } showController(); } @Override public void onSeekTo(long pos) { if (isInPlaybackState()) { if (isRemotePlayback()) { mRoutePlayer.onSeekTo(pos); } else { mMediaPlayer.seekTo(pos, MediaPlayer2.SEEK_PREVIOUS_SYNC); mSeekWhenPrepared = 0; } } else { mSeekWhenPrepared = pos; } showController(); } @Override public void onStop() { if (isRemotePlayback()) { mRoutePlayer.onStop(); } else { resetPlayer(); } showController(); } } }