mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-23 23:36:54 +01:00
wip
This commit is contained in:
44
utils/movpkg-to-vlc/parse/boot.ts
Normal file
44
utils/movpkg-to-vlc/parse/boot.ts
Normal 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;
|
||||
}
|
||||
45
utils/movpkg-to-vlc/parse/streamInfoBoot.ts
Normal file
45
utils/movpkg-to-vlc/parse/streamInfoBoot.ts
Normal 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;
|
||||
}
|
||||
116
utils/movpkg-to-vlc/tools.ts
Normal file
116
utils/movpkg-to-vlc/tools.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user