Compare commits

...

21 Commits

Author SHA1 Message Date
gradle-update-robot
9ea2b81b28 Update Gradle Wrapper from 7.4.1 to 7.4.2.
Signed-off-by: gradle-update-robot <gradle-update-robot@regolo.cc>
(cherry picked from commit 65ebd3dac8)
2022-04-09 11:26:17 +02:00
Cameron
c568862ee1 fix getSubtitleStreamIndex returning the wrong index (#1594)
* dont use mDefaultSubIndex in getSubtitleStreamIndex

* only set mCurrentOptions SubtitleStreamIndex if subtitles load without error

* clear mDefaultSubIndex in switchSubtitleStream

(cherry picked from commit 5fd89ed8c9)
2022-04-09 11:15:58 +02:00
Niels van Velzen
42b767f619 Fix ASPECT_RATIO_BANNER again
(cherry picked from commit 7d1a653c10)
2022-04-09 11:15:48 +02:00
Niels van Velzen
ebf7fc4244 Add appCategory attribute to manifest
(cherry picked from commit e2cdab31ea)
2022-04-09 11:15:35 +02:00
Niels van Velzen
102642d58f Fix ExpandableTextView focusable when text is not ellipsized
(cherry picked from commit db3af65226)
2022-04-09 11:15:27 +02:00
Cameron
5115e1181e fixed subtitles not clearing properly when switching (again) (#1571)
* fixed subtitles not clearing properly when switching (again)

* edited a couple comments so they're clearer

(cherry picked from commit df015146e2)
2022-04-09 11:15:17 +02:00
David Fairbrother
f70ac357b6 Switch to using audio time stretching by default
This matches libVLC's normal default. Looking back in the project
history this was set to no stretching from the very start @commit
9528b9a3ee

Since libVLC use it by default and this wasn't a bug fix I propose we
can opt in the audio stretching by default. This should only trigger when
a user selects a playback speed that isn't 1x anyway.

(cherry picked from commit cfbadba4e3)
2022-04-09 11:15:03 +02:00
cameron
7136341c3c center align subtitles
(cherry picked from commit 079d004f2a)
2022-04-09 11:14:51 +02:00
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
32 changed files with 283 additions and 128 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 -->
@@ -38,15 +40,16 @@
<application
android:name=".JellyfinApplication"
android:allowBackup="true"
android:appCategory="video"
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

@@ -44,10 +44,12 @@ class ExpandableTextView(context: Context, attrs: AttributeSet?) : AppCompatText
}
}
override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
isFocusable = !text.isNullOrBlank()
isClickable = !text.isNullOrBlank()
// Only make focusable when text is ellipsized.
val isEllipsized = !text.isNullOrBlank() && lineCount > 0 && layout.getEllipsisCount(lineCount - 1) > 0
isFocusable = isEllipsized
isClickable = isEllipsized
}
}

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);
@@ -205,7 +214,7 @@ public class PlaybackController {
}
public int getSubtitleStreamIndex() {
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : mDefaultSubIndex;
return (mCurrentOptions != null && mCurrentOptions.getSubtitleStreamIndex() != null) ? mCurrentOptions.getSubtitleStreamIndex() : -1;
}
public List<SubtitleStreamInfo> getSubtitleStreams() {
@@ -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
@@ -381,8 +404,8 @@ public class PlaybackController {
play(position, null);
}
private void play(long position, @Nullable Integer transcodedSubtitle) {
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, transcodedSubtitle);
private void play(long position, @Nullable Integer forcedSubtitleIndex) {
Timber.d("Play called from state: %s with pos: %d and sub index: %d", mPlaybackState, position, forcedSubtitleIndex);
if (position < 0) {
Timber.i("Negative start requested - adjusting to zero");
@@ -491,8 +514,8 @@ public class PlaybackController {
vlcOptions.setEnableDirectStream(false);
vlcOptions.setEnableDirectPlay(false);
}
vlcOptions.setSubtitleStreamIndex(transcodedSubtitle);
vlcOptions.setMediaSourceId(transcodedSubtitle != null ? getCurrentMediaSource().getId() : null);
vlcOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
vlcOptions.setMediaSourceId(forcedSubtitleIndex != null ? getCurrentMediaSource().getId() : null);
DeviceProfile vlcProfile = new LibVlcProfile(isLiveTv);
vlcOptions.setProfile(vlcProfile);
@@ -503,8 +526,8 @@ public class PlaybackController {
if (exoErrorEncountered || (isLiveTv && !directStreamLiveTv))
internalOptions.setEnableDirectStream(false);
internalOptions.setMaxAudioChannels(Utils.downMixAudio() ? 2 : null); //have to downmix at server
internalOptions.setSubtitleStreamIndex(transcodedSubtitle);
internalOptions.setMediaSourceId(transcodedSubtitle != null ? getCurrentMediaSource().getId() : null);
internalOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
internalOptions.setMediaSourceId(forcedSubtitleIndex != null ? getCurrentMediaSource().getId() : null);
DeviceProfile internalProfile = new BaseProfile();
if (DeviceUtils.is60() || userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled())) {
boolean hlsSupported = false;
@@ -729,6 +752,8 @@ public class PlaybackController {
mCurrentOptions.setSubtitleStreamIndex(mDefaultSubIndex);
Timber.d("stream started with burnt in subs");
burningSubs = true;
} else {
mCurrentOptions.setSubtitleStreamIndex(null);
}
Long mbPos = position * 10000;
@@ -753,7 +778,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();
@@ -882,20 +907,23 @@ public class PlaybackController {
public void switchSubtitleStream(int index) {
if (!hasInitializedVideoManager())
return;
// get current timestamp first
refreshCurrentPosition();
Timber.d("Setting subtitle index to: %d", index);
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;
// clear subtitles first
if (mFragment != null) mFragment.addManualSubtitles(null);
mVideoManager.disableSubs();
// clear the default in case there's an error loading the subtitles
mDefaultSubIndex = -1;
// handle setting subtitles as disabled
// restart playback if turning off burnt-in subtitles
if (index < 0) {
mCurrentOptions.setSubtitleStreamIndex(-1);
if (burningSubs) {
stop();
play(mCurrentPosition, -1);
} else {
if (mFragment != null) mFragment.addManualSubtitles(null);
mVideoManager.disableSubs();
}
return;
}
@@ -906,77 +934,77 @@ public class PlaybackController {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.subtitle_error));
return;
}
// handle according to delivery method
SubtitleStreamInfo streamInfo = getSubtitleStreamInfo(index);
if (streamInfo == null) {
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
} else {
switch (streamInfo.getDeliveryMethod()) {
return;
}
case Encode:
// Gonna need to burn in so start a transcode with the sub index
stop();
if (!mVideoManager.isNativeMode()) {
// handle switching on or between burnt-in subtitles, or switching to non-burnt subtitles
// if switching from burnt-in subtitles to another type, playback still needs to be restarted
if (burningSubs || streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode) {
stop();
if (mFragment != null && streamInfo.getDeliveryMethod() == SubtitleDeliveryMethod.Encode)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
play(mCurrentPosition, index);
return;
}
// when burnt-in subtitles are selected, mCurrentOptions SubtitleStreamIndex is set in startItem() as soon as playback starts
// otherwise mCurrentOptions SubtitleStreamIndex is kept null until now so we knew subtitles needed to be enabled but weren't already
switch (streamInfo.getDeliveryMethod()) {
case Embed:
if (!mVideoManager.isNativeMode()) {
if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) {
// error selecting internal subs
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
} else {
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;
}
play(mCurrentPosition, index);
break;
case Embed:
if (!mVideoManager.isNativeMode()) {
if (mFragment != null)
mFragment.addManualSubtitles(null); // in case these were on
if (!mVideoManager.setSubtitleTrack(index, getCurrentlyPlayingItem().getMediaStreams())) {
// error selecting internal subs
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
}
break;
}
// not using vlc - fall through to external handling
case External:
if (mFragment != null) mFragment.addManualSubtitles(null);
mVideoManager.disableSubs();
if (mFragment != null) mFragment.showSubLoadingMsg(true);
stream.setDeliveryMethod(SubtitleDeliveryMethod.External);
stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", apiClient.getValue().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), String.valueOf(stream.getIndex())));
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
}
// not using vlc - fall through to external handling
case External:
if (mFragment != null) mFragment.showSubLoadingMsg(true);
stream.setDeliveryMethod(SubtitleDeliveryMethod.External);
stream.setDeliveryUrl(String.format("%1$s/Videos/%2$s/%3$s/Subtitles/%4$s/0/Stream.JSON", apiClient.getValue().getApiUrl(), mCurrentStreamInfo.getItemId(), mCurrentStreamInfo.getMediaSourceId(), String.valueOf(stream.getIndex())));
apiClient.getValue().getSubtitles(stream.getDeliveryUrl(), new Response<SubtitleTrackInfo>() {
@Override
public void onResponse(final SubtitleTrackInfo info) {
@Override
public void onResponse(final SubtitleTrackInfo info) {
if (info != null) {
Timber.d("Adding json subtitle track to player");
if (mFragment != null) mFragment.addManualSubtitles(info);
} else {
Timber.e("Empty subtitle result");
if (mFragment != null) {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
mFragment.showSubLoadingMsg(false);
}
}
}
@Override
public void onError(Exception ex) {
Timber.e(ex, "Error downloading subtitles");
if (info != null) {
Timber.d("Adding json subtitle track to player");
if (mFragment != null) mFragment.addManualSubtitles(info);
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;
} else {
Timber.e("Empty subtitle result");
if (mFragment != null) {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
mFragment.showSubLoadingMsg(false);
}
}
}
});
break;
case Hls:
break;
}
@Override
public void onError(Exception ex) {
Timber.e(ex, "Error downloading subtitles");
if (mFragment != null) {
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_unable_load_subs));
mFragment.showSubLoadingMsg(false);
}
}
});
break;
case Hls:
break;
}
}
public void pause() {
@@ -1396,7 +1424,6 @@ public class PlaybackController {
if (mPlaybackState == PlaybackState.PAUSED) {
mPlaybackState = PlaybackState.PLAYING;
} else {
// select or disable subtitles
Integer currentSubtitleIndex = mCurrentOptions.getSubtitleStreamIndex();
if (mDefaultSubIndex >= 0 && currentSubtitleIndex != null && currentSubtitleIndex == mDefaultSubIndex) {

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);
}
}
@@ -720,7 +746,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
// Create a new media player
ArrayList<String> options = new ArrayList<>(20);
options.add("--network-caching=" + buffer);
options.add("--no-audio-time-stretch");
options.add("--audio-time-stretch");
options.add("--avcodec-skiploopfilter");
options.add("1");
options.add("--avcodec-skip-frame");

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.0 / 185.0;
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

@@ -54,6 +54,7 @@
android:layout_marginBottom="48dp"
android:textColor="@color/white"
android:textSize="28sp"
android:gravity="center"
app:strokeWidth="5.0"
tools:text="Subtitles" />

View File

@@ -147,7 +147,7 @@
<string name="lbl_filters">Filters</string>
<string name="msg_subtitles_loading">Subtitles loading…</string>
<string name="msg_unable_load_subs">Unable to load subtitle</string>
<string name="msg_burn_sub_warning">Burned in subs can take a few minutes. Please be patient</string>
<string name="msg_burn_sub_warning">Burning in subtitles. This may take a few seconds</string>
<string name="lbl_recording">Recording</string>
<string name="lbl_no_recordings">No Recordings Available</string>
<string name="lbl_live_tv_guide">Live TV Guide</string>

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

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=e5444a57cda4a95f90b0c9446a9e1b47d3d7f69057765bfb54bd4f482542d548
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists