Compare commits

...

20 Commits

Author SHA1 Message Date
Niels van Velzen
7d84011d81 Re-use supportsHevc in one additional place
(cherry picked from commit 6111cbec5a)
2024-08-11 09:59:40 +02:00
Joseph Price
36a307cd7f Really don't request HEVC if unsupported
(cherry picked from commit c7a4c89b74)
2024-08-11 09:59:39 +02:00
Niels van Velzen
2a122803c2 Fix InfoRow not using requested media source
(cherry picked from commit b0cd0c5b37)
2024-08-11 09:59:39 +02:00
Niels van Velzen
dbf6dc0f9a Reset display mode when exiting video player
(cherry picked from commit aa5fb63b71)
2024-08-11 09:59:38 +02:00
Niels van Velzen
fd663aff39 Improve InfoRow colors/font readability
(cherry picked from commit d661583a86)
2024-08-11 09:59:38 +02:00
Niels van Velzen
30e34568c1 Fix intros showing next up screen
(cherry picked from commit fe31c19f15)
2024-08-11 09:59:37 +02:00
Niels van Velzen
5fb77314c4 Fix crash when starting next episode
(cherry picked from commit fcd9751d82)
2024-08-11 09:59:37 +02:00
Niels van Velzen
d436c5de3d Graduate reactive homepage to stable
(cherry picked from commit f8c5345871)
2024-08-11 09:59:36 +02:00
renovate[bot]
8157e3e5c1 Update dependency androidx.tv:tv-material to v1.0.0-rc02
(cherry picked from commit 0385f63fbe)
2024-08-11 09:59:20 +02:00
renovate[bot]
3656c18e8f Update dependency androidx.work:work-runtime to v2.9.1
(cherry picked from commit 6cc1031b8a)
2024-08-11 09:59:19 +02:00
renovate[bot]
9f48b7da39 Update dependency org.jellyfin.sdk:jellyfin-core to v1.5.3
(cherry picked from commit 587f0fb35a)
2024-08-11 09:59:19 +02:00
Niels van Velzen
ef899f98bc Fix SelectQualityAction crashing when opening menu
(cherry picked from commit 044ce67110)
2024-08-06 15:20:34 +02:00
Niels van Velzen
de6c7904f0 Use "direct play" as default audio behavior
(cherry picked from commit f6c536880d)
2024-08-06 15:20:34 +02:00
Marc Vollmer
0a79d5b60f Fix NullPointerException in LeanbackOverlayFragment.onPause
(cherry picked from commit 3b852a031e)
2024-08-06 15:20:33 +02:00
Niels van Velzen
36e0bc1edc Fix NullPointerException in CustomPlaybackOverlayFragment.onResume
(cherry picked from commit 7bc99d2e66)
2024-08-06 15:20:33 +02:00
Niels van Velzen
8faeb3aac1 Remove context requirement from ExoPlayerProfile
(cherry picked from commit 9ecf952351)
2024-08-06 15:20:11 +02:00
majmongoose
160ef4914f Add Google Chromecast HD limitation (#3820)
* Add identification for Google Chromecast HD

Possibly an upstream bug is preventing this from working, as the chromecast is being idenitified correctly in debugging.

* Update DeviceUtils.kt

Added a note for the reason of the change.

* Removed unintentional imports

* Update app/src/main/java/org/jellyfin/androidtv/util/DeviceUtils.kt

Make changes suggested by maintainer.

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>

* Update DeviceUtils.kt

Removed unneeded isChromecastHD

---------

Co-authored-by: Niels van Velzen <nielsvanvelzen@users.noreply.github.com>
(cherry picked from commit a8c9f054ab)
2024-08-06 15:20:10 +02:00
Niels van Velzen
474c0774f1 Fix AudioNowPlayingFragment showing wrong item in extremely rare cases
(cherry picked from commit 61b4178dec)
2024-08-06 15:20:10 +02:00
Niels van Velzen
0c9ba1ccec Fix race condition in PictureViewerViewModel
Using the showNext/showPrevious functions while the album is still loading can cause an IndexOutOfBoundsException. Check if the album is empty (default value) first before trying to read from it to avoid these crashes.

(cherry picked from commit 9aa1759ae7)
2024-08-06 15:20:10 +02:00
Niels van Velzen
0c667ab615 Strip HTML from descriptions in leanback integration
(cherry picked from commit 2e55987939)
2024-08-06 15:20:09 +02:00
25 changed files with 192 additions and 147 deletions

View File

@@ -13,6 +13,7 @@
- [sparky3387](https://github.com/sparky3387)
- [mohd-akram](https://github.com/mohd-akram)
- [3l0w](https://github.com/3l0w)
- [MajMongoose](https://github.com/majmongoose)
# Emby Contributors

View File

@@ -28,6 +28,7 @@ import org.jellyfin.androidtv.ui.startup.StartupActivity
import org.jellyfin.androidtv.util.ImageHelper
import org.jellyfin.androidtv.util.dp
import org.jellyfin.androidtv.util.sdk.isUsable
import org.jellyfin.androidtv.util.stripHtml
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.exception.ApiClientException
import org.jellyfin.sdk.api.client.exception.TimeoutException
@@ -363,7 +364,7 @@ class LeanbackChannelWorker(
.setEpisodeTitle(if (item.type == BaseItemKind.EPISODE) item.name else null)
.setSeasonNumber(seasonString, item.parentIndexNumber ?: 0)
.setEpisodeNumber(episodeString, item.indexNumber ?: 0)
.setDescription(item.overview)
.setDescription(item.overview?.stripHtml())
.setReleaseDate(
if (item.premiereDate != null) DateTimeFormatter.ISO_DATE.format(item.premiereDate)
else null

View File

@@ -96,7 +96,7 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
/**
* Preferred behavior for audio streaming.
*/
var audioBehaviour = enumPreference("audio_behavior", AudioBehavior.DOWNMIX_TO_STEREO)
var audioBehaviour = enumPreference("audio_behavior", AudioBehavior.DIRECT_STREAM)
/**
* Preferred behavior for audio streaming.
@@ -199,11 +199,6 @@ class UserPreferences(context: Context) : SharedPreferenceStore(
* Whether items shown in the screensaver are required to have an age rating set.
*/
var screensaverAgeRatingRequired = booleanPreference("screensaver_agerating_required", true)
/**
* Enable reactive homepage
*/
var homeReactive = booleanPreference("home_reactive", false)
}
init {

View File

@@ -26,6 +26,8 @@ import org.jellyfin.androidtv.util.sdk.getSeasonEpisodeName
import org.jellyfin.androidtv.util.sdk.isNew
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.MediaSourceInfo
import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.MediaStreamType
import org.jellyfin.sdk.model.api.SeriesStatus
import org.jellyfin.sdk.model.api.VideoRangeType
@@ -135,13 +137,23 @@ fun InfoRowSeasonEpisode(item: BaseItemDto) {
}
}
private fun List<MediaStream>.getDefault(type: MediaStreamType, defaultIndex: Int? = null): MediaStream? {
if (defaultIndex != null) {
val byIndex = get(defaultIndex)
if (byIndex.type == type) return byIndex
}
return firstOrNull { it.type == type }
}
@Composable
fun InfoRowMediaDetails(item: BaseItemDto) {
val videoStream = item.mediaStreams?.firstOrNull { it.type == MediaStreamType.VIDEO }
val audioStream = item.mediaStreams?.firstOrNull { it.type == MediaStreamType.AUDIO }
fun InfoRowMediaDetails(mediaSource: MediaSourceInfo) {
val videoStream = mediaSource.mediaStreams?.getDefault(MediaStreamType.VIDEO)
val audioStream = mediaSource.mediaStreams?.getDefault(MediaStreamType.AUDIO, mediaSource.defaultAudioStreamIndex)
val subtitleStream = mediaSource.mediaStreams?.getDefault(MediaStreamType.SUBTITLE, mediaSource.defaultSubtitleStreamIndex)
// Subtitles
if (item.hasSubtitles == true) {
if (subtitleStream != null) {
InfoRowItem(
contentDescription = null,
colors = InfoRowColors.Default,
@@ -219,6 +231,7 @@ fun InfoRowMediaDetails(item: BaseItemDto) {
@Composable
fun BaseItemInfoRow(
item: BaseItemDto,
mediaSource: MediaSourceInfo?,
includeRuntime: Boolean,
) {
val userPreferences = koinInject<UserPreferences>()
@@ -239,7 +252,7 @@ fun BaseItemInfoRow(
InfoRowDate(item)
if (includeRuntime) item.runTimeTicks?.ticks?.let { BaseItemInfoRowRuntime(it) }
item.officialRating?.let { InfoRowParentalRating(it) }
InfoRowMediaDetails(item)
mediaSource?.let { InfoRowMediaDetails(it) }
}
BaseItemKind.BOX_SET -> {
@@ -257,7 +270,7 @@ fun BaseItemInfoRow(
val runtime = item.cumulativeRunTimeTicks ?: item.runTimeTicks
if (includeRuntime) runtime?.ticks?.let { BaseItemInfoRowRuntime(it) }
item.officialRating?.let { InfoRowParentalRating(it) }
InfoRowMediaDetails(item)
mediaSource?.let { InfoRowMediaDetails(it) }
}
BaseItemKind.SERIES -> {
@@ -265,7 +278,7 @@ fun BaseItemInfoRow(
if (includeRuntime) item.runTimeTicks?.ticks?.let { BaseItemInfoRowRuntime(it) }
InfoRowSeriesStatus(item)
item.officialRating?.let { InfoRowParentalRating(it) }
InfoRowMediaDetails(item)
mediaSource?.let { InfoRowMediaDetails(it) }
}
BaseItemKind.PROGRAM -> {
@@ -303,7 +316,7 @@ fun BaseItemInfoRow(
if (includeRuntime) item.runTimeTicks?.ticks?.let { BaseItemInfoRowRuntime(it) }
item.officialRating?.let { InfoRowParentalRating(it) }
InfoRowMediaDetails(item)
mediaSource?.let { InfoRowMediaDetails(it) }
}
BaseItemKind.MUSIC_ARTIST -> {
@@ -343,14 +356,14 @@ fun BaseItemInfoRow(
val runtime = item.cumulativeRunTimeTicks ?: item.runTimeTicks
if (includeRuntime) runtime?.ticks?.let { BaseItemInfoRowRuntime(it) }
item.officialRating?.let { InfoRowParentalRating(it) }
InfoRowMediaDetails(item)
mediaSource?.let { InfoRowMediaDetails(it) }
}
else -> {
InfoRowDate(item)
if (includeRuntime) item.runTimeTicks?.ticks?.let { BaseItemInfoRowRuntime(it) }
item.officialRating?.let { InfoRowParentalRating(it) }
InfoRowMediaDetails(item)
mediaSource?.let { InfoRowMediaDetails(it) }
}
}
}
@@ -364,6 +377,7 @@ class BaseItemInfoRowView @JvmOverloads constructor(
attrs: AttributeSet? = null,
) : AbstractComposeView(context, attrs) {
private val _item = MutableStateFlow<BaseItemDto?>(null)
private val _mediaSource = MutableStateFlow<MediaSourceInfo?>(null)
private val _includeRuntime = MutableStateFlow(false)
var item: BaseItemDto?
@@ -372,6 +386,12 @@ class BaseItemInfoRowView @JvmOverloads constructor(
_item.value = value
}
var mediaSource: MediaSourceInfo?
get() = _mediaSource.value
set(value) {
_mediaSource.value = value
}
var includeRuntime: Boolean
get() = _includeRuntime.value
set(value) {
@@ -386,8 +406,9 @@ class BaseItemInfoRowView @JvmOverloads constructor(
@Composable
override fun Content() {
val item by _item.collectAsState()
val mediaSource by _mediaSource.collectAsState()
val includeRuntime by _includeRuntime.collectAsState()
item?.let { BaseItemInfoRow(it, includeRuntime) }
item?.let { BaseItemInfoRow(it, mediaSource, includeRuntime) }
}
}

View File

@@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.Color
*/
object InfoRowColors {
val Transparent = Color.Transparent to Color.White
val Default = Color(0x80FFFFFF) to Color.Black
val Green = Color(0x8034D97C) to Color.Black
val Red = Color(0x80F2364D) to Color.Black
val Default = Color(0xB3FFFFFF) to Color.Black
val Green = Color(0xB3089562) to Color.White
val Red = Color(0xB3F2364D) to Color.White
}

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ProvideTextStyle
@@ -46,6 +47,7 @@ fun InfoRowItem(
value = TextStyle(
color = foregroundColor,
fontSize = if (backgroundColor.alpha > 0f) 12.sp else 16.sp,
fontWeight = if (backgroundColor.alpha > 0f) FontWeight.W600 else FontWeight.W500,
)
) {
Row(

View File

@@ -166,15 +166,13 @@ class HomeRowsFragment : RowsSupportFragment(), AudioEventListener, View.OnKeyLi
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
if (userPreferences[UserPreferences.homeReactive]) {
api.webSocket.subscribe<UserDataChangedMessage>()
.onEach { refreshRows(force = true, delayed = false) }
.launchIn(this)
api.webSocket.subscribe<UserDataChangedMessage>()
.onEach { refreshRows(force = true, delayed = false) }
.launchIn(this)
api.webSocket.subscribe<LibraryChangedMessage>()
.onEach { refreshRows(force = true, delayed = false) }
.launchIn(this)
}
api.webSocket.subscribe<LibraryChangedMessage>()
.onEach { refreshRows(force = true, delayed = false) }
.launchIn(this)
}
}

View File

@@ -1,6 +1,7 @@
package org.jellyfin.androidtv.ui.itemdetail
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.jellyfin.androidtv.data.repository.ItemMutationRepository
import org.jellyfin.sdk.api.client.ApiClient
@@ -29,7 +30,7 @@ fun ItemListFragment.loadItem(itemId: UUID) {
lifecycleScope.launch {
val item by api.userLibraryApi.getItem(itemId)
setBaseItem(item)
if (isActive) setBaseItem(item)
}
}

View File

@@ -53,6 +53,8 @@ class PictureViewerViewModel(private val api: ApiClient) : ViewModel() {
// Album actions
fun showNext() {
if (album.isEmpty()) return
albumIndex++
if (albumIndex == album.size) albumIndex = 0
@@ -61,6 +63,8 @@ class PictureViewerViewModel(private val api: ApiClient) : ViewModel() {
}
fun showPrevious() {
if (album.isEmpty()) return
albumIndex--
if (albumIndex == -1) albumIndex = album.size - 1

View File

@@ -144,7 +144,7 @@ public class AudioNowPlayingFragment extends Fragment {
@Override
public void onClick(View v) {
mediaManager.getValue().toggleRepeat();
updateButtons(mediaManager.getValue().isPlayingAudio());
updateButtons();
}
});
mRepeatButton.setOnFocusChangeListener(mainAreaFocusListener);
@@ -155,7 +155,7 @@ public class AudioNowPlayingFragment extends Fragment {
@Override
public void onClick(View v) {
mediaManager.getValue().shuffleAudioQueue();
updateButtons(mediaManager.getValue().isPlayingAudio());
updateButtons();
}
});
mShuffleButton.setOnFocusChangeListener(mainAreaFocusListener);
@@ -175,7 +175,7 @@ public class AudioNowPlayingFragment extends Fragment {
mArtistButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mBaseItem.getAlbumArtists() != null && mBaseItem.getAlbumArtists().size() > 0) {
if (mBaseItem.getAlbumArtists() != null && !mBaseItem.getAlbumArtists().isEmpty()) {
navigationRepository.getValue().navigate(Destinations.INSTANCE.itemDetails(mBaseItem.getAlbumArtists().get(0).getId()));
}
}
@@ -217,10 +217,10 @@ public class AudioNowPlayingFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
loadItem();
//link events
mediaManager.getValue().addAudioEventListener(audioEventListener);
updateButtons(mediaManager.getValue().isPlayingAudio());
loadItem();
updateButtons();
// load the item duration and set the position to 0 since it won't be set elsewhere until playback is initialized
if (!mediaManager.getValue().isAudioPlayerInitialized())
@@ -238,13 +238,8 @@ public class AudioNowPlayingFragment extends Fragment {
@Override
public void onPlaybackStateChange(@NonNull PlaybackController.PlaybackState newState, @Nullable org.jellyfin.sdk.model.api.BaseItemDto currentItem) {
Timber.d("**** Got playstate change: %s", newState.toString());
if (newState == PlaybackController.PlaybackState.PLAYING && currentItem != mBaseItem) {
// new item started
loadItem();
updateButtons(true);
} else {
updateButtons(newState == PlaybackController.PlaybackState.PLAYING);
}
if (currentItem != mBaseItem) loadItem();
updateButtons();
}
@Override
@@ -261,7 +256,7 @@ public class AudioNowPlayingFragment extends Fragment {
if (hasQueue) {
loadItem();
if (mediaManager.getValue().isAudioPlayerInitialized()) {
updateButtons(mediaManager.getValue().isPlayingAudio());
updateButtons();
}
} else {
if (navigationRepository.getValue().getCanGoBack()) navigationRepository.getValue().goBack();
@@ -313,8 +308,9 @@ public class AudioNowPlayingFragment extends Fragment {
}
}
private void updateButtons(final boolean playing) {
private void updateButtons() {
Timber.d("Updating buttons");
boolean playing = mediaManager.getValue().isPlayingAudio();
requireActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
@@ -334,14 +330,14 @@ public class AudioNowPlayingFragment extends Fragment {
mShuffleButton.setActivated(mediaManager.getValue().isShuffleMode());
if (mBaseItem != null) {
mAlbumButton.setEnabled(mBaseItem.getAlbumId() != null);
mArtistButton.setEnabled(mBaseItem.getAlbumArtists() != null && mBaseItem.getAlbumArtists().size() > 0);
mArtistButton.setEnabled(mBaseItem.getAlbumArtists() != null && !mBaseItem.getAlbumArtists().isEmpty());
}
}
});
}
private String getArtistName(org.jellyfin.sdk.model.api.BaseItemDto item) {
String artistName = item.getArtists() != null && item.getArtists().size() > 0 ? item.getArtists().get(0) : item.getAlbumArtist();
String artistName = item.getArtists() != null && !item.getArtists().isEmpty() ? item.getArtists().get(0) : item.getAlbumArtist();
return artistName != null ? artistName : "";
}
@@ -365,7 +361,7 @@ public class AudioNowPlayingFragment extends Fragment {
public void setCurrentTime(long time) {
// Round the current time as otherwise the time played and time remaining will not be in sync
time = Math.round(time / 1000) * 1000;
time = Math.round(time / 1000L) * 1000L;
mCurrentProgress.setProgress(((Long) time).intValue());
mCurrentPos.setText(TimeUtils.formatMillis(time));
mRemainingTime.setText(mCurrentDuration > 0 ? "-" + TimeUtils.formatMillis(mCurrentDuration - time) : "");

View File

@@ -7,6 +7,7 @@ import android.content.Context;
import android.graphics.Color;
import android.media.AudioManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.Spannable;
@@ -16,6 +17,7 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.LinearLayout;
@@ -575,7 +577,8 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
// up or down should close panel
if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP) {
hidePopupPanel();
if (playbackControllerContainer.getValue().getPlaybackController().isLiveTv()) hide(); //also close this if live tv
if (playbackControllerContainer.getValue().getPlaybackController().isLiveTv())
hide(); //also close this if live tv
return true;
} else {
return false;
@@ -674,7 +677,8 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
super.onResume();
// Close player when resuming without a valid playback contoller
if (!playbackControllerContainer.getValue().getPlaybackController().hasFragment()) {
PlaybackController playbackController = playbackControllerContainer.getValue().getPlaybackController();
if (playbackController == null || !playbackController.hasFragment()) {
if (navigationRepository.getValue().getCanGoBack()) {
navigationRepository.getValue().goBack();
} else {
@@ -1451,5 +1455,12 @@ public class CustomPlaybackOverlayFragment extends Fragment implements LiveTvGui
// Show system bars
WindowCompat.setDecorFitsSystemWindows(requireActivity().getWindow(), true);
WindowCompat.getInsetsController(requireActivity().getWindow(), requireActivity().getWindow().getDecorView()).show(WindowInsetsCompat.Type.systemBars());
// Reset display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
WindowManager.LayoutParams params = getActivity().getWindow().getAttributes();
params.preferredDisplayModeId = 0;
getActivity().getWindow().setAttributes(params);
}
}
}

View File

@@ -20,9 +20,11 @@ import org.jellyfin.androidtv.data.compat.VideoOptions;
import org.jellyfin.androidtv.data.model.DataRefreshService;
import org.jellyfin.androidtv.preference.UserPreferences;
import org.jellyfin.androidtv.preference.UserSettingPreferences;
import org.jellyfin.androidtv.preference.constant.AudioBehavior;
import org.jellyfin.androidtv.preference.constant.NextUpBehavior;
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.ReportingHelper;
@@ -518,16 +520,16 @@ public class PlaybackController implements PlaybackControllerNotifiable {
internalOptions.setMaxBitrate(maxBitrate);
if (exoErrorEncountered || (isLiveTv && !directStreamLiveTv))
internalOptions.setEnableDirectStream(false);
internalOptions.setMaxAudioChannels(Utils.downMixAudio(mFragment.getContext()) ? 2 : null); //have to downmix at server
internalOptions.setSubtitleStreamIndex(forcedSubtitleIndex);
MediaSourceInfo currentMediaSource = getCurrentMediaSource();
if (!isLiveTv && currentMediaSource != null) {
internalOptions.setMediaSourceId(currentMediaSource.getId());
}
DeviceProfile internalProfile = new ExoPlayerProfile(
mFragment.getContext(),
isLiveTv && !userPreferences.getValue().get(UserPreferences.Companion.getLiveTvDirectPlayEnabled()),
userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled())
userPreferences.getValue().get(UserPreferences.Companion.getAc3Enabled()),
userPreferences.getValue().get(UserPreferences.Companion.getAudioBehaviour()) == AudioBehavior.DOWNMIX_TO_STEREO,
!DeviceUtils.has4kVideoSupport()
);
internalOptions.setProfile(internalProfile);
return internalOptions;

View File

@@ -88,7 +88,9 @@ public class LeanbackOverlayFragment extends PlaybackSupportFragment {
@Override
public void onPause() {
super.onPause();
playerAdapter.getMasterOverlayFragment().onPause();
if (playerAdapter != null) {
playerAdapter.getMasterOverlayFragment().onPause();
}
}
public CustomPlaybackTransportControlGlue getPlayerGlue() {

View File

@@ -10,7 +10,6 @@ import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.ui.playback.PlaybackController
import org.jellyfin.androidtv.ui.playback.VideoQualityController
import org.jellyfin.androidtv.ui.playback.overlay.CustomPlaybackTransportControlGlue
import org.jellyfin.androidtv.ui.playback.overlay.LeanbackOverlayFragment
import org.jellyfin.androidtv.ui.playback.overlay.VideoPlayerAdapter
class SelectQualityAction(
@@ -39,8 +38,11 @@ class SelectQualityAction(
}
menu.setGroupCheckable(0, true, true)
menu.getItem(qualityProfiles.keys.indexOf(qualityController.currentQuality))?.let { item ->
item.isChecked = true
val currentQualityIndex = qualityProfiles.keys.indexOf(qualityController.currentQuality)
if (currentQualityIndex != -1) {
menu.getItem(currentQualityIndex)?.let { item ->
item.isChecked = true
}
}
setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) }

View File

@@ -39,12 +39,6 @@ class DeveloperPreferencesScreen : OptionsFragment() {
}
}
checkbox {
setTitle(R.string.enable_reactive_homepage)
setContent(R.string.enable_playback_module_description)
bind(userPreferences, UserPreferences.homeReactive)
}
// Only show in debug mode
// some strings are hardcoded because these options don't show in beta/release builds
if (BuildConfig.DEVELOPMENT) {

View File

@@ -21,7 +21,7 @@ class MyDetailsOverviewRowPresenter(
fun setItem(row: MyDetailsOverviewRow) {
setTitle(row.item.name)
InfoLayoutHelper.addInfoRow(view.context, row.item, binding.fdMainInfoRow, false)
InfoLayoutHelper.addInfoRow(view.context, row.item, row.item.mediaSources?.getOrNull(row.selectedMediaSourceIndex), binding.fdMainInfoRow, false)
binding.fdGenreRow.text = row.item.genres?.joinToString(" / ")
binding.infoTitle1.text = row.infoItem1?.label

View File

@@ -26,6 +26,9 @@ object DeviceUtils {
// Nvidia Shield TV Model
private const val SHIELD_TV_MODEL = "SHIELD Android TV"
// Google Chromecast HD Model
private const val GOOGLE_CHROMECAST_HD_MODEL = "Chromecast HD"
@JvmStatic
val isFireTv: Boolean = Build.MODEL.startsWith(FIRE_TV_PREFIX)
@@ -53,6 +56,7 @@ object DeviceUtils {
FIRE_STICK_MODEL_GEN_3,
FIRE_STICK_LITE_MODEL,
FIRE_TV_MODEL_GEN_1,
FIRE_TV_MODEL_GEN_2
FIRE_TV_MODEL_GEN_2,
GOOGLE_CHROMECAST_HD_MODEL
)
}

View File

@@ -1,33 +0,0 @@
package org.jellyfin.androidtv.util;
import android.content.Context;
import android.view.View;
import android.widget.LinearLayout;
import org.jellyfin.androidtv.ui.browsing.composable.inforow.BaseItemInfoRowView;
import org.jellyfin.sdk.model.api.BaseItemDto;
public class InfoLayoutHelper {
public static void addInfoRow(Context context, BaseItemDto item, LinearLayout layout, boolean includeRuntime) {
// Find existing BaseItemInfoRowView or create a new one
BaseItemInfoRowView baseItemInfoRowView = null;
for (int i = 0; i < layout.getChildCount(); i++) {
View child = layout.getChildAt(i);
if (child instanceof BaseItemInfoRowView) {
baseItemInfoRowView = (BaseItemInfoRowView) child;
break;
}
}
if (baseItemInfoRowView == null) {
baseItemInfoRowView = new BaseItemInfoRowView(context);
layout.addView(baseItemInfoRowView);
}
// Update item info
baseItemInfoRowView.setItem(item);
baseItemInfoRowView.setIncludeRuntime(includeRuntime);
}
}

View File

@@ -0,0 +1,48 @@
package org.jellyfin.androidtv.util
import android.content.Context
import android.widget.LinearLayout
import org.jellyfin.androidtv.ui.browsing.composable.inforow.BaseItemInfoRowView
import org.jellyfin.sdk.model.api.BaseItemDto
import org.jellyfin.sdk.model.api.MediaSourceInfo
object InfoLayoutHelper {
@JvmStatic
fun addInfoRow(
context: Context,
item: BaseItemDto?,
mediaSource: MediaSourceInfo?,
layout: LinearLayout,
includeRuntime: Boolean
) {
// Find existing BaseItemInfoRowView or create a new one
var baseItemInfoRowView: BaseItemInfoRowView? = null
for (i in 0 until layout.childCount) {
val child = layout.getChildAt(i)
if (child is BaseItemInfoRowView) {
baseItemInfoRowView = child
break
}
}
if (baseItemInfoRowView == null) {
baseItemInfoRowView = BaseItemInfoRowView(context)
layout.addView(baseItemInfoRowView)
}
// Update item info
baseItemInfoRowView.item = item
baseItemInfoRowView.mediaSource = mediaSource ?: item?.mediaSources?.firstOrNull()
baseItemInfoRowView.includeRuntime = includeRuntime
}
@JvmStatic
fun addInfoRow(
context: Context,
item: BaseItemDto?,
layout: LinearLayout,
includeRuntime: Boolean
) = addInfoRow(context, item, null, layout, includeRuntime)
}

View File

@@ -10,6 +10,11 @@ import org.jellyfin.androidtv.R
*/
fun String.toHtmlSpanned(): Spanned = HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT)
/**
* Remove HTML tags from string and return the plain representation.
*/
fun String.stripHtml(): String = HtmlCompat.fromHtml(this, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
/**
* Utility to get the string for the "Load channels" button in the Live TV guide.
*/

View File

@@ -1,16 +1,11 @@
package org.jellyfin.androidtv.util
import android.content.Context
import android.media.AudioManager
import android.widget.Toast
import org.jellyfin.androidtv.preference.UserPreferences
import org.jellyfin.androidtv.preference.UserPreferences.Companion.audioBehaviour
import org.jellyfin.androidtv.preference.constant.AudioBehavior
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import timber.log.Timber
import java.util.UUID
import kotlin.math.roundToInt
@@ -98,17 +93,6 @@ object Utils : KoinComponent {
return themeColor
}
@JvmStatic
fun downMixAudio(context: Context): Boolean {
val am = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (am.isBluetoothA2dpOn) {
Timber.i("Downmixing audio due to wired headset")
return true
}
return get<UserPreferences>()[audioBehaviour] === AudioBehavior.DOWNMIX_TO_STEREO
}
@JvmStatic
fun getSafeSeekPosition(position: Long, duration: Long): Long = when {
position >= duration -> (duration - 1000).coerceAtLeast(0)

View File

@@ -1,9 +1,6 @@
package org.jellyfin.androidtv.util.profile
import android.content.Context
import org.jellyfin.androidtv.constant.Codec
import org.jellyfin.androidtv.util.DeviceUtils
import org.jellyfin.androidtv.util.Utils
import org.jellyfin.androidtv.util.profile.ProfileHelper.audioDirectPlayProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceAV1CodecProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.deviceHevcCodecProfile
@@ -14,6 +11,7 @@ import org.jellyfin.androidtv.util.profile.ProfileHelper.max1080pProfileConditio
import org.jellyfin.androidtv.util.profile.ProfileHelper.maxAudioChannelsCodecProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.photoDirectPlayProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.subtitleProfile
import org.jellyfin.androidtv.util.profile.ProfileHelper.supportsHevc
import org.jellyfin.apiclient.model.dlna.CodecProfile
import org.jellyfin.apiclient.model.dlna.CodecType
import org.jellyfin.apiclient.model.dlna.DeviceProfile
@@ -27,9 +25,10 @@ import org.jellyfin.apiclient.model.dlna.SubtitleDeliveryMethod
import org.jellyfin.apiclient.model.dlna.TranscodingProfile
class ExoPlayerProfile(
context: Context,
disableVideoDirectPlay: Boolean = false,
isAC3Enabled: Boolean = false,
disableVideoDirectPlay: Boolean,
isAC3Enabled: Boolean,
downMixAudio: Boolean,
disable4KVideo: Boolean
) : DeviceProfile() {
private val downmixSupportedAudioCodecs = arrayOf(
Codec.Audio.AAC,
@@ -78,12 +77,12 @@ class ExoPlayerProfile(
this.context = EncodingContext.Streaming
container = Codec.Container.TS
videoCodec = buildList {
if (deviceHevcCodecProfile.ContainsCodec(Codec.Video.HEVC, Codec.Container.TS)) add(Codec.Video.HEVC)
if (supportsHevc) add(Codec.Video.HEVC)
add(Codec.Video.H264)
}.joinToString(",")
audioCodec = when {
Utils.downMixAudio(context) -> downmixSupportedAudioCodecs
else -> allSupportedAudioCodecsWithoutFFmpegExperimental
audioCodec = when (downMixAudio) {
true -> downmixSupportedAudioCodecs
false -> allSupportedAudioCodecsWithoutFFmpegExperimental
}.joinToString(",")
protocol = "hls"
copyTimestamps = false
@@ -129,22 +128,26 @@ class ExoPlayerProfile(
Codec.Video.AV1
).joinToString(",")
audioCodec = when {
Utils.downMixAudio(context) -> downmixSupportedAudioCodecs
else -> allSupportedAudioCodecs
audioCodec = when (downMixAudio) {
true -> downmixSupportedAudioCodecs
false -> allSupportedAudioCodecs
}.joinToString(",")
})
}
// Audio direct play
add(audioDirectPlayProfile(allSupportedAudioCodecs + arrayOf(
Codec.Audio.MPA,
Codec.Audio.WAV,
Codec.Audio.WMA,
Codec.Audio.OGG,
Codec.Audio.OGA,
Codec.Audio.WEBMA,
Codec.Audio.APE,
)))
add(
audioDirectPlayProfile(
allSupportedAudioCodecs + arrayOf(
Codec.Audio.MPA,
Codec.Audio.WAV,
Codec.Audio.WMA,
Codec.Audio.OGG,
Codec.Audio.OGA,
Codec.Audio.WEBMA,
Codec.Audio.APE,
)
)
)
// Photo direct play
add(photoDirectPlayProfile)
}.toTypedArray()
@@ -157,7 +160,7 @@ class ExoPlayerProfile(
conditions = buildList {
add(h264VideoProfileCondition)
add(h264VideoLevelProfileCondition)
if (!DeviceUtils.has4kVideoSupport()) addAll(max1080pProfileConditions)
if (disable4KVideo) addAll(max1080pProfileConditions)
}.toTypedArray()
})
// H264 ref frames profile
@@ -204,15 +207,14 @@ class ExoPlayerProfile(
// AV1 profile
add(deviceAV1CodecProfile)
// Limit video resolution support for older devices
if (!DeviceUtils.has4kVideoSupport()) {
if (disable4KVideo) {
add(CodecProfile().apply {
type = CodecType.Video
conditions = max1080pProfileConditions
})
}
// Audio channel profile
if (!Utils.downMixAudio(context)) add(maxAudioChannelsCodecProfile(channels = 8))
else add(maxAudioChannelsCodecProfile(channels = 2))
add(maxAudioChannelsCodecProfile(channels = if (downMixAudio) 2 else 8))
}.toTypedArray()
subtitleProfiles = arrayOf(

View File

@@ -63,13 +63,17 @@ object ProfileHelper {
}
}
val supportsHevc by lazy {
MediaTest.supportsHevc()
}
val deviceHevcCodecProfile by lazy {
CodecProfile().apply {
type = CodecType.Video
codec = Codec.Video.HEVC
conditions = when {
!MediaTest.supportsHevc() -> {
!supportsHevc -> {
// The following condition is a method to exclude all HEVC
Timber.i("*** Does NOT support HEVC")
arrayOf(
@@ -107,7 +111,7 @@ object ProfileHelper {
val deviceHevcLevelCodecProfiles by lazy {
buildList {
if (MediaTest.supportsHevc()) {
if (supportsHevc) {
add(CodecProfile().apply {
type = CodecType.Video
codec = Codec.Video.HEVC

View File

@@ -217,9 +217,10 @@ class SdkPlaybackHelper(
val addIntros = allowIntros && userPreferences[UserPreferences.cinemaModeEnabled]
if (addIntros) {
val intros = runCatching {
api.userLibraryApi.getIntros(mainItem.id).content.items
}.getOrNull().orEmpty()
val intros = runCatching { api.userLibraryApi.getIntros(mainItem.id).content.items }.getOrNull()
.orEmpty()
// Force the type to be trailer as the legacy playback UI uses it to determine if it should show the next up screen
.map { it.copy(type = BaseItemKind.TRAILER) }
intros + parts
} else {

View File

@@ -20,15 +20,15 @@ androidx-media3 = "1.4.0"
androidx-preference = "1.2.1"
androidx-recyclerview = "1.3.2"
androidx-startup = "1.1.1"
androidx-tv = "1.0.0-rc01"
androidx-tv = "1.0.0-rc02"
androidx-tvprovider = "1.1.0-alpha01"
androidx-window = "1.3.0"
androidx-work = "2.9.0"
androidx-work = "2.9.1"
coil = "2.7.0"
detekt = "1.23.6"
jellyfin-androidx-media = "1.3.1+2"
jellyfin-apiclient = "v0.7.10"
jellyfin-sdk = "1.5.2"
jellyfin-sdk = "1.5.3"
koin = "3.5.6"
koin-compose = "3.5.6"
kotest = "5.9.1"