From be6b398e0fd046bffe65a6f5604a2fd52315ac1b Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Wed, 10 Jan 2024 15:32:52 +0530 Subject: [PATCH 1/9] add database --- Source/Database/ObjectManager.swift | 2 +- Source/TPStreamsSDK.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Database/ObjectManager.swift b/Source/Database/ObjectManager.swift index 3e1ecf1..de423cb 100644 --- a/Source/Database/ObjectManager.swift +++ b/Source/Database/ObjectManager.swift @@ -20,7 +20,7 @@ public class ObjectManager { func get(id: Any) -> T? { return realm.object(ofType: T.self, forPrimaryKey: id) } - + func update(object: T, with attributes: [String: Any]) { try! realm.write { for (key, value) in attributes { diff --git a/Source/TPStreamsSDK.swift b/Source/TPStreamsSDK.swift index 1fc6263..5d6a502 100644 --- a/Source/TPStreamsSDK.swift +++ b/Source/TPStreamsSDK.swift @@ -52,7 +52,7 @@ public class TPStreamsSDK { options.attachViewHierarchy = true } } - + private static func initializeDatabase() { Realm.Configuration.defaultConfiguration = Realm.Configuration(schemaVersion: 1) } From 3c56c324a2811b9e2d46b0920c60940b341935bc Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Wed, 10 Jan 2024 15:58:30 +0530 Subject: [PATCH 2/9] Added non-drm video download support --- Source/Database/Model/OfflineAsset.swift | 9 ++ Source/Database/ObjectManager.swift | 5 + .../Database/TPStreamsDownloadManager.swift | 91 +++++++++++++++++++ Tests/ObjectManagerTest.swift | 33 +++++++ iOSPlayerSDK.xcodeproj/project.pbxproj | 8 ++ 5 files changed, 146 insertions(+) create mode 100644 Source/Database/TPStreamsDownloadManager.swift create mode 100644 Tests/ObjectManagerTest.swift diff --git a/Source/Database/Model/OfflineAsset.swift b/Source/Database/Model/OfflineAsset.swift index 241c83f..b96e250 100644 --- a/Source/Database/Model/OfflineAsset.swift +++ b/Source/Database/Model/OfflineAsset.swift @@ -25,6 +25,15 @@ public class OfflineAsset: Object { public static var manager = ObjectManager() } +extension OfflineAsset { + internal static func create(assetId: String, srcURL: String) -> OfflineAsset { + let offlineAsset = OfflineAsset() + offlineAsset.assetId = assetId + offlineAsset.srcURL = srcURL + return offlineAsset + } +} + enum Status: String { case notStarted = "notStarted" case inProgress = "inProgress" diff --git a/Source/Database/ObjectManager.swift b/Source/Database/ObjectManager.swift index de423cb..dc99dbe 100644 --- a/Source/Database/ObjectManager.swift +++ b/Source/Database/ObjectManager.swift @@ -28,4 +28,9 @@ public class ObjectManager { } } } + + func isExist(assetId: Any) -> Bool { + let object = realm.object(ofType: T.self, forPrimaryKey: assetId) + return object != nil + } } diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift new file mode 100644 index 0000000..1d9cf31 --- /dev/null +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -0,0 +1,91 @@ +// +// TPStreamsDownloadManager.swift +// TPStreamsSDK +// +// Created by Prithuvi on 08/01/24. +// + +import Foundation +import AVFoundation + + +public final class TPStreamsDownloadManager { + + static public let shared = TPStreamsDownloadManager() + private var assetDownloadURLSession: AVAssetDownloadURLSession! + private var assetDownloadDelegate: AssetDownloadDelegate! + + private init() { + let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "com.tpstreams.downloadSession") + assetDownloadDelegate = AssetDownloadDelegate() + assetDownloadURLSession = AVAssetDownloadURLSession( + configuration: backgroundConfiguration, + assetDownloadDelegate: assetDownloadDelegate, + delegateQueue: OperationQueue.main + ) + } + + internal func startDownload(asset: Asset, bitRate: Double) { + + if OfflineAsset.manager.isExist(assetId: asset.id) { return } + + let avUrlAsset = AVURLAsset(url: URL(string: asset.video.playbackURL)!) + + guard let task = assetDownloadURLSession.makeAssetDownloadTask( + asset: avUrlAsset, + assetTitle: asset.title, + assetArtworkData: nil, + options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: bitRate] + ) else { return } + + let offlineAsset = OfflineAsset.create(assetId: asset.id, srcURL: asset.video.playbackURL) + OfflineAsset.manager.add(object: offlineAsset) + assetDownloadDelegate.activeDownloadsMap[task] = offlineAsset + task.resume() + } + +} + +internal class AssetDownloadDelegate: NSObject, AVAssetDownloadDelegate { + + var activeDownloadsMap = [AVAssetDownloadTask: OfflineAsset]() + + public func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) { + guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } + offlineAsset.update(["downloadedPath": location.relativePath]) + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let assetDownloadTask = task as? AVAssetDownloadTask else { return } + guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } + updateDownloadCompleteStatus(error, offlineAsset) + activeDownloadsMap.removeValue(forKey: assetDownloadTask) + } + + private func updateDownloadCompleteStatus(_ error: Error?,_ offlineAsset: OfflineAsset) { + let status: Status = (error == nil) ? .finished : .failed + let updateValues: [String: Any] = ["status": status.rawValue, "downloadedAt": Date()] + offlineAsset.update(updateValues) + } + + public func urlSession(_ session: URLSession, + assetDownloadTask: AVAssetDownloadTask, + didLoad timeRange: CMTimeRange, + totalTimeRangesLoaded loadedTimeRanges: [NSValue], + timeRangeExpectedToLoad: CMTimeRange + ) { + guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } + + let percentageComplete = calculateDownloadPercentage(loadedTimeRanges, timeRangeExpectedToLoad) + offlineAsset.update(["status": Status.inProgress.rawValue, "percentageCompleted": percentageComplete]) + } + + private func calculateDownloadPercentage(_ loadedTimeRanges: [NSValue], _ timeRangeExpectedToLoad: CMTimeRange) -> Double { + var percentageComplete = 0.0 + for value in loadedTimeRanges { + let loadedTimeRange = value.timeRangeValue + percentageComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds + } + return percentageComplete * 100 + } +} diff --git a/Tests/ObjectManagerTest.swift b/Tests/ObjectManagerTest.swift new file mode 100644 index 0000000..10e6bc9 --- /dev/null +++ b/Tests/ObjectManagerTest.swift @@ -0,0 +1,33 @@ +// +// ObjectManagerTest.swift +// iOSPlayerSDKTests +// +// Created by Prithuvi on 10/01/24. +// + +import XCTest + +import XCTest +@testable import TPStreamsSDK +import RealmSwift + +final class ObjectManagerTest: XCTestCase { + + override func setUp() { + Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name + } + + func testAddAndGetObject() throws { + let offlineAsset = OfflineAsset() + offlineAsset.assetId = "test" + offlineAsset.srcURL = "https://www.test.com.m3u8" + // Add object + OfflineAsset.manager.add(object: offlineAsset) + // Get object + let retrivedOfflineAsset = OfflineAsset.manager.get(assetId: "test") + + XCTAssert(retrivedOfflineAsset!.assetId == "test") + XCTAssert(retrivedOfflineAsset!.srcURL == "https://www.test.com.m3u8") + } + +} diff --git a/iOSPlayerSDK.xcodeproj/project.pbxproj b/iOSPlayerSDK.xcodeproj/project.pbxproj index 4337b8a..9ebb454 100644 --- a/iOSPlayerSDK.xcodeproj/project.pbxproj +++ b/iOSPlayerSDK.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ D9C1FAA22B4E7E2600B27A3B /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D9C1FAA12B4E7E2600B27A3B /* RealmSwift */; }; D9C1FAA82B4E7EE700B27A3B /* OfflineAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C1FAA72B4E7EE700B27A3B /* OfflineAsset.swift */; }; D9C1FAB22B4E915C00B27A3B /* ObjectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C1FAB12B4E915C00B27A3B /* ObjectManager.swift */; }; + D9C1FAB62B4EA4CF00B27A3B /* TPStreamsDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C1FAB52B4EA4CF00B27A3B /* TPStreamsDownloadManager.swift */; }; + D9C1FAB82B4EA53200B27A3B /* ObjectManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C1FAB72B4EA53200B27A3B /* ObjectManagerTest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -171,6 +173,8 @@ D904B1DD2B3C5BAC00A7E26C /* TPStreamsSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TPStreamsSDK.xctestplan; sourceTree = ""; }; D9C1FAA72B4E7EE700B27A3B /* OfflineAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineAsset.swift; sourceTree = ""; }; D9C1FAB12B4E915C00B27A3B /* ObjectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectManager.swift; sourceTree = ""; }; + D9C1FAB52B4EA4CF00B27A3B /* TPStreamsDownloadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TPStreamsDownloadManager.swift; sourceTree = ""; }; + D9C1FAB72B4EA53200B27A3B /* ObjectManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectManagerTest.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -392,6 +396,7 @@ isa = PBXGroup; children = ( 8EDE99B32A2643B000E43EA9 /* iOSPlayerSDKTests.swift */, + D9C1FAB72B4EA53200B27A3B /* ObjectManagerTest.swift */, ); path = Tests; sourceTree = ""; @@ -401,6 +406,7 @@ children = ( D9C1FAB02B4E910800B27A3B /* Model */, D9C1FAB12B4E915C00B27A3B /* ObjectManager.swift */, + D9C1FAB52B4EA4CF00B27A3B /* TPStreamsDownloadManager.swift */, ); path = Database; sourceTree = ""; @@ -630,6 +636,7 @@ 8E6389E92A278D1D00306FA4 /* ContentKeyDelegate.swift in Sources */, 8E6389DD2A27338F00306FA4 /* AVPlayerBridge.swift in Sources */, 8E6389BC2A2724D000306FA4 /* TPStreamPlayerView.swift in Sources */, + D9C1FAB62B4EA4CF00B27A3B /* TPStreamsDownloadManager.swift in Sources */, 035351A02A2EDAFA001E38F3 /* PlayerSettingsButton.swift in Sources */, 03913C482A850BF9002E7E0C /* ProgressBar.swift in Sources */, 8E6C5CB52A28BD9A003EC948 /* TPStreamsSDK.swift in Sources */, @@ -661,6 +668,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D9C1FAB82B4EA53200B27A3B /* ObjectManagerTest.swift in Sources */, 8EDE99B42A2643B000E43EA9 /* iOSPlayerSDKTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 8e402fb9f5a6775dbddb2bee4bd4ca7f98978b93 Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Thu, 11 Jan 2024 15:17:59 +0530 Subject: [PATCH 3/9] refcator --- Source/Database/Model/OfflineAsset.swift | 9 ++++++++- Source/Database/TPStreamsDownloadManager.swift | 13 ++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Source/Database/Model/OfflineAsset.swift b/Source/Database/Model/OfflineAsset.swift index b96e250..d93a881 100644 --- a/Source/Database/Model/OfflineAsset.swift +++ b/Source/Database/Model/OfflineAsset.swift @@ -26,7 +26,14 @@ public class OfflineAsset: Object { } extension OfflineAsset { - internal static func create(assetId: String, srcURL: String) -> OfflineAsset { + static func create( + assetId: String, + srcURL: String, + title: String, + resolution: String, + duration:Double, + bitRate: Double + ) -> OfflineAsset { let offlineAsset = OfflineAsset() offlineAsset.assetId = assetId offlineAsset.srcURL = srcURL diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 1d9cf31..2daa526 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -25,7 +25,7 @@ public final class TPStreamsDownloadManager { ) } - internal func startDownload(asset: Asset, bitRate: Double) { + internal func startDownload(asset: Asset, videoQuality: VideoQuality) { if OfflineAsset.manager.isExist(assetId: asset.id) { return } @@ -35,10 +35,17 @@ public final class TPStreamsDownloadManager { asset: avUrlAsset, assetTitle: asset.title, assetArtworkData: nil, - options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: bitRate] + options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: videoQuality.bitrate] ) else { return } - let offlineAsset = OfflineAsset.create(assetId: asset.id, srcURL: asset.video.playbackURL) + let offlineAsset = OfflineAsset.create( + assetId: asset.id, + srcURL: asset.video.playbackURL, + title: asset.title, + resolution:videoQuality.resolution, + duration: 0, + bitRate: videoQuality.bitrate + ) OfflineAsset.manager.add(object: offlineAsset) assetDownloadDelegate.activeDownloadsMap[task] = offlineAsset task.resume() From 29432cbce93fc15d0f22d25cd3f5120743343b5e Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Thu, 11 Jan 2024 16:52:42 +0530 Subject: [PATCH 4/9] refcator --- Source/Database/Model/OfflineAsset.swift | 5 +++++ Source/Database/TPStreamsDownloadManager.swift | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Database/Model/OfflineAsset.swift b/Source/Database/Model/OfflineAsset.swift index d93a881..effefed 100644 --- a/Source/Database/Model/OfflineAsset.swift +++ b/Source/Database/Model/OfflineAsset.swift @@ -37,6 +37,11 @@ extension OfflineAsset { let offlineAsset = OfflineAsset() offlineAsset.assetId = assetId offlineAsset.srcURL = srcURL + offlineAsset.title = title + offlineAsset.resolution = resolution + offlineAsset.duration = duration + offlineAsset.bitRate = bitRate + offlineAsset.size = (bitRate * duration) return offlineAsset } } diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 2daa526..c191ee5 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -43,7 +43,7 @@ public final class TPStreamsDownloadManager { srcURL: asset.video.playbackURL, title: asset.title, resolution:videoQuality.resolution, - duration: 0, + duration: asset.video.duration, bitRate: videoQuality.bitrate ) OfflineAsset.manager.add(object: offlineAsset) From 72b4636e3bec369a4da058c038d1edc0a30c427a Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Thu, 18 Jan 2024 19:03:27 +0530 Subject: [PATCH 5/9] refcator --- Source/Database/ObjectManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Database/ObjectManager.swift b/Source/Database/ObjectManager.swift index dc99dbe..4d98d03 100644 --- a/Source/Database/ObjectManager.swift +++ b/Source/Database/ObjectManager.swift @@ -29,8 +29,8 @@ public class ObjectManager { } } - func isExist(assetId: Any) -> Bool { - let object = realm.object(ofType: T.self, forPrimaryKey: assetId) + func exists(id: Any) -> Bool { + let object = realm.object(ofType: T.self, forPrimaryKey: id) return object != nil } } From 58314dead531f81928bc3d9691d92cdf6d597352 Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Thu, 18 Jan 2024 19:14:29 +0530 Subject: [PATCH 6/9] fix --- Source/Database/TPStreamsDownloadManager.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index c191ee5..0e9c0a3 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -59,7 +59,7 @@ internal class AssetDownloadDelegate: NSObject, AVAssetDownloadDelegate { public func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) { guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } - offlineAsset.update(["downloadedPath": location.relativePath]) + OfflineAsset.manager.update(object: offlineAsset, with: ["downloadedPath": location.relativePath]) } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { @@ -72,7 +72,7 @@ internal class AssetDownloadDelegate: NSObject, AVAssetDownloadDelegate { private func updateDownloadCompleteStatus(_ error: Error?,_ offlineAsset: OfflineAsset) { let status: Status = (error == nil) ? .finished : .failed let updateValues: [String: Any] = ["status": status.rawValue, "downloadedAt": Date()] - offlineAsset.update(updateValues) + OfflineAsset.manager.update(object: offlineAsset, with: updateValues) } public func urlSession(_ session: URLSession, @@ -84,7 +84,7 @@ internal class AssetDownloadDelegate: NSObject, AVAssetDownloadDelegate { guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } let percentageComplete = calculateDownloadPercentage(loadedTimeRanges, timeRangeExpectedToLoad) - offlineAsset.update(["status": Status.inProgress.rawValue, "percentageCompleted": percentageComplete]) + OfflineAsset.manager.update(object: offlineAsset, with: ["status": Status.inProgress.rawValue, "percentageCompleted": percentageComplete]) } private func calculateDownloadPercentage(_ loadedTimeRanges: [NSValue], _ timeRangeExpectedToLoad: CMTimeRange) -> Double { From 0e79ad6daef5fcdaf1432d8e1251d0150d3792bf Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Fri, 19 Jan 2024 12:15:55 +0530 Subject: [PATCH 7/9] reefcator --- Source/Database/TPStreamsDownloadManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 0e9c0a3..33cb07e 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -27,7 +27,7 @@ public final class TPStreamsDownloadManager { internal func startDownload(asset: Asset, videoQuality: VideoQuality) { - if OfflineAsset.manager.isExist(assetId: asset.id) { return } + if OfflineAsset.manager.exists(id: asset.id) { return } let avUrlAsset = AVURLAsset(url: URL(string: asset.video.playbackURL)!) From f39a61d20ece4147c47290e0e0f5370590bb1f1d Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 Date: Tue, 23 Jan 2024 12:27:42 +0530 Subject: [PATCH 8/9] refcator --- .../Database/TPStreamsDownloadManager.swift | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 33cb07e..2e47678 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -31,8 +31,9 @@ public final class TPStreamsDownloadManager { let avUrlAsset = AVURLAsset(url: URL(string: asset.video.playbackURL)!) - guard let task = assetDownloadURLSession.makeAssetDownloadTask( - asset: avUrlAsset, + guard let task = assetDownloadURLSession.aggregateAssetDownloadTask( + with: avUrlAsset, + mediaSelections: [avUrlAsset.preferredMediaSelection], assetTitle: asset.title, assetArtworkData: nil, options: [AVAssetDownloadTaskMinimumRequiredMediaBitrateKey: videoQuality.bitrate] @@ -55,37 +56,38 @@ public final class TPStreamsDownloadManager { internal class AssetDownloadDelegate: NSObject, AVAssetDownloadDelegate { - var activeDownloadsMap = [AVAssetDownloadTask: OfflineAsset]() + var activeDownloadsMap = [AVAggregateAssetDownloadTask: OfflineAsset]() - public func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) { - guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } - OfflineAsset.manager.update(object: offlineAsset, with: ["downloadedPath": location.relativePath]) - } - - public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - guard let assetDownloadTask = task as? AVAssetDownloadTask else { return } + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let assetDownloadTask = task as? AVAggregateAssetDownloadTask else { return } guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } updateDownloadCompleteStatus(error, offlineAsset) activeDownloadsMap.removeValue(forKey: assetDownloadTask) } - private func updateDownloadCompleteStatus(_ error: Error?,_ offlineAsset: OfflineAsset) { - let status: Status = (error == nil) ? .finished : .failed - let updateValues: [String: Any] = ["status": status.rawValue, "downloadedAt": Date()] - OfflineAsset.manager.update(object: offlineAsset, with: updateValues) + func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL) { + guard let offlineAsset = activeDownloadsMap[aggregateAssetDownloadTask] else { return } + OfflineAsset.manager.update(object: offlineAsset, with: ["downloadedPath": String(location.relativePath)]) } - public func urlSession(_ session: URLSession, - assetDownloadTask: AVAssetDownloadTask, - didLoad timeRange: CMTimeRange, - totalTimeRangesLoaded loadedTimeRanges: [NSValue], - timeRangeExpectedToLoad: CMTimeRange + func urlSession(_ session: URLSession, + aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, + didLoad timeRange: CMTimeRange, + totalTimeRangesLoaded loadedTimeRanges: [NSValue], + timeRangeExpectedToLoad: CMTimeRange, + for mediaSelection: AVMediaSelection ) { - guard let offlineAsset = activeDownloadsMap[assetDownloadTask] else { return } + guard let offlineAsset = activeDownloadsMap[aggregateAssetDownloadTask] else { return } let percentageComplete = calculateDownloadPercentage(loadedTimeRanges, timeRangeExpectedToLoad) OfflineAsset.manager.update(object: offlineAsset, with: ["status": Status.inProgress.rawValue, "percentageCompleted": percentageComplete]) } + + private func updateDownloadCompleteStatus(_ error: Error?,_ offlineAsset: OfflineAsset) { + let status: Status = (error == nil) ? .finished : .failed + let updateValues: [String: Any] = ["status": status.rawValue, "downloadedAt": Date()] + OfflineAsset.manager.update(object: offlineAsset, with: updateValues) + } private func calculateDownloadPercentage(_ loadedTimeRanges: [NSValue], _ timeRangeExpectedToLoad: CMTimeRange) -> Double { var percentageComplete = 0.0 From b34ce2642705350066e8d9b15edfed2a45866a18 Mon Sep 17 00:00:00 2001 From: PruthiviRaj27 <112844543+PruthiviRaj27@users.noreply.github.com> Date: Thu, 25 Jan 2024 15:31:55 +0530 Subject: [PATCH 9/9] feat: Add download button in player view (#54) - This commit introduces a download option within the player view accessible through the settings button. The download feature is now displayed as one of the settings and can be enabled using the API. - In Swift UI download option is enabled by default. - In StoryBoard we added `showDownloadOption()` API in `TPStreamPlayerConfiguration`. --- Example/ContentView.swift | 4 +- Source/Database/ObjectManager.swift | 2 +- .../Database/TPStreamsDownloadManager.swift | 2 +- Source/Managers/TPStreamPlayer.swift | 3 ++ Source/TPStreamPlayerConfiguration.swift | 6 +++ .../Views/SwiftUI/PlayerSettingsButton.swift | 30 +++++++++++++- Source/Views/UIKit/PlayerControlsUIView.swift | 41 ++++++++++++++++--- StoryboardExample/ViewController.swift | 1 + 8 files changed, 77 insertions(+), 12 deletions(-) diff --git a/Example/ContentView.swift b/Example/ContentView.swift index d1f6869..32fe973 100644 --- a/Example/ContentView.swift +++ b/Example/ContentView.swift @@ -12,8 +12,8 @@ import AVKit struct ContentView: View { var body: some View { VStack { - let player = TPAVPlayer(assetID: "peBmzxeQ7Mf", - accessToken: "d7ebb4b2-8dee-4dff-bb00-e833195b0756") + let player = TPAVPlayer(assetID: "8eaHZjXt6km", + accessToken: "16b608ba-9979-45a0-94fb-b27c1a86b3c1") TPStreamPlayerView(player: player) .frame(height: 240) Spacer() diff --git a/Source/Database/ObjectManager.swift b/Source/Database/ObjectManager.swift index 4d98d03..2ae93c7 100644 --- a/Source/Database/ObjectManager.swift +++ b/Source/Database/ObjectManager.swift @@ -28,7 +28,7 @@ public class ObjectManager { } } } - + func exists(id: Any) -> Bool { let object = realm.object(ofType: T.self, forPrimaryKey: id) return object != nil diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 2e47678..52c6c75 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -82,7 +82,7 @@ internal class AssetDownloadDelegate: NSObject, AVAssetDownloadDelegate { let percentageComplete = calculateDownloadPercentage(loadedTimeRanges, timeRangeExpectedToLoad) OfflineAsset.manager.update(object: offlineAsset, with: ["status": Status.inProgress.rawValue, "percentageCompleted": percentageComplete]) } - + private func updateDownloadCompleteStatus(_ error: Error?,_ offlineAsset: OfflineAsset) { let status: Status = (error == nil) ? .finished : .failed let updateValues: [String: Any] = ["status": status.rawValue, "downloadedAt": Date()] diff --git a/Source/Managers/TPStreamPlayer.swift b/Source/Managers/TPStreamPlayer.swift index 701d528..c3297c2 100644 --- a/Source/Managers/TPStreamPlayer.swift +++ b/Source/Managers/TPStreamPlayer.swift @@ -26,6 +26,9 @@ class TPStreamPlayer: NSObject { var currentVideoQuality: VideoQuality? { return self.player.availableVideoQualities.first( where: {$0.bitrate == self.player.currentItem?.preferredPeakBitRate }) } + var asset: Asset? { + return self.player.asset + } init(player: TPAVPlayer){ self.player = player diff --git a/Source/TPStreamPlayerConfiguration.swift b/Source/TPStreamPlayerConfiguration.swift index 93639d2..99f510b 100644 --- a/Source/TPStreamPlayerConfiguration.swift +++ b/Source/TPStreamPlayerConfiguration.swift @@ -13,6 +13,7 @@ public struct TPStreamPlayerConfiguration { public var preferredRewindDuration: TimeInterval = 10.0 public var watchedProgressTrackColor: UIColor = .red public var progressBarThumbColor: UIColor = .red + public var showDownloadOption: Bool = false } @@ -43,6 +44,11 @@ public class TPStreamPlayerConfigurationBuilder { return self } + public func showDownloadOption() -> Self { + configuration.showDownloadOption = true + return self + } + public func build() -> TPStreamPlayerConfiguration { return configuration } diff --git a/Source/Views/SwiftUI/PlayerSettingsButton.swift b/Source/Views/SwiftUI/PlayerSettingsButton.swift index 3210e53..2d247a3 100644 --- a/Source/Views/SwiftUI/PlayerSettingsButton.swift +++ b/Source/Views/SwiftUI/PlayerSettingsButton.swift @@ -29,7 +29,7 @@ struct PlayerSettingsButton: View { return ActionSheet( title: Text("Settings"), message: nil, - buttons: [playbackSpeedButton(), videoQualityButton(), .cancel()] + buttons: [playbackSpeedButton(), videoQualityButton(), downloadQualityButton(), .cancel()] ) case .playbackSpeed: return ActionSheet( @@ -43,6 +43,12 @@ struct PlayerSettingsButton: View { message: nil, buttons: videoQualityOptions() + [.cancel()] ) + case .downloadQuality: + return ActionSheet( + title: Text("Download Quality"), + message: nil, + buttons: downloadQualityOptions() + [.cancel()] + ) } } @@ -64,6 +70,15 @@ struct PlayerSettingsButton: View { } } + private func downloadQualityButton() -> ActionSheet.Button { + return .default(Text("Download")) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.showOptions = true + self.currentMenu = .downloadQuality + } + } + } + private func playbackSpeedOptions() -> [ActionSheet.Button] { let playbackSpeeds = PlaybackSpeed.allCases return playbackSpeeds.map { speed in @@ -80,6 +95,17 @@ struct PlayerSettingsButton: View { } } } + + private func downloadQualityOptions() -> [ActionSheet.Button] { + var availableVideoQualities = player.availableVideoQualities + // Remove Auto Quality from the Array + availableVideoQualities.remove(at: 0) + return availableVideoQualities.map { downloadQuality in + .default(Text(downloadQuality.resolution)) { + TPStreamsDownloadManager.shared.startDownload(asset: player.asset!, videoQuality: downloadQuality) + } + } + } } -enum SettingsMenu { case main, playbackSpeed, videoQuality } +enum SettingsMenu { case main, playbackSpeed, videoQuality, downloadQuality } diff --git a/Source/Views/UIKit/PlayerControlsUIView.swift b/Source/Views/UIKit/PlayerControlsUIView.swift index 4c932ba..618f6ca 100644 --- a/Source/Views/UIKit/PlayerControlsUIView.swift +++ b/Source/Views/UIKit/PlayerControlsUIView.swift @@ -119,9 +119,12 @@ class PlayerControlsUIView: UIView { optionsMenu.addAction(UIAlertAction(title: "Playback Speed", style: .default) { _ in self.showPlaybackSpeedMenu()}) optionsMenu.addAction(UIAlertAction(title: "Video Quality", style: .default, handler: { action in self.showVideoQualityMenu()})) optionsMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + if playerConfig.showDownloadOption { + optionsMenu.addAction(UIAlertAction(title: "Download", style: .default, handler: { action in self.showDownloadQualityMenu()})) + } parentViewController?.present(optionsMenu, animated: true, completion: nil) } - + func showPlaybackSpeedMenu(){ let playbackSpeedMenu = createPlaybackSpeedMenu() parentViewController?.present(playbackSpeedMenu, animated: true, completion: nil) @@ -132,14 +135,19 @@ class PlayerControlsUIView: UIView { parentViewController?.present(videoQualityMenu, animated: true, completion: nil) } + func showDownloadQualityMenu(){ + let downloadQualityMenu = createDownloadQualityMenu() + parentViewController?.present(downloadQualityMenu, animated: true, completion: nil) + } + func createPlaybackSpeedMenu() -> UIAlertController { let playbackSpeedMenu = UIAlertController(title: "Playback Speed", message: nil, preferredStyle: ACTION_SHEET_PREFERRED_STYLE) - + for playbackSpeed in PlaybackSpeed.allCases { let action = createActionForPlaybackSpeed(playbackSpeed) playbackSpeedMenu.addAction(action) } - + playbackSpeedMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel)) return playbackSpeedMenu } @@ -153,12 +161,25 @@ class PlayerControlsUIView: UIView { qualityMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel)) return qualityMenu } - + + func createDownloadQualityMenu() -> UIAlertController { + let qualityMenu = UIAlertController(title: "Available resolutions", message: nil, preferredStyle: ACTION_SHEET_PREFERRED_STYLE) + var availableVideoQualities = player.availableVideoQualities + // Remove Auto Quality from the Array + availableVideoQualities.remove(at: 0) + for quality in availableVideoQualities { + let action = createActionForDownload(quality) + qualityMenu.addAction(action) + } + qualityMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + return qualityMenu + } + func createActionForPlaybackSpeed(_ playbackSpeed: PlaybackSpeed) -> UIAlertAction { let action = UIAlertAction(title: playbackSpeed.label, style: .default) { [weak self] _ in self?.player.changePlaybackSpeed(playbackSpeed) } - + if playbackSpeed == .normal && self.player.currentPlaybackSpeed.rawValue == 0.0 || (playbackSpeed.rawValue == self.player.currentPlaybackSpeed.rawValue) { action.setValue(UIImage(named: "checkmark", in: bundle, compatibleWith: nil), forKey: "image") } @@ -176,7 +197,15 @@ class PlayerControlsUIView: UIView { return action } - + + func createActionForDownload(_ quality: VideoQuality) -> UIAlertAction { + let action = UIAlertAction(title: quality.resolution, style: .default, handler: { (_) in + TPStreamsDownloadManager.shared.startDownload(asset: self.player.asset!, videoQuality: quality) + }) + + return action + } + @IBAction func toggleFullScreen(_ sender: Any) { if isFullScreen { fullScreenToggleDelegate?.exitFullScreen() diff --git a/StoryboardExample/ViewController.swift b/StoryboardExample/ViewController.swift index 3fc7097..b39c451 100644 --- a/StoryboardExample/ViewController.swift +++ b/StoryboardExample/ViewController.swift @@ -40,6 +40,7 @@ class ViewController: UIViewController { .setPreferredRewindDuration(5) .setprogressBarThumbColor(.systemBlue) .setwatchedProgressTrackColor(.systemBlue) + .showDownloadOption() .build() playerViewController?.config = config