Compare commits

...

13 Commits

Author SHA1 Message Date
Niels van Velzen
9b49af4c42 Fix player crashing when video ends 2022-03-23 20:57:07 +01:00
Cameron
fa59595dd5 only parse exoplayer TracksInfo when needed - instead of once every 0.5-2 seconds (#1556)
* only parse TracksInfo when needed

* only set exoplayerAudioIndex if streamType == MediaStreamType.Audio

(cherry picked from commit 92d37dc5b3)
2022-03-23 17:19:20 +01:00
David Fairbrother
7c3f495036 Add handling for updating end time based on playback speed
Adds handling to update the end time based on the user's current
playback speed.

This is a bit hacky since exoplayer is built on the assumption that we
have bi-directional communication to our UI layer. Instead the
CustomPlaybackTransportControlGlue is uni-directional polling the video
manager.

We unfortunately have to use hacky poll tricks to work-around the fact
we can't wire up on the "onPlaybackParametersChanged" call to notify the
glue class. However, in the short-term a 5s "delay" between setting and
updating is more favourable than setting up reverse listening
support.

Re #1509

(cherry picked from commit fa7956721e)
2022-03-23 17:19:14 +01:00
Niels van Velzen
d05ef3d810 Add Android 12 backup configuration
(cherry picked from commit 26ac67e302)
2022-03-23 17:19:07 +01:00
Niels van Velzen
f5c875959a Fix ASPECT_RATIO_BANNER
(cherry picked from commit 5f1b43c432)
2022-03-23 17:18:57 +01:00
Niels van Velzen
b45700739a Add fastlane structure to repository
(cherry picked from commit 39c21e607a)
2022-03-23 17:18:49 +01:00
Niels van Velzen
a0430a13d1 Add app name and flavor to version in about section
(cherry picked from commit d8695cb1a2)
2022-03-23 17:18:39 +01:00
Niels van Velzen
9dfe855940 Fix imageBlurHashes must not be null crash
(cherry picked from commit 77f31b9893)
2022-03-23 17:18:25 +01:00
cameron
d09c45f74f prefer refresh rates closest to the source refresh rate
(cherry picked from commit be89cf276b)
2022-03-23 17:18:13 +01:00
cameron
16d6b6b2ed prefer changing refresh rate without changing resolution
(cherry picked from commit 85a49fe757)
2022-03-23 17:18:12 +01:00
cameron
75e8938db3 fix crash from tracepot issue a46fe8ca0001267b
ActionsAdapter.clear() crashes when adapter is already empty

(cherry picked from commit f39ba05b38)
2022-03-23 17:17:49 +01:00
cameron
dff36669bf fix crash from tracepot issue 1d115b860001267b
crash from StreamInfo being null

(cherry picked from commit b1250de574)
2022-03-23 17:17:48 +01:00
cameron
1953508209 fix tracepot issue 1a07276a0001267b
fix crash from null leanbackOverlayFragment

(cherry picked from commit a8dea85a1b)
2022-03-23 17:17:47 +01:00
28 changed files with 200 additions and 53 deletions

View File

@@ -20,7 +20,9 @@
<!-- Generic permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission
android:name="android.permission.ACCESS_WIFI_STATE"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Device feature requirements -->
@@ -39,14 +41,14 @@
android:name=".JellyfinApplication"
android:allowBackup="true"
android:banner="@drawable/app_banner"
android:dataExtractionRules="@xml/backup_rules"
android:fullBackupContent="@xml/backup_content"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:largeHeap="true"
android:roundIcon="@drawable/app_icon_round"
android:theme="@style/Theme.Jellyfin"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning">
android:usesCleartextTraffic="true">
<service
android:name=".auth.AuthenticatorService"

View File

@@ -1258,7 +1258,8 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
}
public void setCurrentTime(long time) {
leanbackOverlayFragment.updateCurrentPosition();
if (leanbackOverlayFragment != null)
leanbackOverlayFragment.updateCurrentPosition();
}
public void setSecondaryTime(long time) {

View File

@@ -29,7 +29,6 @@ import org.jellyfin.androidtv.ui.livetv.TvManager;
import org.jellyfin.androidtv.util.DeviceUtils;
import org.jellyfin.androidtv.util.TimeUtils;
import org.jellyfin.androidtv.util.Utils;
import org.jellyfin.androidtv.util.apiclient.PlaybackHelper;
import org.jellyfin.androidtv.util.apiclient.ReportingHelper;
import org.jellyfin.androidtv.util.apiclient.StreamHelper;
import org.jellyfin.androidtv.util.profile.BaseProfile;
@@ -88,7 +87,7 @@ public class PlaybackController {
private int mDefaultSubIndex = -1;
private int mDefaultAudioIndex = -1;
private boolean burningSubs = false;
private double mRequestedPlaybackSpeed = -1.0;
private float mRequestedPlaybackSpeed = -1.0f;
private PlayMethod mPlaybackMethod = PlayMethod.Transcode;
@@ -163,7 +162,17 @@ public class PlaybackController {
mPlaybackMethod = value;
}
public void setPlaybackSpeed(@NonNull Double speed) {
public float getPlaybackSpeed() {
if (hasInitializedVideoManager()) {
// Actually poll the video manager, since exoplayer can revert back
// to 1x if it can't go faster, so we should check directly
return mVideoManager.getPlaybackSpeed();
} else {
return mRequestedPlaybackSpeed;
}
}
public void setPlaybackSpeed(@NonNull float speed) {
mRequestedPlaybackSpeed = speed;
if (hasInitializedVideoManager()) {
mVideoManager.setPlaybackSpeed(speed);
@@ -281,22 +290,28 @@ public class PlaybackController {
@TargetApi(23)
private void getDisplayModes() {
if (mFragment == null)
return;
Display display = mFragment.requireActivity().getWindowManager().getDefaultDisplay();
mDisplayModes = display.getSupportedModes();
Timber.i("** Available display refresh rates:");
for (Display.Mode mDisplayMode : mDisplayModes) {
Timber.i("%f", mDisplayMode.getRefreshRate());
}
}
@TargetApi(23)
private Display.Mode findBestDisplayMode(MediaStream videoStream) {
if (mDisplayModes == null || videoStream.getRealFrameRate() == null) return null;
if (mFragment == null || mDisplayModes == null || videoStream.getRealFrameRate() == null) return null;
int curWeight = 0;
Display.Mode bestMode = null;
int sourceRate = Math.round(videoStream.getRealFrameRate() * 100);
Display.Mode defaultMode = mFragment.requireActivity().getWindowManager().getDefaultDisplay().getMode();
Timber.d("trying to find display mode for video: %sx%s @%sfps", videoStream.getWidth(), videoStream.getHeight(), videoStream.getRealFrameRate());
for (Display.Mode mode : mDisplayModes) {
// Skip unwanted display modes
if (mode.getPhysicalWidth() < 1280 || mode.getPhysicalHeight() < 720) // Skip non-HD
@@ -309,8 +324,18 @@ public class PlaybackController {
if (rate != sourceRate && rate != sourceRate * 2 && rate != Math.round(sourceRate * 2.5)) // Skip inappropriate rates
continue;
int weight = Math.round(rate * 10000) + (9999 - (mode.getPhysicalWidth() - videoStream.getWidth()));
Timber.d("qualifying display mode: %sx%s @%sfps", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
int resolutionDifference = 0;
if (!(mode.getPhysicalWidth() == defaultMode.getPhysicalWidth() && mode.getPhysicalHeight() == defaultMode.getPhysicalHeight()))
resolutionDifference = mode.getPhysicalWidth() - videoStream.getWidth();
int refreshRateDifference = rate - sourceRate;
// use 100,000 to account for refresh rates 120Hz+ (at 120Hz rate == 12,000)
int weight = 100000 - refreshRateDifference + 100000 - resolutionDifference;
if (weight > curWeight) {
Timber.d("preferring mode: %sx%s @%sfps", mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
curWeight = weight;
bestMode = mode;
}
@@ -342,8 +367,6 @@ public class PlaybackController {
} else {
Timber.i("*** Unable to find display mode for refresh rate: %s", videoStream.getRealFrameRate());
}
}
// central place to update mCurrentPosition
@@ -753,7 +776,7 @@ public class PlaybackController {
// set playback speed to user selection, or 1 if we're watching live-tv
if (mVideoManager != null)
mVideoManager.setPlaybackSpeed(isLiveTv() ? 1.0 : mRequestedPlaybackSpeed);
mVideoManager.setPlaybackSpeed(isLiveTv() ? 1.0f : mRequestedPlaybackSpeed);
if (mFragment != null) mFragment.updateDisplay();

View File

@@ -32,6 +32,8 @@ public class PlaybackManager {
}
public ArrayList<MediaStream> getInPlaybackSelectableAudioStreams(StreamInfo info) {
if (info == null)
return null;
return info.GetSelectableAudioStreams();
}

View File

@@ -77,6 +77,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private int mVideoVisibleWidth;
private int mSarNum;
private int mSarDen;
private Integer exoplayerAudioIndex = null;
private long mForcedTime = -1;
private long mLastTime = -1;
@@ -144,6 +145,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
public void onTimelineChanged(Timeline timeline, int reason) {
Timber.d("Caught player timeline change - reason: %s", reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED ? "PLAYLIST_CHANGED" : "SOURCE_UPDATE");
}
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
Timber.d("Tracks info changed");
exoplayerAudioIndex = null;
}
});
}
@@ -290,6 +297,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
mActivity.finish();
return;
}
exoplayerAudioIndex = null;
mExoPlayer.setPlayWhenReady(true);
normalWidth = mExoPlayerView.getLayoutParams().width;
normalHeight = mExoPlayerView.getLayoutParams().height;
@@ -331,6 +339,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
mExoPlayer.setTrackSelectionParameters(mExoPlayer.getTrackSelectionParameters()
.buildUpon()
.setTrackSelectionOverrides(overrides).build());
exoplayerAudioIndex = null;
} else if (mVlcPlayer != null) {
mVlcPlayer.stop();
}
@@ -451,6 +460,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
if (streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
return -1;
if (exoplayerAudioIndex != null && streamType == MediaStreamType.Audio)
return exoplayerAudioIndex;
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
@@ -471,8 +483,12 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
Timber.d("failed to parse track ID [%s]", trackFormat.id);
return -1;
}
if (id >= 0 && id < allStreams.size())
if (id >= 0 && id < allStreams.size()) {
Timber.d("re-retrieved exoplayer track index %s", id);
if (streamType == MediaStreamType.Audio)
exoplayerAudioIndex = id;
return id;
}
}
return -1;
}
@@ -642,7 +658,17 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
return ndx;
}
public void setPlaybackSpeed(@NonNull Double speed) {
public float getPlaybackSpeed(){
if (!isInitialized()) {
return 1.0f;
} else if (isNativeMode()){
return mExoPlayer.getPlaybackParameters().speed;
} else {
return mVlcPlayer.getRate();
}
}
public void setPlaybackSpeed(@NonNull float speed) {
if (speed < 0.25) {
Timber.w("Invalid playback speed requested: %f", speed);
return;
@@ -650,9 +676,9 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
Timber.d("Setting playback speed: %f", speed);
if (nativeMode) {
mExoPlayer.setPlaybackParameters(new PlaybackParameters(speed.floatValue()));
mExoPlayer.setPlaybackParameters(new PlaybackParameters(speed));
} else {
mVlcPlayer.setRate(speed.floatValue());
mVlcPlayer.setRate(speed);
}
}

View File

@@ -3,16 +3,16 @@ package org.jellyfin.androidtv.ui.playback
class VideoSpeedController(
private val parentController: PlaybackController
) {
enum class SpeedSteps(val speed: Double) {
enum class SpeedSteps(val speed: Float) {
// 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),
SPEED_0_25(speed = 0.25f),
SPEED_0_50(speed = 0.5f),
SPEED_0_75(speed = 0.75f),
SPEED_1_00(speed = 1.0f),
SPEED_1_25(speed = 1.25f),
SPEED_1_50(speed = 1.50f),
SPEED_1_75(speed = 1.75f),
SPEED_2_00(speed = 2.0f),
}
companion object {

View File

@@ -1,5 +1,7 @@
package org.jellyfin.androidtv.ui.playback.overlay;
import static java.lang.Math.round;
import android.content.Context;
import android.os.Handler;
import android.text.format.DateFormat;
@@ -78,9 +80,8 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
this.playbackController = playbackController;
mRefreshEndTime = () -> {
setEndTime();
if (!isPlaying()) {
setEndTime();
mHandler.postDelayed(mRefreshEndTime, 30000);
}
};
@@ -211,8 +212,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
}
void addMediaActions() {
primaryActionsAdapter.clear();
secondaryActionsAdapter.clear();
if (primaryActionsAdapter.size() > 0)
primaryActionsAdapter.clear();
if (secondaryActionsAdapter.size() > 0)
secondaryActionsAdapter.clear();
// Primary Items
primaryActionsAdapter.add(playPauseAction);
@@ -296,6 +299,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
} else if (action == playbackSpeedAction) {
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
playbackSpeedAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
// Post a callback to calculate the new time, since Exoplayer updates this in an async fashion.
// This is a hack, we should instead have onPlaybackParametersChanged call out to this
// class to notify rather than poll. But communication is unidirectional at the moment:
mHandler.postDelayed(mRefreshEndTime, 5000); // 5 seconds
} else if (action == adjustAudioDelayAction) {
getPlayerAdapter().getLeanbackOverlayFragment().setFading(false);
adjustAudioDelayAction.handleClickAction(playbackController, getPlayerAdapter().getLeanbackOverlayFragment(), getContext(), view);
@@ -323,9 +330,11 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
if (mEndsText == null || getPlayerAdapter().getDuration() < 1)
return;
long msLeft = getPlayerAdapter().getDuration() - getPlayerAdapter().getCurrentPosition();
Calendar ends = Calendar.getInstance();
ends.setTimeInMillis(ends.getTimeInMillis() + msLeft);
mEndsText.setText(getContext().getString(R.string.lbl_playback_control_ends, DateFormat.getTimeFormat(getContext()).format(ends.getTime())));
long realTimeLeft = round(msLeft / playbackController.getPlaybackSpeed());
Calendar endTime = Calendar.getInstance();
endTime.setTimeInMillis(endTime.getTimeInMillis() + realTimeLeft);
mEndsText.setText(getContext().getString(R.string.lbl_playback_control_ends, DateFormat.getTimeFormat(getContext()).format(endTime.getTime())));
}
private void notifyActionChanged(Action action) {
@@ -378,8 +387,10 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
}
void invalidatePlaybackControls() {
primaryActionsAdapter.clear();
secondaryActionsAdapter.clear();
if (primaryActionsAdapter.size() > 0)
primaryActionsAdapter.clear();
if (secondaryActionsAdapter.size() > 0)
secondaryActionsAdapter.clear();
addMediaActions();
}

View File

@@ -158,6 +158,10 @@ public class VideoPlayerAdapter extends PlayerAdapter {
return playbackController.getCurrentlyPlayingItem();
}
public double getPlaybackSpeed() {
return playbackController.getPlaybackSpeed();
}
boolean hasChapters() {
BaseItemDto item = getCurrentlyPlayingItem();
List<ChapterInfoDto> chapters = item.getChapters();

View File

@@ -27,6 +27,10 @@ public class SelectAudioAction extends CustomAction {
public void handleClickAction(PlaybackController playbackController, LeanbackOverlayFragment leanbackOverlayFragment, Context context, View view) {
List<MediaStream> audioTracks = KoinJavaComponent.<PlaybackManager>get(PlaybackManager.class).getInPlaybackSelectableAudioStreams(playbackController.getCurrentStreamInfo());
if (audioTracks == null)
return;
int currentAudioIndex = playbackController.getAudioStreamIndex();
PopupMenu audioMenu = new PopupMenu(context, view, Gravity.END);

View File

@@ -11,8 +11,9 @@ fun OptionsScreen.aboutCategory() = category {
setTitle(R.string.pref_about_title)
link {
// Hardcoded strings for troubleshooting purposes
title = "Jellyfin app version"
content = BuildConfig.VERSION_NAME
content = "jellyfin-androidtv ${BuildConfig.VERSION_NAME} ${BuildConfig.BUILD_TYPE}"
icon = R.drawable.ic_jellyfin
}

View File

@@ -41,7 +41,7 @@ import java.util.HashMap;
import timber.log.Timber;
public class CardPresenter extends Presenter {
private static final double ASPECT_RATIO_BANNER = 434 / 79;
private static final double ASPECT_RATIO_BANNER = 1000 / 185;
private int mStaticHeight = 300;
private ImageType mImageType = ImageType.DEFAULT;

View File

@@ -160,7 +160,7 @@ fun LegacyBaseItemDto.asSdk(): ModernBaseItemDto = ModernBaseItemDto(
parentArtItemId = this.parentArtItemId,
parentArtImageTag = this.parentArtImageTag,
seriesThumbImageTag = this.seriesThumbImageTag,
imageBlurHashes = this.imageBlurHashes.asSdk(),
imageBlurHashes = this.imageBlurHashes?.asSdk(),
seriesStudio = this.seriesStudio,
parentThumbItemId = this.parentThumbItemId,
parentThumbImageTag = this.parentThumbImageTag,

View File

@@ -1,16 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Stored servers & users (without access tokens) -->
<include
domain="file"
path="authentication_store.json" />
<!-- UserPreferences -->
<include
domain="sharedpref"
path="org.jellyfin.androidtv_preferences" />
<include
domain="sharedpref"
path="org.jellyfin.androidtv.debug_preferences" />
<!-- SystemPreferences -->
<include
domain="sharedpref"
path="systemprefs" />
<!-- AuthenticationPreferences -->
<include
domain="sharedpref"
path="AuthenticationPreferences" />
</full-backup-content>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<!-- Stored servers & users (without access tokens) -->
<include
domain="file"
path="authentication_store.json" />
<!-- UserPreferences -->
<include
domain="sharedpref"
path="org.jellyfin.androidtv_preferences" />
<include
domain="sharedpref"
path="org.jellyfin.androidtv.debug_preferences" />
<!-- SystemPreferences -->
<include
domain="sharedpref"
path="systemprefs" />
<!-- AuthenticationPreferences -->
<include
domain="sharedpref"
path="AuthenticationPreferences" />
</cloud-backup>
<device-transfer>
<!-- Stored servers & users (without access tokens) -->
<include
domain="file"
path="authentication_store.json" />
<!-- UserPreferences -->
<include
domain="sharedpref"
path="org.jellyfin.androidtv_preferences" />
<include
domain="sharedpref"
path="org.jellyfin.androidtv.debug_preferences" />
<!-- SystemPreferences -->
<include
domain="sharedpref"
path="systemprefs" />
<!-- AuthenticationPreferences -->
<include
domain="sharedpref"
path="AuthenticationPreferences" />
</device-transfer>
</data-extraction-rules>

View File

@@ -1,7 +1,7 @@
package org.jellyfin.androidtv.ui.playback
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.doubles.plusOrMinus
import io.kotest.matchers.floats.plusOrMinus
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.justRun
@@ -17,19 +17,19 @@ class VideoSpeedControllerTests : FunSpec({
test("VideoSpeedController.SpeedSteps uses intervals of 0.25") {
VideoSpeedController.SpeedSteps.values().forEachIndexed { i, v ->
v.speed.shouldBe((i + 1) * 0.25 plusOrMinus 0.0001)
v.speed.shouldBe((i + 1) * 0.25f plusOrMinus 1.0E-4F)
}
}
test("VideoSpeedController speed is 1 by default") {
val mockController = mockk<PlaybackController>(relaxed = true)
val slot = slot<Double>()
val slot = slot<Float>()
justRun { mockController.setPlaybackSpeed(capture(slot)) }
VideoSpeedController(mockController)
verify { mockController.setPlaybackSpeed(any()) }
slot.captured shouldBe (1.0 plusOrMinus 0.0001)
slot.captured shouldBe (1.0f plusOrMinus 1.0E-4F)
}
test("VideoSpeedController.currentSpeed getter returns set value") {
@@ -44,7 +44,7 @@ class VideoSpeedControllerTests : FunSpec({
test("VideoSpeedController.currentSpeed updates the speed in the controller") {
val mockController = mockk<PlaybackController>(relaxed = true)
val slot = slot<Double>()
val slot = slot<Float>()
justRun { mockController.setPlaybackSpeed(capture(slot)) }
val controller = VideoSpeedController(mockController)
@@ -52,7 +52,7 @@ class VideoSpeedControllerTests : FunSpec({
controller.currentSpeed = expected
verify { mockController.setPlaybackSpeed(any()) }
slot.captured shouldBe (expected.speed plusOrMinus 0.0001)
slot.captured shouldBe (expected.speed plusOrMinus 1.0E-4F)
}
test("VideoSpeedController.resetSpeedToDefault() works correctly") {
@@ -62,26 +62,26 @@ class VideoSpeedControllerTests : FunSpec({
videoController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00
videoController.resetSpeedToDefault()
val slot = slot<Double>()
val slot = slot<Float>()
justRun { playbackController.setPlaybackSpeed(capture(slot)) }
VideoSpeedController(playbackController)
verify { playbackController.setPlaybackSpeed(any()) }
slot.captured shouldBe (VideoSpeedController.SpeedSteps.SPEED_1_00.speed plusOrMinus 0.0001)
slot.captured shouldBe (VideoSpeedController.SpeedSteps.SPEED_1_00.speed plusOrMinus 1.0E-4F)
}
test("VideoSpeedController remembers previous instance speed value") {
var lastSetSpeed = 1.0
var lastSetSpeed = 1.0f
VideoSpeedController.SpeedSteps.values().forEach { newSpeed ->
val mockController = mockk<PlaybackController>(relaxed = true)
val slot = slot<Double>()
val slot = slot<Float>()
justRun { mockController.setPlaybackSpeed(capture(slot)) }
val controller = VideoSpeedController(mockController)
verify { mockController.setPlaybackSpeed(any()) }
slot.captured shouldBe (lastSetSpeed plusOrMinus 0.0001)
slot.captured shouldBe (lastSetSpeed plusOrMinus 1.0E-4F)
controller.currentSpeed = newSpeed
lastSetSpeed = newSpeed.speed
@@ -101,13 +101,13 @@ class VideoSpeedControllerTests : FunSpec({
}
val speedController = VideoSpeedController(mockController)
verify { mockController.setPlaybackSpeed(1.0) }
verify { mockController.setPlaybackSpeed(1.0f) }
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00
// Try to set it back to other values should be ignored
speedController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00
verify { mockController.setPlaybackSpeed(1.0) }
verify { mockController.setPlaybackSpeed(1.0f) }
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00
}
@@ -117,12 +117,12 @@ class VideoSpeedControllerTests : FunSpec({
}
val speedController = VideoSpeedController(mockController)
verify { mockController.setPlaybackSpeed(1.0) }
verify { mockController.setPlaybackSpeed(1.0f) }
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_1_00
speedController.currentSpeed = VideoSpeedController.SpeedSteps.SPEED_2_00
verify { mockController.setPlaybackSpeed(2.0) }
verify { mockController.setPlaybackSpeed(2.0f) }
speedController.currentSpeed shouldBe VideoSpeedController.SpeedSteps.SPEED_2_00
}
})

View File

@@ -0,0 +1,3 @@
Thank you for using Jellyfin on Android TV! Using the latest released Jellyfin Server is always recommended.
For more information, please see our blog at jellyfin.org, or read the full changelog on GitHub.

View File

@@ -0,0 +1,14 @@
Your media, on your terms.
The Jellyfin project is an open source, free software media server. No fees, no tracking, no hidden agenda. Get our free server to collect all your audio, video, photos, and more in one place.
To use the app, you must have a Jellyfin server set up and running. Find out more at <a href="https://jellyfin.org/" target="_blank">jellyfin.org</a>.
With a Jellyfin server, you can:
* Watch Live TV and recorded shows from your Jellyfin server (additional hardware/services required)
* Stream to a Chromecast device on your network
* Stream your media to your Android device
* View your collection in an easy to use interface
This is the official Jellyfin companion app for Android TV. Thank you for using Jellyfin!

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1 @@
Television client for Jellyfin, the free software media system

View File

@@ -0,0 +1 @@
Jellyfin for Android TV