This commit is contained in:
Fredrik Burmester
2025-02-16 16:01:49 +01:00
parent 696543d1b2
commit 1a2e044da6
31 changed files with 639 additions and 3062 deletions

View File

@@ -0,0 +1,44 @@
import { XMLParser } from "fast-xml-parser";
export interface Boot {
Version: string;
HLSMoviePackageType: string;
Streams: {
Stream: Stream[];
};
MasterPlaylist: {
NetworkURL: string;
};
DataItems: {
Directory: string;
DataItem: DataItem;
};
}
export interface Stream {
ID: string;
NetworkURL: string;
Path: string;
Complete: string; // "YES" or "NO"
}
export interface DataItem {
ID: string;
Category: string;
Name: string;
DescriptorPath: string;
DataPath: string;
Role: string;
}
export async function parseBootXML(xml: string): Promise<Boot> {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "",
parseAttributeValue: true,
});
const jsonObj = parser.parse(xml);
const b = jsonObj.HLSMoviePackage as Boot;
console.log(b.Streams);
return jsonObj.HLSMoviePackage as Boot;
}

View File

@@ -0,0 +1,45 @@
import { XMLParser } from "fast-xml-parser";
export interface StreamInfo {
Version: string;
Complete: string;
PeakBandwidth: number;
Compressable: string;
MediaPlaylist: MediaPlaylist;
Type: string;
MediaSegments: {
SEG: SEG[];
};
EvictionPolicy: string;
MediaBytesStored: number;
}
export interface MediaPlaylist {
NetworkURL: string;
PathToLocalCopy: string;
}
export interface SEG {
Dur: number;
Len: number;
Off: number;
PATH: string;
SeqNum: number;
Tim: number;
URL: string;
}
export async function parseStreamInfoXml(xml: string): Promise<StreamInfo> {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "",
parseAttributeValue: true,
isArray: (tagName, jPath) => {
// Force SEG elements to always be an array
if (jPath === "StreamInfo.MediaSegments.SEG") return true;
return false;
},
});
const jsonObj = parser.parse(xml);
return jsonObj.StreamInfo as StreamInfo;
}

View File

@@ -0,0 +1,116 @@
import * as FileSystem from "expo-file-system";
import { parseBootXML } from "./parse/boot";
import { parseStreamInfoXml, StreamInfo } from "./parse/streamInfoBoot";
export async function rewriteM3U8Files(baseDir: string): Promise<void> {
const bootData = await loadBootData(baseDir);
if (!bootData) return;
const localPlaylistPaths = await processAllStreams(baseDir, bootData);
await updateMasterPlaylist(
`${baseDir}/Data/${bootData.DataItems.DataItem.DataPath}`,
localPlaylistPaths
);
}
async function loadBootData(baseDir: string): Promise<any | null> {
const bootPath = `${baseDir}/boot.xml`;
try {
const bootInfo = await FileSystem.getInfoAsync(bootPath);
if (!bootInfo.exists) throw new Error("boot.xml not found");
const bootXML = await FileSystem.readAsStringAsync(bootPath);
return parseBootXML(bootXML);
} catch (error) {
console.error(`Failed to load boot.xml from ${baseDir}:`, error);
return null;
}
}
async function processAllStreams(
baseDir: string,
bootData: any
): Promise<string[]> {
const localPaths: string[] = [];
for (const stream of bootData.Streams.Stream) {
const streamDir = `${baseDir}/${stream.ID}`;
try {
const streamInfo = await processStream(streamDir);
if (streamInfo && streamInfo.MediaPlaylist.PathToLocalCopy) {
localPaths.push(
`${streamDir}/${streamInfo.MediaPlaylist.PathToLocalCopy}`
);
}
} catch (error) {
console.error(`Skipping stream ${stream.ID} due to error:`, error);
}
}
return localPaths;
}
async function updateMasterPlaylist(
masterPath: string,
localPlaylistPaths: string[]
): Promise<void> {
try {
const masterContent = await FileSystem.readAsStringAsync(masterPath);
const updatedContent = updatePlaylistWithLocalSegments(
masterContent,
localPlaylistPaths
);
await FileSystem.writeAsStringAsync(masterPath, updatedContent);
} catch (error) {
console.error(`Error updating master playlist at ${masterPath}:`, error);
throw error;
}
}
export function updatePlaylistWithLocalSegments(
content: string,
localPaths: string[]
): string {
const lines = content.split("\n");
let index = 0;
for (let i = 0; i < lines.length && index < localPaths.length; i++) {
if (lines[i].trim() && !lines[i].startsWith("#")) {
lines[i] = localPaths[index++];
}
}
return lines.join("\n");
}
export async function processStream(
streamDir: string
): Promise<StreamInfo | null> {
const streamInfoPath = `${streamDir}/StreamInfoBoot.xml`;
try {
const streamXML = await FileSystem.readAsStringAsync(streamInfoPath);
const streamInfo = await parseStreamInfoXml(streamXML);
const localM3u8RelPath = streamInfo.MediaPlaylist?.PathToLocalCopy;
if (!localM3u8RelPath) {
console.warn(`No local m3u8 specified in ${streamDir}; skipping.`);
return null;
}
const m3u8Path = `${streamDir}/${localM3u8RelPath}`;
const m3u8Content = await FileSystem.readAsStringAsync(m3u8Path);
const localSegmentPaths = streamInfo.MediaSegments.SEG.map(
(seg) => `${streamDir}/${seg.PATH}`
);
const updatedContent = updatePlaylistWithLocalSegments(
m3u8Content,
localSegmentPaths
);
await FileSystem.writeAsStringAsync(m3u8Path, updatedContent);
return streamInfo;
} catch (error) {
console.error(`Error processing stream at ${streamDir}:`, error);
throw error;
}
}