Performance improvments for android playback

Ensured the correct hardware encoding is used for android TV versions
Fixed scaling of the hero layout

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
Lance Chant
2026-05-13 13:50:40 +02:00
parent 4be540fe3c
commit c012bd44bd
5 changed files with 70 additions and 38 deletions

View File

@@ -134,7 +134,7 @@ export default function TabLayout() {
tabBarItemHidden: !Platform.isTV, tabBarItemHidden: !Platform.isTV,
tabBarIcon: tabBarIcon:
Platform.OS === "android" Platform.OS === "android"
? (_e) => require("@/assets/icons/list.png") ? (_e) => require("@/assets/icons/gear.png") //Should maybe use other libraries to have it uniform
: (_e) => ({ sfSymbol: "gearshape.fill" }), : (_e) => ({ sfSymbol: "gearshape.fill" }),
}} }}
/> />

BIN
assets/icons/gear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -329,18 +329,22 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
windowSize={5} windowSize={5}
removeClippedSubviews={false} removeClippedSubviews={false}
maintainVisibleContentPosition={{ minIndexForVisible: 0 }} maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
ListHeaderComponent={
<View style={{ width: sizes.padding.horizontal }} />
}
style={{ overflow: "visible" }} style={{ overflow: "visible" }}
contentInset={{
left: sizes.padding.horizontal,
right: sizes.padding.horizontal,
}}
contentOffset={{ x: -sizes.padding.horizontal, y: 0 }}
contentContainerStyle={{ contentContainerStyle={{
paddingVertical: SCALE_PADDING, paddingVertical: sizes.gaps.small,
paddingLeft: sizes.padding.horizontal,
paddingRight: sizes.padding.horizontal,
}} }}
// Below is a work around with the contentInset, same in TVHeroCarousel, if okay on apple remove
// ListHeaderComponent={
// <View style={{ width: sizes.padding.horizontal }} />
// }
// contentInset={{
// left: sizes.padding.horizontal,
// right: sizes.padding.horizontal,
// }}
// contentOffset={{ x: -sizes.padding.horizontal, y: 0 }}
// contentContainerStyle={{ paddingVertical: SCALE_PADDING }}
ListFooterComponent={ ListFooterComponent={
<View <View
style={{ style={{

View File

@@ -33,6 +33,7 @@ import {
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { scaleSize } from "@/utils/scaleSize";
import { runtimeTicksToMinutes } from "@/utils/time"; import { runtimeTicksToMinutes } from "@/utils/time";
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window"); const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
@@ -129,7 +130,7 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
<GlassPosterView <GlassPosterView
imageUrl={posterUrl} imageUrl={posterUrl}
aspectRatio={16 / 9} aspectRatio={16 / 9}
cornerRadius={24} cornerRadius={scaleSize(24)}
progress={progress} progress={progress}
showWatchedIndicator={false} showWatchedIndicator={false}
isFocused={focused} isFocused={focused}
@@ -154,15 +155,15 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
style={{ style={{
width: sizes.posters.episode, width: sizes.posters.episode,
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
borderRadius: 24, borderRadius: scaleSize(24),
overflow: "hidden", overflow: "hidden",
transform: [{ scale }], transform: [{ scale }],
borderWidth: 2, borderWidth: scaleSize(2),
borderColor: focused ? "#FFFFFF" : "transparent", borderColor: focused ? "#FFFFFF" : "transparent",
shadowColor: "#FFFFFF", shadowColor: "#FFFFFF",
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
shadowOpacity: focused ? 0.6 : 0, shadowOpacity: focused ? 0.6 : 0,
shadowRadius: focused ? 20 : 0, shadowRadius: focused ? scaleSize(20) : 0,
}} }}
> >
{posterUrl ? ( {posterUrl ? (
@@ -183,7 +184,7 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
> >
<Ionicons <Ionicons
name='film-outline' name='film-outline'
size={48} size={scaleSize(48)}
color='rgba(255,255,255,0.3)' color='rgba(255,255,255,0.3)'
/> />
</View> </View>
@@ -472,7 +473,10 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
left: sizes.padding.horizontal, left: sizes.padding.horizontal,
right: sizes.padding.horizontal, right: sizes.padding.horizontal,
bottom: bottom:
40 + sizes.posters.episode * (9 / 16) + sizes.gaps.small * 2 + 20, scaleSize(40) +
sizes.posters.episode * (9 / 16) +
sizes.gaps.small * 2 +
scaleSize(20),
}} }}
> >
{/* Logo or Title */} {/* Logo or Title */}
@@ -480,9 +484,9 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
<Image <Image
source={{ uri: logoUrl }} source={{ uri: logoUrl }}
style={{ style={{
height: 100, height: scaleSize(100),
width: SCREEN_WIDTH * 0.35, width: SCREEN_WIDTH * 0.35,
marginBottom: 16, marginBottom: scaleSize(16),
}} }}
contentFit='contain' contentFit='contain'
contentPosition='left' contentPosition='left'
@@ -493,7 +497,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
fontSize: typography.display, fontSize: typography.display,
fontWeight: "bold", fontWeight: "bold",
color: "#FFFFFF", color: "#FFFFFF",
marginBottom: 12, marginBottom: scaleSize(12),
}} }}
numberOfLines={1} numberOfLines={1}
> >
@@ -507,7 +511,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
style={{ style={{
fontSize: typography.body, fontSize: typography.body,
color: "rgba(255,255,255,0.9)", color: "rgba(255,255,255,0.9)",
marginBottom: 12, marginBottom: scaleSize(12),
}} }}
numberOfLines={1} numberOfLines={1}
> >
@@ -521,7 +525,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
style={{ style={{
fontSize: typography.body, fontSize: typography.body,
color: "rgba(255,255,255,0.8)", color: "rgba(255,255,255,0.8)",
marginBottom: 16, marginBottom: scaleSize(16),
maxWidth: SCREEN_WIDTH * 0.5, maxWidth: SCREEN_WIDTH * 0.5,
lineHeight: typography.body * 1.4, lineHeight: typography.body * 1.4,
}} }}
@@ -536,7 +540,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
style={{ style={{
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 16, gap: scaleSize(16),
}} }}
> >
{year && ( {year && (
@@ -562,10 +566,10 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
{activeItem?.OfficialRating && ( {activeItem?.OfficialRating && (
<View <View
style={{ style={{
paddingHorizontal: 8, paddingHorizontal: scaleSize(8),
paddingVertical: 2, paddingVertical: scaleSize(2),
borderRadius: 4, borderRadius: scaleSize(4),
borderWidth: 1, borderWidth: scaleSize(1),
borderColor: "rgba(255,255,255,0.5)", borderColor: "rgba(255,255,255,0.5)",
}} }}
> >
@@ -584,15 +588,15 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
style={{ style={{
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
gap: 6, gap: scaleSize(6),
}} }}
> >
<View <View
style={{ style={{
width: 60, width: scaleSize(60),
height: 4, height: scaleSize(4),
backgroundColor: "rgba(255,255,255,0.3)", backgroundColor: "rgba(255,255,255,0.3)",
borderRadius: 2, borderRadius: scaleSize(2),
overflow: "hidden", overflow: "hidden",
}} }}
> >
@@ -624,7 +628,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
position: "absolute", position: "absolute",
left: 0, left: 0,
right: 0, right: 0,
bottom: 40, bottom: scaleSize(40),
}} }}
> >
<FlatList <FlatList
@@ -633,12 +637,21 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
style={{ overflow: "visible" }} style={{ overflow: "visible" }}
contentInset={{ contentContainerStyle={{
left: sizes.padding.horizontal, paddingVertical: sizes.gaps.small,
right: sizes.padding.horizontal, paddingLeft: sizes.padding.horizontal,
paddingRight: sizes.padding.horizontal,
}} }}
contentOffset={{ x: -sizes.padding.horizontal, y: 0 }} // Below is a work around with the contentInset, same in infiniteScrollingCollectionList, if okay on apple remove
contentContainerStyle={{ paddingVertical: sizes.gaps.small }} // ListHeaderComponent={
// <View style={{ width: sizes.padding.horizontal }} />
// }
// contentInset={{
// left: sizes.padding.horizontal,
// right: sizes.padding.horizontal,
// }}
// contentOffset={{ x: -sizes.padding.horizontal, y: 0 }}
// contentContainerStyle={{ paddingVertical: sizes.gaps.small }}
renderItem={renderHeroCard} renderItem={renderHeroCard}
removeClippedSubviews={false} removeClippedSubviews={false}
initialNumToRender={8} initialNumToRender={8}

View File

@@ -1,6 +1,8 @@
package expo.modules.mpvplayer package expo.modules.mpvplayer
import android.app.UiModeManager
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.content.res.AssetManager import android.content.res.AssetManager
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -27,7 +29,12 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
const val MPV_FORMAT_DOUBLE = 5 const val MPV_FORMAT_DOUBLE = 5
const val MPV_FORMAT_NODE = 6 const val MPV_FORMAT_NODE = 6
} }
private fun isTvDevice(): Boolean {
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
}
interface Delegate { interface Delegate {
fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double)
fun onPauseChanged(isPaused: Boolean) fun onPauseChanged(isPaused: Boolean)
@@ -157,7 +164,15 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
MPVLib.setOptionString("opengl-es", "yes") MPVLib.setOptionString("opengl-es", "yes")
// Hardware video decoding // Hardware video decoding
MPVLib.setOptionString("hwdec", "mediacodec-copy") // TV: zero-copy (mediacodec) for better performance on low-power devices
// Mobile: copy mode (mediacodec-copy) for better compatibility
val isTV = isTvDevice()
if (isTV) {
MPVLib.setOptionString("hwdec", "mediacodec")
MPVLib.setOptionString("profile", "fast")
} else {
MPVLib.setOptionString("hwdec", "mediacodec-copy")
}
MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1") MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1")
// Cache settings for better network streaming // Cache settings for better network streaming