Playback

In the Playback section we list the common features shared by all player engines (PlayerEngine.apple)

Player states

The following are the possible player states:

  • Idle The player is in .idle state when no content is loaded or being loaded and no additional resources are open. No data is loaded in buffers and no decoder instances are open. This is the initial state of the Player.

  • Buffering: The player does not have enough data to keep in playing and need to fill its buffers further before playback can continue

  • Ended: The final state of the player is reached when the playhead moves to the end of the available content either by normal playback or via a seek operation that seeks at or beyond the end of the content. The player will stay in the .ended state until it is released or the user seeks back into the content timeline. In addition to releasing, the player can also transition to opening here. This allows to load new content that re-uses existing resources such as decoder and DRM instances.

  • Error: The player ends up in the .error state after a fatal error occurred. The player cannot get out of the Error state itself and needs to be released.

  • Opening: The player is in opening state when it started loading content. It will stay in this state until it has enough metadata loaded to build up a track model and transition to .ready.

  • Paused: Playback is possible but paused.

  • Pausing: The player is trying to pause playback.

  • Play: The player tries to start playback. This operation might fail, for instance because if auto-play not being permitted, in which case the player transitions to .pausing. Usually the player transitions out to .playing and playback starts.

  • Playing: The playhead is moving forward and the content is playing

  • Ready: The player reached the .ready state when enough data was loaded to build a track model. At this point how the player proceeds depends on the configuration. The player will in the .ready state until it is transitioned out of the state and loading of data is triggered. This transition can happen automatically or manually depending on the configuration.

  • Seeking: A position change was requested. The player can either reach the new position in its buffer and will transition back to .play or .pausing depending on where it came from or it will first transition into .buffering if the requested position is not found in the current buffer.

  • Stopping: The player is in the process of releasing all its resources (decoders, buffers etc).

The state graph highlights some of the actions that trigger certain transitions. This covers transitions in and out of .idle state and what happens on errors.

Basic playback

An example project of basic HLS playback is provided as part of the PRESTOplay SDK. The example is located in the Examples folder and can be opened with Xcode.

import PRESTOplay
import CastlabsApple

// Initialize the SDK and register the plugins
_ = PRESTOplaySDK.shared.setup("LICENSE", [HLSPlugin()])

// Create the player
var player = PRESTOplaySDK.shared.player()

guard let contentURL = URL(string: "https://example.com/master.m3u8")
else { return }

// Configure the player
let config = PlayerConfiguration(with: contentURL)
player.load(config: config)

player.onState = { previous, state in
  // Handle player state changes
  switch state {
  case .ready:
    print("Player ready")
  // handle other states ...
  }

  if let error = state.playerError {
    print("Error \(error)")
  }
}

// Attach the player to the view
player.attach(to: view.layer)

// Start playback
player.open(autoplay: true)

Local content playback

Local/offline assets can be played by using a file URL in PlayerConfiguration.

let localUrl = URL(fileURLWithPath: "/path/to/local/playlist.m3u8")
let config = PlayerConfiguration(with: localUrl)
player.open(config: config)

If the local content was created by DownloaderAPI, PlayerConfiguration(with:) automatically restores related offline DRM fields when available.

Background playback

You can keep audio playback active while the app is backgrounded:

player.continueAudioPlaybackInBackground = true
player.pausePlaybackOnReturnToForeground = false

Set this before opening content, and ensure your app has UIBackgroundModes with audio in Info.plist.

Playback rate range

The default playback rate range varies depending on the player engine. For the .apple player, the range is from -1.0 to 2.0. For the .castlabs player, the minimum rate is 0.0, and the maximum rate is undefined. If you needed, you can use the API below to modify the range.

PRESTOplaySDK.shared.setPlaybackRateRange(of: .apple, to: 0.0...2.0)
PRESTOplaySDK.shared.setPlaybackRateRange(of: .castlabs, to: 0.0...2.0)

Playback rate and seek

Playback speed is controlled through PlayerAPI.playbackRate. Set values above 1.0 to speed up playback.

player.playbackRate = 1.5

Behavior notes:

  • playbackRate = 1.0 is normal playback.
  • Rates below 1.0 slow playback.
  • Negative rates are supported only with PlayerEngine.apple.
  • Audio is muted when playbackRate < 0.0 or playbackRate > 2.0.
  • High rates can produce frame drops.

The playback-rate range can be configured globally per engine:

PRESTOplaySDK.shared.setPlaybackRateRange(of: .apple, to: -1.0...2.0)
PRESTOplaySDK.shared.setPlaybackRateRange(of: .castlabs, to: 0.0...2.0)

Seek operations

Use seek(_ time: Double) to seek to a position in seconds:

player.seek(120.0)

seek(_ time: CMTime) is deprecated.

During seek, the player transitions through .seeking, then returns to playback states (.play/.playing) or buffers if needed.

Seekable range integration

Use seekableTimeRanges and onSeekableTimeRanges to keep scrubbing controls inside valid timeline ranges, especially for live streams:

player.onSeekableTimeRanges = { ranges in
    guard let range = ranges.last else { return }
    let start = range.start.seconds
    let end = range.end.seconds
    print("Seekable window: \(start) - \(end)")
}

Recommended flow for playback-rate controls:

  1. Set playbackRate for fast-forward/rewind behavior.
  2. Use seek(_:) for explicit timeline jumps.
  3. Clamp target seek positions to seekableTimeRanges.

Video gravity

Video gravity determines how the video content is scaled or stretched within the player bounds.

The player layer supports the following video gravity values:

  • AVLayerVideoGravity.resizeAspect player should preserve the video’s aspect ratio and fit the video within the bounds,

  • AVLayerVideoGravity.resizeAspectFill player should preserve the video’s aspect ratio and fill the bounds,

  • AVLayerVideoGravity.resize video should be stretched to fill the bounds

Phone call

By default the player is paused when a phone call has started and resumes playback when a phone call ends.

Handling Media Services Reset

On iOS and tvOS, a system Media Services Reset can invalidate the active media stack. If you want the SDK to recreate the current player automatically after such a reset, enable the SDK-level flag before opening content:

PRESTOplaySDK.shared.recreatePlayerOnMediaServicesReset = true

This flag is disabled by default.

PlayerAPI also exposes an onMediaServicesReset callback. Use it to restore app-level audio state after the reset. The SDK user is responsible for re-enabling AVAudioSession in this case.

The SDK also reports a .system_media_services_reset PRESTOerror with warning severity through PRESTOplaySDK.shared.onError. This warning exists so applications can implement custom handling around the reset event, for example logging, analytics, UI coordination, or a custom recovery flow in addition to the automatic player recreation controlled by PRESTOplaySDK.shared.recreatePlayerOnMediaServicesReset.

Example from PlayerViewController.swift:

player.onMediaServicesReset = { [weak self] in
  guard let self else { return }

  let audioSession = AVAudioSession.sharedInstance()
  do {
    try audioSession.setCategory(.playback)
    try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
  } catch {
    log_e("Cannot initialize audio session")
  }
}

Set the callback after creating the player and before opening content so your app can restore its AVAudioSession configuration as soon as the reset is reported.

Player statistics

The Player API offers two main methods for accessing statistics data: the onStats callback for real-time monitoring and the getStats method to fetch the latest stats programmatically. If stats are not available, nil is returned.

import PRESTOplay
import CastlabsApple

// Initialize the SDK and register the plugins
_ = PRESTOplaySDK.shared.setup("LICENSE", [HLSPlugin()])

// Create the player
var player = PRESTOplaySDK.shared.player()

guard let contentURL = URL(string: "https://example.com/master.m3u8")
else { return }

// Configure the player
let config = PlayerConfiguration(with: contentURL)
player.load(config: config)

// ...

// Observe stats
player.onStats = { stats in
  print("Stats: \(stats)")
}

// Get the latest reported stats
let stats = player.getStats()
print("Stats: \(stats)")

// ...

Live playback

Behind live window warning

If the player’s position falls behind the live window, a non-fatal error (player_behind_live_window) is reported. AVPlayer will typically recover automatically.

The detection is based on the following condition: position < max(0.0, seekWindow.start - tolerance)

The tolerance value compensates for timing drift between seekable time range updates and the player’s current position. By default the tolerance is set to 3 seconds. You can adjust the tolerance value using the following API:

// ...
let config = PlayerConfiguration(with: contentURL)
config.liveConfiguration = LiveConfiguration(behindLiveWindowTolerance: 5.0)
player.load(config: config)
// ...

Live-edge latency and startup optimization

You can tune live behavior with additional LiveConfiguration values:

let config = PlayerConfiguration(with: contentURL)
config.liveConfiguration = LiveConfiguration(
    liveEdgeLatencyMs: 3000,
    optimizeLiveStartup: true,
    behindLiveWindowTolerance: 3.0
)
player.open(config: config)
  • liveEdgeLatencyMs: Target playback latency behind live edge.
  • optimizeLiveStartup: Enables faster live startup behavior.

Live-edge catch-up (castlabs engine)

For PlayerEngine.castlabs, the player exposes additional live-edge controls through PlayerAPI. These settings let you define the target distance from live edge and how aggressively the player catches up when latency grows.

let config = PlayerConfiguration(with: contentURL)
config.preferredEngine = .castlabs

let player = PRESTOplaySDK.shared.player()

// Target startup/live position behind edge (seconds)
player.liveEdgeDelay = 10.0

// Catch-up strategy: .disabled, .skipFrames, or .speedupPlayback
player.chaseLiveEdge = .speedupPlayback

// Catch-up window tuning (seconds)
player.chaseLiveEdgeCatchupThreshold = 10.0
player.chaseLiveEdgeCutoffThreshold = 5.0

// Strategy-specific knobs
player.chaseLiveEdgeHLSSegmentDuration = 10.0
player.chaseLiveEdgeSpeedupRatio = 1.1

player.open(config: config)

Behavior summary:

  • liveEdgeDelay: Initial target behind live edge.
  • chaseLiveEdge: Catch-up mode (.disabled, .skipFrames, .speedupPlayback).
  • chaseLiveEdgeCatchupThreshold: Maximum tolerated distance behind live edge before catch-up engages.
  • chaseLiveEdgeCutoffThreshold: Nominal distance behind live edge to stabilize around.
  • chaseLiveEdgeHLSSegmentDuration: Segment-duration hint used by .skipFrames.
  • chaseLiveEdgeSpeedupRatio: Temporary playback-rate increase used by .speedupPlayback.

Buffer settings for live playback

Live playback behavior can also be influenced by buffer settings.

let config = PlayerConfiguration(with: contentURL)
config.preferredEngine = .castlabs

let player = PRESTOplaySDK.shared.player()
player.minPrebufferTime = 6.0
player.maxPrebufferTime = 60.0
player.minRebufferTime = 12.0

player.open(config: config)

Common Media Client Data (CMCD)

The SDK clients can transmit valuable information to Content Delivery Networks (CDNs) with each object request. Transmitting that data can improve QoS monitoring, adaptive traffic optimization, and delivery performance, ultimately enhancing the consumer experience. Specification - CTA-5004.

CMCD Support can only be enabled for adaptive streaming formats, DASH, HLS and SmoothStreaming.

CMCD Data Keys:

  1. CMCD-Request: keys whose values vary with each request.
  2. CMCD-Object: keys whose values vary with the object being requested.
  3. CMCD-Status: keys whose values do not vary with every request or object.
  4. CMCD-Session: keys whose values are expected to be invariant over the life of the session.

CMCD Data is transmitted as HTTP Request headers.

To Enable CMCD, an instance of CmcdConfiguration needs to be created and passed to the PlayerConfiguration which is used for building the player.

Network configuration and connectivity handling

The SDK supports two ways to set NetworkConfiguration.

Player-level configuration

Set networkConfiguration on PlayerConfiguration when you want the configuration to be tied to a specific player/content setup:

var config = PlayerConfiguration(with: contentURL)
config.networkConfiguration = NetworkConfiguration(
    httpAdditionalHeaders: [
        "User-Agent": "MyApp/1.0",
        "X-Custom-Header": "value",
    ],
    allowsCellularAccess: true,
    timeoutIntervalForResourceMs: 8000,
    timeoutIntervalForDrmAcquisitionMs: 10000,
    waitsForConnectivity: true
)

player.open(config: config)

SDK-level configuration

Set PRESTOplaySDK.shared.networkConfiguration when you want all network requests handled by the SDK to use the same global configuration:

let networkConfiguration = NetworkConfiguration()
networkConfiguration.timeoutIntervalForResourceMs = 8000
networkConfiguration.timeoutIntervalForDrmAcquisitionMs = 10000
networkConfiguration.waitsForConnectivity = true

PRESTOplaySDK.shared.networkConfiguration = networkConfiguration

waitsForConnectivity = true allows network requests configured through these public APIs to wait for connectivity instead of failing immediately.

In practice, this is useful for DRM/license-related requests. It does not change native media-transport behavior in HLSPlayer.

Connectivity-loss detection

The SDK exposes internet reachability changes through PRESTOplaySDK.shared.onError. Handle these warning events:

  • .system_internet_connection_unavailable
  • .system_internet_connection_available
PRESTOplaySDK.shared.onError = { _, error in
    switch error.type {
    case .system_internet_connection_unavailable:
        // Network became unavailable.
        // Pause UI actions, show offline state, or queue retries.
        break
    case .system_internet_connection_available:
        // Network is available again.
        // Resume pending operations if needed.
        break
    default:
        break
    }
}

State timeouts

State timeouts allow the player to automatically report a recoverable error when it remains stuck in a particular playback state longer than a configured duration. This is useful for detecting stalls or hangs that the player would not otherwise recover from on its own.

Timeouts are configured via StateTimeoutConfiguration and attached to a PlayerConfiguration before loading content.

Default values

State Default timeout (s) Notes
Idle 0 Disabled by default
Opening 15
Ready 10
Buffering 30
Play 10
Pausing 10
Stopping 10
Seeking 20
Ended 0 Disabled by default
Error 0 Disabled by default

Set any value to 0 to disable the timeout for that state.

Usage

import PRESTOplay
import CastlabsApple

_ = PRESTOplaySDK.shared.setup("LICENSE", [HLSPlugin()])

var player = PRESTOplaySDK.shared.player()

guard let contentURL = URL(string: "https://example.com/master.m3u8")
else { return }

var config = PlayerConfiguration(with: contentURL)

// Configure per-state timeouts (all values in seconds; 0 = disabled)
config.stateTimeoutConfiguration = StateTimeoutConfiguration(
    openingSeconds: 15,
    bufferingSeconds: 30,
    seekingSeconds: 20
)

player.load(config: config)
player.open(autoplay: true)

When a timeout fires it is reported as a recoverable error through PRESTOplaySDK.shared.onError (and may also appear in the error log). It does not transition the player to the .error state and is not exposed via state.playerError:

PRESTOplaySDK.shared.onError = { error in
    // Handle recoverable timeout error
    print("Playback error: \(error)")
}

Passing nil to disable all timeouts

Passing nil for stateTimeoutConfiguration disables all timeouts entirely:

config.stateTimeoutConfiguration = nil