Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b49af4c42 | ||
|
|
fa59595dd5 | ||
|
|
7c3f495036 | ||
|
|
d05ef3d810 | ||
|
|
f5c875959a | ||
|
|
b45700739a | ||
|
|
a0430a13d1 | ||
|
|
9dfe855940 | ||
|
|
d09c45f74f | ||
|
|
16d6b6b2ed | ||
|
|
75e8938db3 | ||
|
|
dff36669bf | ||
|
|
1953508209 |
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ public class PlaybackManager {
|
||||
}
|
||||
|
||||
public ArrayList<MediaStream> getInPlaybackSelectableAudioStreams(StreamInfo info) {
|
||||
if (info == null)
|
||||
return null;
|
||||
return info.GetSelectableAudioStreams();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
47
app/src/main/res/xml/backup_rules.xml
Normal 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>
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/default.txt
Normal 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.
|
||||
14
fastlane/metadata/android/en-US/full_description.txt
Normal 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!
|
||||
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
fastlane/metadata/android/en-US/images/tvBanner.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
fastlane/metadata/android/en-US/images/tvScreenshots/1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
fastlane/metadata/android/en-US/images/tvScreenshots/2.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
fastlane/metadata/android/en-US/images/tvScreenshots/3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Television client for Jellyfin, the free software media system
|
||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Jellyfin for Android TV
|
||||