From 4644d2ab5849d2338ecc9e2a9d35df6e2715e031 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Sat, 15 Feb 2025 15:40:21 +0100 Subject: [PATCH] feat: first version of AVAssetDownloadURLSession module for ios --- modules/expo-hls-downloader/.eslintrc.js | 5 + modules/expo-hls-downloader/.gitignore | 57 + modules/expo-hls-downloader/.npmignore | 14 + .../expo-hls-downloader/example/.gitignore | 36 + modules/expo-hls-downloader/example/App.tsx | 270 +++ .../example/android/.gitignore | 16 + .../example/android/app/build.gradle | 176 ++ .../example/android/app/debug.keystore | Bin 0 -> 2257 bytes .../example/android/app/proguard-rules.pro | 14 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 32 + .../hlsdownloader/example/MainActivity.kt | 61 + .../hlsdownloader/example/MainApplication.kt | 57 + .../res/drawable-hdpi/splashscreen_logo.png | Bin 0 -> 20754 bytes .../res/drawable-mdpi/splashscreen_logo.png | Bin 0 -> 12863 bytes .../res/drawable-xhdpi/splashscreen_logo.png | Bin 0 -> 29081 bytes .../res/drawable-xxhdpi/splashscreen_logo.png | Bin 0 -> 47123 bytes .../drawable-xxxhdpi/splashscreen_logo.png | Bin 0 -> 66529 bytes .../res/drawable/ic_launcher_background.xml | 6 + .../res/drawable/rn_edit_text_material.xml | 37 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 3300 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 8031 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 4103 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 2048 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 5079 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 2613 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 4535 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 11145 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 5673 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 7345 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 18064 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 9091 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 10108 bytes .../ic_launcher_foreground.webp | Bin 0 -> 25030 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 12469 bytes .../app/src/main/res/values-night/colors.xml | 1 + .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/strings.xml | 5 + .../app/src/main/res/values/styles.xml | 17 + .../example/android/build.gradle | 41 + .../example/android/gradle.properties | 56 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../example/android/gradlew | 252 +++ .../example/android/gradlew.bat | 94 + .../example/android/settings.gradle | 38 + modules/expo-hls-downloader/example/app.json | 30 + .../example/assets/adaptive-icon.png | Bin 0 -> 17547 bytes .../example/assets/favicon.png | Bin 0 -> 1466 bytes .../example/assets/icon.png | Bin 0 -> 22380 bytes .../example/assets/splash-icon.png | Bin 0 -> 17547 bytes .../example/babel.config.js | 6 + modules/expo-hls-downloader/example/index.ts | 8 + .../example/ios/.gitignore | 30 + .../example/ios/.xcode.env | 11 + .../expo-hls-downloader/example/ios/Podfile | 66 + .../example/ios/Podfile.lock | 1921 +++++++++++++++++ .../example/ios/Podfile.properties.json | 5 + .../project.pbxproj | 538 +++++ .../expohlsdownloaderexample.xcscheme | 88 + .../contents.xcworkspacedata | 10 + .../expohlsdownloaderexample/AppDelegate.h | 7 + .../expohlsdownloaderexample/AppDelegate.mm | 62 + .../App-Icon-1024x1024@1x.png | Bin 0 -> 59468 bytes .../AppIcon.appiconset/Contents.json | 14 + .../Images.xcassets/Contents.json | 6 + .../Contents.json | 20 + .../SplashScreenLogo.imageset/Contents.json | 23 + .../SplashScreenLogo.imageset/image.png | Bin 0 -> 60870 bytes .../SplashScreenLogo.imageset/image@2x.png | Bin 0 -> 60870 bytes .../SplashScreenLogo.imageset/image@3x.png | Bin 0 -> 60870 bytes .../ios/expohlsdownloaderexample/Info.plist | 80 + .../PrivacyInfo.xcprivacy | 48 + .../SplashScreen.storyboard | 44 + .../Supporting/Expo.plist | 12 + ...expohlsdownloaderexample-Bridging-Header.h | 3 + .../expohlsdownloaderexample.entitlements | 5 + .../ios/expohlsdownloaderexample/main.m | 10 + .../expohlsdownloaderexample/noop-file.swift | 4 + .../example/metro.config.js | 34 + .../expo-hls-downloader/example/package.json | 29 + .../expo-hls-downloader/example/tsconfig.json | 10 + .../example/webpack.config.js | 20 + .../expo-module.config.json | 9 + .../ios/ExpoHlsDownloader.podspec | 29 + .../ios/ExpoHlsDownloaderModule.swift | 100 + .../ios/ExpoHlsDownloaderView.swift | 38 + modules/expo-hls-downloader/package.json | 43 + .../src/ExpoHlsDownloader.ts | 163 ++ .../src/ExpoHlsDownloader.types.ts | 24 + .../src/ExpoHlsDownloaderModule.ts | 10 + .../src/ExpoHlsDownloaderView.tsx | 11 + modules/expo-hls-downloader/src/index.ts | 10 + modules/expo-hls-downloader/tsconfig.json | 9 + 96 files changed, 4905 insertions(+) create mode 100644 modules/expo-hls-downloader/.eslintrc.js create mode 100644 modules/expo-hls-downloader/.gitignore create mode 100644 modules/expo-hls-downloader/.npmignore create mode 100644 modules/expo-hls-downloader/example/.gitignore create mode 100644 modules/expo-hls-downloader/example/App.tsx create mode 100644 modules/expo-hls-downloader/example/android/.gitignore create mode 100644 modules/expo-hls-downloader/example/android/app/build.gradle create mode 100644 modules/expo-hls-downloader/example/android/app/debug.keystore create mode 100644 modules/expo-hls-downloader/example/android/app/proguard-rules.pro create mode 100644 modules/expo-hls-downloader/example/android/app/src/debug/AndroidManifest.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/AndroidManifest.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/java/expo/modules/hlsdownloader/example/MainActivity.kt create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/java/expo/modules/hlsdownloader/example/MainApplication.kt create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/values-night/colors.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/values/colors.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/values/strings.xml create mode 100644 modules/expo-hls-downloader/example/android/app/src/main/res/values/styles.xml create mode 100644 modules/expo-hls-downloader/example/android/build.gradle create mode 100644 modules/expo-hls-downloader/example/android/gradle.properties create mode 100644 modules/expo-hls-downloader/example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 modules/expo-hls-downloader/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 modules/expo-hls-downloader/example/android/gradlew create mode 100644 modules/expo-hls-downloader/example/android/gradlew.bat create mode 100644 modules/expo-hls-downloader/example/android/settings.gradle create mode 100644 modules/expo-hls-downloader/example/app.json create mode 100644 modules/expo-hls-downloader/example/assets/adaptive-icon.png create mode 100644 modules/expo-hls-downloader/example/assets/favicon.png create mode 100644 modules/expo-hls-downloader/example/assets/icon.png create mode 100644 modules/expo-hls-downloader/example/assets/splash-icon.png create mode 100644 modules/expo-hls-downloader/example/babel.config.js create mode 100644 modules/expo-hls-downloader/example/index.ts create mode 100644 modules/expo-hls-downloader/example/ios/.gitignore create mode 100644 modules/expo-hls-downloader/example/ios/.xcode.env create mode 100644 modules/expo-hls-downloader/example/ios/Podfile create mode 100644 modules/expo-hls-downloader/example/ios/Podfile.lock create mode 100644 modules/expo-hls-downloader/example/ios/Podfile.properties.json create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample.xcodeproj/project.pbxproj create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample.xcodeproj/xcshareddata/xcschemes/expohlsdownloaderexample.xcscheme create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample.xcworkspace/contents.xcworkspacedata create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/AppDelegate.h create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/AppDelegate.mm create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/Contents.json create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/SplashScreenBackground.colorset/Contents.json create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/SplashScreenLogo.imageset/Contents.json create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/SplashScreenLogo.imageset/image.png create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/SplashScreenLogo.imageset/image@2x.png create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Images.xcassets/SplashScreenLogo.imageset/image@3x.png create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Info.plist create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/PrivacyInfo.xcprivacy create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/SplashScreen.storyboard create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/Supporting/Expo.plist create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/expohlsdownloaderexample-Bridging-Header.h create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/expohlsdownloaderexample.entitlements create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/main.m create mode 100644 modules/expo-hls-downloader/example/ios/expohlsdownloaderexample/noop-file.swift create mode 100644 modules/expo-hls-downloader/example/metro.config.js create mode 100644 modules/expo-hls-downloader/example/package.json create mode 100644 modules/expo-hls-downloader/example/tsconfig.json create mode 100644 modules/expo-hls-downloader/example/webpack.config.js create mode 100644 modules/expo-hls-downloader/expo-module.config.json create mode 100644 modules/expo-hls-downloader/ios/ExpoHlsDownloader.podspec create mode 100644 modules/expo-hls-downloader/ios/ExpoHlsDownloaderModule.swift create mode 100644 modules/expo-hls-downloader/ios/ExpoHlsDownloaderView.swift create mode 100644 modules/expo-hls-downloader/package.json create mode 100644 modules/expo-hls-downloader/src/ExpoHlsDownloader.ts create mode 100644 modules/expo-hls-downloader/src/ExpoHlsDownloader.types.ts create mode 100644 modules/expo-hls-downloader/src/ExpoHlsDownloaderModule.ts create mode 100644 modules/expo-hls-downloader/src/ExpoHlsDownloaderView.tsx create mode 100644 modules/expo-hls-downloader/src/index.ts create mode 100644 modules/expo-hls-downloader/tsconfig.json diff --git a/modules/expo-hls-downloader/.eslintrc.js b/modules/expo-hls-downloader/.eslintrc.js new file mode 100644 index 00000000..8e39d62a --- /dev/null +++ b/modules/expo-hls-downloader/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + root: true, + extends: ["universe/native", "universe/web"], + ignorePatterns: ["build"], +}; diff --git a/modules/expo-hls-downloader/.gitignore b/modules/expo-hls-downloader/.gitignore new file mode 100644 index 00000000..e64b91c9 --- /dev/null +++ b/modules/expo-hls-downloader/.gitignore @@ -0,0 +1,57 @@ +# OSX +# +.DS_Store + +# VSCode +.vscode/ +jsconfig.json + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.classpath +.cxx +.gradle +.idea +.project +.settings +local.properties +android.iml +android/app/libs +android/keystores/debug.keystore + +# Cocoapods +# +example/ios/Pods + +# Ruby +example/vendor/ + +# node.js +# +node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# Expo +.expo/* diff --git a/modules/expo-hls-downloader/.npmignore b/modules/expo-hls-downloader/.npmignore new file mode 100644 index 00000000..937d158a --- /dev/null +++ b/modules/expo-hls-downloader/.npmignore @@ -0,0 +1,14 @@ +# Exclude all top-level hidden directories by convention +/.*/ + +# Exclude tarballs generated by `npm pack` +/*.tgz + +__mocks__ +__tests__ + +/babel.config.js +/android/src/androidTest/ +/android/src/test/ +/android/build/ +/example/ diff --git a/modules/expo-hls-downloader/example/.gitignore b/modules/expo-hls-downloader/example/.gitignore new file mode 100644 index 00000000..d16e1efb --- /dev/null +++ b/modules/expo-hls-downloader/example/.gitignore @@ -0,0 +1,36 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo diff --git a/modules/expo-hls-downloader/example/App.tsx b/modules/expo-hls-downloader/example/App.tsx new file mode 100644 index 00000000..2f104caf --- /dev/null +++ b/modules/expo-hls-downloader/example/App.tsx @@ -0,0 +1,270 @@ +import * as FileSystem from "expo-file-system"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Button, Dimensions, StyleSheet, Text, View } from "react-native"; + +import { + PipStartedPayload, + PlaybackStatePayload, + ProgressUpdatePayload, + VlcPlayerViewRef, +} from "../../vlc-player/src/VlcPlayer.types"; +import VlcPlayerView from "../../vlc-player/src/VlcPlayerView"; +import { + downloadHLSAsset, + useDownloadComplete, + useDownloadError, + useDownloadProgress, +} from "../src/ExpoHlsDownloader"; + +/** + * Parses boot.xml in the root download directory to extract the stream folder name. + */ +async function getStreamFolderFromBootXml( + downloadDir: string +): Promise { + try { + const bootPath = `${downloadDir}/boot.xml`; + const bootInfo = await FileSystem.getInfoAsync(bootPath); + if (!bootInfo.exists) { + console.error("boot.xml not found in", downloadDir); + return null; + } + const xmlContent = await FileSystem.readAsStringAsync(bootPath); + const match = xmlContent.match(/]*Path="([^"]+)"/); + return match ? match[1].trim() : null; + } catch (error) { + console.error("Error reading boot.xml:", error); + return null; + } +} + +/** + * Parses the StreamInfoBoot.xml file in the stream folder and returns a mapping: + * TS filename -> local fragment filename. + */ +async function parseStreamInfoMapping( + downloadDir: string, + streamFolder: string +): Promise> { + const mapping: Record = {}; + try { + const streamInfoPath = `${downloadDir}/${streamFolder}/StreamInfoBoot.xml`; + const info = await FileSystem.getInfoAsync(streamInfoPath); + if (!info.exists) { + console.error("StreamInfoBoot.xml not found in", streamFolder); + return mapping; + } + const xmlContent = await FileSystem.readAsStringAsync(streamInfoPath); + // Use a regex to match all elements and capture PATH and URL attributes. + const segRegex = /]*PATH="([^"]+)"\s+[^>]*URL="([^"]+)"[^>]*>/g; + let match: RegExpExecArray | null; + while ((match = segRegex.exec(xmlContent)) !== null) { + const fragName = match[1].trim(); + const urlAttr = match[2].trim(); + // Extract the TS filename from the URL (assumes it's the last path component) + const tsFilename = urlAttr.substring(urlAttr.lastIndexOf("/") + 1); + mapping[tsFilename] = fragName; + } + } catch (error) { + console.error("Error parsing StreamInfoBoot.xml:", error); + } + return mapping; +} + +/** + * Reads the master playlist, replaces TS filenames with local fragment paths from the mapping, + * and writes the modified playlist to a new file. + * @param masterUri Absolute path to the original master playlist. + * @param baseDir The stream folder directory. + * @param mapping Mapping from TS filename to fragment filename. + * @returns The new file URI for the modified playlist. + */ +async function rewriteMasterPlaylist( + masterUri: string, + baseDir: string, + mapping: Record +): Promise { + try { + const content = await FileSystem.readAsStringAsync(masterUri); + const lines = content.split("\n"); + const modifiedLines = lines.map((line) => { + // Only modify lines that look like segment references (not comments) + if (!line.startsWith("#") && line.trim().endsWith(".ts")) { + const tsFilename = line.trim(); + if (mapping[tsFilename]) { + // Replace with absolute path to the frag file. + return baseDir + "/" + mapping[tsFilename]; + } + // Fallback: use the TS filename with baseDir prefix. + return baseDir + "/" + tsFilename; + } + return line; + }); + const newContent = modifiedLines.join("\n"); + const newUri = masterUri.replace(/\.m3u8$/, "_modified.m3u8"); + await FileSystem.writeAsStringAsync(newUri, newContent); + console.log("Rewritten master playlist saved to:", newUri); + return newUri; + } catch (error) { + console.error("Error rewriting master playlist:", error); + throw error; + } +} + +/** + * Reads the root boot.xml to get the stream folder, then reads StreamInfoBoot.xml to extract + * the master playlist filename from master.m3u8 or from the MediaPlaylist. + * For simplicity, here we assume that the master playlist file is in the stream folder. + */ +async function getMasterPlaylistUri( + downloadDir: string, + streamFolder: string +): Promise { + try { + // Look in the stream folder for a file ending with ".m3u8" + const folderPath = `${downloadDir}/${streamFolder}`; + const items = await FileSystem.readDirectoryAsync(folderPath); + const masterFile = items.find((f) => f.toLowerCase().endsWith(".m3u8")); + if (!masterFile) { + console.error("No master playlist found in", folderPath); + return null; + } + return folderPath + "/" + masterFile; + } catch (error) { + console.error("Error reading master playlist:", error); + return null; + } +} + +export default function App() { + const progress = useDownloadProgress(); + const error = useDownloadError(); + const downloadLocation = useDownloadComplete("video-dir"); + const [playbackUrl, setPlaybackUrl] = useState(null); + const { width } = Dimensions.get("window"); + + useEffect(() => { + async function preparePlayback() { + if (!downloadLocation) return; + console.log("Download folder:", downloadLocation); + + // 1. Read boot.xml to get the stream folder. + const streamFolder = await getStreamFolderFromBootXml(downloadLocation); + if (!streamFolder) { + console.error("Stream folder not found in boot.xml"); + return; + } + console.log("Stream folder:", streamFolder); + + // 2. Parse StreamInfoBoot.xml to build mapping TS -> frag. + const mapping = await parseStreamInfoMapping( + downloadLocation, + streamFolder + ); + console.log("Mapping:", mapping); + + // 3. Get the master playlist file URI from the stream folder. + const masterUri = await getMasterPlaylistUri( + downloadLocation, + streamFolder + ); + if (!masterUri) { + console.error("Master playlist not found."); + return; + } + console.log("Master playlist found at:", masterUri); + + // 4. Rewrite the master playlist using the mapping. + const baseDir = `${downloadLocation}/${streamFolder}`; + const modifiedMasterUri = await rewriteMasterPlaylist( + masterUri, + baseDir, + mapping + ); + + console.log("setPlaybackUrl: ", modifiedMasterUri); + setPlaybackUrl(modifiedMasterUri); + } + preparePlayback(); + }, [downloadLocation]); + + const handleDownload = () => { + // Start the HLS download with a sample URL and asset title. + downloadHLSAsset( + "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8", + // "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8", + // "https://fredflix.se/videos/48129b3e-eb6c-5b35-a3f0-5aa4519f20e9/master.m3u8?DeviceId=TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEyNS4wLjAuMCBTYWZhcmkvNTM3LjM2fDE3MTc2MjEwNjI2MDE1&MediaSourceId=48129b3eeb6c5b35a3f05aa4519f20e9&VideoCodec=av1,hevc,h264,vp9&AudioCodec=aac,opus,flac&AudioStreamIndex=1&VideoBitrate=226308506&AudioBitrate=191999&MaxFramerate=23.976025&PlaySessionId=110e05ff016a4778864a5e736b234ef9&api_key=9a506def548b4684a74a5ab410604de2&SubtitleMethod=Encode&TranscodingMaxAudioChannels=2&RequireAvc=false&EnableAudioVbrEncoding=true&Tag=40a3a2b4cb21057a5e1ea5f018b0c189&SegmentContainer=mp4&MinSegments=2&BreakOnNonKeyFrames=True&hevc-level=120&hevc-videobitdepth=10&hevc-profile=main10&av1-profile=main&av1-rangetype=SDR,HDR10,HLG&av1-level=19&vp9-rangetype=SDR,HDR10,HLG&hevc-rangetype=SDR,HDR10,HLG&hevc-deinterlace=true&h264-profile=high,main,baseline,constrainedbaseline,high10&h264-rangetype=SDR&h264-level=52&h264-deinterlace=true&TranscodeReasons=ContainerNotSupported,%20AudioChannelsNotSupported", + "MyHLSAsset" + ); + }; + + const downloading = useMemo(() => progress > 0 && progress < 1, [progress]); + const videoRef = useRef(null); + + return ( + + HLS Downloader Example +