From a38aa5eddee918930e99dabef80b8fbf9e8e1423 Mon Sep 17 00:00:00 2001 From: Ruben Garcia Date: Wed, 4 Feb 2026 20:25:15 -0500 Subject: [PATCH 01/17] Add ordered params support and fix url encoding for Uplynk signature validation --- .../UplynkSSAIConfiguration+Extensions.swift | 15 ++++++++++++++- Code/Uplynk/Source/UplynkSSAIConfiguration.swift | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift index fab531df..a962b2a8 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift @@ -14,6 +14,19 @@ extension UplynkSSAIConfiguration { } var urlParameters: String { + if let orderedParams = orderedPreplayParameters, !orderedParams.isEmpty { + // Define strict allowed characters for query values + // We MUST encode '%' (to preserve pre-encoded values), '&', '=', '+', and others that alter URL structure. + var allowed = CharacterSet.urlQueryAllowed + allowed.remove(charactersIn: "&+=?%,") + + let joinedParameters = orderedParams.map { (key, value) in + let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value + return "\(key)=\(encodedValue)" + }.joined(separator: "&") + return "&\(joinedParameters)" + } + guard !preplayParameters.isEmpty else { return "" } @@ -31,7 +44,7 @@ extension UplynkSSAIConfiguration { var pingParameters: String { let pingFeature = pingFeature if pingFeature == .noPing { - return "&ad.pingc=0" + return "" } else { return "&ad.pingc=1&ad.pingf=\(pingFeature.rawValue)" } diff --git a/Code/Uplynk/Source/UplynkSSAIConfiguration.swift b/Code/Uplynk/Source/UplynkSSAIConfiguration.swift index 62cd2dd6..7a21c797 100644 --- a/Code/Uplynk/Source/UplynkSSAIConfiguration.swift +++ b/Code/Uplynk/Source/UplynkSSAIConfiguration.swift @@ -27,6 +27,7 @@ public class UplynkSSAIConfiguration: CustomServerSideAdInsertionConfiguration { public let id: ID public let prefix: String? public let preplayParameters: [String: String] + public let orderedPreplayParameters: [(String, String)]? public let assetType: AssetType public let contentProtected: Bool public let assetInfo: Bool @@ -38,6 +39,7 @@ public class UplynkSSAIConfiguration: CustomServerSideAdInsertionConfiguration { assetType: AssetType, prefix: String? = nil, preplayParameters: [String: String] = [:], + orderedPreplayParameters: [(String, String)]? = nil, contentProtected: Bool = false, assetInfo: Bool = false, uplynkPingConfiguration: UplynkPingConfiguration = .init(), @@ -47,6 +49,7 @@ public class UplynkSSAIConfiguration: CustomServerSideAdInsertionConfiguration { self.assetType = assetType self.prefix = prefix self.preplayParameters = preplayParameters + self.orderedPreplayParameters = orderedPreplayParameters self.contentProtected = contentProtected self.assetInfo = assetInfo self.pingConfiguration = uplynkPingConfiguration From 21fe39ca937504486980133247699fe2c44458fc Mon Sep 17 00:00:00 2001 From: Ceyhun Ozgoc Date: Fri, 6 Feb 2026 11:38:00 +0100 Subject: [PATCH 02/17] Use correct URL parameter for the ping API https://docs.uplynk.com/reference/ping-v2 --- .../Source/Internal/UplynkSSAIConfiguration+Extensions.swift | 2 +- .../Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift index a962b2a8..aa80ca3a 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift @@ -46,7 +46,7 @@ extension UplynkSSAIConfiguration { if pingFeature == .noPing { return "" } else { - return "&ad.pingc=1&ad.pingf=\(pingFeature.rawValue)" + return "&ad.cping=1&ad.pingf=\(pingFeature.rawValue)" } } diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 542b24e2..ef1e0464 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -63,8 +63,8 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { let prefix = "https://content.uplynk.com" let assetID = "a123" - let validNoPingQueryParameter = "ad.pingc=0" - let validPingQueryParameter = "ad.pingc=1&ad.pingf=\(pingFeature.rawValue)" + let validNoPingQueryParameter = "ad.cping=0" + let validPingQueryParameter = "ad.cping=1&ad.pingf=\(pingFeature.rawValue)" let pingConfiguration = switch pingFeature { case .noPing: From b7cca09694abe7844b613ac9a026ccd34b85ab82 Mon Sep 17 00:00:00 2001 From: Ceyhun Ozgoc Date: Fri, 6 Feb 2026 11:46:28 +0100 Subject: [PATCH 03/17] Update preplay docs --- Code/Uplynk/docs/preplay.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Code/Uplynk/docs/preplay.md b/Code/Uplynk/docs/preplay.md index 28b87cfc..c3d18649 100644 --- a/Code/Uplynk/docs/preplay.md +++ b/Code/Uplynk/docs/preplay.md @@ -33,6 +33,8 @@ We start by creating an `UplynkSSAIConfiguration` object that describes how to c - `preplayParameters`: The `preplayParameters` object should have string-key-string-value combinations, which will be used as query parameters for the Preplay API call. Nested objects are not supported. +- `orderedPreplayParameters`: The `orderedPreplayParameters` object should have string-key-string-value combinations, which will be used as query parameters for the Preplay API call. Nested objects are not supported. Unlike `preplayParameters`, `orderedPreplayParameters` preserves the order of parameters while making a request, which is neccessary to prevent unsigned parameters that could break signature validation. + - `contentProtected`: Boolean value which will internally set any necessary content-protection information. No content-protection details have to be specified by the customer. - **A Preplay request must include all parameters defined within the playback request, hence these parameters must be included in the THEOplayer source**. This request must also include a digital signature if the 'Require a token for playback' option is enabled in the back-end on the corresponding live channel. (See also : [Signing a Playback URL Tutorial](https://docs.uplynk.com/docs/sign-playback-url)) @@ -62,7 +64,7 @@ Ad specific parameters can be passed in the `preplayParameters` argument of the assetType: ..., prefix: ..., preplayParameters: [ - // Parameters here should specify the necessary ad parameters for the Preplay API + // Parameters here should specify the necessary ad parameters for the Preplay API. Use `orderedPreplayParameters` instead to pass these in the order given. "ad.param1": "param_val1", "ad.param2": "param_val2" ], From 43ef5ad7e6cbf8e839f8c3f4a1fa3e8ecabd0e08 Mon Sep 17 00:00:00 2001 From: Ceyhun Ozgoc Date: Fri, 6 Feb 2026 11:58:37 +0100 Subject: [PATCH 04/17] Update CHANGELOG --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ea0749..97e1b7f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Uplynk + - Added `orderedPreplayParameters` to `UplynkSSAIConfiguration`, which can be used to maintain the order of preplay parameters when making a request. + +### Fixed + +- Uplynk + - Improved URL encoding for characters such as `%`, `&`, `=`, `+` and `,` to preserve pre-encoded values and prevent server-side double decoding issues. + +### Changed + +- Uplynk + - When ping feature is not configured, the player will now send an empty string instead of `"&ad.pingc=0"` to prevent unsigned parameters that could break signature validation. + ## [10.8.0.1] - 2026-01-20 ### Fixed From 8188d856981799ae76d2db26a078f191bc7a6146 Mon Sep 17 00:00:00 2001 From: Ceyhun Ozgoc Date: Fri, 6 Feb 2026 12:11:34 +0100 Subject: [PATCH 05/17] Update tests --- Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index ef1e0464..8c1d4277 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -63,7 +63,7 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { let prefix = "https://content.uplynk.com" let assetID = "a123" - let validNoPingQueryParameter = "ad.cping=0" + let validNoPingQueryParameter = "" let validPingQueryParameter = "ad.cping=1&ad.pingf=\(pingFeature.rawValue)" let pingConfiguration = switch pingFeature { From 14853edda2ffb2627735f046c2ed43d8e4965251 Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 11:52:59 +0200 Subject: [PATCH 06/17] Fix tests for slate --- Code/Uplynk/Tests/Mocks/MockAdBreak.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Code/Uplynk/Tests/Mocks/MockAdBreak.swift b/Code/Uplynk/Tests/Mocks/MockAdBreak.swift index 75f075e2..86738737 100644 --- a/Code/Uplynk/Tests/Mocks/MockAdBreak.swift +++ b/Code/Uplynk/Tests/Mocks/MockAdBreak.swift @@ -9,6 +9,7 @@ import Foundation import THEOplayerSDK struct MockAdBreak: AdBreak { + let id: String? = UUID().uuidString var ads: [Ad] = [] var maxDuration: Int = 0 var maxRemainingDuration: Double = 0 @@ -18,6 +19,7 @@ struct MockAdBreak: AdBreak { } struct MockAd: Ad { + let isSlate = false var adBreak: AdBreak = MockAdBreak() var companions: [CompanionAd] = [] var type: String = "" From c811bd9b9c22d160c400302acf1c9135f2b9b7ca Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 11:57:42 +0200 Subject: [PATCH 07/17] test empty query parameters --- ...lynkSSAIConfigurationURLBuilderTests.swift | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 8c1d4277..28b1ee4b 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -139,4 +139,41 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { XCTAssertTrue(builtPreplayURL.contains(validPrePlayParameters) || builtPreplayURL.contains(anotherValidPrePlayParameters)) } + + /// Makes sure Uplynk URLs do not contain the sequence "?&" + /// FIFA raised an issue that the Uplynk backend does not support those kind of URLs. + /// See [THEOSD-16266] [OPTI-1771] + func testEmptyQueryParameter() { + let hasEmptyParameter: (String)->Bool = { $0.contains("?&") } + let faultyURL = "https://content.uplynk.com/preplay/ID?&sig=signature" + XCTAssert(hasEmptyParameter(faultyURL)) + + let assetID = "a123" + let vodBuilder = UplynkSSAIURLBuilder( + ssaiConfiguration: UplynkSSAIConfiguration( + id: .asset(ids: [assetID]), + assetType: .asset + ) + ) + let liveBuilder = UplynkSSAIURLBuilder( + ssaiConfiguration: UplynkSSAIConfiguration( + id: .asset(ids: [assetID]), + assetType: .channel + ) + ) + + let preplayVod = vodBuilder.buildPreplayVODURL() + let preplayFaultyLive = vodBuilder.buildPreplayLiveURL() + print(preplayVod) + XCTAssertFalse(hasEmptyParameter(preplayVod)) + print(preplayFaultyLive) + XCTAssertFalse(hasEmptyParameter(preplayFaultyLive)) + + let preplayFaultyVod = liveBuilder.buildPreplayVODURL() + let preplayLive = liveBuilder.buildPreplayLiveURL() + print(preplayFaultyVod) + XCTAssertFalse(hasEmptyParameter(preplayFaultyVod)) + print(preplayLive) + XCTAssertFalse(hasEmptyParameter(preplayLive)) + } } From e1bc8c97637ba1d4e77405cc54b307722752679e Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 14:14:16 +0200 Subject: [PATCH 08/17] Add URL encoding tests --- ...lynkSSAIConfigurationURLBuilderTests.swift | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 28b1ee4b..64f86941 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -176,4 +176,67 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { print(preplayLive) XCTAssertFalse(hasEmptyParameter(preplayLive)) } + + func testPreplayArray() { + let normalParameter = [("keyA", "valueA")] + let specialValue = "?&+=,%" + let specialEncodedValue = "%3F%26%2B%3D%2C%25" + let specialParameter = [("special",specialValue)] + let specialEncodedParameter = ["special": specialEncodedValue] + let mixedParameters = [("keyA", "valueA"), ("special",specialValue)] + let mixedEncodedParamteres = ["keyA": "valueA", "special": specialEncodedValue] + + let configs: [TestConfig] = [ + TestConfig(assetType: .asset, preplayArray: normalParameter, expectedParams: ["keyA": "valueA"]), + TestConfig(assetType: .asset, preplayArray: specialParameter, expectedParams: specialEncodedParameter), + TestConfig(assetType: .asset, preplayArray: mixedParameters, expectedParams: mixedEncodedParamteres), + TestConfig(assetType: .channel, preplayArray: normalParameter, expectedParams: ["keyA": "valueA"]), + TestConfig(assetType: .channel, preplayArray: specialParameter, expectedParams: specialEncodedParameter), + TestConfig(assetType: .channel, preplayArray: mixedParameters, expectedParams: mixedEncodedParamteres) + ] + + for config in configs { + config.assertUrlContainsPreplayParams() + } + + let emptyParams = TestConfig(assetType: .asset, preplayArray: [], expectedParams: [:]) + XCTAssertFalse(emptyParams.url.contains("&"), "A config without params should not contain an `&` character") + } +} + +struct TestConfig { + let assetType: UplynkSSAIConfiguration.AssetType + let preplayArray: [(String,String)] + let expectedParams: [String:String] + + var url: String { + let assetID = UplynkSSAIConfiguration.ID.asset(ids: ["a123"]) + let config = UplynkSSAIConfiguration(id: assetID, assetType: assetType, orderedPreplayParameters: preplayArray) + let builder = UplynkSSAIURLBuilder(ssaiConfiguration: config) + switch assetType { + case .asset: return builder.buildPreplayVODURL() + case .channel: return builder.buildPreplayLiveURL() + } + } + + func assertUrlContainsPreplayParams() { + let url = self.url + for (key, value) in expectedParams { + let exptectation = "\(key)=\(value)" + if !url.contains(exptectation) { + XCTFail("Generated url (\(url)) does not contain \(exptectation)") + } + } + guard let parsedUrl = URLComponents(string: url) else { + return XCTFail("Could not parse the generated URL \(url)") + } + let parsedQueryItems = parsedUrl.queryItems ?? [] + for (key, value) in preplayArray { + guard parsedQueryItems.contains(where: { item in + item.name == key && item.value == value + }) else { + return XCTFail("Generated URL does not contain preplay param \(key)=\(value)") + } + } + } } From ae09553c98bb75d6d83ece70d7b47e22604d0cbe Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 14:51:09 +0200 Subject: [PATCH 09/17] Add preplay param ordering test --- ...lynkSSAIConfigurationURLBuilderTests.swift | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 64f86941..8872ad82 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -182,15 +182,15 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { let specialValue = "?&+=,%" let specialEncodedValue = "%3F%26%2B%3D%2C%25" let specialParameter = [("special",specialValue)] - let specialEncodedParameter = ["special": specialEncodedValue] + let specialEncodedParameter = [("special", specialEncodedValue)] let mixedParameters = [("keyA", "valueA"), ("special",specialValue)] - let mixedEncodedParamteres = ["keyA": "valueA", "special": specialEncodedValue] + let mixedEncodedParamteres = [("keyA", "valueA"), ("special", specialEncodedValue)] let configs: [TestConfig] = [ - TestConfig(assetType: .asset, preplayArray: normalParameter, expectedParams: ["keyA": "valueA"]), + TestConfig(assetType: .asset, preplayArray: normalParameter, expectedParams: [("keyA", "valueA")]), TestConfig(assetType: .asset, preplayArray: specialParameter, expectedParams: specialEncodedParameter), TestConfig(assetType: .asset, preplayArray: mixedParameters, expectedParams: mixedEncodedParamteres), - TestConfig(assetType: .channel, preplayArray: normalParameter, expectedParams: ["keyA": "valueA"]), + TestConfig(assetType: .channel, preplayArray: normalParameter, expectedParams: [("keyA", "valueA")]), TestConfig(assetType: .channel, preplayArray: specialParameter, expectedParams: specialEncodedParameter), TestConfig(assetType: .channel, preplayArray: mixedParameters, expectedParams: mixedEncodedParamteres) ] @@ -199,15 +199,36 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { config.assertUrlContainsPreplayParams() } - let emptyParams = TestConfig(assetType: .asset, preplayArray: [], expectedParams: [:]) + let emptyParams = TestConfig(assetType: .asset, preplayArray: [], expectedParams: []) XCTAssertFalse(emptyParams.url.contains("&"), "A config without params should not contain an `&` character") } + + func testPreplayParameterOrdering() { + let preplayParams = (0..<20).map { index in + ("key\(index)", "value\(index)") + } + let vod = TestConfig(assetType: .asset, preplayArray: preplayParams, expectedParams: []).url + + var reversedOrder = Array(preplayParams.reversed()) + var lastIndex = vod.startIndex + while let item = reversedOrder.popLast() { + if let range = vod.range(of: item.0) { + let newIndex = range.upperBound + if newIndex < lastIndex { + XCTFail("Parameter \(item.0) is out of order. ExpectedOrder: \(preplayParams.map(\.0)), Actual: \(vod)") + } + lastIndex = newIndex + } else { + XCTFail("Missing parameter: \(item)") + } + } + } } struct TestConfig { let assetType: UplynkSSAIConfiguration.AssetType let preplayArray: [(String,String)] - let expectedParams: [String:String] + let expectedParams: [(String,String)] var url: String { let assetID = UplynkSSAIConfiguration.ID.asset(ids: ["a123"]) From 2b230e60a5444172a55e06d9728f6983a2dfba7a Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 14:51:58 +0200 Subject: [PATCH 10/17] Simplify ordered preplay params for Uplynk --- .../UplynkSSAIConfiguration+Extensions.swift | 36 +++++++++---------- .../Source/UplynkSSAIConfiguration.swift | 31 +++++++++++++--- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift index aa80ca3a..ed783c66 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift @@ -14,27 +14,11 @@ extension UplynkSSAIConfiguration { } var urlParameters: String { - if let orderedParams = orderedPreplayParameters, !orderedParams.isEmpty { - // Define strict allowed characters for query values - // We MUST encode '%' (to preserve pre-encoded values), '&', '=', '+', and others that alter URL structure. - var allowed = CharacterSet.urlQueryAllowed - allowed.remove(charactersIn: "&+=?%,") - - let joinedParameters = orderedParams.map { (key, value) in - let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowed) ?? value - return "\(key)=\(encodedValue)" - }.joined(separator: "&") - return "&\(joinedParameters)" - } - - guard !preplayParameters.isEmpty else { - return "" - } - let joinedParameters = preplayParameters.map { - "\($0.key)=\($0.value)" - }.joined(separator: "&") + guard !orderedPreplayParameters.isEmpty else { return "" } - return "&\(joinedParameters)" + var components = URLComponents() + components.percentEncodedQueryItems = orderedPreplayParameters.map(URLQueryItem.encodedForUplynk) + return "&\(components.percentEncodedQuery!)" } var pingFeature: UplynkPingFeature { @@ -89,3 +73,15 @@ extension UplynkSSAIConfiguration { } } } + +extension CharacterSet { + fileprivate static let uplynkUrlQueryValueAllowed = CharacterSet.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "&+=?,")) +} +extension URLQueryItem { + fileprivate static func encodedForUplynk(name: String, value: String) -> URLQueryItem { + URLQueryItem( + name: name, + value: value.addingPercentEncoding(withAllowedCharacters: .uplynkUrlQueryValueAllowed) + ) + } +} diff --git a/Code/Uplynk/Source/UplynkSSAIConfiguration.swift b/Code/Uplynk/Source/UplynkSSAIConfiguration.swift index 7a21c797..2a2c0aed 100644 --- a/Code/Uplynk/Source/UplynkSSAIConfiguration.swift +++ b/Code/Uplynk/Source/UplynkSSAIConfiguration.swift @@ -26,20 +26,19 @@ public class UplynkSSAIConfiguration: CustomServerSideAdInsertionConfiguration { public let id: ID public let prefix: String? - public let preplayParameters: [String: String] - public let orderedPreplayParameters: [(String, String)]? + public let orderedPreplayParameters: [(String, String)] public let assetType: AssetType public let contentProtected: Bool public let assetInfo: Bool public let pingConfiguration: UplynkPingConfiguration public let playbackURLParameters: [(String, String)] + @available(*, deprecated, message: "Use the initializer with orderedPreplayParameters instead.") public init( id: ID, assetType: AssetType, prefix: String? = nil, preplayParameters: [String: String] = [:], - orderedPreplayParameters: [(String, String)]? = nil, contentProtected: Bool = false, assetInfo: Bool = false, uplynkPingConfiguration: UplynkPingConfiguration = .init(), @@ -48,11 +47,35 @@ public class UplynkSSAIConfiguration: CustomServerSideAdInsertionConfiguration { self.id = id self.assetType = assetType self.prefix = prefix - self.preplayParameters = preplayParameters + self.orderedPreplayParameters = Array(preplayParameters) + self.contentProtected = contentProtected + self.assetInfo = assetInfo + self.pingConfiguration = uplynkPingConfiguration + self.playbackURLParameters = playbackURLParameters + } + + public init( + id: ID, + assetType: AssetType, + orderedPreplayParameters: [(String, String)], + prefix: String? = nil, + contentProtected: Bool = false, + assetInfo: Bool = false, + uplynkPingConfiguration: UplynkPingConfiguration = .init(), + playbackURLParameters: [(String, String)] = [] + ) { + self.id = id + self.assetType = assetType self.orderedPreplayParameters = orderedPreplayParameters + self.prefix = prefix self.contentProtected = contentProtected self.assetInfo = assetInfo self.pingConfiguration = uplynkPingConfiguration self.playbackURLParameters = playbackURLParameters } + + +// var preplayParameters: [String: String] { +// Dictionary(orderedPreplayParameters) { left, right in left } +// } } From b2dedb77ffde7090e9ba28aa5e59c832a78700da Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 15:08:36 +0200 Subject: [PATCH 11/17] fix ping test --- Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 8872ad82..ee659e69 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -63,7 +63,6 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { let prefix = "https://content.uplynk.com" let assetID = "a123" - let validNoPingQueryParameter = "" let validPingQueryParameter = "ad.cping=1&ad.pingf=\(pingFeature.rawValue)" let pingConfiguration = switch pingFeature { @@ -89,7 +88,7 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { let builtPreplayURL = UplynkSSAIURLBuilder(ssaiConfiguration: configurationWithAssetID).buildPreplayVODURL() switch (pingFeature) { case .noPing: - XCTAssertTrue(builtPreplayURL.contains(validNoPingQueryParameter)) + XCTAssertFalse(builtPreplayURL.contains(validPingQueryParameter), "built PreplayURL should not contain the ping query parameter \(validPingQueryParameter). url: \(builtPreplayURL)") default: XCTAssertTrue(builtPreplayURL.contains(validPingQueryParameter)) From 5b92022410a9189fba5f945ecc1004f4e034ffd4 Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 16:25:13 +0200 Subject: [PATCH 12/17] Add query param tests --- ...lynkSSAIConfigurationURLBuilderTests.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index ee659e69..60a6acc3 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -222,6 +222,36 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { } } } + + func testUrlWithDrmAndPing() { + let assetID = UplynkSSAIConfiguration.ID.asset(ids: ["a123"]) + let extraParams = [ + ("a", "1"), + ("b", "2") + ] + let drmAndPingConfig = UplynkSSAIConfiguration( + id: assetID, + assetType: .asset, + orderedPreplayParameters: extraParams, + contentProtected: true, + uplynkPingConfiguration: .init(adImpressions: true, freeWheelVideoViews: true, linearAdData: true) + ) + XCTAssertEqual( + UplynkSSAIURLBuilder(ssaiConfiguration: drmAndPingConfig).buildPreplayVODURL(), + "https://content.uplynk.com/preplay/a123.json?v=2&manifest=m3u8&rmt=fps&ad.cping=1&ad.pingf=3&a=1&b=2" + ) + + let drmConfig = UplynkSSAIConfiguration( + id: assetID, + assetType: .asset, + orderedPreplayParameters: extraParams, + contentProtected: true + ) + XCTAssertEqual( + UplynkSSAIURLBuilder(ssaiConfiguration: drmConfig).buildPreplayVODURL(), + "https://content.uplynk.com/preplay/a123.json?v=2&manifest=m3u8&rmt=fps&a=1&b=2" + ) + } } struct TestConfig { From 5073c4ca2f2f20876c632ef4166bdb47766f8dfc Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 16:26:36 +0200 Subject: [PATCH 13/17] Simplify query string building --- .../UplynkSSAIConfiguration+Extensions.swift | 40 ++++++------------- .../Internal/UplynkSSAIURLBuilder.swift | 30 +++++++++++--- .../Source/UplynkSSAIConfiguration.swift | 8 ++-- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift index ed783c66..7944ebc6 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift @@ -9,28 +9,26 @@ import Foundation extension UplynkSSAIConfiguration { - var drmParameters: String { - contentProtected ? "&manifest=m3u8&rmt=fps" : "" - } - - var urlParameters: String { - guard !orderedPreplayParameters.isEmpty else { return "" } - - var components = URLComponents() - components.percentEncodedQueryItems = orderedPreplayParameters.map(URLQueryItem.encodedForUplynk) - return "&\(components.percentEncodedQuery!)" + var drmParameters: [URLQueryItem] { + guard contentProtected else {return []} + return [ + URLQueryItem(name: "manifest", value: "m3u8"), + URLQueryItem(name: "rmt", value: "fps") + ] } var pingFeature: UplynkPingFeature { UplynkPingFeature(ssaiConfiguration: self) } - var pingParameters: String { + var pingParameters: [URLQueryItem] { let pingFeature = pingFeature - if pingFeature == .noPing { - return "" - } else { - return "&ad.cping=1&ad.pingf=\(pingFeature.rawValue)" + if pingFeature == .noPing { return [] } + else { + return [ + URLQueryItem(name: "ad.cping", value: "1"), + URLQueryItem(name: "ad.pingf", value: pingFeature.rawValue.description) + ] } } @@ -73,15 +71,3 @@ extension UplynkSSAIConfiguration { } } } - -extension CharacterSet { - fileprivate static let uplynkUrlQueryValueAllowed = CharacterSet.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "&+=?,")) -} -extension URLQueryItem { - fileprivate static func encodedForUplynk(name: String, value: String) -> URLQueryItem { - URLQueryItem( - name: name, - value: value.addingPercentEncoding(withAllowedCharacters: .uplynkUrlQueryValueAllowed) - ) - } -} diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift b/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift index 7798a5da..bd59b6a7 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift @@ -9,6 +9,7 @@ import Foundation class UplynkSSAIURLBuilder { private static let DEFAULT_PREFIX: String = "https://content.uplynk.com" + private static let versionQueryItem = CollectionOfOne(URLQueryItem(name: "v", value: "2")) private let ssaiConfiguration: UplynkSSAIConfiguration @@ -22,17 +23,24 @@ class UplynkSSAIURLBuilder { private var urlAssetType: String { ssaiConfiguration.urlAssetType } private var urlAssetID: String { ssaiConfiguration.urlAssetID } - private var drmParameters: String { ssaiConfiguration.drmParameters } - private var pingParameters: String { ssaiConfiguration.pingParameters } - private var urlParameters: String { ssaiConfiguration.urlParameters } + private var drmParameters: [URLQueryItem] { ssaiConfiguration.drmParameters } + private var pingParameters: [URLQueryItem] { ssaiConfiguration.pingParameters } + private var urlParameters: [URLQueryItem] { ssaiConfiguration.orderedPreplayParameters.map(URLQueryItem.init) } private var id: UplynkSSAIConfiguration.ID { ssaiConfiguration.id } + var queryString: String { + let items = Self.versionQueryItem + drmParameters + pingParameters + urlParameters + var urlBuilder = URLComponents() + urlBuilder.percentEncodedQueryItems = items.map(\.encodedForUplynk) + return urlBuilder.percentEncodedQuery! + } + func buildPreplayVODURL() -> String { - return "\(prefix)/preplay/\(urlAssetID)?v=2\(drmParameters)\(pingParameters)\(urlParameters)" + return "\(prefix)/preplay/\(urlAssetID)?\(queryString)" } func buildPreplayLiveURL() -> String { - return "\(prefix)/preplay/\(urlAssetType)/\(urlAssetID)?v=2\(drmParameters)\(pingParameters)\(urlParameters)" + return "\(prefix)/preplay/\(urlAssetType)/\(urlAssetID)?v=2\(queryString)" } func buildAssetInfoURLs( @@ -81,3 +89,15 @@ class UplynkSSAIURLBuilder { "\(prefix)/session/ping/\(sessionID).json?v=3&pt=\(currentTimeSeconds)" } } + +extension CharacterSet { + fileprivate static let uplynkUrlQueryValueAllowed = CharacterSet.urlQueryAllowed.subtracting(CharacterSet(charactersIn: "&+=?,")) +} +extension URLQueryItem { + fileprivate var encodedForUplynk: URLQueryItem { + URLQueryItem( + name: name, + value: value?.addingPercentEncoding(withAllowedCharacters: .uplynkUrlQueryValueAllowed) + ) + } +} diff --git a/Code/Uplynk/Source/UplynkSSAIConfiguration.swift b/Code/Uplynk/Source/UplynkSSAIConfiguration.swift index 2a2c0aed..4e93c13a 100644 --- a/Code/Uplynk/Source/UplynkSSAIConfiguration.swift +++ b/Code/Uplynk/Source/UplynkSSAIConfiguration.swift @@ -74,8 +74,8 @@ public class UplynkSSAIConfiguration: CustomServerSideAdInsertionConfiguration { self.playbackURLParameters = playbackURLParameters } - -// var preplayParameters: [String: String] { -// Dictionary(orderedPreplayParameters) { left, right in left } -// } + @available(*, deprecated, renamed: "orderedPreplayParameters", message: "Passing preplayParameters as a dictionary is no longer supported. Use orderedPreplayParameters instead.") + var preplayParameters: [String: String] { + Dictionary(orderedPreplayParameters) { left, right in left } + } } From ef808d8bf932dcfe95a191d039630dcf89a610d3 Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Tue, 31 Mar 2026 16:59:38 +0200 Subject: [PATCH 14/17] simplify preplayArray test --- ...lynkSSAIConfigurationURLBuilderTests.swift | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 60a6acc3..6be0194e 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -177,20 +177,18 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { } func testPreplayArray() { - let normalParameter = [("keyA", "valueA")] - let specialValue = "?&+=,%" - let specialEncodedValue = "%3F%26%2B%3D%2C%25" - let specialParameter = [("special",specialValue)] - let specialEncodedParameter = [("special", specialEncodedValue)] - let mixedParameters = [("keyA", "valueA"), ("special",specialValue)] - let mixedEncodedParamteres = [("keyA", "valueA"), ("special", specialEncodedValue)] + let normalParameter = ("keyA", "valueA") + let specialParameter = ("special","?&+=,%") + let specialEncodedParameter = (specialParameter.0, "%3F%26%2B%3D%2C%25") + let mixedParameters = [normalParameter, specialParameter] + let mixedEncodedParamteres = [normalParameter, specialEncodedParameter] let configs: [TestConfig] = [ - TestConfig(assetType: .asset, preplayArray: normalParameter, expectedParams: [("keyA", "valueA")]), - TestConfig(assetType: .asset, preplayArray: specialParameter, expectedParams: specialEncodedParameter), + TestConfig(assetType: .asset, preplayArray: [normalParameter], expectedParams: [normalParameter]), + TestConfig(assetType: .asset, preplayArray: [specialParameter], expectedParams: [specialEncodedParameter]), TestConfig(assetType: .asset, preplayArray: mixedParameters, expectedParams: mixedEncodedParamteres), - TestConfig(assetType: .channel, preplayArray: normalParameter, expectedParams: [("keyA", "valueA")]), - TestConfig(assetType: .channel, preplayArray: specialParameter, expectedParams: specialEncodedParameter), + TestConfig(assetType: .channel, preplayArray: [normalParameter], expectedParams: [normalParameter]), + TestConfig(assetType: .channel, preplayArray: [specialParameter], expectedParams: [specialEncodedParameter]), TestConfig(assetType: .channel, preplayArray: mixedParameters, expectedParams: mixedEncodedParamteres) ] From 4c1a72fd45c25b4ce47ca139ef7ff9ca94e2987b Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Wed, 1 Apr 2026 10:46:06 +0200 Subject: [PATCH 15/17] Use better naming for preplay parameters --- Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift b/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift index bd59b6a7..df43dc9f 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift @@ -25,11 +25,11 @@ class UplynkSSAIURLBuilder { private var urlAssetID: String { ssaiConfiguration.urlAssetID } private var drmParameters: [URLQueryItem] { ssaiConfiguration.drmParameters } private var pingParameters: [URLQueryItem] { ssaiConfiguration.pingParameters } - private var urlParameters: [URLQueryItem] { ssaiConfiguration.orderedPreplayParameters.map(URLQueryItem.init) } + private var preplayParameters: [URLQueryItem] { ssaiConfiguration.orderedPreplayParameters.map(URLQueryItem.init) } private var id: UplynkSSAIConfiguration.ID { ssaiConfiguration.id } var queryString: String { - let items = Self.versionQueryItem + drmParameters + pingParameters + urlParameters + let items = Self.versionQueryItem + drmParameters + pingParameters + preplayParameters var urlBuilder = URLComponents() urlBuilder.percentEncodedQueryItems = items.map(\.encodedForUplynk) return urlBuilder.percentEncodedQuery! From 4f4bf4507777477c71301f6db5b8e223523c1fb5 Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Wed, 1 Apr 2026 13:54:10 +0200 Subject: [PATCH 16/17] Add deprication log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e1b7f9..56279b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Uplynk - When ping feature is not configured, the player will now send an empty string instead of `"&ad.pingc=0"` to prevent unsigned parameters that could break signature validation. + - Deprecated the old `preplayParameters` in favor of the new `orderedPreplayParameters` ## [10.8.0.1] - 2026-01-20 From cee2e25f3cee7afdb687d62da44ee853755f375b Mon Sep 17 00:00:00 2001 From: Damiaan Dufaux Date: Wed, 1 Apr 2026 14:07:24 +0200 Subject: [PATCH 17/17] fix typo in liveURL --- .../Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift | 2 +- .../UplynkSSAIConfigurationURLBuilderTests.swift | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift b/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift index df43dc9f..8433cefb 100644 --- a/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift +++ b/Code/Uplynk/Source/Internal/UplynkSSAIURLBuilder.swift @@ -40,7 +40,7 @@ class UplynkSSAIURLBuilder { } func buildPreplayLiveURL() -> String { - return "\(prefix)/preplay/\(urlAssetType)/\(urlAssetID)?v=2\(queryString)" + return "\(prefix)/preplay/\(urlAssetType)/\(urlAssetID)?\(queryString)" } func buildAssetInfoURLs( diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 6be0194e..b7b0f018 100644 --- a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift +++ b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift @@ -249,6 +249,17 @@ final class UplynkSSAIConfigurationURLBuilderTests: XCTestCase { UplynkSSAIURLBuilder(ssaiConfiguration: drmConfig).buildPreplayVODURL(), "https://content.uplynk.com/preplay/a123.json?v=2&manifest=m3u8&rmt=fps&a=1&b=2" ) + + let liveDrmConfig = UplynkSSAIConfiguration( + id: assetID, + assetType: .channel, + orderedPreplayParameters: extraParams, + contentProtected: true + ) + XCTAssertEqual( + UplynkSSAIURLBuilder(ssaiConfiguration: liveDrmConfig).buildPreplayLiveURL(), + "https://content.uplynk.com/preplay/channel/a123.json?v=2&manifest=m3u8&rmt=fps&a=1&b=2" + ) } }