package com.etiennelawlor.loop.fragments; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.MediaRouteButton; import android.support.v7.view.ContextThemeWrapper; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import com.bumptech.glide.Glide; import com.etiennelawlor.loop.LoopApplication; import com.etiennelawlor.loop.R; import com.etiennelawlor.loop.models.AccessToken; import com.etiennelawlor.loop.models.VideoSavedState; import com.etiennelawlor.loop.network.ServiceGenerator; import com.etiennelawlor.loop.network.VimeoPlayerService; import com.etiennelawlor.loop.network.interceptors.AuthorizedNetworkInterceptor; import com.etiennelawlor.loop.network.models.response.Video; import com.etiennelawlor.loop.network.models.response.VideoConfig; import com.etiennelawlor.loop.prefs.LoopPrefs; import com.etiennelawlor.loop.utilities.FontCache; import com.etiennelawlor.loop.utilities.NetworkLogUtility; import com.etiennelawlor.loop.utilities.NetworkUtility; import com.etiennelawlor.loop.utilities.TrestleUtility; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.cast.framework.CastSession; import com.google.android.gms.cast.framework.SessionManagerListener; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.common.images.WebImage; import java.util.ArrayList; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.Unbinder; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; /** * Created by etiennelawlor on 1/3/17. */ public class VideoDetailsFragment extends BaseFragment { // ExoPlayer Guide // https://google.github.io/ExoPlayer/guide.html // https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/SimpleExoPlayerView.html // region Constants private static final int VIDEO_SHARE_REQUEST_CODE = 1002; public static final String KEY_VIDEO_ID = "KEY_VIDEO_ID"; public static final String KEY_VIDEO = "KEY_VIDEO"; // endregion // region Views @BindView(R.id.toolbar) Toolbar toolbar; @Nullable @BindView(R.id.viewpager) ViewPager viewPager; @Nullable @BindView(R.id.tabs) TabLayout tabLayout; @BindView(R.id.sepv) SimpleExoPlayerView simpleExoPlayerView; // @BindView(R.id.loading_iv) // LoadingImageView loadingImageView; // @BindView(R.id.error_ll) // LinearLayout errorLinearLayout; // @BindView(R.id.error_tv) // TextView errorTextView; @BindView(R.id.exo_thumbnail) ImageView thumbnailImageView; @BindView(R.id.cast_info_tv) TextView castInfoTextView; @BindView(R.id.exo_shutter) View shutterView; @BindView(R.id.exo_progress) SeekBar exoSeekBar; @BindView(R.id.exo_play) ImageButton exoPlayButton; @BindView(R.id.exo_pause) ImageButton exoPauseButton; @BindView(R.id.exo_replay) ImageButton replayImageButton; @BindView(R.id.exo_btns_fl) FrameLayout exoButtonsFrameLayout; @BindView(R.id.control_view_ll) LinearLayout controlViewLinearLayout; @BindView(R.id.mrb) MediaRouteButton mediaRouteButton; @BindView(R.id.exo_content_frame) AspectRatioFrameLayout aspectRatioFrameLayout; // endregion // region Member Variables private Video video; private String videoUrl; private VimeoPlayerService vimeoPlayerService; private VideoSavedState videoSavedState; private Unbinder unbinder; private Typeface font; private SimpleExoPlayer exoPlayer; private CastContext castContext; private PlaybackLocation location; private CastSession castSession; private PlaybackState playbackState; private boolean isLocalVideoPrepared = false; private long currentPosition = 0; private SessionManagerListener<CastSession> sessionManagerListener; // endregion // region Listeners @OnClick(R.id.exo_play) public void onPlayButtonClicked(){ playbackState = PlaybackState.PLAYING; exoPlayButton.setVisibility(View.GONE); exoPauseButton.setVisibility(View.VISIBLE); resumeLocalVideo(); if(location == PlaybackLocation.REMOTE){ resumeRemoteVideo(); castInfoTextView.setText(String.format("Casting to %s", castSession.getCastDevice().getFriendlyName())); } } @OnClick(R.id.exo_pause) public void onPauseButtonClicked(){ playbackState = PlaybackState.PAUSED; exoPauseButton.setVisibility(View.GONE); exoPlayButton.setVisibility(View.VISIBLE); pauseLocalVideo(); if(location == PlaybackLocation.REMOTE){ pauseRemoteVideo(); castInfoTextView.setText(getString(R.string.cast_is_paused)); } } @OnClick(R.id.exo_replay) public void onReplayButtonClicked() { replayImageButton.setVisibility(View.GONE); exoButtonsFrameLayout.setVisibility(View.VISIBLE); switch (location){ case LOCAL: updateLocalVideoPosition(0); break; case REMOTE: updateLocalVideoPosition(0); pauseLocalVideo(); playRemoteVideo(0, true); break; default: break; } } // @OnClick(R.id.reload_btn) // public void onReloadButtonClicked() { //// errorLinearLayout.setVisibility(View.GONE); //// loadingImageView.setVisibility(View.VISIBLE); // // Call getVideoConfigCall = vimeoPlayerService.getVideoConfig(videoId); // calls.add(getVideoConfigCall); // getVideoConfigCall.enqueue(getVideoConfigCallback); // } private ExoPlayer.EventListener exoPlayerEventListener = new ExoPlayer.EventListener() { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { } @Override public void onLoadingChanged(boolean isLoading) { } @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { switch (playbackState){ case ExoPlayer.STATE_BUFFERING: case ExoPlayer.STATE_READY: replayImageButton.setVisibility(View.GONE); exoButtonsFrameLayout.setVisibility(View.VISIBLE); break; case ExoPlayer.STATE_ENDED: exoButtonsFrameLayout.setVisibility(View.GONE); replayImageButton.setVisibility(View.VISIBLE); break; default: break; } } @Override public void onPlayerError(ExoPlaybackException error) { } @Override public void onPositionDiscontinuity() { } }; private PlaybackControlView.VisibilityListener playbackControlViewVisibilityListener = new PlaybackControlView.VisibilityListener() { @Override public void onVisibilityChange(int visibility) { int orientation = getContext().getResources().getConfiguration().orientation; switch (orientation){ case ORIENTATION_PORTRAIT: if(visibility == View.GONE){ toolbar.animate().alpha(0.0f).setDuration(300); hidePortraitSystemUI(); } else { showPortraitSystemUI(); toolbar.animate().alpha(1.0f).setDuration(300); } break; case ORIENTATION_LANDSCAPE: if(visibility == View.GONE){ toolbar.animate().alpha(0.0f).setDuration(300); hideLandscapeSystemUI(); } else { showLandscapeSystemUI(); toolbar.animate().alpha(1.0f).setDuration(300); } break; default: break; } } }; private SeekBar.OnSeekBarChangeListener seekBarOnSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if(fromUser){ currentPosition = (long)(exoPlayer.getDuration() * (progress/1000.0D)); switch (location) { case LOCAL: updateLocalVideoPosition(currentPosition); break; case REMOTE: updateLocalVideoPosition(currentPosition); pauseLocalVideo(); castSession.getRemoteMediaClient().addListener(new RemoteMediaClient.Listener() { @Override public void onStatusUpdated() { if(isResumed()){ if(isRemoteVideoPlaying()){ playbackState = PlaybackState.PLAYING; resumeLocalVideo(); castInfoTextView.setText(String.format("Casting to %s", castSession.getCastDevice().getFriendlyName())); } else { // playbackState = PlaybackState.PAUSED; pauseLocalVideo(); // castInfoTextView.setText(getString(R.string.cast_is_paused)); if(playbackState == PlaybackState.PLAYING){ castInfoTextView.setText(getString(R.string.cast_is_loading)); } } castInfoTextView.setVisibility(View.VISIBLE); } } @Override public void onMetadataUpdated() { } @Override public void onQueueStatusUpdated() { } @Override public void onPreloadStatusUpdated() { } @Override public void onSendingRemoteMediaRequest() { } @Override public void onAdBreakStatusUpdated() { } }); updateRemoteVideoPosition(currentPosition); break; default: break; } } else { // playbackState = PlaybackState.PLAYING; } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }; // private RemoteMediaClient.Listener remoteMediaClientListener = new RemoteMediaClient.Listener() { // @Override // public void onStatusUpdated() { // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onStatusUpdated()"); // if(isResumed()){ // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onStatusUpdated() : isResumed()"); // // if(isRemoteVideoPlaying()){ // playbackState = PlaybackState.PLAYING; // // Timber.d("VideoDetailsFragment : loadRemoteMedia() : onStatusUpdated() : isResumed() : isRemoteVideoPlaying() : getFormattedPosition(position) - %s", getFormattedPosition(currentPosition)); // Timber.d("VideoDetailsFragment : loadRemoteMedia() : onStatusUpdated() : isResumed() : isRemoteVideoPlaying() : getFormattedPosition(castSession.getRemoteMediaClient().getApproximateStreamPosition()) - %s", getFormattedPosition(castSession.getRemoteMediaClient().getApproximateStreamPosition())); // // updateLocalVideoVolume(0.0f); // // if(!isLocalVideoPrepared) // // Prepare the player with the source. // exoPlayer.prepare(getMediaSource(videoUrl)); // //// currentPosition = (exoPlayer.getDuration() * (exoSeekBar.getProgress()/1000L)); //// currentPosition = exoPlayer.getCurrentPosition(); // currentPosition = castSession.getRemoteMediaClient().getApproximateStreamPosition(); // // updateLocalVideoPosition(currentPosition); // resumeLocalVideo(); // //// remoteMediaClient.removeListener(this); // castInfoTextView.setText(String.format("Casting to %s", castSession.getCastDevice().getFriendlyName())); //// remoteMediaClient.removeListener(this); // } else { //// playbackState = PlaybackState.PAUSED; // pauseLocalVideo(); // //// castInfoTextView.setText(getString(R.string.cast_is_paused)); // if(playbackState == PlaybackState.PLAYING){ // castInfoTextView.setText(getString(R.string.cast_is_loading)); // } // } // castInfoTextView.setVisibility(View.VISIBLE); // //// Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class); //// startActivity(intent); //// remoteMediaClient.removeListener(this); // } // } // // @Override // public void onMetadataUpdated() { // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onMetadataUpdated()"); // } // // @Override // public void onQueueStatusUpdated() { // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onQueueStatusUpdated()"); // } // // @Override // public void onPreloadStatusUpdated() { // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onPreloadStatusUpdated()"); // } // // @Override // public void onSendingRemoteMediaRequest() { // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onSendingRemoteMediaRequest()"); // } // // @Override // public void onAdBreakStatusUpdated() { // Timber.d("VideoDetailsFragment : remoteMediaClientListener : onAdBreakStatusUpdated()"); // } // }; // endregion // region Callbacks private Callback<VideoConfig> getVideoConfigCallback = new Callback<VideoConfig>() { @Override public void onResponse(Call<VideoConfig> call, Response<VideoConfig> response) { // loadingImageView.setVisibility(View.GONE); if (!response.isSuccessful()) { int responseCode = response.code(); switch (responseCode){ case 504: // 504 Unsatisfiable Request (only-if-cached) // errorTextView.setText("Can't load data.\nCheck your network connection."); // errorLinearLayout.setVisibility(View.VISIBLE); break; case 403: // Forbidden Snackbar.make(getActivity().findViewById(R.id.main_content), TrestleUtility.getFormattedText("Cannot play this video.", font, 16), Snackbar.LENGTH_LONG) .show(); break; } return; } VideoConfig videoConfig = response.body(); if (videoConfig != null) { videoUrl = videoConfig.getVideoUrl(); if (!TextUtils.isEmpty(videoUrl)) { playVideo(0); } } } @Override public void onFailure(Call<VideoConfig> call, Throwable t) { NetworkLogUtility.logFailure(call, t); if (!call.isCanceled()){ // loadingImageView.setVisibility(View.GONE); if(NetworkUtility.isKnownException(t)){ // errorTextView.setText("Can't load data.\nCheck your network connection."); // errorLinearLayout.setVisibility(View.VISIBLE); } } } }; // endregion // region Constructors public VideoDetailsFragment() { } // endregion // region Factory Methods public static VideoDetailsFragment newInstance(Bundle extras) { VideoDetailsFragment fragment = new VideoDetailsFragment(); fragment.setArguments(extras); return fragment; } public static VideoDetailsFragment newInstance() { VideoDetailsFragment fragment = new VideoDetailsFragment(); Bundle args = new Bundle(); fragment.setArguments(args); return fragment; } // endregion // region Lifecycle Methods @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // // Retain this fragment across configuration changes. setRetainInstance(true); if (getArguments() != null) { video = (Video) getArguments().get(LikedVideosFragment.KEY_VIDEO); // mTransitionName = getArguments().getString("TRANSITION_KEY"); } AccessToken token = LoopPrefs.getAccessToken(getActivity()); vimeoPlayerService = ServiceGenerator.createService( VimeoPlayerService.class, VimeoPlayerService.BASE_URL, new AuthorizedNetworkInterceptor(token)); setHasOptionsMenu(true); font = FontCache.getTypeface("Ubuntu-Medium.ttf", getContext()); castContext = CastContext.getSharedInstance(getContext()); castSession = castContext.getSessionManager().getCurrentCastSession(); playbackState = PlaybackState.PLAYING; if (castSession != null && castSession.isConnected()) { location = PlaybackLocation.REMOTE; } else { location = PlaybackLocation.LOCAL; } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_video_details, container, false); unbinder = ButterKnife.bind(this, rootView); return rootView; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar); ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(""); } if (video != null) { setUpExoPlayer(); setUpSimpleExoPlayerView(); setUpThumbnail(); setUpCastButton(); exoSeekBar.setOnSeekBarChangeListener(seekBarOnSeekBarChangeListener); setupCastListener(); } int orientation = view.getResources().getConfiguration().orientation; switch (orientation){ case ORIENTATION_PORTRAIT: if (viewPager != null) { setupViewPager(viewPager); } tabLayout.setupWithViewPager(viewPager); tabLayout.setTabMode(TabLayout.MODE_FIXED); updateTabLayout(); showPortraitSystemUI(); break; case ORIENTATION_LANDSCAPE: showLandscapeSystemUI(); break; default: break; } VideoSavedState videoSavedState = getVideoSavedState(); if(videoSavedState != null && !TextUtils.isEmpty(videoSavedState.getVideoUrl())){ // loadingImageView.setVisibility(View.GONE); long currentPosition = videoSavedState.getCurrentPosition(); videoUrl = videoSavedState.getVideoUrl(); playbackState = videoSavedState.getPlaybackState(); castInfoTextView.setText(videoSavedState.getCastInfo()); castInfoTextView.setVisibility(View.VISIBLE); boolean autoPlay = false; switch (playbackState){ case PLAYING: autoPlay = true; break; case PAUSED: autoPlay = false; break; default: break; } switch (location) { case LOCAL: playLocalVideo(currentPosition, autoPlay); updateLocalVideoVolume(videoSavedState.getCurrentVolume()); break; case REMOTE: playLocalVideo(castSession.getRemoteMediaClient().getApproximateStreamPosition(), autoPlay); updateLocalVideoVolume(videoSavedState.getCurrentVolume()); break; default: break; } } else { Call getVideoConfigCall = vimeoPlayerService.getVideoConfig(video.getId()); calls.add(getVideoConfigCall); getVideoConfigCall.enqueue(getVideoConfigCallback); } } @Override public void onResume() { super.onResume(); castContext.getSessionManager().addSessionManagerListener( sessionManagerListener, CastSession.class); if (castSession != null && castSession.isConnected()) { location = PlaybackLocation.REMOTE; } else { location = PlaybackLocation.LOCAL; } if(!isLocalVideoPlaying() && playbackState == PlaybackState.PLAYING) { switch (location){ case LOCAL: resumeLocalVideo(); break; case REMOTE: updateLocalVideoPosition(castSession.getRemoteMediaClient().getApproximateStreamPosition()); resumeLocalVideo(); break; default: break; } } } @Override public void onPause() { super.onPause(); castContext.getSessionManager().removeSessionManagerListener( sessionManagerListener, CastSession.class); switch (location) { case LOCAL: pauseLocalVideo(); break; case REMOTE: break; default: break; } } @Override public void onDestroyView() { super.onDestroyView(); if(!TextUtils.isEmpty(videoUrl)){ VideoSavedState videoSavedState = new VideoSavedState(); videoSavedState.setVideoUrl(videoUrl); videoSavedState.setCurrentPosition(exoPlayer.getCurrentPosition()); videoSavedState.setCurrentVolume(exoPlayer.getVolume()); videoSavedState.setPlaybackState(playbackState); videoSavedState.setCastInfo(castInfoTextView.getText().toString()); setVideoSavedState(videoSavedState); } removeListeners(); unbinder.unbind(); } @Override public void onDestroy() { super.onDestroy(); exoPlayer.release(); } // endregion @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.video_details_menu, menu); } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.share: if (video != null) { // EventLogger.fire(ProductShareEvent.start(mProduct.getId())); Intent sendIntent = new Intent(Intent.ACTION_SEND); sendIntent.setType("text/plain"); sendIntent.putExtra(Intent.EXTRA_TEXT, String.format("I found this on Loop. Check it out.\n\n%s\n\n%s", video.getName(), video.getLink())); String title = getResources().getString(R.string.share_this_video); Intent chooser = Intent.createChooser(sendIntent, title); if (sendIntent.resolveActivity(getActivity().getPackageManager()) != null) { startActivityForResult(chooser, VIDEO_SHARE_REQUEST_CODE); } } return true; default: break; } return super.onOptionsItemSelected(item); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case VIDEO_SHARE_REQUEST_CODE: if (resultCode == Activity.RESULT_OK) { if (video != null) { // EventLogger.fire(ProductShareEvent.submit(mProduct.getId())); } } else if (resultCode == Activity.RESULT_CANCELED) { } break; default: break; } } // region Helper Methods private void setUpExoPlayer(){ // Create a default TrackSelector TrackSelector trackSelector = createTrackSelector(); // Create a default LoadControl LoadControl loadControl = new DefaultLoadControl(); // Create the player exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl); resumeLocalVideo(); exoPlayer.addListener(exoPlayerEventListener); } private void setUpThumbnail() { String thumbnailUrl = video.getThumbnailUrl(); if (!TextUtils.isEmpty(thumbnailUrl)) { Glide.with(getActivity()) .load(thumbnailUrl) // .placeholder(R.drawable.ic_placeholder) // .error(R.drawable.ic_error) .into(thumbnailImageView); } } private void setUpSimpleExoPlayerView(){ simpleExoPlayerView.setPlayer(exoPlayer); simpleExoPlayerView.setControllerVisibilityListener(playbackControlViewVisibilityListener); } // This snippet hides the system bars. private void hidePortraitSystemUI() { final View decorView = getActivity().getWindow().getDecorView(); // Set the IMMERSIVE flag. // Set the content to appear under the system bars so that the content // doesn't resize when the system bars hide and show. decorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar | View.SYSTEM_UI_FLAG_IMMERSIVE); } // This snippet shows the system bars. It does this by removing all the flags // except for the ones that make the content appear under the system bars. private void showPortraitSystemUI() { final View decorView = getActivity().getWindow().getDecorView(); decorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } // This snippet hides the system bars. private void hideLandscapeSystemUI() { final View decorView = getActivity().getWindow().getDecorView(); // Set the IMMERSIVE flag. // Set the content to appear under the system bars so that the content // doesn't resize when the system bars hide and show. decorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar | View.SYSTEM_UI_FLAG_IMMERSIVE); } // This snippet shows the system bars. It does this by removing all the flags // except for the ones that make the content appear under the system bars. private void showLandscapeSystemUI() { final View decorView = getActivity().getWindow().getDecorView(); decorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } public void setVideoSavedState(VideoSavedState videoSavedState) { this.videoSavedState = videoSavedState; } public VideoSavedState getVideoSavedState() { return videoSavedState; } private TrackSelector createTrackSelector(){ // Create a default TrackSelector // Measures bandwidth during playback. Can be null if not required. BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveVideoTrackSelection.Factory(bandwidthMeter); TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); return trackSelector; } private MediaSource getMediaSource(String videoUrl){ DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); // Produces DataSource instances through which media data is loaded. // DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getContext(), Util.getUserAgent(getContext(), "Loop"), bandwidthMeter); DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(LoopApplication.getInstance().getApplicationContext(), Util.getUserAgent(LoopApplication.getInstance().getApplicationContext(), "Loop"), bandwidthMeter); // Produces Extractor instances for parsing the media data. ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); // This is the MediaSource representing the media to be played. MediaSource mediaSource = new ExtractorMediaSource(Uri.parse(videoUrl), dataSourceFactory, extractorsFactory, null, null); // Loops the video indefinitely. // LoopingMediaSource loopingSource = new LoopingMediaSource(mediaSource); return mediaSource; } private void removeListeners() { exoPlayer.removeListener(exoPlayerEventListener); simpleExoPlayerView.setControllerVisibilityListener(null); exoSeekBar.setOnSeekBarChangeListener(null); } private void setupViewPager(ViewPager viewPager) { Adapter adapter = new Adapter(getChildFragmentManager()); Bundle bundle = new Bundle(); bundle.putParcelable(KEY_VIDEO, video); VideoDetailsInfoFragment videoDetailsInfoFragment = VideoDetailsInfoFragment.newInstance(bundle); adapter.addFragment(videoDetailsInfoFragment, getString(R.string.info)); Bundle bundle2 = new Bundle(); bundle2.putLong(KEY_VIDEO_ID, video.getId()); RelatedVideosFragment relatedVideosFragment = RelatedVideosFragment.newInstance(bundle2); adapter.addFragment(relatedVideosFragment, getString(R.string.related_videos)); viewPager.setAdapter(adapter); } private void updateTabLayout(){ ViewGroup vg = (ViewGroup) tabLayout.getChildAt(0); int tabsCount = vg.getChildCount(); for (int j = 0; j < tabsCount; j++) { ViewGroup vgTab = (ViewGroup) vg.getChildAt(j); int tabChildsCount = vgTab.getChildCount(); for (int i = 0; i < tabChildsCount; i++) { View tabViewChild = vgTab.getChildAt(i); if (tabViewChild instanceof TextView) { ((TextView) tabViewChild).setTypeface(font); } } } } private void setUpCastButton(){ Drawable remoteIndicatorDrawable = getRemoteIndicatorDrawable(); DrawableCompat.setTint(remoteIndicatorDrawable, ContextCompat.getColor(getContext(), android.R.color.white)); mediaRouteButton.setRemoteIndicatorDrawable(remoteIndicatorDrawable); CastButtonFactory.setUpMediaRouteButton(getContext().getApplicationContext(), mediaRouteButton); } private Drawable getRemoteIndicatorDrawable(){ Context castContext = new ContextThemeWrapper(getContext(), android.support.v7.mediarouter.R.style.Theme_MediaRouter); TypedArray a = castContext.obtainStyledAttributes(null, android.support.v7.mediarouter.R.styleable.MediaRouteButton, android.support.v7.mediarouter.R.attr.mediaRouteButtonStyle, 0); Drawable remoteIndicatorDrawable = a.getDrawable( android.support.v7.mediarouter.R.styleable.MediaRouteButton_externalRouteEnabledDrawable); a.recycle(); return remoteIndicatorDrawable; } private void setupCastListener() { sessionManagerListener = new SessionManagerListener<CastSession>() { @Override public void onSessionEnded(CastSession session, int error) { onApplicationDisconnected(); } @Override public void onSessionResumed(CastSession session, boolean wasSuspended) { onApplicationConnected(session); } @Override public void onSessionResumeFailed(CastSession session, int error) { onApplicationDisconnected(); } @Override public void onSessionStarted(CastSession session, String sessionId) { onApplicationConnected(session); } @Override public void onSessionStartFailed(CastSession session, int error) { onApplicationDisconnected(); } @Override public void onSessionStarting(CastSession session) { } @Override public void onSessionEnding(CastSession session) { } @Override public void onSessionResuming(CastSession session, String sessionId) { } @Override public void onSessionSuspended(CastSession session, int reason) { } private void onApplicationConnected(CastSession session) { castSession = session; // castSession.getRemoteMediaClient().addListener(remoteMediaClientListener); location = PlaybackLocation.REMOTE; pauseLocalVideo(); thumbnailImageView.animate().alpha(1.0f).setDuration(1000); castInfoTextView.setText(getString(R.string.connecting_to_receiver)); castInfoTextView.setVisibility(View.VISIBLE); if(!TextUtils.isEmpty(videoUrl)){ if (playbackState == PlaybackState.PLAYING) { loadRemoteMedia(exoPlayer.getCurrentPosition(), true); } else if(playbackState == PlaybackState.PAUSED){ loadRemoteMedia(exoPlayer.getCurrentPosition(), false); } // else { // playbackState = PlaybackState.IDLE; // } } } private void onApplicationDisconnected() { castSession = null; location = PlaybackLocation.LOCAL; // playbackState = PlaybackState.IDLE; thumbnailImageView.animate().alpha(0.0f).setDuration(1000); castInfoTextView.setVisibility(View.GONE); updateLocalVideoVolume(1.0f); } }; } private void loadRemoteMedia(final long position, boolean autoPlay) { currentPosition = position; final RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient(); remoteMediaClient.addListener(new RemoteMediaClient.Listener() { @Override public void onStatusUpdated() { if(isResumed()){ if(isRemoteVideoPlaying()) { playbackState = PlaybackState.PLAYING; updateLocalVideoVolume(0.0f); if(!isLocalVideoPrepared) // Prepare the player with the source. exoPlayer.prepare(getMediaSource(videoUrl)); updateLocalVideoPosition(position); resumeLocalVideo(); castInfoTextView.setText(String.format("Casting to %s", castSession.getCastDevice().getFriendlyName())); remoteMediaClient.removeListener(this); } else { if(playbackState == PlaybackState.PLAYING){ castInfoTextView.setText(getString(R.string.cast_is_loading)); } } castInfoTextView.setVisibility(View.VISIBLE); // Intent intent = new Intent(LocalPlayerActivity.this, ExpandedControlsActivity.class); // startActivity(intent); // remoteMediaClient.removeListener(this); } } @Override public void onMetadataUpdated() { } @Override public void onQueueStatusUpdated() { } @Override public void onPreloadStatusUpdated() { } @Override public void onSendingRemoteMediaRequest() { } @Override public void onAdBreakStatusUpdated() { } }); castSession.getRemoteMediaClient().load(buildMediaInfo(), autoPlay, position); } private MediaInfo buildMediaInfo() { MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, video.getUser().getName()); movieMetadata.putString(MediaMetadata.KEY_TITLE, video.getName()); movieMetadata.addImage(new WebImage(Uri.parse(video.getThumbnailUrl()))); return new MediaInfo.Builder(videoUrl) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType("videos/mp4") .setMetadata(movieMetadata) .setStreamDuration(exoPlayer.getDuration() * 1000) .build(); } private void playVideo(long position){ switch (location) { case LOCAL: playLocalVideo(position, true); break; case REMOTE: playRemoteVideo(position, true); break; default: break; } } private void playLocalVideo(long position, boolean autoPlay){ if(location == PlaybackLocation.LOCAL){ thumbnailImageView.animate().alpha(0.0f).setDuration(1000); castInfoTextView.setVisibility(View.GONE); } updateLocalVideoPosition(position); // Prepare the player with the source. exoPlayer.prepare(getMediaSource(videoUrl)); isLocalVideoPrepared = true; if(!autoPlay) pauseLocalVideo(); } private void playRemoteVideo(long position, boolean autoPlay){ updateRemoteVideoPosition(position); loadRemoteMedia(position, autoPlay); } private void updateVideoPosition(int position){ switch (location) { case LOCAL: updateLocalVideoPosition(position); break; case REMOTE: updateRemoteVideoPosition(position); break; default: break; } } private void updateLocalVideoPosition(long position){ exoPlayer.seekTo(position); } private void updateRemoteVideoPosition(long position){ castSession.getRemoteMediaClient().seek(position, RemoteMediaClient.RESUME_STATE_UNCHANGED); } private boolean isVideoPlaying(){ switch (location) { case LOCAL: return isLocalVideoPlaying(); case REMOTE: return isRemoteVideoPlaying(); default: return false; } } private boolean isLocalVideoPlaying(){ return exoPlayer.getPlayWhenReady(); } private boolean isRemoteVideoPlaying(){ if(castSession != null){ RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient(); return remoteMediaClient.isPlaying(); } return false; } private void resumeVideo(){ switch (location) { case LOCAL: resumeLocalVideo(); break; case REMOTE: resumeRemoteVideo(); break; default: break; } } private void resumeLocalVideo(){ exoPlayer.setPlayWhenReady(true); } private void resumeRemoteVideo(){ if(castSession != null){ RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient(); if(remoteMediaClient != null){ remoteMediaClient.play(); } } } private void pauseVideo(){ switch (location) { case LOCAL: pauseLocalVideo(); break; case REMOTE: pauseRemoteVideo(); break; default: break; } } private void pauseLocalVideo(){ exoPlayer.setPlayWhenReady(false); } private void pauseRemoteVideo(){ if(castSession != null){ RemoteMediaClient remoteMediaClient = castSession.getRemoteMediaClient(); if(remoteMediaClient != null){ remoteMediaClient.pause(); } } } private void updateVideoVolume(float volume){ switch (location) { case LOCAL: updateLocalVideoVolume(volume); break; case REMOTE: updateRemoteVideoVolume(volume); break; default: break; } } private void updateLocalVideoVolume(float volume){ exoPlayer.setVolume(volume); } private void updateRemoteVideoVolume(float volume){ } // endregion // region Inner Classes public static class Adapter extends FragmentPagerAdapter { private final List<Fragment> fragments = new ArrayList<>(); private final List<String> fragmentTitles = new ArrayList<>(); public Adapter(FragmentManager fm) { super(fm); } public void addFragment(Fragment fragment, String title) { fragments.add(fragment); fragmentTitles.add(title); } @Override public Fragment getItem(int position) { return fragments.get(position); } @Override public int getCount() { return fragments.size(); } @Override public CharSequence getPageTitle(int position) { return fragmentTitles.get(position); } } /** * indicates whether we are doing a local or a remote playback */ public enum PlaybackLocation { LOCAL, REMOTE } /** * List of various states that we can be in */ public enum PlaybackState { PLAYING, PAUSED, BUFFERING, IDLE } // endregion }