Change history
Every change to this page, newest first. Expand an entry to see what changed.
- 2 Oct 2026 · +55 −56 lines
Show changes
- // Wandura playback in an iOS app (Swift 5.9+, iOS 15+). Reviewed by hand: the docs CI runs `swiftc -parse` on this - // file when the runner has a Swift toolchain, and always checks that brackets balance. - // Guide: https://docs.wandura.ai/developers/player-sdk/ios - // - // The app never talks to Wandura and never holds an API key. It talks to YOUR backend, which applies your paywall - // and asks Wandura for tickets. Only `PlayerSDK` touches the Player SDK: fill it in with the calls named in the - // Player SDK guide (partner.wandura.ai → Developers → Player SDK). - import Foundation + # Player SDK on iOS - // #region sdk + > **TL;DR** — Install `TTSDKFramework` `1.52.100.4-standard` with CocoaPods (iOS 11+, Xcode 14+, Bitcode off), add the licence file Wandura sends you, start the SDK, and read the two device values after the licence check passes. Your app sends them to **your** backend, gets a ticket back, and gives the ticket to the SDK. On an SDK error, ask for **one** fresh ticket and resume. + + ## Prerequisites {#prerequisites} + + | | | + |---|---| + | Licence | The `.lic` file and App ID from Wandura, bound to your **final** bundle ID. See [Player SDK](index.md#licence). | + | Toolchain | Xcode 14 or later, iOS 11.0 or later, Bitcode disabled, CocoaPods (always open the `.xcworkspace`). | + | Devices | A real iPhone for encrypted playback. | + | Your backend | A ticket endpoint: [Playback flow and paywall](../guides/playback-flow.md). | + + The exact SDK calls (start-up, licence notification, device values, playback, subtitles) are in the Player SDK guide on partner.wandura.ai → **Developers** → **Player SDK**. + + ## The flow in your app {#flow} + + 1. **Start the SDK** at launch with your App ID and licence file, and wait for the licence result before anything else. + 2. **Read the device values** (device ID → `union_info`, device public key → `drm_kek`). Never send `nil`, `"(null)"` or an empty string: throw instead. + 3. **Ask your backend** to play the episode. Your backend applies your paywall and asks Wandura; your app never holds an API key. + 4. **Play**: give `vid` and `play_auth_token` to the SDK; add the subtitles. + 5. **Preload** the next episodes with the prefetched tickets your backend returns. + 6. **On any SDK error** (an expired ticket included): request **one** fresh ticket and resume at the same position. Do not branch on SDK error codes, and do not loop. + 7. **Back from the background**: if the current ticket has less than a minute left, replace it before resuming. + + ## Code {#code} + + The file below is the app side of that flow, in Swift. Only `PlayerSDK` touches the SDK: fill it in with the calls from the portal guide. Your backend's response shape matches the [Node and Python backend examples](../guides/playback-flow.md#code). + + ### The SDK behind one protocol {#code-sdk} + + ```swift struct DeviceValues { let unionInfo: String // device ID generated by the Player SDK ⋯ var onError: ((Error) -> Void)? { get set } } - // #endregion sdk - - // #region contract - struct Subtitle: Decodable { - let lang: String - let url: URL - let format: String - } - - struct Ticket: Decodable { - let vid: String - let playAuthToken: String // memory only: never log it, never write it to disk - let expiresAt: Date - let subtitles: [Subtitle] - - enum CodingKeys: String, CodingKey { - case vid, subtitles - case playAuthToken = "play_auth_token" - case expiresAt = "expires_at" - } - - /// Wandura's clock decides; allow a minute for a device clock that runs slow. - var isFresh: Bool { expiresAt.timeIntervalSinceNow > 60 } - } - - struct NextTicket: Decodable { - let episodeNo: Int - let ticket: Ticket - enum CodingKeys: String, CodingKey { case ticket, episodeNo = "episode_no" } - } - - enum Outcome: String, Decodable { - case locked, unavailable, busy, broken - case notReady = "not_ready" - } + ``` - /// The shape the example backends (Node, Python) return from YOUR `POST /v1/play`. - struct PlayResponse: Decodable { - let ok: Bool - let current: Ticket? - let next: [NextTicket]? - let outcome: Outcome? - } - // #endregion contract + ### Calling your backend {#code-backend} - // #region backend + ```swift final class Backend { private let baseURL: URL ⋯ } } - // #endregion backend + ``` - // #region controller + ### Start, prefetch, expiry, errors {#code-controller} + + ```swift enum ScreenState { case loading ⋯ } } - // #endregion controller + ``` + + ## Checklist {#checklist} + + - [ ] The licence result is checked before device values are read. + - [ ] Device values are never `nil`, `"(null)"` or empty when sent. + - [ ] The app holds no `wnd_live_` key (search the built `.ipa`). + - [ ] `play_auth_token` is kept in memory only: not in `UserDefaults`, logs or crash reports. + - [ ] An SDK error leads to one fresh ticket and a resume, never a loop. + - [ ] Resuming after more than 29 minutes in the background replaces the ticket first. + - [ ] An episode without subtitles plays without an error. + + ## Related {#related} + + - [Player SDK overview and licence](index.md) + - [Smooth playback under load](../guides/smooth-playback.md#in-the-app) + - [Subtitles](../guides/subtitles.md)
- 2 Oct 2026 · Page created