diff --git a/app/src/main/java/ie/macinnes/tvheadend/TvhMappings.java b/app/src/main/java/ie/macinnes/tvheadend/TvhMappings.java index 3f05619..a794571 100644 --- a/app/src/main/java/ie/macinnes/tvheadend/TvhMappings.java +++ b/app/src/main/java/ie/macinnes/tvheadend/TvhMappings.java @@ -16,7 +16,10 @@ package ie.macinnes.tvheadend; +import android.util.Log; + public class TvhMappings { + private static final String TAG = TvhMappings.class.getName(); private TvhMappings() { throw new IllegalAccessError("Utility class"); @@ -33,4 +36,49 @@ public static int sriToRate(int sri) { return mSampleRates[sri & 0xf]; } + + public static int androidSpeedToTvhSpeed(float speed) { + // Translate the speed value from what Android uses, to what TVHeadend expects. + // TVHeadend expects: 0=pause, 100=1x fwd, -100=1x backward) + + int translatedSpeed; + + switch((int) speed) { + case 1: // Normal Playback + translatedSpeed = 100; + break; + + case -2: // 2x Rewind + translatedSpeed = -200; + break; + case -4: // 3x Rewind + translatedSpeed = -300; + break; + case -12: // 4x Rewind + translatedSpeed = -400; + break; + case -48: // 5x Rewind + translatedSpeed = -500; + break; + + case 2: // 2x Fast forward + translatedSpeed = 200; + break; + case 8: // 3x Fast forward + translatedSpeed = 300; + break; + case 32: // 4x Fast forward + translatedSpeed = 400; + break; + case 128: // 5x Fast forward + translatedSpeed = 500; + break; + + default: + throw new IllegalArgumentException("Unknown speed: " + speed); + } + + Log.d(TAG, "Translated android speed " + speed + " to TVH speed " + translatedSpeed); + return translatedSpeed; + } } diff --git a/app/src/main/java/ie/macinnes/tvheadend/player/ExoPlayerUtils.java b/app/src/main/java/ie/macinnes/tvheadend/player/ExoPlayerUtils.java index e1da726..7cac498 100644 --- a/app/src/main/java/ie/macinnes/tvheadend/player/ExoPlayerUtils.java +++ b/app/src/main/java/ie/macinnes/tvheadend/player/ExoPlayerUtils.java @@ -144,4 +144,35 @@ private static String joinWithSeparator(String first, String second) { return first + ", " + second; } } + + public static float androidSpeedToExoPlayerSpeed(float speed) { + // Translate the speed value from what Android uses, to what ExoPlayer expects. Must be + // greater than zero, and cannot be used for rewind. + float translatedSpeed; + + switch((int) speed) { + case 1: // Normal Playback + translatedSpeed = 1.0f; + break; + + case 2: // 2x Fast forward + translatedSpeed = 2.0f; + break; + case 8: // 3x Fast forward + translatedSpeed = 3.0f; + break; + case 32: // 4x Fast forward + translatedSpeed = 4.0f; + break; + case 128: // 5x Fast forward + translatedSpeed = 5.0f; + break; + + default: + throw new IllegalArgumentException("Unknown speed: " + speed); + } + + Log.d(TAG, "Translated android speed " + speed + " to ExoPlayer speed " + translatedSpeed); + return translatedSpeed; + } } diff --git a/app/src/main/java/ie/macinnes/tvheadend/player/HtspSubscriptionDataSource.java b/app/src/main/java/ie/macinnes/tvheadend/player/HtspSubscriptionDataSource.java index 674c2f1..73512f0 100644 --- a/app/src/main/java/ie/macinnes/tvheadend/player/HtspSubscriptionDataSource.java +++ b/app/src/main/java/ie/macinnes/tvheadend/player/HtspSubscriptionDataSource.java @@ -328,7 +328,9 @@ public long getTimeshiftOffsetPts() { @Override public void setSpeed(int speed) { - + if (mSubscriber != null) { + mSubscriber.setSpeed(speed); + } } // Misc Internal Methods diff --git a/app/src/main/java/ie/macinnes/tvheadend/player/TvheadendPlayer.java b/app/src/main/java/ie/macinnes/tvheadend/player/TvheadendPlayer.java index 9cf4426..2b0b0aa 100644 --- a/app/src/main/java/ie/macinnes/tvheadend/player/TvheadendPlayer.java +++ b/app/src/main/java/ie/macinnes/tvheadend/player/TvheadendPlayer.java @@ -25,6 +25,7 @@ import android.media.tv.TvTrackInfo; import android.net.Uri; import android.os.Build; +import android.os.Handler; import android.support.annotation.RequiresApi; import android.util.Log; import android.util.SparseArray; @@ -70,11 +71,14 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import ie.macinnes.htsp.SimpleHtspConnection; import ie.macinnes.tvheadend.Constants; import ie.macinnes.tvheadend.R; import ie.macinnes.tvheadend.TvContractUtils; +import ie.macinnes.tvheadend.TvhMappings; public class TvheadendPlayer implements Player.EventListener { private static final String TAG = TvheadendPlayer.class.getName(); @@ -112,6 +116,8 @@ public interface Listener { private final SimpleHtspConnection mConnection; private final Listener mListener; + private final Handler mHandler; + private final Timer mTimer; private final SharedPreferences mSharedPreferences; private SimpleExoPlayer mExoPlayer; @@ -133,11 +139,16 @@ public interface Listener { private Uri mCurrentChannelUri; + private float mSpeed = 1.0f; + private TimerTask mRewindTimerTask; + public TvheadendPlayer(Context context, SimpleHtspConnection connection, Listener listener) { mContext = context; mConnection = connection; mListener = listener; + mHandler = new Handler(); + mTimer = new Timer(); mSharedPreferences = mContext.getSharedPreferences( Constants.PREFERENCE_TVHEADEND, Context.MODE_PRIVATE); @@ -193,10 +204,12 @@ public boolean selectTrack(int type, String trackId) { public void play() { // Start playback when ready mExoPlayer.setPlayWhenReady(true); + cancelRewind(); } public void resume() { mExoPlayer.setPlayWhenReady(true); + cancelRewind(); if (mDataSource != null) { Log.d(TAG, "Resuming HtspDataSource"); @@ -208,6 +221,7 @@ public void resume() { public void pause() { mExoPlayer.setPlayWhenReady(false); + cancelRewind(); if (mDataSource != null) { Log.d(TAG, "Pausing HtspDataSource"); @@ -233,49 +247,78 @@ public void seek(long timeMs) { @RequiresApi(api = Build.VERSION_CODES.M) public void setPlaybackParams(PlaybackParams params) { - float rawSpeed = params.getSpeed(); - int speed = (int) rawSpeed; - int translatedSpeed; - - switch(speed) { - case 0: - translatedSpeed = 100; - break; - case -2: - translatedSpeed = -200; - break; - case -4: - translatedSpeed = -300; - break; - case -12: - translatedSpeed = -400; - break; - case -48: - translatedSpeed = -500; - break; - case 2: - translatedSpeed = 200; - break; - case 4: - translatedSpeed = 300; - break; - case 12: - translatedSpeed = 400; - break; - case 48: - translatedSpeed = 500; - break; - default: - Log.d(TAG, "Unknown speed??? " + rawSpeed); - return; + Log.d(TAG, "setPlaybackParams: Speed: " + params.getSpeed()); + + if (mDataSource != null) { + mSpeed = params.getSpeed(); + + if (mSpeed == 1.0f) { + setVolume(1.0f); + } else { + setVolume(0f); + } + + if (mSpeed > 0) { + // Forward Playback + // Convert from TIF speed format, over to TVH and ExoPlayer formats + int tvhSpeed = TvhMappings.androidSpeedToTvhSpeed(mSpeed); + float exoSpeed = ExoPlayerUtils.androidSpeedToExoPlayerSpeed(mSpeed); + + mDataSource.setSpeed(tvhSpeed); + mExoPlayer.setPlaybackParameters(new PlaybackParameters(exoSpeed, 1)); + } else { + // Reverse Playback + rewind(); + } } + } - Log.d(TAG, "Speed: " + params.getSpeed() + " / " + translatedSpeed); + private void rewind() { + cancelRewind(); - if (mDataSource != null) { - mDataSource.setSpeed(translatedSpeed); - mExoPlayer.setPlaybackParameters(new PlaybackParameters(translatedSpeed, 0)); + mExoPlayer.setPlayWhenReady(false); + + mRewindTimerTask = new TimerTask() { + @Override + public void run() { + mHandler.post(new Runnable() { + @Override + public void run() { + int seekSize; + + switch((int) mSpeed) { + case -2: // 2x Rewind + seekSize = -2000; + break; + case -4: // 3x Rewind + seekSize = -3000; + break; + case -12: // 4x Rewind + seekSize = -4000; + break; + case -48: // 5x Rewind + seekSize = -5000; + break; + + default: + throw new IllegalArgumentException("Unknown speed: " + mSpeed); + } + + seek(Math.max(getTimeshiftCurrentPosition() - seekSize, getTimeshiftStartPosition())); + } + }); + } + }; + + mTimer.scheduleAtFixedRate(mRewindTimerTask, 0, 1000); + } + + private void cancelRewind() { + if (mRewindTimerTask != null) { + mRewindTimerTask.cancel(); } + + mExoPlayer.setPlayWhenReady(true); } private void stop() { @@ -545,7 +588,7 @@ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray tra } } - if(hasVideoTrack) { + if (hasVideoTrack) { disableRadioInfoScreen(); } else { enableRadioInfoScreen(); diff --git a/app/src/main/java/ie/macinnes/tvheadend/tvinput/HtspSession.java b/app/src/main/java/ie/macinnes/tvheadend/tvinput/HtspSession.java index d0d5b10..508d3ea 100644 --- a/app/src/main/java/ie/macinnes/tvheadend/tvinput/HtspSession.java +++ b/app/src/main/java/ie/macinnes/tvheadend/tvinput/HtspSession.java @@ -133,6 +133,7 @@ public void onSetStreamVolume(float volume) { @Override public View onCreateOverlayView() { Log.d(TAG, "Session onCreateOverlayView (" + mSessionNumber + ")"); + return mTvheadendPlayer.getOverlayView( mCaptioningManager.getUserStyle(), mCaptioningManager.getFontScale()); } @@ -190,6 +191,12 @@ public void onTimeShiftPause() { public void onTimeShiftResume() { Log.d(TAG, "onTimeShiftResume"); mTvheadendPlayer.resume(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PlaybackParams normalParams = new PlaybackParams(); + normalParams.setSpeed(1); + onTimeShiftSetPlaybackParams(normalParams); + } } @Override @@ -202,9 +209,9 @@ public void onTimeShiftSeekTo(long timeMs) { @RequiresApi(api = Build.VERSION_CODES.M) @Override public void onTimeShiftSetPlaybackParams(PlaybackParams params) { - Log.d(TAG, "onTimeShiftSetPlaybackParams: " + params); + Log.d(TAG, "onTimeShiftSetPlaybackParams: Speed: " + params.getSpeed()); - Toast.makeText(mContext, "Unsupported", Toast.LENGTH_SHORT).show(); + mTvheadendPlayer.setPlaybackParams(params); } @RequiresApi(api = Build.VERSION_CODES.M)