Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Playback speed controls #1302

Merged
merged 14 commits into from
Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
var defaultSubtitlesSize = Preference.int("subtitles_size", 28)
}

override operator fun <T : Preference<V>, V : Enum<V>> get(preference: T): V {
// Mock-able point for unit tests
return super.get(preference)
}
DavidFair marked this conversation as resolved.
Show resolved Hide resolved

init {
// Note: Create a single migration per app version
// Note: Migrations are never executed for fresh installs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import android.view.Display;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.jellyfin.androidtv.R;
Expand Down Expand Up @@ -86,6 +87,7 @@ public class PlaybackController {
private VideoOptions mCurrentOptions;
private int mDefaultSubIndex = -1;
private int mDefaultAudioIndex = -1;
private double mRequestedPlaybackSpeed = -1.0;
DavidFair marked this conversation as resolved.
Show resolved Hide resolved

private PlayMethod mPlaybackMethod = PlayMethod.Transcode;

Expand Down Expand Up @@ -150,10 +152,17 @@ public PlayMethod getPlaybackMethod() {
return mPlaybackMethod;
}

public void setPlaybackMethod(PlayMethod value) {
public void setPlaybackMethod(@NonNull PlayMethod value) {
mPlaybackMethod = value;
}

public void setPlaybackSpeed(@NonNull Double speed) {
mRequestedPlaybackSpeed = speed;
if (hasInitializedVideoManager()) {
mVideoManager.setPlaybackSpeed(speed);
}
}

public BaseItemDto getCurrentlyPlayingItem() {
return mItems.size() > mCurrentIndex ? mItems.get(mCurrentIndex) : null;
}
Expand Down Expand Up @@ -719,6 +728,7 @@ private void startItem(BaseItemDto item, long position, StreamInfo response) {

// get subtitle info
mSubtitleStreams = response.GetSubtitleProfiles(false, apiClient.getValue().getApiUrl(), apiClient.getValue().getAccessToken());
mVideoManager.setPlaybackSpeed(mRequestedPlaybackSpeed);

if (mFragment != null) mFragment.updateDisplay();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.android.exoplayer2.DefaultRenderersFactory;
Expand Down Expand Up @@ -143,8 +144,13 @@ public void setNativeMode(boolean value) {
}
}

public boolean isNativeMode() { return nativeMode; }
public int getZoomMode() { return mZoomMode; }
public boolean isNativeMode() {
return nativeMode;
}

public int getZoomMode() {
return mZoomMode;
}

public void setZoom(int mode) {
mZoomMode = mode;
Expand Down Expand Up @@ -175,7 +181,7 @@ public void setMetaDuration(long duration) {
}

public long getDuration() {
if (nativeMode){
if (nativeMode) {
return mExoPlayer.getDuration() > 0 ? mExoPlayer.getDuration() : mMetaDuration;
} else {
return mVlcPlayer.getLength() > 0 ? mVlcPlayer.getLength() : mMetaDuration;
Expand Down Expand Up @@ -285,13 +291,14 @@ public long seekTo(long pos) {
mLastTime = mVlcPlayer.getTime();
Timber.i("VLC length in seek is: %d", mVlcPlayer.getLength());
try {
if (getDuration() > 0) mVlcPlayer.setPosition((float)pos / getDuration()); else mVlcPlayer.setTime(pos);
if (getDuration() > 0) mVlcPlayer.setPosition((float) pos / getDuration());
else mVlcPlayer.setTime(pos);

return pos;

} catch (Exception e) {
Timber.e(e, "Error seeking in VLC");
Utils.showToast(mActivity, mActivity.getString(R.string.seek_error));
Utils.showToast(mActivity, mActivity.getString(R.string.seek_error));
return -1;
}
}
Expand Down Expand Up @@ -349,7 +356,7 @@ public boolean setSubtitleTrack(int index, @Nullable List<MediaStream> allStream
} catch (IndexOutOfBoundsException e) {
Timber.e("Could not locate subtitle with index %s in vlc track info", index);
return false;
} catch (NullPointerException e){
} catch (NullPointerException e) {
Timber.e("No subtitle tracks found in player trying to set subtitle with index %s in vlc track info", index);
return false;
}
Expand Down Expand Up @@ -387,7 +394,7 @@ public void setAudioTrack(int ndx, List<MediaStream> allStreams) {
Timber.e("Could not locate audio with index %s in vlc track info", ndx);
mVlcPlayer.setAudioTrack(ndx);
return;
} catch (NullPointerException e){
} catch (NullPointerException e) {
Timber.e("No subtitle tracks found in player trying to set subtitle with index %s in vlc track info", ndx);
mVlcPlayer.setAudioTrack(vlcIndex);
return;
Expand All @@ -409,6 +416,14 @@ public void setAudioTrack(int ndx, List<MediaStream> allStreams) {
}
}

public void setPlaybackSpeed(@NonNull Double speed) {
if (nativeMode) {
DavidFair marked this conversation as resolved.
Show resolved Hide resolved
mExoPlayer.setPlaybackSpeed(speed.floatValue());
} else {
mVlcPlayer.setRate(speed.floatValue());
}
}

public void setAudioDelay(long value) {
if (!nativeMode && mVlcPlayer != null) {
if (!mVlcPlayer.setAudioDelay(value * 1000)) {
Expand Down Expand Up @@ -437,7 +452,7 @@ public void setAudioMode() {
}

private void setVlcAudioOptions() {
if(!Utils.downMixAudio()) {
if (!Utils.downMixAudio()) {
mVlcPlayer.setAudioDigitalOutputEnabled(true);
} else {
setCompatibleAudio();
Expand Down Expand Up @@ -544,7 +559,7 @@ public void contractVideo(int height) {
Activity activity = TvApp.getApplication().getCurrentActivity();
int sw = activity.getWindow().getDecorView().getWidth();
int sh = activity.getWindow().getDecorView().getHeight();
float ar = (float)sw / sh;
float ar = (float) sw / sh;
lp.height = height;
lp.width = (int) Math.ceil(height * ar);
lp.rightMargin = ((lp.width - normalWidth) / 2) - 110;
Expand Down Expand Up @@ -610,10 +625,10 @@ private void changeSurfaceLayout(int videoWidth, int videoHeight, int videoVisib
double ar;
if (sarDen == sarNum) {
/* No indication about the density, assuming 1:1 */
ar = (double)videoVisibleWidth / (double)videoVisibleHeight;
ar = (double) videoVisibleWidth / (double) videoVisibleHeight;
} else {
/* Use the specified aspect ratio */
double vw = videoVisibleWidth * (double)sarNum / sarDen;
double vw = videoVisibleWidth * (double) sarNum / sarDen;
ar = vw / videoVisibleHeight;
}

Expand All @@ -627,7 +642,7 @@ private void changeSurfaceLayout(int videoWidth, int videoHeight, int videoVisib

// set display size
ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
lp.width = (int) Math.ceil(dw * videoWidth / videoVisibleWidth);
lp.width = (int) Math.ceil(dw * videoWidth / videoVisibleWidth);
lp.height = (int) Math.ceil(dh * videoHeight / videoVisibleHeight);
normalWidth = lp.width;
normalHeight = lp.height;
Expand Down Expand Up @@ -671,6 +686,7 @@ public void setOnProgressListener(PlaybackListener listener) {

private PlaybackListener progressListener;
private Runnable progressLoop;

private void startProgressLoop() {
progressLoop = new Runnable() {
@Override
Expand Down Expand Up @@ -716,7 +732,7 @@ public void onNewVideoLayout(IVLCVout vout, int width, int height, int visibleWi
mVideoHeight = height;
mVideoWidth = width;
mVideoVisibleHeight = visibleHeight;
mVideoVisibleWidth = visibleWidth;
mVideoVisibleWidth = visibleWidth;
mSarNum = sarNum;
mSarDen = sarDen;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.jellyfin.androidtv.ui.playback

class VideoSpeedController(playbackController: PlaybackController) {
companion object {
enum class SpeedSteps(val speed: Double) {
DavidFair marked this conversation as resolved.
Show resolved Hide resolved
// Use named parameter so detekt knows these aren't magic values
SPEED_0_25(speed = 0.25),
SPEED_0_50(speed = 0.5),
SPEED_0_75(speed = 0.75),
SPEED_1_00(speed = 1.0),
SPEED_1_25(speed = 1.25),
SPEED_1_50(speed = 1.50),
SPEED_1_75(speed = 1.75),
SPEED_2_00(speed = 2.0),
}

private var previousSpeedSelection = SpeedSteps.SPEED_1_00
fun resetPreviousSpeedToDefault() {
previousSpeedSelection = SpeedSteps.SPEED_1_00
}
}

private val parentController = playbackController
DavidFair marked this conversation as resolved.
Show resolved Hide resolved

init {
// Carry forward the user's recent speed selection onto the next video(s)
setNewSpeed(previousSpeedSelection)
}

fun getCurrentSpeed(): SpeedSteps {
// Currently getCurrentSpeed uses previousSpeedSelection (from the companion)
// but this is an implementation detail I'd rather not leak in-case we ever need
// to separate out the two details. So implement a custom named getter...
return previousSpeedSelection
DavidFair marked this conversation as resolved.
Show resolved Hide resolved
}

fun setNewSpeed(selectedSpeed: SpeedSteps) {
previousSpeedSelection = selectedSpeed
parentController.setPlaybackSpeed(selectedSpeed.speed)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.jellyfin.androidtv.ui.playback.overlay.action.ChapterAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.ClosedCaptionsAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.GuideAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.PlaybackSpeedAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.PreviousLiveTvChannelAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.RecordAction;
import org.jellyfin.androidtv.ui.playback.overlay.action.SelectAudioAction;
Expand All @@ -48,6 +49,7 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
private PlaybackControlsRow.SkipNextAction skipNextAction;
private SelectAudioAction selectAudioAction;
private ClosedCaptionsAction closedCaptionsAction;
private PlaybackSpeedAction playbackSpeedAction;
private AdjustAudioDelayAction adjustAudioDelayAction;
private ZoomAction zoomAction;
private ChapterAction chapterAction;
Expand Down Expand Up @@ -173,6 +175,8 @@ private void initActions(Context context) {
selectAudioAction.setLabels(new String[]{context.getString(R.string.lbl_audio_track)});
closedCaptionsAction = new ClosedCaptionsAction(context, this);
closedCaptionsAction.setLabels(new String[]{context.getString(R.string.lbl_subtitle_track)});
playbackSpeedAction = new PlaybackSpeedAction(context, this, playbackController);
playbackSpeedAction.setLabels(new String[]{context.getString(R.string.lbl_playback_speed)});
adjustAudioDelayAction = new AdjustAudioDelayAction(context, this);
adjustAudioDelayAction.setLabels(new String[]{context.getString(R.string.lbl_audio_delay)});
zoomAction = new ZoomAction(context, this);
Expand Down Expand Up @@ -219,6 +223,8 @@ void addMediaActions() {
primaryActionsAdapter.add(closedCaptionsAction);
}

primaryActionsAdapter.add(playbackSpeedAction);

if (hasMultiAudio()) {
primaryActionsAdapter.add(selectAudioAction);
}
Expand Down Expand Up @@ -282,6 +288,9 @@ public void onCustomActionClicked(Action action, View view) {
} else if (action == closedCaptionsAction) {
leanbackOverlayFragment.setFading(false);
closedCaptionsAction.handleClickAction(playbackController, leanbackOverlayFragment, getContext(), view);
} else if (action == playbackSpeedAction) {
leanbackOverlayFragment.setFading(false);
playbackSpeedAction.handleClickAction(playbackController, leanbackOverlayFragment, getContext(), view);
} else if (action == adjustAudioDelayAction) {
leanbackOverlayFragment.setFading(false);
adjustAudioDelayAction.handleClickAction(playbackController, leanbackOverlayFragment, getContext(), view);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.jellyfin.androidtv.ui.playback.overlay.action

import android.content.Context
import android.view.Gravity
import android.view.View
import android.widget.PopupMenu
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.playback.PlaybackController
import org.jellyfin.androidtv.ui.playback.VideoSpeedController
import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue
import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment
import java.util.*

class PlaybackSpeedAction(
context: Context,
customPlaybackTransportControlGlue: CustomPlaybackTransportControlGlue,
playbackController: PlaybackController
) : CustomAction(context, customPlaybackTransportControlGlue) {
private val speedController = VideoSpeedController(playbackController)
private val speeds = VideoSpeedController.Companion.SpeedSteps.values()

init {
initializeWithIcon(R.drawable.ic_playback_speed)
}

override fun handleClickAction(
playbackController: PlaybackController,
leanbackOverlayFragment: LeanbackOverlayFragment,
context: Context, view: View
) {
val speedMenu = populateMenu(context, view, speedController)

speedMenu.setOnDismissListener { leanbackOverlayFragment.setFading(true) }

speedMenu.setOnMenuItemClickListener { menuItem ->
speedController.setNewSpeed(speeds[menuItem.itemId])
speedMenu.dismiss()
true
}

speedMenu.show()
}

private fun populateMenu(
context: Context,
view: View,
speedController: VideoSpeedController
) = PopupMenu(context, view, Gravity.END).apply {
speeds.forEachIndexed { i, selected ->
// Since this is purely numeric data, coerce to en_us to keep the linter happy
menu.add(0, i, i, String.format(Locale.US, "%.2fx", selected.speed))
}

menu.setGroupCheckable(0, true, true)
menu.getItem(speeds.indexOf(speedController.getCurrentSpeed())).isChecked = true
}

}
25 changes: 25 additions & 0 deletions app/src/main/res/drawable/ic_playback_speed.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.05,9.79L10,7.5v9l3.05,-2.29L16,12zM13.05,9.79L10,7.5v9l3.05,-2.29L16,12zM13.05,9.79L10,7.5v9l3.05,-2.29L16,12zM11,4.07L11,2.05c-2.01,0.2 -3.84,1 -5.32,2.21L7.1,5.69c1.11,-0.86 2.44,-1.44 3.9,-1.62zM5.69,7.1L4.26,5.68C3.05,7.16 2.25,8.99 2.05,11h2.02c0.18,-1.46 0.76,-2.79 1.62,-3.9zM4.07,13L2.05,13c0.2,2.01 1,3.84 2.21,5.32l1.43,-1.43c-0.86,-1.1 -1.44,-2.43 -1.62,-3.89zM5.68,19.74C7.16,20.95 9,21.75 11,21.95v-2.02c-1.46,-0.18 -2.79,-0.76 -3.9,-1.62l-1.42,1.43zM22,12c0,5.16 -3.92,9.42 -8.95,9.95v-2.02C16.97,19.41 20,16.05 20,12s-3.03,-7.41 -6.95,-7.93L13.05,2.05C18.08,2.58 22,6.84 22,12z"
android:fillColor="#FFFFFF"/>
</vector>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
<string name="lbl_stretch">Stretch</string>
<string name="lbl_fit">Normal</string>
<string name="lbl_audio_track">Select audio track</string>
<string name="lbl_playback_speed">Playback Speed</string>
DavidFair marked this conversation as resolved.
Show resolved Hide resolved
<string name="lbl_subtitle_track">Select subtitle track</string>
<string name="msg_external_path">This feature will only work if you have properly set up your library on the server with network paths or path substitution and the client you are using can directly access these locations over the network.</string>
<string name="btn_got_it">Got it</string>
Expand Down
Loading