/* * This file is part of Popcorn Time. * * Popcorn Time is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Popcorn Time is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Popcorn Time. If not, see <http://www.gnu.org/licenses/>. */ package pct.droid.tv.fragments; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Html; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import com.github.sv244.torrentstream.StreamStatus; import com.github.sv244.torrentstream.Torrent; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.lang.ref.WeakReference; import butterknife.Bind; import butterknife.ButterKnife; import de.greenrobot.event.EventBus; import pct.droid.base.content.preferences.Prefs; import pct.droid.base.fragments.BaseVideoPlayerFragment; import pct.droid.base.providers.media.models.Media; import pct.droid.base.subs.Caption; import pct.droid.base.torrent.StreamInfo; import pct.droid.base.utils.PrefUtils; import pct.droid.base.widget.StrokedTextView; import pct.droid.tv.R; import pct.droid.tv.activities.PTVMediaDetailActivity; import pct.droid.tv.events.ConfigureSubtitleEvent; import pct.droid.tv.events.PausePlaybackEvent; import pct.droid.tv.events.PlaybackProgressChangedEvent; import pct.droid.tv.events.ScaleVideoEvent; import pct.droid.tv.events.SeekBackwardEvent; import pct.droid.tv.events.SeekForwardEvent; import pct.droid.tv.events.StartPlaybackEvent; import pct.droid.tv.events.StreamProgressChangedEvent; import pct.droid.tv.events.ToggleSubtitleEvent; import pct.droid.tv.events.UpdatePlaybackStateEvent; public class PTVVideoPlayerFragment extends BaseVideoPlayerFragment { @Bind(R.id.video_surface) SurfaceView mVideoSurface; @Bind(R.id.subtitle_text) StrokedTextView mSubtitleText; @Bind(R.id.progress_indicator) ProgressBar mProgressIndicator; private boolean mIsSubtitleEnabled = false; private boolean mMediaSessionMetadataApplied = false; private Handler mDisplayHandler; private MediaSession mMediaSession; private StreamInfo mStreamInfo; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mStreamInfo = ((PTVVideoPlayerFragment.Callback) getActivity()).getInfo(); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_videoplayer, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ButterKnife.bind(this, view); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setRetainInstance(true); mVideoSurface.setVisibility(View.VISIBLE); mSubtitleText.setVisibility(View.INVISIBLE); mSubtitleText.setText(""); mSubtitleText.setTextColor(PrefUtils.get(getActivity(), Prefs.SUBTITLE_COLOR, Color.WHITE)); mSubtitleText.setStrokeColor(PrefUtils.get(getActivity(), Prefs.SUBTITLE_STROKE_COLOR, Color.BLACK)); mSubtitleText.setStrokeWidth(TypedValue.COMPLEX_UNIT_DIP, PrefUtils.get(getActivity(), Prefs.SUBTITLE_STROKE_WIDTH, 2)); } @Override public void onResume() { super.onResume(); if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this); } } @Override public void onPause() { super.onPause(); if (!isMediaSessionActive()) { EventBus.getDefault().unregister(this); } } @Override public void onDestroy() { super.onDestroy(); if (!isMediaSessionActive() && mMediaSession != null) { mMediaSession.release(); } } @Override protected SurfaceView getVideoSurface() { return mVideoSurface; } @Override protected void onErrorEncountered() { /* Encountered Error, exit player with a message */ AlertDialog dialog = new AlertDialog.Builder(getActivity()) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { getActivity().finish(); } }) .setTitle(R.string.encountered_error_title) .setMessage(R.string.encountered_error) .create(); dialog.show(); } @Override protected void showPlayerInfo(String text) { } @Override protected void showOverlay() { } @Override protected void loadMedia() { super.loadMedia(); activateMediaSession(); } @Override protected void updatePlayPauseState() { EventBus.getDefault().post(new ToggleSubtitleEvent(mIsSubtitleEnabled)); EventBus.getDefault().post(new UpdatePlaybackStateEvent(isPlaying())); if (mMediaSession != null) { PlaybackState.Builder builder = new PlaybackState.Builder(); builder.setActions(isPlaying() ? PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PAUSE : PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PLAY); builder.setState( isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, getCurrentTime(), 1.0f); mMediaSession.setPlaybackState(builder.build()); } } @Override public void onHardwareAccelerationError() { showErrorDialog( getString(R.string.hardware_acceleration_error_title), getString(R.string.hardware_acceleration_error_message)); } @Override protected void showTimedCaptionText(final Caption text) { if (mDisplayHandler == null) mDisplayHandler = new Handler(Looper.getMainLooper()); mDisplayHandler.post(new Runnable() { @Override public void run() { if (text == null) { if (mSubtitleText.getText().length() > 0) { mSubtitleText.setText(""); mSubtitleText.setVisibility(View.INVISIBLE); } return; } SpannableStringBuilder styledString = (SpannableStringBuilder) Html.fromHtml(text.content); ForegroundColorSpan[] toRemoveSpans = styledString.getSpans(0, styledString.length(), ForegroundColorSpan.class); for (ForegroundColorSpan remove : toRemoveSpans) { styledString.removeSpan(remove); } if (!mSubtitleText.getText().toString().equals(styledString.toString())) { mSubtitleText.setText(styledString); mSubtitleText.setVisibility(View.VISIBLE); } } }); } @Override protected void setProgressVisible(boolean visible) { if(mProgressIndicator.getVisibility() == View.VISIBLE && visible) return; if(mProgressIndicator.getVisibility() == View.GONE && !visible) return; mProgressIndicator.setVisibility(visible ? View.VISIBLE : View.GONE); } @Override protected void onProgressChanged(long currentTime, long duration) { EventBus.getDefault().post(new PlaybackProgressChangedEvent(currentTime, duration)); if (mMediaSession != null && duration > 0) { PlaybackState.Builder builder = new PlaybackState.Builder(); builder.setActions(isPlaying() ? PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PAUSE : PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PLAY); builder.setState( isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, getCurrentTime(), 1.0f); mMediaSession.setPlaybackState(builder.build()); if (!mMediaSessionMetadataApplied) { setupMediaMetadata(); } } } @Override public void onPlaybackEndReached() { EventBus.getDefault().post(new UpdatePlaybackStateEvent(false)); mMediaSession.setActive(false); mMediaSession.release(); } @Override public void onStreamReady(Torrent torrent) { } @Override public void onStreamProgress(Torrent torrent, StreamStatus streamStatus) { super.onStreamProgress(torrent, streamStatus); EventBus.getDefault().post(new StreamProgressChangedEvent(getStreamerProgress())); } @Override public void onStreamError(Torrent torrent, Exception e) { showErrorDialog(getString(R.string.torrent_failed), e.getMessage()); } private void showErrorDialog(String title, String message) { AlertDialog dialog = new AlertDialog.Builder(getActivity()) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { disableHardwareAcceleration(); loadMedia(); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { getActivity().finish(); } }) .setTitle(title) .setMessage(message) .create(); if (!getActivity().isFinishing()) dialog.show(); } @Override protected void updateSubtitleSize(int size) { mSubtitleText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, size); } @Override protected void onSubtitleEnabledStateChanged(boolean enabled) { super.onSubtitleEnabledStateChanged(enabled); mIsSubtitleEnabled = enabled; mSubtitleText.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); EventBus.getDefault().post(new ToggleSubtitleEvent(mIsSubtitleEnabled)); } @Override public void startBeamPlayerActivity() { } @SuppressWarnings("unused") public void onEventMainThread(StartPlaybackEvent event) { togglePlayPause(); if (mMediaSession != null) { PlaybackState.Builder builder = new PlaybackState.Builder(); builder.setActions(PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_PLAY_PAUSE); builder.setState(PlaybackState.STATE_PLAYING, getCurrentTime(), 1.0f); mMediaSession.setPlaybackState(builder.build()); } } @SuppressWarnings("unused") public void onEventMainThread(PausePlaybackEvent event) { togglePlayPause(); if (mMediaSession != null) { PlaybackState.Builder builder = new PlaybackState.Builder(); builder.setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PLAY_PAUSE); builder.setState(PlaybackState.STATE_PAUSED, getCurrentTime(), 1.0f); mMediaSession.setPlaybackState(builder.build()); } } @SuppressWarnings("unused") public void onEventMainThread(SeekBackwardEvent event) { if (!isSeeking()) { setSeeking(true); seek(event.getSeek()); setSeeking(false); } } @SuppressWarnings("unused") public void onEventMainThread(SeekForwardEvent event) { if (!isSeeking()) { setSeeking(true); seek(event.getSeek()); setSeeking(false); } } @SuppressWarnings("unused") public void onEventMainThread(ScaleVideoEvent event) { scaleClick(); } @SuppressWarnings("unused") public void onEventMainThread(ConfigureSubtitleEvent event) { subsClick(); } private void activateMediaSession() { if (null != mMediaSession) return; Activity activity = getActivity(); mMediaSession = new MediaSession(activity, "PopcornTimeMediaSession"); mMediaSession.setCallback(new PopcornTimeMediaSessionCallback(activity)); mMediaSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); mMediaSession.setActive(true); activity.setMediaController(new MediaController(activity, mMediaSession.getSessionToken())); } private void setupMediaMetadata() { mMediaSessionMetadataApplied = false; if(mStreamInfo.getMedia() == null) return; final MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder(); metadataBuilder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, mStreamInfo.getMedia().videoId); metadataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, getDuration()); if (mStreamInfo.isShow()) { metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, mStreamInfo.getShowTitle()); metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, mStreamInfo.getShowEpisodeTitle()); metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, mStreamInfo.getShowTitle()); } else { metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, mStreamInfo.getTitle()); metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, mStreamInfo.getTitle()); } String imageUrl = mStreamInfo.getMedia().image; if (imageUrl != null && !imageUrl.equals("")) { metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, imageUrl); metadataBuilder.putString(MediaMetadata.METADATA_KEY_ART_URI, imageUrl); Picasso.with(getActivity()).load(imageUrl).into(new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap); mMediaSession.setMetadata(metadataBuilder.build()); mMediaSessionMetadataApplied = true; } @Override public void onBitmapFailed(Drawable errorDrawable) { mMediaSession.setMetadata(metadataBuilder.build()); mMediaSessionMetadataApplied = true; } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { } }); } else { mMediaSession.setMetadata(metadataBuilder.build()); mMediaSessionMetadataApplied = true; } } public boolean isMediaSessionActive() { return mMediaSession != null && mMediaSession.isActive(); } public void deactivateMediaSession() { if (mMediaSession != null && mMediaSession.isActive()) mMediaSession.setActive(false); } private class PopcornTimeMediaSessionCallback extends MediaSession.Callback { private final WeakReference<Context> contextReference; public PopcornTimeMediaSessionCallback(Context context) { contextReference = new WeakReference<>(context); } @Override public void onPlay() { EventBus.getDefault().post(new StartPlaybackEvent()); } @Override public void onPause() { EventBus.getDefault().post(new PausePlaybackEvent()); } @Override public void onFastForward() { EventBus.getDefault().post(new SeekForwardEvent()); } @Override public void onRewind() { EventBus.getDefault().post(new SeekForwardEvent()); } @Override public void onPlayFromMediaId(@NonNull String mediaId, @NonNull Bundle extras) { super.onPlayFromMediaId(mediaId, extras); if (contextReference.get() == null) return; Context context = contextReference.get(); Media media = extras.getParcelable(PTVMediaDetailActivity.EXTRA_ITEM); if (media == null) return; Intent detailIntent = PTVMediaDetailActivity.buildIntent( context, media); detailIntent.setAction(media.videoId); context.startActivity(detailIntent); } @Override public boolean onMediaButtonEvent(@NonNull Intent intent) { if (!intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) return false; KeyEvent keyEvent = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) return false; if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE) { EventBus.getDefault().post(new PausePlaybackEvent()); return true; } if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { if (isPlaying()) { EventBus.getDefault().post(new PausePlaybackEvent()); } else { EventBus.getDefault().post(new StartPlaybackEvent()); } return true; } return false; } } }