forked from Ninjalama/streamyfin_mirror
wip
This commit is contained in:
@@ -45,6 +45,10 @@ import {
|
||||
useDownloadProgress,
|
||||
useDownloadError,
|
||||
useDownloadComplete,
|
||||
addCompleteListener,
|
||||
addErrorListener,
|
||||
addProgressListener,
|
||||
checkForExistingDownloads,
|
||||
} from "@/modules/hls-downloader";
|
||||
|
||||
interface NativeDownloadButton extends ViewProps {
|
||||
@@ -56,8 +60,7 @@ interface NativeDownloadButton extends ViewProps {
|
||||
|
||||
type DownloadState = {
|
||||
id: string;
|
||||
bytesDownloaded: number;
|
||||
bytesTotal: number;
|
||||
progress: number;
|
||||
state: DownloadTaskState;
|
||||
metadata?: {};
|
||||
};
|
||||
@@ -111,10 +114,6 @@ export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||
bottomSheetModalRef.current?.dismiss();
|
||||
}, []);
|
||||
|
||||
const progress = useDownloadProgress();
|
||||
const complete = useDownloadComplete("download");
|
||||
const downloadError = useDownloadError();
|
||||
|
||||
const acceptDownloadOptions = useCallback(async () => {
|
||||
if (userCanDownload === true) {
|
||||
closeModal();
|
||||
@@ -146,6 +145,7 @@ export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||
if (res.url.includes("master.m3u8")) {
|
||||
// TODO: Download with custom native module
|
||||
downloadHLSAsset(
|
||||
item.Id!,
|
||||
res.url,
|
||||
`${FileSystem.documentDirectory}${item.Name}.mkv`
|
||||
);
|
||||
@@ -166,8 +166,7 @@ export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||
toast.success("Download started");
|
||||
setActiveDownload({
|
||||
id: jobId,
|
||||
bytesDownloaded: 0,
|
||||
bytesTotal: expectedBytes,
|
||||
progress: 0,
|
||||
state: "DOWNLOADING",
|
||||
});
|
||||
})
|
||||
@@ -202,17 +201,55 @@ export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(progress);
|
||||
}, [progress]);
|
||||
const progressListener = addProgressListener((item) => {
|
||||
console.log("progress ~", item);
|
||||
setActiveDownload({
|
||||
id: activeDownload?.id!,
|
||||
progress: item.progress,
|
||||
state: "DOWNLOADING",
|
||||
});
|
||||
});
|
||||
|
||||
checkForExistingDownloads().then((downloads) => {
|
||||
console.log(
|
||||
"AVAssetDownloadURLSession ~ checkForExistingDownloads ~",
|
||||
downloads
|
||||
);
|
||||
const firstDownload = downloads?.[0];
|
||||
if (!download) return;
|
||||
|
||||
setActiveDownload({
|
||||
id: firstDownload?.id,
|
||||
progress: firstDownload?.progress,
|
||||
state: firstDownload?.state,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
progressListener.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log(progress);
|
||||
|
||||
// // setActiveDownload({
|
||||
// // id: activeDownload?.id!,
|
||||
// // progress,
|
||||
// // state: "DOWNLOADING",
|
||||
// // });
|
||||
// }, [progress]);
|
||||
|
||||
useEffect(() => {
|
||||
RNBackgroundDownloader.checkForExistingDownloads().then((downloads) => {
|
||||
console.log("checkForExistingDownloads ~", downloads);
|
||||
console.log(
|
||||
"RNBackgroundDownloader ~ checkForExistingDownloads ~",
|
||||
downloads
|
||||
);
|
||||
const e = downloads?.[0];
|
||||
setActiveDownload({
|
||||
id: e?.id,
|
||||
bytesDownloaded: e?.bytesDownloaded,
|
||||
bytesTotal: e?.bytesTotal,
|
||||
progress: e?.bytesDownloaded / e?.bytesTotal,
|
||||
state: e?.state,
|
||||
});
|
||||
|
||||
@@ -220,8 +257,7 @@ export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||
console.log(`Downloaded: ${bytesDownloaded} of ${bytesTotal}`);
|
||||
setActiveDownload({
|
||||
id: e?.id,
|
||||
bytesDownloaded,
|
||||
bytesTotal,
|
||||
progress: bytesDownloaded / bytesTotal,
|
||||
state: e?.state,
|
||||
});
|
||||
});
|
||||
@@ -271,14 +307,10 @@ export const NativeDownloadButton: React.FC<NativeDownloadButton> = ({
|
||||
size={size}
|
||||
onPress={onButtonPress}
|
||||
>
|
||||
{activeDownload &&
|
||||
activeDownload?.bytesTotal > 0 &&
|
||||
activeDownload?.bytesDownloaded > 0 ? (
|
||||
{activeDownload && activeDownload?.progress > 0 ? (
|
||||
<ProgressCircle
|
||||
size={24}
|
||||
fill={
|
||||
(activeDownload.bytesDownloaded / activeDownload.bytesTotal) * 100
|
||||
}
|
||||
fill={activeDownload.progress * 100}
|
||||
width={4}
|
||||
tintColor="#9334E9"
|
||||
backgroundColor="#bdc3c7"
|
||||
|
||||
@@ -11,11 +11,29 @@ import HlsDownloaderModule from "./src/HlsDownloaderModule";
|
||||
|
||||
/**
|
||||
* Initiates an HLS download.
|
||||
* @param id - A unique identifier for the download.
|
||||
* @param url - The HLS stream URL.
|
||||
* @param assetTitle - A title for the asset.
|
||||
*/
|
||||
function downloadHLSAsset(url: string, assetTitle: string): void {
|
||||
HlsDownloaderModule.downloadHLSAsset(url, assetTitle);
|
||||
function downloadHLSAsset(id: string, url: string, assetTitle: string): void {
|
||||
HlsDownloaderModule.downloadHLSAsset(id, url, assetTitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for existing downloads.
|
||||
* Returns an array of downloads with additional fields:
|
||||
* id, progress, bytesDownloaded, bytesTotal, and state.
|
||||
*/
|
||||
async function checkForExistingDownloads(): Promise<
|
||||
Array<{
|
||||
id: string;
|
||||
progress: number;
|
||||
bytesDownloaded: number;
|
||||
bytesTotal: number;
|
||||
state: "PENDING" | "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
|
||||
}>
|
||||
> {
|
||||
return HlsDownloaderModule.checkForExistingDownloads();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,7 +136,7 @@ function useDownloadComplete(destinationFileName?: string): string | null {
|
||||
console.log("Setting up download complete listener");
|
||||
|
||||
const subscription = addCompleteListener(
|
||||
async (event: { location: string }) => {
|
||||
async (event: OnCompleteEventPayload) => {
|
||||
console.log("Download complete event received:", event);
|
||||
console.log("Original download location:", event.location);
|
||||
|
||||
@@ -162,7 +180,12 @@ function useDownloadComplete(destinationFileName?: string): string | null {
|
||||
|
||||
export {
|
||||
downloadHLSAsset,
|
||||
checkForExistingDownloads,
|
||||
useDownloadComplete,
|
||||
useDownloadError,
|
||||
useDownloadProgress,
|
||||
addCompleteListener,
|
||||
addErrorListener,
|
||||
addProgressListener,
|
||||
HlsDownloaderModule,
|
||||
};
|
||||
|
||||
@@ -1,39 +1,31 @@
|
||||
// ios/HlsDownloaderModule.swift
|
||||
import ExpoModulesCore
|
||||
import AVFoundation
|
||||
|
||||
public class HlsDownloaderModule: Module {
|
||||
// Optional: Keep a strong reference to the delegate (for the current download)
|
||||
private var currentDelegate: HLSDownloadDelegate?
|
||||
var activeDownloads: [Int: (task: AVAssetDownloadTask, delegate: HLSDownloadDelegate)] = [:]
|
||||
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("HlsDownloader")
|
||||
|
||||
// Declare the events you wish to expose.
|
||||
Events("onProgress", "onError", "onComplete")
|
||||
|
||||
// Expose the download function.
|
||||
Function("downloadHLSAsset") { (url: String, assetTitle: String) -> Void in
|
||||
print("[HlsDownloaderModule] downloadHLSAsset called with url: \(url) and assetTitle: \(assetTitle)")
|
||||
|
||||
Function("downloadHLSAsset") { (providedId: String, url: String, assetTitle: String) -> Void in
|
||||
guard let assetURL = URL(string: url) else {
|
||||
print("[HlsDownloaderModule] Invalid URL: \(url)")
|
||||
self.sendEvent("onError", ["error": "Invalid URL"])
|
||||
self.sendEvent("onError", ["id": providedId, "error": "Invalid URL", "state": "FAILED"])
|
||||
return
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: assetURL)
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.hlsdownload.\(UUID().uuidString)")
|
||||
print("[HlsDownloaderModule] Created background session configuration")
|
||||
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.hlsdownload")
|
||||
let delegate = HLSDownloadDelegate(module: self)
|
||||
self.currentDelegate = delegate
|
||||
delegate.providedId = providedId
|
||||
|
||||
let downloadSession = AVAssetDownloadURLSession(
|
||||
configuration: configuration,
|
||||
assetDownloadDelegate: delegate,
|
||||
delegateQueue: OperationQueue.main
|
||||
)
|
||||
print("[HlsDownloaderModule] Created download session")
|
||||
|
||||
guard let task = downloadSession.makeAssetDownloadTask(
|
||||
asset: asset,
|
||||
@@ -41,32 +33,68 @@ public class HlsDownloaderModule: Module {
|
||||
assetArtworkData: nil,
|
||||
options: nil
|
||||
) else {
|
||||
print("[HlsDownloaderModule] Failed to create download task")
|
||||
self.sendEvent("onError", ["error": "Failed to create download task"])
|
||||
self.sendEvent("onError", ["id": providedId, "error": "Failed to create download task", "state": "FAILED"])
|
||||
return
|
||||
}
|
||||
|
||||
print("[HlsDownloaderModule] Starting download task for asset: \(assetTitle)")
|
||||
delegate.taskIdentifier = task.taskIdentifier
|
||||
self.activeDownloads[task.taskIdentifier] = (task, delegate)
|
||||
|
||||
self.sendEvent("onProgress", [
|
||||
"id": providedId,
|
||||
"progress": 0.0,
|
||||
"state": "PENDING"
|
||||
])
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
// Called when JavaScript starts observing events.
|
||||
OnStartObserving {
|
||||
print("[HlsDownloaderModule] Started observing events")
|
||||
// Additional setup if needed.
|
||||
Function("checkForExistingDownloads") {
|
||||
() -> [[String: Any]] in
|
||||
var downloads: [[String: Any]] = []
|
||||
for (id, pair) in self.activeDownloads {
|
||||
let task = pair.task
|
||||
let delegate = pair.delegate
|
||||
let downloaded = delegate.downloadedSeconds
|
||||
let total = delegate.totalSeconds
|
||||
let progress = total > 0 ? downloaded / total : 0
|
||||
downloads.append([
|
||||
"id": delegate.providedId.isEmpty ? String(id) : delegate.providedId,
|
||||
"progress": progress,
|
||||
"bytesDownloaded": downloaded,
|
||||
"bytesTotal": total,
|
||||
"state": self.mappedState(for: task)
|
||||
])
|
||||
}
|
||||
return downloads
|
||||
}
|
||||
|
||||
// Called when JavaScript stops observing events.
|
||||
OnStopObserving {
|
||||
print("[HlsDownloaderModule] Stopped observing events")
|
||||
// Clean up if necessary.
|
||||
OnStartObserving { }
|
||||
OnStopObserving { }
|
||||
}
|
||||
|
||||
func removeDownload(with id: Int) {
|
||||
activeDownloads.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
func mappedState(for task: URLSessionTask, errorOccurred: Bool = false) -> String {
|
||||
if errorOccurred { return "FAILED" }
|
||||
switch task.state {
|
||||
case .running: return "DOWNLOADING"
|
||||
case .suspended: return "PAUSED"
|
||||
case .completed: return "DONE"
|
||||
case .canceling: return "STOPPED"
|
||||
@unknown default: return "PENDING"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate that listens to AVAssetDownloadURLSession events and emits them to JS.
|
||||
class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
||||
weak var module: HlsDownloaderModule?
|
||||
var taskIdentifier: Int = 0
|
||||
var providedId: String = ""
|
||||
var downloadedSeconds: Double = 0
|
||||
var totalSeconds: Double = 0
|
||||
|
||||
init(module: HlsDownloaderModule) {
|
||||
self.module = module
|
||||
@@ -76,25 +104,45 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
||||
didLoad timeRange: CMTimeRange,
|
||||
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
|
||||
timeRangeExpectedToLoad: CMTimeRange) {
|
||||
let loadedSeconds = loadedTimeRanges.reduce(0.0) { result, value in
|
||||
result + CMTimeGetSeconds(value.timeRangeValue.duration)
|
||||
var loadedSeconds = 0.0
|
||||
for value in loadedTimeRanges {
|
||||
loadedSeconds += CMTimeGetSeconds(value.timeRangeValue.duration)
|
||||
}
|
||||
let totalSeconds = CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
|
||||
let progress = totalSeconds > 0 ? loadedSeconds / totalSeconds : 0
|
||||
print("[HLSDownloadDelegate] Progress: \(progress * 100)%")
|
||||
module?.sendEvent("onProgress", ["progress": progress])
|
||||
let total = CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
|
||||
downloadedSeconds = loadedSeconds
|
||||
totalSeconds = total
|
||||
let progress = total > 0 ? loadedSeconds / total : 0
|
||||
let state = module?.mappedState(for: assetDownloadTask) ?? "PENDING"
|
||||
|
||||
module?.sendEvent("onProgress", [
|
||||
"id": providedId,
|
||||
"progress": progress,
|
||||
"bytesDownloaded": loadedSeconds,
|
||||
"bytesTotal": total,
|
||||
"state": state
|
||||
])
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if let error = error {
|
||||
print("[HLSDownloadDelegate] Error: \(error.localizedDescription)")
|
||||
module?.sendEvent("onError", ["error": error.localizedDescription])
|
||||
let state = module?.mappedState(for: task, errorOccurred: true) ?? "FAILED"
|
||||
module?.sendEvent("onError", [
|
||||
"id": providedId,
|
||||
"error": error.localizedDescription,
|
||||
"state": state
|
||||
])
|
||||
}
|
||||
module?.removeDownload(with: task.taskIdentifier)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
||||
didFinishDownloadingTo location: URL) {
|
||||
print("[HLSDownloadDelegate] Download complete: \(location.absoluteString)")
|
||||
module?.sendEvent("onComplete", ["location": location.absoluteString])
|
||||
let state = module?.mappedState(for: assetDownloadTask) ?? "DONE"
|
||||
module?.sendEvent("onComplete", [
|
||||
"id": providedId,
|
||||
"location": location.absoluteString,
|
||||
"state": state
|
||||
])
|
||||
module?.removeDownload(with: assetDownloadTask.taskIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user