Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef899f98bc | ||
|
|
de6c7904f0 | ||
|
|
0a79d5b60f | ||
|
|
36e0bc1edc | ||
|
|
8faeb3aac1 | ||
|
|
160ef4914f | ||
|
|
474c0774f1 | ||
|
|
0c9ba1ccec | ||
|
|
0c667ab615 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) : "");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,22 +127,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 +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(
|
||||
|
||||
Reference in New Issue
Block a user