package com.cresta.exoplayerhlssimple;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.text.Layout;
import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.cresta.exoplayerhlssimple.exoplayer.DemoPlayer;
import com.cresta.exoplayerhlssimple.exoplayer.HlsRendererBuilder;
import com.google.android.exoplayer.AspectRatioFrameLayout;
import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
import com.google.android.exoplayer.chunk.Format;
import butterknife.Bind;
import butterknife.ButterKnife;
/**
* Created by Gaspar de Elias (gaspar.deelias@cresta.com.ar)
*/
public class ExoPlayerFragment extends Fragment {
private static final String TAG = ExoPlayerFragment.class.getSimpleName();
public enum PlayerUiState {noStream, streamSoon, streaming, error, buffering, notAllowed};
private DemoPlayer mPlayer;
private boolean mNeedsPrepare;
private String mUrl = "http://vevoplaylist-live.hls.adaptive.level3.net/vevo/ch1/appleman.m3u8";
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private AudioCapabilities mAudioCapabilities;
@Bind(R.id.surface_view) SurfaceView mSurface;
@Bind(R.id.video_frame) AspectRatioFrameLayout mVideoFrame;
@Bind(R.id.playerInfoText) TextView mPlayerInfoText;
@Bind(R.id.playerDebugText) TextView mPlayerDebugText;
@Bind(R.id.debugBlock) LinearLayout mDebugBlock;
@Bind(R.id.logLevelSpinner) Spinner mLogLevelSpinner;
@Bind(R.id.playerDebugCheck) CheckBox mPlayerDebugCheckbox;
@Bind(R.id.bufferingProgress) ProgressBar mBufferingProgress;
public ExoPlayerFragment() {
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getActivity(), mAudioCapabilitiesListener);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_exoplayer, container, false);
ButterKnife.bind(this, view);
setupDebugStuff();
return view;
}
@Override
public void onResume() {
super.onResume();
mAudioCapabilitiesReceiver.register();
preparePlayer();
}
@Override
public void onPause() {
super.onPause();
mAudioCapabilitiesReceiver.unregister();
releasePlayer();
}
private void preparePlayer() {
if (mPlayer == null) {
HlsRendererBuilder rendererBuilder = new HlsRendererBuilder(getActivity(), "USER_AGENT", mUrl, mAudioCapabilities);
mPlayer = new DemoPlayer(rendererBuilder);
mPlayer.addListener(mPlayerListener);
mPlayer.setInfoListener(mInfoListener);
mNeedsPrepare = true;
}
if (mNeedsPrepare) {
mPlayer.prepare();
mNeedsPrepare = false;
}
mPlayer.setSurface(mSurface.getHolder().getSurface());
mPlayer.setPlayWhenReady(true);
mVideoFrame.setAspectRatio(1.6f);
}
private void releasePlayer() {
if (mPlayer != null) {
mPlayer.release();
mPlayer = null;
}
}
// TODO: Find more information about audioCapabilites stuff.
private AudioCapabilitiesReceiver.Listener mAudioCapabilitiesListener = new AudioCapabilitiesReceiver.Listener() {
@Override
public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
boolean audioCapabilitiesChanged = !audioCapabilities.equals(mAudioCapabilities);
if (mPlayer == null || audioCapabilitiesChanged) {
mAudioCapabilities = audioCapabilities;
releasePlayer();
preparePlayer();
} else if (mPlayer != null) {
mPlayer.setBackgrounded(false);
}
}
};
private DemoPlayer.Listener mPlayerListener = new DemoPlayer.Listener() {
@Override
public void onStateChanged(boolean playWhenReady, int playbackState) {
Log.d(TAG, "onStateChanged playWhenReady:" + playWhenReady + " playbackState:" + playbackState);
addDebugMessage("onStateChanged: state:" + playbackState + " playwhenready:" + playWhenReady, LogLevel.DEBUG);
switch(playbackState) {
case ExoPlayer.STATE_IDLE:
updatePlayerUi(PlayerUiState.noStream);
break;
case ExoPlayer.STATE_PREPARING:
updatePlayerUi(PlayerUiState.buffering);
break;
case ExoPlayer.STATE_BUFFERING:
updatePlayerUi(PlayerUiState.buffering);
break;
case ExoPlayer.STATE_READY:
updatePlayerUi(PlayerUiState.streaming);
break;
case ExoPlayer.STATE_ENDED:
updatePlayerUi(PlayerUiState.noStream);
break;
}
}
private void updatePlayerUi(PlayerUiState playerUiState) {
mPlayerInfoText.setVisibility(View.VISIBLE);
mBufferingProgress.setVisibility(View.INVISIBLE);
switch(playerUiState) {
case noStream:
mPlayerInfoText.setText(R.string.livestream_status_no_stream);
break;
case streamSoon:
mPlayerInfoText.setText(R.string.livestream_status_starting_soon);
break;
case streaming:
mPlayerInfoText.setVisibility(View.GONE);
break;
case buffering:
mPlayerInfoText.setText(R.string.livestream_status_buffering);
mBufferingProgress.setVisibility(View.VISIBLE);
break;
case error:
mPlayerInfoText.setText(R.string.error_video_player_failed);
break;
case notAllowed:
//FIXME
mPlayerInfoText.setText("NOT ALLOWED");
//mPlayerInfoText.setVisibility(View.GONE);
break;
}
}
@Override
public void onError(Exception e) {
Log.e(TAG, "onError " + e.getMessage());
updatePlayerUi(PlayerUiState.error);
addDebugMessage("ERROR: " + e.getMessage() + ":" + e.toString(), LogLevel.ERROR);
}
@Override
public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
Log.d(TAG, "onVideoSizeChanged: width:" + width + ", height:" + height + ", ratio:" + pixelWidthHeightRatio);
mVideoFrame.setAspectRatio(height == 0 ? 1 : (width * pixelWidthHeightRatio) / height);
addDebugMessage("OnVideSize: " + width + "x" + height + " : ratio: " + pixelWidthHeightRatio, LogLevel.DEBUG);
}
};
private DemoPlayer.InfoListener mInfoListener = new DemoPlayer.InfoListener() {
@Override
public void onVideoFormatEnabled(Format format, int trigger, int mediaTimeMs) {
addDebugMessage("videoFormat:" + format.toString(), LogLevel.DEBUG);
}
@Override
public void onAudioFormatEnabled(Format format, int trigger, int mediaTimeMs) {
addDebugMessage("audioFormat:" + format.toString(), LogLevel.DEBUG);
}
@Override
public void onDroppedFrames(int count, long elapsed) {
addDebugMessage("droppedFrames:" + count + ":elapsed:" + elapsed, LogLevel.VERBOSE);
}
@Override
public void onBandwidthSample(int elapsedMs, long bytes, long bitrateEstimate) {
addDebugMessage("onBandwidthSample:" + bytes + " bytes, bitrateEstimate:" + bitrateEstimate, LogLevel.DEBUG);
}
@Override
public void onLoadStarted(int sourceId, long length, int type, int trigger, Format format, int mediaStartTimeMs, int mediaEndTimeMs) {
addDebugMessage("onLoadStarted: "+sourceId+":"+ (format!=null?format.bitrate:null)+" bps.", LogLevel.VERBOSE);
}
@Override
public void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Format format, int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs) {
addDebugMessage("onLoadCompleted: "+sourceId+":"+ (format!=null?format.bitrate:null)+" bps.", LogLevel.VERBOSE);
}
@Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, long initializationDurationMs) {
addDebugMessage("onDecoderInitialized: " + decoderName + ", elapsedRealTime: " + elapsedRealtimeMs + ", initDurationMs: " +initializationDurationMs, LogLevel.VERBOSE);
}
@Override
public void onSeekRangeChanged(TimeRange seekRange) {
addDebugMessage("onSeekRangeChanged: range:" + seekRange.toString(), LogLevel.VERBOSE);
}
};
/**
* DEBUG Section
*/
private enum LogLevel{ERROR, DEBUG, VERBOSE};
private LogLevel mLogLevel = LogLevel.VERBOSE;
private void setupDebugStuff() {
mDebugBlock.setVisibility(View.VISIBLE);
mPlayerDebugCheckbox.setChecked(true);
mPlayerDebugText.setMovementMethod(new ScrollingMovementMethod());
mPlayerDebugCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mPlayerDebugText.setVisibility(isChecked ? View.VISIBLE : View.INVISIBLE);
}
});
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(getActivity(), R.array.debugLogLevel, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mLogLevelSpinner.setAdapter(adapter);
mLogLevelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
String selection = (String) parent.getItemAtPosition(position);
mLogLevel = LogLevel.valueOf(selection);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* Add debug message. (taken from http://stackoverflow.com/questions/3506696/auto-scrolling-textview-in-android-to-bring-text-into-view)
* @param msg
*/
private void addDebugMessage(String msg, LogLevel level) {
if (level.ordinal() <=mLogLevel.ordinal()) {
// append the new string
mPlayerDebugText.append(msg + "\n");
// find the amount we need to scroll. This works by
// asking the TextView's internal layout for the position
// of the final line and then subtracting the TextView's height
Layout textViewLayout = mPlayerDebugText.getLayout();
if (textViewLayout != null) {
final int scrollAmount = textViewLayout.getLineTop(mPlayerDebugText.getLineCount()) - mPlayerDebugText.getHeight();
// if there is no need to scroll, scrollAmount will be <=0
if (scrollAmount > 0)
mPlayerDebugText.scrollTo(0, scrollAmount);
else
mPlayerDebugText.scrollTo(0, 0);
}
}
}
}