Playing protected content with Nagra DRM - iOS

Overview

Digital Rights Management (DRM) solutions often require specific data exchanges with a license server, usually in proprietary formats. Rather than integrating multiple license providers directly into the core of our player, we leverage flexible configuration options through the player configuration. This guide explains how to integrate the Bitmovin iOS Player SDK with Nagra Secure Services Platform (SSP) for DRM license acquisition and secure session management.

Pre-requisites

Before you begin, please ensure you have the following information and resources required for playing back Nagra-protected DRM streams:

  • HLS_MANIFEST_URL : URL of the HLS multivariant URL for the content to be played.
  • NAGRA_FAIRPLAY_CERTIFICATE_URL : Nagra Fairplay certificate URL.
  • NAGRA_FAIRPLAY_LICENSE_SERVER_URL : Nagra Fairplay license server endpoint for initial license acquisition.
  • NAGRA_CONTENT_TOKEN_URL : Nagra endpoint for fetching content token.
  • NAGRA_SSM_SETUP_URL : Nagra SSP endpoint for setting a secure session.
  • NAGRA_SSM_TEARDOWN_URL : Nagra SSP endpoint for secure session teardown.
  • NAGRA_CONTENT_ID : Nagra content Id specific for the content to be played.
  • BITMOVIN_PLAYER_KEY : You can get in touch with your Nagra or Bitmovin contact for this. You can also sign up for a trial account.

Configuration

1. Application preparation for DRM content playback and Secure Session Management

📘

Note:

The steps below describe interactions between your application and Nagra’s backend (SSP or OpenTV).

These are outside the scope of Bitmovin’s integration but are included here because they are required to obtain certain authorization tokens needed to successfully request DRM licenses from the Nagra backend.

Please refer to Nagra’s own documentation portal or contact your Nagra representatives for detailed information about these application-to-backend interactions.

  1. Authorization
    Your application signs in to a SIGNON_URL endpoint using SIGNON_USERNAME, SIGNON_PASSWORD and any required device/user-agent metadata. Upon successful sign-on, you obtain a login token.
  2. Content Token Retrieval
    Using the NAGRA_CONTENT_TOKEN_URL endpoint and your NAGRA_CONTENT_ID, your application requests a CONTENT_TOKEN from the Nagra backend.
  3. Secure Session Setup
    Your application initiates a secure session with the Nagra SSP backend by sending the CONTENT_TOKEN to NAGRA_SSM_SETUP_URL. Upon successful setup, you receive an SSM_TOKEN.
  4. Secure Session Teardown
    Once playback finishes or you switch to another piece of content, your application terminates the secure session by calling NAGRA_SSM_TEARDOWN_URL. If you plan to play a different piece of DRM-protected content, you must first repeat the Content Token Retrieval and Secure Session Setup steps to obtain a new content token and SSM_TOKEN.

2. Bitmovin Player Configuration for playback

Once the CONTENT_TOKEN as well as the SSM_TOKEN are fetched, they can be used with the other required properties to create a SourceConfig including FairplayConfig and load it into the player.

private let FAIRPLAY_CERTIFICATE_URL: String = "<NAGRA_FAIRPLAY_CERTIFICATE_URL>"
private let FAIRPLAY_LICENSE_SERVER_URL: String = "<NAGRA_FAIRPLAY_LICENSE_SERVER_URL>"
private let FAIRPLAY_LICENSE_RENEWAL_URL: String = "<NAGRA_FAIRPLAY_RENEWAL_SERVER_URL>"
private let HLS_MANIFEST_URL: String = "<HLS_MANIFEST_URL>"
private let CONTENT_TOKEN: String = "<fetch content token>"
private let SSM_TOKEN: String = "<fetch ssm token>"

func loadSource() {
  guard let hlsStreamUrl = URL(string: HLS_MANIFEST_URL),
  let certificateUrl = URL(string: FAIRPLAY_CERTIFICATE_URL),
  let licenseUrl = URL(string: FAIRPLAY_LICENSE_SERVER_URL) else {
  	fatalError("Invalid URL(s) when setting up DRM playback sample")
  }
	sourceConfig = SourceConfig(url: hlsStreamUrl, type: .hls)
	sourceConfig.drmConfig = createNagraFairplayConfig(licenseUrl: licenseUrl,
                                                   	 certificateUrl: certificateUrl)
	player.load(sourceConfig: sourceConfig)
}
    
func createNagraFairplayConfig(licenseUrl: URL, certificateUrl: URL) -> FairplayConfig  {
  var nagraFairplayConfig = FairplayConfig(license: licenseUrl, certificateURL: certificateUrl)
  nagraFairplayConfig.prepareContentId = { contentId in
    prepareContentId(content: contentId) ?? contentId
  }
  nagraFairplayConfig.prepareMessage = { data, _ in data }
  nagraFairplayConfig.prepareLicense = { data in
    prepareLicenseResponse(licenseData: data) ?? data
  }
  nagraFairplayConfig.licenseRequestHeaders = ["content-type": "application/octet-stream",
                                               "accept": "application/json",
                                               "nv-authorizations": "<CONTENT_TOKEN>" + "," + "<SSM_TOKEN>"]

  return nagraFairplayConfig
}

private func prepareContentId(content: String) -> String? {
  guard let base64Encoded = content.components(separatedBy: "skd://").last?.components(separatedBy: "?").first,
  let data = Data(base64Encoded: base64Encoded),
  let decodedString = String(data: data, encoding: .utf8) else {
    return nil
  }

  // Parse the decoded JSON string into a dictionary
  if let json = try? JSONSerialization.jsonObject(with: Data(decodedString.utf8), options: []) as? [String: Any],
  let contentId = json["ContentId"] as? String,
  let keyId = json["KeyId"] as? String {

    // Construct the final JSON string
    let result = """
            {"contentRef":"\(contentId)","KeyId":"\(keyId)"}
            """
    print("ION | prepareContentIds | ContentID: \(result)")
    return result
  }

  return nil
}

private func prepareLicenseResponse(licenseData: Data) -> Data? {
  print("ION | fairplayLicenseResponse | licenceData: \(licenseData)")
  do {
    // Deserialize the JSON response
    guard let drmObj = try JSONSerialization.jsonObject(with: licenseData, options: []) as? [String: Any] else {
      print("Error: License data is not in the expected format.")
      return nil
    }
    // Extract the CKC message
    guard let ckcMessage = drmObj["CkcMessage"] as? String else {
      print("Error: CKC Message key not found in DRM object.")
      return nil
    }
    // Decode the CKC message from base64
    guard let ckcData = Data(base64Encoded: ckcMessage, options: NSData.Base64DecodingOptions(rawValue: 0)) else {
      print("Error: Unable to convert CKC message to data.")
      return nil
    }
    return ckcData

  } catch {
    print("Error: In Parsing - \(error.localizedDescription)")
    return nil
  }
}

Note: the <STRING>represent placeholders for the actual values, which you should have as per the Prerequisites list.

Example of Nagra Fairplay Content Playback.

Below is a minimal, Swift code sample showing how to use the Bitmovin iOS Player SDK for playback of Nagra Fairplay protected content. The code sample is based on public Bitmovin sample BasicDRMPlayback. Replace all placeholder variables with valid values provided by Nagra or your internal configuration.

//
// Bitmovin Player iOS SDK
// Copyright (C) 2025, Bitmovin GmbH, All Rights Reserved
//
// This source code and its use and distribution, is subject to the terms
// and conditions of the applicable license agreement.
//

import BitmovinPlayer
import Combine
import SwiftUI

// You can find your player license key on the Bitmovin player license dashboard:
// https://bitmovin.com/dashboard/player/licenses
private let playerLicenseKey = "<BITMOVIN_PLAYER_KEY>"
// You can find your analytics license key on the Bitmovin analytics license dashboard:
// https://bitmovin.com/dashboard/analytics/licenses
private let analyticsLicenseKey = "<BITMOVIN_ANALYTICS_KEY>"

private let FAIRPLAY_CERTIFICATE_URL: String = "<NAGRA_FAIRPLAY_CERTIFICATE_URL>"
private let FAIRPLAY_LICENSE_SERVER_URL: String = "<NAGRA_FAIRPLAY_LICENSE_SERVER_URL>"
private let HLS_MANIFEST_URL: String = "<HLS_MANIFEST_URL>"
private let CONTENT_TOKEN: String = "<fetch content token>"
private let SSM_TOKEN: String = "<fetch ssm token>"

struct ContentView: View {
    private let player: Player
    private let playerViewConfig: PlayerViewConfig
    private let sourceConfig: SourceConfig
   
    init() {
        // Define needed resources
        guard let hlsStreamUrl = URL(string: HLS_MANIFEST_URL),
              let certificateUrl = URL(string: FAIRPLAY_CERTIFICATE_URL),
              let licenseUrl = URL(string: FAIRPLAY_LICENSE_SERVER_URL) else {
            fatalError("Invalid URL(s) when setting up DRM playback sample")
        }
        // Create player configuration
        let playerConfig = PlayerConfig()

        // Set your player license key on the player configuration
        playerConfig.key = playerLicenseKey

        // Create analytics configuration with your analytics license key
        let analyticsConfig = AnalyticsConfig(licenseKey: analyticsLicenseKey)

        // Create player based on player and analytics configurations
        player = PlayerFactory.createPlayer(
            playerConfig: playerConfig,
            analytics: .enabled(
                analyticsConfig: analyticsConfig
            )
        )

        // Create player view configuration
        playerViewConfig = PlayerViewConfig()

        // See documentation for more details.
        sourceConfig = SourceConfig(url: hlsStreamUrl, type: .hls)
        sourceConfig.drmConfig = createNagraFairplayConfig(licenseUrl: licenseUrl,
                                                           certificateUrl: certificateUrl)
    }
    
    func createNagraFairplayConfig(licenseUrl: URL, certificateUrl: URL) -> FairplayConfig  {
        var nagraFairplayConfig = FairplayConfig(license: licenseUrl, certificateURL: certificateUrl)
        nagraFairplayConfig.prepareContentId = { contentId in
            prepareContentIds(content: contentId) ?? contentId
        }
        nagraFairplayConfig.prepareMessage = { data, _ in data }
        nagraFairplayConfig.prepareLicense = { data in
            prepareLicenseResponse(licenseData: data) ?? data
        }
        nagraFairplayConfig.licenseRequestHeaders = ["content-type": "application/octet-stream",
                                                     "accept": "application/json",
                                                     "nv-authorizations": "<CONTENT_TOKEN>" + "," + "<SSM_TOKEN>"]
        
        return nagraFairplayConfig
    }
    
    /// Prepares content IDs by decoding and parsing the provided content string in Nagra Fairplay signalization.
    ///
    /// This method takes the content string, extracts and decodes the base64-encoded section,
    /// parses it as JSON, and extracts relevant identifiers (`ContentId` and `KeyId`).
    /// The extracted values are then used to construct a new JSON string.
    ///
    /// - Parameter content: The input string containing a base64-encoded section prefixed by "skd://".
    /// - Returns: A JSON string with `contentRef` and `KeyId` if parsing is successful; otherwise, `nil`.
    ///
    /// - Example Input:
    ///   "skd://eyJDb250ZW50SWQiOiIxMjM0NTY3ODkiLCJLZXlJZCI6Ijg3NjU0MzIxMCJ9"
    /// - Example Output:
    ///   "{\"contentRef\":\"123456789\",\"KeyId\":\"876543210\"}"
    private func prepareContentIds(content: String) -> String? {
        guard let base64Encoded = content.components(separatedBy: "skd://").last?.components(separatedBy: "?").first,
              let data = Data(base64Encoded: base64Encoded),
              let decodedString = String(data: data, encoding: .utf8) else {
            return nil
        }
      
        // Parse the decoded JSON string into a dictionary
        if let json = try? JSONSerialization.jsonObject(with: Data(decodedString.utf8), options: []) as? [String: Any],
           let contentId = json["ContentId"] as? String,
           let keyId = json["KeyId"] as? String {
            
            // Construct the final JSON string
            let result = """
            {"contentRef":"\(contentId)","KeyId":"\(keyId)"}
            """
            print("ION | prepareContentIds | ContentID: \(result)")
            return result
        }
        
        return nil
    }

    /// Processes the Nagra FairPlay license response.
    ///
    /// This method decodes a DRM response in JSON format to extract the CKC message,
    /// which is a base64-encoded string. The CKC message is then decoded into binary data
    /// for further use in the FairPlay content decryption process.
    ///
    /// - Parameter licenseData: A `Data` object containing the DRM license response in JSON format.
    /// - Returns: The decoded CKC message as `Data` if successful; otherwise, `nil`.
    ///
    /// - Example DRM License Response (Input):
    ///   {
    ///     "CkcMessage": "BASE64_ENCODED_STRING"
    ///   }
    /// - Example Output:
    ///   Decoded `Data` object containing the CKC message.
    private func prepareLicenseResponse(licenseData: Data) -> Data? {
        do {
            // Deserialize the JSON response
            guard let drmObj = try JSONSerialization.jsonObject(with: licenseData, options: []) as? [String: Any] else {
                print("Error: License data is not in the expected format.")
                return nil
            }
            // Extract the CKC message
            guard let ckcMessage = drmObj["CkcMessage"] as? String else {
                print("Error: CKC Message key not found in DRM object.")
                return nil
            }
            // Decode the CKC message from base64
            guard let ckcData = Data(base64Encoded: ckcMessage, options: NSData.Base64DecodingOptions(rawValue: 0)) else {
                print("Error: Unable to convert CKC message to data.")
                return nil
            }
            return ckcData
          
        } catch {
            print("Error: In Parsing - \(error.localizedDescription)")
            return nil
        }
    }

    var body: some View {
        ZStack {
            Color.black

            VideoPlayerView(
                player: player,
                playerViewConfig: playerViewConfig
            )
            .onReceive(player.events.on(PlayerEvent.self)) { (event: PlayerEvent) in
                dump(event, name: "[Player Event]", maxDepth: 1)
            }
            .onReceive(player.events.on(SourceEvent.self)) { (event: SourceEvent) in
                dump(event, name: "[Source Event]", maxDepth: 1)
            }
        }
        .padding()
        .onAppear {
            player.load(sourceConfig: sourceConfig)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And that's it! If you have any issues implementing this please reach out to your Bitmovin and Nagra representatives.

For similar examples across other platforms also using Nagra security please see the links below:

Playing protected content with Nagra DRM - Web

Playing protected content with Nagra DRM - Android

Playing protected content with Nagra CONNECT - Android STB