This commit is contained in:
Alex Kim
2025-12-07 01:43:47 +11:00
parent 2648877eb8
commit 7135be198a
6 changed files with 26 additions and 52 deletions

View File

@@ -69,7 +69,7 @@ export default function page() {
const [isMuted, setIsMuted] = useState(false);
const [isBuffering, setIsBuffering] = useState(true);
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [trackCount, setTrackCount] = useState(0);
const [tracksReady, setTracksReady] = useState(false);
const progress = useSharedValue(0);
const isSeeking = useSharedValue(false);
@@ -338,6 +338,7 @@ export default function page() {
const currentPlayStateInfo = useCallback(() => {
if (!stream || !item?.Id) return;
console.log("subtitle");
return {
itemId: item.Id,
audioStreamIndex: audioIndex ? audioIndex : undefined,
@@ -674,7 +675,7 @@ export default function page() {
item={item}
mediaSource={stream?.mediaSource}
isVideoLoaded={isVideoLoaded}
trackCount={trackCount}
tracksReady={tracksReady}
>
<VideoProvider>
<View
@@ -710,9 +711,8 @@ export default function page() {
);
writeToLog("ERROR", "Video Error", e.nativeEvent);
}}
onTracksReady={(e) => {
console.log("[Player] Tracks ready:", e.nativeEvent.trackCount);
setTrackCount(e.nativeEvent.trackCount);
onTracksReady={() => {
setTracksReady(true);
}}
/>
</View>

View File

@@ -16,7 +16,7 @@ interface PlayerContextProps {
item: BaseItemDto;
mediaSource: MediaSourceInfo | null | undefined;
isVideoLoaded: boolean;
trackCount: number;
tracksReady: boolean;
}
const PlayerContext = createContext<PlayerContextProps | undefined>(undefined);
@@ -27,7 +27,7 @@ interface PlayerProviderProps {
item: BaseItemDto;
mediaSource: MediaSourceInfo | null | undefined;
isVideoLoaded: boolean;
trackCount: number;
tracksReady: boolean;
}
export const PlayerProvider: React.FC<PlayerProviderProps> = ({
@@ -36,11 +36,11 @@ export const PlayerProvider: React.FC<PlayerProviderProps> = ({
item,
mediaSource,
isVideoLoaded,
trackCount,
tracksReady,
}) => {
const value = useMemo(
() => ({ playerRef, item, mediaSource, isVideoLoaded, trackCount }),
[playerRef, item, mediaSource, isVideoLoaded, trackCount],
() => ({ playerRef, item, mediaSource, isVideoLoaded, tracksReady }),
[playerRef, item, mediaSource, isVideoLoaded, tracksReady],
);
return (

View File

@@ -64,10 +64,6 @@
* The order of subtitles in Jellyfin's MediaStreams matches the order in MPV.
*/
import {
type MediaStream,
SubtitleDeliveryMethod,
} from "@jellyfin/sdk/lib/generated-client";
import { router, useLocalSearchParams } from "expo-router";
import type React from "react";
import {
@@ -79,6 +75,10 @@ import {
useState,
} from "react";
import type { AudioTrack, SubtitleTrack } from "@/modules";
import {
isImageBasedSubtitle,
isSubtitleInMpv,
} from "@/utils/jellyfin/subtitleUtils";
import type { Track } from "../types";
import { usePlayerContext, usePlayerControls } from "./PlayerContext";
@@ -95,7 +95,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
const { trackCount, mediaSource } = usePlayerContext();
const { tracksReady, mediaSource } = usePlayerContext();
const playerControls = usePlayerControls();
const { itemId, audioIndex, bitrateValue, subtitleIndex, playbackPosition } =
@@ -115,10 +115,6 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
const isTranscoding = Boolean(mediaSource?.TranscodingUrl);
/** Check if subtitle is image-based (PGS, VOBSUB, etc.) */
const isImageBased = (sub: MediaStream): boolean =>
sub.IsTextSubtitleStream === false;
/**
* Check if the currently selected subtitle is image-based.
* Used to determine if we need to refresh the player when changing subs.
@@ -128,7 +124,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
const currentSub = allSubs.find(
(s) => s.Index?.toString() === subtitleIndex,
);
return currentSub ? isImageBased(currentSub) : false;
return currentSub ? isImageBasedSubtitle(currentSub) : false;
}, [allSubs, subtitleIndex]);
/**
@@ -150,29 +146,9 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
router.replace(`player/direct-player?${queryParams}` as any);
};
/**
* Determine if a subtitle is available in MPV's track list.
*
* A subtitle is in MPV if:
* - Delivery is Embed/Hls/External AND not an image-based sub during transcode
*/
const isSubtitleInMpv = (sub: MediaStream): boolean => {
// During transcoding, image-based subs are burned in, not in MPV
if (isTranscoding && isImageBased(sub)) {
return false;
}
// Embed/Hls/External methods mean the sub is loaded into MPV
return (
sub.DeliveryMethod === SubtitleDeliveryMethod.Embed ||
sub.DeliveryMethod === SubtitleDeliveryMethod.Hls ||
sub.DeliveryMethod === SubtitleDeliveryMethod.External
);
};
// Fetch tracks when track count changes
// Fetch tracks when ready
useEffect(() => {
if (trackCount === 0) return;
if (!tracksReady) return;
const fetchTracks = async () => {
const [subtitleData, audioData] = await Promise.all([
@@ -184,7 +160,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
let mpvIndex = 0; // MPV track index counter (only incremented for subs in MPV)
const subs: Track[] = allSubs.map((sub) => {
const inMpv = isSubtitleInMpv(sub);
const inMpv = isSubtitleInMpv(sub, isTranscoding);
// Get MPV track ID: only if this sub is actually in MPV's track list
const mpvId = inMpv
@@ -200,7 +176,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
// Need to refresh player so Jellyfin burns in the new sub
if (
isTranscoding &&
(isImageBased(sub) || isCurrentSubImageBased)
(isImageBasedSubtitle(sub) || isCurrentSubImageBased)
) {
replacePlayer({ subtitleIndex: String(sub.Index) });
return;
@@ -257,7 +233,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
};
fetchTracks();
}, [trackCount, mediaSource]);
}, [tracksReady, mediaSource]);
return (
<VideoContext.Provider value={{ subtitleTracks, audioTracks }}>

View File

@@ -9,7 +9,7 @@ protocol MPVSoftwareRendererDelegate: AnyObject {
func renderer(_ renderer: MPVSoftwareRenderer, didChangePause isPaused: Bool)
func renderer(_ renderer: MPVSoftwareRenderer, didChangeLoading isLoading: Bool)
func renderer(_ renderer: MPVSoftwareRenderer, didBecomeReadyToSeek: Bool)
func renderer(_ renderer: MPVSoftwareRenderer, didUpdateTrackList trackCount: Int)
func renderer(_ renderer: MPVSoftwareRenderer, didBecomeTracksReady: Bool)
}
final class MPVSoftwareRenderer {
@@ -961,7 +961,7 @@ final class MPVSoftwareRenderer {
Logger.shared.log("Track list updated: \(trackCount) tracks available", type: "Info")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.delegate?.renderer(self, didUpdateTrackList: Int(trackCount))
self.delegate?.renderer(self, didBecomeTracksReady: true)
}
}
default:

View File

@@ -317,10 +317,10 @@ extension MpvPlayerView: MPVSoftwareRendererDelegate {
}
}
func renderer(_: MPVSoftwareRenderer, didUpdateTrackList trackCount: Int) {
func renderer(_: MPVSoftwareRenderer, didBecomeTracksReady: Bool) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.onTracksReady(["trackCount": trackCount])
self.onTracksReady([:])
}
}
}

View File

@@ -21,9 +21,7 @@ export type OnErrorEventPayload = {
error: string;
};
export type OnTracksReadyEventPayload = {
trackCount: number;
};
export type OnTracksReadyEventPayload = Record<string, never>;
export type MpvPlayerModuleEvents = {
onChange: (params: ChangeEventPayload) => void;