forked from Ninjalama/streamyfin_mirror
245 lines
7.8 KiB
Swift
245 lines
7.8 KiB
Swift
import AVFoundation
|
|
import ExpoModulesCore
|
|
|
|
public class HlsDownloaderModule: Module {
|
|
var activeDownloads:
|
|
[Int: (
|
|
task: AVAssetDownloadTask, delegate: HLSDownloadDelegate, metadata: [String: Any],
|
|
startTime: Double
|
|
)] = [:]
|
|
|
|
public func definition() -> ModuleDefinition {
|
|
Name("HlsDownloader")
|
|
|
|
Events("onProgress", "onError", "onComplete")
|
|
|
|
Function("downloadHLSAsset") {
|
|
(providedId: String, url: String, metadata: [String: Any]?) -> Void in
|
|
let startTime = Date().timeIntervalSince1970
|
|
print(
|
|
"Starting download - ID: \(providedId), URL: \(url), Metadata: \(String(describing: metadata)), StartTime: \(startTime)"
|
|
)
|
|
|
|
guard let assetURL = URL(string: url) else {
|
|
self.sendEvent(
|
|
"onError",
|
|
[
|
|
"id": providedId,
|
|
"error": "Invalid URL",
|
|
"state": "FAILED",
|
|
"metadata": metadata ?? [:],
|
|
"startTime": startTime,
|
|
])
|
|
return
|
|
}
|
|
|
|
let asset = AVURLAsset(url: assetURL)
|
|
let configuration = URLSessionConfiguration.background(
|
|
withIdentifier: "com.example.hlsdownload")
|
|
let delegate = HLSDownloadDelegate(module: self)
|
|
delegate.providedId = providedId
|
|
delegate.startTime = startTime
|
|
let downloadSession = AVAssetDownloadURLSession(
|
|
configuration: configuration,
|
|
assetDownloadDelegate: delegate,
|
|
delegateQueue: OperationQueue.main
|
|
)
|
|
|
|
guard
|
|
let task = downloadSession.makeAssetDownloadTask(
|
|
asset: asset,
|
|
assetTitle: providedId,
|
|
assetArtworkData: nil,
|
|
options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: startTime]
|
|
)
|
|
else {
|
|
self.sendEvent(
|
|
"onError",
|
|
[
|
|
"id": providedId,
|
|
"error": "Failed to create download task",
|
|
"state": "FAILED",
|
|
"metadata": metadata ?? [:],
|
|
"startTime": startTime,
|
|
])
|
|
return
|
|
}
|
|
|
|
delegate.taskIdentifier = task.taskIdentifier
|
|
self.activeDownloads[task.taskIdentifier] = (task, delegate, metadata ?? [:], startTime)
|
|
self.sendEvent(
|
|
"onProgress",
|
|
[
|
|
"id": providedId,
|
|
"progress": 0.0,
|
|
"state": "PENDING",
|
|
"metadata": metadata ?? [:],
|
|
"startTime": startTime,
|
|
])
|
|
|
|
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 startTime = pair.startTime
|
|
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),
|
|
"metadata": metadata,
|
|
"startTime": startTime,
|
|
])
|
|
}
|
|
return downloads
|
|
}
|
|
|
|
OnStartObserving {}
|
|
OnStopObserving {}
|
|
}
|
|
|
|
func removeDownload(with id: Int) {
|
|
activeDownloads.removeValue(forKey: id)
|
|
}
|
|
|
|
func persistDownloadedFolder(originalLocation: URL, folderName: String) throws -> URL {
|
|
let fileManager = FileManager.default
|
|
let documents = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
let destinationDir = documents.appendingPathComponent("downloads", isDirectory: true)
|
|
if !fileManager.fileExists(atPath: destinationDir.path) {
|
|
try fileManager.createDirectory(at: destinationDir, withIntermediateDirectories: true)
|
|
}
|
|
let newLocation = destinationDir.appendingPathComponent(folderName, isDirectory: true)
|
|
if fileManager.fileExists(atPath: newLocation.path) {
|
|
try fileManager.removeItem(at: newLocation)
|
|
}
|
|
try fileManager.moveItem(at: originalLocation, to: newLocation)
|
|
return newLocation
|
|
}
|
|
|
|
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"
|
|
}
|
|
}
|
|
}
|
|
|
|
class HLSDownloadDelegate: NSObject, AVAssetDownloadDelegate {
|
|
weak var module: HlsDownloaderModule?
|
|
var taskIdentifier: Int = 0
|
|
var providedId: String = ""
|
|
var downloadedSeconds: Double = 0
|
|
var totalSeconds: Double = 0
|
|
var startTime: Double = 0
|
|
|
|
init(module: HlsDownloaderModule) {
|
|
self.module = module
|
|
}
|
|
|
|
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 ?? [:]
|
|
let startTime = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.startTime ?? 0
|
|
|
|
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,
|
|
"startTime": startTime,
|
|
])
|
|
}
|
|
|
|
func urlSession(
|
|
_ session: URLSession, assetDownloadTask: AVAssetDownloadTask,
|
|
didFinishDownloadingTo location: URL
|
|
) {
|
|
let metadata = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.metadata ?? [:]
|
|
let startTime = module?.activeDownloads[assetDownloadTask.taskIdentifier]?.startTime ?? 0
|
|
let folderName = providedId
|
|
do {
|
|
guard let module = module else { return }
|
|
let newLocation = try module.persistDownloadedFolder(
|
|
originalLocation: location, folderName: folderName)
|
|
|
|
if !metadata.isEmpty {
|
|
let metadataLocation = newLocation.deletingLastPathComponent().appendingPathComponent(
|
|
"\(providedId).json")
|
|
let jsonData = try JSONSerialization.data(withJSONObject: metadata, options: .prettyPrinted)
|
|
try jsonData.write(to: metadataLocation)
|
|
}
|
|
|
|
module.sendEvent(
|
|
"onComplete",
|
|
[
|
|
"id": providedId,
|
|
"location": newLocation.absoluteString,
|
|
"state": "DONE",
|
|
"metadata": metadata,
|
|
"startTime": startTime,
|
|
])
|
|
} catch {
|
|
module?.sendEvent(
|
|
"onError",
|
|
[
|
|
"id": providedId,
|
|
"error": error.localizedDescription,
|
|
"state": "FAILED",
|
|
"metadata": metadata,
|
|
"startTime": startTime,
|
|
])
|
|
}
|
|
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 ?? [:]
|
|
let startTime = module?.activeDownloads[task.taskIdentifier]?.startTime ?? 0
|
|
module?.sendEvent(
|
|
"onError",
|
|
[
|
|
"id": providedId,
|
|
"error": error.localizedDescription,
|
|
"state": "FAILED",
|
|
"metadata": metadata,
|
|
"startTime": startTime,
|
|
])
|
|
module?.removeDownload(with: taskIdentifier)
|
|
}
|
|
}
|
|
}
|