pct.droid.fragments.BaseVideoPlayerFragment.java Source code

Java tutorial

Introduction

Here is the source code for pct.droid.fragments.BaseVideoPlayerFragment.java

Source

/*
 * 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.fragments;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.DialogInterface;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.Toast;

import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import org.videolan.libvlc.EventHandler;
import org.videolan.libvlc.IVideoPlayer;
import org.videolan.libvlc.LibVLC;
import org.videolan.vlc.util.VLCInstance;
import org.videolan.vlc.util.WeakHandler;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;

import pct.droid.R;
import pct.droid.activities.VideoPlayerActivity;
import pct.droid.base.preferences.Prefs;
import pct.droid.base.providers.media.models.Media;
import pct.droid.base.providers.subs.SubsProvider;
import pct.droid.base.subs.Caption;
import pct.droid.base.subs.FormatSRT;
import pct.droid.base.subs.TimedTextObject;
import pct.droid.base.torrent.DownloadStatus;
import pct.droid.base.torrent.StreamInfo;
import pct.droid.base.torrent.TorrentService;
import pct.droid.base.utils.FileUtils;
import pct.droid.base.utils.LocaleUtils;
import pct.droid.base.utils.PrefUtils;
import pct.droid.base.utils.ThreadUtils;
import pct.droid.dialogfragments.FileSelectorDialogFragment;
import pct.droid.dialogfragments.StringArraySelectorDialogFragment;
import timber.log.Timber;

public abstract class BaseVideoPlayerFragment extends Fragment implements IVideoPlayer, TorrentService.Listener {

    private LibVLC mLibVLC;
    private String mLocation;

    private static final int SURFACE_BEST_FIT = 0;
    private static final int SURFACE_FIT_HORIZONTAL = 1;
    private static final int SURFACE_FIT_VERTICAL = 2;
    private static final int SURFACE_FILL = 3;
    private static final int SURFACE_16_9 = 4;
    private static final int SURFACE_4_3 = 5;
    private static final int SURFACE_ORIGINAL = 6;
    private int mCurrentSize = SURFACE_BEST_FIT;

    private int mStreamerProgress = 0;

    protected StreamInfo mStreamInfo;
    protected Media mMedia;
    private String mCurrentSubsLang = "no-subs";
    private TimedTextObject mSubs;
    private Caption mLastSub = null;
    private File mSubsFile = null;

    private boolean mEnded = false;
    private boolean mSeeking = false;
    private boolean mReadyToPlay = false;

    private int mVideoHeight;
    private int mVideoWidth;
    private int mVideoVisibleHeight;
    private int mVideoVisibleWidth;
    private int mSarNum;
    private int mSarDen;

    private boolean mDisabledHardwareAcceleration = false;
    private int mPreviousHardwareAccelerationMode;

    protected Callback mCallback;

    /**
     * Handle libvlc asynchronous events
     */
    private final Handler mVlcEventHandler = new VideoPlayerEventHandler(this);

    private SurfaceHolder mVideoSurfaceHolder;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        mStreamInfo = mCallback.getInfo();
        mMedia = mStreamInfo.getMedia();

        //start subtitles
        if (null != mStreamInfo.getSubtitleLanguage()) {
            mCurrentSubsLang = mStreamInfo.getSubtitleLanguage();
            if (!mCurrentSubsLang.equals("no-subs")) {
                mSubsFile = new File(SubsProvider.getStorageLocation(getActivity()),
                        mMedia.videoId + "-" + mCurrentSubsLang + ".srt");
                startSubtitles();
            }
        }

        mLibVLC = VLCInstance.get();
        mLibVLC.setHardwareAcceleration(
                PrefUtils.get(getActivity(), Prefs.HW_ACCELERATION, LibVLC.HW_ACCELERATION_AUTOMATIC));

        mVideoSurfaceHolder = getVideoSurface().getHolder();
        // Comment Chroma code out, experimental: will not work on all devices. To be added in settings later.
        //String chroma = mSettings.getString("chroma_format", "");
        //if(chroma.equals("YV12")) {
        //    mSurfaceHolder.setFormat(ImageFormat.YV12);
        //} else if (chroma.equals("RV16")) {
        //    mSurfaceHolder.setFormat(PixelFormat.RGB_565);
        //} else {
        mVideoSurfaceHolder.setFormat(PixelFormat.RGBX_8888);
        //}
        mVideoSurfaceHolder.addCallback(mSurfaceCallback);

        EventHandler em = EventHandler.getInstance();
        em.addHandler(mVlcEventHandler);

        Timber.d("Hardware acceleration mode: " + Integer.toString(mLibVLC.getHardwareAcceleration()));

        PrefUtils.save(getActivity(), VideoPlayerActivity.RESUME_POSITION, 0);

        if (mCallback.getService() != null)
            mCallback.getService().addListener(BaseVideoPlayerFragment.this);

        if (mReadyToPlay) {
            loadMedia();
        }
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (activity instanceof Callback)
            mCallback = (Callback) activity;
    }

    @Override
    public void onPause() {
        super.onPause();

        if (mLibVLC != null) {
            long currentTime = mLibVLC.getTime();
            PrefUtils.save(getActivity(), VideoPlayerActivity.RESUME_POSITION, currentTime);

            /*
             * Pausing here generates errors because the vout is constantly
             * trying to refresh itself every 80ms while the surface is not
             * accessible anymore.
             * To workaround that, we keep the last known position in the preferences
             */
            mLibVLC.stop();
        }

        getVideoSurface().setKeepScreenOn(false);
    }

    @Override
    public void onResume() {
        super.onResume();

        resumeVideo();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        EventHandler em = EventHandler.getInstance();
        em.removeHandler(mVlcEventHandler);

        // HW acceleration was temporarily disabled because of an error, restore the previous value.
        if (mDisabledHardwareAcceleration)
            mLibVLC.setHardwareAcceleration(mPreviousHardwareAccelerationMode);

        PrefUtils.save(getActivity(), VideoPlayerActivity.RESUME_POSITION, 0);
    }

    /**
     * External extras: - position (long) - position of the video to start with (in ms)
     */
    @SuppressWarnings({ "unchecked" })
    public void loadMedia() {
        if (mStreamInfo != null && null != mStreamInfo.getVideoLocation()) {
            mLocation = mStreamInfo.getVideoLocation();
        } else {
            mReadyToPlay = true;
            return;
        }

        getVideoSurface().setKeepScreenOn(true);

        if (mLibVLC == null || mLibVLC.isPlaying() || mLocation == null || mLocation.isEmpty()) {
            mReadyToPlay = true;
            return;
        }

        if (!mLocation.startsWith("http"))
            mLocation = LibVLC.PathToURI(mLocation);

        Timber.d("Trying to play file: %s", mLocation);

        mLibVLC.playMRL(mLocation);
        mEnded = false;

        long resumeTime = PrefUtils.get(getActivity(), VideoPlayerActivity.RESUME_POSITION, 0);
        if (resumeTime > 0) {
            setCurrentTime(resumeTime);
        }
    }

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     * abstract
     * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    protected abstract void setProgressVisible(boolean visible);

    protected abstract void showOverlay();

    protected abstract void showPlayerInfo(String info);

    protected abstract void onProgressChanged(long currentTime, long duration);

    protected abstract void updatePlayPauseState();

    protected abstract void onErrorEncountered();

    abstract void onHardwareAccelerationError();

    protected abstract void showTimedCaptionText(Caption text);

    protected abstract SurfaceView getVideoSurface();

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
     * vlc methods
     * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

    protected void setSeeking(boolean seeking) {
        mSeeking = seeking;
    }

    protected boolean isSeeking() {
        return mSeeking;
    }

    private void resumeVideo() {
        if (mLibVLC == null)
            return;

        long resumePosition = PrefUtils.get(getActivity(), VideoPlayerActivity.RESUME_POSITION, 0);
        long length = mLibVLC.getLength();
        if (length > resumePosition && resumePosition > 0) {
            setCurrentTime(resumePosition);
            PrefUtils.save(getActivity(), VideoPlayerActivity.RESUME_POSITION, 0);
        }
    }

    private void play() {
        mLibVLC.play();
        getVideoSurface().setKeepScreenOn(true);

        resumeVideo();
    }

    private void pause() {
        mLibVLC.pause();
        getVideoSurface().setKeepScreenOn(false);
    }

    public void togglePlayPause() {
        if (mLibVLC == null)
            return;

        if (mEnded) {
            loadMedia();
        }

        if (mLibVLC.isPlaying()) {
            pause();
        } else {
            play();
        }
        updatePlayPauseState();
    }

    public void seekForwardClick() {
        seek(10000);
    }

    public void seekBackwardClick() {
        seek(-10000);
    }

    public void scaleClick() {
        if (mCurrentSize < SURFACE_ORIGINAL) {
            mCurrentSize++;
        } else {
            mCurrentSize = 0;
        }
        changeSurfaceSize(true);
        showOverlay();
    }

    protected void disableHardwareAcceleration() {
        mDisabledHardwareAcceleration = true;
        mPreviousHardwareAccelerationMode = getHardwareAccelerationMode();

        setHardwareAccelerationMode(LibVLC.HW_ACCELERATION_DISABLED);
    }

    protected void setCurrentTime(long time) {
        if (time / getDuration() * 100 <= getStreamerProgress()) {
            mLibVLC.setTime(time);
        }
    }

    protected long getCurrentTime() {
        return mLibVLC.getTime();
    }

    protected long getDuration() {
        return mLibVLC.getLength();
    }

    public int getStreamerProgress() {
        return mStreamerProgress;
    }

    /**
     * Is a video currently playing with VLC
     *
     * @return
     */
    protected boolean isPlaying() {
        if (null != mLibVLC && mLibVLC.isPlaying())
            return true;
        return false;
    }

    // Required method for LibVLC
    public void eventHardwareAccelerationError() {
        EventHandler em = EventHandler.getInstance();
        em.callback(EventHandler.HardwareAccelerationError, new Bundle());
    }

    private void endReached() {
        mEnded = true;
        /* Exit player when reaching the end */
        // TODO: END, ASK USER TO CLOSE PLAYER?
    }

    public abstract void onPlaybackEndReached();

    public void subsClick() {
        if (mMedia != null && mMedia.subtitles != null) {
            if (getFragmentManager().findFragmentByTag("overlay_fragment") != null)
                return;
            String[] subtitles = mMedia.subtitles.keySet().toArray(new String[mMedia.subtitles.size()]);
            Arrays.sort(subtitles);
            final String[] adapterSubtitles = new String[subtitles.length + 2];
            System.arraycopy(subtitles, 0, adapterSubtitles, 1, subtitles.length);

            adapterSubtitles[0] = "no-subs";
            adapterSubtitles[adapterSubtitles.length - 1] = "custom";
            String[] readableNames = new String[adapterSubtitles.length];

            for (int i = 0; i < readableNames.length - 1; i++) {
                String language = adapterSubtitles[i];
                if (language.equals("no-subs")) {
                    readableNames[i] = getString(R.string.no_subs);
                } else {
                    Locale locale = LocaleUtils.toLocale(language);
                    readableNames[i] = locale.getDisplayName(locale);
                }
            }

            readableNames[readableNames.length - 1] = "Custom..";

            StringArraySelectorDialogFragment.showSingleChoice(getFragmentManager(), R.string.subtitles,
                    readableNames, Arrays.asList(adapterSubtitles).indexOf(mCurrentSubsLang),
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(final DialogInterface dialog, int position) {
                            if (position == adapterSubtitles.length - 1) {
                                FileSelectorDialogFragment.show(getChildFragmentManager(),
                                        new FileSelectorDialogFragment.Listener() {
                                            @Override
                                            public void onFileSelected(File f) {
                                                if (!f.getPath().endsWith(".srt")) {
                                                    Toast.makeText(getActivity(), R.string.unknown_error,
                                                            Toast.LENGTH_SHORT).show();
                                                    return;
                                                }
                                                FileSelectorDialogFragment.hide();
                                                mSubsFile = f;
                                                startSubtitles();
                                                dialog.dismiss();
                                            }
                                        });
                                return;
                            }
                            onSubtitleLanguageSelected(adapterSubtitles[position]);
                            dialog.dismiss();
                        }
                    });
        }
    }

    private void handleHardwareAccelerationError() {
        mLibVLC.stop();
        onHardwareAccelerationError();
    }

    protected int getHardwareAccelerationMode() {
        return mLibVLC.getHardwareAcceleration();
    }

    private void setHardwareAccelerationMode(int mode) {
        mLibVLC.setHardwareAcceleration(mode);
    }

    @Override
    public void setSurfaceLayout(int width, int height, int visible_width, int visible_height, int sar_num,
            int sar_den) {
        if (width * height == 0)
            return;

        // store video size
        mVideoHeight = height;
        mVideoWidth = width;
        mVideoVisibleHeight = visible_height;
        mVideoVisibleWidth = visible_width;
        mSarNum = sar_num;
        mSarDen = sar_den;
        ThreadUtils.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                changeSurfaceSize(false);

            }
        });
    }

    @Override
    public int configureSurface(Surface surface, final int width, final int height, final int hal) {
        return -1;
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
    private void changeSurfaceSize(boolean message) {
        int sw = getActivity().getWindow().getDecorView().getWidth();
        int sh = getActivity().getWindow().getDecorView().getHeight();

        double dw = sw, dh = sh;

        if (sw < sh) {
            dw = sh;
            dh = sw;
        }

        // sanity check
        if (dw * dh == 0 || mVideoWidth * mVideoHeight == 0) {
            Timber.e("Invalid surface size");
            return;
        }

        // compute the aspect ratio
        double ar, vw;
        if (mSarDen == mSarNum) {
            /* No indication about the density, assuming 1:1 */
            vw = mVideoVisibleWidth;
            ar = (double) mVideoVisibleWidth / (double) mVideoVisibleHeight;
        } else {
            /* Use the specified aspect ratio */
            vw = mVideoVisibleWidth * (double) mSarNum / mSarDen;
            ar = vw / mVideoVisibleHeight;
        }

        // compute the display aspect ratio
        double dar = dw / dh;

        switch (mCurrentSize) {
        case SURFACE_BEST_FIT:
            if (message)
                showPlayerInfo(getString(R.string.best_fit));
            if (dar < ar)
                dh = dw / ar;
            else
                dw = dh * ar;
            break;
        case SURFACE_FIT_HORIZONTAL:
            dh = dw / ar;
            if (message)
                showPlayerInfo(getString(R.string.fit_horizontal));
            break;
        case SURFACE_FIT_VERTICAL:
            dw = dh * ar;
            if (message)
                showPlayerInfo(getString(R.string.fit_vertical));
            break;
        case SURFACE_FILL:
            if (message)
                showPlayerInfo(getString(R.string.fill));
            break;
        case SURFACE_16_9:
            if (message)
                showPlayerInfo("16:9");
            ar = 16.0 / 9.0;
            if (dar < ar)
                dh = dw / ar;
            else
                dw = dh * ar;
            break;
        case SURFACE_4_3:
            if (message)
                showPlayerInfo("4:3");
            ar = 4.0 / 3.0;
            if (dar < ar)
                dh = dw / ar;
            else
                dw = dh * ar;
            break;
        case SURFACE_ORIGINAL:
            if (message)
                showPlayerInfo(getString(R.string.original_size));
            dh = mVideoVisibleHeight;
            dw = vw;
            break;
        }

        // force surface buffer size
        mVideoSurfaceHolder.setFixedSize(mVideoWidth, mVideoHeight);

        // set display size
        ViewGroup.LayoutParams lp = getVideoSurface().getLayoutParams();
        lp.width = (int) Math.ceil(dw * mVideoWidth / mVideoVisibleWidth);
        lp.height = (int) Math.ceil(dh * mVideoHeight / mVideoVisibleHeight);
        getVideoSurface().setLayoutParams(lp);

        getVideoSurface().invalidate();
    }

    void seek(int delta) {
        if (mLibVLC.getLength() <= 0 && !mSeeking)
            return;

        long position = mLibVLC.getTime() + delta;
        if (position < 0)
            position = 0;
        setCurrentTime(position);
        showOverlay();
        onProgressChanged(getCurrentTime(), getDuration());
        mLastSub = null;
        checkSubs();
    }

    protected void setLastSub(Caption sub) {
        mLastSub = sub;
    }

    private void startSubtitles() {
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... voids) {
                try {
                    FileInputStream fileInputStream = new FileInputStream(mSubsFile);
                    FormatSRT formatSRT = new FormatSRT();
                    mSubs = formatSRT.parseFile(mSubsFile.toString(),
                            FileUtils.inputstreamToCharsetString(fileInputStream, mCurrentSubsLang).split("\n"));
                    checkSubs();
                } catch (FileNotFoundException e) {
                    if (e.getMessage().contains("EBUSY")) {
                        startSubtitles();
                    }
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }

    protected void checkSubs() {
        if (mLibVLC != null && mLibVLC.isPlaying() && mSubs != null) {
            Collection<Caption> subtitles = mSubs.captions.values();
            double currentTime = getCurrentTime();
            if (mLastSub != null && currentTime >= mLastSub.start.getMilliseconds()
                    && currentTime <= mLastSub.end.getMilliseconds()) {
                showTimedCaptionText(mLastSub);
            } else {
                for (Caption caption : subtitles) {
                    if (currentTime >= caption.start.getMilliseconds()
                            && currentTime <= caption.end.getMilliseconds()) {
                        mLastSub = caption;

                        showTimedCaptionText(caption);
                        break;
                    } else if (currentTime > caption.end.getMilliseconds()) {
                        showTimedCaptionText(null);
                    }
                }
            }
        }
    }

    public void onSubtitleLanguageSelected(String language) {
        if (mCurrentSubsLang != null && (language == null || mCurrentSubsLang.equals(language))) {
            return;
        }

        showTimedCaptionText(null);

        mCurrentSubsLang = language;

        if (language.equals("no-subs")) {
            mSubs = null;
            return;
        }

        SubsProvider.download(getActivity(), mMedia, language, new com.squareup.okhttp.Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                mSubs = null;
                mCurrentSubsLang = "no-subs";

                try {
                    Toast.makeText(getActivity(), "Subtitle download failed", Toast.LENGTH_SHORT).show();
                } catch (RuntimeException runtimeException) {
                    runtimeException.printStackTrace();
                }
            }

            @Override
            public void onResponse(Response response) throws IOException {
                mSubsFile = new File(SubsProvider.getStorageLocation(getActivity()),
                        mMedia.videoId + "-" + mCurrentSubsLang + ".srt");
                startSubtitles();
            }
        });
    }

    private static class VideoPlayerEventHandler extends WeakHandler<BaseVideoPlayerFragment> {
        public VideoPlayerEventHandler(BaseVideoPlayerFragment owner) {
            super(owner);
        }

        @Override
        public void handleMessage(Message msg) {
            BaseVideoPlayerFragment fragment = getOwner();
            if (fragment == null)
                return;

            switch (msg.getData().getInt("event")) {
            case EventHandler.MediaPlayerPlaying:
                fragment.resumeVideo();
                fragment.setProgressVisible(false);
                fragment.showOverlay();
                break;
            case EventHandler.MediaPlayerEndReached:
                fragment.endReached();
                break;
            case EventHandler.MediaPlayerEncounteredError:
                fragment.onErrorEncountered();
                break;
            case EventHandler.HardwareAccelerationError:
                fragment.handleHardwareAccelerationError();
                break;
            case EventHandler.MediaPlayerTimeChanged:
            case EventHandler.MediaPlayerPositionChanged:
                fragment.onProgressChanged(fragment.getCurrentTime(), fragment.getDuration());
                fragment.checkSubs();
                break;
            }
            fragment.updatePlayPauseState();
        }
    }

    @Override
    public void onStreamStarted() {
    }

    @Override
    public void onStreamError(Exception e) {
    }

    @Override
    public void onStreamReady(File videoLocation) {
    }

    @Override
    public void onStreamProgress(DownloadStatus status) {
        int newProgress = (int) ((getDuration() / 100) * status.progress);
        if (mStreamerProgress < newProgress) {
            mStreamerProgress = newProgress;
        }
    }

    /**
     * attach and disattach surface to the lib
     */
    private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            if (format == PixelFormat.RGBX_8888)
                Timber.d("Pixel format is RGBX_8888");
            else if (format == PixelFormat.RGB_565)
                Timber.d("Pixel format is RGB_565");
            else if (format == ImageFormat.YV12)
                Timber.d("Pixel format is YV12");
            else
                Timber.d("Pixel format is other/unknown");
            if (mLibVLC != null)
                mLibVLC.attachSurface(holder.getSurface(), BaseVideoPlayerFragment.this);
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (mLibVLC != null)
                mLibVLC.detachSurface();
        }
    };

    public interface Callback {
        StreamInfo getInfo();

        TorrentService getService();
    }

}