fix: a few playback and home recommendation issues

Fixed the home recommendation display to use proper images.
Fixed an issue for the subtitles background rendering
Fixed playback resume from position

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
This commit is contained in:
Lance Chant
2026-05-21 18:04:45 +02:00
parent d1794798e7
commit cc89b5df89
7 changed files with 105 additions and 33 deletions

View File

@@ -543,11 +543,6 @@ export default function page() {
], ],
); );
/** Gets the initial playback position in seconds. */
const _startPosition = useMemo(() => {
return ticksToSeconds(getInitialPlaybackTicks());
}, [getInitialPlaybackTicks]);
/** Build video source config for MPV */ /** Build video source config for MPV */
const videoSource = useMemo<MpvVideoSource | undefined>(() => { const videoSource = useMemo<MpvVideoSource | undefined>(() => {
if (!stream?.url) return undefined; if (!stream?.url) return undefined;
@@ -1104,6 +1099,13 @@ export default function page() {
applySubtitleSettings(); applySubtitleSettings();
}, [isVideoLoaded, settings]); }, [isVideoLoaded, settings]);
// Seek to resume position after file is loaded (MPV_EVENT_FILE_LOADED)
useEffect(() => {
if (!tracksReady || !videoRef.current) return;
const ticks = getInitialPlaybackTicks();
videoRef.current?.seekTo?.(ticksToSeconds(ticks));
}, [tracksReady, getInitialPlaybackTicks]);
// Apply initial playback speed when video loads // Apply initial playback speed when video loads
useEffect(() => { useEffect(() => {
if (!isVideoLoaded || !videoRef.current) return; if (!isVideoLoaded || !videoRef.current) return;

View File

@@ -69,6 +69,7 @@ export const PlayButton: React.FC<Props> = ({
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "", subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
mediaSourceId: selectedOptions.mediaSource?.Id ?? "", mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "", bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
}); });
const queryString = queryParams.toString(); const queryString = queryParams.toString();

View File

@@ -462,7 +462,19 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver {
fun setSubtitleFontSize(size: Int) { fun setSubtitleFontSize(size: Int) {
MPVLib.setPropertyInt("sub-font-size", size) MPVLib.setPropertyInt("sub-font-size", size)
} }
fun setSubtitleBorderStyle(style: String) {
MPVLib.setPropertyString("sub-border-style", style)
}
fun setSubtitleBackgroundColor(color: String) {
MPVLib.setPropertyString("sub-back-color", color)
}
fun setSubtitleAssOverride(mode: String) {
MPVLib.setPropertyString("sub-ass-override", mode)
}
// MARK: - Audio Track Controls // MARK: - Audio Track Controls
fun getAudioTracks(): List<Map<String, Any>> { fun getAudioTracks(): List<Map<String, Any>> {

View File

@@ -151,6 +151,18 @@ class MpvPlayerModule : Module() {
view.setSubtitleFontSize(size) view.setSubtitleFontSize(size)
} }
AsyncFunction("setSubtitleBorderStyle") { view: MpvPlayerView, style: String ->
view.setSubtitleBorderStyle(style)
}
AsyncFunction("setSubtitleBackgroundColor") { view: MpvPlayerView, color: String ->
view.setSubtitleBackgroundColor(color)
}
AsyncFunction("setSubtitleAssOverride") { view: MpvPlayerView, mode: String ->
view.setSubtitleAssOverride(mode)
}
// Audio track functions // Audio track functions
AsyncFunction("getAudioTracks") { view: MpvPlayerView -> AsyncFunction("getAudioTracks") { view: MpvPlayerView ->
view.getAudioTracks() view.getAudioTracks()

View File

@@ -271,7 +271,19 @@ class MpvPlayerView(context: Context, appContext: AppContext) : ExpoView(context
fun setSubtitleFontSize(size: Int) { fun setSubtitleFontSize(size: Int) {
renderer?.setSubtitleFontSize(size) renderer?.setSubtitleFontSize(size)
} }
fun setSubtitleBorderStyle(style: String) {
renderer?.setSubtitleBorderStyle(style)
}
fun setSubtitleBackgroundColor(color: String) {
renderer?.setSubtitleBackgroundColor(color)
}
fun setSubtitleAssOverride(mode: String) {
renderer?.setSubtitleAssOverride(mode)
}
// MARK: - Audio Track Controls // MARK: - Audio Track Controls
fun getAudioTracks(): List<Map<String, Any>> { fun getAudioTracks(): List<Map<String, Any>> {

View File

@@ -243,6 +243,7 @@ internal object TvRecommendationsPublisher {
.setContentId(providerId) .setContentId(providerId)
.setIntentUri(buildIntentUri(context, item.optString("playRoute").ifBlank { item.optString("route") })) .setIntentUri(buildIntentUri(context, item.optString("playRoute").ifBlank { item.optString("route") }))
.setWeight(weight) .setWeight(weight)
.setPosterArtAspectRatio(TvContractCompat.PreviewPrograms.ASPECT_RATIO_16_9)
item.optString("subtitle").takeIf { it.isNotBlank() }?.let { item.optString("subtitle").takeIf { it.isNotBlank() }?.let {
builder.setDescription(it) builder.setDescription(it)
@@ -250,7 +251,6 @@ internal object TvRecommendationsPublisher {
imageUrl.takeIf { it.isNotBlank() }?.let { imageUrl.takeIf { it.isNotBlank() }?.let {
val imageUri = Uri.parse(it) val imageUri = Uri.parse(it)
builder.setPosterArtUri(imageUri)
builder.setThumbnailUri(imageUri) builder.setThumbnailUri(imageUri)
} }

View File

@@ -25,30 +25,60 @@ export interface TVDiscoveryPayload {
sections: TVDiscoverySection[]; sections: TVDiscoverySection[];
} }
function getTVDiscoveryImageUrl( function getTVDiscoveryImage(
item: BaseItemDto, item: BaseItemDto,
api: Api, api: Api,
): string | undefined { ): { url: string } | undefined {
const baseUrl = api.basePath; const baseUrl = api.basePath;
if (item.Type === "Episode") { // 1. Episode backdrop
if (item.SeriesId && item.SeriesPrimaryImageTag) { const episodeBackdrop = item.BackdropImageTags?.[0];
return `${baseUrl}/Items/${item.SeriesId}/Images/Primary?quality=90&tag=${encodeURIComponent(item.SeriesPrimaryImageTag)}&width=500`; if (item.Id && episodeBackdrop) {
} return {
url:
if (item.ParentPrimaryImageItemId && item.ParentPrimaryImageTag) { `${baseUrl}/Items/${item.Id}/Images/Backdrop/0` +
return `${baseUrl}/Items/${item.ParentPrimaryImageItemId}/Images/Primary?quality=90&tag=${encodeURIComponent(item.ParentPrimaryImageTag)}&width=500`; `?fillWidth=1920` +
} `&fillHeight=1080` +
`&quality=90` +
`&tag=${encodeURIComponent(episodeBackdrop)}`,
};
} }
// 2. Series backdrop
if (item.SeriesId) {
return {
url:
`${baseUrl}/Items/${item.SeriesId}/Images/Backdrop` +
`?fillWidth=1920` +
`&fillHeight=1080` +
`&quality=90`,
};
}
// 3. Generic item backdrop
const backdrop = item.BackdropImageTags?.[0];
if (item.Id && backdrop) {
return {
url:
`${baseUrl}/Items/${item.Id}/Images/Backdrop/0` +
`?fillWidth=1920` +
`&fillHeight=1080` +
`&quality=90` +
`&tag=${encodeURIComponent(backdrop)}`,
};
}
// 4. Last resort: crop poster into landscape
const primaryTag = item.ImageTags?.Primary; const primaryTag = item.ImageTags?.Primary;
if (item.Id && primaryTag) { if (item.Id && primaryTag) {
return `${baseUrl}/Items/${item.Id}/Images/Primary?quality=90&tag=${encodeURIComponent(primaryTag)}&width=500`; return {
} url:
`${baseUrl}/Items/${item.Id}/Images/Primary` +
const backdropTag = item.BackdropImageTags?.[0]; `?fillWidth=1920` +
if (item.Id && backdropTag) { `&fillHeight=1080` +
return `${baseUrl}/Items/${item.Id}/Images/Backdrop/0?quality=90&tag=${encodeURIComponent(backdropTag)}&width=800`; `&quality=90` +
`&tag=${encodeURIComponent(primaryTag)}`,
};
} }
return undefined; return undefined;
@@ -98,15 +128,18 @@ function sectionFromItems(
const payloadItems = (items || []) const payloadItems = (items || [])
.filter((item) => item.Id && item.Name) .filter((item) => item.Id && item.Name)
.slice(0, TV_DISCOVERY_ITEM_LIMIT) .slice(0, TV_DISCOVERY_ITEM_LIMIT)
.map((item) => ({ .map((item) => {
id: item.Id!, const image = getTVDiscoveryImage(item, api);
itemType: item.Type || undefined, return {
title: getTVDiscoveryTitle(item), id: item.Id!,
subtitle: getTVDiscoverySubtitle(item), itemType: item.Type || undefined,
imageUrl: getTVDiscoveryImageUrl(item, api), title: getTVDiscoveryTitle(item),
route: `streamyfin://topshelf/item?id=${encodeURIComponent(item.Id!)}&type=${encodeURIComponent(item.Type || "")}`, subtitle: getTVDiscoverySubtitle(item),
playRoute: `streamyfin://topshelf/play?id=${encodeURIComponent(item.Id!)}`, imageUrl: image?.url,
})); route: `streamyfin://topshelf/item?id=${encodeURIComponent(item.Id!)}&type=${encodeURIComponent(item.Type || "")}`,
playRoute: `streamyfin://topshelf/play?id=${encodeURIComponent(item.Id!)}`,
};
});
if (payloadItems.length === 0) return null; if (payloadItems.length === 0) return null;