CONNECT Player SDK 5 for Apple (FPS)

Player lifecycle

To create your project and initialize the player instance, see  Creating the player.

Creating a player

There are several ways to create a player instance. You can:

  • Create a player from a URL.

  • Create a player from a player item initialized from a URL.

  • Creating a player from a player item initialized from an OTVAVURLAsset.

  • Create a player and set the player item

Click here to see the example code.
let assetURL = URL(string: "https://d3bqrzf9w11pn3.cloudfront.net/basic_hls_bbb_clear/index.m3u8")!
let player = OTVAVPlayer(url: assetURL)
let assetURL = URL(string: "https://d3bqrzf9w11pn3.cloudfront.net/basic_hls_bbb_clear/index.m3u8")!
let playerItem = OTVAVPlayerItem(url: assetURL)
let player = OTVAVPlayer(playerItem: playerItem)
let assetURL = URL(string: "https://d3bqrzf9w11pn3.cloudfront.net/basic_hls_bbb_clear/index.m3u8")!
let asset = OTVAVURLAsset(url: assetURL)
let playerItem = OTVAVPlayerItem(asset: asset)
let player = OTVAVPlayer(playerItem: playerItem)
let assetURL = URL(string: "https://d3bqrzf9w11pn3.cloudfront.net/basic_hls_bbb_clear/index.m3u8")!
let asset = OTVAVURLAsset(url: assetURL)
let playerItem = OTVAVPlayerItem(asset: asset)
let player = OTVAVPlayer()
player.replaceCurrentItem(with: playerItem)

Attaching a player to  UIView

Before playing a stream, the player must be attached to a UIView to get the audio and video rendered.

Click here to see the example code.
class PlayerView: UIView {
  var player: AVPlayer? {
    get { return playerLayer.player }
    set { playerLayer.player = newValue }
  }
  var playerLayer: AVPlayerLayer {
    return layer as! AVPlayerLayer
  }
  override class var layerClass: AnyClass {
    return AVPlayerLayer.self
  }
}
@IBOutlet weak var playerView: PlayerView!
playerView.player = player

The following code can be added to indicate how the layer displays the video content within its bounds.

playerView.playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill

For more information, see the Apple document AVFoundation/AVLayerVideoGravity.

AVLayerVideoGravity

Descriptions

resizeAspect

The player should preserve the video’s aspect ratio and fit the video within the layer’s bounds.

resizeAspectFill

The player should preserve the video’s aspect ratio and fill the layer’s bounds.

resize

The video should be stretched to fill the layer’s bounds.

Playing a stream

Play a stream that has been set by calling:

player.play() /* https://developer.apple.com/documentation/avfoundation/avplayer/1386726-play */

or

player.rate = 1.0 /* https://developer.apple.com/documentation/avfoundation/avplayer/1388846-rate */

or

player.playImmediately(atRate: 1.0) /* https://developer.apple.com/documentation/avfoundation/avplayer/1643480-playimmediately */

Pausing a playback

Pause the playback by calling:

player.pause() /* https://developer.apple.com/documentation/avfoundation/avplayer/1387895-pause */

or

player.rate = 0.0 /* https://developer.apple.com/documentation/avfoundation/avplayer/1388846-rate */

Seeking through a playback stream

You can seek through a playback stream.

Click here to see the example code.
// Sets the current playback time to the specified time and executes the specified block when the seek operation completes or is interrupted.
func seek(to: CMTime, completionHandler: ((Bool) -> Void)?) /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1387418-seek */

//Sets the current playback time within a specified time bound and invokes the specified block when the seek operation completes or is interrupted.
func seek(to: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime, completionHandler: ((Bool) -> Void)?) /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1387753-seek */

// Sets the current playback time to the time specified by the date object.
func seek(to: Date, completionHandler: ((Bool) -> Void)?) -> Bool /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1389877-seek */

// Cancels any pending seek requests and invokes the corresponding completion handlers if present.
func cancelPendingSeeks() /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1388316-cancelpendingseeks */
// Returns the current time of the item.
func currentTime() -> CMTime /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1387230-currenttime */

// Returns the current time of the item as an NSDate object.
func currentDate() -> Date? /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1386188-currentdate */

// The duration of the item.
var duration: CMTime /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1389386-duration */

// The timebase information for the item.
var timebase: CMTimebase? /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1387605-timebase */
// An array of time ranges indicating media data that is readily available.
var loadedTimeRanges: [NSValue] /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1389953-loadedtimeranges */

// An array of time ranges within which it is possible to seek.
var seekableTimeRanges: [NSValue] /* https://developer.apple.com/documentation/avfoundation/avplayeritem/1386155-seekabletimeranges */



VOD

LIVE

currentTime() -> CMTime

The time of the current playback position. The value starts from 0 to the duration.

The time of the current playback position. The value starts from the time near the end of the seekable window (three segments before the last one of the manifest) and keeps increasing. It should drop in the seekable range window.

currentDate() -> Date?

The date and time of the current playback position. The value of the program date time from the manifest.
E.g.  #EXT-X-PROGRAM-DATE-TIME:2022-02-19T14:54:23.031+08:00

The date and time of the current playback position. The value of the program date time from the manifest.
E.g.  #EXT-X-PROGRAM-DATE-TIME:2022-02-19T14:54:23.031+08:00

duration: CMTime

The duration of the content.

The duration of all the segments in the manifest.

seekableTimeRanges: [NSValue]

The time range of the static seekable window, [0, duration].

The time range of the dynamic seekable window, [startTime, startTime + duration ]. The startTime is the current time of the first segment in the manifest, and the value keeps updating.

For more information, see the Apple document Seeking Through Media.

Observing the playback time

You can use the following code to observe the currentTime change and handle the UI about the time update in the closure.

// You need to retain the instance timeObserverToken to make sure the closure can be called.
timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) {
	[weak self] time in
    // update player UI for time update
}
// You also need to remove the observer when it's not used:
player.removeTimeObserver(timeObserverToken)
timeObserverToken = nil

For more information, see the Apple document Observing the Playback Time.

Changing streams

The playback stream can be changed by calling:

let playerItem = OTVAVPlayerItem(url: assetURL)
player.replaceCurrentItem(with: playerItem)

Stopping playback

Playback can be stopped by calling:

player.replaceCurrentItem(with: nil)

Observing player errors using the OTVAVFoundationErrors notification

When OTVAVPlayer and OTVAVPlayerItem encounter an error when initiating or playing content it produces a notification using the "OTVAVFoundationErrors" notification. 

/// Notification when OTVAVPlayer or OTVAVPlayerItem encounters an error.
/// which gives the statusCode, domain and message for the error encounted. 
public let OTVAVFoundationErrors = Notification.Name("otvAVFoundationErrors")

There are two types of of OTVAVFoundationErrors which are encountered, OTVAVPlayer and OTVAVPlayerItem errors

/**
 `OTVAVFoundationError` is an enum that represent they type of OTVAfoundation type either, OTVAVPlayer or OTVAVPlayerItem
 */
public enum OTVAVFoundationError: Int {
  // AVPlayerError
  case otvAVPlayer = 1101
  // AVPlayerItemError
  case otvAVPlayerItem = 1102
}

Listening to OTVAVFoundationErrors can be done by first registering to listen to the notification 

NotificationCenter.default.addObserver(self, selector: #selector(opyPlayerErrorsReturned), name: OTVAVFoundationErrors, object: nil)

The user must then filter on the type of OTVAVFoundationError returned, which as mentioned above is either 'otvAVPlayer' or 'otvAVPlayerItem' and then using the information returned from the userInfo object of the notification. The userInfo object is a dictionary containing 3 keys, statusCode, domain and message. 

@objc func opyPlayerErrorsReturned(_ notification: NSNotification) {
  if let error = notification.object as? OTVAVFoundationError, let userInfo = notification.userInfo {
      let statusCode = userInfo["statusCode"]
      let domain = userInfo["domain"]
     let messsage = userInfo["message"]
      switch error {
            case .otvAVPlayer:
                  print("OTVAVPlayer errror:: Status code = ", statusCode, " domain = ", domain, " message = ", messsage)
            case .otvAVPlayerItem:
                  print("OTVAVPlayerItem errror:: Status code = ", statusCode, " domain = ", domain, " message = ", messsage)
            @unknown default:
                    NSLog("Unexpected OPY error: \(error)")
            }
      }
}

In the case of otvAVPlayerItem errors, the errors returned are that of AVPlayerItem error codes returned during playback when the item fails to play or the item status changes to failed state. 

Observing player errors using the AVPlayerItemNewErrorLogEntry notification

To listen to AVPlayerItemNewErrorLogEntry for the content being played is by creating a notification observer on 'AVPlayerItemNewErrorLogEntry' using the OTVAVPlayerItem of the content being played.

NotificationCenter.default.addObserver(self, selector: #selector(handleAVPlayerItemErrorLog),
                                           name: NSNotification.Name.AVPlayerItemNewErrorLogEntry,
                                           object: self.playerItem)

This notification is triggered every time the player item encounters a media error of any kind. You can filter on last error log event using the code below

 private func latestPlayerItemErrorLogEvent() -> AVPlayerItemErrorLogEvent? {
    guard let item = playerItem, let errorLog = item.errorLog() else {
      return nil
    }
    return errorLog.events.last
  }

An example of how to handle the information returned from the 'AVPlayerItemNewErrorLogEntry' notification. All errors returned through this notification are from AVFoundation only and they do not contain any OPYPlayback errors.

  @objc func handleAVPlayerItemErrorLog(notification: Notification) {
    if let statusCode = latestPlayerItemErrorLogEvent()?.errorStatusCode, let message = latestPlayerItemErrorLogEvent()?.errorComment,  let domain = latestPlayerItemErrorLogEvent()?.errorDomain {
      print("The status code is: ", statusCode, " the error domain is: ", domain, " the error message is: ", message)
    }
  }