From df7144ede9617a9ba6bf7095913f6353f92cb9bc Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 15 Feb 2025 23:26:28 +0100 Subject: [PATCH] wip --- bun.lock | 1 + package.json | 1 + providers/NativeDownloadProvider.tsx | 3 + utils/hls/av-file-parser.ts | 104 +++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 utils/hls/av-file-parser.ts diff --git a/bun.lock b/bun.lock index ed72b3a9..448a563e 100644 --- a/bun.lock +++ b/bun.lock @@ -54,6 +54,7 @@ "expo-task-manager": "~12.0.5", "expo-updates": "~0.26.17", "expo-web-browser": "~14.0.2", + "fast-xml-parser": "^4.5.1", "ffmpeg-kit-react-native": "^6.0.2", "i18next": "^24.2.2", "jotai": "^2.11.3", diff --git a/package.json b/package.json index 8618924c..bedc47dd 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "expo-task-manager": "~12.0.5", "expo-updates": "~0.26.17", "expo-web-browser": "~14.0.2", + "fast-xml-parser": "^4.5.1", "ffmpeg-kit-react-native": "^6.0.2", "i18next": "^24.2.2", "jotai": "^2.11.3", diff --git a/providers/NativeDownloadProvider.tsx b/providers/NativeDownloadProvider.tsx index 179545a9..b2c05f58 100644 --- a/providers/NativeDownloadProvider.tsx +++ b/providers/NativeDownloadProvider.tsx @@ -12,6 +12,7 @@ import { } from "@/modules/hls-downloader"; import * as FileSystem from "expo-file-system"; import { DownloadInfo } from "@/modules/hls-downloader/src/HlsDownloader.types"; +import { processStream } from "@/utils/hls/av-file-parser"; type DownloadContextType = { downloads: Record; @@ -124,6 +125,8 @@ export const NativeDownloadProvider: React.FC<{ payload.metadata.Name ); console.log("File successfully persisted to:", newLocation); + + processStream(newLocation); } else { console.log( "No filename in metadata, using original location", diff --git a/utils/hls/av-file-parser.ts b/utils/hls/av-file-parser.ts new file mode 100644 index 00000000..02d56761 --- /dev/null +++ b/utils/hls/av-file-parser.ts @@ -0,0 +1,104 @@ +import * as FileSystem from "expo-file-system"; +import { XMLParser } from "fast-xml-parser"; + +const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: "@_", +}); + +export interface StreamDefinition { + id: string; + networkURL: string; + path: string; + complete: boolean; +} + +export interface StreamInfo { + mediaPlaylistURL: string; + localM3U8: string; + segPaths: string[]; +} + +// 1. Parse boot.xml to extract stream definitions. +export function parseBootXML(xml: string): StreamDefinition[] { + const json = parser.parse(xml); + const pkg = json.HLSMoviePackage; + const streams = pkg.Streams.Stream; + const streamArray: any[] = Array.isArray(streams) ? streams : [streams]; + return streamArray.map((s) => ({ + id: s["@_ID"], + networkURL: s["@_NetworkURL"], + path: s["@_Path"], + complete: s.Complete.trim() === "YES", + })); +} + +// 2. Parse StreamInfoBoot.xml to extract the local m3u8 path and segment paths. +export function parseStreamInfo(xml: string): StreamInfo { + const json = parser.parse(xml); + const streamInfo = json.StreamInfo; + const mediaPlaylist = streamInfo.MediaPlaylist; + const networkURL = mediaPlaylist.NetworkURL; + const pathToLocalCopy = mediaPlaylist.PathToLocalCopy; + let segs = mediaPlaylist?.MediaSegments?.SEG || []; + if (!Array.isArray(segs)) segs = [segs]; + const segPaths = segs + .map((seg: any) => seg["@_PATH"]) + .filter((p: string) => !!p); + return { + mediaPlaylistURL: + (typeof networkURL === "string" ? networkURL : networkURL?.trim()) || "", + localM3U8: + (typeof pathToLocalCopy === "string" + ? pathToLocalCopy + : pathToLocalCopy?.trim()) || "", + segPaths, + }; +} + +// 3. Update the m3u8 playlist content by replacing remote segment URLs with local paths. +export function updatePlaylistWithLocalSegments( + playlistContent: string, + segPaths: string[] +): string { + const lines = playlistContent.split("\n"); + let segIndex = 0; + for (let i = 0; i < lines.length; i++) { + // Replace non-comment lines (assumed to be segment URIs) with local paths. + if ( + lines[i].trim() && + !lines[i].startsWith("#") && + segIndex < segPaths.length + ) { + lines[i] = segPaths[segIndex++]; + } + } + return lines.join("\n"); +} + +// Example: Process a stream directory using Expo FileSystem. +export async function processStream(streamDir: string): Promise { + // Read StreamInfoBoot.xml from the stream directory. + const streamInfoPath = `${streamDir}/StreamInfoBoot.xml`; + const streamInfoXML = await FileSystem.readAsStringAsync(streamInfoPath, { + encoding: FileSystem.EncodingType.UTF8, + }); + const streamInfo = parseStreamInfo(streamInfoXML); + + // Read the local m3u8 file. + const playlistPath = `${streamDir}/${streamInfo.localM3U8}`; + const playlistContent = await FileSystem.readAsStringAsync(playlistPath, { + encoding: FileSystem.EncodingType.UTF8, + }); + + // Replace remote segment URIs with local segment paths. + const updatedPlaylist = updatePlaylistWithLocalSegments( + playlistContent, + streamInfo.segPaths + ); + + // Save the updated playlist back to disk. + await FileSystem.writeAsStringAsync(playlistPath, updatedPlaylist, { + encoding: FileSystem.EncodingType.UTF8, + }); +}