Compare commits

...

9 Commits

Author SHA1 Message Date
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
13 changed files with 74 additions and 70 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.

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

@@ -575,7 +575,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 +675,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 {

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,8 +88,10 @@ public class LeanbackOverlayFragment extends PlaybackSupportFragment {
@Override
public void onPause() {
super.onPause();
if (playerAdapter != null) {
playerAdapter.getMasterOverlayFragment().onPause();
}
}
public CustomPlaybackTransportControlGlue getPlayerGlue() {
return playerGlue;

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,9 +38,12 @@ class SelectQualityAction(
}
menu.setGroupCheckable(0, true, true)
menu.getItem(qualityProfiles.keys.indexOf(qualityController.currentQuality))?.let { item ->
val currentQualityIndex = qualityProfiles.keys.indexOf(qualityController.currentQuality)
if (currentQualityIndex != -1) {
menu.getItem(currentQualityIndex)?.let { item ->
item.isChecked = true
}
}
setOnDismissListener { videoPlayerAdapter.leanbackOverlayFragment.setFading(true) }
setOnMenuItemClickListener { menuItem ->

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

@@ -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
@@ -27,9 +24,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,
@@ -81,9 +79,9 @@ class ExoPlayerProfile(
if (deviceHevcCodecProfile.ContainsCodec(Codec.Video.HEVC, Codec.Container.TS)) 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,14 +127,16 @@ 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(
add(
audioDirectPlayProfile(
allSupportedAudioCodecs + arrayOf(
Codec.Audio.MPA,
Codec.Audio.WAV,
Codec.Audio.WMA,
@@ -144,7 +144,9 @@ class ExoPlayerProfile(
Codec.Audio.OGA,
Codec.Audio.WEBMA,
Codec.Audio.APE,
)))
)
)
)
// Photo direct play
add(photoDirectPlayProfile)
}.toTypedArray()
@@ -157,7 +159,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 +206,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(