From 137495a4476285b890fb9c2f7e59770e21051082 Mon Sep 17 00:00:00 2001 From: Balamurugan Date: Wed, 4 Mar 2026 19:40:48 +0530 Subject: [PATCH 1/5] feat: Add resolution fallback support to download API - Enable optional fallback when the requested resolution is unavailable - Select the closest available resolution when fallback is enabled - Prefer the lower resolution if multiple options are equally close --- .../Database/TPStreamsDownloadManager.swift | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index f5315d0..859614b 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -95,6 +95,7 @@ public final class TPStreamsDownloadManager { assetID: String, accessToken: String? = nil, resolution: String? = nil, + allowResolutionFallback: Bool = false, metadata: [String: Any]? = nil, presentingViewController: UIViewController? = nil, completion: ((Result) -> Void)? = nil @@ -125,7 +126,13 @@ public final class TPStreamsDownloadManager { completion?(.failure(error)) case .success(let (qualities, playlistModel)): if let requestedResolution = resolution { - guard let quality = qualities.first(where: { $0.resolution == requestedResolution }) else { + let selectedQuality = self.selectQuality( + qualities, + requestedResolution: requestedResolution, + allowResolutionFallback: allowResolutionFallback + ) + + guard let quality = selectedQuality else { completion?(.failure(.resolutionNotAvailable(requestedResolution))) return } @@ -151,6 +158,37 @@ public final class TPStreamsDownloadManager { } } + private func selectQuality( + _ qualities: [VideoQuality], + requestedResolution: String, + allowResolutionFallback: Bool + ) -> VideoQuality? { + if let exactMatch = qualities.first(where: { $0.resolution == requestedResolution }) { + return exactMatch + } + + guard allowResolutionFallback, let requestedHeight = parseResolution(requestedResolution) else { + return nil + } + + return qualities.min { q1, q2 in + let h1 = parseResolution(q1.resolution) ?? 0 + let h2 = parseResolution(q2.resolution) ?? 0 + let d1 = abs(h1 - requestedHeight) + let d2 = abs(h2 - requestedHeight) + + if d1 == d2 { + return h1 < h2 // Prefer lower on tie + } + return d1 < d2 + } + } + + private func parseResolution(_ resolution: String) -> Int? { + let digits = resolution.filter { $0.isNumber } + return Int(digits) + } + private func showQualityPicker( asset: Asset, token: String?, From b9a7373b3c85aa57790bae91e87915d969cdbd00 Mon Sep 17 00:00:00 2001 From: Balamurugan Date: Wed, 4 Mar 2026 20:01:35 +0530 Subject: [PATCH 2/5] fix AI comments --- .../Database/TPStreamsDownloadManager.swift | 29 +++++++++---------- StoryboardExample/MainViewController.swift | 3 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 859614b..7127cae 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -167,26 +167,25 @@ public final class TPStreamsDownloadManager { return exactMatch } - guard allowResolutionFallback, let requestedHeight = parseResolution(requestedResolution) else { + var requestedHeight: Int = 0 + guard allowResolutionFallback, Scanner(string: requestedResolution).scanInt(&requestedHeight) else { return nil } - return qualities.min { q1, q2 in - let h1 = parseResolution(q1.resolution) ?? 0 - let h2 = parseResolution(q2.resolution) ?? 0 - let d1 = abs(h1 - requestedHeight) - let d2 = abs(h2 - requestedHeight) - - if d1 == d2 { - return h1 < h2 // Prefer lower on tie - } - return d1 < d2 + let qualitiesWithHeights: [(quality: VideoQuality, height: Int)] = qualities.compactMap { quality in + var height: Int = 0 + return Scanner(string: quality.resolution).scanInt(&height) ? (quality, height) : nil } - } - private func parseResolution(_ resolution: String) -> Int? { - let digits = resolution.filter { $0.isNumber } - return Int(digits) + return qualitiesWithHeights.min { first, second in + let diff1 = abs(first.height - requestedHeight) + let diff2 = abs(second.height - requestedHeight) + + if diff1 == diff2 { + return first.height < second.height // Prefer lower resolution on tie + } + return diff1 < diff2 + }?.quality } private func showQualityPicker( diff --git a/StoryboardExample/MainViewController.swift b/StoryboardExample/MainViewController.swift index bc0e890..78446f7 100644 --- a/StoryboardExample/MainViewController.swift +++ b/StoryboardExample/MainViewController.swift @@ -25,7 +25,8 @@ class MainViewController: UIViewController { TPStreamsDownloadManager.shared.startDownload( assetID: "BEArYFdaFbt", accessToken: "ecf6366b-c2ee-408c-9472-6ed4e4b3047e", - // resolution: "720p", + resolution: "1440p", + allowResolutionFallback: true, presentingViewController: self, ) { result in switch result { From 36200df059069eaede6a0500763933c8ea408019 Mon Sep 17 00:00:00 2001 From: Balamurugan Date: Thu, 5 Mar 2026 11:51:14 +0530 Subject: [PATCH 3/5] extract the select quality to a utils class --- .../Database/TPStreamsDownloadManager.swift | 36 ++------------- Source/Utils/VideoQualityUtils.swift | 45 +++++++++++++++++++ StoryboardExample/MainViewController.swift | 2 +- iOSPlayerSDK.xcodeproj/project.pbxproj | 4 ++ 4 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 Source/Utils/VideoQualityUtils.swift diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 7127cae..de136b1 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -126,10 +126,10 @@ public final class TPStreamsDownloadManager { completion?(.failure(error)) case .success(let (qualities, playlistModel)): if let requestedResolution = resolution { - let selectedQuality = self.selectQuality( + let selectedQuality = VideoQualityUtils.findQualityForResolution( qualities, - requestedResolution: requestedResolution, - allowResolutionFallback: allowResolutionFallback + resolution: requestedResolution, + allowFallback: allowResolutionFallback ) guard let quality = selectedQuality else { @@ -158,36 +158,6 @@ public final class TPStreamsDownloadManager { } } - private func selectQuality( - _ qualities: [VideoQuality], - requestedResolution: String, - allowResolutionFallback: Bool - ) -> VideoQuality? { - if let exactMatch = qualities.first(where: { $0.resolution == requestedResolution }) { - return exactMatch - } - - var requestedHeight: Int = 0 - guard allowResolutionFallback, Scanner(string: requestedResolution).scanInt(&requestedHeight) else { - return nil - } - - let qualitiesWithHeights: [(quality: VideoQuality, height: Int)] = qualities.compactMap { quality in - var height: Int = 0 - return Scanner(string: quality.resolution).scanInt(&height) ? (quality, height) : nil - } - - return qualitiesWithHeights.min { first, second in - let diff1 = abs(first.height - requestedHeight) - let diff2 = abs(second.height - requestedHeight) - - if diff1 == diff2 { - return first.height < second.height // Prefer lower resolution on tie - } - return diff1 < diff2 - }?.quality - } - private func showQualityPicker( asset: Asset, token: String?, diff --git a/Source/Utils/VideoQualityUtils.swift b/Source/Utils/VideoQualityUtils.swift new file mode 100644 index 0000000..b661940 --- /dev/null +++ b/Source/Utils/VideoQualityUtils.swift @@ -0,0 +1,45 @@ +// +// VideoQualityUtils.swift +// TPStreamsSDK +// +// Created by Prithuvi on 05/03/26. +// + +import Foundation + +extension VideoQuality { + var resolutionHeight: Int? { + var height: Int = 0 + return Scanner(string: resolution).scanInt(&height) ? height : nil + } +} + +public class VideoQualityUtils { + public static func findQualityForResolution( + _ qualities: [VideoQuality], + resolution: String, + allowFallback: Bool + ) -> VideoQuality? { + if let exactMatch = qualities.first(where: { $0.resolution == resolution }) { + return exactMatch + } + + var requestedHeight: Int = 0 + guard allowFallback, Scanner(string: resolution).scanInt(&requestedHeight) else { + return nil + } + + return qualities.compactMap { quality -> (quality: VideoQuality, height: Int)? in + guard let height = quality.resolutionHeight else { return nil } + return (quality, height) + }.min { first, second in + let diff1 = abs(first.height - requestedHeight) + let diff2 = abs(second.height - requestedHeight) + + if diff1 == diff2 { + return first.height < second.height + } + return diff1 < diff2 + }?.quality + } +} diff --git a/StoryboardExample/MainViewController.swift b/StoryboardExample/MainViewController.swift index 78446f7..17168e8 100644 --- a/StoryboardExample/MainViewController.swift +++ b/StoryboardExample/MainViewController.swift @@ -25,7 +25,7 @@ class MainViewController: UIViewController { TPStreamsDownloadManager.shared.startDownload( assetID: "BEArYFdaFbt", accessToken: "ecf6366b-c2ee-408c-9472-6ed4e4b3047e", - resolution: "1440p", + resolution: "140p", allowResolutionFallback: true, presentingViewController: self, ) { result in diff --git a/iOSPlayerSDK.xcodeproj/project.pbxproj b/iOSPlayerSDK.xcodeproj/project.pbxproj index 6d284c1..6ad96e6 100644 --- a/iOSPlayerSDK.xcodeproj/project.pbxproj +++ b/iOSPlayerSDK.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 03CE75022A7A337B00B84304 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CE75012A7A337B00B84304 /* UIView.swift */; }; 03D7F4252C21C64D00DF3597 /* LiveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D7F4242C21C64D00DF3597 /* LiveIndicatorView.swift */; }; 03D7F4282C22C4F900DF3597 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D7F4272C22C4F900DF3597 /* PlaybackSpeed.swift */; }; + 2D3CE81F2F5957EE0011CAB7 /* VideoQualityUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3CE81E2F5957EE0011CAB7 /* VideoQualityUtils.swift */; }; 2D46EC6A2EE856BB008B559A /* M3U8Kit in Frameworks */ = {isa = PBXBuildFile; productRef = 2D46EC692EE856BB008B559A /* M3U8Kit */; }; 2DD12A212D4CF75400272433 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D92E48F22CB0226A00B1FAC3 /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 70D42E0B2E6075F3002AC32C /* AnyRealmValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70D42E0A2E6075F3002AC32C /* AnyRealmValue.swift */; }; @@ -201,6 +202,7 @@ 03CE75012A7A337B00B84304 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 03D7F4242C21C64D00DF3597 /* LiveIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveIndicatorView.swift; sourceTree = ""; }; 03D7F4272C22C4F900DF3597 /* PlaybackSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackSpeed.swift; sourceTree = ""; }; + 2D3CE81E2F5957EE0011CAB7 /* VideoQualityUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoQualityUtils.swift; sourceTree = ""; }; 70D42E0A2E6075F3002AC32C /* AnyRealmValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyRealmValue.swift; sourceTree = ""; }; 8E6389BB2A2724D000306FA4 /* TPStreamPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TPStreamPlayerView.swift; sourceTree = ""; }; 8E6389C12A27277D00306FA4 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -373,6 +375,7 @@ isa = PBXGroup; children = ( 03CE74FF2A7946A800B84304 /* Time.swift */, + 2D3CE81E2F5957EE0011CAB7 /* VideoQualityUtils.swift */, 0367F6F42B861CFC0000922D /* Sentry.swift */, D960A8FA2CEDEE7C003B0B04 /* M3U8Parser.swift */, ); @@ -799,6 +802,7 @@ 03CA2D372A30A8E500532549 /* PlayerProgressBar.swift in Sources */, 0374E3782C1B26EC00CE9CF2 /* NoticeView.swift in Sources */, 0377C40E2A2B1C7300F7E58F /* BaseAPI.swift in Sources */, + 2D3CE81F2F5957EE0011CAB7 /* VideoQualityUtils.swift in Sources */, 03CA2D312A2F757D00532549 /* TimeIndicatorView.swift in Sources */, 035351A22A2F49E3001E38F3 /* MediaControlsView.swift in Sources */, D92E48B62CAFBD6F00B1FAC3 /* ObjectManager.swift in Sources */, From 131cfd9484b6844a7e7dc46b69a63dd978a4cda5 Mon Sep 17 00:00:00 2001 From: Balamurugan Date: Thu, 5 Mar 2026 14:43:27 +0530 Subject: [PATCH 4/5] refactor: rename method name to be explicit --- Source/Database/TPStreamsDownloadManager.swift | 6 +++--- Source/Utils/VideoQualityUtils.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index de136b1..97773f3 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -126,9 +126,9 @@ public final class TPStreamsDownloadManager { completion?(.failure(error)) case .success(let (qualities, playlistModel)): if let requestedResolution = resolution { - let selectedQuality = VideoQualityUtils.findQualityForResolution( - qualities, - resolution: requestedResolution, + let selectedQuality = VideoQualityUtils.closestQuality( + in: qualities, + for: requestedResolution, allowFallback: allowResolutionFallback ) diff --git a/Source/Utils/VideoQualityUtils.swift b/Source/Utils/VideoQualityUtils.swift index b661940..f86abc7 100644 --- a/Source/Utils/VideoQualityUtils.swift +++ b/Source/Utils/VideoQualityUtils.swift @@ -15,9 +15,9 @@ extension VideoQuality { } public class VideoQualityUtils { - public static func findQualityForResolution( - _ qualities: [VideoQuality], - resolution: String, + public static func closestQuality( + in qualities: [VideoQuality], + for resolution: String, allowFallback: Bool ) -> VideoQuality? { if let exactMatch = qualities.first(where: { $0.resolution == resolution }) { From 000efdca7c733aaf43b3a4e70587714d192ddc2a Mon Sep 17 00:00:00 2001 From: Balamurugan Date: Thu, 5 Mar 2026 14:52:46 +0530 Subject: [PATCH 5/5] update method name --- Source/Database/TPStreamsDownloadManager.swift | 2 +- Source/Utils/VideoQualityUtils.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Database/TPStreamsDownloadManager.swift b/Source/Database/TPStreamsDownloadManager.swift index 97773f3..dcbc20e 100644 --- a/Source/Database/TPStreamsDownloadManager.swift +++ b/Source/Database/TPStreamsDownloadManager.swift @@ -126,7 +126,7 @@ public final class TPStreamsDownloadManager { completion?(.failure(error)) case .success(let (qualities, playlistModel)): if let requestedResolution = resolution { - let selectedQuality = VideoQualityUtils.closestQuality( + let selectedQuality = VideoQualityUtils.selectClosestQuality( in: qualities, for: requestedResolution, allowFallback: allowResolutionFallback diff --git a/Source/Utils/VideoQualityUtils.swift b/Source/Utils/VideoQualityUtils.swift index f86abc7..5bde6fa 100644 --- a/Source/Utils/VideoQualityUtils.swift +++ b/Source/Utils/VideoQualityUtils.swift @@ -15,7 +15,7 @@ extension VideoQuality { } public class VideoQualityUtils { - public static func closestQuality( + public static func selectClosestQuality( in qualities: [VideoQuality], for resolution: String, allowFallback: Bool