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 diff --git a/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift b/Code/Uplynk/Source/Internal/UplynkSSAIConfiguration+Extensions.swift index fab531df..aa80ca3a 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,9 +44,9 @@ 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)" + return "&ad.cping=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 diff --git a/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift b/Code/Uplynk/Tests/UplynkSSAIConfigurationURLBuilderTests.swift index 542b24e2..8c1d4277 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 = "" + let validPingQueryParameter = "ad.cping=1&ad.pingf=\(pingFeature.rawValue)" let pingConfiguration = switch pingFeature { case .noPing: 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" ],