package tv.emby.embyatv.playback;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Handler;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.devbrackets.android.exomedia.EMVideoView;
import org.videolan.libvlc.IVLCVout;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
import java.util.ArrayList;
import java.util.List;
import mediabrowser.model.dto.MediaSourceInfo;
import mediabrowser.model.entities.MediaStream;
import mediabrowser.model.entities.MediaStreamType;
import tv.emby.embyatv.R;
import tv.emby.embyatv.TvApp;
import tv.emby.embyatv.util.Utils;
/**
* Created by Eric on 7/11/2015.
*/
public class VideoManager implements IVLCVout.Callback {
public final static int ZOOM_NORMAL = 0;
public final static int ZOOM_VERTICAL = 1;
public final static int ZOOM_HORIZONTAL = 2;
public final static int ZOOM_FULL = 3;
private int mZoomMode = ZOOM_NORMAL;
private PlaybackOverlayActivity mActivity;
private SurfaceHolder mSurfaceHolder;
private SurfaceView mSurfaceView;
private SurfaceView mSubtitlesSurface;
private FrameLayout mSurfaceFrame;
private EMVideoView mVideoView;
private LibVLC mLibVLC;
private org.videolan.libvlc.MediaPlayer mVlcPlayer;
private String mCurrentVideoPath;
private String mCurrentVideoMRL;
private Media mCurrentMedia;
private VlcEventHandler mVlcHandler = new VlcEventHandler();
private Handler mHandler = new Handler();
private int mVideoHeight;
private int mVideoWidth;
private int mVideoVisibleHeight;
private int mVideoVisibleWidth;
private int mSarNum;
private int mSarDen;
private long mForcedTime = -1;
private long mLastTime = -1;
private long mMetaDuration = -1;
private boolean nativeMode = false;
private boolean mSurfaceReady = false;
public boolean isContracted = false;
private boolean hasSubtitlesSurface = false;
public VideoManager(PlaybackOverlayActivity activity, View view, int buffer) {
mActivity = activity;
mSurfaceView = (SurfaceView) view.findViewById(R.id.player_surface);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(mSurfaceCallback);
mSurfaceFrame = (FrameLayout) view.findViewById(R.id.player_surface_frame);
mSubtitlesSurface = (SurfaceView) view.findViewById(R.id.subtitles_surface);
if (Utils.is50()) {
mSubtitlesSurface.setZOrderMediaOverlay(true);
mSubtitlesSurface.getHolder().setFormat(PixelFormat.TRANSLUCENT);
hasSubtitlesSurface = true;
} else {
mSubtitlesSurface.setVisibility(View.GONE);
}
mVideoView = (EMVideoView) view.findViewById(R.id.videoView);
createPlayer(buffer);
}
public void setNativeMode(boolean value) {
nativeMode = value;
if (nativeMode) {
mVideoView.setVisibility(View.VISIBLE);
} else {
mVideoView.setVisibility(View.GONE);
}
}
public boolean isNativeMode() { return nativeMode; }
public int getZoomMode() { return mZoomMode; }
public void setZoom(int mode) {
mZoomMode = mode;
switch (mode) {
case ZOOM_NORMAL:
mVideoView.setScaleY(1);
mVideoView.setScaleX(1);
break;
case ZOOM_VERTICAL:
mVideoView.setScaleX(1);
mVideoView.setScaleY(1.33f);
break;
case ZOOM_HORIZONTAL:
mVideoView.setScaleY(1);
mVideoView.setScaleX(1.33f);
break;
case ZOOM_FULL:
mVideoView.setScaleX(1.33f);
mVideoView.setScaleY(1.33f);
break;
}
}
public void setMetaDuration(long duration) {
mMetaDuration = duration;
}
public long getDuration() {
if (nativeMode){
return mVideoView.getDuration() > 0 ? mVideoView.getDuration() : mMetaDuration;
} else {
return mVlcPlayer.getLength() > 0 ? mVlcPlayer.getLength() : mMetaDuration;
}
}
public long getCurrentPosition() {
if (nativeMode) return mVideoView.getCurrentPosition();
if (mVlcPlayer == null) return 0;
long time = mVlcPlayer.getTime();
if (mForcedTime != -1 && mLastTime != -1) {
/* XXX: After a seek, mLibVLC.getTime can return the position before or after
* the seek position. Therefore we return mForcedTime in order to avoid the seekBar
* to move between seek position and the actual position.
* We have to wait for a valid position (that is after the seek position).
* to re-init mLastTime and mForcedTime to -1 and return the actual position.
*/
if (mLastTime > mForcedTime) {
if (time <= mLastTime && time > mForcedTime)
mLastTime = mForcedTime = -1;
} else {
if (time > mForcedTime)
mLastTime = mForcedTime = -1;
}
}
return mForcedTime == -1 ? time : mForcedTime;
}
public boolean isPlaying() {
return nativeMode ? mVideoView.isPlaying() : mVlcPlayer != null && mVlcPlayer.isPlaying();
}
public boolean canSeek() { return nativeMode || mVlcPlayer.isSeekable(); }
public void start() {
if (nativeMode) {
mVideoView.start();
mVideoView.setKeepScreenOn(true);
normalWidth = mVideoView.getLayoutParams().width;
normalHeight = mVideoView.getLayoutParams().height;
} else {
if (!mSurfaceReady) {
TvApp.getApplication().getLogger().Error("Attempt to play before surface ready");
return;
}
if (!mVlcPlayer.isPlaying()) {
mVlcPlayer.play();
}
}
}
public void play() {
if (nativeMode) {
mVideoView.start();
mVideoView.setKeepScreenOn(true);
} else {
mVlcPlayer.play();
mSurfaceView.setKeepScreenOn(true);
// work around losing audio when pausing bug
int sav = mVlcPlayer.getAudioTrack();
mVlcPlayer.setAudioTrack(-1);
mVlcPlayer.setAudioTrack(sav);
//
}
}
public void pause() {
if (nativeMode) {
mVideoView.pause();
mVideoView.setKeepScreenOn(false);
} else {
mVlcPlayer.pause();
mSurfaceView.setKeepScreenOn(false);
}
}
public void setPlaySpeed(float speed) {
if (!nativeMode) mVlcPlayer.setRate(speed);
}
public void stopPlayback() {
if (nativeMode) {
mVideoView.stopPlayback();
} else {
mVlcPlayer.stop();
}
stopProgressLoop();
}
public long seekTo(long pos) {
if (nativeMode) {
Long intPos = pos;
mVideoView.seekTo(intPos.intValue());
return pos;
} else {
if (mVlcPlayer == null || !mVlcPlayer.isSeekable()) return -1;
mForcedTime = pos;
mLastTime = mVlcPlayer.getTime();
TvApp.getApplication().getLogger().Info("VLC length in seek is: " + mVlcPlayer.getLength());
try {
if (getDuration() > 0) mVlcPlayer.setPosition((float)pos / getDuration()); else mVlcPlayer.setTime(pos);
return pos;
} catch (Exception e) {
TvApp.getApplication().getLogger().ErrorException("Error seeking in VLC", e);
Utils.showToast(mActivity, "Unable to seek");
return -1;
}
}
}
public void setVideoPath(String path) {
mCurrentVideoPath = path;
TvApp.getApplication().getLogger().Info("Video path set to: "+path);
if (nativeMode) {
try {
mVideoView.setVideoPath(path);
} catch (IllegalStateException e) {
TvApp.getApplication().getLogger().ErrorException("Unable to set video path. Probably backing out.", e);
}
} else {
mSurfaceHolder.setKeepScreenOn(true);
mCurrentMedia = new Media(mLibVLC, Uri.parse(path));
mCurrentMedia.parse();
mVlcPlayer.setMedia(mCurrentMedia);
mCurrentMedia.release();
}
}
public void hideSurface() {
if (nativeMode) {
mVideoView.setVisibility(View.INVISIBLE);
} else {
mSurfaceView.setVisibility(View.INVISIBLE);
}
}
public void showSurface() {
if (nativeMode) {
mVideoView.setVisibility(View.VISIBLE);
} else {
mSurfaceView.setVisibility(View.VISIBLE);
}
}
public void disableSubs() {
if (!nativeMode && mVlcPlayer != null) mVlcPlayer.setSpuTrack(-1);
}
public boolean setSubtitleTrack(int index, List<MediaStream> allStreams) {
if (!nativeMode) {
//find the relative order of our sub index within the sub tracks in VLC
int vlcIndex = 1; // start at 1 to account for "disabled"
for (MediaStream stream : allStreams) {
if (stream.getType() == MediaStreamType.Subtitle && !stream.getIsExternal()) {
if (stream.getIndex() == index) {
break;
}
vlcIndex++;
}
}
org.videolan.libvlc.MediaPlayer.TrackDescription vlcSub;
try {
vlcSub = getSubtitleTracks()[vlcIndex];
} catch (IndexOutOfBoundsException e) {
TvApp.getApplication().getLogger().Error("Could not locate subtitle with index %s in vlc track info", index);
return false;
} catch (NullPointerException e){
TvApp.getApplication().getLogger().Error("No subtitle tracks found in player trying to set subtitle with index %s in vlc track info", index);
return false;
}
TvApp.getApplication().getLogger().Info("Setting Vlc sub to "+vlcSub.name);
return mVlcPlayer.setSpuTrack(vlcSub.id);
}
return false;
}
public boolean addSubtitleTrack(String path) {
return !nativeMode && mVlcPlayer.setSubtitleFile(path);
}
public int getAudioTrack() {
return nativeMode ? -1 : mVlcPlayer.getAudioTrack();
}
public void setAudioTrack(int id) {
if (!nativeMode) mVlcPlayer.setAudioTrack(id);
}
public void setAudioDelay(long value) {
if (!nativeMode && mVlcPlayer != null) {
if (!mVlcPlayer.setAudioDelay(value * 1000)) {
TvApp.getApplication().getLogger().Error("Error setting audio delay");
} else {
TvApp.getApplication().getLogger().Info("Audio delay set to "+value);
}
}
}
public long getAudioDelay() { return mVlcPlayer != null ? mVlcPlayer.getAudioDelay() / 1000 : 0;}
public void setCompatibleAudio() {
if (!nativeMode) {
mVlcPlayer.setAudioOutput("opensles_android");
mVlcPlayer.setAudioOutputDevice("hdmi");
}
}
public void setAudioMode() {
if (!nativeMode) {
mVlcPlayer.setAudioOutput(Utils.downMixAudio() ? "opensles_android" : "android_audiotrack");
mVlcPlayer.setAudioOutputDevice("hdmi");
}
}
public void setVideoTrack(MediaSourceInfo mediaSource) {
if (!nativeMode && mediaSource != null && mediaSource.getMediaStreams() != null) {
for (MediaStream stream : mediaSource.getMediaStreams()) {
if (stream.getType() == MediaStreamType.Video && stream.getIndex() >= 0) {
TvApp.getApplication().getLogger().Debug("Setting video index to: "+stream.getIndex());
mVlcPlayer.setVideoTrack(stream.getIndex());
return;
}
}
}
}
public org.videolan.libvlc.MediaPlayer.TrackDescription[] getSubtitleTracks() {
return nativeMode ? null : mVlcPlayer.getSpuTracks();
}
public void destroy() {
releasePlayer();
}
private void createPlayer(int buffer) {
try {
// Create a new media player
ArrayList<String> options = new ArrayList<>(20);
options.add("--network-caching=" + buffer);
options.add("--no-audio-time-stretch");
options.add("--avcodec-skiploopfilter");
options.add("" + 1);
options.add("--avcodec-skip-frame");
options.add("0");
options.add("--avcodec-skip-idct");
options.add("0");
options.add("--androidwindow-chroma");
options.add("RV32");
options.add("--audio-resampler");
options.add("soxr");
options.add("--stats");
// options.add("--subsdec-encoding");
// options.add("Universal (UTF-8)");
options.add("-v");
mLibVLC = new LibVLC(options);
TvApp.getApplication().getLogger().Info("Network buffer set to " + buffer);
LibVLC.setOnNativeCrashListener(new LibVLC.OnNativeCrashListener() {
@Override
public void onNativeCrash() {
new Exception().printStackTrace();
//todo custom error reporter
mActivity.finish();
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
});
mVlcPlayer = new org.videolan.libvlc.MediaPlayer(mLibVLC);
mVlcPlayer.setAudioOutput(Utils.downMixAudio() ? "opensles_android" : "android_audiotrack");
mVlcPlayer.setAudioOutputDevice("hdmi");
mSurfaceHolder.addCallback(mSurfaceCallback);
mVlcPlayer.setEventListener(mVlcHandler);
//setup surface
mVlcPlayer.getVLCVout().detachViews();
mVlcPlayer.getVLCVout().setVideoView(mSurfaceView);
if (hasSubtitlesSurface) mVlcPlayer.getVLCVout().setSubtitlesView(mSubtitlesSurface);
mVlcPlayer.getVLCVout().attachViews();
TvApp.getApplication().getLogger().Debug("Surface attached");
mSurfaceReady = true;
mVlcPlayer.getVLCVout().addCallback(this);
} catch (Exception e) {
TvApp.getApplication().getLogger().ErrorException("Error creating VLC player", e);
Utils.showToast(TvApp.getApplication(), TvApp.getApplication().getString(R.string.msg_video_playback_error));
}
}
private void releasePlayer() {
if (mVlcPlayer == null) return;
mVlcPlayer.setEventListener(null);
mVlcPlayer.stop();
mVlcPlayer.getVLCVout().detachViews();
mVlcPlayer.release();
mLibVLC = null;
mVlcPlayer = null;
mSurfaceView.setKeepScreenOn(false);
}
int normalWidth;
int normalHeight;
public void contractVideo(int height) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mVideoView.getLayoutParams() : mSurfaceView.getLayoutParams());
if (isContracted) return;
Activity activity = TvApp.getApplication().getCurrentActivity();
int sw = activity.getWindow().getDecorView().getWidth();
int sh = activity.getWindow().getDecorView().getHeight();
float ar = (float)sw / sh;
lp.height = height;
lp.width = (int) Math.ceil(height * ar);
lp.rightMargin = ((lp.width - normalWidth) / 2) - 110;
lp.bottomMargin = ((lp.height - normalHeight) / 2) - 50;
if (nativeMode) {
mVideoView.setLayoutParams(lp);
mVideoView.invalidate();
} else mSurfaceView.setLayoutParams(lp);
isContracted = true;
}
public void setVideoFullSize() {
if (normalHeight == 0) return;
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) (nativeMode ? mVideoView.getLayoutParams() : mSurfaceView.getLayoutParams());
lp.height = normalHeight;
lp.width = normalWidth;
if (nativeMode) {
lp.rightMargin = 0;
lp.bottomMargin = 0;
mVideoView.setLayoutParams(lp);
mVideoView.invalidate();
} else mSurfaceView.setLayoutParams(lp);
isContracted = false;
}
private void changeSurfaceLayout(int videoWidth, int videoHeight, int videoVisibleWidth, int videoVisibleHeight, int sarNum, int sarDen) {
int sw;
int sh;
// get screen size
Activity activity = TvApp.getApplication().getCurrentActivity();
if (activity == null) return; //called during destroy
sw = activity.getWindow().getDecorView().getWidth();
sh = activity.getWindow().getDecorView().getHeight();
double dw = sw, dh = sh;
boolean isPortrait;
isPortrait = mActivity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
if (sw > sh && isPortrait || sw < sh && !isPortrait) {
dw = sh;
dh = sw;
}
// sanity check
if (dw * dh == 0 || videoWidth * videoHeight == 0) {
TvApp.getApplication().getLogger().Error("Invalid surface size");
return;
}
// compute the aspect ratio
double ar, vw;
if (sarDen == sarNum) {
/* No indication about the density, assuming 1:1 */
vw = videoVisibleWidth;
ar = (double)videoVisibleWidth / (double)videoVisibleHeight;
} else {
/* Use the specified aspect ratio */
vw = videoVisibleWidth * (double)sarNum / sarDen;
ar = vw / videoVisibleHeight;
}
// compute the display aspect ratio
double dar = dw / dh;
if (dar < ar)
dh = dw / ar;
else
dw = dh * ar;
// set display size
ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
lp.width = (int) Math.ceil(dw * videoWidth / videoVisibleWidth);
lp.height = (int) Math.ceil(dh * videoHeight / videoVisibleHeight);
normalWidth = lp.width;
normalHeight = lp.height;
mSurfaceView.setLayoutParams(lp);
if (hasSubtitlesSurface) mSubtitlesSurface.setLayoutParams(lp);
// set frame size (crop if necessary)
if (mSurfaceFrame != null) {
lp = mSurfaceFrame.getLayoutParams();
lp.width = (int) Math.floor(dw);
lp.height = (int) Math.floor(dh);
mSurfaceFrame.setLayoutParams(lp);
}
TvApp.getApplication().getLogger().Debug("Surface sized "+ lp.width+"x"+lp.height);
mSurfaceView.invalidate();
if (hasSubtitlesSurface) mSubtitlesSurface.invalidate();
}
public void setOnErrorListener(final PlaybackListener listener) {
mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
TvApp.getApplication().getLogger().Error("***** Got error from player");
listener.onEvent();
stopProgressLoop();
return true;
}
});
mVlcHandler.setOnErrorListener(listener);
}
public void setOnCompletionListener(final PlaybackListener listener) {
mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
listener.onEvent();
stopProgressLoop();
}
});
mVlcHandler.setOnCompletionListener(listener);
}
private MediaPlayer mNativeMediaPlayer;
public void setOnPreparedListener(final PlaybackListener listener) {
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mNativeMediaPlayer = mp;
listener.onEvent();
startProgressLoop();
}
});
mVlcHandler.setOnPreparedListener(listener);
}
public void setOnProgressListener(PlaybackListener listener) {
progressListener = listener;
mVlcHandler.setOnProgressListener(listener);
}
private PlaybackListener progressListener;
private Runnable progressLoop;
private void startProgressLoop() {
progressLoop = new Runnable() {
@Override
public void run() {
if (progressListener != null) progressListener.onEvent();
mHandler.postDelayed(this, 500);
}
};
mHandler.post(progressLoop);
}
private void stopProgressLoop() {
if (progressLoop != null) {
mHandler.removeCallbacks(progressLoop);
}
}
private SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mVlcPlayer != null) mVlcPlayer.getVLCVout().detachViews();
mSurfaceReady = false;
}
};
@Override
public void onNewLayout(IVLCVout vout, int width, int height, int visibleWidth, int visibleHeight, int sarNum, int sarDen) {
if (width * height == 0 || isContracted)
return;
// store video size
mVideoHeight = height;
mVideoWidth = width;
mVideoVisibleHeight = visibleHeight;
mVideoVisibleWidth = visibleWidth;
mSarNum = sarNum;
mSarDen = sarDen;
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
changeSurfaceLayout(mVideoWidth, mVideoHeight, mVideoVisibleWidth, mVideoVisibleHeight, mSarNum, mSarDen);
}
});
}
@Override
public void onSurfacesCreated(IVLCVout ivlcVout) {
}
@Override
public void onSurfacesDestroyed(IVLCVout ivlcVout) {
}
}