Java tutorial
/* * Author: Scott Ware <scoot.software@gmail.com> * Copyright (c) 2015 Scott Ware * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.scooter1556.sms.androidtv.fragment; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.v17.leanback.app.PlaybackOverlayFragment; import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.ControlButtonPresenterSelector; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.support.v17.leanback.widget.PlaybackControlsRow.FastForwardAction; import android.support.v17.leanback.widget.PlaybackControlsRow.PlayPauseAction; import android.support.v17.leanback.widget.PlaybackControlsRow.RewindAction; import android.support.v17.leanback.widget.PlaybackControlsRowPresenter; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.view.View; import android.view.WindowManager; import android.widget.Toast; import com.bumptech.glide.Glide; import com.bumptech.glide.request.animation.GlideAnimation; import com.bumptech.glide.request.target.SimpleTarget; import com.google.android.exoplayer.ExoPlayer; import com.google.gson.Gson; import com.loopj.android.http.JsonHttpResponseHandler; import com.loopj.android.http.TextHttpResponseHandler; import com.scooter1556.sms.androidtv.R; import com.scooter1556.sms.androidtv.activity.VideoPlayerActivity; import com.scooter1556.sms.lib.android.domain.MediaElement; import com.scooter1556.sms.lib.android.domain.TranscodeProfile; import com.scooter1556.sms.lib.android.player.SMSVideoPlayer; import com.scooter1556.sms.lib.android.service.RESTService; import org.json.JSONObject; import java.util.Timer; import java.util.TimerTask; public class VideoPlayerFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment implements SurfaceHolder.Callback, SMSVideoPlayer.Listener { static final String SUPPORTED_CODECS = "h264,vp8,aac,mp3,vorbis"; static final String MCH_CODECS = "ac3"; static final String FORMAT = "matroska"; static final int MAX_SAMPLE_RATE = 48000; private static final int CARD_SIZE = 240; private static final int BACKGROUND_TYPE = PlaybackOverlayFragment.BG_LIGHT; private static final int DEFAULT_UPDATE_PERIOD = 1000; private static final int DEFAULT_SEEK_DELAY = 1000; private static final int DEFAULT_SEEK_SPEED = 10000; private static final int MAX_SEEK_SPEED = 300000; // REST Client RESTService restService = null; // Player private SMSVideoPlayer player; private MediaElement mediaElement; // Override automatic controller visibility private Boolean initialised = false; private Boolean ready = false; private Boolean paused = false; private ArrayObjectAdapter rowsAdapter; private ArrayObjectAdapter primaryActionsAdapter; private ArrayObjectAdapter secondaryActionsAdapter; private FastForwardAction fastForwardAction; private PlayPauseAction playPauseAction; private RewindAction rewindAction; private PlaybackControlsRow playbackControlsRow; private Handler handler; private Runnable runnable; // Seek private Handler seekHandler; private Runnable seekRunnable; private int seekSpeed = DEFAULT_SEEK_SPEED; private boolean seekInProgress = false; private SurfaceHolder surfaceHolder; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); restService = RESTService.getInstance(); player = new SMSVideoPlayer(); surfaceHolder = ((VideoPlayerActivity) getActivity()).getSurfaceHolder(); surfaceHolder.addCallback(this); handler = new Handler(); seekHandler = new Handler(); // Retrieve media element to play mediaElement = (MediaElement) getActivity().getIntent().getSerializableExtra("MediaElement"); setBackgroundType(BACKGROUND_TYPE); setFadingEnabled(false); setupRows(); } @Override public void onResume() { super.onResume(); // If our surface is ready start playing video (surface is not destroyed if the power button is pressed) if (ready) { preparePlayer(); } } @Override public void onViewCreated(View view, Bundle savedInstanceState) { view.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { if (player != null) { if (player.isPlaying()) { player.pause(); } else { player.start(); } } return true; } } return false; } }); } @Override public void onDestroy() { super.onDestroy(); stopProgressAutomation(); if (player != null) { if (player.getTranscodeProfile() != null) { restService.endJob(player.getTranscodeProfile().getID()); } releasePlayer(); } } @Override public void onPause() { super.onPause(); stopProgressAutomation(); setFadingEnabled(false); paused = true; if (player != null) { player.setOffset(player.getCurrentPosition()); player.removeListener(this); player.release(); } } private void setupRows() { ClassPresenterSelector presenter = new ClassPresenterSelector(); PlaybackControlsRowPresenter playbackControlsRowPresenter; playbackControlsRowPresenter = new PlaybackControlsRowPresenter(new DescriptionPresenter()); playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() { public void onActionClicked(Action action) { if (action.getId() == playPauseAction.getId()) { if (seekInProgress && player != null) { seekTo(playbackControlsRow.getCurrentTime()); } else { togglePlayback(); } } else if (action.getId() == fastForwardAction.getId()) { if (player != null) { if (seekInProgress) { if (seekSpeed > 0) { seekSpeed = seekSpeed * 2; if (seekSpeed > MAX_SEEK_SPEED) { seekSpeed = MAX_SEEK_SPEED; } } else { seekSpeed = DEFAULT_SEEK_SPEED; } } else { seekSpeed = DEFAULT_SEEK_SPEED; startSeeking(); } } } else if (action.getId() == rewindAction.getId()) { if (player != null) { if (seekInProgress) { if (seekSpeed < 0) { seekSpeed = seekSpeed * 2; if (seekSpeed < (MAX_SEEK_SPEED * -1)) { seekSpeed = (MAX_SEEK_SPEED * -1); } } else { seekSpeed = (DEFAULT_SEEK_SPEED * -1); } } else { seekSpeed = (DEFAULT_SEEK_SPEED * -1); startSeeking(); } } } } }); presenter.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); presenter.addClassPresenter(ListRow.class, new ListRowPresenter()); rowsAdapter = new ArrayObjectAdapter(presenter); addPlaybackControlsRow(); setAdapter(rowsAdapter); } private void addPlaybackControlsRow() { playbackControlsRow = new PlaybackControlsRow(mediaElement); playbackControlsRow.setCurrentTime(0); playbackControlsRow.setTotalTime(mediaElement.getDuration() * 1000); rowsAdapter.add(playbackControlsRow); ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector(); primaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); playbackControlsRow.setPrimaryActionsAdapter(primaryActionsAdapter); playPauseAction = new PlayPauseAction(getActivity()); fastForwardAction = new FastForwardAction(getActivity()); rewindAction = new RewindAction(getActivity()); // Add main controls to primary adapter. primaryActionsAdapter.add(rewindAction); primaryActionsAdapter.add(playPauseAction); primaryActionsAdapter.add(fastForwardAction); updateVideoImage(); } private void startProgressAutomation() { if (runnable == null) { runnable = new Runnable() { @Override public void run() { int currentTime = (int) player.getCurrentPosition(); playbackControlsRow.setCurrentTime(currentTime); handler.postDelayed(this, DEFAULT_UPDATE_PERIOD); } }; handler.postDelayed(runnable, DEFAULT_UPDATE_PERIOD); } } private void stopProgressAutomation() { if (handler != null && runnable != null) { handler.removeCallbacks(runnable); runnable = null; } } private void startSeeking() { if (!player.isPlaying()) { return; } player.pause(); seekInProgress = true; setFadingEnabled(false); stopProgressAutomation(); playPauseAction.setIndex(PlayPauseAction.PLAY); rowsAdapter.notifyArrayItemRangeChanged(rowsAdapter.indexOf(playbackControlsRow), 1); if (seekRunnable == null) { seekRunnable = new Runnable() { @Override public void run() { int currentTime = playbackControlsRow.getCurrentTime() + seekSpeed; if (currentTime < 0) { seekTo(0); } else if (currentTime > playbackControlsRow.getTotalTime()) { releasePlayer(); getActivity().finish(); } else { playbackControlsRow.setCurrentTime(currentTime); seekHandler.postDelayed(this, DEFAULT_SEEK_DELAY); } } }; seekHandler.postDelayed(seekRunnable, DEFAULT_SEEK_DELAY); } } private void stopSeeking() { if (seekHandler != null && seekRunnable != null) { seekHandler.removeCallbacks(seekRunnable); seekRunnable = null; seekInProgress = false; } } private void updateVideoImage() { Glide.with(getActivity()).load(RESTService.getInstance().getConnection().getUrl() + "/image/" + mediaElement.getID() + "/cover/" + CARD_SIZE).asBitmap() .into(new SimpleTarget<Bitmap>(CARD_SIZE, CARD_SIZE) { @Override public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) { playbackControlsRow.setImageBitmap(getActivity(), resource); rowsAdapter.notifyArrayItemRangeChanged(0, rowsAdapter.size()); } }); } private void initialiseVideo() { // Get settings final SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getActivity()); if (mediaElement == null) { Toast error = Toast.makeText(getActivity(), getString(R.string.error_loading_media), Toast.LENGTH_SHORT); error.show(); } // Initialise Stream restService.initialiseStream(mediaElement.getID(), null, SUPPORTED_CODECS, null, FORMAT, Integer.parseInt(settings.getString("pref_video_quality", "0")), MAX_SAMPLE_RATE, null, null, settings.getBoolean("pref_direct_play", false), new JsonHttpResponseHandler() { @Override public void onSuccess(int statusCode, cz.msebera.android.httpclient.Header[] headers, JSONObject response) { // Parse profile Gson parser = new Gson(); TranscodeProfile profile = parser.fromJson(response.toString(), TranscodeProfile.class); // Configure Player player.setTranscodeProfile(profile); player.setID(mediaElement.getID()); player.setQuality(settings.getString("pref_video_quality", "0")); player.setMultiChannelEnabled(settings.getBoolean("pref_audio_multichannel", false)); if (mediaElement.getDuration() != null) { player.setDuration(mediaElement.getDuration()); } preparePlayer(); initialised = true; } @Override public void onFailure(int statusCode, cz.msebera.android.httpclient.Header[] headers, String responseString, Throwable throwable) { Toast error = Toast.makeText(getActivity(), getString(R.string.error_loading_media), Toast.LENGTH_SHORT); error.show(); } }); } private void preparePlayer() { String streamUrl = restService.getConnection().getUrl() + "/stream/" + player.getTranscodeProfile().getID() + "?offset=" + (int) (player.getOffset() * 0.001); Uri contentUri = Uri.parse(streamUrl); player.addListener(this); player.initialise(getActivity(), contentUri, surfaceHolder.getSurface(), !paused); } private void releasePlayer() { if (player != null) { player.release(); player = null; } } private void togglePlayback() { if (player != null) { if (player.isPlaying()) { player.pause(); playPauseAction.setIndex(PlayPauseAction.PLAY); stopProgressAutomation(); setFadingEnabled(false); getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); paused = true; } else { player.start(); playPauseAction.setIndex(PlayPauseAction.PAUSE); startProgressAutomation(); setFadingEnabled(true); getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); paused = false; } rowsAdapter.notifyArrayItemRangeChanged(rowsAdapter.indexOf(playbackControlsRow), 1); } } private void seekTo(int position) { if (player != null) { // Stop seeking operations stopSeeking(); // Generate new stream uri String streamUrl = restService.getConnection().getUrl() + "/stream/" + player.getTranscodeProfile().getID() + "?offset=" + (int) (position * 0.001); Uri contentUri = Uri.parse(streamUrl); // Initialise player in new position player.release(); player.setOffset(position); player.initialise(getActivity(), contentUri, surfaceHolder.getSurface(), !paused); } } @Override public void onStateChanged(boolean playWhenReady, int playbackState) { switch (playbackState) { case ExoPlayer.STATE_BUFFERING: break; case ExoPlayer.STATE_ENDED: releasePlayer(); getActivity().finish(); break; case ExoPlayer.STATE_IDLE: break; case ExoPlayer.STATE_PREPARING: break; case ExoPlayer.STATE_READY: // Set timeline playbackControlsRow.setCurrentTime((int) player.getCurrentPosition()); if (player.isPlaying()) { playPauseAction.setIndex(PlayPauseAction.PAUSE); setFadingEnabled(true); getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); startProgressAutomation(); } else { playPauseAction.setIndex(PlayPauseAction.PLAY); } rowsAdapter.notifyArrayItemRangeChanged(rowsAdapter.indexOf(playbackControlsRow), 1); break; default: break; } } @Override public void onError(Exception e) { // Display error message Toast warning = Toast.makeText(getActivity(), getString(R.string.error_media_playback), Toast.LENGTH_SHORT); warning.show(); // Release player and finish activity releasePlayer(); getActivity().finish(); } @Override public void surfaceCreated(SurfaceHolder holder) { if (initialised) { paused = true; preparePlayer(); } else { initialiseVideo(); } // Surface is ready ready = true; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Do nothing. } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (player != null) { ready = false; player.release(); } } private static final class DescriptionPresenter extends AbstractDetailsDescriptionPresenter { @Override protected void onBindDescription(ViewHolder viewHolder, Object item) { MediaElement element = ((MediaElement) item); if (element != null) { viewHolder.getTitle().setText(element.getTitle()); } } } }