mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-20 00:06:32 +00:00
wip
This commit is contained in:
@@ -3,6 +3,7 @@ import { type EventSubscription } from "expo-modules-core";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type {
|
||||
DownloadMetadata,
|
||||
OnCompleteEventPayload,
|
||||
OnErrorEventPayload,
|
||||
OnProgressEventPayload,
|
||||
@@ -14,9 +15,15 @@ import HlsDownloaderModule from "./src/HlsDownloaderModule";
|
||||
* @param id - A unique identifier for the download.
|
||||
* @param url - The HLS stream URL.
|
||||
* @param assetTitle - A title for the asset.
|
||||
* @param destination - The destination path for the downloaded asset.
|
||||
*/
|
||||
function downloadHLSAsset(id: string, url: string, assetTitle: string): void {
|
||||
HlsDownloaderModule.downloadHLSAsset(id, url, assetTitle);
|
||||
function downloadHLSAsset(
|
||||
id: string,
|
||||
url: string,
|
||||
assetTitle: string,
|
||||
metadata: DownloadMetadata
|
||||
): void {
|
||||
HlsDownloaderModule.downloadHLSAsset(id, url, assetTitle, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,63 +1,72 @@
|
||||
// ios/HlsDownloaderModule.swift
|
||||
import ExpoModulesCore
|
||||
import AVFoundation
|
||||
|
||||
public class HlsDownloaderModule: Module {
|
||||
var activeDownloads: [Int: (task: AVAssetDownloadTask, delegate: HLSDownloadDelegate)] = [:]
|
||||
var activeDownloads: [Int: (task: AVAssetDownloadTask, delegate: HLSDownloadDelegate, metadata: [String: Any])] = [:]
|
||||
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("HlsDownloader")
|
||||
|
||||
|
||||
Events("onProgress", "onError", "onComplete")
|
||||
|
||||
Function("downloadHLSAsset") { (providedId: String, url: String, assetTitle: String) -> Void in
|
||||
print("Starting download - ID: \(providedId), URL: \(url), Title: \(assetTitle)")
|
||||
|
||||
Function("downloadHLSAsset") { (providedId: String, url: String, assetTitle: String, metadata: [String: Any]?) -> Void in
|
||||
print("Starting download - ID: \(providedId), URL: \(url), Title: \(assetTitle), Metadata: \(String(describing: metadata))")
|
||||
|
||||
guard let assetURL = URL(string: url) else {
|
||||
self.sendEvent("onError", ["id": providedId, "error": "Invalid URL", "state": "FAILED"])
|
||||
self.sendEvent("onError", [
|
||||
"id": providedId,
|
||||
"error": "Invalid URL",
|
||||
"state": "FAILED",
|
||||
"metadata": metadata ?? [:]
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let asset = AVURLAsset(url: assetURL)
|
||||
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.hlsdownload")
|
||||
let delegate = HLSDownloadDelegate(module: self)
|
||||
delegate.providedId = providedId
|
||||
|
||||
let downloadSession = AVAssetDownloadURLSession(
|
||||
configuration: configuration,
|
||||
assetDownloadDelegate: delegate,
|
||||
delegateQueue: OperationQueue.main
|
||||
)
|
||||
|
||||
|
||||
guard let task = downloadSession.makeAssetDownloadTask(
|
||||
asset: asset,
|
||||
assetTitle: assetTitle,
|
||||
assetArtworkData: nil,
|
||||
options: nil
|
||||
) else {
|
||||
self.sendEvent("onError", ["id": providedId, "error": "Failed to create download task", "state": "FAILED"])
|
||||
self.sendEvent("onError", [
|
||||
"id": providedId,
|
||||
"error": "Failed to create download task",
|
||||
"state": "FAILED",
|
||||
"metadata": metadata ?? [:]
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delegate.taskIdentifier = task.taskIdentifier
|
||||
self.activeDownloads[task.taskIdentifier] = (task, delegate)
|
||||
|
||||
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:])
|
||||
self.sendEvent("onProgress", [
|
||||
"id": providedId,
|
||||
"id": providedId,
|
||||
"progress": 0.0,
|
||||
"state": "PENDING"
|
||||
])
|
||||
|
||||
"state": "PENDING",
|
||||
"metadata": metadata ?? [:]
|
||||
])
|
||||
|
||||
task.resume()
|
||||
print("Download task started with identifier: \(task.taskIdentifier)")
|
||||
}
|
||||
|
||||
|
||||
Function("checkForExistingDownloads") {
|
||||
() -> [[String: Any]] in
|
||||
var downloads: [[String: Any]] = []
|
||||
for (id, pair) in self.activeDownloads {
|
||||
let task = pair.task
|
||||
let delegate = pair.delegate
|
||||
let metadata = pair.metadata
|
||||
let downloaded = delegate.downloadedSeconds
|
||||
let total = delegate.totalSeconds
|
||||
let progress = total > 0 ? downloaded / total : 0
|
||||
@@ -66,20 +75,21 @@ public class HlsDownloaderModule: Module {
|
||||
"progress": progress,
|
||||
"bytesDownloaded": downloaded,
|
||||
"bytesTotal": total,
|
||||
"state": self.mappedState(for: task)
|
||||
])
|
||||
}
|
||||
"state": self.mappedState(for: task),
|
||||
"metadata": metadata
|
||||
])
|
||||
}
|
||||
return downloads
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
@@ -98,54 +108,55 @@ class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
||||
var providedId: String = ""
|
||||
var downloadedSeconds: Double = 0
|
||||
var totalSeconds: Double = 0
|
||||
|
||||
init(module: HlsDownloaderModule) {
|
||||
self.module = module
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
||||
didLoad timeRange: CMTimeRange,
|
||||
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
|
||||
timeRangeExpectedToLoad: CMTimeRange) {
|
||||
var loadedSeconds = 0.0
|
||||
for value in loadedTimeRanges {
|
||||
loadedSeconds += CMTimeGetSeconds(value.timeRangeValue.duration)
|
||||
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
|
||||
let downloaded = loadedTimeRanges.reduce(0.0) { total, value in
|
||||
let timeRange = value.timeRangeValue
|
||||
return total + CMTimeGetSeconds(timeRange.duration)
|
||||
}
|
||||
|
||||
let total = CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
|
||||
let metadata = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.metadata ?? [:]
|
||||
|
||||
self.downloadedSeconds = downloaded
|
||||
self.totalSeconds = total
|
||||
|
||||
let progress = total > 0 ? downloaded / total : 0
|
||||
|
||||
module?.sendEvent("onProgress", [
|
||||
"id": providedId,
|
||||
"progress": progress,
|
||||
"bytesDownloaded": downloaded,
|
||||
"bytesTotal": total,
|
||||
"state": progress >= 1.0 ? "DONE" : "DOWNLOADING",
|
||||
"metadata": metadata
|
||||
])
|
||||
}
|
||||
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 {
|
||||
let state = module?.mappedState(for: task, errorOccurred: true) ?? "FAILED"
|
||||
module?.sendEvent("onError", [
|
||||
"id": providedId,
|
||||
"error": error.localizedDescription,
|
||||
"state": state
|
||||
])
|
||||
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
let metadata = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.metadata ?? [:]
|
||||
module?.sendEvent("onComplete", [
|
||||
"id": providedId,
|
||||
"location": location.absoluteString,
|
||||
"state": "DONE",
|
||||
"metadata": metadata
|
||||
])
|
||||
module?.removeDownload(with: assetDownloadTask.taskIdentifier)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
if let error = error {
|
||||
let metadata = module?.activeDownloads[task.taskIdentifier]?.metadata ?? [:]
|
||||
module?.sendEvent("onError", [
|
||||
"id": providedId,
|
||||
"error": error.localizedDescription,
|
||||
"state": "FAILED",
|
||||
"metadata": metadata
|
||||
])
|
||||
module?.removeDownload(with: taskIdentifier)
|
||||
}
|
||||
}
|
||||
module?.removeDownload(with: task.taskIdentifier)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
||||
didFinishDownloadingTo location: URL) {
|
||||
let state = module?.mappedState(for: assetDownloadTask) ?? "DONE"
|
||||
module?.sendEvent("onComplete", [
|
||||
"id": providedId,
|
||||
"location": location.absoluteString,
|
||||
"state": state
|
||||
])
|
||||
module?.removeDownload(with: assetDownloadTask.taskIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
export type OnProgressEventPayload = {
|
||||
progress: number;
|
||||
state: "PENDING" | "DOWNLOADING" | "PAUSED" | "DONE" | "FAILED" | "STOPPED";
|
||||
export type DownloadState =
|
||||
| "PENDING"
|
||||
| "DOWNLOADING"
|
||||
| "PAUSED"
|
||||
| "DONE"
|
||||
| "FAILED"
|
||||
| "STOPPED";
|
||||
|
||||
export interface DownloadMetadata {
|
||||
Name: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type BaseEventPayload = {
|
||||
id: string;
|
||||
state: DownloadState;
|
||||
metadata?: DownloadMetadata;
|
||||
};
|
||||
|
||||
export type OnProgressEventPayload = BaseEventPayload & {
|
||||
progress: number;
|
||||
bytesDownloaded: number;
|
||||
bytesTotal: number;
|
||||
};
|
||||
|
||||
export type OnErrorEventPayload = {
|
||||
export type OnErrorEventPayload = BaseEventPayload & {
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type OnCompleteEventPayload = {
|
||||
export type OnCompleteEventPayload = BaseEventPayload & {
|
||||
location: string;
|
||||
};
|
||||
|
||||
@@ -19,3 +36,15 @@ export type HlsDownloaderModuleEvents = {
|
||||
onError: (params: OnErrorEventPayload) => void;
|
||||
onComplete: (params: OnCompleteEventPayload) => void;
|
||||
};
|
||||
|
||||
// Export a common interface that can be used by both HLS and regular downloads
|
||||
export interface DownloadInfo {
|
||||
id: string;
|
||||
progress: number;
|
||||
state: DownloadState;
|
||||
bytesDownloaded?: number;
|
||||
bytesTotal?: number;
|
||||
location?: string;
|
||||
error?: string;
|
||||
metadata?: DownloadMetadata;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user