From 9763c260460f6612f7967db45f85329c97c163ec Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Wed, 28 Jan 2026 19:40:18 +0100 Subject: [PATCH] fix(player): handle remote streams and live tv containers correctly --- app/(auth)/player/direct-player.tsx | 30 +++++++++++++++++++++------- utils/jellyfin/media/getStreamUrl.ts | 24 +++++++++++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 3155849b..00f3e74f 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -262,6 +262,7 @@ export default function page() { mediaSource: MediaSourceInfo; sessionId: string; url: string; + requiredHttpHeaders?: Record; } const [stream, setStream] = useState(null); @@ -324,7 +325,7 @@ export default function page() { deviceProfile: generateDeviceProfile(), }); if (!res) return null; - const { mediaSource, sessionId, url } = res; + const { mediaSource, sessionId, url, requiredHttpHeaders } = res; if (!sessionId || !mediaSource || !url) { Alert.alert( @@ -333,7 +334,7 @@ export default function page() { ); return null; } - result = { mediaSource, sessionId, url }; + result = { mediaSource, sessionId, url, requiredHttpHeaders }; } setStream(result); setStreamStatus({ isLoading: false, isError: false }); @@ -601,17 +602,32 @@ export default function page() { source.externalSubtitles = externalSubs; } - // Add auth headers only for online streaming (not for local file:// URLs) - if (!offline && api?.accessToken) { - source.headers = { - Authorization: `MediaBrowser Token="${api.accessToken}"`, - }; + // Add headers for online streaming (not for local file:// URLs) + if (!offline) { + const headers: Record = {}; + const isRemoteStream = + mediaSource?.IsRemote && mediaSource?.Protocol === "Http"; + + // Add auth header only for Jellyfin API requests (not for external/remote streams) + if (api?.accessToken && !isRemoteStream) { + headers.Authorization = `MediaBrowser Token="${api.accessToken}"`; + } + + // Add any required headers from the media source (e.g., for external/remote streams) + if (stream?.requiredHttpHeaders) { + Object.assign(headers, stream.requiredHttpHeaders); + } + + if (Object.keys(headers).length > 0) { + source.headers = headers; + } } return source; }, [ stream?.url, stream?.mediaSource, + stream?.requiredHttpHeaders, item?.UserData?.PlaybackPositionTicks, playbackPositionFromUrl, api?.basePath, diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index c2124720..35572718 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -12,6 +12,7 @@ interface StreamResult { url: string; sessionId: string | null; mediaSource: MediaSourceInfo | undefined; + requiredHttpHeaders?: Record; } /** @@ -50,10 +51,24 @@ const getPlaybackUrl = ( return `${api.basePath}${transcodeUrl}`; } + // Handle remote/external streams (like live TV with external URLs) + // These have Protocol "Http" and IsRemote true, with the actual URL in Path + if ( + mediaSource?.IsRemote && + mediaSource?.Protocol === "Http" && + mediaSource?.Path + ) { + console.log("Video is remote stream, using direct Path:", mediaSource.Path); + return mediaSource.Path; + } + // Fall back to direct play + // Use the mediaSource's actual container when available (important for live TV + // where the container may be ts/hls, not mp4) + const container = params.container || mediaSource?.Container || "mp4"; const streamParams = new URLSearchParams({ static: params.static || "true", - container: params.container || "mp4", + container, mediaSourceId: mediaSource?.Id || "", subtitleStreamIndex: params.subtitleStreamIndex?.toString() || "", audioStreamIndex: params.audioStreamIndex?.toString() || "", @@ -163,6 +178,7 @@ export const getStreamUrl = async ({ url: string | null; sessionId: string | null; mediaSource: MediaSourceInfo | undefined; + requiredHttpHeaders?: Record; } | null> => { if (!api || !userId || !item?.Id) { console.warn("Missing required parameters for getStreamUrl"); @@ -210,6 +226,9 @@ export const getStreamUrl = async ({ url, sessionId: sessionId || null, mediaSource, + requiredHttpHeaders: mediaSource?.RequiredHttpHeaders as + | Record + | undefined, }; } @@ -254,6 +273,9 @@ export const getStreamUrl = async ({ url, sessionId: sessionId || null, mediaSource, + requiredHttpHeaders: mediaSource?.RequiredHttpHeaders as + | Record + | undefined, }; };