PageRenderTime 77ms CodeModel.GetById 35ms RepoModel.GetById 1ms app.codeStats 0ms

/app/src/main/java/com/alecgdouglas/gbenthusiast/ui/PlaybackActivity.java

https://bitbucket.org/chadlevesque/gbenthusiast
Java | 465 lines | 401 code | 48 blank | 16 comment | 84 complexity | eee054be7b650a14a2cd2d317322be04 MD5 | raw file
Possible License(s): GPL-3.0
  1. package com.alecgdouglas.gbenthusiast.ui;
  2. import android.app.Fragment;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.media.AudioManager;
  6. import android.net.Uri;
  7. import android.os.Bundle;
  8. import android.os.Handler;
  9. import android.os.PowerManager;
  10. import android.os.SystemClock;
  11. import android.util.Log;
  12. import android.view.KeyEvent;
  13. import android.widget.ImageButton;
  14. import com.alecgdouglas.gbenthusiast.DefaultQualityPrefUtils;
  15. import com.alecgdouglas.gbenthusiast.EmptyVideoRendererEventListener;
  16. import com.alecgdouglas.gbenthusiast.R;
  17. import com.alecgdouglas.gbenthusiast.ResettingBandwidthMeter;
  18. import com.alecgdouglas.gbenthusiast.Utils;
  19. import com.alecgdouglas.gbenthusiast.data.VideoPositionDbUtils;
  20. import com.alecgdouglas.gbenthusiast.model.Video;
  21. import com.alecgdouglas.gbenthusiast.model.VideoQuality;
  22. import com.google.android.exoplayer2.C;
  23. import com.google.android.exoplayer2.DefaultLoadControl;
  24. import com.google.android.exoplayer2.ExoPlaybackException;
  25. import com.google.android.exoplayer2.ExoPlayer;
  26. import com.google.android.exoplayer2.ExoPlayerFactory;
  27. import com.google.android.exoplayer2.Format;
  28. import com.google.android.exoplayer2.SimpleExoPlayer;
  29. import com.google.android.exoplayer2.Timeline;
  30. import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
  31. import com.google.android.exoplayer2.source.ExtractorMediaSource;
  32. import com.google.android.exoplayer2.source.MediaSource;
  33. import com.google.android.exoplayer2.source.TrackGroupArray;
  34. import com.google.android.exoplayer2.source.hls.HlsMediaSource;
  35. import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
  36. import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
  37. import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
  38. import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
  39. import com.google.android.exoplayer2.trackselection.TrackSelection;
  40. import com.google.android.exoplayer2.trackselection.TrackSelections;
  41. import com.google.android.exoplayer2.ui.PlaybackControlView;
  42. import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
  43. import com.google.android.exoplayer2.upstream.DataSource;
  44. import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
  45. import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
  46. import java.util.concurrent.TimeUnit;
  47. public class PlaybackActivity extends CommonActivity
  48. implements ExoPlayer.EventListener, AudioManager.OnAudioFocusChangeListener {
  49. private static final String TAG = "PlaybackOverlayAct";
  50. private static final int DEFAULT_MAX_INITIAL_BITRATE = 70000;
  51. private static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
  52. private static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 15000;
  53. private static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 15000;
  54. private static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
  55. private static final int REWIND_MILLIS = 10000;
  56. private static final int FAST_FORWARD_MILLIS = 15000;
  57. private static final long SUPER_SEEK_MILLIS = TimeUnit.SECONDS.toMillis(30);
  58. private static final String BUFFERING_SPINNER_TAG = "buffering_spinner";
  59. // 10 second interval between saving position while the video is playing.
  60. private static final int SAVE_POSITION_INTERVAL_MILLIS = 10 * 1000;
  61. private final ResettingBandwidthMeter mBandwidthMeter = new ResettingBandwidthMeter();
  62. private final SelectStreamTrackDialogFragment mSelectStreamTrackDialog =
  63. new SelectStreamTrackDialogFragment();
  64. private SimpleExoPlayerView mSimpleExoPlayerView;
  65. private PlaybackControlView mController;
  66. private SimpleExoPlayer mPlayer;
  67. private MappingTrackSelector mTrackSelector;
  68. private boolean mPlayerNeedsSource = true;
  69. private Video mVideo;
  70. private VideoQuality mVideoQuality;
  71. private Handler mMainHandler;
  72. private Runnable mSavePositionRunnable;
  73. private PowerManager.WakeLock mWakeLock;
  74. @Override
  75. protected void onCreate(Bundle savedInstanceState) {
  76. super.onCreate(savedInstanceState);
  77. setContentView(R.layout.activity_playback);
  78. final PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
  79. mWakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, getLocalClassName());
  80. mMainHandler = new Handler();
  81. mSimpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
  82. mSimpleExoPlayerView.requestFocus();
  83. mController =
  84. (PlaybackControlView) findViewById(com.google.android.exoplayer2.R.id.control);
  85. // Hide the next/previous buttons.
  86. final ImageButton prevButton = (ImageButton) mSimpleExoPlayerView
  87. .findViewById(com.google.android.exoplayer2.R.id.prev);
  88. final ImageButton nextButton = (ImageButton) mSimpleExoPlayerView
  89. .findViewById(com.google.android.exoplayer2.R.id.next);
  90. prevButton.setImageDrawable(null);
  91. nextButton.setImageDrawable(null);
  92. mSimpleExoPlayerView.setRewindIncrementMs(REWIND_MILLIS);
  93. mSimpleExoPlayerView.setFastForwardIncrementMs(FAST_FORWARD_MILLIS);
  94. }
  95. @Override
  96. public void onStart() {
  97. super.onStart();
  98. final Intent intent = getIntent();
  99. mVideo = (Video) intent.getSerializableExtra(VideoDetailsActivity.VIDEO);
  100. mVideoQuality = (VideoQuality) intent.getSerializableExtra(VideoDetailsActivity.QUALITY);
  101. if (mVideoQuality == null) {
  102. mVideoQuality = DefaultQualityPrefUtils.getDefaultQuality(this);
  103. }
  104. initializePlayer();
  105. }
  106. @Override
  107. public void onResume() {
  108. super.onResume();
  109. if (mPlayer == null) {
  110. initializePlayer();
  111. }
  112. }
  113. @Override
  114. public void onPause() {
  115. super.onPause();
  116. releasePlayer(true);
  117. }
  118. @Override
  119. public void onStop() {
  120. super.onStop();
  121. // Finish the activity now to ensure the app resumes at either the main activity or the
  122. // details activity and never the playback activity.
  123. finish();
  124. }
  125. @Override
  126. public void onDestroy() {
  127. releasePlayer(true);
  128. mMainHandler.removeCallbacks(mSavePositionRunnable);
  129. super.onDestroy();
  130. }
  131. private void initializePlayer() {
  132. if (mPlayer == null) {
  133. final TrackSelection.Factory videoTrackSelectionFactory = mVideo.isLive() ?
  134. new AdaptiveVideoTrackSelection.Factory(mBandwidthMeter,
  135. DEFAULT_MAX_INITIAL_BITRATE,
  136. DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
  137. DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
  138. DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
  139. DEFAULT_BANDWIDTH_FRACTION) : new FixedTrackSelection.Factory();
  140. mTrackSelector = new DefaultTrackSelector(mMainHandler, videoTrackSelectionFactory);
  141. mPlayer = ExoPlayerFactory
  142. .newSimpleInstance(this, mTrackSelector, new DefaultLoadControl(), null, false);
  143. mPlayer.addListener(this);
  144. mSelectStreamTrackDialog.init(mTrackSelector, videoTrackSelectionFactory);
  145. mPlayer.setVideoDebugListener(new EmptyVideoRendererEventListener() {
  146. @Override
  147. public void onVideoInputFormatChanged(Format format) {
  148. if (!mVideo.isLive()) {
  149. if (!Utils.supportsFormat(format)) {
  150. Log.i(TAG, "This device doesn't support this format, downgrading to " +
  151. "the next quality down");
  152. final VideoQuality downQuality = VideoQuality.stepDown(mVideoQuality);
  153. Log.i(TAG, "Downgrading from " + mVideoQuality.toString() + " to " +
  154. downQuality.toString());
  155. mVideoQuality = downQuality;
  156. releasePlayer(false);
  157. initializePlayer();
  158. }
  159. } else if (!Utils.supports1080p60()) {
  160. // Live video on low-end device
  161. updateTrackBlacklist();
  162. }
  163. }
  164. });
  165. mSimpleExoPlayerView.setPlayer(mPlayer);
  166. mPlayerNeedsSource = true;
  167. }
  168. if (mPlayerNeedsSource) {
  169. final MediaSource mediaSource = getMediaSource();
  170. if (!mVideo.isLive()) {
  171. final Long savedPosition =
  172. VideoPositionDbUtils.getCurrentPositionFromDbSync(this, mVideo.getId());
  173. if (savedPosition != null) {
  174. Log.i(TAG, "Saved position retrieved: " + savedPosition + " ms");
  175. Log.i(TAG, "Video length: " + mVideo.getDurationMillis() + " ms");
  176. if (savedPosition < mVideo.getDurationMillis()) {
  177. Log.i(TAG, "Restoring saved position at " + savedPosition + " ms");
  178. mPlayer.seekTo(savedPosition);
  179. } else {
  180. Log.i(TAG, "Saved position was at the end of the video, starting video " +
  181. "over " + "from the beginning");
  182. }
  183. }
  184. }
  185. mPlayer.prepare(mediaSource);
  186. mPlayerNeedsSource = false;
  187. mPlayer.setPlayWhenReady(true);
  188. }
  189. }
  190. private void updateTrackBlacklist() {
  191. // Blacklist tracks that are too much for some devices to handle.
  192. final TrackSelections<MappingTrackSelector.MappedTrackInfo> selections =
  193. mTrackSelector.getCurrentSelections();
  194. for (int i = 0; i < selections.length; i++) {
  195. final TrackSelection selection = selections.get(i);
  196. if (selection == null) continue;
  197. for (int j = 0; j < selection.length(); j++) {
  198. final Format trackFormat = selection.getFormat(j);
  199. if (trackFormat == null) continue;
  200. if (!Utils.supportsFormat(trackFormat)) {
  201. selection.blacklist(j,
  202. SystemClock.elapsedRealtime() + TimeUnit.HOURS.toMillis(24L));
  203. }
  204. }
  205. }
  206. }
  207. private MediaSource getMediaSource() {
  208. final Uri videoUri = Uri.parse(mVideo.getVideoUrlForQuality(mVideoQuality));
  209. final String userAgent =
  210. Utils.getUserAgent(this, getResources().getString(R.string.app_name));
  211. final DataSource.Factory dataSourceFactory =
  212. mVideo.isLive() ? new DefaultHttpDataSourceFactory(userAgent, mBandwidthMeter) :
  213. new DefaultDataSourceFactory(this, userAgent);
  214. if (mVideo.isLive()) {
  215. return new HlsMediaSource(videoUri, dataSourceFactory, mMainHandler, null);
  216. } else {
  217. return new ExtractorMediaSource(videoUri, dataSourceFactory,
  218. new DefaultExtractorsFactory(), mMainHandler, null);
  219. }
  220. }
  221. private void releasePlayer(boolean savePosition) {
  222. if (mWakeLock.isHeld()) {
  223. Log.i(TAG, "Releasing wake lock prior to releasing player");
  224. mWakeLock.release();
  225. abandonAudioFocus();
  226. }
  227. if (mPlayer == null) return;
  228. if (savePosition) {
  229. saveCurrentPosition();
  230. }
  231. mPlayer.release();
  232. mPlayer = null;
  233. mTrackSelector = null;
  234. mSelectStreamTrackDialog.release();
  235. }
  236. @Override
  237. public void onAudioFocusChange(int focusChange) {
  238. if (focusChange == AudioManager.AUDIOFOCUS_LOSS && mPlayer != null) {
  239. Log.i(TAG, "Audio focus lost, stopping playback");
  240. mPlayer.setPlayWhenReady(false);
  241. }
  242. }
  243. @Override
  244. public void onLoadingChanged(boolean isLoading) {
  245. // Do nothing... yet.
  246. }
  247. @Override
  248. public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
  249. if (playWhenReady) {
  250. if (!mWakeLock.isHeld()) {
  251. Log.i(TAG, "Acquiring wake lock on player state changed to playing");
  252. mWakeLock.acquire();
  253. requestAudioFocus();
  254. /* TODO: Test this with a real live stream.
  255. if (mVideo.isLive() && mPlayer.getDuration() == C.TIME_UNSET) {
  256. mPlayer.seekTo(C.TIME_UNSET);
  257. }
  258. */
  259. }
  260. if (mSavePositionRunnable == null) {
  261. mSavePositionRunnable = new Runnable() {
  262. @Override
  263. public void run() {
  264. try {
  265. saveCurrentPosition();
  266. } finally {
  267. mMainHandler.postDelayed(this, SAVE_POSITION_INTERVAL_MILLIS);
  268. }
  269. }
  270. };
  271. mMainHandler.postDelayed(mSavePositionRunnable, SAVE_POSITION_INTERVAL_MILLIS);
  272. }
  273. } else {
  274. if (mWakeLock.isHeld()) {
  275. Log.i(TAG, "Releasing wake lock on player state changed to paused");
  276. mWakeLock.release();
  277. abandonAudioFocus();
  278. }
  279. mMainHandler.removeCallbacks(mSavePositionRunnable);
  280. mSavePositionRunnable = null;
  281. saveCurrentPosition();
  282. }
  283. switch (playbackState) {
  284. case ExoPlayer.STATE_IDLE:
  285. break;
  286. case ExoPlayer.STATE_BUFFERING:
  287. showSpinner();
  288. break;
  289. case ExoPlayer.STATE_READY:
  290. hideSpinner();
  291. break;
  292. case ExoPlayer.STATE_ENDED:
  293. savePosition(mVideo.getDurationMillis());
  294. finish();
  295. break;
  296. }
  297. }
  298. private void showSpinner() {
  299. if (getFragmentManager().findFragmentByTag(BUFFERING_SPINNER_TAG) != null) return;
  300. final SpinnerFragment spinnerFragment = new SpinnerFragment();
  301. getFragmentManager().beginTransaction()
  302. .add(android.R.id.content, spinnerFragment, BUFFERING_SPINNER_TAG).commit();
  303. }
  304. private void hideSpinner() {
  305. final Fragment removeSpinnerFragment =
  306. getFragmentManager().findFragmentByTag(BUFFERING_SPINNER_TAG);
  307. if (removeSpinnerFragment != null) {
  308. getFragmentManager().beginTransaction().remove(removeSpinnerFragment).commit();
  309. }
  310. }
  311. private void requestAudioFocus() {
  312. final int requestResult = ((AudioManager) getSystemService(Context.AUDIO_SERVICE))
  313. .requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
  314. final boolean success = requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
  315. Log.i(TAG, "Audio focus was " + (success ? "" : "not ") + "granted");
  316. }
  317. private void abandonAudioFocus() {
  318. final int requestResult =
  319. ((AudioManager) getSystemService(Context.AUDIO_SERVICE)).abandonAudioFocus(this);
  320. final boolean success = requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
  321. Log.i(TAG, "Audio focus was " + (success ? "" : "not ") + "abandoned successfully");
  322. }
  323. @Override
  324. public void onTimelineChanged(Timeline timeline, Object manifest) {
  325. // Just you wait, we'll do something here eventually.
  326. }
  327. @Override
  328. public void onPlayerError(ExoPlaybackException error) {
  329. Log.w(TAG, "Player error encountered", error);
  330. final PlayerErrorDialogFragment playerErrorDialog = new PlayerErrorDialogFragment();
  331. playerErrorDialog.setError(error);
  332. playerErrorDialog.show(getFragmentManager(), "playerError");
  333. }
  334. @Override
  335. public void onPositionDiscontinuity() {
  336. // Skipping around... must be something we can do here at some point.
  337. }
  338. private void savePosition(final long positionMillis) {
  339. if (mVideo == null) {
  340. return;
  341. }
  342. VideoPositionDbUtils.updateCurrentPosition(this, mVideo, positionMillis);
  343. }
  344. private void saveCurrentPosition() {
  345. if (mPlayer == null || mPlayer.getPlaybackState() == ExoPlayer.STATE_ENDED) {
  346. return;
  347. }
  348. savePosition(mPlayer.getCurrentPosition());
  349. }
  350. @Override
  351. public boolean dispatchKeyEvent(KeyEvent event) {
  352. if (mPlayer != null && event.getAction() == KeyEvent.ACTION_DOWN) {
  353. // We consume all seek keys if the video doesn't yet have a duration to avoid resetting
  354. // saved progress while the video is loading.
  355. final boolean playerHasDuration = mPlayer.getDuration() > 0;
  356. switch (event.getKeyCode()) {
  357. case KeyEvent.KEYCODE_DPAD_CENTER:
  358. final KeyEvent redispatchKey =
  359. new KeyEvent(event.getAction(), KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
  360. return super.dispatchKeyEvent(redispatchKey);
  361. case KeyEvent.KEYCODE_DPAD_RIGHT:
  362. if (playerHasDuration) superFastForward();
  363. return true;
  364. case KeyEvent.KEYCODE_DPAD_LEFT:
  365. if (playerHasDuration) superRewind();
  366. return true;
  367. case KeyEvent.KEYCODE_DPAD_UP:
  368. case KeyEvent.KEYCODE_DPAD_DOWN:
  369. if (mController != null) mController.show();
  370. break;
  371. case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
  372. case KeyEvent.KEYCODE_MEDIA_REWIND:
  373. if (!playerHasDuration) return true;
  374. break;
  375. case KeyEvent.KEYCODE_MENU:
  376. if (showTrackSelectionDialog()) return true;
  377. break;
  378. }
  379. }
  380. return super.dispatchKeyEvent(event);
  381. }
  382. private boolean showTrackSelectionDialog() {
  383. if (mTrackSelector != null && mVideo != null && mVideo.isLive()) {
  384. final TrackSelections<MappingTrackSelector.MappedTrackInfo> trackSelections =
  385. mTrackSelector.getCurrentSelections();
  386. if (trackSelections == null) {
  387. return false;
  388. }
  389. int videoIndex = -1;
  390. for (int i = 0; i < trackSelections.length; i++) {
  391. final TrackGroupArray trackGroups = trackSelections.info.getTrackGroups(i);
  392. if (trackGroups.length != 0 && mPlayer.getRendererCount() > i &&
  393. mPlayer.getRendererType(i) == C.TRACK_TYPE_VIDEO) {
  394. videoIndex = i;
  395. break;
  396. }
  397. }
  398. if (videoIndex >= 0) {
  399. mSelectStreamTrackDialog.show(getFragmentManager(), "selectLiveStreamTrack",
  400. mTrackSelector.getCurrentSelections().info, videoIndex, getResources());
  401. return true;
  402. }
  403. }
  404. return false;
  405. }
  406. private void superFastForward() {
  407. if (mPlayer == null) return;
  408. if (mController != null) mController.show();
  409. mPlayer.seekTo(
  410. Math.min(mPlayer.getCurrentPosition() + SUPER_SEEK_MILLIS, mPlayer.getDuration()));
  411. }
  412. private void superRewind() {
  413. if (mPlayer == null) return;
  414. if (mController != null) mController.show();
  415. mPlayer.seekTo(Math.max(mPlayer.getCurrentPosition() - SUPER_SEEK_MILLIS, 0L));
  416. }
  417. }