/* * 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.base.fragments; import android.content.Context; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import com.github.sv244.torrentstream.StreamStatus; import com.github.sv244.torrentstream.Torrent; import com.github.sv244.torrentstream.listeners.TorrentListener; import java.util.Map; import hugo.weaving.DebugLog; import pct.droid.base.R; import pct.droid.base.activities.TorrentActivity; import pct.droid.base.beaming.server.BeamServer; import pct.droid.base.beaming.server.BeamServerService; import pct.droid.base.content.preferences.Prefs; import pct.droid.base.providers.media.models.Episode; import pct.droid.base.providers.media.models.Media; import pct.droid.base.providers.media.models.Movie; import pct.droid.base.providers.subs.SubsProvider; import pct.droid.base.subs.SubtitleDownloader; import pct.droid.base.subs.TimedTextObject; import pct.droid.base.torrent.StreamInfo; import pct.droid.base.torrent.TorrentService; import pct.droid.base.utils.PrefUtils; import pct.droid.base.utils.ThreadUtils; /** * This fragment handles starting a stream of a torrent. * <p/> * <p/> * It does multiple things: * <p/> * <pre> * 1. Downloads torrent file * 2. Downloads subtitles file * (1 and 2 happen asynchronously. Generally the torrent will take much longer than downloading subs) * * 3. Starts downloading (buffering) the torrent * 4. Starts the Video activity * </pre> * <p/> * <p/> * <p/> * //todo: most of this logic should probably be factored out into its own service at some point */ public abstract class BaseStreamLoadingFragment extends Fragment implements TorrentListener, SubtitleDownloader.ISubtitleDownloaderListener, SubsProvider.Callback { protected FragmentListener mCallback; private SubsProvider mSubsProvider; protected boolean mPlayingExternal = false; protected Boolean mPlayerStarted = false; private Boolean mHasSubs = false; private TorrentService mService; protected StreamInfo mStreamInfo; private State mState; private enum SubsStatus {SUCCESS, FAILURE, DOWNLOADING} private SubsStatus mSubsStatus = SubsStatus.DOWNLOADING; private String mSubtitleLanguage = null, mVideoLocation = ""; public enum State { UNINITIALISED, WAITING_TORRENT, WAITING_SUBTITLES, BUFFERING, STREAMING, ERROR; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { mStreamInfo = mCallback.getStreamInformation(); if (mStreamInfo == null) { getActivity().finish(); return; } loadSubtitles(); } }); if (!(getActivity() instanceof TorrentActivity)) { throw new IllegalStateException("Parent activity is not a TorrentBaseActivity"); } } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof FragmentListener) mCallback = (FragmentListener) context; } public void onTorrentServiceConnected() { if(getActivity() == null) return; mService = ((TorrentActivity)getActivity()).getTorrentService(); if(mService != null) { mService.addListener(this); startStream(); } } public void onTorrentServiceDisconnected() { if(mService != null) { mService.removeListener(this); } } @Override public void onPause() { super.onPause(); if (null != mService) { mService.removeListener(this); } } /** * Update the view based on a state. * * @param state * @param extra - an optional extra piece of data relating to the state, such as an error message, or status data */ protected abstract void updateView(State state, Object extra); /** * Start the internal player for a streaming torrent * * @param location * @param resumePosition */ protected abstract void startPlayerActivity(String location, int resumePosition); @DebugLog protected void setState(final State state) { setState(state, null); } @DebugLog protected void setState(final State state, final Object extra) { mState = state; ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { updateView(state, extra); } }); } /** * Starts the player for a torrent stream. * <p/> * Will either start an external player, or the internal one */ @DebugLog private void startPlayer(String location) { if (mHasSubs && mSubsStatus == SubsStatus.DOWNLOADING) { ThreadUtils.runOnUiThread(new Runnable() { @Override public void run() { setState(State.WAITING_SUBTITLES); } }); return; } if (!mPlayerStarted) { mService.removeListener(BaseStreamLoadingFragment.this); startPlayerActivity(location, 0); mPlayerStarted = true; } } @Override public void onResume() { super.onResume(); if (mPlayerStarted) { BeamServer beamService = BeamServerService.getServer(); if (beamService != null) { beamService.stop(); } if(!mPlayingExternal) getActivity().onBackPressed(); } if(mService != null && mService.isStreaming() && mService.isReady()) { onStreamReady(mService.getCurrentTorrent()); } if(mState == null) { setState(State.WAITING_TORRENT); } else { setState(mState); } } /** * Starts the torrent service streaming a torrent url */ private void startStream() { if (null == mService) throw new IllegalStateException("Torrent service must be bound"); String torrentUrl = mStreamInfo.getTorrentUrl(); //if the torrent service is currently streaming another file, stop it. if (mService.isStreaming() && !mService.getCurrentTorrentUrl().equals(torrentUrl)) { mService.stopStreaming(); } else if(mService.isReady()) { onStreamReady(mService.getCurrentTorrent()); } //start streaming the new file mService.streamTorrent(torrentUrl); } /** * Stops the torrent service streaming */ @DebugLog public void cancelStream() { if (mService != null) { mService.stopStreaming(); } } @Override @DebugLog public void onStreamStarted(Torrent torrent) { setState(State.BUFFERING); } @Override @DebugLog public void onStreamError(Torrent torrent, final Exception e) { if (e.getMessage().equals("Write error")) { setState(State.ERROR, getString(R.string.error_files)); } else if (e.getMessage().equals("Torrent error")) { setState(State.ERROR, getString(R.string.torrent_failed)); } else { setState(State.ERROR, getString(R.string.unknown_error)); } } /** * Called when torrent buffering has reached 100% * * @param torrent */ @Override @DebugLog public void onStreamReady(Torrent torrent) { mVideoLocation = torrent.getVideoFile().toString(); startPlayer(mVideoLocation); } /** * Called when the torrent buffering status has been updated * * @param status */ @Override @DebugLog public void onStreamProgress(Torrent torrent, StreamStatus status) { if (!mVideoLocation.isEmpty()) { startPlayer(mVideoLocation); } setState(State.STREAMING, status); } @Override public void onStreamPrepared(Torrent torrent) { torrent.startDownload(); } @Override public void onStreamStopped() { } /** * Downloads the subs file */ private void loadSubtitles() { Media media = mStreamInfo.getMedia(); if (media == null) return; mSubsProvider = media.getSubsProvider(); if (mSubsProvider == null) return; if (mStreamInfo.isShow()) { mSubsProvider.getList((Episode) media, this); } else { mSubsProvider.getList((Movie) media, this); } } @Override public void onSuccess(Map<String, String> items) { Media media = mStreamInfo.getMedia(); media.subtitles = items; mSubsStatus = SubsStatus.SUCCESS; mHasSubs = false; if (media.subtitles == null || media.subtitles.size() == 0) return; if (mStreamInfo.getSubtitleLanguage() == null) { if (media.subtitles.containsKey(PrefUtils.get(getActivity(), Prefs.SUBTITLE_DEFAULT, SubsProvider.SUBTITLE_LANGUAGE_NONE))) { mStreamInfo.setSubtitleLanguage(PrefUtils.get(getActivity(), Prefs.SUBTITLE_DEFAULT, SubsProvider.SUBTITLE_LANGUAGE_NONE)); } else { mStreamInfo.setSubtitleLanguage(SubsProvider.SUBTITLE_LANGUAGE_NONE); } } if (mStreamInfo.getSubtitleLanguage() != null && !mStreamInfo.getSubtitleLanguage().equals(SubsProvider.SUBTITLE_LANGUAGE_NONE)) { mSubtitleLanguage = mStreamInfo.getSubtitleLanguage(); mSubsStatus = SubsStatus.DOWNLOADING; mHasSubs = true; SubtitleDownloader subtitleDownloader = new SubtitleDownloader(getActivity(), mStreamInfo, mSubtitleLanguage); subtitleDownloader.setSubtitleDownloaderListener(this); subtitleDownloader.downloadSubtitle(); } } @Override public void onFailure(Exception e) { mSubsStatus = SubsStatus.FAILURE; } @Override public void onSubtitleDownloadCompleted(boolean isSuccessful, TimedTextObject subtitleFile) { mSubsStatus = isSuccessful ? SubsStatus.SUCCESS : SubsStatus.FAILURE; } public interface FragmentListener { StreamInfo getStreamInformation(); } }