Compare commits

...

36 Commits

Author SHA1 Message Date
Andy Walsh
786e8d7a33 fix, don't send invalid sub/audio index
fix DlnaProfileType

(cherry picked from commit 1fa68ab51e)
2022-07-09 17:01:27 +02:00
Andy Walsh
36fd201113 add AVC "high 10" profile test
enable AllowVideoStreamCopy, setAllowAudioStream

(cherry picked from commit 90d0e7ed13)
2022-07-09 17:01:03 +02:00
Cameron
21c7b6de09 fix subtitle background alignment, center subtitle text (#1786)
* fix subtitle background alignment, center subtitle text

* add suppress RtlHardcoded annotation

(cherry picked from commit affdb6dd75)
2022-07-09 17:00:35 +02:00
Niels van Velzen
e2a8c7a285 Hide channel number from ChannelCardView when null
(cherry picked from commit a6c9fd0ac6)
2022-07-09 16:59:46 +02:00
Niels van Velzen
683c326c0c Disable ACRA crash reporting 2022-07-08 22:26:38 +02:00
Cameron
9bbbfa4e26 fix player track selection - offset MediaStream indexes to account for external tracks (#1732)
* offset mediastream indexes used for player track selection to account for external tracks

* use imports for libVLC classes

(cherry picked from commit 5e767ad7d1)
2022-05-28 13:59:49 +02:00
Niels van Velzen
050110922c Fix favorite button in 0.13.z 2022-05-09 16:37:52 +02:00
Niels van Velzen
f97db4643e Use JF_BOT_TOKEN in app-publish workflow
(cherry picked from commit 876e6bc5e9)
2022-05-09 16:35:15 +02:00
dependabot[bot]
1715dc02b7 Bump github/codeql-action from 1 to 2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 57e814844c)
2022-05-05 22:34:04 +02:00
dependabot[bot]
23c8b42e79 Bump actions/setup-java from 2 to 3
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 3.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 7b1b678fcd)
2022-05-05 22:33:56 +02:00
naveensrinivasan
3ddcf4d284 Set permissions for GitHub actions
- Included permissions for the action. https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions

https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions

https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs

[Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)

 Restrict the GitHub token permissions only to the required ones; this way, even if the attackers will succeed in compromising your workflow, they won’t be able to do much.

Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>
(cherry picked from commit f76ad3765e)
2022-05-05 22:33:41 +02:00
cameron
6a97e9b05b revert subtitle alignment back to left align, fix srt newline parsing
(cherry picked from commit 408f6073d0)
2022-05-05 19:37:45 +02:00
Cameron
6af1df5122 Use enum for refresh rate switching setting - allow choosing between scaling on device or on tv (#1647)
* use enum for refresh rate switching preference

* fixed enum value comments

* renamed preference key

* log every considered mode, restored accidentally deleted string

* added missing brackets

* updated comment for migration, use original depends for preference screen

* renamed string

* offset default resolutionDifference by 1 to fix SCALE_ON_DEVICE ranking

(cherry picked from commit 6dafe0a9d0)
2022-05-05 19:37:37 +02:00
cameron
68f0edca71 initialize video manager when falling back to vlc
(cherry picked from commit 04967b5811)
2022-05-05 19:36:26 +02:00
Niels van Velzen
f5fda8c7de Fix button remapping not saving the new button
(cherry picked from commit 9feaf107b2)
2022-05-05 19:36:03 +02:00
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
48 changed files with 562 additions and 321 deletions

View File

@@ -6,6 +6,9 @@ on:
- master
pull_request:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-20.04
@@ -13,7 +16,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
distribution: adopt
java-version: 11

View File

@@ -7,14 +7,20 @@ on:
- release-*
pull_request:
permissions:
contents: read
jobs:
lint:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
runs-on: ubuntu-20.04
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
distribution: adopt
java-version: 11
@@ -22,7 +28,7 @@ jobs:
- name: Run detekt and lint task
run: ./gradlew --build-cache --no-daemon --info detekt lint
- name: Upload SARIF files
uses: github/codeql-action/upload-sarif@v1
uses: github/codeql-action/upload-sarif@v2
if: ${{ always() }}
with:
sarif_file: .

View File

@@ -13,7 +13,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
distribution: adopt
java-version: 11
@@ -50,7 +50,7 @@ jobs:
- name: Upload release archive to GitHub release
uses: alexellis/upload-assets@0.3.0
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
with:
asset_paths: '["build/jellyfin-publish/*"]'
- name: Upload release archive to repo.jellyfin.org

View File

@@ -7,6 +7,9 @@ on:
- release-*
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-20.04
@@ -14,7 +17,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
distribution: adopt
java-version: 11

View File

@@ -8,6 +8,9 @@ on:
paths:
- '**/gradlе-wrapper.jar'
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-20.04

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

@@ -1,6 +1,5 @@
package org.jellyfin.androidtv
import android.content.Context
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
@@ -13,12 +12,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.acra.config.dialog
import org.acra.config.httpSender
import org.acra.config.limiter
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.jellyfin.androidtv.auth.SessionRepository
import org.jellyfin.androidtv.integration.LeanbackChannelWorker
import org.jellyfin.androidtv.util.AutoBitrate
@@ -77,26 +70,4 @@ class JellyfinApplication : TvApp() {
// Detect auto bitrate
GlobalScope.launch(Dispatchers.IO) { autoBitrate.detect() }
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
httpSender {
uri = "https://collector.tracepot.com/a2eda9d9"
httpMethod = HttpSender.Method.POST
}
dialog {
withResTitle(R.string.acra_dialog_title)
withResText(R.string.acra_dialog_text)
withResTheme(R.style.Theme_Jellyfin)
}
limiter {}
}
}
}

View File

@@ -10,6 +10,7 @@ import org.jellyfin.androidtv.preference.constant.ClockBehavior
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
import org.jellyfin.androidtv.preference.constant.RatingType
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.preference.constant.WatchedIndicatorBehavior
import org.jellyfin.androidtv.preference.constant.defaultAudioBehavior
import org.jellyfin.androidtv.util.DeviceUtils
@@ -90,9 +91,9 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
var videoPlayer = Preference.enum("video_player", PreferredVideoPlayer.EXOPLAYER)
/**
* Enable refresh rate switching when device supports it
* Change refresh rate to match media when device supports it
*/
var refreshRateSwitchingEnabled = Preference.boolean("pref_refresh_switching", false)
var refreshRateSwitchingBehavior = Preference.enum("refresh_rate_switching_behavior", RefreshRateSwitchingBehavior.DISABLED)
/**
* Send a path instead to the external player
@@ -230,6 +231,15 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
// Disable AC3 (Dolby Digital) on Fire Stick Gen 1 devices
if (DeviceUtils.isFireTvStickGen1()) putBoolean("pref_bitstream_ac3", false)
}
// v0.13.3 to v0.13.4
migration(toVersion = 6) {
putEnum("refresh_rate_switching_behavior",
when {
it.getBoolean("pref_refresh_switching", false) -> RefreshRateSwitchingBehavior.SCALE_ON_TV
else -> RefreshRateSwitchingBehavior.DISABLED
})
}
}
}
}

View File

@@ -0,0 +1,22 @@
package org.jellyfin.androidtv.preference.constant
import org.jellyfin.androidtv.R
import org.jellyfin.androidtv.ui.preference.dsl.EnumDisplayOptions
enum class RefreshRateSwitchingBehavior {
@EnumDisplayOptions(R.string.state_disabled)
DISABLED,
/**
* When comparing modes, use difference in resolution to rank modes.
*/
@EnumDisplayOptions(R.string.pref_refresh_rate_scale_on_tv)
SCALE_ON_TV,
/**
* When comparing modes, rank native resolution modes highest.
* Otherwise use difference in resolution to rank modes.
*/
@EnumDisplayOptions(R.string.pref_refresh_rate_scale_on_device)
SCALE_ON_DEVICE,
}

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

@@ -22,7 +22,9 @@ public class ChannelCardView extends FrameLayout {
}
public void setItem(final ChannelInfoDto channel) {
binding.name.setText(channel.getNumber() + " " + channel.getName());
if (channel.getNumber() != null) binding.name.setText(channel.getNumber() + " " + channel.getName());
else binding.name.setText(channel.getName());
BaseItemDto program = channel.getCurrentProgram();
if (program != null) {
if (program.getEndDate() != null && System.currentTimeMillis() > TimeUtils.convertToLocalDate(program.getEndDate()).getTime()) {

View File

@@ -15,6 +15,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
@@ -622,7 +623,7 @@ public class ItemListActivity extends FragmentActivity {
@Override
public void onResponse(UserItemDataDto response) {
mBaseItem.setUserData(response);
((TextUnderButton)v).setImageResource(response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart);
((ImageButton)v).setImageResource(response.getIsFavorite() ? R.drawable.ic_heart_red : R.drawable.ic_heart);
dataRefreshService.getValue().setLastFavoriteUpdate(System.currentTimeMillis());
}
});

View File

@@ -13,6 +13,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
import android.text.SpannableString;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@@ -1258,6 +1259,7 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
}
public void setCurrentTime(long time) {
if (leanbackOverlayFragment != null)
leanbackOverlayFragment.updateCurrentPosition();
}
@@ -1482,13 +1484,19 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
// Encode whitespace as html entities
final String htmlText = text
.replaceAll("\\r\\n", "<br>")
.replaceAll("\\n", "<br>")
.replaceAll("\\\\h", "&ensp;");
final SpannableString span = new SpannableString(TextUtilsKt.toHtmlSpanned(htmlText));
if (subtitlesBackgroundEnabled) {
// Disable the text outlining when the background is enabled
binding.subtitlesText.setStrokeWidth(0.0f);
span.setSpan(new PaddedLineBackgroundSpan(ContextCompat.getColor(requireContext(), R.color.black_opaque), SUBTITLE_PADDING), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// get the alignment gravity of the TextView
// extract the absolute horizontal gravity so the span can draw its background aligned
int gravity = binding.subtitlesText.getGravity();
int horizontalGravity = Gravity.getAbsoluteGravity(gravity, binding.subtitlesText.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
span.setSpan(new PaddedLineBackgroundSpan(ContextCompat.getColor(requireContext(), R.color.black_opaque), SUBTITLE_PADDING, horizontalGravity), 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
binding.subtitlesText.setText(span);

View File

@@ -90,8 +90,11 @@ public class GetPlaybackInfoResponse extends Response<PlaybackInfoResponse> {
streamInfo = new StreamInfo();
streamInfo.setItemId(options.getItemId());
if (isVideo) {
streamInfo.setMediaType(DlnaProfileType.Video);
} else {
streamInfo.setMediaType(DlnaProfileType.Audio);
}
streamInfo.setMediaSource(mediaSourceInfo);
streamInfo.setRunTimeTicks(mediaSourceInfo.getRunTimeTicks());

View File

@@ -25,11 +25,11 @@ import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.preference.UserSettingPreferences;
import org.jellyfin.androidtv.preference.constant.NextUpBehavior;
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer;
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior;
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 +88,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;
@@ -116,15 +116,18 @@ public class PlaybackController {
private long lastPlaybackError = 0;
private Display.Mode[] mDisplayModes;
private boolean refreshRateSwitchingEnabled;
private RefreshRateSwitchingBehavior refreshRateSwitchingBehavior = RefreshRateSwitchingBehavior.DISABLED;
public PlaybackController(List<BaseItemDto> items, CustomPlaybackOverlayFragment fragment) {
mItems = items;
mFragment = fragment;
mHandler = new Handler();
refreshRateSwitchingEnabled = DeviceUtils.is60() && userPreferences.getValue().get(UserPreferences.Companion.getRefreshRateSwitchingEnabled());
if (refreshRateSwitchingEnabled) getDisplayModes();
if (DeviceUtils.is60()) {
refreshRateSwitchingBehavior = userPreferences.getValue().get(UserPreferences.Companion.getRefreshRateSwitchingBehavior());
if (refreshRateSwitchingBehavior != RefreshRateSwitchingBehavior.DISABLED)
getDisplayModes();
}
// Set default value for useVlc field
// when set to auto the default will be exoplayer
@@ -163,7 +166,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 +218,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,23 +294,31 @@ 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());
Timber.d("display mode %s - %dx%d@%f", mDisplayMode.getModeId(), mDisplayMode.getPhysicalWidth(), mDisplayMode.getPhysicalHeight(), 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: %dx%d@%f", videoStream.getWidth(), videoStream.getHeight(), videoStream.getRealFrameRate());
for (Display.Mode mode : mDisplayModes) {
Timber.d("considering display mode: %s - %dx%d@%f", mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
// Skip unwanted display modes
if (mode.getPhysicalWidth() < 1280 || mode.getPhysicalHeight() < 720) // Skip non-HD
continue;
@@ -309,8 +330,28 @@ 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: %s - %dx%d@%f", mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
// if scaling on-device, keep native resolution modes at diff 0 (best score)
// for other resolutions when scaling on device, or if scaling on tv, score based on distance from media resolution
// use -1 as the default so, with SCALE_ON_DEVICE, a mode at native resolution will rank higher than
// a mode with equal refresh rate and the same resolution as the media
int resolutionDifference = -1;
if ((refreshRateSwitchingBehavior == RefreshRateSwitchingBehavior.SCALE_ON_DEVICE &&
!(mode.getPhysicalWidth() == defaultMode.getPhysicalWidth() && mode.getPhysicalHeight() == defaultMode.getPhysicalHeight())) ||
refreshRateSwitchingBehavior == RefreshRateSwitchingBehavior.SCALE_ON_TV) {
resolutionDifference = Math.abs(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: %s - %dx%d@%f", mode.getModeId(), mode.getPhysicalWidth(), mode.getPhysicalHeight(), mode.getRefreshRate());
curWeight = weight;
bestMode = mode;
}
@@ -332,7 +373,8 @@ public class PlaybackController {
Timber.i("*** Best refresh mode is: %s - %dx%d/%f",
best.getModeId(), best.getPhysicalWidth(), best.getPhysicalHeight(), best.getRefreshRate());
if (current.getModeId() != best.getModeId()) {
Timber.i("*** Attempting to change refresh rate from %s/%s", current.getModeId(), current.getRefreshRate());
Timber.i("*** Attempting to change refresh rate from: %s - %dx%d@%f", current.getModeId(), current.getPhysicalWidth(),
current.getPhysicalHeight(),current.getRefreshRate());
WindowManager.LayoutParams params = mFragment.requireActivity().getWindow().getAttributes();
params.preferredDisplayModeId = best.getModeId();
mFragment.requireActivity().getWindow().setAttributes(params);
@@ -340,10 +382,8 @@ public class PlaybackController {
Timber.i("Display is already in best mode");
}
} else {
Timber.i("*** Unable to find display mode for refresh rate: %s", videoStream.getRealFrameRate());
Timber.i("*** Unable to find display mode for refresh rate: %f", videoStream.getRealFrameRate());
}
}
// central place to update mCurrentPosition
@@ -381,8 +421,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 +531,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 +543,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;
@@ -649,15 +689,22 @@ public class PlaybackController {
if (mVideoManager == null)
return;
mVideoManager.init(getBufferAmount(), useDeinterlacing);
Timber.d("server default: %s inferred first track: %s", internalResponse.getMediaSource().getDefaultAudioStreamIndex(), bestGuessAudioTrack(internalResponse.getMediaSource()));
mCurrentOptions = useVlc ? vlcOptions : internalOptions;
startItem(item, position, useVlc ? vlcResponse : internalResponse);
}
@Override
public void onError(Exception exception) {
Timber.e(exception, "Unable to get internal stream info");
Timber.e(exception, "Unable to get stream info for internal player - falling back to libVLC");
if (mVideoManager == null)
return;
boolean useDeinterlacing = vlcResponse.getMediaSource().getVideoStream() != null &&
vlcResponse.getMediaSource().getVideoStream().getIsInterlaced() &&
(vlcResponse.getMediaSource().getVideoStream().getWidth() == null ||
vlcResponse.getMediaSource().getVideoStream().getWidth() > 1200);
mVideoManager.init(getBufferAmount(), useDeinterlacing);
mCurrentOptions = vlcOptions;
startItem(item, position, vlcResponse);
}
@@ -711,15 +758,14 @@ public class PlaybackController {
mCurrentStreamInfo = response;
// set the audio index so that it is preserved if the stream is rebuilt when seeking
mCurrentOptions.setAudioStreamIndex(response.getMediaSource().getDefaultAudioStreamIndex());
// set media source Id in case we need to switch to transcoding
mCurrentOptions.setMediaSourceId(response.getMediaSource().getId());
setDefaultAudioIndex(response);
// get subtitle info
mSubtitleStreams = response.GetSubtitleProfiles(false, apiClient.getValue().getApiUrl(), apiClient.getValue().getAccessToken());
mDefaultSubIndex = response.getMediaSource().getDefaultSubtitleStreamIndex() != null ? response.getMediaSource().getDefaultSubtitleStreamIndex() : mDefaultSubIndex;
setDefaultAudioIndex(response);
Timber.d("default audio index set to %s remote default %s", mDefaultAudioIndex, response.getMediaSource().getDefaultAudioStreamIndex());
Timber.d("default sub index set to %s remote default %s", mDefaultSubIndex, response.getMediaSource().getDefaultSubtitleStreamIndex());
// if burning in, set the subtitle index and the burningSubs flag so that onPrepared and switchSubtitleStream will know that we already have subtitles enabled
@@ -729,6 +775,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;
@@ -747,13 +795,13 @@ public class PlaybackController {
}
// set refresh rate
if (refreshRateSwitchingEnabled) {
if (refreshRateSwitchingBehavior != RefreshRateSwitchingBehavior.DISABLED) {
setRefreshRate(response.getMediaSource().getVideoStream());
}
// 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();
@@ -806,17 +854,21 @@ public class PlaybackController {
}
public int getAudioStreamIndex() {
int currIndex = mDefaultAudioIndex;
int currIndex = -1;
// if not transcoding, libVLC & exoplayer are able to switch tracks and report their selected track
if (hasInitializedVideoManager() && !isTranscoding() && mVideoManager.getVLCAudioTrack() > -1) {
currIndex = mVideoManager.getVLCAudioTrack();
} else if (hasInitializedVideoManager() && !isTranscoding() && isNativeMode() && mVideoManager.getExoPlayerTrack(MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams()) > -1) {
currIndex = mVideoManager.getExoPlayerTrack(MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams());
} else if (!isTranscoding() && isNativeMode() && mCurrentOptions.getAudioStreamIndex() != null) {
// Use stream index from mCurrentOptions if it's set.
// This should be null until the player has been queried at least once after playback starts
//
// Use DefaultAudioStreamIndex for transcoding since they are encoded with only one stream
//
// Otherwise, query the players
if (mCurrentOptions.getAudioStreamIndex() != null) {
currIndex = mCurrentOptions.getAudioStreamIndex();
} else if (getCurrentMediaSource().getDefaultAudioStreamIndex() != null) {
} else if (isTranscoding() && getCurrentMediaSource().getDefaultAudioStreamIndex() != null) {
currIndex = getCurrentMediaSource().getDefaultAudioStreamIndex();
} else if (hasInitializedVideoManager() && !isTranscoding()) {
currIndex = isNativeMode() ? mVideoManager.getExoPlayerTrack(MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams()) :
mVideoManager.getVLCAudioTrack(getCurrentlyPlayingItem().getMediaStreams());
}
return currIndex;
}
@@ -841,8 +893,13 @@ public class PlaybackController {
if (mDefaultAudioIndex != -1)
return;
if (bestGuessAudioTrack(info.getMediaSource()) != null)
mDefaultAudioIndex = bestGuessAudioTrack(info.getMediaSource());
Integer remoteDefault = info.getMediaSource().getDefaultAudioStreamIndex();
Integer bestGuess = bestGuessAudioTrack(info.getMediaSource());
if (remoteDefault != null)
mDefaultAudioIndex = remoteDefault;
else if (bestGuess != null)
mDefaultAudioIndex = bestGuess;
Timber.d("default audio index set to %s", mDefaultAudioIndex);
}
@@ -854,6 +911,10 @@ public class PlaybackController {
Timber.d("trying to switch audio stream from %s to %s", currAudioIndex, index);
if (currAudioIndex == index) {
Timber.d("skipping setting audio stream, already set to requested index %s", index);
if (mCurrentOptions.getAudioStreamIndex() == null || mCurrentOptions.getAudioStreamIndex() != index) {
Timber.d("setting mCurrentOptions audio stream index from %s to %s", mCurrentOptions.getAudioStreamIndex(), index);
mCurrentOptions.setAudioStreamIndex(index);
}
return;
}
@@ -863,7 +924,7 @@ public class PlaybackController {
if (isNativeMode() && !isTranscoding() && mVideoManager.setExoPlayerTrack(index, MediaStreamType.Audio, getCurrentlyPlayingItem().getMediaStreams())) {
mCurrentOptions.setMediaSourceId(getCurrentMediaSource().getId());
mCurrentOptions.setAudioStreamIndex(index);
} else if (!isNativeMode() && !isTranscoding() && mVideoManager.setVLCAudioTrack(index) == index) {
} else if (!isNativeMode() && !isTranscoding() && mVideoManager.setVLCAudioTrack(index, getCurrentlyPlayingItem().getMediaStreams()) == index) {
// if setAudioTrack succeeded it will return the requested index
mCurrentOptions.setMediaSourceId(getCurrentMediaSource().getId());
mCurrentOptions.setAudioStreamIndex(index);
@@ -882,20 +943,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,40 +970,41 @@ 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()) {
case Encode:
// Gonna need to burn in so start a transcode with the sub index
stop();
if (!mVideoManager.isNativeMode()) {
if (mFragment != null)
Utils.showToast(mFragment.getContext(), mFragment.getString(R.string.msg_burn_sub_warning));
return;
}
// 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);
break;
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 (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));
} else {
mCurrentOptions.setSubtitleStreamIndex(index);
mDefaultSubIndex = index;
}
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())));
@@ -951,6 +1016,8 @@ public class PlaybackController {
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) {
@@ -974,9 +1041,6 @@ public class PlaybackController {
case Hls:
break;
}
}
}
public void pause() {
@@ -1396,7 +1460,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();
}
@@ -56,6 +58,8 @@ public class PlaybackManager {
request.setDeviceProfile(options.getProfile());
request.setMaxAudioChannels(options.getMaxAudioChannels());
request.setAllowAudioStreamCopy(true);
apiClient.GetPlaybackInfoWithPost(request, new GetPlaybackInfoResponse(this, deviceInfo, apiClient, options, response, false, startPositionTicks));
}
@@ -65,14 +69,24 @@ public class PlaybackManager {
request.setUserId(apiClient.getCurrentUserId());
request.setMaxStreamingBitrate(Long.valueOf(options.getMaxBitrate()));
request.setMediaSourceId(options.getMediaSourceId());
request.setAudioStreamIndex(options.getAudioStreamIndex());
request.setSubtitleStreamIndex(options.getSubtitleStreamIndex());
request.setStartTimeTicks(startPositionTicks);
request.setDeviceProfile(options.getProfile());
request.setEnableDirectStream(options.getEnableDirectStream());
request.setEnableDirectPlay(options.getEnableDirectPlay());
request.setMaxAudioChannels(options.getMaxAudioChannels());
Integer audioIdx = options.getAudioStreamIndex();
if (audioIdx != null && audioIdx >= 0) {
request.setAudioStreamIndex(audioIdx);
}
Integer subIdx = options.getSubtitleStreamIndex();
if (subIdx != null && subIdx >= 0) {
request.setSubtitleStreamIndex(subIdx);
}
request.setAllowVideoStreamCopy(true);
request.setAllowAudioStreamCopy(true);
apiClient.GetPlaybackInfoWithPost(request, new GetPlaybackInfoResponse(this, deviceInfo, apiClient, options, response, true, startPositionTicks));
}

View File

@@ -44,9 +44,10 @@ import org.koin.java.KoinJavaComponent;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
import org.videolan.libvlc.interfaces.IVLCVout;
import org.videolan.libvlc.MediaPlayer;
import org.videolan.libvlc.MediaPlayer.TrackDescription;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import timber.log.Timber;
@@ -67,7 +68,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
private PlayerView mExoPlayerView;
private AspectRatioFrameLayout mAspectRatioFrameLayout;
private LibVLC mLibVLC;
private org.videolan.libvlc.MediaPlayer mVlcPlayer;
private MediaPlayer mVlcPlayer;
private Media mCurrentMedia;
private VlcEventHandler mVlcHandler = new VlcEventHandler();
private Handler mHandler = new Handler();
@@ -144,6 +145,11 @@ 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");
}
});
}
@@ -412,28 +418,37 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
}
public boolean setSubtitleTrack(int index, @Nullable List<MediaStream> allStreams) {
if (!nativeMode && allStreams != null) {
//find the relative order of our sub index within the sub tracks in VLC
int vlcIndex = 1; // start at 1 to account for "disabled"
private int offsetStreamIndex(int index, boolean adjustByAdding, @Nullable List<MediaStream> allStreams) {
if (index < 0 || allStreams == null)
return -1;
for (MediaStream stream : allStreams) {
if (stream.getType() == MediaStreamType.Subtitle && !stream.getIsExternal()) {
if (stream.getIndex() == index) {
if (!stream.getIsExternal())
break;
}
vlcIndex++;
}
index += adjustByAdding ? 1 : -1;
}
org.videolan.libvlc.MediaPlayer.TrackDescription vlcSub;
try {
vlcSub = getSubtitleTracks()[vlcIndex];
return index < 0 || index >= allStreams.size() ? -1 : index;
}
} catch (IndexOutOfBoundsException e) {
Timber.e("Could not locate subtitle with index %s in vlc track info", index);
public boolean setSubtitleTrack(int index, @Nullable List<MediaStream> allStreams) {
if (isNativeMode() || allStreams == null)
return false;
} catch (NullPointerException e) {
Timber.e("No subtitle tracks found in player trying to set subtitle with index %s in vlc track info", index);
int vlcIndex = offsetStreamIndex(index, false, allStreams);
if (vlcIndex < 0)
return false;
TrackDescription vlcSub = null;
for (TrackDescription subTrack: mVlcPlayer.getSpuTracks()) {
Timber.d("libvlc subtitle track %s %s", subTrack.id, subTrack.name);
if (subTrack.id == vlcIndex)
vlcSub = subTrack;
}
if (vlcSub == null) {
Timber.e("Could not locate subtitle with index %s in vlc track info", vlcIndex);
return false;
}
@@ -442,9 +457,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
return false;
}
public int getExoPlayerTrack(@Nullable MediaStreamType streamType, @Nullable List<MediaStream> allStreams) {
if (!nativeMode || !isInitialized() || streamType == null || allStreams == null)
return -1;
@@ -453,8 +465,11 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
int matchedIndex = -2;
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
for (TracksInfo.TrackGroupInfo groupInfo : exoTracks.getTrackGroupInfos()) {
if (matchedIndex > -2)
break;
// Group level information.
@C.TrackType int trackType = groupInfo.getTrackType();
TrackGroup group = groupInfo.getTrackGroup();
@@ -463,58 +478,50 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
Format trackFormat = group.getFormat(i);
if (trackType == chosenTrackType) {
if (groupInfo.isTrackSelected(i)) {
// we found the track, set to -1 first to handle failed int parsing
matchedIndex = -1;
if (trackFormat.id != null) {
int id;
try {
id = Integer.parseInt(trackFormat.id) - 1;
id = Integer.parseInt(trackFormat.id);
} catch (NumberFormatException e) {
Timber.d("failed to parse track ID [%s]", trackFormat.id);
break;
}
matchedIndex = id;
}
break;
}
}
}
}
// offset the stream index to account for external streams
int exoTrackID = offsetStreamIndex(matchedIndex, true, allStreams);
if (exoTrackID < 0)
return -1;
}
if (id >= 0 && id < allStreams.size())
return id;
}
return -1;
}
}
}
}
return -1;
Timber.d("re-retrieved exoplayer track index %s", exoTrackID);
return exoTrackID;
}
public boolean setExoPlayerTrack(int index, @Nullable MediaStreamType streamType, @Nullable List<MediaStream> allStreams) {
if (!nativeMode || !isInitialized() || streamType == null || allStreams == null || index < 0 || index >= allStreams.size())
return false;
if (streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
if (!nativeMode || !isInitialized() || allStreams == null || streamType != MediaStreamType.Subtitle && streamType != MediaStreamType.Audio)
return false;
int chosenTrackType = streamType == MediaStreamType.Subtitle ? C.TRACK_TYPE_TEXT : C.TRACK_TYPE_AUDIO;
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
List<String> internallyRenderedSubFormats = Arrays.asList("ass", "ssa");
// make sure the chosen track exists, is the correct type, and isn't external
boolean isValidMediaStream = false;
for (MediaStream stream : allStreams) {
Timber.d("MediaStream track %s type %s label %s codec %s isExternal %s", stream.getIndex(), stream.getType(), stream.getTitle(), stream.getCodec(), stream.getIsExternal());
if (stream.getType() == streamType) {
if (stream.getIndex() == index) {
if (stream.getIsExternal())
continue;
if (streamType == MediaStreamType.Subtitle && !internallyRenderedSubFormats.contains(stream.getCodec())) {
continue;
}
isValidMediaStream = true;
}
}
}
if (!isValidMediaStream)
MediaStream candidate = allStreams.get(index);
if (candidate.getIsExternal() || candidate.getType() != streamType)
return false;
// MediaStream IDs start at 0 and exoplayer tracks start at 1
// force selection of the chosen track by setting its TrackGroup as the only allowed group for its type
int exoTrackID = offsetStreamIndex(index, false, allStreams);
if (exoTrackID < 0)
return false;
// print the streams for debugging
for (MediaStream stream : allStreams) {
Timber.d("MediaStream track %s type %s label %s codec %s isExternal %s", stream.getIndex(), stream.getType(), stream.getTitle(), stream.getCodec(), stream.getIsExternal());
}
// design choices for exoplayer track selection overrides:
// * build upon the prior parameters so we can mix overrides of different track types without erasing priors
@@ -523,8 +530,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
// if we want most formats to be handled by the external subtitle handler (which has adjustable size, background), we leave sub track selection disabled
// if we decide to use exoplayer to render a specific subtitle format, allow subtitle track selection and restrict selection to the chosen group
int exoTrackID = index + 1;
TracksInfo exoTracks = mExoPlayer.getCurrentTracksInfo();
TrackGroup matchedGroup = null;
for (TracksInfo.TrackGroupInfo groupInfo : exoTracks.getTrackGroupInfos()) {
// Group level information.
@@ -581,68 +587,66 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
return true;
}
public int getVLCAudioTrack() {
public int getVLCAudioTrack(@Nullable List<MediaStream> allStreams) {
if (!isInitialized() || nativeMode)
return -1;
return mVlcPlayer.getAudioTrack();
int ndx = offsetStreamIndex(mVlcPlayer.getAudioTrack(), true, allStreams);
Timber.d("re-retrieved libVLC audio track index %s", ndx);
return ndx;
}
public int setVLCAudioTrack(int ndx) {
if (!isInitialized() || ndx < 0)
public int setVLCAudioTrack(int ndx, @Nullable List<MediaStream> allStreams) {
if (!isInitialized() || isNativeMode())
return -1;
if (isNativeMode()) {
Timber.e("Cannot set audio track in native mode");
int vlcID = offsetStreamIndex(ndx, false, allStreams);
if (vlcID < 0)
return -1;
}
//debug
Timber.d("Setting VLC audio track index to: %d", ndx);
for (org.videolan.libvlc.MediaPlayer.TrackDescription track : mVlcPlayer.getAudioTracks()) {
TrackDescription vlcTrack = null;
Timber.d("Setting VLC audio track index to: %d", vlcID);
int vlcNdx = 0;
for (TrackDescription track : mVlcPlayer.getAudioTracks()) {
Timber.d("VLC Audio Track: %s / %d", track.name, track.id);
}
//
boolean matched = false;
for (org.videolan.libvlc.MediaPlayer.TrackDescription track : mVlcPlayer.getAudioTracks()) {
if (track.id == ndx) {
matched = true;
break;
}
if (track.id == vlcID)
vlcTrack = track;
if (vlcTrack == null)
vlcNdx++;
}
org.videolan.libvlc.MediaPlayer.TrackDescription vlcTrack = null;
if (matched) {
try {
vlcTrack = mVlcPlayer.getAudioTracks()[ndx];
} catch (IndexOutOfBoundsException e) {
Timber.e("Could not locate audio with index %s in vlc track info", ndx);
return -1;
} catch (NullPointerException e) {
Timber.e("Could not locate audio track with index %s in vlc, null exception", ndx);
if (vlcTrack == null) {
Timber.e("Could not locate audio track with index %s in vlc", vlcID);
return -1;
}
}
if (!matched || vlcTrack == null) {
Timber.e("Could not locate audio track with index %s in vlc", ndx);
return -1;
} else if (mVlcPlayer.getAudioTrack() == ndx) {
if (mVlcPlayer.getAudioTrack() == vlcID) {
Timber.d("provided index points to the audio track already in use, aborting");
return -1;
return ndx;
}
if (mVlcPlayer.setAudioTrack(vlcTrack.id)) {
Timber.i("Setting by ID was successful");
} else {
Timber.i("Setting by ID not successful, trying index");
mVlcPlayer.setAudioTrack(ndx);
Timber.i("Setting by ID not successful, trying index %s", vlcNdx);
mVlcPlayer.setAudioTrack(vlcNdx);
}
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 +654,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);
}
}
@@ -703,10 +707,6 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
}
}
public org.videolan.libvlc.MediaPlayer.TrackDescription[] getSubtitleTracks() {
return nativeMode ? null : mVlcPlayer.getSpuTracks();
}
public void destroy() {
stopPlayback();
releasePlayer();
@@ -720,7 +720,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");
@@ -746,7 +746,7 @@ public class VideoManager implements IVLCVout.OnNewVideoLayoutListener {
mLibVLC = new LibVLC(mActivity, options);
Timber.i("Network buffer set to %d", buffer);
mVlcPlayer = new org.videolan.libvlc.MediaPlayer(mLibVLC);
mVlcPlayer = new MediaPlayer(mLibVLC);
setVlcAudioOptions();
mSurfaceHolder.addCallback(mSurfaceCallback);

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 = () -> {
if (!isPlaying()) {
setEndTime();
if (!isPlaying()) {
mHandler.postDelayed(mRefreshEndTime, 30000);
}
};
@@ -211,7 +212,9 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
}
void addMediaActions() {
if (primaryActionsAdapter.size() > 0)
primaryActionsAdapter.clear();
if (secondaryActionsAdapter.size() > 0)
secondaryActionsAdapter.clear();
// Primary Items
@@ -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,7 +387,9 @@ public class CustomPlaybackTransportControlGlue extends PlaybackTransportControl
}
void invalidatePlaybackControls() {
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

@@ -13,13 +13,10 @@ class ButtonRemapPreference(
) : DialogPreference(context, attrs) {
override fun getDialogLayoutResource() = R.layout.preference_button_remap
private var innerKeyCode: Int = -1
var keyCode: Int
get() = innerKeyCode
var keyCode: Int = -1
set(value) {
innerKeyCode = value
notifyDependencyChange(false)
notifyChanged()
field = value
callChangeListener(value)
}
var defaultKeyCode: Int = -1

View File

@@ -7,6 +7,7 @@ import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.androidtv.preference.constant.NEXTUP_TIMER_DISABLED
import org.jellyfin.androidtv.preference.constant.NextUpBehavior
import org.jellyfin.androidtv.preference.constant.PreferredVideoPlayer
import org.jellyfin.androidtv.preference.constant.RefreshRateSwitchingBehavior
import org.jellyfin.androidtv.ui.preference.custom.DurationSeekBarPreference
import org.jellyfin.androidtv.ui.preference.dsl.OptionsFragment
import org.jellyfin.androidtv.ui.preference.dsl.checkbox
@@ -134,10 +135,9 @@ class PlaybackPreferencesScreen : OptionsFragment() {
depends { userPreferences[UserPreferences.videoPlayer] == PreferredVideoPlayer.EXTERNAL }
}
checkbox {
enum<RefreshRateSwitchingBehavior> {
setTitle(R.string.lbl_refresh_switching)
setContent(R.string.pref_refresh_switching_description)
bind(userPreferences, UserPreferences.refreshRateSwitchingEnabled)
bind(userPreferences, UserPreferences.refreshRateSwitchingBehavior)
depends { DeviceUtils.is60() && userPreferences[UserPreferences.videoPlayer] != PreferredVideoPlayer.EXTERNAL }
}
}

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

@@ -3,6 +3,7 @@ package org.jellyfin.androidtv.ui.shared
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.view.Gravity
import android.text.style.LineBackgroundSpan
import androidx.annotation.ColorInt
import androidx.annotation.Dimension
@@ -14,7 +15,8 @@ import kotlin.math.roundToInt
*/
class PaddedLineBackgroundSpan(
@ColorInt private val backgroundColor: Int,
@Dimension private val horizontalPadding: Int
@Dimension private val horizontalPadding: Int,
private val horizontalGravity: Int,
) : LineBackgroundSpan {
private val backgroundRect = Rect()
@@ -35,11 +37,25 @@ class PaddedLineBackgroundSpan(
// Measure the current line of text
val textWidth = paint.measureText(text, start, end).roundToInt()
@Suppress("RtlHardcoded")
val rectLeft = when(horizontalGravity) {
Gravity.CENTER_HORIZONTAL -> ((right - textWidth) / 2) - horizontalPadding
Gravity.RIGHT -> right - textWidth - horizontalPadding
else -> left - horizontalPadding
}
@Suppress("RtlHardcoded")
val rectRight = when(horizontalGravity) {
Gravity.CENTER_HORIZONTAL -> right - rectLeft
Gravity.RIGHT -> right + horizontalPadding
else -> left + textWidth + horizontalPadding
}
// Set the dimensions of the background rectangle
backgroundRect.set(
left - horizontalPadding,
rectLeft,
top,
left + textWidth + horizontalPadding,
rectRight,
bottom
)

View File

@@ -1,9 +1,12 @@
package org.jellyfin.androidtv.util.profile;
import static android.media.MediaCodecInfo.CodecProfileLevel.AVCLevel4;
import static android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh10;
import static android.media.MediaCodecInfo.CodecProfileLevel.H263Level10;
import static android.media.MediaCodecInfo.CodecProfileLevel.H263Level45;
import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel5;
import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain10;
import static android.media.MediaFormat.MIMETYPE_VIDEO_AVC;
import static android.media.MediaFormat.MIMETYPE_VIDEO_H263;
import static android.media.MediaFormat.MIMETYPE_VIDEO_HEVC;
@@ -36,6 +39,10 @@ public class MediaCodecCapabilitiesTest {
return hasDecoder(MIMETYPE_VIDEO_HEVC, HEVCProfileMain10, HEVCMainTierLevel5);
}
public boolean supportsAVCHigh10() {
return hasDecoder(MIMETYPE_VIDEO_AVC, AVCProfileHigh10, AVCLevel4);
}
private boolean checkDecoder(String mime, int profile, int level) {
if (!hasDecoder(mime, profile, level)) {
Timber.i("no %s decoder for profile %d and level %d", mime, profile, level);

View File

@@ -84,11 +84,12 @@ object ProfileHelper {
ProfileCondition(
ProfileConditionType.EqualsAny,
ProfileConditionValue.VideoProfile,
arrayOf(
listOfNotNull(
"high",
"main",
"baseline",
"constrained baseline"
"constrained baseline",
if (MediaTest.supportsAVCHigh10()) "high 10" else null
).joinToString("|")
)
}

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

@@ -52,6 +52,7 @@
android:layout_gravity="bottom|center_horizontal"
android:layout_marginHorizontal="120dp"
android:layout_marginBottom="48dp"
android:gravity="center"
android:textColor="@color/white"
android:textSize="28sp"
app:strokeWidth="5.0"

View File

@@ -11,6 +11,7 @@
<string name="settings_title">Preferences</string>
<string name="pref_authentication_cat">Authentication</string>
<string name="chapters">Chapters</string>
<string name="state_disabled">Disabled</string>
<string name="lbl_settings">Preferences</string>
<string name="lbl_continue_watching">Continue Watching</string>
<string name="lbl_next_up">Next Up</string>
@@ -147,7 +148,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>
@@ -204,8 +205,6 @@
<string name="lbl_open">Open</string>
<string name="lbl_show_backdrop">Show backdrops</string>
<string name="pref_show_backdrop_description">Change background image to selected items</string>
<string name="lbl_refresh_switching">Refresh rate switching</string>
<string name="pref_refresh_switching_description">Monitor refresh rate is changed to match video</string>
<string name="lbl_show_premieres">Show season premieres</string>
<string name="desc_premieres">Show a row of new episode pilots for any series you watch</string>
<string name="lbl_bitstream_dts">Bitstream DTS audio</string>
@@ -486,4 +485,17 @@
<string name="pref_direct_stream_live_off">Transcoding is used when necessary</string>
<string name="pref_direct_stream_live_on">Transcoding is disabled</string>
<string name="home_section_resume_book">Continue reading</string>
<string name="action_login">Sign in</string>
<string name="action_use_password">Use a password</string>
<string name="action_use_quickconnect">Use QuickConnect</string>
<string name="input_username">Username</string>
<string name="input_password">Password</string>
<string name="login_connect_to">Connecting to %1$s</string>
<string name="login_other_options">Other options:</string>
<string name="login_quickconnect_step_1">1. Open the Jellyfin app on your phone or webbrowser, and sign in with your account</string>
<string name="login_quickconnect_step_2">2. Open the user menu and go to the Quick Connect page</string>
<string name="login_quickconnect_step_3">3. Enter the following code:</string>
<string name="lbl_refresh_switching">Refresh rate switching</string>
<string name="pref_refresh_rate_scale_on_tv">Scale on TV</string>
<string name="pref_refresh_rate_scale_on_device">Scale on device</string>
</resources>

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