package io.github.xwz.base.activities; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.net.Uri; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.view.SurfaceHolder; import android.view.View; import android.widget.Toast; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; import com.google.android.exoplayer.drm.UnsupportedDrmException; import com.google.android.exoplayer.text.Cue; import com.google.android.exoplayer.util.Util; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import io.github.xwz.base.R; import io.github.xwz.base.Utils; import io.github.xwz.base.api.EpisodeBaseModel; import io.github.xwz.base.api.PlayHistory; import io.github.xwz.base.content.ContentManagerBase; import io.github.xwz.base.player.DurationLogger; import io.github.xwz.base.player.EventLogger; import io.github.xwz.base.player.HlsRendererBuilder; import io.github.xwz.base.player.VideoPlayer; import io.github.xwz.base.views.PlaybackControls; import io.github.xwz.base.views.VideoPlayerView; /** * An activity that plays media using {@link VideoPlayer}. */ public abstract class VideoPlayerActivity extends BaseActivity implements SurfaceHolder.Callback, VideoPlayer.Listener, VideoPlayer.CaptionListener, AudioCapabilitiesReceiver.Listener { private static final String TAG = "PlayerActivity"; private static final String MEDIA_SESSION_TAG = "io.github.xwz.base.MEDIA_SESSION_TAG"; private static final String RESUME_POSITION = "io.github.xwz.base.RESUME_POSITION"; private static final CookieManager defaultCookieManager; static { defaultCookieManager = new CookieManager(); defaultCookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); } private EventLogger eventLogger; private MediaSession mediaSession; private PlaybackControls mediaController; private DurationLogger timeLogger; private VideoPlayerView videoPlayerView; private VideoPlayer player; private boolean playerNeedsPrepare; private long playerPosition; private Uri contentUri; private AudioCapabilitiesReceiver audioCapabilitiesReceiver; private AudioCapabilities audioCapabilities; private boolean ready = false; private EpisodeBaseModel mCurrentEpisode; private List<String> mOtherEpisodeUrls; private long resumePosition; private final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.d(TAG, "Action: " + action + ", tag: " + intent.getStringExtra(ContentManagerBase.CONTENT_TAG)); if (ContentManagerBase.CONTENT_AUTH_DONE.equals(action)) { prepareStream(intent); } if (ContentManagerBase.CONTENT_AUTH_ERROR.equals(action)) { authFailed(intent); } if (ContentManagerBase.CONTENT_AUTH_FETCHING.equals(action)) { if (videoPlayerView != null) { videoPlayerView.showStatusText("Preparing..."); } } } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EpisodeBaseModel episode = (EpisodeBaseModel) getIntent().getSerializableExtra(ContentManagerBase.CONTENT_ID); mOtherEpisodeUrls = Arrays.asList(getIntent().getStringArrayExtra(ContentManagerBase.OTHER_EPISODES)); resumePosition = getIntent().getLongExtra(RESUME_POSITION, 0); if (resumePosition <= 0 && episode.getResumePosition() > 0) { resumePosition = episode.getResumePosition(); Log.d(TAG, "Resume from recently played"); } setContentView(R.layout.video_player_activity); View root = findViewById(R.id.root); mediaController = new PlaybackControls(this); mediaController.setAnchorView(root); videoPlayerView = new VideoPlayerView(this, mediaController, root); audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); CookieHandler currentHandler = CookieHandler.getDefault(); if (currentHandler != defaultCookieManager) { CookieHandler.setDefault(defaultCookieManager); } playEpisode(episode); } private void playEpisode(EpisodeBaseModel episode) { releasePlayer(); playerPosition = resumePosition; ready = false; mCurrentEpisode = episode; videoPlayerView.setEpisode(episode); getContentManger().fetchAuthToken(episode); } protected abstract ContentManagerBase getContentManger(); private void prepareStream(Intent intent) { contentUri = getContentManger().getEpisodeStreamUrl(mCurrentEpisode); if (contentUri != null) { ready = true; Log.d(TAG, "Ready to play:" + mCurrentEpisode); preparePlayer(); } } private void authFailed(Intent intent) { String href = intent.getStringExtra(ContentManagerBase.CONTENT_ID); String error = intent.getStringExtra(ContentManagerBase.CONTENT_TAG); Log.e(TAG, error + ":" + href); Utils.showToast(this, error); } @Override public void onResume() { super.onResume(); registerReceiver(); videoPlayerView.configureSubtitleView(); videoPlayerView.showShutter(false); // The player will be prepared on receiving audio capabilities. audioCapabilitiesReceiver.register(); } @Override public void onPause() { super.onPause(); if (!requestVisibleBehind(true)) { releasePlayer(); audioCapabilitiesReceiver.unregister(); videoPlayerView.showShutter(true); } LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); } @Override public void onVisibleBehindCanceled() { super.onVisibleBehindCanceled(); releasePlayer(); audioCapabilitiesReceiver.unregister(); videoPlayerView.showShutter(true); LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); } private void registerReceiver() { Log.i(TAG, "Register receiver"); IntentFilter filter = new IntentFilter(); filter.addAction(ContentManagerBase.CONTENT_AUTH_FETCHING); filter.addAction(ContentManagerBase.CONTENT_AUTH_START); filter.addAction(ContentManagerBase.CONTENT_AUTH_DONE); filter.addAction(ContentManagerBase.CONTENT_AUTH_ERROR); LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter); } @Override public void onDestroy() { super.onDestroy(); releasePlayer(); } // AudioCapabilitiesReceiver.Listener methods @Override public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { boolean audioCapabilitiesChanged = !audioCapabilities.equals(this.audioCapabilities); if (player == null || audioCapabilitiesChanged) { this.audioCapabilities = audioCapabilities; releasePlayer(); preparePlayer(); } else if (player != null) { player.setBackgrounded(false); } } // Internal methods private VideoPlayer.RendererBuilder getRendererBuilder() { String userAgent = Util.getUserAgent(this, getString(R.string.app_name)); return new HlsRendererBuilder(this, userAgent, contentUri.toString(), audioCapabilities); } private void preparePlayer() { if (ready) { if (player == null) { Log.d(TAG, "Prepare player, position:" + playerPosition); player = new VideoPlayer(getRendererBuilder()); player.addListener(this); player.setCaptionListener(this); player.seekTo(playerPosition); playerNeedsPrepare = true; mediaController.setMediaPlayer(player.getPlayerControl()); mediaController.setEnabled(true); mediaController.setPrevNextListeners(getNextEpisodeListener(), getPrevEpisodeListener()); eventLogger = new EventLogger(); eventLogger.startSession(); player.addListener(eventLogger); player.setInfoListener(eventLogger); player.setInternalErrorListener(eventLogger); videoPlayerView.startDebugView(player); videoPlayerView.resetView(); videoPlayerView.setMediaPlayer(player.getPlayerControl()); timeLogger = new DurationLogger(); timeLogger.addListener(30L, new DurationLogger.OnTimeReached() { @Override public void onPositionRemainingReached(long duration, long position) { suggestNextEpisode(); } }); timeLogger.addListener(0L, new DurationLogger.OnTimeReached() { @Override public void onPositionRemainingReached(long duration, long position) { playNextEpisode(); } }); player.addListener(timeLogger); timeLogger.start(player); if (mediaSession != null) { mediaSession.release(); } mediaSession = new MediaSession(this, MEDIA_SESSION_TAG); mediaSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); mediaSession.setActive(true); updateMediaSessionData(); } if (playerNeedsPrepare) { player.prepare(); playerNeedsPrepare = false; } player.setSurface(videoPlayerView.getVideoSurface()); player.setPlayWhenReady(true); } } private void releasePlayer() { if (player != null) { Log.d(TAG, "Release player"); videoPlayerView.stopDebugView(); playerPosition = player.getCurrentPosition(); long duration = player.getDuration(); player.getPlayerControl().pause(); updatePlaybackState(ExoPlayer.STATE_IDLE); updateMediaSessionIntent(); player.release(); player = null; eventLogger.endSession(); eventLogger = null; timeLogger.endSession(); timeLogger = null; if (playerPosition >= duration) { mediaSession.setActive(false); mediaSession.release(); mediaSession = null; } } } private void updateMediaSessionData() { if (mCurrentEpisode == null) { return; } final MediaMetadata.Builder builder = new MediaMetadata.Builder(); updatePlaybackState(ExoPlayer.STATE_IDLE); updateMediaSessionIntent(); builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, mCurrentEpisode.getSeriesTitle()); builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, mCurrentEpisode.getTitle()); builder.putLong(MediaMetadata.METADATA_KEY_DURATION, mCurrentEpisode.getDuration() * 1000); builder.putString(MediaMetadata.METADATA_KEY_TITLE, mCurrentEpisode.getSeriesTitle()); builder.putString(MediaMetadata.METADATA_KEY_ARTIST, mCurrentEpisode.getTitle()); Point size = new Point(getResources().getDimensionPixelSize(R.dimen.card_width), getResources().getDimensionPixelSize(R.dimen.card_height)); Picasso.with(this).load(mCurrentEpisode.getThumbnail()).resize(size.x, size.y).into(new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { builder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap); mediaSession.setMetadata(builder.build()); } @Override public void onBitmapFailed(Drawable errorDrawable) { } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { } }); } protected abstract Class getVideoPlayerActivityClass(); private void updateMediaSessionIntent() { Intent intent = new Intent(this, getVideoPlayerActivityClass()); intent.putExtra(ContentManagerBase.CONTENT_ID, mCurrentEpisode); String[] others = mOtherEpisodeUrls.toArray(new String[mOtherEpisodeUrls.size()]); intent.putExtra(ContentManagerBase.OTHER_EPISODES, others); intent.putExtra(RESUME_POSITION, playerPosition); PlayHistory.updateProgress(mCurrentEpisode, playerPosition); PendingIntent pending = PendingIntent.getActivity(this, 99, intent, PendingIntent.FLAG_UPDATE_CURRENT); mediaSession.setSessionActivity(pending); } private void updatePlaybackState(int playbackState) { PlaybackState.Builder state = new PlaybackState.Builder(); long position = player.getCurrentPosition(); if (ExoPlayer.STATE_PREPARING == playbackState) { state.setState(PlaybackState.STATE_CONNECTING, position, 1.0f); } else if (ExoPlayer.STATE_BUFFERING == playbackState) { state.setState(PlaybackState.STATE_BUFFERING, position, 1.0f); } else { if (player.getPlayerControl().isPlaying()) { state.setState(PlaybackState.STATE_PLAYING, position, 1.0f); } else { state.setState(PlaybackState.STATE_PAUSED, position, 1.0f); } } mediaSession.setPlaybackState(state.build()); } private View.OnClickListener getNextEpisodeListener() { EpisodeBaseModel next = getNextEpisode(mCurrentEpisode); Log.d(TAG, "next episode:" + next); if (next != null) { return new View.OnClickListener() { @Override public void onClick(View v) { playNextEpisode(); } }; } return null; } private View.OnClickListener getPrevEpisodeListener() { EpisodeBaseModel prev = getPrevEpisode(mCurrentEpisode); Log.d(TAG, "previous episode:" + prev); if (prev != null) { return new View.OnClickListener() { @Override public void onClick(View v) { playPrevEpisode(); } }; } return null; } private EpisodeBaseModel getNextEpisode(EpisodeBaseModel current) { return getContentManger().findNextEpisode(mOtherEpisodeUrls, current.getHref()); } private EpisodeBaseModel getPrevEpisode(EpisodeBaseModel current) { List<String> others = new ArrayList<>(mOtherEpisodeUrls); Collections.reverse(others); return getContentManger().findNextEpisode(others, current.getHref()); } private void suggestNextEpisode() { EpisodeBaseModel next = getNextEpisode(mCurrentEpisode); Log.d(TAG, "Suggest next episode: " + next); if (next != null) { videoPlayerView.suggestNextEpisode(next); } } private void playNextEpisode() { EpisodeBaseModel next = getNextEpisode(mCurrentEpisode); Log.d(TAG, "Play next episode: " + next); if (next != null) { playEpisode(next); } } private void playPrevEpisode() { EpisodeBaseModel next = getPrevEpisode(mCurrentEpisode); Log.d(TAG, "Play previous episode: " + next); if (next != null) { playEpisode(next); } } // VideoPlayer.Listener implementation @Override public void onStateChanged(boolean playWhenReady, int playbackState) { videoPlayerView.onStateChanged(playWhenReady, playbackState); updatePlaybackState(playbackState); } @Override public void onError(Exception e) { if (e instanceof UnsupportedDrmException) { // Special case DRM failures. UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e; int stringId = Util.SDK_INT < 18 ? R.string.drm_error_not_supported : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.drm_error_unsupported_scheme : R.string.drm_error_unknown; Toast.makeText(getApplicationContext(), stringId, Toast.LENGTH_LONG).show(); } playerNeedsPrepare = true; videoPlayerView.showControls(); } @Override public void onVideoSizeChanged(int width, int height, float pixelWidthAspectRatio) { videoPlayerView.showShutter(false); videoPlayerView.setVideoFrameAspectRatio(height == 0 ? 1 : (width * pixelWidthAspectRatio) / height); } // VideoPlayer.CaptionListener implementation @Override public void onCues(List<Cue> cues) { videoPlayerView.setCues(cues); } // SurfaceHolder.Callback implementation @Override public void surfaceCreated(SurfaceHolder holder) { if (player != null) { player.setSurface(holder.getSurface()); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Do nothing. } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (player != null) { player.blockingClearSurface(); } } }